forked from ungleich-public/dynamicweb
133 lines
5 KiB
Python
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)
|