mirror of https://github.com/interlegis/sapl.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
528 lines
18 KiB
528 lines
18 KiB
import logging
|
|
|
|
from django import apps
|
|
from django.conf import settings
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db.models import Q
|
|
from django.db.models.fields.files import FileField
|
|
from django.utils.decorators import classonlymethod
|
|
from django.utils.text import capfirst
|
|
from django.utils.translation import ugettext_lazy as _
|
|
import django_filters
|
|
from django_filters.filters import CharFilter
|
|
from django_filters.rest_framework.backends import DjangoFilterBackend
|
|
from django_filters.rest_framework.filterset import FilterSet
|
|
from django_filters.utils import resolve_field
|
|
from rest_framework import serializers as rest_serializers
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from rest_framework.viewsets import ModelViewSet
|
|
|
|
from sapl.api.forms import SaplFilterSetMixin
|
|
from sapl.api.permissions import SaplModelPermissions
|
|
from sapl.api.serializers import ChoiceSerializer
|
|
from sapl.base.models import Autor, AppConfig, DOC_ADM_OSTENSIVO
|
|
from sapl.materia.models import Proposicao, TipoMateriaLegislativa,\
|
|
MateriaLegislativa, Tramitacao
|
|
from sapl.parlamentares.models import Parlamentar
|
|
from sapl.protocoloadm.models import DocumentoAdministrativo,\
|
|
DocumentoAcessorioAdministrativo, TramitacaoAdministrativo, Anexado
|
|
from sapl.sessao.models import SessaoPlenaria, ExpedienteSessao
|
|
from sapl.utils import models_with_gr_for_model, choice_anos_com_sessaoplenaria
|
|
|
|
|
|
class BusinessRulesNotImplementedMixin:
|
|
def create(self, request, *args, **kwargs):
|
|
raise Exception(_("POST Create não implementado"))
|
|
|
|
def update(self, request, *args, **kwargs):
|
|
raise Exception(_("PUT and PATCH não implementado"))
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
raise Exception(_("DELETE Delete não implementado"))
|
|
|
|
|
|
class SaplApiViewSet(ModelViewSet):
|
|
filter_backends = (DjangoFilterBackend,)
|
|
|
|
|
|
class SaplApiViewSetConstrutor():
|
|
|
|
_built_sets = {}
|
|
|
|
@classonlymethod
|
|
def get_class_for_model(cls, model):
|
|
return cls._built_sets[model._meta.app_config][model]
|
|
|
|
@classonlymethod
|
|
def build_class(cls):
|
|
import inspect
|
|
from sapl.api import serializers
|
|
|
|
# Carrega todas as classes de sapl.api.serializers que possuam
|
|
# "Serializer" como Sufixo.
|
|
serializers_classes = inspect.getmembers(serializers)
|
|
serializers_classes = {i[0]: i[1] for i in filter(
|
|
lambda x: x[0].endswith('Serializer'),
|
|
serializers_classes
|
|
)}
|
|
|
|
# Carrega todas as classes de sapl.api.forms que possuam
|
|
# "FilterSet" como Sufixo.
|
|
from sapl.api import forms
|
|
filters_classes = inspect.getmembers(forms)
|
|
filters_classes = {i[0]: i[1] for i in filter(
|
|
lambda x: x[0].endswith('FilterSet'),
|
|
filters_classes
|
|
)}
|
|
|
|
built_sets = {}
|
|
|
|
def build(_model):
|
|
object_name = _model._meta.object_name
|
|
|
|
# Caso Exista, pega a classe sapl.api.serializers.{model}Serializer
|
|
serializer_name = '{model}Serializer'.format(model=object_name)
|
|
_serializer_class = serializers_classes.get(serializer_name, None)
|
|
|
|
# Caso Exista, pega a classe sapl.api.forms.{model}FilterSet
|
|
filter_name = '{model}FilterSet'.format(model=object_name)
|
|
_filter_class = filters_classes.get(filter_name, None)
|
|
|
|
def create_class():
|
|
# Define uma classe padrão para serializer caso não tenha sido
|
|
# criada a classe sapl.api.serializers.{model}Serializer
|
|
class SaplSerializer(rest_serializers.ModelSerializer):
|
|
class Meta:
|
|
model = _model
|
|
fields = '__all__'
|
|
|
|
# Define uma classe padrão para filtro caso não tenha sido
|
|
# criada a classe sapl.api.forms.{model}FilterSet
|
|
class SaplFilterSet(SaplFilterSetMixin):
|
|
class Meta(SaplFilterSetMixin.Meta):
|
|
model = _model
|
|
|
|
# Define uma classe padrão ModelViewSet de DRF
|
|
class ModelSaplViewSet(SaplApiViewSet):
|
|
queryset = _model.objects.all()
|
|
|
|
# Utiliza o filtro customizado pela classe
|
|
# sapl.api.forms.{model}FilterSet
|
|
# ou utiliza o trivial SaplFilterSet definido acima
|
|
filter_class = _filter_class \
|
|
if _filter_class else SaplFilterSet
|
|
|
|
# Utiliza o serializer customizado pela classe
|
|
# sapl.api.serializers.{model}Serializer
|
|
# ou utiliza o trivial SaplSerializer definido acima
|
|
serializer_class = _serializer_class \
|
|
if _serializer_class else SaplSerializer
|
|
|
|
return ModelSaplViewSet
|
|
|
|
viewset = create_class()
|
|
viewset.__name__ = '%sModelSaplViewSet' % _model.__name__
|
|
return viewset
|
|
|
|
apps_sapl = [apps.apps.get_app_config(
|
|
n[5:]) for n in settings.SAPL_APPS]
|
|
for app in apps_sapl:
|
|
cls._built_sets[app] = {}
|
|
for model in app.get_models():
|
|
cls._built_sets[app][model] = build(model)
|
|
|
|
|
|
SaplApiViewSetConstrutor.build_class()
|
|
|
|
"""
|
|
1. Constroi uma rest_framework.viewsets.ModelViewSet para
|
|
todos os models de todas as apps do sapl
|
|
2. Define DjangoFilterBackend como ferramenta de filtro dos campos
|
|
3. Define Serializer como a seguir:
|
|
3.1 - Define um Serializer genérico para cada módel
|
|
3.2 - Recupera Serializer customizado em sapl.api.serializers
|
|
3.3 - Para todo model é opcional a existência de
|
|
sapl.api.serializers.{model}Serializer.
|
|
Caso não seja definido um Serializer customizado, utiliza-se o trivial
|
|
4. Define um FilterSet como a seguir:
|
|
4.1 - Define um FilterSet genérico para cada módel
|
|
4.2 - Recupera FilterSet customizado em sapl.api.forms
|
|
4.3 - Para todo model é opcional a existência de
|
|
sapl.api.forms.{model}FilterSet.
|
|
Caso não seja definido um FilterSet customizado, utiliza-se o trivial
|
|
4.4 - todos os campos que aceitam lookup 'exact'
|
|
podem ser filtrados por default
|
|
|
|
5. SaplApiViewSetConstrutor não cria padrões e/ou exige conhecimento alem dos
|
|
exigidos pela DRF.
|
|
|
|
6. As rotas são criadas seguindo nome da app e nome do model
|
|
http://localhost:9000/api/{applabel}/{model_name}/
|
|
e seguem as variações definidas em:
|
|
https://www.django-rest-framework.org/api-guide/routers/#defaultrouter
|
|
|
|
7. Todas as viewsets construídas por SaplApiViewSetConstrutor e suas rotas
|
|
(paginate list, detail, edit, create, delete)
|
|
bem como testes em ambiente de desenvolvimento podem ser conferidas em:
|
|
http://localhost:9000/api/
|
|
desde que settings.DEBUG=True
|
|
|
|
**SaplSetViews** é um dict de dicts de models conforme:
|
|
{
|
|
...
|
|
|
|
'audiencia': {
|
|
'tipoaudienciapublica': TipoAudienciaPublicaViewSet,
|
|
'audienciapublica': AudienciaPublicaViewSet,
|
|
'anexoaudienciapublica': AnexoAudienciaPublicaViewSet
|
|
|
|
...
|
|
|
|
},
|
|
|
|
...
|
|
|
|
'base': {
|
|
'casalegislativa': CasaLegislativaViewSet,
|
|
'appconfig': AppConfigViewSet,
|
|
|
|
...
|
|
|
|
}
|
|
|
|
...
|
|
|
|
}
|
|
"""
|
|
|
|
# Toda Classe construida acima, pode ser redefinida e aplicado quaisquer
|
|
# das possibilidades para uma classe normal criada a partir de
|
|
# rest_framework.viewsets.ModelViewSet conforme exemplo para a classe autor
|
|
|
|
# decorator para recuperar e transformar o default
|
|
|
|
|
|
class customize(object):
|
|
def __init__(self, model):
|
|
self.model = model
|
|
|
|
def __call__(self, cls):
|
|
|
|
class _SaplApiViewSet(
|
|
cls,
|
|
SaplApiViewSetConstrutor._built_sets[
|
|
self.model._meta.app_config][self.model]
|
|
):
|
|
pass
|
|
|
|
if hasattr(_SaplApiViewSet, 'build'):
|
|
_SaplApiViewSet = _SaplApiViewSet.build()
|
|
|
|
SaplApiViewSetConstrutor._built_sets[
|
|
self.model._meta.app_config][self.model] = _SaplApiViewSet
|
|
return _SaplApiViewSet
|
|
|
|
|
|
# Customização para AutorViewSet com implementação de actions específicas
|
|
|
|
|
|
@customize(Autor)
|
|
class _AutorViewSet:
|
|
"""
|
|
Neste exemplo de customização do que foi criado em
|
|
SaplApiViewSetConstrutor além do ofertado por
|
|
rest_framework.viewsets.ModelViewSet, dentre outras customizações
|
|
possíveis, foi adicionado as rotas referentes aos relacionamentos genéricos
|
|
|
|
* padrão de ModelViewSet
|
|
/api/base/autor/ POST - create
|
|
/api/base/autor/ GET - list
|
|
/api/base/autor/{pk}/ GET - detail
|
|
/api/base/autor/{pk}/ PUT - update
|
|
/api/base/autor/{pk}/ PATCH - partial_update
|
|
/api/base/autor/{pk}/ DELETE - destroy
|
|
|
|
* rotas desta classe local:
|
|
/api/base/autor/parlamentar
|
|
devolve apenas autores que são parlamentares
|
|
/api/base/autor/comissao
|
|
devolve apenas autores que são comissões
|
|
/api/base/autor/bloco
|
|
devolve apenas autores que são blocos parlamentares
|
|
/api/base/autor/bancada
|
|
devolve apenas autores que são bancadas parlamentares
|
|
/api/base/autor/frente
|
|
devolve apenas autores que são Frene parlamentares
|
|
/api/base/autor/orgao
|
|
devolve apenas autores que são Órgãos
|
|
|
|
|
|
"""
|
|
|
|
def list_for_content_type(self, content_type):
|
|
qs = self.get_queryset()
|
|
qs = qs.filter(content_type=content_type)
|
|
|
|
page = self.paginate_queryset(qs)
|
|
if page is not None:
|
|
serializer = self.serializer_class(page, many=True)
|
|
return self.get_paginated_response(serializer.data)
|
|
|
|
serializer = self.get_serializer(page, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@classonlymethod
|
|
def build(cls):
|
|
|
|
models_with_gr_for_autor = models_with_gr_for_model(Autor)
|
|
|
|
for _model in models_with_gr_for_autor:
|
|
|
|
@action(detail=False, name=_model._meta.model_name)
|
|
def actionclass(self, request, *args, **kwargs):
|
|
model = getattr(self, self.action)._AutorViewSet__model
|
|
|
|
content_type = ContentType.objects.get_for_model(model)
|
|
return self.list_for_content_type(content_type)
|
|
|
|
func = actionclass
|
|
func.mapping['get'] = func.kwargs['name']
|
|
func.url_name = func.kwargs['name']
|
|
func.url_path = func.kwargs['name']
|
|
func.__model = _model
|
|
|
|
setattr(cls, _model._meta.model_name, func)
|
|
return cls
|
|
|
|
|
|
@customize(Parlamentar)
|
|
class _ParlamentarViewSet:
|
|
@action(detail=True)
|
|
def proposicoes(self, request, *args, **kwargs):
|
|
"""
|
|
Lista de proposições públicas de parlamentar específico
|
|
|
|
:param int id: - Identificador do parlamentar que se quer recuperar as proposições
|
|
:return: uma lista de proposições
|
|
"""
|
|
# /api/parlamentares/parlamentar/{id}/proposicoes/
|
|
# recupera proposições enviadas e incorporadas do parlamentar
|
|
# deve coincidir com
|
|
# /parlamentar/{pk}/proposicao
|
|
content_type = ContentType.objects.get_for_model(Parlamentar)
|
|
|
|
qs = Proposicao.objects.filter(
|
|
data_envio__isnull=False,
|
|
data_recebimento__isnull=False,
|
|
cancelado=False,
|
|
autor__object_id=kwargs['pk'],
|
|
autor__content_type=content_type
|
|
)
|
|
|
|
page = self.paginate_queryset(qs)
|
|
if page is not None:
|
|
serializer = SaplApiViewSetConstrutor.get_class_for_model(
|
|
Proposicao).serializer_class(page, many=True)
|
|
return self.get_paginated_response(serializer.data)
|
|
|
|
serializer = self.get_serializer(page, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
@customize(Proposicao)
|
|
class _ProposicaoViewSet():
|
|
"""
|
|
list:
|
|
Retorna lista de Proposições
|
|
|
|
* Permissões:
|
|
|
|
* Usuário Dono:
|
|
* Pode listar todas suas Proposições
|
|
|
|
* Usuário Conectado ou Anônimo:
|
|
* Pode listar todas as Proposições incorporadas
|
|
|
|
retrieve:
|
|
Retorna uma proposição passada pelo 'id'
|
|
|
|
* Permissões:
|
|
|
|
* Usuário Dono:
|
|
* Pode recuperar qualquer de suas Proposições
|
|
|
|
* Usuário Conectado ou Anônimo:
|
|
* Pode recuperar qualquer das proposições incorporadas
|
|
|
|
"""
|
|
class ProposicaoPermission(SaplModelPermissions):
|
|
def has_permission(self, request, view):
|
|
if request.method == 'GET':
|
|
return True
|
|
# se a solicitação é list ou detail, libera o teste de permissão
|
|
# e deixa o get_queryset filtrar de acordo com a regra de
|
|
# visibilidade das proposições, ou seja:
|
|
# 1. proposição incorporada é proposição pública
|
|
# 2. não incorporada só o autor pode ver
|
|
else:
|
|
perm = super().has_permission(request, view)
|
|
return perm
|
|
# não é list ou detail, então passa pelas regras de permissão e,
|
|
# depois disso ainda passa pelo filtro de get_queryset
|
|
|
|
permission_classes = (ProposicaoPermission, )
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
|
|
q = Q(data_recebimento__isnull=False, object_id__isnull=False)
|
|
if not self.request.user.is_anonymous():
|
|
q |= Q(autor__user=self.request.user)
|
|
|
|
qs = qs.filter(q)
|
|
return qs
|
|
|
|
|
|
@customize(MateriaLegislativa)
|
|
class _MateriaLegislativaViewSet:
|
|
|
|
@action(detail=True, methods=['GET'])
|
|
def ultima_tramitacao(self, request, *args, **kwargs):
|
|
|
|
materia = self.get_object()
|
|
if not materia.tramitacao_set.exists():
|
|
return Response({})
|
|
|
|
ultima_tramitacao = materia.tramitacao_set.last()
|
|
|
|
serializer_class = SaplApiViewSetConstrutor.get_class_for_model(
|
|
Tramitacao).serializer_class(ultima_tramitacao)
|
|
|
|
return Response(serializer_class.data)
|
|
|
|
@action(detail=True, methods=['GET'])
|
|
def anexadas(self, request, *args, **kwargs):
|
|
self.queryset = self.get_object().anexadas.all()
|
|
return self.list(request, *args, **kwargs)
|
|
|
|
|
|
@customize(TipoMateriaLegislativa)
|
|
class _TipoMateriaLegislativaViewSet:
|
|
|
|
@action(detail=True, methods=['POST'])
|
|
def change_position(self, request, *args, **kwargs):
|
|
result = {
|
|
'status': 200,
|
|
'message': 'OK'
|
|
}
|
|
d = request.data
|
|
if 'pos_ini' in d and 'pos_fim' in d:
|
|
if d['pos_ini'] != d['pos_fim']:
|
|
pk = kwargs['pk']
|
|
TipoMateriaLegislativa.objects.reposicione(pk, d['pos_fim'])
|
|
|
|
return Response(result)
|
|
|
|
|
|
@customize(DocumentoAdministrativo)
|
|
class _DocumentoAdministrativoViewSet:
|
|
|
|
class DocumentoAdministrativoPermission(SaplModelPermissions):
|
|
def has_permission(self, request, view):
|
|
if request.method == 'GET':
|
|
comportamento = AppConfig.attr('documentos_administrativos')
|
|
if comportamento == DOC_ADM_OSTENSIVO:
|
|
return True
|
|
"""
|
|
Diante da lógica implementada na manutenção de documentos
|
|
administrativos:
|
|
- Se o comportamento é doc adm ostensivo, deve passar pelo
|
|
teste de permissões sem avaliá-las
|
|
- se o comportamento é doc adm restritivo, deve passar pelo
|
|
teste de permissões avaliando-as
|
|
"""
|
|
return super().has_permission(request, view)
|
|
|
|
permission_classes = (DocumentoAdministrativoPermission, )
|
|
|
|
def get_queryset(self):
|
|
"""
|
|
mesmo tendo passado pelo teste de permissões, deve ser filtrado,
|
|
pelo campo restrito. Sendo este igual a True, disponibilizar apenas
|
|
a um usuário conectado. Apenas isso, sem critérios outros de permissão,
|
|
conforme implementado em DocumentoAdministrativoCrud
|
|
"""
|
|
qs = super().get_queryset()
|
|
|
|
if self.request.user.is_anonymous():
|
|
qs = qs.exclude(restrito=True)
|
|
return qs
|
|
|
|
|
|
@customize(DocumentoAcessorioAdministrativo)
|
|
class _DocumentoAcessorioAdministrativoViewSet:
|
|
|
|
permission_classes = (
|
|
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, )
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
|
|
if self.request.user.is_anonymous():
|
|
qs = qs.exclude(documento__restrito=True)
|
|
return qs
|
|
|
|
|
|
@customize(TramitacaoAdministrativo)
|
|
class _TramitacaoAdministrativoViewSet(BusinessRulesNotImplementedMixin):
|
|
# TODO: Implementar regras de manutenção das tramitações de docs adms
|
|
|
|
permission_classes = (
|
|
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, )
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
|
|
if self.request.user.is_anonymous():
|
|
qs = qs.exclude(documento__restrito=True)
|
|
return qs
|
|
|
|
|
|
@customize(Anexado)
|
|
class _AnexadoViewSet(BusinessRulesNotImplementedMixin):
|
|
|
|
permission_classes = (
|
|
_DocumentoAdministrativoViewSet.DocumentoAdministrativoPermission, )
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
|
|
if self.request.user.is_anonymous():
|
|
qs = qs.exclude(documento__restrito=True)
|
|
return qs
|
|
|
|
|
|
@customize(SessaoPlenaria)
|
|
class _SessaoPlenariaViewSet:
|
|
|
|
@action(detail=False)
|
|
def years(self, request, *args, **kwargs):
|
|
years = choice_anos_com_sessaoplenaria()
|
|
|
|
serializer = ChoiceSerializer(years, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True)
|
|
def expedientes(self, request, *args, **kwargs):
|
|
|
|
sessao = self.get_object()
|
|
|
|
page = self.paginate_queryset(sessao.expedientesessao_set.all())
|
|
if page is not None:
|
|
serializer = SaplApiViewSetConstrutor.get_class_for_model(
|
|
ExpedienteSessao).serializer_class(page, many=True)
|
|
return self.get_paginated_response(serializer.data)
|
|
|
|
serializer = self.get_serializer(page, many=True)
|
|
return Response(serializer.data)
|
|
|