Repository URL to install this package:
|
Version:
0.3.0a1 ▾
|
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)