Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
dj-kaos / models / configs.py
Size: Mime:
from __future__ import annotations

from collections import OrderedDict
from functools import reduce
from typing import Sequence, Type

from dj_kaos.utils.sequence import flatten


class BaseModelConfig:
    name: str | None
    _orders: dict[str, int] = {}
    _parents: Sequence[BaseModelConfig] = ()

    fields = ()  # base -> zenith
    _fieldsets: dict[None | str, dict] = OrderedDict()  # base -> zenith
    search_fields = ()  # base -> zenith
    filterset_fields: dict[str, Sequence] = {}  # unordered
    list_display = ()  # base -> zenith
    list_filter = ()  # zenith -> base
    list_filter_autocomplete = ()  # unordered
    readonly_fields = ()  # unordered
    edit_readonly_fields: Sequence[str] = ()  # unordered
    actions = ()  # zenith -> base
    change_actions: Sequence[str] = ()  # actions
    unrequired_fields: Sequence[str] = ()  # unordered
    attrs_to_track: Sequence[str] = ()  # unordered

    extras: dict = {}

    _seq_config_fields = (
        'fields',
        'search_fields',
        'list_display',
        'readonly_fields',
        'edit_readonly_fields',
        'unrequired_fields',
        'attrs_to_track',
    )
    _seq_reverse_config_fields = (
        'list_filter',
        'list_filter_autocomplete',
        'actions',
        'change_actions',
    )
    _dict_config_fields = (
        'filterset_fields',
        'extras',
    )

    def __init__(
            self,
            name=None,
            *,
            orders=None,
            parents=None,
            fieldsets=None,
            **kwargs,
    ):
        self.name = self.__class__.__name__ if name is None else name
        self._orders = self._orders if orders is None else orders
        self._parents = self._parents if parents is None else parents

        fieldsets = self._fieldsets if fieldsets is None else fieldsets
        self._fieldsets = fieldsets if isinstance(fieldsets, dict) else OrderedDict(fieldsets)

        for config_field in self._seq_config_fields + self._seq_reverse_config_fields + self._dict_config_fields:
            try:
                setattr(
                    self,
                    config_field,
                    kwargs.get(config_field, getattr(self, config_field))
                )
            except AttributeError:
                raise

    @property
    def fieldsets(self):
        sorted_fieldsets = sorted(
            self._fieldsets.items(),
            key=lambda fs: fs[1].get('order', 0)
        )
        return tuple(
            (header, {k: v for k, v in config.items() if k != 'order'})
            for header, config in sorted_fieldsets
        )

    def get_flat_fields(self, exclude=None):
        flat_fields = flatten(self.fields)
        return tuple(
            (field for field in flat_fields if field not in set(exclude))
            if exclude
            else flat_fields
        )

    def make_nested_filterset_fields(self, rel_field_name):
        return {f'{rel_field_name}__{field}': filters for field, filters in self.filterset_fields.items()}

    def __str__(self):
        return self.name

    def __repr__(self):
        return f'<MC: {self}>'


class ModelConfigFactoryMixin(BaseModelConfig):
    _parents: Sequence[ModelConfigFactoryMixin] = ()

    def get_root_parents(self):
        if not self._parents:
            yield self
            return
        for parent in self._parents:
            yield from parent.get_root_parents()

    @staticmethod
    def bulk_get_root_parents(parents: Sequence[ModelConfig]):
        root_parents = []
        for parent in parents:
            root_parents.extend(parent.get_root_parents())
        return root_parents

    @staticmethod
    def combine(*parents: Sequence[ModelConfig], name=None):
        def _sort_classes(field_name: str, classes, reverse=False):
            def get_key(c):
                order = getattr(c, '_orders', {}).get(field_name, 0)
                return order if not reverse else -order

            sorted_classes = sorted(classes, key=get_key)
            return reversed(sorted_classes) if reverse else sorted_classes

        def combine_seq(field_name: str, reverse=False):
            return reduce(
                lambda acc, curr: (*acc, *getattr(curr, field_name)),
                _sort_classes(field_name, parents, reverse=reverse),
                ()
            )

        def combine_dict(field_name: str):
            return reduce(
                lambda acc, curr: {**acc, **getattr(curr, field_name)},
                parents,
                {}
            )

        def combine_fieldsets():
            def _mix_fs_configs(config1: dict, config2: dict) -> dict:
                d = {
                    'fields': (
                        *(config1.get('fields', ())),
                        *(config2.get('fields', ()))
                    ),
                    'order': config1.get('order', 0) + config2.get('order', 0),
                }

                if config1.get('classes') is not None or config2.get('classes') is not None:
                    d['classes'] = (
                        *(config1.get('classes', ())),
                        *(config2.get('classes', ()))
                    )

                return d

            def _mix_fs(fs1: OrderedDict, fs2: OrderedDict):
                return OrderedDict([
                    (k, _mix_fs_configs(fs1.get(k, {}), fs2.get(k, {})))
                    for k in (fs1 | fs2).keys()
                ])

            return reduce(
                lambda acc, curr: _mix_fs(acc, curr._fieldsets),
                _sort_classes('fieldsets', parents),
                {}
            )

        fieldsets = combine_fieldsets()

        config_fields_to_values = {
            **{
                config_field: combine_seq(config_field)
                for config_field in BaseModelConfig._seq_config_fields
            },
            **{
                config_field: combine_seq(config_field, reverse=True)
                for config_field in BaseModelConfig._seq_reverse_config_fields
            },
            **{
                config_field: combine_dict(config_field)
                for config_field in BaseModelConfig._dict_config_fields
            },
        }

        return ModelConfig(
            name=f'{name} config',
            parents=parents,
            orders={},
            fieldsets=fieldsets,
            **config_fields_to_values,
        )

    def finalize(self, name: str, parents: Sequence[ModelConfig]):
        parents = (*parents, self)
        return ModelConfigFactoryMixin.combine(*parents, name=name)


class ModelConfig(ModelConfigFactoryMixin, BaseModelConfig):
    _parents: Sequence[ModelConfig]

    def get_actions(self, request, obj=None):
        return self.actions

    def get_change_actions(self, request, obj=None):
        return self.change_actions

    def get_search_fields(self, request):
        return self.search_fields

    def get_list_display(self, request):
        return self.list_display

    def get_list_filter(self, request):
        return self.list_filter

    def get_list_filter_autocomplete(self, request, obj=None):
        return self.list_filter_autocomplete

    def get_readonly_fields(self, request, obj=None):
        return self.readonly_fields

    def get_edit_readonly_fields(self, request, obj=None):
        return self.edit_readonly_fields

    def get_fields(self, request, obj=None):
        return self.fields

    def get_fieldsets(self, request, obj=None):
        return self.fieldsets

    def get_filterset_fields(self, request, obj=None):
        return self.filterset_fields


class ModelConfigMeta:
    parents: Sequence[ModelConfig | Type[ModelConfig]]
    mixins: Sequence[ModelConfig | Type[ModelConfig]]
    orders: dict

    fields: Sequence[str]  # base -> zenith
    fieldsets: dict[None | str, dict]  # base -> zenith
    search_fields: Sequence[str]  # base -> zenith
    filterset_fields: dict[str, Sequence]  # unordered

    list_display: Sequence[str]  # base -> zenith
    list_filter: Sequence[str]  # zenith -> base
    list_filter_autocomplete: Sequence[str]  # unordered

    readonly_fields: Sequence[str]  # unordered
    edit_readonly_fields: Sequence[str]  # unordered

    actions: Sequence[str]  # zenith -> base
    change_actions: Sequence[str]  # actions

    unrequired_fields: Sequence[str]  # unordered
    attrs_to_track: Sequence[str]  # unordered

    extras: dict

    @staticmethod
    def make_config(meta_config, name, inferred_parents):
        orders: dict[str, int] = getattr(meta_config, 'orders', {})
        fieldsets = getattr(meta_config, 'fieldsets', {})
        config_fields_to_values = {
            **{
                config_field: getattr(meta_config, config_field, ())
                for config_field in (BaseModelConfig._seq_config_fields +
                                     BaseModelConfig._seq_reverse_config_fields)
            },
            **{
                config_field: getattr(meta_config, config_field, {})
                for config_field in BaseModelConfig._dict_config_fields
            },
        }

        mixins: Sequence[ModelConfig | Type[ModelConfig]] = getattr(meta_config, 'mixins', ())
        parents: Sequence[ModelConfig | Type[ModelConfig]] = getattr(meta_config, 'parents',
                                                                     (*inferred_parents, *mixins))
        parents = [parent if isinstance(parent, ModelConfig) else parent() for parent in parents]
        root_parents = ModelConfig.bulk_get_root_parents(parents)

        if not any(config_fields_to_values.values()):
            model_config = ModelConfig(name=f'partial {name} config')
        else:
            model_config = ModelConfig(
                name=f'partial {name} config',
                orders=orders,
                fieldsets=fieldsets,
                **config_fields_to_values
            )
        return model_config.finalize(name, root_parents)