Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 191 additions & 1 deletion drfautoapi/drfautoapi.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
from collections import OrderedDict
from functools import cached_property
import django_filters
import importlib
import inspect
import logging
import re

from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields.jsonb import JSONField
from django.contrib.auth import get_user_model
from django.db.models.base import ModelBase
from django.db.models.fields import TextField, CharField
from django.db.models.fields.files import FileField
from django.db.models.fields.related import ManyToManyField
from django.template.defaultfilters import capfirst
from django.utils.translation import ugettext_lazy as _
from django.urls.conf import path
from django.urls import reverse
from django_filters.constants import ALL_FIELDS, EMPTY_VALUES
from django_filters.fields import ModelMultipleChoiceField
from django_filters.filters import CharFilter
from django_filters.filterset import FilterSet
from django_filters.rest_framework.backends import DjangoFilterBackend
from django_filters.utils import resolve_field, get_all_model_fields
from rest_framework import serializers as rest_serializers
from rest_framework.relations import ManyRelatedField
from rest_framework.response import Response
from rest_framework.routers import DefaultRouter
from rest_framework.viewsets import ModelViewSet
Expand Down Expand Up @@ -85,6 +89,7 @@ def clean(self, value):
class ApiFilterSetMixin(FilterSet):

o = CharFilter(method='filter_o')
id__in = CharFilter(method='filter_id__in')

class Meta:
fields = '__all__'
Expand All @@ -109,6 +114,13 @@ class Meta:
},
}

def filter_id__in(self, queryset, name, value):
try:
ids = [int(v.strip()) for v in value.split(',')]
return queryset.filter(id__in=ids)
except:
return queryset

def filter_o(self, queryset, name, value):
try:
return queryset.order_by(
Expand Down Expand Up @@ -434,3 +446,181 @@ class _ApiViewSet(
return _ApiViewSet


class DrfAutoApiSerializerMixin(rest_serializers.ModelSerializer):
"""
Mixin de Serializer que implementa expansão dinâmica de campos
via parâmetros de query string:
- `expand`: expande os campos informados
- `include`: inclui apenas os campos informados
- `exclude`: exclui os campos informados

Exemplo:
- ?expand=campo1;campo2.sub_campo1,sub_campo2;campo3.sub_campo1.sub_sub_campo1,sub_sub_campo2
- ?include=campo1;campo2.sub_campo1,sub_campo2;campo3.sub_campo1.sub_sub_campo1,sub_sub_campo2
- ?exclude=campo1;campo2.sub_campo1,sub_campo2;campo3.sub_campo1.sub_sub_campo1,sub_sub_campo2
- ?expand=campo1&include=campo1.id,name&exclude=campo1.secret_field

Onde:
- campo1, campo2, campo3 são campos do model raiz
- sub_campo1, sub_campo2 são campos relacionados do campo2
- sub_sub_campo1, sub_sub_campo2 são campos relacionados do sub_campo1

Ou seja:
";" separa campos independentes do nível atual
"," separa campos relacionados do mesmo campo pai
"." indica o nível de profundidade dos campos relacionados

e ainda:
- `expand` pode ser usado para expansão direta, ou seja, campo1.sub_campo1 já expande campo1
- `expand`, `include` e `exclude` podem ser usados juntos na mesma requisição
- `include` e `exclude` só funcionam em subniveis se o campo pai estiver expandido
- `include` tem precedência sobre `exclude` e já remove todo o resto
- `exclude` remove o campo do resultado final, mesmo que esteja em `include`
- Se nenhum dos parâmetros for informado, nenhum campo será expandido
- Filtros da API, paginação (`page` e `page_size`) e ordenação (`o`)
podem ser usados normalmente para filtrar os resultados

Atenção:
- A expansão não é aplicada para o model User do Django
- A expansão não é aplicada para campos customizados que utilizam SerializerMethodField
- Uma exceção é lançada e registrada no log caso ocorra algum erro na expansão de algum campo,
inclusive devido a recursão infinita
- A expansão automática de todos os campos relacionados (expand=all) está desabilitada
por necessidade de controle mais refinado.
- A expansão de campos relacionados ForeignKey e OneToOne é suportada.
- A expansão de campos relacionados ManyToMany é suportada.
- A expansão de campos relacionados reversos (related_name) não é suportada, mas pode ser implementada manualmente no serializer customizado, ou vir a ser implementada no futuro.
"""

__str__ = rest_serializers.SerializerMethodField()

class Meta:
fields = '__all__'

def get___str__(self, obj) -> str:
return str(obj)

@cached_property
def user_model(self):
return get_user_model()

def get_control_fields(self, control_field='expand'):
request = self.context.get('request', None)
if request:
param = request.query_params.get(control_field, '')
param = [e.strip() for e in param.split(';') if e.strip()]
param = [e.split('.') for e in param]
return param
return []

def get_fields(self):
fields = super().get_fields()

if not hasattr(self.root, 'drf_expand_fields'):
self.root.drf_expand_fields = self.get_control_fields('expand')
self.root.drf_include_fields = self.get_control_fields('include')
self.root.drf_exclude_fields = self.get_control_fields('exclude')
if not (self.root.drf_expand_fields or self.root.drf_include_fields or self.root.drf_exclude_fields):
return fields

model = self.Meta.model
expand_fields = self.root.drf_expand_fields
include_fields = self.root.drf_include_fields
exclude_fields = self.root.drf_exclude_fields

expand_all = False #['all'] in expand_fields

#if expand_all:
# request = self.context.get('request', None)
# user = getattr(request, 'user', None)
# expand_all = user and user.is_superuser

def parents(nd):
if not nd:
return []
return parents(nd.parent) + [nd.field_name]

sources = parents(self)
sources = list(filter(lambda x: x, sources))

if expand_fields:
exps = []
for exp in expand_fields:
if len(exp) > len(sources) and exp[0:len(sources)] == sources:
exps.extend(exp[len(sources)].split(','))
expand_fields = exps

if include_fields:
incls = []
for inf in include_fields:
if len(inf) - 1 == len(sources) and inf[:-1] == sources:
incls.extend(inf[-1].split(','))
if incls:
fields = OrderedDict(
[(k, v) for k, v in fields.items() if k in incls]
)

if exclude_fields:
excls = []
for inf in exclude_fields:
if len(inf) - 1 == len(sources) and inf[:-1] == sources:
excls.extend(inf[-1].split(','))
if excls:
fields = OrderedDict(
[(k, v) for k, v in fields.items() if k not in excls]
)

fields_with_relations_map_model = {f.name: f.related_model for f in model._meta.get_fields()
if f.is_relation and f.name in fields}

set_fields_with_relations = set(fields_with_relations_map_model.keys())
set_expand_fields = set(expand_fields)
set_fields_serialized = set(
map(
lambda kv: kv[0],
filter(
lambda kv: not isinstance(kv[1], rest_serializers.SerializerMethodField),
fields.items()
)
)
)

expand_fields = set_fields_with_relations.intersection(set_fields_serialized)

if not expand_all:
expand_fields = expand_fields.intersection(set_expand_fields)

# remove o User model da expansão
if self.user_model in fields_with_relations_map_model.values():
expand_fields = [f for f in expand_fields
if fields_with_relations_map_model[f] != self.user_model]

if not expand_fields:
return fields

for field_name in expand_fields:
field = fields[field_name]

model = fields_with_relations_map_model[field_name]

if model:
try:
serializer_class = ApiViewSetConstrutor.get_viewset_for_model(model).serializer_class
if hasattr(field, 'many') and field.many or isinstance(field, ManyRelatedField):
serializer_class = serializer_class(
many=True,
read_only=True,
context=self.context
)
else:
serializer_class = serializer_class(
read_only=True,
context=self.context
)

fields[field_name] = serializer_class

except Exception as e:
logger.error(f'Erro ao expandir campo {field_name} do model {model}: {e}')

return fields
18 changes: 12 additions & 6 deletions sapl/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import Q
from django.urls import reverse
from drfautoapi.drfautoapi import DrfAutoApiSerializerMixin
from image_cropping.utils import get_backend
from rest_framework import serializers
from rest_framework.fields import SerializerMethodField
Expand All @@ -13,16 +15,13 @@
from sapl.sessao.models import OrdemDia, SessaoPlenaria


class SaplSerializerMixin(serializers.ModelSerializer):
__str__ = SerializerMethodField()
class SaplSerializerMixin(DrfAutoApiSerializerMixin):
link_detail_backend = serializers.SerializerMethodField()
metadata = SerializerMethodField()

class Meta:
class Meta(DrfAutoApiSerializerMixin.Meta):
fields = '__all__'

def get___str__(self, obj) -> str:
return str(obj)

def get_metadata(self, obj) -> dict:
try:
metadata = Metadata.objects.get(
Expand All @@ -35,6 +34,13 @@ def get_metadata(self, obj) -> dict:
finally:
return metadata

def get_link_detail_backend(self, obj) -> str:
try:
return reverse(f'{self.Meta.model._meta.app_config.name}:{self.Meta.model._meta.model_name}_detail',
kwargs={'pk': obj.pk})
except:
return ''


class ChoiceSerializer(serializers.Serializer):
value = serializers.SerializerMethodField()
Expand Down