From e3ea566aa1f7113cffea57257dc4fd82c986d34f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LeandroJata=C3=AD?= Date: Wed, 21 Sep 2022 16:07:53 -0300 Subject: [PATCH] Refatora a API (#3606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * revert: remove url model_list deprecated de api * impl: isola api para futura transf em interlegis/drfautoapi * refactor: ref sapl/api para impl drfautoapi * elimina viewset.py * separa customização de classes em modulos * cria class method import_modules * cria class method router() * altera nome de classmethod * elimina endpoint obsoleto e migra outro" * migra endpoint deprecated para drfautoapi * migra endpoint .../provaveis para drfautoapi * migra endpoint api/autor para drfautoapi * rebuild frontend * inclui em drfautoapi a construção individual para models --- drfautoapi/__init__.py | 1 + drfautoapi/drfautoapi.py | 364 ++++++++++ frontend/src/__global/js/functions.js | 27 +- frontend/webpack-stats.json | 114 +-- sapl/api/apps.py | 3 + sapl/api/core/__init__.py | 310 -------- sapl/api/core/filters.py | 116 --- sapl/api/core/forms.py | 25 - sapl/api/core/serializers.py | 50 -- sapl/api/deprecated.py | 669 +----------------- sapl/api/forms.py | 182 +++++ sapl/api/{core => }/schema.py | 0 sapl/api/serializers.py | 205 +++++- sapl/api/signals.py | 10 + sapl/api/urls.py | 51 +- sapl/api/views.py | 89 ++- sapl/api/views_audiencia.py | 11 + sapl/api/views_base.py | 180 +++++ sapl/api/views_comissoes.py | 12 + sapl/api/views_compilacao.py | 12 + sapl/api/views_materia.py | 129 ++++ sapl/api/views_norma.py | 14 + sapl/api/views_painel.py | 11 + sapl/api/views_parlamentares.py | 119 ++++ sapl/api/views_protocoloadm.py | 102 +++ sapl/api/views_sessao.py | 47 ++ sapl/api/viewset.py | 418 ----------- sapl/materia/forms.py | 37 +- sapl/materia/views.py | 7 + sapl/protocoloadm/urls.py | 6 - sapl/settings.py | 9 +- .../sapl/frontend/js/global.babaa14f.js | 2 - .../sapl/frontend/js/global.babaa14f.js.gz | Bin 14060 -> 0 bytes .../sapl/frontend/js/global.e8c9c610.js | 2 + ...NSE.txt => global.e8c9c610.js.LICENSE.txt} | 0 .../sapl/frontend/js/global.e8c9c610.js.gz | Bin 0 -> 14070 bytes sapl/templates/base/autor_form.html | 10 +- sapl/templates/materia/autoria_form.html | 24 +- .../materia/autoria_multicreate_form.html | 22 +- .../materia/materialegislativa_form.html | 29 +- .../materia/tipoproposicao_form.html | 77 +- sapl/templates/norma/autorianorma_form.html | 25 +- .../protocoloadm/protocolar_materia.html | 27 +- 43 files changed, 1699 insertions(+), 1849 deletions(-) create mode 100644 drfautoapi/__init__.py create mode 100644 drfautoapi/drfautoapi.py delete mode 100644 sapl/api/core/__init__.py delete mode 100644 sapl/api/core/filters.py delete mode 100644 sapl/api/core/forms.py delete mode 100644 sapl/api/core/serializers.py rename sapl/api/{core => }/schema.py (100%) create mode 100644 sapl/api/signals.py create mode 100644 sapl/api/views_audiencia.py create mode 100644 sapl/api/views_base.py create mode 100644 sapl/api/views_comissoes.py create mode 100644 sapl/api/views_compilacao.py create mode 100644 sapl/api/views_materia.py create mode 100644 sapl/api/views_norma.py create mode 100644 sapl/api/views_painel.py create mode 100644 sapl/api/views_parlamentares.py create mode 100644 sapl/api/views_protocoloadm.py create mode 100644 sapl/api/views_sessao.py delete mode 100644 sapl/api/viewset.py delete mode 100644 sapl/static/sapl/frontend/js/global.babaa14f.js delete mode 100644 sapl/static/sapl/frontend/js/global.babaa14f.js.gz create mode 100644 sapl/static/sapl/frontend/js/global.e8c9c610.js rename sapl/static/sapl/frontend/js/{global.babaa14f.js.LICENSE.txt => global.e8c9c610.js.LICENSE.txt} (100%) create mode 100644 sapl/static/sapl/frontend/js/global.e8c9c610.js.gz diff --git a/drfautoapi/__init__.py b/drfautoapi/__init__.py new file mode 100644 index 000000000..14c50cc00 --- /dev/null +++ b/drfautoapi/__init__.py @@ -0,0 +1 @@ +# Transformar em projeto externo instalável para uso geral diff --git a/drfautoapi/drfautoapi.py b/drfautoapi/drfautoapi.py new file mode 100644 index 000000000..5d1518d06 --- /dev/null +++ b/drfautoapi/drfautoapi.py @@ -0,0 +1,364 @@ +from collections import OrderedDict +import importlib +import inspect +import logging + +from django.apps.config import AppConfig +from django.apps.registry import apps +from django.conf import settings +from django.contrib.postgres.fields.jsonb import JSONField +from django.db.models.base import ModelBase +from django.db.models.fields.files import FileField +from django.template.defaultfilters import capfirst +from django.utils.translation import ugettext_lazy as _ +import django_filters +from django_filters.constants import ALL_FIELDS +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.response import Response +from rest_framework.routers import DefaultRouter +from rest_framework.viewsets import ModelViewSet + + +logger = logging.getLogger(__name__) + + +class ApiFilterSetMixin(FilterSet): + + o = CharFilter(method='filter_o') + + class Meta: + fields = '__all__' + filter_overrides = { + FileField: { + 'filter_class': django_filters.CharFilter, + 'extra': lambda f: { + 'lookup_expr': 'exact', + }, + }, + JSONField: { + 'filter_class': django_filters.CharFilter, + 'extra': lambda f: { + 'lookup_expr': 'exact', + }, + }, + } + + def filter_o(self, queryset, name, value): + try: + return queryset.order_by( + *map(str.strip, value.split(','))) + except: + return queryset + + @classmethod + def get_fields(cls): + model = cls._meta.model + fields_model = get_all_model_fields(model) + fields_filter = cls._meta.fields + exclude = cls._meta.exclude + + if exclude is not None and fields_filter is None: + fields_filter = ALL_FIELDS + + fields = fields_filter if isinstance(fields_filter, dict) else {} + + for f_str in fields_model: + if f_str not in fields: + + f = model._meta.get_field(f_str) + + if f.many_to_many: + fields[f_str] = ['exact'] + continue + + fields[f_str] = ['exact'] + + def get_keys_lookups(cl, sub_f): + r = [] + for lk, lv in cl.items(): + + if lk == 'contained_by': + continue + + sflk = f'{sub_f}{"__" if sub_f else ""}{lk}' + r.append(sflk) + + if hasattr(lv, 'class_lookups'): + r += get_keys_lookups(lv.class_lookups, sflk) + + if hasattr(lv, 'output_field') and hasattr(lv, 'output_field.class_lookups'): + r.append(f'{sflk}{"__" if sflk else ""}range') + + r += get_keys_lookups(lv.output_field.class_lookups, sflk) + + return r + + fields[f_str] = list( + set(fields[f_str] + get_keys_lookups(f.class_lookups, ''))) + + # Remove excluded fields + exclude = exclude or [] + + fields = [(f, lookups) + for f, lookups in fields.items() if f not in exclude] + + return OrderedDict(fields) + + @classmethod + def filter_for_field(cls, f, name, lookup_expr='exact'): + # Redefine método estático para ignorar filtro para + # fields que não possuam lookup_expr informado + + f, lookup_type = resolve_field(f, lookup_expr) + + default = { + 'field_name': name, + 'label': capfirst(f.verbose_name), + 'lookup_expr': lookup_expr + } + + filter_class, params = cls.filter_for_lookup( + f, lookup_type) + default.update(params) + if filter_class is not None: + return filter_class(**default) + return None + + +class BusinessRulesNotImplementedMixin: + http_method_names = ['get', 'head', 'options', 'trace'] + + 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 ApiViewSetConstrutor(): + + _built_sets = {} + + class ApiViewSet(ModelViewSet): + filter_backends = (DjangoFilterBackend,) + + @classmethod + def get_viewset_for_model(cls, model): + return cls._built_sets[model._meta.app_config][model] + + @classmethod + def update(cls, other): + cls._built_sets.update(other._built_sets) + + @classmethod + def import_modules(cls, modules): + for m in modules: + importlib.import_module(m) + + @classmethod + def router(cls, router_class=DefaultRouter): + router = router_class() + for app, built_sets in cls._built_sets.items(): + for model, viewset in built_sets.items(): + router.register( + f'{app.label}/{model._meta.model_name}', viewset) + return router + + @classmethod + def build_class(cls, apps_or_models): + + DRFAUTOAPI = settings.DRFAUTOAPI + + serializers_classes = {} + filters_classes = {} + + global_serializer_mixin = rest_serializers.ModelSerializer + global_filter_class = ApiFilterSetMixin + + try: + if DRFAUTOAPI: + if 'DEFAULT_SERIALIZER_MODULE' in DRFAUTOAPI: + serializers = importlib.import_module( + DRFAUTOAPI['DEFAULT_SERIALIZER_MODULE'] + ) + serializers_classes = inspect.getmembers(serializers) + serializers_classes = {i[0]: i[1] for i in filter( + lambda x: x[0].endswith('Serializer'), + serializers_classes + )} + + if 'DEFAULT_FILTER_MODULE' in DRFAUTOAPI: + filters = importlib.import_module( + DRFAUTOAPI['DEFAULT_FILTER_MODULE'] + ) + filters_classes = inspect.getmembers(filters) + filters_classes = {i[0]: i[1] for i in filter( + lambda x: x[0].endswith('FilterSet'), + filters_classes + )} + + if 'GLOBAL_SERIALIZER_MIXIN' in DRFAUTOAPI: + cs = DRFAUTOAPI['GLOBAL_SERIALIZER_MIXIN'].split('.') + module = importlib.import_module( + '.'.join(cs[0:-1])) + global_serializer_mixin = getattr(module, cs[-1]) + + if 'GLOBAL_FILTERSET_MIXIN' in DRFAUTOAPI: + cs = DRFAUTOAPI['GLOBAL_FILTERSET_MIXIN'].split('.') + m = importlib.import_module('.'.join(cs[0:-1])) + global_filter_class = getattr(m, cs[-1]) + + except Exception as e: + logger.error(e) + + built_sets = {} + + def build(_model): + object_name = _model._meta.object_name + + serializer_name = f'{object_name}Serializer' + _serializer_class = serializers_classes.get( + serializer_name, global_serializer_mixin) + + filter_name = f'{object_name}FilterSet' + _filterset_class = filters_classes.get( + filter_name, global_filter_class) + + def create_class(): + + _meta_serializer = object if not hasattr( + _serializer_class, 'Meta') else _serializer_class.Meta + + class ApiSerializer(_serializer_class): + + class Meta(_meta_serializer): + if not hasattr(_meta_serializer, 'ref_name'): + ref_name = f'{object_name}Serializer' + + if not hasattr(_meta_serializer, 'model'): + model = _model + + if hasattr(_meta_serializer, 'exclude'): + exclude = _meta_serializer.exclude + else: + if not hasattr(_meta_serializer, 'fields'): + fields = '__all__' + elif _meta_serializer.fields != '__all__': + fields = list(_meta_serializer.fields) + else: + fields = _meta_serializer.fields + + _meta_filterset = object if not hasattr( + _filterset_class, 'Meta') else _filterset_class.Meta + + class ApiFilterSet(_filterset_class): + + class Meta(_meta_filterset, ): + if not hasattr(_meta_filterset, 'model'): + model = _model + + class ModelApiViewSet(ApiViewSetConstrutor.ApiViewSet): + queryset = _model.objects.all() + filterset_class = ApiFilterSet + serializer_class = ApiSerializer + + return ModelApiViewSet + + viewset = create_class() + viewset.__name__ = '%sModelViewSet' % _model.__name__ + return viewset + + for am in apps_or_models: + + if isinstance(am, ModelBase): + app = am._meta.app_config + else: + app = am + + if app not in cls._built_sets: + cls._built_sets[app] = {} + + if am != app: + cls._built_sets[app][am] = build(am) + continue + + for model in app.get_models(): + cls._built_sets[app][model] = build(model) + + return cls + + +# 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 que processa um endpoint detail trivial com base no model passado, +# Um endpoint detail geralmente é um conteúdo baseado numa FK com outros possíveis filtros +# e os passados pelo proprio cliente, além de o serializer e o filterset +# ser desse model passado + + +class wrapper_queryset_response_for_drf_action(object): + + def __init__(self, model): + self.model = model + + def __call__(self, cls): + + def wrapper(instance_view, *args, **kwargs): + # recupera a viewset do model anotado + iv = instance_view + viewset_from_model = ApiViewSetConstrutor._built_sets[ + self.model._meta.app_config][self.model] + + # apossa da instancia da viewset mae do action + # em uma viewset que processa dados do model passado no decorator + iv.queryset = viewset_from_model.queryset + iv.serializer_class = viewset_from_model.serializer_class + iv.filterset_class = viewset_from_model.filterset_class + + iv.queryset = instance_view.filter_queryset( + iv.get_queryset()) + + # chama efetivamente o metodo anotado que deve devolver um queryset + # com os filtros específicos definido pelo programador customizador + qs = cls(instance_view, *args, **kwargs) + + page = iv.paginate_queryset(qs) + data = iv.get_serializer( + page if page is not None else qs, many=True).data + + return iv.get_paginated_response( + data) if page is not None else Response(data) + + return wrapper + + +# decorator para recuperar e transformar o default +class customize(object): + + def __init__(self, model): + self.model = model + + def __call__(self, cls): + + class _ApiViewSet( + cls, + ApiViewSetConstrutor._built_sets[ + self.model._meta.app_config][self.model] + ): + pass + + if hasattr(_ApiViewSet, 'build'): + _ApiViewSet = _ApiViewSet.build() + + ApiViewSetConstrutor._built_sets[ + self.model._meta.app_config][self.model] = _ApiViewSet + return _ApiViewSet diff --git a/frontend/src/__global/js/functions.js b/frontend/src/__global/js/functions.js index f4e86d0b4..1f9b44a6a 100644 --- a/frontend/src/__global/js/functions.js +++ b/frontend/src/__global/js/functions.js @@ -10,11 +10,11 @@ window.refreshDatePicker = function () { } window.getCookie = function (name) { - var cookieValue = null + let cookieValue = null if (document.cookie && document.cookie !== '') { - var cookies = document.cookie.split(';') - for (var i = 0; i < cookies.length; i++) { - var cookie = $.trim(cookies[i]) + const cookies = document.cookie.split(';') + for (let i = 0; i < cookies.length; i++) { + const cookie = $.trim(cookies[i]) if (cookie.substring(0, name.length + 1) === name + '=') { cookieValue = decodeURIComponent(cookie.substring(name.length + 1)) break @@ -26,7 +26,7 @@ window.getCookie = function (name) { window.autorModal = function () { $(function () { - var dialog = $('#modal_autor').dialog({ + const dialog = $('#modal_autor').dialog({ autoOpen: false, modal: true, width: 500, @@ -65,12 +65,11 @@ window.autorModal = function () { }) $('#pesquisar').click(function () { - var name_in_query = $('#q').val() - // var q_0 = "q_0=nome__icontains" - // var q_1 = name_in_query - // query = q_1 - - $.get('/api/autor?q=' + name_in_query, function (data) { + const json_data = { + q: $('#q').val() + // get_all: true + } + $.get('/api/base/autor', json_data, function (data) { $('#div-resultado') .children() .remove() @@ -82,15 +81,15 @@ window.autorModal = function () { return } - var select = $( + const select = $( ''),e.results.forEach(function(e){t.append(i("