dynamicweb/cms/models/query.py

133 lines
5 KiB
Python

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)