Browse Source

706 refatorar model autor tipoautor (#739)

* Ref Autor, TipoAutor, cria app api DRF

- 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<pk>[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

* Ajusta front para busca por possiveis autores

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

* Conc refatoração no Cada de Autor e Tipos de Autor

* Alt backend de perm e pag de drf e ref layout topo

* Add procedimento na alteração de username de Autor

Na edição de Autores foi adicionado o tratamento por opção do usuário do
que deve ser feito com o usuário que está sendo desvinculado no caso de
uma alteração do username de um Autor.

Foram dadas três opções:

1) Apenas retirar Perfil de Autor do Usuário que está sendo desvinculado
2) Retirar Perfil de Autor e desativar Usuário que está sendo desvinculado
3) Excluir Usuário

* Add field cargo em AutorForm para tipos sem CT

Cadastro de Autores de Tipos sem ContentType podem adicionar nome, cargo
e usuário.

* Add Bloco, Bancada, Frente possíveis Tip de Autores

* Corrige frase de mensagem no cadastro de Autor

* ref buscador modal de Autores c pesq param reversa

* Add documentação e faz modificações na api/autor

* Ref Crud para Listar GenericRelations

* Ref List da aba Proposições para parlamentares

* Altera imports de teste na app materia

* Corrige comentários da classe AutorListView

* Customiza layout do drf docs.

* Altera criação do grp Autor para inc list e detail

* Remove customização do bootstrap

Após fork e ajustes feitos no tema drunken-parrot-ui-flat, alterações
feitas nos arquivos deste commit não são mais necessárias, passando a
ser renderizado os arquivos que são padrão da biblioteca
django-crispy-forms.

* Adequa inserção dinamica de radio ao crispy-forms
pull/752/head
Leandro Roberto da Silva 8 years ago
committed by Edward
parent
commit
d288872830
  1. 1
      requirements/requirements.txt
  2. 1
      sapl/api/__init__.py
  3. 0
      sapl/api/admin.py
  4. 8
      sapl/api/apps.py
  5. 57
      sapl/api/forms.py
  6. 0
      sapl/api/migrations/__init__.py
  7. 34
      sapl/api/pagination.py
  8. 17
      sapl/api/permissions.py
  9. 43
      sapl/api/serializers.py
  10. 30
      sapl/api/urls.py
  11. 169
      sapl/api/views.py
  12. 414
      sapl/base/forms.py
  13. 55
      sapl/base/migrations/0022_auto_20161009_1222.py
  14. 25
      sapl/base/migrations/0023_auto_20161009_1852.py
  15. 26
      sapl/base/migrations/0024_auto_20161010_1002.py
  16. 20
      sapl/base/migrations/0025_tipoautor_cria_usuario.py
  17. 19
      sapl/base/migrations/0026_remove_tipoautor_cria_usuario.py
  18. 22
      sapl/base/migrations/0027_auto_20161011_1624.py
  19. 63
      sapl/base/models.py
  20. 21
      sapl/base/urls.py
  21. 122
      sapl/base/views.py
  22. 12
      sapl/comissoes/models.py
  23. 10
      sapl/compilacao/views.py
  24. 5
      sapl/crispy_layout_mixin.py
  25. 17
      sapl/crud/base.py
  26. 133
      sapl/materia/forms.py
  27. 53
      sapl/materia/migrations/0054_auto_20161009_1222.py
  28. 32
      sapl/materia/migrations/0055_auto_20161009_1418.py
  29. 16
      sapl/materia/migrations/0056_merge.py
  30. 87
      sapl/materia/models.py
  31. 7
      sapl/materia/tests/test_materia.py
  32. 16
      sapl/materia/urls.py
  33. 145
      sapl/materia/views.py
  34. 27
      sapl/parlamentares/models.py
  35. 3
      sapl/parlamentares/views.py
  36. 7
      sapl/protocoloadm/forms.py
  37. 26
      sapl/protocoloadm/migrations/0003_auto_20161009_1222.py
  38. 3
      sapl/protocoloadm/models.py
  39. 8
      sapl/protocoloadm/urls.py
  40. 15
      sapl/protocoloadm/views.py
  41. 12
      sapl/relatorios/views.py
  42. 7
      sapl/sessao/forms.py
  43. 25
      sapl/sessao/models.py
  44. 8
      sapl/sessao/urls.py
  45. 73
      sapl/settings.py
  46. 35
      sapl/static/js/app.js
  47. 1
      sapl/static/styles/app.scss
  48. 4
      sapl/templates/base.html
  49. 146
      sapl/templates/base/autor_form.html
  50. 13
      sapl/templates/base/layouts.yaml
  51. 20
      sapl/templates/base/tipoautor_form.html
  52. 17
      sapl/templates/materia/layouts.yaml
  53. 81
      sapl/templates/rest_framework_docs/base.html
  54. 123
      sapl/templates/rest_framework_docs/home.html
  55. 7
      sapl/templates/sistema.html
  56. 6
      sapl/test_urls.py
  57. 3
      sapl/urls.py
  58. 95
      sapl/utils.py
  59. 28
      scripts/inicializa_grupos_autorizacoes.py

1
requirements/requirements.txt

@ -13,6 +13,7 @@ django-floppyforms==1.6.2
django-model-utils==2.5 django-model-utils==2.5
django-sass-processor==0.4.6 django-sass-processor==0.4.6
djangorestframework djangorestframework
drfdocs
easy-thumbnails==2.3 easy-thumbnails==2.3
git+git://github.com/interlegis/trml2pdf.git git+git://github.com/interlegis/trml2pdf.git
libsass==0.11.1 libsass==0.11.1

1
sapl/api/__init__.py

@ -0,0 +1 @@
default_app_config = 'sapl.api.apps.AppConfig'

0
sapl/api/admin.py

8
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')

57
sapl/api/forms.py

@ -0,0 +1,57 @@
from django.contrib.contenttypes.fields import GenericRel
from django.db.models import Q
from django_filters.filters import MethodFilter, ModelChoiceFilter
from rest_framework.filters import FilterSet
from sapl.base.forms import autores_models_generic_relations
from sapl.base.models import Autor, TipoAutor
from sapl.utils import SaplGenericRelation
class AutorChoiceFilterSet(FilterSet):
q = MethodFilter()
tipo = ModelChoiceFilter(queryset=TipoAutor.objects.all())
class Meta:
model = Autor
fields = ['q',
'tipo',
'nome', ]
def filter_q(self, queryset, value):
query = value.split(' ')
if query:
q = Q()
for qtext in query:
if not qtext:
continue
q_fs = Q(nome__icontains=qtext)
order_by = []
for gr in autores_models_generic_relations():
model = gr[0]
sgr = gr[1]
for item in sgr:
if item.related_model != Autor:
continue
flag_order_by = True
for field in item.fields_search:
if flag_order_by:
flag_order_by = False
order_by.append('%s__%s' % (
item.related_query_name(),
field[0])
)
q_fs = q_fs | Q(**{'%s__%s%s' % (
item.related_query_name(),
field[0],
field[1]): qtext})
q = q & q_fs
if q:
queryset = queryset.filter(q).order_by(*order_by)
return queryset

0
sapl/api/migrations/__init__.py

34
sapl/api/pagination.py

@ -0,0 +1,34 @@
from django.core.paginator import EmptyPage
from django.utils.encoding import force_text
from rest_framework import pagination
from rest_framework.response import Response
class StandardPagination(pagination.PageNumberPagination):
page_size = 10
page_size_query_param = 'page_size'
max_page_size = 50
def get_paginated_response(self, data):
try:
previous_page_number = self.page.previous_page_number()
except EmptyPage:
previous_page_number = None
try:
next_page_number = self.page.next_page_number()
except EmptyPage:
next_page_number = None
return Response({
'pagination': {
'previous_page': previous_page_number,
'next_page': next_page_number,
'start_index': self.page.start_index(),
'end_index': self.page.end_index(),
'total_entries': self.page.paginator.count,
'total_pages': self.page.paginator.num_pages,
'page': self.page.number,
},
'models': data,
})

17
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'],
}

43
sapl/api/serializers.py

@ -0,0 +1,43 @@
from django.contrib.contenttypes.fields import GenericRel
from rest_framework import serializers
from sapl.base.models import Autor
from sapl.utils import SaplGenericRelation
class ChoiceSerializer(serializers.Serializer):
value = serializers.SerializerMethodField()
text = serializers.SerializerMethodField()
def get_text(self, obj):
return obj[1]
def get_value(self, obj):
return obj[0]
class AutorChoiceSerializer(ChoiceSerializer):
def get_text(self, obj):
return obj.nome
def get_value(self, obj):
return obj.id
class Meta:
model = Autor
fields = ['id', 'nome']
class AutorObjectRelatedField(serializers.RelatedField):
def to_representation(self, value):
return str(value)
class AutorSerializer(serializers.ModelSerializer):
autor_related = AutorObjectRelatedField(read_only=True)
class Meta:
model = Autor
fields = ['id', 'tipo', 'nome', 'object_id', 'autor_related', 'user']

30
sapl/api/urls.py

@ -0,0 +1,30 @@
from django.conf import settings
from django.conf.urls import url, include
from sapl.api.views import AutorListView
from .apps import AppConfig
app_name = AppConfig.name
# router = DefaultRouter()
# urlpatterns += router.urls
urlpatterns_api = [
# url(r'^$', api_root),
url(r'^autor',
AutorListView.as_view(),
name='autor_list'),
]
if settings.DEBUG:
urlpatterns_api += [
url(r'^docs', include('rest_framework_docs.urls')), ]
urlpatterns = [
url(r'^api/', include(urlpatterns_api))
]

169
sapl/api/views.py

@ -0,0 +1,169 @@
from django.db.models import Q
from django.http import Http404
from django.utils.translation import ugettext_lazy as _
from rest_framework.filters import DjangoFilterBackend
from rest_framework.generics import ListAPIView
from rest_framework.permissions import IsAuthenticated, AllowAny
from sapl.api.forms import AutorChoiceFilterSet
from sapl.api.serializers import ChoiceSerializer, AutorSerializer,\
AutorChoiceSerializer
from sapl.base.models import Autor, TipoAutor
from sapl.utils import SaplGenericRelation, sapl_logger
class AutorListView(ListAPIView):
"""
Listagem de Autores com filtro para autores cadastrados
e/ou possíveis autores.
- tr - tipo do resultado
Prepera Lista de Autores para 3 cenários distintos
- default = 1
= 1 -> para (value, text) usados geralmente
em combobox, radiobox, checkbox, etc com pesquisa básica
de Autores feita pelo django-filter
-> processo usado nas pesquisas, o mais usado.
= 2 -> para (value, text) usados geralmente
em combobox, radiobox, checkbox, etc com pesquisa básica
de Autores mas feito para Possíveis Autores armazenados
segundo o ContentType associado ao Tipo de Autor via
relacionamento genérico.
Busca feita sem django-filter processada no get_queryset
-> processo no cadastro de autores para seleção e busca
dos possíveis autores
= 3 -> Devolve instancias da classe Autor filtradas pelo
django-filter
- tipo - chave primária do Tipo de Autor a ser filtrado
- q - busca textual no nome do Autor ou em fields_search
declarados no field SaplGenericRelation das GenericFks
A busca textual acontece via django-filter com a
variável `tr` igual 1 ou 3. Em caso contrário,
o django-filter é desativado e a busca é feita
no model do ContentType associado ao tipo.
Outros campos
"""
TR_AUTOR_CHOICE_SERIALIZER = 1
TR_CHOICE_SERIALIZER = 2
TR_AUTOR_SERIALIZER = 3
# FIXME aplicar permissão correta de usuário
permission_classes = (AllowAny,)
serializer_class = AutorSerializer
queryset = Autor.objects.all()
model = Autor
filter_class = AutorChoiceFilterSet
filter_backends = (DjangoFilterBackend, )
serializer_class = AutorChoiceSerializer
@property
def tr(self):
try:
tr = int(self.request.GET.get
('tr', AutorListView.TR_AUTOR_CHOICE_SERIALIZER))
assert tr in (
AutorListView.TR_AUTOR_CHOICE_SERIALIZER,
AutorListView.TR_CHOICE_SERIALIZER,
AutorListView.TR_AUTOR_SERIALIZER), sapl_logger.info(
_("Tipo do Resultado a ser fornecido não existe!"))
except:
return AutorListView.TR_AUTOR_CHOICE_SERIALIZER
else:
return tr
def get(self, request, *args, **kwargs):
"""
desativa o django-filter se a busca for por possiveis autores
parametro tr = TR_CHOICE_SERIALIZER
"""
if self.tr == AutorListView.TR_CHOICE_SERIALIZER:
self.filter_class = None
self.filter_backends = []
self.serializer_class = ChoiceSerializer
elif self.tr == AutorListView.TR_AUTOR_SERIALIZER:
self.serializer_class = AutorSerializer
self.permission_classes = (IsAuthenticated,)
return ListAPIView.get(self, request, *args, **kwargs)
def get_queryset(self):
queryset = ListAPIView.get_queryset(self)
if self.filter_backends:
return queryset
params = {'content_type__isnull': False}
tipo = ''
try:
tipo = int(self.request.GET.get('tipo', ''))
if tipo:
params['id'] = tipo
except:
pass
tipos = TipoAutor.objects.filter(**params)
if not tipos.exists() and tipo:
raise Http404()
r = []
for tipo in tipos:
q = self.request.GET.get('q', '').strip()
model_class = tipo.content_type.model_class()
fields = list(filter(
lambda field: isinstance(field, SaplGenericRelation) and
field.related_model == Autor,
model_class._meta.get_fields(include_hidden=True)))
"""
fields - é um array de SaplGenericRelation que deve possuir o
atributo fields_search. Verifique na documentação da classe
a estrutura de fields_search.
"""
assert len(fields) >= 1, (_(
'Não foi encontrado em %(model)s um atributo do tipo '
'SaplGenericRelation que use o model %(model_autor)s') % {
'model': model_class._meta.verbose_name,
'model_autor': Autor._meta.verbose_name})
qs = model_class.objects.all()
q_filter = Q()
if q:
for item in fields:
if item.related_model != Autor:
continue
q_fs = Q()
for field in item.fields_search:
q_fs = q_fs | Q(**{'%s%s' % (
field[0],
field[1]): q})
q_filter = q_filter & q_fs
qs = qs.filter(q_filter).distinct(
fields[0].fields_search[0][0])
qs = qs.order_by(fields[0].fields_search[0][0]).values_list(
'id', fields[0].fields_search[0][0])
r += list(qs)
if tipos.count() > 1:
r.sort(key=lambda x: x[1].upper())
return r

414
sapl/base/forms.py

@ -1,23 +1,425 @@
import django_filters from crispy_forms.bootstrap import FieldWithButtons, InlineRadios, StrictButton
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button, Fieldset, Layout from crispy_forms.layout import HTML, Button, Div, Field, Fieldset, Layout, Row
from crispy_forms.templatetags.crispy_forms_field import css_class
from django import forms from django 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.forms import AuthenticationForm
from django.contrib.auth.models import Group
from django.contrib.auth.password_validation import validate_password
from django.contrib.contenttypes.fields import GenericRel
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.db import models from django.db import models, transaction
from django.forms import ModelForm from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _, string_concat
import django_filters
from sapl.crispy_layout_mixin import form_actions, to_row from sapl.base.models import Autor, TipoAutor
from sapl.crispy_layout_mixin import (SaplFormLayout, form_actions, to_column,
to_row)
from sapl.materia.models import MateriaLegislativa from sapl.materia.models import MateriaLegislativa
from sapl.sessao.models import SessaoPlenaria from sapl.sessao.models import SessaoPlenaria
from sapl.settings import MAX_IMAGE_UPLOAD_SIZE from sapl.settings import MAX_IMAGE_UPLOAD_SIZE
from sapl.utils import (RANGE_ANOS, ImageThumbnailFileInput, from sapl.utils import (RANGE_ANOS, ImageThumbnailFileInput,
RangeWidgetOverride, autor_label, autor_modal) RangeWidgetOverride, autor_label, autor_modal,
SaplGenericRelation)
from .models import AppConfig, CasaLegislativa from .models import AppConfig, CasaLegislativa
ACTION_CREATE_USERS_AUTOR_CHOICE = [
('C', _('Criar novo Usuário')),
('A', _('Associar um usuário existente')),
('N', _('Autor sem Usuário de Acesso ao Sapl')),
]
STATUS_USER_CHOICE = [
('R', _('Apenas retirar Perfil de Autor do Usuário que está sendo'
' desvinculado')),
('D', _('Retirar Perfil de Autor e desativar Usuário que está sendo'
' desvinculado')),
('X', _('Excluir Usuário')),
]
def autores_models_generic_relations():
models_of_generic_relations = list(map(
lambda x: x.related_model,
filter(
lambda obj: obj.is_relation and
hasattr(obj, 'field') and
isinstance(obj, GenericRel),
Autor._meta.get_fields(include_hidden=True))
))
models = list(map(
lambda x: (x,
list(filter(
lambda field: (
isinstance(
field, SaplGenericRelation) and
field.related_model == Autor),
x._meta.get_fields(include_hidden=True)))),
models_of_generic_relations
))
return models
class TipoAutorForm(ModelForm):
content_type = forms.ModelChoiceField(
queryset=ContentType.objects.all(),
label=TipoAutor._meta.get_field('content_type').verbose_name,
required=False)
class Meta:
model = TipoAutor
fields = ['descricao',
'content_type']
def __init__(self, *args, **kwargs):
super(TipoAutorForm, self).__init__(*args, **kwargs)
# Models que apontaram uma GenericRelation com Autor
models_of_generic_relations = list(map(
lambda x: x.related_model,
filter(
lambda obj: obj.is_relation and
hasattr(obj, 'field') and
isinstance(obj, GenericRel),
Autor._meta.get_fields(include_hidden=True))
))
content_types = ContentType.objects.get_for_models(
*models_of_generic_relations)
self.fields['content_type'].choices = [
('', _('Outros (Especifique)'))] + [
(ct.pk, ct) for key, ct in content_types.items()]
class ChoiceWithoutValidationField(forms.ChoiceField):
def validate(self, value):
if self.required and not value:
raise ValidationError(
self.error_messages['required'], code='required')
class AutorForm(ModelForm):
senha = forms.CharField(
max_length=20,
label=_('Senha'),
required=False,
widget=forms.PasswordInput())
senha_confirma = forms.CharField(
max_length=20,
label=_('Confirmar Senha'),
required=False,
widget=forms.PasswordInput())
email = forms.EmailField(
required=False,
label=_('Email'))
confirma_email = forms.EmailField(
required=False,
label=_('Confirmar Email'))
username = forms.CharField(label=get_user_model()._meta.get_field(
'username').verbose_name.capitalize(),
required=False,
max_length=50)
q = forms.CharField(
max_length=50, required=False,
label='Pesquise o nome do Autor com o '
'tipo Selecionado e marque o escolhido.')
autor_related = ChoiceWithoutValidationField(label='',
required=False,
widget=forms.RadioSelect())
action_user = forms.ChoiceField(
label=_('Usuário com acesso ao Sistema para este Autor'),
choices=ACTION_CREATE_USERS_AUTOR_CHOICE,
widget=forms.RadioSelect())
status_user = forms.ChoiceField(
label=_('Bloqueio do Usuário Existente'),
choices=STATUS_USER_CHOICE,
widget=forms.RadioSelect(),
required=False,
help_text=_('Se vc está trocando ou removendo o usuário deste Autor, '
'como o Sistema deve proceder com o usuário que está sendo'
' desvinculado?'))
class Meta:
model = Autor
fields = ['tipo',
'nome',
'cargo',
'autor_related',
'q',
'action_user',
'username']
def __init__(self, *args, **kwargs):
autor_related = Div(
FieldWithButtons(
Field('q',
placeholder=_('Pesquisar por possíveis autores para '
'o Tipo de Autor selecionado.')),
StrictButton(
_('Filtrar'), css_class='btn-default btn-filtrar-autor',
type='button')),
css_class='hidden',
data_action='create',
data_application='AutorSearch',
data_field='autor_related')
autor_select = Row(to_column(('tipo', 3)),
Div(to_column(('nome', 5)),
to_column(('cargo', 4)), css_class="div_nome_cargo"),
to_column((autor_related, 9)),
to_column((Div(
Field('autor_related'),
css_class='radiogroup-autor-related hidden'),
12)))
row2 = Row(to_column((InlineRadios('action_user'), 8)),
to_column((Div('username'), 4)))
row3 = Row(to_column(('senha', 3)),
to_column(('senha_confirma', 3)),
to_column(('email', 3)),
to_column(('confirma_email', 3)),
css_class='new_user_fields hidden')
row4 = Row(to_column((Div(InlineRadios('status_user'),
css_class='radiogroup-status hidden'), 12)))
controle_acesso = Fieldset(
_('Controle de Acesso do Autor'),
row2, row3, row4
)
self.helper = FormHelper()
self.helper.layout = SaplFormLayout(autor_select, controle_acesso)
super(AutorForm, self).__init__(*args, **kwargs)
self.fields['action_user'].initial = 'N'
if self.instance.pk:
if self.instance.autor_related:
self.fields['autor_related'].choices = [
(self.instance.autor_related.pk,
self.instance.autor_related)]
self.fields['q'].initial = self.instance.nome
if self.instance.user:
self.fields['username'].initial = self.instance.user.username
self.fields['action_user'].initial = 'A'
self.fields['status_user'].initial = 'R'
self.fields['username'].label = string_concat(
self.fields['username'].label,
' (', self.instance.user.username, ')')
self.fields['status_user'].label = string_concat(
self.fields['status_user'].label,
' (', self.instance.user.username, ')')
self.fields['username'].widget.attrs.update({
'data': self.instance.user.username
if self.instance.user else ''})
self.fields['status_user'].widget.attrs.update({
'data': self.instance.user.username
if self.instance.user else ''})
def valida_igualdade(self, texto1, texto2, msg):
if texto1 != texto2:
raise ValidationError(msg)
return True
def clean(self):
User = get_user_model()
cd = self.cleaned_data
if 'action_user' not in cd or not cd['action_user']:
raise ValidationError(_('Informe se o Autor terá usuário '
'vinculado para acesso ao Sistema.'))
if self.instance.pk and self.instance.user_id:
if self.instance.user.username != cd['username']:
if 'status_user' not in cd or not cd['status_user']:
raise ValidationError(
_('Foi trocado ou removido o usuário deste Autor, '
'mas não foi informado como se deve proceder com o '
'usuário que está sendo desvinculado?'))
qs_user = User.objects.all()
qs_autor = Autor.objects.all()
if self.instance.pk:
qs_autor = qs_autor.exclude(pk=self.instance.pk)
if self.instance.user:
qs_user = qs_user.exclude(pk=self.instance.user.pk)
if cd['action_user'] == 'C':
if User.objects.filter(username=cd['username']).exists():
raise ValidationError(
_('Já existe usuário com o username "%s". '
'Para utilizar esse username você deve selecionar '
'"Associar um usuário existente".') % cd['username'])
if ('senha' not in cd or 'senha_confirma' not in cd or
not cd['senha'] or not cd['senha_confirma']):
raise ValidationError(_(
'A senha e sua confirmação devem ser informadas.'))
msg = _('As senhas não conferem.')
self.valida_igualdade(cd['senha'], cd['senha_confirma'], msg)
try:
validate_password(self.cleaned_data['senha'])
except ValidationError as error:
raise ValidationError(error)
if ('email' not in cd or 'confirma_email' not in cd or
not cd['email'] or not cd['confirma_email']):
raise ValidationError(_(
'O email e sua confirmação devem ser informados.'))
msg = _('Os emails não conferem.')
self.valida_igualdade(cd['email'], cd['confirma_email'], msg)
if qs_user.filter(email=cd['email']).exists():
raise ValidationError(_('Este email já foi cadastrado.'))
if qs_autor.filter(user__email=cd['email']).exists():
raise ValidationError(
_('Já existe um Autor com este email.'))
elif cd['action_user'] == 'A':
if not User.objects.filter(username=cd['username']).exists():
raise ValidationError(
_('Não existe usuário com username "%s". '
'Para utilizar esse username você deve selecionar '
'"Criar novo Usuário".') % cd['username'])
if cd['action_user'] != 'N':
if 'username' not in cd or not cd['username']:
raise ValidationError(_('O username deve ser informado.'))
if qs_autor.filter(user__username=cd['username']).exists():
raise ValidationError(
_('Já existe um Autor para este usuário.'))
"""
'if' não é necessário por ser campo obrigatório e o framework
mostrar a mensagem de obrigatório junto ao campo. mas foi colocado
ainda assim para renderizar um message.danger no topo do form.
"""
if 'tipo' not in cd or not cd['tipo']:
raise ValidationError(
_('O Tipo do Autor deve ser selecionado.'))
tipo = cd['tipo']
if not tipo.content_type:
if 'nome' not in cd or not cd['nome']:
raise ValidationError(
_('O Nome do Autor deve ser informado.'))
else:
if 'autor_related' not in cd or not cd['autor_related']:
raise ValidationError(
_('Um registro de %s deve ser escolhido para ser '
'vinculado ao cadastro de Autor') % tipo.descricao)
if not tipo.content_type.model_class().objects.filter(
pk=cd['autor_related']).exists():
raise ValidationError(
_('O Registro definido (%s-%s) não está na base de %s.'
) % (cd['autor_related'], cd['q'], tipo.descricao))
if qs_autor.filter(object_id=cd['autor_related']).exists():
autor = qs_autor.filter(object_id=cd['autor_related']).first()
raise ValidationError(
_('Já existe um autor Cadastrado para %s'
) % autor.autor_related)
return self.cleaned_data
@transaction.atomic
def save(self, commit=False):
autor = super(AutorForm, self).save(commit)
user_old = autor.user if autor.user_id else None
u = None
if self.cleaned_data['action_user'] == 'A':
u = get_user_model().objects.get(
username=self.cleaned_data['username'])
if not u.is_active:
u.is_active = settings.DEBUG
u.save()
elif self.cleaned_data['action_user'] == 'C':
u = get_user_model().objects.create(
username=self.cleaned_data['username'],
email=self.cleaned_data['email'])
u.set_password(self.cleaned_data['senha'])
# Define usuário como ativo em ambiente de desenvolvimento
# pode logar sem a necessidade de passar pela validação de email
# troque par False para testar o envio de email em desenvolvimento
u.is_active = settings.DEBUG
u.save()
autor.user = u
if not autor.tipo.content_type:
autor.content_type = None
autor.object_id = None
autor.autor_related = None
else:
autor.autor_related = autor.tipo.content_type.model_class(
).objects.get(pk=self.cleaned_data['autor_related'])
autor.nome = str(autor.autor_related)
autor.save()
# FIXME melhorar captura de grupo de Autor, levando em conta,
# no mínimo, a tradução.
grupo = Group.objects.filter(name='Autor')[0]
if self.cleaned_data['action_user'] != 'N':
autor.user.groups.add(grupo)
if user_old and user_old != autor.user:
user_old.groups.remove(grupo)
else:
if 'status_user' in self.cleaned_data and user_old:
if self.cleaned_data['status_user'] == 'X':
user_old.delete()
elif self.cleaned_data['status_user'] == 'D':
user_old.groups.remove(grupo)
user_old.is_active = False
user_old.save()
elif self.cleaned_data['status_user'] == 'R':
user_old.groups.remove(grupo)
elif user_old:
user_old.groups.remove(grupo)
return autor
class RelatorioAtasFilterSet(django_filters.FilterSet): class RelatorioAtasFilterSet(django_filters.FilterSet):
filter_overrides = {models.DateField: { filter_overrides = {models.DateField: {

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

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

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

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

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

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

63
sapl/base/models.py

@ -10,7 +10,7 @@ from django.db.utils import DEFAULT_DB_ALIAS
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat from django.utils.translation import string_concat
from sapl.utils import UF, YES_NO_CHOICES from sapl.utils import UF, YES_NO_CHOICES, get_settings_auth_user_model
TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')), TIPO_DOCUMENTO_ADMINISTRATIVO = (('O', _('Ostensivo')),
('R', _('Restritivo'))) ('R', _('Restritivo')))
@ -132,6 +132,67 @@ class AppConfig(models.Model):
'id': self.id} 'id': self.id}
class TipoAutor(models.Model):
descricao = models.CharField(max_length=50, verbose_name=_('Descrição'))
content_type = models.OneToOneField(
ContentType,
null=True, default=None,
verbose_name=_('Modelagem no SAPL'))
class Meta:
verbose_name = _('Tipo de Autor')
verbose_name_plural = _('Tipos de Autor')
def __str__(self):
return self.descricao
class Autor(models.Model):
user = models.OneToOneField(get_settings_auth_user_model(),
on_delete=models.SET_NULL,
null=True)
tipo = models.ForeignKey(TipoAutor, verbose_name=_('Tipo do Autor'))
content_type = models.ForeignKey(
ContentType,
blank=True, null=True, default=None)
object_id = models.PositiveIntegerField(
blank=True, null=True, default=None)
autor_related = GenericForeignKey('content_type', 'object_id')
nome = models.CharField(
max_length=50, blank=True, verbose_name=_('Nome do Autor'))
cargo = models.CharField(max_length=50, blank=True)
class Meta:
verbose_name = _('Autor')
verbose_name_plural = _('Autores')
unique_together = (('content_type', 'object_id'), )
def __str__(self):
if self.autor_related:
return str(self.autor_related)
else:
if str(self.cargo):
return _('%(nome)s - %(cargo)s') % {
'nome': self.nome, 'cargo': self.cargo}
else:
return str(self.nome)
"""if str(self.tipo) == 'Parlamentar' and self.parlamentar:
return self.parlamentar.nome_parlamentar
elif str(self.tipo) == 'Comissao' and self.comissao:
return str(self.comissao)
elif str(self.tipo) == 'Partido' and self.partido:
return str(self.partido)
else:
"""
def create_proxy_permissions( def create_proxy_permissions(
app_config, verbosity=2, interactive=True, app_config, verbosity=2, interactive=True,
using=DEFAULT_DB_ALIAS, **kwargs): using=DEFAULT_DB_ALIAS, **kwargs):

21
sapl/base/urls.py

@ -3,6 +3,8 @@ from django.contrib.auth import views
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from sapl.base.views import AutorCrud, TipoAutorCrud
from .apps import AppConfig from .apps import AppConfig
from .forms import LoginForm from .forms import LoginForm
from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView, from .views import (AppConfigCrud, CasaLegislativaCrud, HelpView,
@ -16,19 +18,19 @@ app_name = AppConfig.name
urlpatterns = [ urlpatterns = [
url(r'^sistema/autor/tipo/', include(TipoAutorCrud.get_urls())),
url(r'^sistema/autor/', include(AutorCrud.get_urls())),
url(r'^sistema/ajuda/', TemplateView.as_view(template_name='ajuda.html')), url(r'^sistema/ajuda/', TemplateView.as_view(template_name='ajuda.html')),
url(r'^sistema/ajuda/(?P<topic>\w+)$', HelpView.as_view(), name='help_topic'), url(r'^sistema/ajuda/(?P<topic>\w+)$',
url(r'^sistema/ajuda/', TemplateView.as_view(template_name='ajuda/index.html'), HelpView.as_view(), name='help_topic'),
url(r'^sistema/ajuda/',
TemplateView.as_view(template_name='ajuda/index.html'),
name='help_base'), name='help_base'),
url(r'^sistema/casa-legislativa/', include(CasaLegislativaCrud.get_urls()), url(r'^sistema/casa-legislativa/', include(CasaLegislativaCrud.get_urls()),
name="casa_legislativa"), name="casa_legislativa"),
url(r'^sistema/app-config/', include(AppConfigCrud.get_urls())), 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' # TODO mover estas telas para a app 'relatorios'
url(r'^sistema/relatorios/$', TemplateView.as_view( url(r'^sistema/relatorios/$', TemplateView.as_view(
template_name='base/relatorios_list.html')), template_name='base/relatorios_list.html')),
@ -50,8 +52,13 @@ urlpatterns = [
RelatorioAtasView.as_view(), RelatorioAtasView.as_view(),
name='atas'), name='atas'),
# todos os sublink s de sistema devem vir acima deste
url(r'^sistema/', permission_required('base.view_tabelas_auxiliares') url(r'^sistema/', permission_required('base.view_tabelas_auxiliares')
(TemplateView.as_view(template_name='sistema.html'))), (TemplateView.as_view(template_name='sistema.html'))),
url(r'^login/$', views.login, {
'template_name': 'base/login.html', 'authentication_form': LoginForm},
name='login'),
url(r'^logout/$', views.logout, {'next_page': '/login'}, name='logout'),
] ]

122
sapl/base/views.py

@ -1,12 +1,20 @@
from django.conf import settings
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.auth.models import Group
from django.contrib.auth.tokens import default_token_generator
from django.core.mail import send_mail
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db.models import Count, Q from django.db.models import Count, Q
from django.http import HttpResponseRedirect 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.utils.translation import ugettext_lazy as _
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django_filters.views import FilterView from django_filters.views import FilterView
from sapl.base.forms import AutorForm, TipoAutorForm
from sapl.base.models import Autor, TipoAutor
from sapl.crud.base import CrudAux from sapl.crud.base import CrudAux
from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa from sapl.materia.models import MateriaLegislativa, TipoMateriaLegislativa
from sapl.parlamentares.models import Parlamentar from sapl.parlamentares.models import Parlamentar
@ -26,6 +34,120 @@ def get_casalegislativa():
return CasaLegislativa.objects.first() return CasaLegislativa.objects.first()
class TipoAutorCrud(CrudAux):
model = TipoAutor
help_path = 'tipo-autor'
class BaseMixin(CrudAux.BaseMixin):
list_field_names = ['descricao', 'content_type']
form_class = TipoAutorForm
class AutorCrud(CrudAux):
model = Autor
help_path = 'autor'
class BaseMixin(CrudAux.BaseMixin):
list_field_names = ['tipo', 'nome', 'user__username']
class DeleteView(CrudAux.DeleteView):
def delete(self, *args, **kwargs):
self.object = self.get_object()
# FIXME melhorar captura de grupo de Autor, levando em conta trad
grupo = Group.objects.filter(name='Autor')[0]
self.object.user.groups.remove(grupo)
return CrudAux.DeleteView.delete(self, *args, **kwargs)
class UpdateView(CrudAux.UpdateView):
layout_key = None
form_class = AutorForm
def form_valid(self, form):
# devido a implement do form o form_valid do Crud deve ser pulado
return super(CrudAux.UpdateView, self).form_valid(form)
def get_success_url(self):
# FIXME try except - testar envio de emails
pk_autor = self.object.id
try:
kwargs = {}
user = self.object.user
if user.is_active:
return reverse('sapl.base:autor_detail',
kwargs={'pk': pk_autor})
kwargs['token'] = default_token_generator.make_token(user)
kwargs['uidb64'] = urlsafe_base64_encode(force_bytes(user.pk))
assunto = "SAPL - Confirmação de Conta"
full_url = self.request.get_raw_uri()
url_base = full_url[:full_url.find('sistema') - 1]
mensagem = (
"Este e-mail foi utilizado para fazer cadastro no " +
"SAPL com o perfil de Autor. Agora você pode " +
"criar/editar/enviar Proposições.\n" +
"Seu nome de usuário é: " +
self.request.POST['username'] + "\n"
"Caso você não tenha feito este cadastro, por favor " +
"ignore esta mensagem. Caso tenha, clique " +
"no link abaixo\n" + url_base +
reverse('sapl.materia:confirmar_email', kwargs=kwargs))
remetente = [settings.EMAIL_SEND_USER]
destinatario = [user.email]
send_mail(assunto, mensagem, remetente, destinatario,
fail_silently=False)
except:
pass
return reverse('sapl.base:autor_detail',
kwargs={'pk': pk_autor})
class CreateView(CrudAux.CreateView):
form_class = AutorForm
layout_key = None
def form_valid(self, form):
# devido a implement do form o form_valid do Crud deve ser pulado
return super(CrudAux.CreateView, self).form_valid(form)
def get_success_url(self):
pk_autor = self.object.id
try:
# FIXME try except - testar envio de emails
kwargs = {}
user = self.object.user
kwargs['token'] = default_token_generator.make_token(user)
kwargs['uidb64'] = urlsafe_base64_encode(force_bytes(user.pk))
assunto = "SAPL - Confirmação de Conta"
full_url = self.request.get_raw_uri()
url_base = full_url[:full_url.find('sistema') - 1]
mensagem = (
"Este e-mail foi utilizado para fazer cadastro no " +
"SAPL com o perfil de Autor. Agora você pode " +
"criar/editar/enviar Proposições.\n" +
"Seu nome de usuário é: " +
self.request.POST['username'] + "\n"
"Caso você não tenha feito este cadastro, por favor " +
"ignore esta mensagem. Caso tenha, clique " +
"no link abaixo\n" + url_base +
reverse('sapl.materia:confirmar_email', kwargs=kwargs))
remetente = settings.EMAIL_SEND_USER
destinatario = [user.email]
send_mail(assunto, mensagem, remetente, destinatario,
fail_silently=False)
except:
pass
return reverse('sapl.base:autor_detail',
kwargs={'pk': pk_autor})
class RelatorioAtasView(FilterView): class RelatorioAtasView(FilterView):
model = SessaoPlenaria model = SessaoPlenaria
filterset_class = RelatorioAtasFilterSet filterset_class = RelatorioAtasFilterSet

12
sapl/comissoes/models.py

@ -3,8 +3,9 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils import Choices from model_utils import Choices
from sapl.base.models import Autor
from sapl.parlamentares.models import Parlamentar 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): class TipoComissao(models.Model):
@ -79,12 +80,19 @@ class Comissao(models.Model):
choices=YES_NO_CHOICES, choices=YES_NO_CHOICES,
verbose_name=_('Comissão Ativa?')) verbose_name=_('Comissão Ativa?'))
autor = SaplGenericRelation(Autor,
related_query_name='comissao_set',
fields_search=(
('nome', '__icontains'),
('sigla', '__icontains')
))
class Meta: class Meta:
verbose_name = _('Comissão') verbose_name = _('Comissão')
verbose_name_plural = _('Comissões') verbose_name_plural = _('Comissões')
def __str__(self): def __str__(self):
return self.nome return self.sigla + ' - ' + self.nome
class Periodo(models.Model): # PeriodoCompComissao class Periodo(models.Model): # PeriodoCompComissao

10
sapl/compilacao/views.py

@ -1,7 +1,7 @@
import logging
import sys
from collections import OrderedDict from collections import OrderedDict
from datetime import datetime, timedelta from datetime import datetime, timedelta
import logging
import sys
from braces.views import FormMessagesMixin from braces.views import FormMessagesMixin
from django import forms from django import forms
@ -20,8 +20,8 @@ from django.shortcuts import get_object_or_404, redirect
from django.utils.dateparse import parse_date from django.utils.dateparse import parse_date
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.utils.translation import ugettext_lazy as _
from django.utils.translation import string_concat from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.generic.detail import DetailView from django.views.generic.detail import DetailView
from django.views.generic.edit import (CreateView, DeleteView, FormView, from django.views.generic.edit import (CreateView, DeleteView, FormView,
@ -47,6 +47,8 @@ from sapl.compilacao.models import (Dispositivo, Nota,
from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED, from sapl.compilacao.utils import (DISPOSITIVO_SELECT_RELATED,
DISPOSITIVO_SELECT_RELATED_EDIT) DISPOSITIVO_SELECT_RELATED_EDIT)
from sapl.crud.base import Crud, CrudListView, make_pagination from sapl.crud.base import Crud, CrudListView, make_pagination
from sapl.settings import BASE_DIR
TipoNotaCrud = Crud.build(TipoNota, 'tipo_nota') TipoNotaCrud = Crud.build(TipoNota, 'tipo_nota')
TipoVideCrud = Crud.build(TipoVide, 'tipo_vide') TipoVideCrud = Crud.build(TipoVide, 'tipo_vide')
@ -55,7 +57,7 @@ VeiculoPublicacaoCrud = Crud.build(VeiculoPublicacao, 'veiculo_publicacao')
TipoDispositivoCrud = Crud.build( TipoDispositivoCrud = Crud.build(
TipoDispositivo, 'tipo_dispositivo') TipoDispositivo, 'tipo_dispositivo')
logger = logging.getLogger(__name__) logger = logging.getLogger(BASE_DIR.name)
def get_integrations_view_names(): def get_integrations_view_names():

5
sapl/crispy_layout_mixin.py

@ -138,8 +138,9 @@ class CrispyLayoutFormMixin:
# simply return None if there is no get_form on super # simply return None if there is no get_form on super
pass pass
else: else:
form.helper = FormHelper() if self.layout_key:
form.helper.layout = SaplFormLayout(*self.get_layout()) form.helper = FormHelper()
form.helper.layout = SaplFormLayout(*self.get_layout())
return form return form
@property @property

17
sapl/crud/base.py

@ -10,6 +10,7 @@ from django.conf.urls import url
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.db import models from django.db import models
from django.db.models.fields.related import ForeignKey
from django.http.response import Http404 from django.http.response import Http404
from django.utils.decorators import classonlymethod from django.utils.decorators import classonlymethod
from django.utils.encoding import force_text from django.utils.encoding import force_text
@ -21,10 +22,11 @@ from django.views.generic.base import ContextMixin
from django.views.generic.list import MultipleObjectMixin from django.views.generic.list import MultipleObjectMixin
from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display from sapl.crispy_layout_mixin import CrispyLayoutFormMixin, get_field_display
from sapl.settings import BASE_DIR
from sapl.utils import normalize from sapl.utils import normalize
logger = logging.getLogger(__name__) logger = logging.getLogger(BASE_DIR.name)
ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \ ACTION_LIST, ACTION_CREATE, ACTION_DETAIL, ACTION_UPDATE, ACTION_DELETE = \
'list', 'create', 'detail', 'update', 'delete' 'list', 'create', 'detail', 'update', 'delete'
@ -1027,10 +1029,17 @@ class MasterDetailCrud(Crud):
parent_model = None parent_model = None
if '__' in obj.parent_field: if '__' in obj.parent_field:
fields = obj.parent_field.split('__') fields = obj.parent_field.split('__')
parent_model = self.model parent_model = pm = self.model
for field in fields: for field in fields:
parent_model = getattr( pm = getattr(pm, field)
parent_model, field).field.related_model if isinstance(pm.field, ForeignKey):
parent_model = getattr(
parent_model, field).field.related_model
else:
parent_model = getattr(
parent_model, field).rel.related_model
pm = parent_model
else: else:
parent_model = getattr( parent_model = getattr(
self.model, obj.parent_field).field.related_model self.model, obj.parent_field).field.related_model

133
sapl/materia/forms.py

@ -1,18 +1,16 @@
from datetime import datetime from datetime import datetime
import django_filters
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout from crispy_forms.layout import HTML, Button, Column, Fieldset, Layout
from django import forms 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.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models, transaction from django.db import models
from django.db.models import Max from django.db.models import Max
from django.forms import ModelForm from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters
from sapl.base.models import Autor
from sapl.comissoes.models import Comissao from sapl.comissoes.models import Comissao
from sapl.crispy_layout_mixin import form_actions, to_row from sapl.crispy_layout_mixin import form_actions, to_row
from sapl.norma.models import (LegislacaoCitada, NormaJuridica, from sapl.norma.models import (LegislacaoCitada, NormaJuridica,
@ -22,7 +20,7 @@ from sapl.settings import MAX_DOC_UPLOAD_SIZE
from sapl.utils import (RANGE_ANOS, RangeWidgetOverride, autor_label, from sapl.utils import (RANGE_ANOS, RangeWidgetOverride, autor_label,
autor_modal) autor_modal)
from .models import (AcompanhamentoMateria, Anexada, Autor, Autoria, from .models import (AcompanhamentoMateria, Anexada, Autoria,
DespachoInicial, DocumentoAcessorio, MateriaLegislativa, DespachoInicial, DocumentoAcessorio, MateriaLegislativa,
Numeracao, Proposicao, Relatoria, TipoMateriaLegislativa, Numeracao, Proposicao, Relatoria, TipoMateriaLegislativa,
Tramitacao, UnidadeTramitacao) Tramitacao, UnidadeTramitacao)
@ -508,7 +506,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
'data_apresentacao', 'data_apresentacao',
'data_publicacao', 'data_publicacao',
'autoria__autor__tipo', 'autoria__autor__tipo',
'autoria__autor__partido', # 'autoria__autor__partido',
'relatoria__parlamentar_id', 'relatoria__parlamentar_id',
'local_origem_externa', 'local_origem_externa',
'tramitacao__unidade_tramitacao_destino', 'tramitacao__unidade_tramitacao_destino',
@ -544,7 +542,7 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
self.filters['tipo'].label = 'Tipo de Matéria' self.filters['tipo'].label = 'Tipo de Matéria'
self.filters['autoria__autor__tipo'].label = 'Tipo de Autor' 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' self.filters['relatoria__parlamentar_id'].label = 'Relatoria'
row1 = to_row( row1 = to_row(
@ -566,7 +564,8 @@ class MateriaLegislativaFilterSet(django_filters.FilterSet):
css_class='btn btn-primary btn-sm'), 10)]) css_class='btn btn-primary btn-sm'), 10)])
row5 = to_row( row5 = to_row(
[('autoria__autor__tipo', 6), [('autoria__autor__tipo', 6),
('autoria__autor__partido', 6)]) # ('autoria__autor__partido', 6)
])
row6 = to_row( row6 = to_row(
[('relatoria__parlamentar_id', 6), [('relatoria__parlamentar_id', 6),
('local_origem_externa', 6)]) ('local_origem_externa', 6)])
@ -666,122 +665,6 @@ class AutoriaForm(ModelForm):
return self.cleaned_data 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): class AcessorioEmLoteFilterSet(django_filters.FilterSet):
filter_overrides = {models.DateField: { filter_overrides = {models.DateField: {

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

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

16
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 = [
]

87
sapl/materia/models.py

@ -3,8 +3,9 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils import Choices from model_utils import Choices
from sapl.base.models import Autor
from sapl.comissoes.models import Comissao 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, from sapl.utils import (RANGE_ANOS, YES_NO_CHOICES,
get_settings_auth_user_model, get_settings_auth_user_model,
restringe_tipos_de_arquivo_txt) restringe_tipos_de_arquivo_txt)
@ -143,6 +144,12 @@ class MateriaLegislativa(models.Model):
verbose_name=_('Texto Original (PDF)'), verbose_name=_('Texto Original (PDF)'),
validators=[restringe_tipos_de_arquivo_txt]) validators=[restringe_tipos_de_arquivo_txt])
autores = models.ManyToManyField(
Autor,
through='Autoria',
through_fields=('materia', 'autor'),
symmetrical=False,)
class Meta: class Meta:
verbose_name = _('Matéria Legislativa') verbose_name = _('Matéria Legislativa')
verbose_name_plural = _('Matérias Legislativas') verbose_name_plural = _('Matérias Legislativas')
@ -153,6 +160,22 @@ class MateriaLegislativa(models.Model):
'tipo': self.tipo, 'numero': self.numero, 'ano': self.ano} '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): class AcompanhamentoMateria(models.Model):
usuario = models.CharField(max_length=50) usuario = models.CharField(max_length=50)
materia = models.ForeignKey(MateriaLegislativa) materia = models.ForeignKey(MateriaLegislativa)
@ -204,68 +227,6 @@ class AssuntoMateria(models.Model):
return self.assunto 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): class DespachoInicial(models.Model):
# TODO M2M? # TODO M2M?
materia = models.ForeignKey(MateriaLegislativa) materia = models.ForeignKey(MateriaLegislativa)

7
sapl/materia/tests/test_materia.py

@ -1,14 +1,15 @@
import pytest
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.core.files.uploadedfile import SimpleUploadedFile from django.core.files.uploadedfile import SimpleUploadedFile
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from model_mommy import mommy from model_mommy import mommy
import pytest
from sapl.base.models import TipoAutor, Autor
from sapl.comissoes.models import Comissao, TipoComissao from sapl.comissoes.models import Comissao, TipoComissao
from sapl.materia.models import (Anexada, Autor, Autoria, DespachoInicial, from sapl.materia.models import (Anexada, Autoria, DespachoInicial,
DocumentoAcessorio, MateriaLegislativa, DocumentoAcessorio, MateriaLegislativa,
Numeracao, Proposicao, RegimeTramitacao, Numeracao, Proposicao, RegimeTramitacao,
StatusTramitacao, TipoAutor, TipoDocumento, StatusTramitacao, TipoDocumento,
TipoMateriaLegislativa, TipoProposicao, TipoMateriaLegislativa, TipoProposicao,
Tramitacao, UnidadeTramitacao) Tramitacao, UnidadeTramitacao)
from sapl.norma.models import (LegislacaoCitada, NormaJuridica, from sapl.norma.models import (LegislacaoCitada, NormaJuridica,

16
sapl/materia/urls.py

@ -3,7 +3,7 @@ from django.conf.urls import include, url
from sapl.materia.views import (AcompanhamentoConfirmarView, from sapl.materia.views import (AcompanhamentoConfirmarView,
AcompanhamentoExcluirView, AcompanhamentoExcluirView,
AcompanhamentoMateriaView, AnexadaCrud, AcompanhamentoMateriaView, AnexadaCrud,
AutorCrud, AutoriaCrud, ConfirmarEmailView, AutoriaCrud, ConfirmarEmailView,
ConfirmarProposicao, DespachoInicialCrud, ConfirmarProposicao, DespachoInicialCrud,
DocumentoAcessorioCrud, DocumentoAcessorioCrud,
DocumentoAcessorioEmLoteView, DocumentoAcessorioEmLoteView,
@ -15,11 +15,11 @@ from sapl.materia.views import (AcompanhamentoConfirmarView,
ProposicaoRecebida, ProposicaoTaView, ProposicaoRecebida, ProposicaoTaView,
ReceberProposicao, ReciboProposicaoView, ReceberProposicao, ReciboProposicaoView,
RegimeTramitacaoCrud, RelatoriaCrud, RegimeTramitacaoCrud, RelatoriaCrud,
StatusTramitacaoCrud, TipoAutorCrud, StatusTramitacaoCrud, TipoDocumentoCrud,
TipoDocumentoCrud, TipoFimRelatoriaCrud, TipoFimRelatoriaCrud, TipoMateriaCrud,
TipoMateriaCrud, TipoProposicaoCrud, TipoProposicaoCrud, TramitacaoCrud,
TramitacaoCrud, TramitacaoEmLoteView, TramitacaoEmLoteView, UnidadeTramitacaoCrud,
UnidadeTramitacaoCrud, recuperar_materia) recuperar_materia)
from .apps import AppConfig from .apps import AppConfig
@ -78,6 +78,8 @@ urlpatterns_proposicao = [
name='proposicao-devolvida'), name='proposicao-devolvida'),
url(r'^proposicao/confirmar/(?P<pk>\d+)', ConfirmarProposicao.as_view(), url(r'^proposicao/confirmar/(?P<pk>\d+)', ConfirmarProposicao.as_view(),
name='proposicao-confirmar'), name='proposicao-confirmar'),
url(r'^sistema/proposicao/tipo/',
include(TipoProposicaoCrud.get_urls())),
url(r'^proposicao/(?P<pk>[0-9]+)/ta$', url(r'^proposicao/(?P<pk>[0-9]+)/ta$',
ProposicaoTaView.as_view(), name='proposicao_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/tipo/', include(TipoMateriaCrud.get_urls())),
url(r'^sistema/materia/regime-tramitacao/', url(r'^sistema/materia/regime-tramitacao/',
include(RegimeTramitacaoCrud.get_urls())), include(RegimeTramitacaoCrud.get_urls())),
url(r'^sistema/materia/tipo-autor/', include(TipoAutorCrud.get_urls())),
url(r'^sistema/materia/tipo-documento/', url(r'^sistema/materia/tipo-documento/',
include(TipoDocumentoCrud.get_urls())), include(TipoDocumentoCrud.get_urls())),
url(r'^sistema/materia/tipo-fim-relatoria/', url(r'^sistema/materia/tipo-fim-relatoria/',
@ -97,7 +98,6 @@ urlpatterns_sistema = [
url(r'^sistema/materia/unidade-tramitacao/', url(r'^sistema/materia/unidade-tramitacao/',
include(UnidadeTramitacaoCrud.get_urls())), include(UnidadeTramitacaoCrud.get_urls())),
url(r'^sistema/materia/origem/', include(OrigemCrud.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/', url(r'^sistema/materia/status-tramitacao/',
include(StatusTramitacaoCrud.get_urls())), include(StatusTramitacaoCrud.get_urls())),
url(r'^sistema/materia/orgao/', include(OrgaoCrud.get_urls())), url(r'^sistema/materia/orgao/', include(OrgaoCrud.get_urls())),

145
sapl/materia/views.py

@ -3,12 +3,10 @@ from random import choice
from string import ascii_letters, digits from string import ascii_letters, digits
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button from crispy_forms.layout import HTML
from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.mixins import PermissionRequiredMixin 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.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.core.mail import send_mail from django.core.mail import send_mail
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@ -17,16 +15,15 @@ from django.http import JsonResponse
from django.http.response import HttpResponseRedirect from django.http.response import HttpResponseRedirect
from django.shortcuts import redirect from django.shortcuts import redirect
from django.template import Context, loader from django.template import Context, loader
from django.utils.encoding import force_bytes from django.utils.http import urlsafe_base64_decode
from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from django.views.generic import CreateView, ListView, TemplateView, UpdateView from django.views.generic import CreateView, ListView, TemplateView, UpdateView
from django.views.generic.base import RedirectView from django.views.generic.base import RedirectView
from django_filters.views import FilterView from django_filters.views import FilterView
from sapl.base.models import AppConfig, CasaLegislativa from sapl.base.models import AppConfig, Autor, CasaLegislativa, TipoAutor
from sapl.compilacao.views import IntegracaoTaView from sapl.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, from sapl.crud.base import (ACTION_CREATE, ACTION_DELETE, ACTION_DETAIL,
ACTION_LIST, ACTION_UPDATE, RP_DETAIL, RP_LIST, ACTION_LIST, ACTION_UPDATE, RP_DETAIL, RP_LIST,
Crud, CrudAux, CrudDetailView, MasterDetailCrud, Crud, CrudAux, CrudDetailView, MasterDetailCrud,
@ -37,23 +34,24 @@ from sapl.norma.models import LegislacaoCitada
from sapl.utils import (TURNO_TRAMITACAO_CHOICES, YES_NO_CHOICES, autor_label, from sapl.utils import (TURNO_TRAMITACAO_CHOICES, YES_NO_CHOICES, autor_label,
autor_modal, gerar_hash_arquivo, get_base_url, autor_modal, gerar_hash_arquivo, get_base_url,
permissoes_autor, permissoes_materia, permissoes_autor, permissoes_materia,
permissoes_protocoloadm, permission_required_for_app) permissoes_protocoloadm, permission_required_for_app,
montar_row_autor)
from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm, from .forms import (AcessorioEmLoteFilterSet, AcompanhamentoMateriaForm,
AutorForm, ConfirmarProposicaoForm, DocumentoAcessorioForm, ConfirmarProposicaoForm, DocumentoAcessorioForm,
MateriaLegislativaFilterSet, MateriaLegislativaFilterSet,
PrimeiraTramitacaoEmLoteFilterSet, ProposicaoForm, PrimeiraTramitacaoEmLoteFilterSet, ProposicaoForm,
ReceberProposicaoForm, TramitacaoEmLoteFilterSet, ReceberProposicaoForm, TramitacaoEmLoteFilterSet,
filtra_tramitacao_destino, filtra_tramitacao_destino,
filtra_tramitacao_destino_and_status, filtra_tramitacao_destino_and_status,
filtra_tramitacao_status) filtra_tramitacao_status)
from .models import (AcompanhamentoMateria, Anexada, Autor, Autoria, from .models import (AcompanhamentoMateria, Anexada, Autoria, DespachoInicial,
DespachoInicial, DocumentoAcessorio, MateriaLegislativa, DocumentoAcessorio, MateriaLegislativa, Numeracao, Orgao,
Numeracao, Orgao, Origem, Proposicao, RegimeTramitacao, Origem, Proposicao, RegimeTramitacao, Relatoria,
Relatoria, StatusTramitacao, TipoAutor, TipoDocumento, StatusTramitacao, TipoDocumento, TipoFimRelatoria,
TipoFimRelatoria, TipoMateriaLegislativa, TipoProposicao, TipoMateriaLegislativa, TipoProposicao, Tramitacao,
Tramitacao, UnidadeTramitacao) UnidadeTramitacao)
OrigemCrud = Crud.build(Origem, '') OrigemCrud = Crud.build(Origem, '')
@ -69,20 +67,17 @@ TipoDocumentoCrud = CrudAux.build(
TipoFimRelatoriaCrud = CrudAux.build( TipoFimRelatoriaCrud = CrudAux.build(
TipoFimRelatoria, 'fim_relatoria') TipoFimRelatoria, 'fim_relatoria')
TipoAutorCrud = CrudAux.build(
TipoAutor, 'regime_tramitacao')
class MateriaTaView(IntegracaoTaView): class MateriaTaView(IntegracaoTaView):
model = MateriaLegislativa model = MateriaLegislativa
model_type_foreignkey = TipoMateriaLegislativa 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): 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'): if AppConfig.attr('texto_articulado_materia'):
return IntegracaoTaView.get(self, request, *args, **kwargs) return IntegracaoTaView.get(self, request, *args, **kwargs)
else: else:
@ -118,89 +113,6 @@ def recuperar_materia(request):
return response 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('<br /><br />'))
# 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('<a href="{{ view.cancel_url }}"'
' class="btn btn-inverse">Cancelar</a>')]))
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): class ConfirmarEmailView(TemplateView):
template_name = "confirma_email.html" template_name = "confirma_email.html"
@ -398,6 +310,12 @@ class ConfirmarProposicao(PermissionRequiredMixin, CreateView):
class ProposicaoCrud(Crud): class ProposicaoCrud(Crud):
"""
TODO: Entre outros comportamento gerais, mesmo que um usuário tenha
Perfil de Autor o Crud de proposição não deverá permitir acesso a
proposições. O acesso deve ser permitido se existe um Autor registrado
e vinculado ao usuário. Essa tarefa deve ser realizada nas Tabelas Aux.
"""
model = Proposicao model = Proposicao
help_path = '' help_path = ''
@ -661,19 +579,6 @@ class TramitacaoCrud(MasterDetailCrud):
return HttpResponseRedirect(url) 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): def montar_helper_documento_acessorio(self):
autor_row = montar_row_autor('autor') autor_row = montar_row_autor('autor')
self.helper = FormHelper() self.helper = FormHelper()

27
sapl/parlamentares/models.py

@ -4,8 +4,9 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils import Choices from model_utils import Choices
from sapl.base.models import Autor
from sapl.utils import (INDICADOR_AFASTAMENTO, UF, YES_NO_CHOICES, from sapl.utils import (INDICADOR_AFASTAMENTO, UF, YES_NO_CHOICES,
intervalos_tem_intersecao, SaplGenericRelation, intervalos_tem_intersecao,
restringe_tipos_de_arquivo_img) restringe_tipos_de_arquivo_img)
@ -262,6 +263,18 @@ class Parlamentar(models.Model):
verbose_name=_('Fotografia'), verbose_name=_('Fotografia'),
validators=[restringe_tipos_de_arquivo_img]) validators=[restringe_tipos_de_arquivo_img])
# campo conceitual de reversão genérica para o model Autor que dá a
# o meio possível de localização de tipos de autores.
autor = SaplGenericRelation(
Autor,
related_query_name='parlamentar_set',
fields_search=(
# na primeira posição dever ser campo simples sem __
('nome_completo', '__icontains'),
('nome_parlamentar', '__icontains'),
('filiacao__partido__sigla', '__icontains'),
))
class Meta: class Meta:
verbose_name = _('Parlamentar') verbose_name = _('Parlamentar')
verbose_name_plural = _('Parlamentares') verbose_name_plural = _('Parlamentares')
@ -447,6 +460,18 @@ class Frente(models.Model):
blank=True, null=True, verbose_name=_('Data Dissolução')) blank=True, null=True, verbose_name=_('Data Dissolução'))
descricao = models.TextField(blank=True, verbose_name=_('Descrição')) descricao = models.TextField(blank=True, verbose_name=_('Descrição'))
# campo conceitual de reversão genérica para o model Autor que dá a
# o meio possível de localização de tipos de autores.
autor = SaplGenericRelation(
Autor,
related_query_name='frente_set',
fields_search=(
('nome', '__icontains'),
('descricao', '__icontains'),
('parlamentares__filiacao__partido__sigla', '__icontains'),
('parlamentares__filiacao__partido__nome', '__icontains'),
))
class Meta: class Meta:
verbose_name = _('Frente') verbose_name = _('Frente')
verbose_name_plural = _('Frentes') verbose_name_plural = _('Frentes')

3
sapl/parlamentares/views.py

@ -59,14 +59,13 @@ class RelatoriaParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
class ProposicaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView): class ProposicaoParlamentarCrud(CrudBaseForListAndDetailExternalAppView):
model = Proposicao model = Proposicao
list_field_names = ['tipo', 'descricao'] list_field_names = ['tipo', 'descricao']
parent_field = 'autor__parlamentar' parent_field = 'autor__parlamentar_set'
namespace = AppConfig.name namespace = AppConfig.name
class ListView(CrudBaseForListAndDetailExternalAppView.ListView): class ListView(CrudBaseForListAndDetailExternalAppView.ListView):
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter( return super().get_queryset().filter(
autor__parlamentar_id=self.kwargs['pk'],
data_envio__isnull=False) data_envio__isnull=False)

7
sapl/protocoloadm/forms.py

@ -1,6 +1,5 @@
from datetime import datetime from datetime import datetime
import django_filters
from crispy_forms.bootstrap import InlineRadios from crispy_forms.bootstrap import InlineRadios
from crispy_forms.helper import FormHelper from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button, Fieldset, Layout, Submit from crispy_forms.layout import HTML, Button, Fieldset, Layout, Submit
@ -9,9 +8,11 @@ from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.db import models from django.db import models
from django.forms import ModelForm from django.forms import ModelForm
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import django_filters
from sapl.base.models import Autor
from sapl.crispy_layout_mixin import form_actions, to_row from sapl.crispy_layout_mixin import form_actions, to_row
from sapl.materia.models import Autor, UnidadeTramitacao from sapl.materia.models import UnidadeTramitacao
from sapl.utils import (RANGE_ANOS, RangeWidgetOverride, autor_label, from sapl.utils import (RANGE_ANOS, RangeWidgetOverride, autor_label,
autor_modal) autor_modal)
@ -19,6 +20,7 @@ from .models import (DocumentoAcessorioAdministrativo, DocumentoAdministrativo,
Protocolo, TipoDocumentoAdministrativo, Protocolo, TipoDocumentoAdministrativo,
TramitacaoAdministrativo) TramitacaoAdministrativo)
TIPOS_PROTOCOLO = [('0', 'Enviado'), ('1', 'Recebido'), ('', 'Ambos')] TIPOS_PROTOCOLO = [('0', 'Enviado'), ('1', 'Recebido'), ('', 'Ambos')]
NATUREZA_PROCESSO = [('', 'Ambos'), NATUREZA_PROCESSO = [('', 'Ambos'),
@ -421,6 +423,7 @@ class ProtocoloMateriaForm(ModelForm):
super(ProtocoloMateriaForm, self).__init__( super(ProtocoloMateriaForm, self).__init__(
*args, **kwargs) *args, **kwargs)
self.fields['tipo_protocolo'].inline_class = True
class DocumentoAcessorioAdministrativoForm(ModelForm): class DocumentoAcessorioAdministrativoForm(ModelForm):

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

3
sapl/protocoloadm/models.py

@ -4,7 +4,8 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils import Choices from model_utils import Choices
from sapl.materia.models import (Autor, TipoMateriaLegislativa, from sapl.base.models import Autor
from sapl.materia.models import (TipoMateriaLegislativa,
UnidadeTramitacao) UnidadeTramitacao)
from sapl.utils import RANGE_ANOS, YES_NO_CHOICES from sapl.utils import RANGE_ANOS, YES_NO_CHOICES

8
sapl/protocoloadm/urls.py

@ -17,7 +17,7 @@ from sapl.protocoloadm.views import (AnularProtocoloAdmView,
ProtocoloPesquisaView, ProtocoloPesquisaView,
StatusTramitacaoAdministrativoCrud, StatusTramitacaoAdministrativoCrud,
TipoDocumentoAdministrativoCrud, TipoDocumentoAdministrativoCrud,
TramitacaoAdmCrud, pesquisa_autores) TramitacaoAdmCrud)
from .apps import AppConfig from .apps import AppConfig
@ -70,11 +70,11 @@ urlpatterns_sistema = [
url(r'^sistema/status-tramitacao-adm/', url(r'^sistema/status-tramitacao-adm/',
include(StatusTramitacaoAdministrativoCrud.get_urls())), include(StatusTramitacaoAdministrativoCrud.get_urls())),
# FIXME: Usado para pesquisar autor # FIXME: Usado para pesquisar autor- SOLUÇÃO-foi transformado em api/autor
# Melhor forma de fazer? # Melhor forma de fazer?
# Deve mudar de app? # Deve mudar de app?
url(r'^protocoloadm/pesquisar-autor', # url(r'^protocoloadm/pesquisar-autor',
pesquisa_autores, name='pesquisar_autor'), # pesquisa_autores, name='pesquisar_autor'),
] ]
urlpatterns = (urlpatterns_documento_administrativo + urlpatterns = (urlpatterns_documento_administrativo +

15
sapl/protocoloadm/views.py

@ -1,5 +1,5 @@
import json
from datetime import date, datetime from datetime import date, datetime
import json
from braces.views import FormValidMessageMixin from braces.views import FormValidMessageMixin
from django.contrib import messages from django.contrib import messages
@ -15,7 +15,7 @@ from django.views.generic.base import TemplateView
from django_filters.views import FilterView from django_filters.views import FilterView
from sapl.base.apps import AppConfig as AppsAppConfig from sapl.base.apps import AppConfig as AppsAppConfig
from sapl.base.models import AppConfig from sapl.base.models import AppConfig, Autor
from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination from sapl.crud.base import Crud, CrudAux, MasterDetailCrud, make_pagination
from sapl.materia.models import TipoMateriaLegislativa from sapl.materia.models import TipoMateriaLegislativa
from sapl.utils import (create_barcode, get_client_ip, permissoes_adm, from sapl.utils import (create_barcode, get_client_ip, permissoes_adm,
@ -26,11 +26,12 @@ from .forms import (AnularProcoloAdmForm, DocumentoAcessorioAdministrativoForm,
DocumentoAdministrativoForm, ProtocoloDocumentForm, DocumentoAdministrativoForm, ProtocoloDocumentForm,
ProtocoloFilterSet, ProtocoloMateriaForm, ProtocoloFilterSet, ProtocoloMateriaForm,
TramitacaoAdmEditForm, TramitacaoAdmForm) TramitacaoAdmEditForm, TramitacaoAdmForm)
from .models import (Autor, DocumentoAcessorioAdministrativo, from .models import (DocumentoAcessorioAdministrativo,
DocumentoAdministrativo, Protocolo, DocumentoAdministrativo, Protocolo,
StatusTramitacaoAdministrativo, StatusTramitacaoAdministrativo,
TipoDocumentoAdministrativo, TramitacaoAdministrativo) TipoDocumentoAdministrativo, TramitacaoAdministrativo)
TipoDocumentoAdministrativoCrud = CrudAux.build( TipoDocumentoAdministrativoCrud = CrudAux.build(
TipoDocumentoAdministrativo, '') TipoDocumentoAdministrativo, '')
@ -584,6 +585,7 @@ class TramitacaoAdmCrud(MasterDetailCrud):
pass pass
"""
def get_nome_autor(request): def get_nome_autor(request):
nome_autor = '' nome_autor = ''
if request.method == 'GET': if request.method == 'GET':
@ -597,9 +599,9 @@ def get_nome_autor(request):
except ObjectDoesNotExist: except ObjectDoesNotExist:
pass pass
return HttpResponse("{\"nome\":\"" + nome_autor + "\"}", return HttpResponse("{\"nome\":\"" + nome_autor + "\"}",
content_type="application/json; charset=utf-8") content_type="application/json; charset=utf-8")"""
"""
def pesquisa_autores(request): def pesquisa_autores(request):
q = '' q = ''
if request.method == 'GET': if request.method == 'GET':
@ -611,6 +613,8 @@ def pesquisa_autores(request):
Q(comissao__nome__icontains=q) Q(comissao__nome__icontains=q)
) )
autor = Autor.objects.filter(nome__icontains=q)
autores = [] autores = []
for a in autor: for a in autor:
@ -630,3 +634,4 @@ def pesquisa_autores(request):
sort_keys=True, sort_keys=True,
ensure_ascii=False), ensure_ascii=False),
content_type="application/json; charset=utf-8") content_type="application/json; charset=utf-8")
"""

12
sapl/relatorios/views.py

@ -4,9 +4,9 @@ from bs4 import BeautifulSoup
from django.http import Http404, HttpResponse from django.http import Http404, HttpResponse
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from sapl.base.models import CasaLegislativa from sapl.base.models import CasaLegislativa, Autor
from sapl.comissoes.models import Comissao from sapl.comissoes.models import Comissao
from sapl.materia.models import (Autor, Autoria, MateriaLegislativa, Numeracao, from sapl.materia.models import (Autoria, MateriaLegislativa, Numeracao,
Tramitacao, UnidadeTramitacao) Tramitacao, UnidadeTramitacao)
from sapl.parlamentares.models import (CargoMesa, ComposicaoMesa, Filiacao, from sapl.parlamentares.models import (CargoMesa, ComposicaoMesa, Filiacao,
Parlamentar) Parlamentar)
@ -102,12 +102,8 @@ def get_materias(mats):
dic['txt_ementa'] = materia.ementa dic['txt_ementa'] = materia.ementa
autores = Autoria.objects.filter(materia=materia) autores = Autoria.objects.filter(materia=materia)
dic['nom_autor'] = " " dic['nom_autor'] = ', '.join(
for autoria in autores: [str(autoria.autor) for autoria in autores])
if autoria.autor.parlamentar:
dic['nom_autor'] = autoria.autor.parlamentar.nome_completo
elif autoria.autor.comissao:
dic['nom_autor'] = autoria.autor.comissao.nome
des_status = '' des_status = ''
txt_tramitacao = '' txt_tramitacao = ''

7
sapl/sessao/forms.py

@ -212,7 +212,7 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet):
'data_apresentacao', 'data_apresentacao',
'data_publicacao', 'data_publicacao',
'autoria__autor__tipo', 'autoria__autor__tipo',
'autoria__autor__partido', # 'autoria__autor__partido',
'relatoria__parlamentar_id', 'relatoria__parlamentar_id',
'local_origem_externa', 'local_origem_externa',
'em_tramitacao', 'em_tramitacao',
@ -231,7 +231,7 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet):
self.filters['tipo'].label = 'Tipo de Matéria' self.filters['tipo'].label = 'Tipo de Matéria'
self.filters['autoria__autor__tipo'].label = 'Tipo de Autor' 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' self.filters['relatoria__parlamentar_id'].label = 'Relatoria'
row1 = to_row( row1 = to_row(
@ -253,7 +253,8 @@ class AdicionarVariasMateriasFilterSet(MateriaLegislativaFilterSet):
css_class='btn btn-primary btn-sm'), 10)]) css_class='btn btn-primary btn-sm'), 10)])
row5 = to_row( row5 = to_row(
[('autoria__autor__tipo', 6), [('autoria__autor__tipo', 6),
('autoria__autor__partido', 6)]) # ('autoria__autor__partido', 6)
])
row6 = to_row( row6 = to_row(
[('relatoria__parlamentar_id', 6), [('relatoria__parlamentar_id', 6),
('local_origem_externa', 6)]) ('local_origem_externa', 6)])

25
sapl/sessao/models.py

@ -2,10 +2,12 @@ from django.db import models
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from model_utils import Choices from model_utils import Choices
from sapl.base.models import Autor
from sapl.materia.models import MateriaLegislativa from sapl.materia.models import MateriaLegislativa
from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar, from sapl.parlamentares.models import (CargoMesa, Legislatura, Parlamentar,
Partido, SessaoLegislativa) Partido, SessaoLegislativa)
from sapl.utils import YES_NO_CHOICES, restringe_tipos_de_arquivo_txt from sapl.utils import YES_NO_CHOICES, restringe_tipos_de_arquivo_txt,\
SaplGenericRelation
class CargoBancada(models.Model): class CargoBancada(models.Model):
@ -38,6 +40,16 @@ class Bancada(models.Model):
verbose_name=_('Data Extinção')) verbose_name=_('Data Extinção'))
descricao = models.TextField(blank=True, verbose_name=_('Descrição')) descricao = models.TextField(blank=True, verbose_name=_('Descrição'))
# campo conceitual de reversão genérica para o model Autor que dá a
# o meio possível de localização de tipos de autores.
autor = SaplGenericRelation(Autor, related_query_name='bancada_set',
fields_search=(
('nome', '__icontains'),
('descricao', '__icontains'),
('partido__sigla', '__icontains'),
('partido__nome', '__icontains'),
))
class Meta: class Meta:
verbose_name = _('Bancada') verbose_name = _('Bancada')
verbose_name_plural = _('Bancadas') verbose_name_plural = _('Bancadas')
@ -341,6 +353,17 @@ class Bloco(models.Model):
blank=True, null=True, verbose_name=_('Data Dissolução')) blank=True, null=True, verbose_name=_('Data Dissolução'))
descricao = models.TextField(blank=True, verbose_name=_('Descrição')) descricao = models.TextField(blank=True, verbose_name=_('Descrição'))
# campo conceitual de reversão genérica para o model Autor que dá a
# o meio possível de localização de tipos de autores.
autor = SaplGenericRelation(Autor,
related_query_name='bloco_set',
fields_search=(
('nome', '__icontains'),
('descricao', '__icontains'),
('partidos__sigla', '__icontains'),
('partidos__nome', '__icontains'),
))
class Meta: class Meta:
verbose_name = _('Bloco') verbose_name = _('Bloco')
verbose_name_plural = _('Blocos') verbose_name_plural = _('Blocos')

8
sapl/sessao/urls.py

@ -11,10 +11,9 @@ from sapl.sessao.views import (AdicionarVariasMateriasExpediente,
PesquisarPautaSessaoView, PesquisarPautaSessaoView,
PesquisarSessaoPlenariaView, PesquisarSessaoPlenariaView,
PresencaOrdemDiaView, PresencaView, ResumoView, PresencaOrdemDiaView, PresencaView, ResumoView,
SessaoCrud, SessaoCrud, TipoExpedienteCrud,
TipoExpedienteCrud, TipoResultadoVotacaoCrud, TipoResultadoVotacaoCrud, TipoSessaoCrud,
TipoSessaoCrud, VotacaoEditView, VotacaoEditView, VotacaoExpedienteEditView,
VotacaoExpedienteEditView,
VotacaoExpedienteView, VotacaoNominalEditView, VotacaoExpedienteView, VotacaoNominalEditView,
VotacaoNominalExpedienteEditView, VotacaoNominalExpedienteEditView,
VotacaoNominalExpedienteView, VotacaoNominalExpedienteView,
@ -30,6 +29,7 @@ from .apps import AppConfig
app_name = AppConfig.name app_name = AppConfig.name
urlpatterns = [ urlpatterns = [
url(r'^sessao/', include(SessaoCrud.get_urls() + OradorCrud.get_urls() + url(r'^sessao/', include(SessaoCrud.get_urls() + OradorCrud.get_urls() +
OradorExpedienteCrud.get_urls() + OradorExpedienteCrud.get_urls() +

73
sapl/settings.py

@ -13,6 +13,9 @@ Quick-start development settings - unsuitable for production
See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
""" """
import logging
import sys
from decouple import config from decouple import config
from dj_database_url import parse as db_url from dj_database_url import parse as db_url
from unipath import Path from unipath import Path
@ -20,13 +23,13 @@ from unipath import Path
from .temp_suppress_crispy_form_warnings import \ from .temp_suppress_crispy_form_warnings import \
SUPRESS_CRISPY_FORM_WARNINGS_LOGGING SUPRESS_CRISPY_FORM_WARNINGS_LOGGING
BASE_DIR = Path(__file__).ancestor(1) BASE_DIR = Path(__file__).ancestor(1)
PROJECT_DIR = Path(__file__).ancestor(2) PROJECT_DIR = Path(__file__).ancestor(2)
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = config('SECRET_KEY', default='') SECRET_KEY = config('SECRET_KEY', default='')
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = config('DEBUG', default=False, cast=bool) DEBUG = config('DEBUG', default=False, cast=bool)
@ -35,6 +38,9 @@ ALLOWED_HOSTS = ['*']
LOGIN_REDIRECT_URL = '/' LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login/?next=' LOGIN_URL = '/login/?next='
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
# SAPL business apps in dependency order # SAPL business apps in dependency order
SAPL_APPS = ( SAPL_APPS = (
'sapl.base', 'sapl.base',
@ -48,6 +54,7 @@ SAPL_APPS = (
'sapl.painel', 'sapl.painel',
'sapl.protocoloadm', 'sapl.protocoloadm',
'sapl.compilacao', 'sapl.compilacao',
'sapl.api'
) )
INSTALLED_APPS = ( INSTALLED_APPS = (
@ -69,10 +76,11 @@ INSTALLED_APPS = (
'sass_processor', 'sass_processor',
'rest_framework', 'rest_framework',
) + SAPL_APPS ) + SAPL_APPS
if DEBUG: if DEBUG:
INSTALLED_APPS += ('debug_toolbar',) INSTALLED_APPS += ('debug_toolbar', 'rest_framework_docs',)
MIDDLEWARE_CLASSES = ( MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware',
@ -86,6 +94,32 @@ MIDDLEWARE_CLASSES = (
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
) )
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": (
"rest_framework.renderers.JSONRenderer",
#"rest_framework.renderers.BrowsableAPIRenderer",
),
"DEFAULT_PARSER_CLASSES": (
"rest_framework.parsers.JSONParser",
),
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated",
),
"DEFAULT_PERMISSION_CLASSES": (
"sapl.api.permissions.DjangoModelPermissions",
),
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework.authentication.SessionAuthentication",
),
"DEFAULT_PAGINATION_CLASS": "sapl.api.pagination.StandardPagination",
"DEFAULT_FILTER_BACKENDS": (
"rest_framework.filters.SearchFilter",
"rest_framework.filters.DjangoFilterBackend",
),
}
ROOT_URLCONF = 'sapl.urls' ROOT_URLCONF = 'sapl.urls'
TEMPLATES = [ TEMPLATES = [
@ -193,10 +227,41 @@ SASS_PROCESSOR_INCLUDE_DIRS = (BOWER_COMPONENTS_ROOT.child(
'bower_components', 'bootstrap-sass', 'assets', 'stylesheets'), 'bower_components', 'bootstrap-sass', 'assets', 'stylesheets'),
) )
# suprime texto de ajuda default do django-filter
FILTERS_HELP_TEXT_FILTER = False
# FIXME update cripy-forms and remove this # FIXME update cripy-forms and remove this
# hack to suppress many annoying warnings from crispy_forms # hack to suppress many annoying warnings from crispy_forms
# see sapl.temp_suppress_crispy_form_warnings # see sapl.temp_suppress_crispy_form_warnings
LOGGING = SUPRESS_CRISPY_FORM_WARNINGS_LOGGING LOGGING = SUPRESS_CRISPY_FORM_WARNINGS_LOGGING
# suprime texto de ajuda default do django-filter
FILTERS_HELP_TEXT_FILTER = False LOGGING_CONSOLE = config('LOGGING_CONSOLE', default=False, cast=bool)
if DEBUG and LOGGING_CONSOLE:
# Descomentar linha abaixo fará com que logs aparecam, inclusive SQL
# LOGGING['handlers']['console']['level'] = 'DEBUG'
LOGGING['loggers']['django']['level'] = 'DEBUG'
LOGGING.update({
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(pathname)s '
'%(funcName)s %(message)s'
},
'simple': {
'format': '%(levelname)s %(message)s'
},
},
})
LOGGING['handlers']['console']['formatter'] = 'verbose'
LOGGING['loggers'][BASE_DIR.name] = {
'handlers': ['console'],
'level': 'DEBUG',
}
def excepthook(*args):
logging.getLogger(BASE_DIR.name).error(
'Uncaught exception:', exc_info=args)
sys.excepthook = excepthook

35
sapl/static/js/app.js

@ -90,28 +90,25 @@ function autorModal() {
$("#pesquisar").click(function() { $("#pesquisar").click(function() {
var query = $("#q").val() var query = $("#q").val()
$.get("/protocoloadm/pesquisar-autor?q="+ query, function(
data, status){
$("#div-resultado").children().remove(); $.get("/api/autor?q=" + query, function(data, status) {
$("#div-resultado").children().remove();
if (data.pagination.total_entries == 0) {
$("#selecionar").attr("hidden", "hidden");
$("#div-resultado").html(
"<span class='alert'><strong>Nenhum resultado</strong></span>");
return;
}
if (data.length == 0) { var select = $(
$("#selecionar").attr("hidden", "hidden"); '<select id="resultados" \
$("#div-resultado").html( style="min-width: 90%; max-width:90%;" size="5"/>');
"<span class='alert'><strong>Nenhum resultado</strong></span>");
return;
}
var select = $( data.models.forEach(function(item, index) {
'<select id="resultados" \ select.append($("<option>").attr('value', item.value).text(item.text));
style="min-width: 90%; max-width:90%;" size="5"/>'); });
for (i = 0; i < data.length; i++) {
id = data[i][0];
nome = data[i][1];
select.append($("<option>").attr('value',id).text(nome));
}
$("#div-resultado").append("<br/>").append(select); $("#div-resultado").append("<br/>").append(select);
$("#selecionar").removeAttr("hidden", "hidden"); $("#selecionar").removeAttr("hidden", "hidden");
@ -138,7 +135,7 @@ function autorModal() {
}); });
}); });
function get_nome_autor(fieldname) { /*function get_nome_autor(fieldname) {
if ($(fieldname).length > 0) { // se campo existir if ($(fieldname).length > 0) { // se campo existir
if ($(fieldname).val() != "") { // e não for vazio if ($(fieldname).val() != "") { // e não for vazio
var id = $(fieldname).val(); var id = $(fieldname).val();
@ -150,7 +147,7 @@ function autorModal() {
} }
get_nome_autor("#id_autor"); get_nome_autor("#id_autor");
get_nome_autor("#id_autoria__autor"); get_nome_autor("#id_autoria__autor");*/
} }
$(document).ready(function(){ $(document).ready(function(){

1
sapl/static/styles/app.scss

@ -438,6 +438,7 @@ p {
} }
@media (min-width: 1092px) and (max-width: 1199px) { @media (min-width: 1092px) and (max-width: 1199px) {
.container { .container {
width: 1070px; width: 1070px;

4
sapl/templates/base.html

@ -278,16 +278,12 @@
<script type="text/javascript" src="{% static 'jquery/dist/jquery.min.js' %}"></script> <script type="text/javascript" src="{% static 'jquery/dist/jquery.min.js' %}"></script>
<script type="text/javascript" src="{% static 'bootstrap-sass/assets/javascripts/bootstrap.min.js' %}"></script> <script type="text/javascript" src="{% static 'bootstrap-sass/assets/javascripts/bootstrap.min.js' %}"></script>
<script type="text/javascript" src="{% static 'jquery-ui/jquery-ui.min.js' %}"></script> <script type="text/javascript" src="{% static 'jquery-ui/jquery-ui.min.js' %}"></script>
<script type="text/javascript" src="{% static 'jquery-ui/ui/i18n/datepicker-pt-BR.js' %}"></script> <script type="text/javascript" src="{% static 'jquery-ui/ui/i18n/datepicker-pt-BR.js' %}"></script>
<script type="text/javascript" src="{% static 'js/jquery.runner.js' %}"></script> <script type="text/javascript" src="{% static 'js/jquery.runner.js' %}"></script>
<script type="text/javascript" src="{% static 'jquery-mask-plugin/dist/jquery.mask.js' %}"></script> <script type="text/javascript" src="{% static 'jquery-mask-plugin/dist/jquery.mask.js' %}"></script>
<script type="text/javascript" src="{% static 'drunken-parrot-flat-ui/js/checkbox.js' %}"></script>
<script type="text/javascript" src="{% static 'drunken-parrot-flat-ui/js/radio.js' %}"></script>
<script src="{% static 'tinymce/tinymce.min.js' %}"></script> <script src="{% static 'tinymce/tinymce.min.js' %}"></script>
<script type="text/javascript" src="{% static 'jsdiff/diff.min.js' %}"></script> <script type="text/javascript" src="{% static 'jsdiff/diff.min.js' %}"></script>

146
sapl/templates/base/autor_form.html

@ -0,0 +1,146 @@
{% extends "crud/form.html" %}
{% load i18n %}
{% block extra_js %}
<script type="text/javascript">
$(document).ready(function(){
var flag_create = false;
if (location.href.indexOf('create') > 0) {
$('.radiogroup-status').remove();
flag_create = true
}
var active = function(str, atualizar=true) {
if (str == 'nome') {
if (atualizar)
$('#id_nome, #id_q, #id_cargo').val('');
$('.div_nome_cargo').removeClass('hidden');
$("#div_id_autor_related .controls").html('');
$("[data-application='AutorSearch'], .radiogroup-autor-related").addClass('hidden');
}
else {
$('#id_nome, #id_cargo').val('');
$('.div_nome_cargo').addClass('hidden');
$("#div_id_autor_related .alert").remove();
$("[data-application='AutorSearch'], .radiogroup-autor-related").removeClass('hidden');
}
}
var update_search = function(pk, atualizar=true) {
var q = $('#id_q').val();
var url = '{% url 'sapl.api:autor_list'%}'
var formData = {
'q' : q,
'tipo' : pk,
'tr' : '2' // tipo_resultado = 2 - api fornecerá possíveis Autores
}
$.get(url, formData).done(function(data) {
active('pesquisa');
if (atualizar) {
var radios = $("#div_id_autor_related .controls").html('');
data.models.forEach(function (val, index) {
var html_radio = '<div class="radio"><label><span class="icons"><span class="first-icon"></span><span class="second-icon"></span></span><input type="radio" name="autor_related" id="id_autor_related_'+index+'" value="'+val.value+'" style="display:none;">'+val.text+'</label></div>';
radios.append(html_radio);
});
if (data.models.length > 1) {
$('input[name=autor_related]').change(function(event){
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]').closest('.radio').addClass('checked');
}
if (data.pagination.total_entries > 10)
radios.before('<div class="alert alert-info" role="alert"><strong>{% trans "Foram encontrados" %} '+data.pagination.total_entries+' {% trans "registros"%}</strong>'+' {% trans "mas será mostrado apenas os 10 primeiros resultados. Coloque mais informações no campo pesquisa para refinar sua busca."%}</div>');
else if (data.pagination.total_entries == 0)
radios.before('<div class="alert alert-info" role="alert"><strong>{% trans "Não foram encontrados registros com os termos de pesquisa informados." %}</div>');
}
else{
$('#id_nome, #id_q').val('');
}
}).fail(function(data) {
active('nome', atualizar);
});
}
$('#id_tipo').change(function(event) {
if (event.target.selectedIndex == 0) {
$('#id_nome, #id_q').val('');
active('nome');
}
else {
var pk = this[event.target.selectedIndex].value;
$('input[name=autor_related]').closest('.radio').remove();
update_search(pk, false)
}
});
$('.btn-filtrar-autor').click(function(event) {
var pk = $('#id_tipo').val();
update_search(pk);
});
$('input[name=action_user]').change(function(event) {
if (!this.checked)
return;
$('#div_id_username input').prop('readonly', '');
if (event.target.value == 'C') {
$('.new_user_fields, #div_id_username').removeClass('hidden');
$('input[name=username]').val('');
}
else if (event.target.value == 'N') {
$('.new_user_fields').addClass('hidden');
if ($('input[name=username]').attr('data') != '')
$('.radiogroup-status').removeClass('hidden');
if (flag_create) {
$('#div_id_username').addClass('hidden');
}
else {
$('#div_id_username input').prop('readonly', 'readonly');
}
}
else {
$('.radiogroup-status').addClass('hidden');
$('#div_id_username').removeClass('hidden');
$('.new_user_fields').addClass('hidden');
}
if (!flag_create) {
var username = $('input[name=username]');
if (username.length == 1) {
if ((event.target.value == 'A' && username.attr('data') != '' && username[0].value != username.attr('data'))
|| (event.target.value == 'C' && username.attr('data') != ''))
$('.radiogroup-status').removeClass('hidden');
}
}
});
$('input[name=username]').keyup(function(event) {
if (!flag_create)
if (this.getAttribute('data') != '' && this.value != this.getAttribute('data'))
$('.radiogroup-status').removeClass('hidden');
else
$('.radiogroup-status').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)
});
</script>
{% endblock %}

13
sapl/templates/base/layouts.yaml

@ -15,3 +15,16 @@ AppConfig:
- documentos_administrativos sequencia_numeracao painel_aberto - documentos_administrativos sequencia_numeracao painel_aberto
{% trans 'Textos Articulados' %}: {% trans 'Textos Articulados' %}:
- texto_articulado_proposicao texto_articulado_materia texto_articulado_norma - texto_articulado_proposicao texto_articulado_materia texto_articulado_norma
TipoAutor:
{% trans 'Tipo Autor' %}:
- content_type:4 descricao
Autor:
{% trans 'Autor' %}:
- tipo:3 nome
- user:6 cargo
AutorCreate:
{% trans 'Cadastro de Usuários Autores' %}:
- tipo:3 search_autor

20
sapl/templates/base/tipoautor_form.html

@ -0,0 +1,20 @@
{% extends "crud/form.html" %}
{% load i18n %}
{% block extra_js %}
<script type="text/javascript">
$(document).ready(function(){
$('#id_content_type').change(function(event) {
if (event.target.selectedIndex == 0)
$('#id_descricao').val('');
else
$('#id_descricao').val(this[event.target.selectedIndex].text);
});
});
</script>
{% endblock %}

17
sapl/templates/materia/layouts.yaml

@ -47,23 +47,6 @@ AnexadaDetail:
- materia_anexada - materia_anexada
- data_anexacao data_desanexacao - 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: Autoria:
{% trans 'Autoria' %}: {% trans 'Autoria' %}:
- autor primeiro_autor - autor primeiro_autor

81
sapl/templates/rest_framework_docs/base.html

@ -0,0 +1,81 @@
{% load static from staticfiles %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}DRF Docs{% endblock %}</title>
{% block style %}
<link rel="stylesheet" href="{% static "rest_framework_docs/css/style.css" %}">
{% endblock %}
</head>
<body>
{% block github_badge %}
<a href="https://github.com/ekonstantinidis/drf-docs/" class="github-corner" target="_blank">
<svg width="80" height="80" viewBox="0 0 250 250" style="fill:#18bc9c; color:#fff; position: absolute; top: 0; border: 0; right: 0;">
<path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path>
<path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path>
<path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path>
</svg>
</a>
{% endblock %}
<div class="container">
<nav class="navbar navbar-default">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#drfdoc-navbar" aria-expanded="false">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
{% block logo %}<a class="navbar-brand" href="http://www.drfdocs.com/">DRF Docs</a>{% endblock %}
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse" id="drfdoc-navbar">
<form method="get" action="." class="navbar-form navbar-right" role="search">
<div class="form-group">
<input type="text" class="form-control" name="search" value="{{ query }}" placeholder="Search">
</div>
</form>
<ul class="nav navbar-nav navbar-right">
{% block apps_menu %}{% endblock %}
</ul>
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
{% block jumbotron %}
<div class="jumbotron">
<h1>DRF Docs</h1>
<h3>Document Web APIs made with <a href="http://www.django-rest-framework.org/" target="_blank">Django REST Framework</a>.</h3>
</div>
{% endblock %}
{% block content %}{% endblock %}
{% block footer %}
<div class="footer">
<div class="links">
<a href="http://www.iamemmanouil.com"><i class="fa fa-link"></i></a>
<a href="http://www.github.com/ekonstantinidis"><i class="fa fa-github"></i></a>
<a href="http://www.twitter.com/iamemmanouil"><i class="fa fa-twitter"></i></a>
</div>
Copyright © 2016 Emmanouil Konstantinidis.
</div>
{% endblock %}
</div>
<!-- Dist.js - Inlcuded Live API, jQuery, Bootstrap -->
<script type="text/javascript" src="{% static "rest_framework_docs/js/dist.min.js" %}"></script>
</body>
</html>

123
sapl/templates/rest_framework_docs/home.html

@ -0,0 +1,123 @@
{% extends "rest_framework_docs/docs.html" %}
{% block style %}{{block.super}}
<style media="screen">
.lead {
font-size: 14px;
}
</style>
{% endblock %}
{% block logo %}
<a class="navbar-brand" href="http://sapl31demo.interlegis.leg.br">DRF Docs - SAPL - Sistema de Apoio ao Processo Legislativo</a>
{% endblock %}
{% block title %}SAPL - Sistema de Apoio ao Processo Legislativo{% endblock %}
{% block jumbotron %}
<div class="jumbotron">
<h1>SAPL </h1>
<h3>Sistema de Apoio ao Processo Legislativo.</h3>
</div>
{% endblock %}
{% block apps_menu %}
{% regroup endpoints by name_parent as endpoints_grouped %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Jump To <span class="caret"></span></a>
<ul class="dropdown-menu">
{% for group in endpoints_grouped %}
<li><a href="#{{ group.grouper|lower }}-group">{{ group.grouper }}</a></li>
{% endfor %}
</ul>
</li>
{% endblock %}
{% block content %}
{% regroup endpoints by name_parent as endpoints_grouped %}
{% if endpoints_grouped %}
{% for group in endpoints_grouped %}
<h1 id="{{ group.grouper|lower }}-group">{{group.grouper}}</h1>
<div class="panel-group" role="tablist">
{% for endpoint in group.list %}
<div class="panel panel-default endpoint">
<div class="panel-heading" role="tab" data-toggle="collapse" data-target="#{{ endpoint.path|slugify }}">
<div class="row">
<div class="col-md-7">
<h4 class="panel-title title">
<i class="fa fa-link"></i> {{ endpoint.path }}
</h4>
</div>
<div class="col-md-5">
<ul class="list-inline methods">
{% for method in endpoint.allowed_methods %}
<li class="method {{ method|lower }}">{{ method }}</li>
{% endfor %}
<li class="method plug"
data-toggle="modal"
data-path="{{ endpoint.path }}"
data-methods="{{ endpoint.allowed_methods }}"
data-permissions="{{ endpoint.permissions }}"
data-fields="{{ endpoint.fields_json }}">
<i class="fa fa-plug"></i></li>
</ul>
</div>
</div>
</div>
<div id="{{ endpoint.path|slugify }}" class="panel-collapse collapse" role="tabpanel">
<div class="panel-body">
{% if endpoint.docstring %}
<pre class="lead">{{ endpoint.docstring}}</pre>
{% endif %}
{% if endpoint.errors %}
<div class="alert alert-danger" role="alert">Oops! There was something wrong with {{ endpoint.errors }}. Please check your code.</div>
{% endif %}
{% if endpoint.fields %}
<p class="fields-desc">Fields:</p>
<ul class="list fields">
{% for field in endpoint.fields %}
<li class="field">{{ field.name }}: {{ field.type }} {% if field.required %}<span class="label label-primary label-required" title="Required">R</span>{% endif %}</li>
{% endfor %}
</ul>
{% elif not endpoint.errors %}
<p>No fields.</p>
{% endif %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
{% elif not query %}
<h2 class="text-center">There are currently no api endpoints to document.</h2>
{% else %}
<h2 class="text-center">No endpoints found for {{ query }}.</h2>
{% endif %}
<!-- Modal -->
<div class="modal fade api-modal" id="liveAPIModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Live API Endpoints <span class="label label-info">Beta</span></h4>
</div>
<div id="liveAPIEndpoints"></div>
</div>
</div>
</div>
{% endblock %}

7
sapl/templates/sistema.html

@ -3,10 +3,12 @@
{% block base_content %} {% block base_content %}
<h2>Configuração Inicial</h2> <h2>Configuração Gerais</h2>
<div class="row"> <div class="row">
<div class="col-md-6"><a href="{% url 'sapl.base:casalegislativa_list' %}" class="btn btn-link">Casa Legislativa</a></div> <div class="col-md-6"><a href="{% url 'sapl.base:casalegislativa_list' %}" class="btn btn-link">Casa Legislativa</a></div>
<div class="col-md-6"><a href="{% url 'sapl.base:autor_list' %}" class="btn btn-link">Autor</a></div>
<div class="col-md-6"><a href="{% url 'sapl.base:appconfig_list' %}" class="btn btn-link">Configurações da Aplicação</a></div> <div class="col-md-6"><a href="{% url 'sapl.base:appconfig_list' %}" class="btn btn-link">Configurações da Aplicação</a></div>
<div class="col-md-6"><a href="{% url 'sapl.base:tipoautor_list' %}" class="btn btn-link">Tipo de Autor</a></div>
</div> </div>
<hr /> <hr />
@ -49,7 +51,6 @@
<h2>Módulo Proposições</h2> <h2>Módulo Proposições</h2>
<div class="row"> <div class="row">
<div class="col-md-6"><a href="{% url 'sapl.materia:tipoproposicao_list' %}" class="btn btn-link">Tipo de Proposição</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:tipoproposicao_list' %}" class="btn btn-link">Tipo de Proposição</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:autor_list' %}" class="btn btn-link">Autor</a></div>
</div> </div>
<hr /> <hr />
@ -57,12 +58,10 @@
<div class="row"> <div class="row">
<div class="col-md-6"><a href="{% url 'sapl.materia:tipomaterialegislativa_list' %}" class="btn btn-link">Tipo de Matéria Legislativa</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:tipomaterialegislativa_list' %}" class="btn btn-link">Tipo de Matéria Legislativa</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:regimetramitacao_list' %}" class="btn btn-link">Regime de Tramitação</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:regimetramitacao_list' %}" class="btn btn-link">Regime de Tramitação</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:tipoautor_list' %}" class="btn btn-link">Tipo de Autor</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:tipodocumento_list' %}" class="btn btn-link">Tipo de Documento</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:tipodocumento_list' %}" class="btn btn-link">Tipo de Documento</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:tipofimrelatoria_list' %}" class="btn btn-link">Tipo de fim de Relatoria</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:tipofimrelatoria_list' %}" class="btn btn-link">Tipo de fim de Relatoria</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:unidadetramitacao_list' %}" class="btn btn-link">Unidade de Tramitação</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:unidadetramitacao_list' %}" class="btn btn-link">Unidade de Tramitação</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:origem_list' %}" class="btn btn-link">Origem</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:origem_list' %}" class="btn btn-link">Origem</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:autor_list' %}" class="btn btn-link">Autor</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:statustramitacao_list' %}" class="btn btn-link">Status da Tramitação</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:statustramitacao_list' %}" class="btn btn-link">Status da Tramitação</a></div>
<div class="col-md-6"><a href="{% url 'sapl.materia:orgao_list' %}" class="btn btn-link">Órgão</a></div> <div class="col-md-6"><a href="{% url 'sapl.materia:orgao_list' %}" class="btn btn-link">Órgão</a></div>
</div> </div>

6
sapl/test_urls.py

@ -5,6 +5,7 @@ from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import transaction from django.db import transaction
from django.utils.translation import string_concat from django.utils.translation import string_concat
from django.utils.translation import string_concat
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
import pytest import pytest
@ -148,6 +149,10 @@ def test_crudaux_list_do_crud_esta_na_pagina_sistema(url_item, admin_client):
""" % (url, app_name) """ % (url, app_name)
apps_url_patterns_prefixs_and_users = { apps_url_patterns_prefixs_and_users = {
'api': {
'prefixs': [
'/api/',
]},
'base': { 'base': {
'users': {'operador_geral': ['/sistema']}, 'users': {'operador_geral': ['/sistema']},
'prefixs': [ 'prefixs': [
@ -434,7 +439,6 @@ def test_permissions_urls_for_users_by_apps(url_item, client):
assert app in apps_url_patterns_prefixs_and_users, """ assert app in apps_url_patterns_prefixs_and_users, """
O app_label (%s) associado a url (%s) não está na base de testes. O app_label (%s) associado a url (%s) não está na base de testes.
%s
""" % (app_name, url) """ % (app_name, url)
if 'users' not in apps_url_patterns_prefixs_and_users[app]: if 'users' not in apps_url_patterns_prefixs_and_users[app]:

3
sapl/urls.py

@ -20,6 +20,7 @@ from django.contrib import admin
from django.views.generic.base import TemplateView from django.views.generic.base import TemplateView
from django.views.static import serve as view_static_server from django.views.static import serve as view_static_server
import sapl.api.urls
import sapl.base.urls import sapl.base.urls
import sapl.comissoes.urls import sapl.comissoes.urls
import sapl.compilacao.urls import sapl.compilacao.urls
@ -50,6 +51,8 @@ urlpatterns = [
# must come at the end # must come at the end
# so that base /sistema/ url doesn't capture its children # so that base /sistema/ url doesn't capture its children
url(r'', include(sapl.base.urls)), url(r'', include(sapl.base.urls)),
url(r'', include(sapl.api.urls)),
] ]

95
sapl/utils.py

@ -4,16 +4,20 @@ from unicodedata import normalize as unicodedata_normalize
import hashlib import hashlib
import logging import logging
from crispy_forms.helper import FormHelper
from crispy_forms.layout import HTML, Button
from django import forms from django import forms
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.contenttypes.fields import GenericRelation
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import PermissionDenied, ValidationError from django.core.exceptions import PermissionDenied, ValidationError
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
from floppyforms import ClearableFileInput from floppyforms import ClearableFileInput
from sapl.crispy_layout_mixin import SaplFormLayout, form_actions, to_row
import magic import magic
from sapl.settings import BASE_DIR from sapl.settings import BASE_DIR
@ -55,6 +59,97 @@ 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('<br /><br />'))
# 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('<a href="{{ view.cancel_url }}"'
' class="btn btn-inverse">Cancelar</a>')]))
class SaplGenericRelation(GenericRelation):
"""
Extenção da class GenericRelation para implmentar o atributo fields_search
fields_search é uma tupla de tuplas de dois strings no padrão de construção
de campos porém com [Field Lookups][ref_1]
exemplo:
[No Model Parlamentar em][ref_2] existe a implementação dessa
classe no atributo autor. Parlamentar possui três informações
relevantes para buscas realacionadas a Autor:
- nome_completo;
- nome_parlamentar; e
- filiacao__partido__sigla
que devem ser pesquisados, coincidentemente
pelo FieldLookup __icontains
portanto a estrutura de fields_search seria:
fields_search=(
('nome_completo', '__icontains'),
('nome_parlamentar', '__icontains'),
('filiacao__partido__sigla', '__icontains'),
)
[ref_1]: https://docs.djangoproject.com/el/1.10/topics/db/queries/#field-lookups
[ref_2]: https://github.com/interlegis/sapl/blob/master/sapl/parlamentares/models.py
"""
def __init__(self, to, fields_search=(), **kwargs):
assert 'related_query_name' in kwargs, _(
'SaplGenericRelation não pode ser instanciada sem '
'related_query_name.')
assert fields_search, _(
'SaplGenericRelation não pode ser instanciada sem fields_search.')
for field in fields_search:
# descomente para ver todas os campos que são elementos de busca
#print(kwargs['related_query_name'], field)
assert isinstance(field, (tuple, list)), _(
'fields_search deve ser um array de tuplas ou listas.')
assert len(field) == 2, _(
'cada tupla de fields_search deve possuir duas strins')
# TODO implementar assert para validar campos do Model e lookups
self.fields_search = fields_search
super().__init__(to, **kwargs)
class ImageThumbnailFileInput(ClearableFileInput): class ImageThumbnailFileInput(ClearableFileInput):
template_name = 'floppyforms/image_thumbnail.html' template_name = 'floppyforms/image_thumbnail.html'

28
scripts/inicializa_grupos_autorizacoes.py

@ -10,6 +10,7 @@ if True:
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from sapl.base.models import Autor
class InicializaGruposAutorizacoes(): class InicializaGruposAutorizacoes():
@ -108,20 +109,23 @@ class InicializaGruposAutorizacoes():
self.cria_usuario(nome_usuario, grupo_geral) self.cria_usuario(nome_usuario, grupo_geral)
# Autor # Autor
perms_autor = []
perms_autor.append(Permission.objects.get(name='Can add Proposição'))
perms_autor.append(
Permission.objects.get(name='Can change Proposição'))
perms_autor.append(
Permission.objects.get(name='Can delete Proposição'))
# Configura Permissoes Autor
grupo = self.cria_ou_reseta_grupo('Autor') grupo = self.cria_ou_reseta_grupo('Autor')
for p in perms_autor:
grupo.permissions.add(p)
nome_usuario = 'operador_autor' list(map(lambda permissao: grupo.permissions.add(permissao),
self.cria_usuario(nome_usuario, grupo) list(Permission.objects.filter(
content_type=ContentType.objects.get_by_natural_key(
app_label='materia', model='proposicao')))))
"""
Mesmo para teste, um usuário com perfil Autor criado via /admin
não deverá ser criado pois esse é um papel do operador_geral fazer
nas tabelas auxiliares.
A tentativa de acesso a qualquer container (hoje apenas proposições)
do SAPL de Usuários com perfil Autor mas sem um Autor cadastrado
nas tabelas auxiliares será negado e notificado via front-end.
"""
# nome_usuario = 'operador_autor'
# self.cria_usuario(nome_usuario, grupo)
def __call__(self): def __call__(self):
self.cria_grupos_permissoes() self.cria_grupos_permissoes()

Loading…
Cancel
Save