document wrappers

This commit is contained in:
Laura Klünder 2017-06-21 19:01:00 +02:00
parent 8306345856
commit da543f8ee1

View file

@ -15,6 +15,14 @@ from c3nav.editor.utils import is_created_pk
class BaseWrapper: class BaseWrapper:
"""
Base Class for all wrappers.
Saves wrapped object along with the changeset and the author for new changes.
getattr, setattr and delattr will be forwarded to the object, exceptions are specified in _not_wrapped.
If the value of an attribute is a model, model instance, manager or queryset, it will be wrapped, to.
Callables will only be returned be getattr when they are inside _allowed_callables.
Callables in _wrapped_callables will be returned wrapped, so that their self if the wrapping instance.
"""
_not_wrapped = ('_changeset', '_author', '_obj', '_created_pks', '_result', '_extra', '_result_cache', _not_wrapped = ('_changeset', '_author', '_obj', '_created_pks', '_result', '_extra', '_result_cache',
'_initial_values') '_initial_values')
_allowed_callables = () _allowed_callables = ()
@ -27,6 +35,9 @@ class BaseWrapper:
# noinspection PyUnresolvedReferences # noinspection PyUnresolvedReferences
def _wrap_model(self, model): def _wrap_model(self, model):
"""
Wrap a model, with same changeset and author as this wrapper.
"""
if isinstance(model, type) and issubclass(model, ModelInstanceWrapper): if isinstance(model, type) and issubclass(model, ModelInstanceWrapper):
model = model._parent model = model._parent
if isinstance(model, ModelWrapper): if isinstance(model, ModelWrapper):
@ -37,6 +48,9 @@ class BaseWrapper:
return ModelWrapper(self._changeset, model, self._author) return ModelWrapper(self._changeset, model, self._author)
def _wrap_instance(self, instance): def _wrap_instance(self, instance):
"""
Wrap a model instance, with same changeset and author as this wrapper.
"""
if isinstance(instance, ModelInstanceWrapper): if isinstance(instance, ModelInstanceWrapper):
if self._author == instance._author and self._changeset == instance._changeset: if self._author == instance._author and self._changeset == instance._changeset:
return instance return instance
@ -45,6 +59,10 @@ class BaseWrapper:
return self._wrap_model(type(instance)).create_wrapped_model_class()(self._changeset, instance, self._author) return self._wrap_model(type(instance)).create_wrapped_model_class()(self._changeset, instance, self._author)
def _wrap_manager(self, manager): def _wrap_manager(self, manager):
"""
Wrap a manager, with same changeset and author as this wrapper.
Detects RelatedManager or ManyRelatedmanager instances and chooses the Wrapper accordingly.
"""
assert isinstance(manager, Manager) assert isinstance(manager, Manager)
if hasattr(manager, 'through'): if hasattr(manager, 'through'):
return ManyRelatedManagerWrapper(self._changeset, manager, self._author) return ManyRelatedManagerWrapper(self._changeset, manager, self._author)
@ -53,6 +71,9 @@ class BaseWrapper:
return ManagerWrapper(self._changeset, manager, self._author) return ManagerWrapper(self._changeset, manager, self._author)
def _wrap_queryset(self, queryset): def _wrap_queryset(self, queryset):
"""
Wrap a queryset, with same changeset and author as this wrapper.
"""
return QuerySetWrapper(self._changeset, queryset, self._author) return QuerySetWrapper(self._changeset, queryset, self._author)
def __getattr__(self, name): def __getattr__(self, name):
@ -88,7 +109,12 @@ class BaseWrapper:
class ModelWrapper(BaseWrapper): class ModelWrapper(BaseWrapper):
_allowed_callables = ('EditorForm',) """
Wraps a model class.
Can be compared to other wrapped or non-wrapped model classes.
Can be called (like a class) to get a wrapped model instance
that has the according ModelWrapper as its type / metaclass.
"""
_submodels_by_model = {} _submodels_by_model = {}
def __eq__(self, other): def __eq__(self, other):
@ -96,12 +122,20 @@ class ModelWrapper(BaseWrapper):
return self._obj is other._obj return self._obj is other._obj
return self._obj is other return self._obj is other
# noinspection PyPep8Naming
@cached_property @cached_property
def EditorForm(self): def EditorForm(self):
"""
Returns an editor form for this model.
"""
return create_editor_form(self._obj) return create_editor_form(self._obj)
@classmethod @classmethod
def get_submodels(cls, model): def get_submodels(cls, model: models.Model):
"""
Get non-abstract submodels for a model including the model itself.
Result is cached.
"""
try: try:
return cls._submodels_by_model[model] return cls._submodels_by_model[model]
except KeyError: except KeyError:
@ -116,19 +150,31 @@ class ModelWrapper(BaseWrapper):
@cached_property @cached_property
def _submodels(self): def _submodels(self):
"""
Get non-abstract submodels for this model including the model itself.
"""
return self.get_submodels(self._obj) return self.get_submodels(self._obj)
def create_wrapped_model_class(self) -> typing.Type['ModelInstanceWrapper']: def create_wrapped_model_class(self) -> typing.Type['ModelInstanceWrapper']:
"""
Return a ModelInstanceWrapper that has a proxy to this instance as its type / metaclass. #voodoo
"""
# noinspection PyTypeChecker # noinspection PyTypeChecker
return self.create_metaclass()(self._obj.__name__ + 'InstanceWrapper', (ModelInstanceWrapper,), {}) return self.create_metaclass()(self._obj.__name__ + 'InstanceWrapper', (ModelInstanceWrapper,), {})
def __call__(self, **kwargs): def __call__(self, **kwargs):
"""
Create a wrapped instance of this model. _wrap_instance will call create_wrapped_model_class().
"""
instance = self._wrap_instance(self._obj()) instance = self._wrap_instance(self._obj())
for name, value in kwargs.items(): for name, value in kwargs.items():
setattr(instance, name, value) setattr(instance, name, value)
return instance return instance
def create_metaclass(self): def create_metaclass(self):
"""
Create the proxy metaclass for craeate_wrapped_model_class().
"""
parent = self parent = self
class ModelInstanceWrapperMeta(type): class ModelInstanceWrapperMeta(type):
@ -152,10 +198,20 @@ class ModelWrapper(BaseWrapper):
class ModelInstanceWrapper(BaseWrapper): class ModelInstanceWrapper(BaseWrapper):
"""
Wraps a model instance. Don't use this directly, call a ModelWrapper instead / use ChangeSet.wrap().
Creates changes in changeset when save() is called.
Updates updated values on existing objects on init.
Can be compared to other wrapped or non-wrapped model instances.
"""
_allowed_callables = ('full_clean', '_perform_unique_checks', '_perform_date_checks') _allowed_callables = ('full_clean', '_perform_unique_checks', '_perform_date_checks')
_wrapped_callables = ('validate_unique', '_get_pk_val') _wrapped_callables = ('validate_unique', '_get_pk_val')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""
Get initial values of this instance, so we know what changed on save.
Updates values according to cangeset if this is an existing object.
"""
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
updates = self._changeset.updated_existing.get(type(self._obj), {}).get(self._obj.pk, {}) updates = self._changeset.updated_existing.get(type(self._obj), {}).get(self._obj.pk, {})
self._initial_values = {} self._initial_values = {}
@ -199,6 +255,10 @@ class ModelInstanceWrapper(BaseWrapper):
return self.pk == other.pk return self.pk == other.pk
def __setattr__(self, name, value): def __setattr__(self, name, value):
"""
We have to intercept here because RelatedFields won't accept
Wrapped model instances values, so we have to trick them.
"""
if name in self._not_wrapped: if name in self._not_wrapped:
return super().__setattr__(name, value) return super().__setattr__(name, value)
class_value = getattr(type(self._obj), name, None) class_value = getattr(type(self._obj), name, None)
@ -225,6 +285,9 @@ class ModelInstanceWrapper(BaseWrapper):
return [(self._wrap_model(model), unique) for model, unique in unique_checks], date_checks return [(self._wrap_model(model), unique) for model, unique in unique_checks], date_checks
def save(self, author=None): def save(self, author=None):
"""
Create changes in changeset instead of saving.
"""
if author is None: if author is None:
author = self._author author = self._author
if self.pk is None: if self.pk is None:
@ -263,6 +326,10 @@ class ModelInstanceWrapper(BaseWrapper):
def get_queryset(func): def get_queryset(func):
"""
Wraps methods of BaseQueryWrapper that manipulate a queryset.
If self is a Manager, not an object, preceed the method call with a filter call according to the manager.
"""
@wraps(func) @wraps(func)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
if hasattr(self, 'get_queryset'): if hasattr(self, 'get_queryset'):
@ -272,6 +339,10 @@ def get_queryset(func):
def queryset_only(func): def queryset_only(func):
"""
Wraps methods of BaseQueryWrapper that execute a queryset.
If self is a Manager, they throw an error, because you have to get a Queryset (e.g. using .all()) first.
"""
@wraps(func) @wraps(func)
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
if hasattr(self, 'get_queryset'): if hasattr(self, 'get_queryset'):
@ -281,6 +352,13 @@ def queryset_only(func):
class BaseQueryWrapper(BaseWrapper): class BaseQueryWrapper(BaseWrapper):
"""
Base class for everything that wraps a QuerySet or manager.
Don't use this directly, but via WrappedModel.objects or WrappedInstance.groups or similar.
Intercepts all query methods to exclude ids / include ids for each filter according to changeset changes.
Keeps track of which created objects the current filtering still applies to.
When evaluated, just does everything as if the queryset was applied to the databse.
"""
_allowed_callables = ('_add_hints', 'get_prefetch_queryset', '_apply_rel_filters') _allowed_callables = ('_add_hints', 'get_prefetch_queryset', '_apply_rel_filters')
def __init__(self, changeset, obj, author=None, created_pks=None, extra=()): def __init__(self, changeset, obj, author=None, created_pks=None, extra=()):
@ -291,13 +369,18 @@ class BaseQueryWrapper(BaseWrapper):
self._extra = extra self._extra = extra
def _get_initial_created_pks(self): def _get_initial_created_pks(self):
"""
Get all created pks for this query's model an submodels.
"""
self.model.get_submodels(self.model._obj) self.model.get_submodels(self.model._obj)
return reduce(operator.or_, (self._changeset.get_created_pks(model) for model in self.model._submodels)) return reduce(operator.or_, (self._changeset.get_created_pks(model) for model in self.model._submodels))
def _wrap_instance(self, instance):
return super()._wrap_instance(instance)
def _wrap_queryset(self, queryset, created_pks=None, add_extra=()): def _wrap_queryset(self, queryset, created_pks=None, add_extra=()):
"""
Wraps a queryset, usually after manipulating the current one.
:param created_pks: set of created pks to be still in the next queryset (the same ones as this one by default)
:param add_extra: extra() calls that have been added to the query
"""
if created_pks is None: if created_pks is None:
created_pks = self._created_pks created_pks = self._created_pks
if created_pks is False: if created_pks is False:
@ -318,6 +401,11 @@ class BaseQueryWrapper(BaseWrapper):
@get_queryset @get_queryset
def prefetch_related(self, *lookups): def prefetch_related(self, *lookups):
"""
We split up all prefetch related lookups into one-level prefetches
and convert them into Prefetch() objects with custom querysets.
This makes sure that the prefetch also happens on the virtually modified database.
"""
lookups_splitted = tuple(tuple(lookup.split('__')) for lookup in lookups) lookups_splitted = tuple(tuple(lookup.split('__')) for lookup in lookups)
max_depth = max(len(lookup) for lookup in lookups_splitted) max_depth = max(len(lookup) for lookup in lookups_splitted)
lookups_by_depth = [] lookups_by_depth = []
@ -362,9 +450,19 @@ class BaseQueryWrapper(BaseWrapper):
@get_queryset @get_queryset
def order_by(self, *args): def order_by(self, *args):
"""
Order by is not yet supported on created instances because this is not needed so far.
"""
return self._wrap_queryset(self._obj.order_by(*args)) return self._wrap_queryset(self._obj.order_by(*args))
def _filter_values(self, q, field_name, check): def _filter_values(self, q, field_name, check):
"""
Filter by value.
:param q: base Q object to give to the database and to modify
:param field_name: name of the field whose value should be compared
:param check: comparision function that only gets the new value
:return: new Q object and set of matched existing pks
"""
other_values = () other_values = ()
submodels = [model for model in self.model._submodels] submodels = [model for model in self.model._submodels]
for model in submodels: for model in submodels:
@ -382,6 +480,11 @@ class BaseQueryWrapper(BaseWrapper):
return (q & ~Q(pk__in=remove_pks)) | Q(pk__in=add_pks), created_pks return (q & ~Q(pk__in=remove_pks)) | Q(pk__in=add_pks), created_pks
def _filter_kwarg(self, filter_name, filter_value): def _filter_kwarg(self, filter_name, filter_value):
"""
filter by kwarg.
The core filtering happens here, as also Q objects are just a collection / combination of kwarg filters.
:return: new Q object and set of matched existing pks
"""
# print(filter_name, '=', filter_value, sep='') # print(filter_name, '=', filter_value, sep='')
segments = filter_name.split('__') segments = filter_name.split('__')
@ -391,19 +494,25 @@ class BaseQueryWrapper(BaseWrapper):
except AttributeError: except AttributeError:
raise ValueError('%s has no attribute %s' % (self._obj.model, field_name)) raise ValueError('%s has no attribute %s' % (self._obj.model, field_name))
# create a base q that we'll modify later
q = Q(**{filter_name: filter_value}) q = Q(**{filter_name: filter_value})
# check if the filter begins with pk or the name of the primary key
if field_name == 'pk' or field_name == self._obj.model._meta.pk.name: if field_name == 'pk' or field_name == self._obj.model._meta.pk.name:
if not segments: if not segments:
# if the check is just 'pk' or the name or the name of the primary key, return the mathing object
if is_created_pk(filter_value): if is_created_pk(filter_value):
return Q(pk__in=()), set([int(filter_value[1:])]) return Q(pk__in=()), set([int(filter_value[1:])])
return q, set() return q, set()
elif segments == ['in']: elif segments == ['in']:
# if the check is 'pk__in' it's nearly as easy
return (Q(pk__in=tuple(pk for pk in filter_value if not is_created_pk(pk))), return (Q(pk__in=tuple(pk for pk in filter_value if not is_created_pk(pk))),
set(int(pk[1:]) for pk in filter_value if is_created_pk(pk))) set(int(pk[1:]) for pk in filter_value if is_created_pk(pk)))
# check if we are filtering by a foreign key field
if isinstance(class_value, ForwardManyToOneDescriptor): if isinstance(class_value, ForwardManyToOneDescriptor):
if not segments: if not segments:
# turn 'foreign_obj' into 'foreign_obj__pk' for later
filter_name = field_name + '__pk' filter_name = field_name + '__pk'
filter_value = filter_value.pk filter_value = filter_value.pk
segments = ['pk'] segments = ['pk']
@ -412,6 +521,7 @@ class BaseQueryWrapper(BaseWrapper):
filter_type = segments.pop(0) filter_type = segments.pop(0)
if not segments and filter_type == 'in': if not segments and filter_type == 'in':
# turn 'foreign_obj__in' into 'foreign_obj__pk' for later
filter_name = field_name+'__pk__in' filter_name = field_name+'__pk__in'
filter_value = tuple(obj.pk for obj in filter_value) filter_value = tuple(obj.pk for obj in filter_value)
filter_type = 'pk' filter_type = 'pk'
@ -419,29 +529,36 @@ class BaseQueryWrapper(BaseWrapper):
q = Q(**{filter_name: filter_value}) q = Q(**{filter_name: filter_value})
if filter_type == class_value.field.model._meta.pk.name: if filter_type == class_value.field.model._meta.pk.name:
# turn <name of the primary key field> into pk for later
filter_type = 'pk' filter_type = 'pk'
if filter_type == 'pk' and segments == ['in']: if filter_type == 'pk' and segments == ['in']:
# foreign_obj__pk__in
q = Q(**{field_name+'__pk__in': tuple(pk for pk in filter_value if not is_created_pk(pk))}) q = Q(**{field_name+'__pk__in': tuple(pk for pk in filter_value if not is_created_pk(pk))})
filter_value = tuple(str(pk) for pk in filter_value) filter_value = tuple(str(pk) for pk in filter_value)
return self._filter_values(q, field_name, lambda val: str(val) in filter_value) return self._filter_values(q, field_name, lambda val: str(val) in filter_value)
if segments: if segments:
# wo don't do multi-level lookups
raise NotImplementedError raise NotImplementedError
if filter_type == 'pk': if filter_type == 'pk':
# foreign_obj__pk
if is_created_pk(filter_value): if is_created_pk(filter_value):
q = Q(pk__in=()) q = Q(pk__in=())
filter_value = str(filter_value) filter_value = str(filter_value)
return self._filter_values(q, field_name, lambda val: str(val) == filter_value) return self._filter_values(q, field_name, lambda val: str(val) == filter_value)
if filter_type == 'isnull': if filter_type == 'isnull':
# foreign_obj__isnull
return self._filter_values(q, field_name, lambda val: (val is None) is filter_value) return self._filter_values(q, field_name, lambda val: (val is None) is filter_value)
raise NotImplementedError raise NotImplementedError
# check if we are filtering by a many to many field
if isinstance(class_value, ManyToManyDescriptor): if isinstance(class_value, ManyToManyDescriptor):
if not segments: if not segments:
# turn 'm2m' into 'm2m__pk' for later
filter_name = field_name + '__pk' filter_name = field_name + '__pk'
filter_value = filter_value.pk filter_value = filter_value.pk
segments = ['pk'] segments = ['pk']
@ -450,6 +567,7 @@ class BaseQueryWrapper(BaseWrapper):
filter_type = segments.pop(0) filter_type = segments.pop(0)
if not segments and filter_type == 'in': if not segments and filter_type == 'in':
# turn 'm2m__in' into 'm2m__pk__in' for later
filter_name = field_name+'__pk__in' filter_name = field_name+'__pk__in'
filter_value = tuple(obj.pk for obj in filter_value) filter_value = tuple(obj.pk for obj in filter_value)
filter_type = 'pk' filter_type = 'pk'
@ -457,10 +575,13 @@ class BaseQueryWrapper(BaseWrapper):
q = Q(**{filter_name: filter_value}) q = Q(**{filter_name: filter_value})
if filter_type == class_value.field.model._meta.pk.name: if filter_type == class_value.field.model._meta.pk.name:
# turn <name of the primary key field> into pk for later
filter_type = 'pk' filter_type = 'pk'
if filter_type == 'pk' and segments == ['in']: if filter_type == 'pk' and segments == ['in']:
# m2m__pk__in
if not class_value.reverse: if not class_value.reverse:
# we don't do this in reverse
raise NotImplementedError raise NotImplementedError
# so... e.g. we want to get all groups that belong to one of the given spaces. # so... e.g. we want to get all groups that belong to one of the given spaces.
@ -495,9 +616,11 @@ class BaseQueryWrapper(BaseWrapper):
return q, created_pks return q, created_pks
if segments: if segments:
# we don't to multi-level lookups
raise NotImplementedError raise NotImplementedError
if filter_type == 'pk': if filter_type == 'pk':
# m2m__pk
if class_value.reverse: if class_value.reverse:
model = class_value.field.model model = class_value.field.model
@ -516,23 +639,29 @@ class BaseQueryWrapper(BaseWrapper):
Q(pk__in=(pk for pk in add_pks if not is_created_pk(pk)))), Q(pk__in=(pk for pk in add_pks if not is_created_pk(pk)))),
set(int(pk[1:]) for pk in add_pks if is_created_pk(pk))) set(int(pk[1:]) for pk in add_pks if is_created_pk(pk)))
# sorry, no reverse lookup
raise NotImplementedError raise NotImplementedError
raise NotImplementedError raise NotImplementedError
# check if field is a deffered attribute, e.g. a CharField
if isinstance(class_value, DeferredAttribute): if isinstance(class_value, DeferredAttribute):
if not segments: if not segments:
# field=
return self._filter_values(q, field_name, lambda val: val == filter_value) return self._filter_values(q, field_name, lambda val: val == filter_value)
filter_type = segments.pop(0) filter_type = segments.pop(0)
if segments: if segments:
# we don't to field__whatever__whatever
raise NotImplementedError raise NotImplementedError
if filter_type == 'in': if filter_type == 'in':
# field__in
return self._filter_values(q, field_name, lambda val: val in filter_value) return self._filter_values(q, field_name, lambda val: val in filter_value)
if filter_type == 'lt': if filter_type == 'lt':
# field__lt
return self._filter_values(q, field_name, lambda val: val < filter_value) return self._filter_values(q, field_name, lambda val: val < filter_value)
raise NotImplementedError raise NotImplementedError
@ -540,6 +669,11 @@ class BaseQueryWrapper(BaseWrapper):
raise NotImplementedError('cannot filter %s by %s (%s)' % (self._obj.model, filter_name, class_value)) raise NotImplementedError('cannot filter %s by %s (%s)' % (self._obj.model, filter_name, class_value))
def _filter_q(self, q): def _filter_q(self, q):
"""
filter by Q object.
Split it up into recursive _filter_q and _filter_kwarg calls and combine them again.
:return: new Q object and set of matched existing pks
"""
filters, created_pks = zip(*((self._filter_q(c) if isinstance(c, Q) else self._filter_kwarg(*c)) filters, created_pks = zip(*((self._filter_q(c) if isinstance(c, Q) else self._filter_kwarg(*c))
for c in q.children)) for c in q.children))
result = Q(*filters) result = Q(*filters)
@ -598,6 +732,9 @@ class BaseQueryWrapper(BaseWrapper):
@get_queryset @get_queryset
def extra(self, select): def extra(self, select):
"""
We only support the kind of extra() call that a many to many prefetch_related does.
"""
for key in select.keys(): for key in select.keys():
if not key.startswith('_prefetch_related_val'): if not key.startswith('_prefetch_related_val'):
raise NotImplementedError('extra() calls are only supported for prefetch_related!') raise NotImplementedError('extra() calls are only supported for prefetch_related!')
@ -605,14 +742,23 @@ class BaseQueryWrapper(BaseWrapper):
@get_queryset @get_queryset
def _next_is_sticky(self): def _next_is_sticky(self):
"""
Needed by prefetch_related.
"""
return self._wrap_queryset(self._obj._next_is_sticky()) return self._wrap_queryset(self._obj._next_is_sticky())
def _get_created_objects(self, get_foreign_objects=True): def _get_created_objects(self, get_foreign_objects=True):
"""
Get ModelInstanceWrapper instance for all matched created objects.
"""
return (self._changeset.get_created_object(self._obj.model, pk, get_foreign_objects=get_foreign_objects) return (self._changeset.get_created_object(self._obj.model, pk, get_foreign_objects=get_foreign_objects)
for pk in sorted(self._created_pks)) for pk in sorted(self._created_pks))
@queryset_only @queryset_only
def _get_cached_result(self): def _get_cached_result(self):
"""
Get results, make sure prefetch is prefetching and so on.
"""
obj = self._obj obj = self._obj
obj._prefetch_done = True obj._prefetch_done = True
obj._fetch_all() obj._fetch_all()
@ -625,6 +771,7 @@ class BaseQueryWrapper(BaseWrapper):
result += list(self._get_created_objects()) result += list(self._get_created_objects())
for extra in self._extra: for extra in self._extra:
# implementing the extra() call for prefetch_related
ex = extra[22:] ex = extra[22:]
for f in self._obj.model._meta.get_fields(): for f in self._obj.model._meta.get_fields():
if isinstance(f, ManyToManyRel) and f.through._meta.get_field(f.field.m2m_field_name()).attname == ex: if isinstance(f, ManyToManyRel) and f.through._meta.get_field(f.field.m2m_field_name()).attname == ex:
@ -667,6 +814,8 @@ class BaseQueryWrapper(BaseWrapper):
@_result_cache.setter @_result_cache.setter
def _result_cache(self, value): def _result_cache(self, value):
# prefetch_related will try to set this property
# it has to overwrite our final result because it already contains the created objects
self.__dict__['_cached_result'] = value self.__dict__['_cached_result'] = value
@queryset_only @queryset_only
@ -696,19 +845,38 @@ class BaseQueryWrapper(BaseWrapper):
class ManagerWrapper(BaseQueryWrapper): class ManagerWrapper(BaseQueryWrapper):
"""
Wraps a manager.
This class itself is used to wrap Model.objects managers.
"""
def get_queryset(self): def get_queryset(self):
"""
make sure that the database does not return objects that have been deleted in this changeset
"""
qs = self._wrap_queryset(self._obj.model.objects.all()) qs = self._wrap_queryset(self._obj.model.objects.all())
return qs.exclude(pk__in=self._changeset.deleted_existing.get(self._obj.model, ())) return qs.exclude(pk__in=self._changeset.deleted_existing.get(self._obj.model, ()))
class RelatedManagerWrapper(ManagerWrapper): class RelatedManagerWrapper(ManagerWrapper):
"""
Wraps a related manager.
"""
def _get_cache_name(self): def _get_cache_name(self):
"""
get cache name to fetch prefetch_related results
"""
return self._obj.field.related_query_name() return self._obj.field.related_query_name()
def get_queryset(self): def get_queryset(self):
"""
filter queryset by related manager filters
"""
return super().get_queryset().filter(**self._obj.core_filters) return super().get_queryset().filter(**self._obj.core_filters)
def all(self): def all(self):
"""
get prefetched result if it exists
"""
try: try:
return self.instance._prefetched_objects_cache[self._get_cache_name()] return self.instance._prefetched_objects_cache[self._get_cache_name()]
except(AttributeError, KeyError): except(AttributeError, KeyError):
@ -723,6 +891,9 @@ class RelatedManagerWrapper(ManagerWrapper):
class ManyRelatedManagerWrapper(RelatedManagerWrapper): class ManyRelatedManagerWrapper(RelatedManagerWrapper):
"""
Wraps a many related manager (see RelatedManagerWrapper for details)
"""
def _check_through(self): def _check_through(self):
if not self._obj.through._meta.auto_created: if not self._obj.through._meta.auto_created:
raise AttributeError('Cannot do this an a ManyToManyField which specifies an intermediary model.') raise AttributeError('Cannot do this an a ManyToManyField which specifies an intermediary model.')
@ -768,6 +939,9 @@ class ManyRelatedManagerWrapper(RelatedManagerWrapper):
class QuerySetWrapper(BaseQueryWrapper): class QuerySetWrapper(BaseQueryWrapper):
"""
Wraps a queryset.
"""
@property @property
def _iterable_class(self): def _iterable_class(self):
return self._obj._iterable_class return self._obj._iterable_class