Package: px-django-massaffect
Django configurable affectorsUnified interface to create affector/filters configurations.Installationpipinstallpx-django-massaffectConfigurationLibrary allows to define any amount of filter sets. So to use filtering capabilities you should generate base classes and define adapters to apply filter.First - create root affector model, that all other affectors will inherit from.models/something.pyfrompxd_massaffect.modelsimportmake_formula_fieldclassSomethingThatCanAffect(models.Model):# Simple text field, where user may define affectors formula.filters_formula=make_formula_field()models/affector.pyfrompxd_massaffect.modelsimportmake_config_base# Affector model for any other affector to inherit from:classAffector(make_config_base(# Abstract model name prefix.'affector',# Parent model, that will store configurations inside.'yourapp.SomethingThatCanAffect',# Related name to find all affectors.related_name='affectors',# Your app label. Not required.app_label='yourapp',)):# Each of your affectors instance fill have a variable name to use in# affecting formula.# You may change prefix to any simple string. Simpler - better)variable_prefix='F'Now you may define any particular affector you want. You'll need a configuration model and affector adapter to apply filtering:models/affectors.py- Configfrom.affectorimportAffectorclassCategoriesAffectorConfig(Affector):categories=models.ManyToManyField(Category)affector_adapters.py- Adapters storage:fromdjango.dbimportmodelsfrompxd_massaffect.registryimportAffectDescriptor,AffectAdapterfrom.modelsimportCategoriesAffectorConfig,ModelThatCanBeAffectedclassCategoriesAffectAdapter(AffectAdapter):# By implementing `affect_Q` or `affect_queryset` we are making affector# capable of queryset handling.defaffect_Q(self,config:CategoriesAffectorConfig,**kw)->models.Q:returnmodels.Q(category__in=config.categories.all())# If we want to affect some list of models instances we should# implement either `affect_item` or `affect_items`.defaffect_item(self,config:CategoriesAffectorConfig,item,**kw):returnitem.category_idin{x.pkforxinconfig.categories.all()}# Now, to register filter we need to have a filter descriptor instance:categories_affector=AffectDescriptor(CategoriesAffectorConfig,CategoriesAffectAdapter({ModelThatCanBeAffected}),)Then you need to define your affectors registry:affectors_registry.pyfrompxd_massaffect.registryimportAffectRegistryfrom.affector_adaptersimportcategories_affectorregistry=AffectRegistry()# Add affect descriptors to a registry:registry.multiregister([categories_affector,# or'yourapp.affector_adapters.categories_affector',])Admin interfaceAll your affectors will be presented through 1 inline set:frompxd_massaffect.adminimportAffectConfigSetInline,AffectConfigInlinefrom.affectors_registryimportregistryfrom.modelsimportAffectorclassAffectorSetInline(AffectConfigSetInline):registry=registrymodel=AffectorAnd add this inline to aSomethingThatCanAffectmodel admin definition:fromdjango.contribimportadminimportnested_adminfrom.modelsimportSomethingThatCanAffectfrom.affectors_adminimportAffectorSetInline@admin.register(SomethingThatCanAffect)classSomethingThatCanAffectAdmin(nested_admin.NestedPolymorphicModelAdmin):inlines=[AffectorSetInline,]UsageAll that configuration above was made for you to be able to:Calculate something during db selection request and add that calculation to annotation field.Calculate something for each element in list and get back result.For that purpose there are two classes:AffectorandParentsAffector.Affectoris a basic class takes arbitrary configuration instances and runs them for a set of objects/queryset.WhenParentsAffectoris a little bit more complicated, because it collects affector configurations and resolving formulas from "parent" instances passed inside and then usesAffectorto get the result.frompxd_massaffect.affectorimport(Affector,ParentsAffector,and_resolver,or_resolver,make_formula_resolver,)from.affectors_registryimportregistryaffector=Affector(registry)classSomethingAffector(ParentsAffector):affector=affector# Based on formula from 'filters_formula' configurations will be resolved# for each passed parent:defget_parent_resolver(self,parent:SomethingThatCanAffect):returnmake_formula_resolver(parent.filters_formula)something=SomethingThatCanAffect.objects.first()something_affector=SomethingAffector([something])# QuerySet will have additional annotation fields with result.# And `fields_map` is the mapping parent->field_name.# This way you can choose how you want to interpolate/query that data by# yourself.queryset,fields_map=something_affector.affect_queryset(SomeAffectableModel.objects.all())# > (queryset, {something: 'autogenerated_field_name'})# Line above with usage of base Affector class could look something like that:queryset,field_name=affector.affect_queryset(# Passing list of affectorssomething.affectors.all(),# Queryset to affectSomeAffectableModel.objects.all(),# Formula resolver. By default it will be a "bitwise and" conjunction:resolver=make_formula_resolver(something.filters_formula),# Additional kwargs to adapters:kwargs={},)# > (queryset, 'autogenerated_field_name')# But `ParentsAffector` is more suitable for multiple parents handling at once.# After that you may do anything with that field:queryset.filter(**{field_name:True})Same logic could be used for a lists of objects. You just need to change the method fromaffect_querysettoaffect_items.ChangelogAll notable changes to this project will be documented in this file.The format is based onKeep a Changelog,
and this project adheres toSemantic Versioning.[Unreleased][0.1.2]AddedDefault value configuration for affector result.[0.1.1]Fixedaffect_Qmethod kwargs passing.[0.1.0]Initial version.