From bcd129a03dacddef4f2dbd7c986cd2f00727b4dd Mon Sep 17 00:00:00 2001 From: Iwo Herka Date: Thu, 26 Apr 2018 13:29:57 +0200 Subject: [PATCH] Move relational operators from EntityManager to EavQuerySet; allow for chaining --- eav/decorators.py | 116 +++++++++++++++++++++++++++++++++++++++++- eav/managers.py | 127 +--------------------------------------------- eav/queryset.py | 47 +++++++++++++++++ 3 files changed, 164 insertions(+), 126 deletions(-) create mode 100644 eav/queryset.py diff --git a/eav/decorators.py b/eav/decorators.py index ab19e1e..a6ac4d6 100644 --- a/eav/decorators.py +++ b/eav/decorators.py @@ -1,3 +1,10 @@ +from functools import wraps + +from django.db import models + +from .models import Attribute, Value + + def register_eav(**kwargs): """ Registers the given model(s) classes and wrapped Model class with @@ -16,4 +23,111 @@ def register_eav(**kwargs): register(model_class, **kwargs) return model_class - return _model_eav_wrapper \ No newline at end of file + return _model_eav_wrapper + + +def eav_filter(func): + ''' + Decorator used to wrap filter and exclude methods. Passes args through + expand_q_filters and kwargs through expand_eav_filter. Returns the + called function (filter or exclude). + ''' + + @wraps(func) + def wrapper(self, *args, **kwargs): + nargs = [] + nkwargs = {} + + for arg in args: + if isinstance(arg, models.Q): + # Modify Q objects (warning: recursion ahead). + # import pdb; pdb.set_trace() + arg = expand_q_filters(arg, self.model) + nargs.append(arg) + + o = self.model.objects.all().first() + fn = self.model.objects.filter + # print(args, kwargs) + + for key, value in kwargs.items(): + # Modify kwargs (warning: recursion ahead). + nkey, nval = expand_eav_filter(self.model, key, value) + + if nkey in nkwargs: + # Apply AND to both querysets. + nkwargs[nkey] = (nkwargs[nkey] & nval).distinct() + else: + nkwargs.update({nkey: nval}) + + return func(self, *nargs, **nkwargs) + + return wrapper + + +def expand_q_filters(q, root_cls): + ''' + Takes a Q object and a model class. + Recursivley passes each filter / value in the Q object tree leaf nodes + through expand_eav_filter + ''' + new_children = [] + + for qi in q.children: + if type(qi) is tuple: + # this child is a leaf node: in Q this is a 2-tuple of: + # (filter parameter, value) + key, value = expand_eav_filter(root_cls, *qi) + new_children.append(models.Q(**{key: value})) + else: + # this child is another Q node: recursify! + new_children.append(expand_q_filters(qi, root_cls)) + + _q = models.Q() + _q.children = new_children + _q.connector = q.connector + _q.negated = q.negated + return _q + + +def expand_eav_filter(model_cls, key, value): + ''' + Accepts a model class and a key, value. + Recurisively replaces any eav filter with a subquery. + + For example:: + + key = 'eav__height' + value = 5 + + Would return:: + + key = 'eav_values__in' + value = Values.objects.filter(value_int=5, attribute__slug='height') + ''' + fields = key.split('__') + config_cls = getattr(model_cls, '_eav_config_cls', None) + + if len(fields) > 1 and config_cls and \ + fields[0] == config_cls.eav_attr: + slug = fields[1] + gr_name = config_cls.generic_relation_attr + datatype = Attribute.objects.get(slug=slug).datatype + + lookup = '__%s' % fields[2] if len(fields) > 2 else '' + kwargs = {'value_%s%s' % (datatype, lookup): value, + 'attribute__slug': slug} + value = Value.objects.filter(**kwargs) + + return '%s__in' % gr_name, value + + try: + field = model_cls._meta.get_field(fields[0]) + except models.FieldDoesNotExist: + return key, value + + if not field.auto_created or field.concrete: + return key, value + else: + sub_key = '__'.join(fields[1:]) + key, value = expand_eav_filter(field.model, sub_key, value) + return '%s__%s' % (fields[0], key), value diff --git a/eav/managers.py b/eav/managers.py index b1eeacf..bb796ab 100644 --- a/eav/managers.py +++ b/eav/managers.py @@ -24,139 +24,16 @@ Contains the custom manager used by entities registered with eav. Functions and Classes --------------------- ''' -from functools import wraps - from django.db import models -from .models import Attribute, Value - - -def eav_filter(func): - ''' - Decorator used to wrap filter and exclude methods. Passes args through - expand_q_filters and kwargs through expand_eav_filter. Returns the - called function (filter or exclude). - ''' - - @wraps(func) - def wrapper(self, *args, **kwargs): - nargs = [] - for arg in args: - if isinstance(arg, models.Q): - # Modify Q objects (warning: recursion ahead). - arg = expand_q_filters(arg, self.model) - nargs.append(arg) - - nkwargs = {} - for key, value in kwargs.items(): - # Modify kwargs (warning: recursion ahead). - nkey, nval = expand_eav_filter(self.model, key, value) - - if nkey in nkwargs: - # Apply AND to both querysets. - nkwargs[nkey] = (nkwargs[nkey] & nval).distinct() - else: - nkwargs.update({nkey: nval}) - - return func(self, *nargs, **nkwargs) - - return wrapper - - -def expand_q_filters(q, root_cls): - ''' - Takes a Q object and a model class. - Recursivley passes each filter / value in the Q object tree leaf nodes - through expand_eav_filter - ''' - new_children = [] - - for qi in q.children: - if type(qi) is tuple: - # this child is a leaf node: in Q this is a 2-tuple of: - # (filter parameter, value) - key, value = expand_eav_filter(root_cls, *qi) - new_children.append(models.Q(**{key: value})) - else: - # this child is another Q node: recursify! - new_children.append(expand_q_filters(qi, root_cls)) - - q.children = new_children - return q - - -def expand_eav_filter(model_cls, key, value): - ''' - Accepts a model class and a key, value. - Recurisively replaces any eav filter with a subquery. - - For example:: - - key = 'eav__height' - value = 5 - - Would return:: - - key = 'eav_values__in' - value = Values.objects.filter(value_int=5, attribute__slug='height') - ''' - fields = key.split('__') - - config_cls = getattr(model_cls, '_eav_config_cls', None) - if len(fields) > 1 and config_cls and \ - fields[0] == config_cls.eav_attr: - slug = fields[1] - gr_name = config_cls.generic_relation_attr - datatype = Attribute.objects.get(slug=slug).datatype - - lookup = '__%s' % fields[2] if len(fields) > 2 else '' - kwargs = {'value_%s%s' % (datatype, lookup): value, - 'attribute__slug': slug} - value = Value.objects.filter(**kwargs) - - return '%s__in' % gr_name, value - - try: - field = model_cls._meta.get_field(fields[0]) - except models.FieldDoesNotExist: - return key, value - - if not field.auto_created or field.concrete: - return key, value - else: - sub_key = '__'.join(fields[1:]) - key, value = expand_eav_filter(field.model, sub_key, value) - return '%s__%s' % (fields[0], key), value +from .queryset import EavQuerySet class EntityManager(models.Manager): ''' Our custom manager, overriding ``models.Manager`` ''' - - @eav_filter - def filter(self, *args, **kwargs): - ''' - Pass *args* and *kwargs* through :func:`eav_filter`, then pass to - the ``models.Manager`` filter method. - ''' - return super(EntityManager, self).filter(*args, **kwargs).distinct() - - @eav_filter - def exclude(self, *args, **kwargs): - ''' - Pass *args* and *kwargs* through :func:`eav_filter`, then pass to - the ``models.Manager`` exclude method. - ''' - return super(EntityManager, self).exclude(*args, **kwargs).distinct() - - @eav_filter - def get(self, *args, **kwargs): - ''' - Pass *args* and *kwargs* through :func:`eav_filter`, then pass to - the ``models.Manager`` get method. - ''' - return super(EntityManager, self).get(*args, **kwargs) + _queryset_class = EavQuerySet def create(self, **kwargs): ''' diff --git a/eav/queryset.py b/eav/queryset.py new file mode 100644 index 0000000..be71470 --- /dev/null +++ b/eav/queryset.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# +# This software is derived from EAV-Django originally written and +# copyrighted by Andrey Mikhaylenko +# +# This is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published +# by the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with EAV-Django. If not, see . + +from django.db.models.query import QuerySet + +from .decorators import eav_filter + + +class EavQuerySet(QuerySet): + @eav_filter + def filter(self, *args, **kwargs): + ''' + Pass *args* and *kwargs* through :func:`eav_filter`, then pass to + the ``models.Manager`` filter method. + ''' + return super().filter(*args, **kwargs).distinct() + + @eav_filter + def exclude(self, *args, **kwargs): + ''' + Pass *args* and *kwargs* through :func:`eav_filter`, then pass to + the ``models.Manager`` exclude method. + ''' + return super().exclude(*args, **kwargs).distinct() + + @eav_filter + def get(self, *args, **kwargs): + ''' + Pass *args* and *kwargs* through :func:`eav_filter`, then pass to + the ``models.Manager`` get method. + ''' + return super().get(*args, **kwargs)