Browse Source

Impl expansão dinâmica de campos na API

pull/3809/head
LeandroJatai 3 weeks ago
parent
commit
b599dd98bb
  1. 192
      drfautoapi/drfautoapi.py
  2. 18
      sapl/api/serializers.py

192
drfautoapi/drfautoapi.py

@ -1,4 +1,5 @@
from collections import OrderedDict from collections import OrderedDict
from functools import cached_property
import django_filters import django_filters
import importlib import importlib
import inspect import inspect
@ -6,14 +7,16 @@ import logging
import re import re
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields.jsonb import JSONField 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.base import ModelBase
from django.db.models.fields import TextField, CharField from django.db.models.fields import TextField, CharField
from django.db.models.fields.files import FileField from django.db.models.fields.files import FileField
from django.db.models.fields.related import ManyToManyField from django.db.models.fields.related import ManyToManyField
from django.template.defaultfilters import capfirst from django.template.defaultfilters import capfirst
from django.utils.translation import ugettext_lazy as _ 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.constants import ALL_FIELDS, EMPTY_VALUES
from django_filters.fields import ModelMultipleChoiceField from django_filters.fields import ModelMultipleChoiceField
from django_filters.filters import CharFilter from django_filters.filters import CharFilter
@ -21,6 +24,7 @@ from django_filters.filterset import FilterSet
from django_filters.rest_framework.backends import DjangoFilterBackend from django_filters.rest_framework.backends import DjangoFilterBackend
from django_filters.utils import resolve_field, get_all_model_fields from django_filters.utils import resolve_field, get_all_model_fields
from rest_framework import serializers as rest_serializers from rest_framework import serializers as rest_serializers
from rest_framework.relations import ManyRelatedField
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.routers import DefaultRouter from rest_framework.routers import DefaultRouter
from rest_framework.viewsets import ModelViewSet from rest_framework.viewsets import ModelViewSet
@ -85,6 +89,7 @@ class M2MFilter(django_filters.ModelMultipleChoiceFilter):
class ApiFilterSetMixin(FilterSet): class ApiFilterSetMixin(FilterSet):
o = CharFilter(method='filter_o') o = CharFilter(method='filter_o')
id__in = CharFilter(method='filter_id__in')
class Meta: class Meta:
fields = '__all__' fields = '__all__'
@ -109,6 +114,13 @@ class ApiFilterSetMixin(FilterSet):
}, },
} }
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): def filter_o(self, queryset, name, value):
try: try:
return queryset.order_by( return queryset.order_by(
@ -434,3 +446,181 @@ class customize(object):
return _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 expande campo1
- `expand`, `include` e `exclude` podem ser usados juntos na mesma requisição
- `include` e `exclude` funcionam em subniveis se o campo pai estiver expandido
- `include` tem precedência sobre `exclude` e 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
sapl/api/serializers.py

@ -4,6 +4,8 @@ from django.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
from django.db.models import Q from django.db.models import Q
from django.urls import reverse
from drfautoapi.drfautoapi import DrfAutoApiSerializerMixin
from image_cropping.utils import get_backend from image_cropping.utils import get_backend
from rest_framework import serializers from rest_framework import serializers
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
@ -13,16 +15,13 @@ from sapl.parlamentares.models import Parlamentar, Mandato, Legislatura
from sapl.sessao.models import OrdemDia, SessaoPlenaria from sapl.sessao.models import OrdemDia, SessaoPlenaria
class SaplSerializerMixin(serializers.ModelSerializer): class SaplSerializerMixin(DrfAutoApiSerializerMixin):
__str__ = SerializerMethodField() link_detail_backend = serializers.SerializerMethodField()
metadata = SerializerMethodField() metadata = SerializerMethodField()
class Meta: class Meta(DrfAutoApiSerializerMixin.Meta):
fields = '__all__' fields = '__all__'
def get___str__(self, obj) -> str:
return str(obj)
def get_metadata(self, obj) -> dict: def get_metadata(self, obj) -> dict:
try: try:
metadata = Metadata.objects.get( metadata = Metadata.objects.get(
@ -35,6 +34,13 @@ class SaplSerializerMixin(serializers.ModelSerializer):
finally: finally:
return metadata 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): class ChoiceSerializer(serializers.Serializer):
value = serializers.SerializerMethodField() value = serializers.SerializerMethodField()

Loading…
Cancel
Save