dynamicweb/cms/models/query.py

133 lines
5 KiB
Python
Raw Normal View History

import warnings
from collections import Counter
from django.db import models
from django.db.models import Case, F, When
from cms.exceptions import NoHomeFound
from cms.utils.compat.warnings import RemovedInDjangoCMS60Warning
from cms.utils.mptree import get_queryset_base, get_tree_backend
# Resolved once at import time (same as the model base in pagemodel.py). In
# mptree mode the base is a plain QuerySet and treebeard is never imported.
_USING_TREEBEARD = get_tree_backend() == "treebeard"
class PageQuerySet(get_queryset_base()):
node_warning = ("As of django CMS 5.0 the Page model does not have a node property anymore. "
"Use the related fields directly.")
def delete(self, *args, **kwargs):
if _USING_TREEBEARD:
# treebeard's MP_NodeQuerySet.delete removes whole subtrees by path
# and fixes parent numchild.
return super().delete(*args, **kwargs)
# mptree backend: the parent FK's on_delete=CASCADE removes descendants
# (no orphans), but the surviving parents' numchild cache still needs
# decrementing -- mirror treebeard's behaviour using parent_id instead
# of path. (Instance .delete() is handled on the model mixin; this path
# covers bulk ``Page.objects.filter(...).delete()``.)
rows = list(self.values_list("pk", "parent_id"))
deleted = {pk for pk, _ in rows}
lost = Counter(
parent_id for _, parent_id in rows
if parent_id is not None and parent_id not in deleted
)
using = self.db
result = models.QuerySet.delete(self, *args, **kwargs)
for parent_id, num_lost in lost.items():
self.model._base_manager.using(using).filter(pk=parent_id).update(
# Floored at zero in case the cache was already stale. Not with
# Greatest(F("numchild") - n, 0): ``numchild`` is unsigned, so
# MySQL raises "value is out of range" on the subtraction before
# GREATEST ever sees it. CASE evaluates its branch lazily and so
# only subtracts where the result cannot underflow.
numchild=Case(
When(numchild__gte=num_lost, then=F("numchild") - num_lost),
default=0,
output_field=models.PositiveIntegerField(),
)
)
return result
def get_descendants(self, parent=None):
if parent is None:
return self.all()
if parent.is_leaf():
# leaf nodes have no children
return self.none()
return self.filter(path__startswith=parent.path, depth__gte=parent.depth)
def on_site(self, site=None):
from django.contrib.sites.models import Site
if site is None:
site = Site.objects.get_current()
return self.filter(site=site)
def get_home(self, site=None):
try:
home = self.on_site(site).distinct().get(is_home=True)
except self.model.DoesNotExist:
raise NoHomeFound('No Root page found')
return home
def has_apphooks(self):
"""
Returns True if any page on this queryset has an apphook attached.
"""
return self.exclude(application_urls=None).exclude(application_urls='').exists()
def delete_fast(self):
# plain Django delete, skipping any tree-fixup delete (both backends)
models.QuerySet.delete(self)
def root_only(self):
return self.filter(depth=1)
def select_related(self, *fieldnames):
modified = []
for f in fieldnames:
if isinstance(f, str) and f.startswith('node'):
warnings.warn(
self.node_warning,
RemovedInDjangoCMS60Warning,
stacklevel=2,
)
if f != 'node':
modified.append(f.replace('node__', ''))
else:
modified.append(f)
return super().select_related(*modified)
def filter(self, *args, **kwargs):
modified = {}
for key, val in kwargs.items():
if isinstance(key, str) and key.startswith('node'):
warnings.warn(
self.node_warning,
RemovedInDjangoCMS60Warning,
stacklevel=2,
)
if key == 'node':
continue
modified[key[6:]] = val
else:
modified[key] = val
return super().filter(*args, **modified)
def order_by(self, *fieldnames):
modified = []
for f in fieldnames:
if isinstance(f, str) and f.startswith("node__"):
warnings.warn(
self.node_warning,
RemovedInDjangoCMS60Warning,
stacklevel=2,
)
modified.append(f[6:])
else:
modified.append(f)
return super().order_by(*modified)