Skip to content

Commit

Permalink
Cache tree structure in loops
Browse files Browse the repository at this point in the history
  • Loading branch information
timobrembeck committed Jan 13, 2022
1 parent 16c0e15 commit cc5a3dc
Show file tree
Hide file tree
Showing 8 changed files with 156 additions and 12 deletions.
146 changes: 145 additions & 1 deletion integreat_cms/cms/models/abstract_tree_node.py
Expand Up @@ -3,11 +3,85 @@
from django.utils.translation import ugettext_lazy as _

from treebeard.exceptions import InvalidPosition
from treebeard.ns_tree import NS_Node, get_result_class
from treebeard.ns_tree import NS_Node, NS_NodeQuerySet, get_result_class

logger = logging.getLogger(__name__)


class TreeNodeQuerySet(NS_NodeQuerySet):
"""
This queryset provides custom caching mechanisms for trees
"""

def cache_tree(self):
"""
Caches a tree queryset in a python data structure.
:return: A list of tree nodes with cached children, descendants and ancestors
:rtype: list
"""
result = {}
for node in self.order_by("tree_id", "lft"):
# Only include the element in the tree if it is either a root node or the parent is contained in the set
if not node.parent_id or node.parent_id in result:
if node.parent_id:
# Cache the node as child of the parent node
# pylint: disable=protected-access
result[node.parent_id]._cached_children.append(node)
result[node.parent_id]._cached_descendants.append(node)
# Cache the parent node as ancestor of the current node
node._cached_ancestors.extend(
result[node.parent_id]._cached_ancestors
)
node._cached_ancestors.append(result[node.parent_id])
# Cache the current node as descendant of the parent's node ancestors
for ancestor in result[node.parent_id]._cached_ancestors:
result[ancestor.id]._cached_descendants.append(node)
# Mark as initialized to know the difference between no children and no cache
# pylint: disable=protected-access
node._cache_initialized = True
result[node.id] = node
logger.debug("Node %r cached", node)
else:
logger.debug("Node %r skipped", node)
logger.debug("Cached result: %r", result)
return list(result.values())

def prefetch_ancestors(self):
"""
Prefetches all ancestors of a tree nodes
:return: The queryset of tree nodes
:rtype: ~integreat_cms.cms.models.abstract_tree_node.TreeNodeQuerySet [ ~integreat_cms.cms.models.abstract_tree_node.AbstractTreeNode ]
"""
model = get_result_class(self.model)
return self.annotate(
prefetched_ancestors=models.Subquery(
model.filter(
tree_id=models.OuterRef("tree_id"),
lft__lte=models.OuterRef("lft"),
rgt__gte=models.OuterRef("rgt"),
)
)
)


# pylint: disable=too-few-public-methods
class TreeNodeManager(models.Manager):
"""
Custom manager for this queryset
"""

def get_queryset(self):
"""
Get the queryset of tree nodes
:return: The queryset of tree nodes
:rtype: ~integreat_cms.cms.models.abstract_tree_node.TreeNodeQuerySet [ ~integreat_cms.cms.models.abstract_tree_node.AbstractTreeNode ]
"""
return TreeNodeQuerySet(self.model).order_by("tree_id", "lft")


class AbstractTreeNode(NS_Node):
"""
Abstract data model representing a tree node within a region.
Expand All @@ -29,6 +103,16 @@ class AbstractTreeNode(NS_Node):
verbose_name=_("region"),
)

#: Custom model manager :class:`~integreat_cms.cms.models.abstract_tree_node.TreeNodeManager` for tree objects
objects = TreeNodeManager()

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cache_initialized = False
self._cached_children = []
self._cached_descendants = []
self._cached_ancestors = []

def save(self, *args, **kwargs):
# Update parent to fix inconsistencies between tree fields
if self.id:
Expand Down Expand Up @@ -135,12 +219,31 @@ def get_ancestors(self, include_self=False):
the root node and descending to the parent.
:rtype: ~treebeard.ns_tree.NS_NodeQuerySet
"""
if self._cache_initialized:
return self.get_cached_ancestors(include_self=include_self)
if include_self:
return get_result_class(self.__class__).objects.filter(
tree_id=self.tree_id, lft__lte=self.lft, rgt__gte=self.rgt
)
return super().get_ancestors()

def get_cached_ancestors(self, include_self=False):
"""
Get the cached ancestors of a specific node
:param include_self: Whether the current node should be included in the result (defaults to ``False``)
:type include_self: bool
:return: A :class:`~django.db.models.query.QuerySet` containing the current node object's ancestors, starting by
the root node and descending to the parent.
:rtype: ~treebeard.ns_tree.NS_NodeQuerySet
"""
if not self._cache_initialized:
return self.get_ancestors(include_self=include_self)
if include_self:
return [*self._cached_ancestors, self]
return self._cached_ancestors

# pylint: disable=arguments-differ
def get_descendants(self, include_self=False):
"""
Expand All @@ -152,13 +255,54 @@ def get_descendants(self, include_self=False):
:return: A :class:`~django.db.models.query.QuerySet` of all the node's descendants as DFS
:rtype: ~treebeard.ns_tree.NS_NodeQuerySet
"""
if self._cache_initialized:
return self.get_cached_descendants(include_self=include_self)
if self.is_leaf():
return get_result_class(self.__class__).objects.none()
tree = self.__class__.get_tree(parent=self)
if include_self:
return tree
return tree.exclude(pk=self.pk)

def get_cached_descendants(self, include_self=False):
"""
Get the cached ancestors of a specific node
:param include_self: Whether the current node should be included in the result (defaults to ``False``)
:type include_self: bool
:return: A :class:`~django.db.models.query.QuerySet` containing the current node object's ancestors, starting by
the root node and descending to the parent.
:rtype: ~treebeard.ns_tree.NS_NodeQuerySet
"""
if not self._cache_initialized:
return self.get_descendants(include_self=include_self)
if include_self:
return [*self._cached_descendants, self]
return self._cached_descendants

def get_children(self):
"""
A wrapper around get_children() to make use of cached children
:returns: A queryset or list of all the node's children
:rtype: list
"""
if self._cache_initialized:
return self.get_cached_children()
return super().get_children()

def get_cached_children(self):
"""
Get all cached children
:returns: A list of all the node's cached children
:rtype: list
"""
if not self._cache_initialized:
return self.get_children()
return self._cached_children

def move(self, target, pos=None):
"""
Moves the current node and all it's descendants to a new position
Expand Down
2 changes: 1 addition & 1 deletion integreat_cms/cms/models/pages/abstract_base_page.py
Expand Up @@ -198,7 +198,7 @@ def get_translation_state(self, language):
return translation_status.MISSING
if translation.currently_in_translation:
return translation_status.IN_TRANSLATION
source_language = self.region.language_tree_by_id.get(language.parent_id)
source_language = self.region.language_node_by_id.get(language.parent_id)
if not source_language:
return translation_status.UP_TO_DATE
source_translation = self.get_public_translation(source_language.slug)
Expand Down
Expand Up @@ -6,7 +6,7 @@
{% endif %}
<tr data-drop-id="{{ node.id }}" data-drop-position="last-child" class="drop drop-on border-t border-b border-solid border-gray-200 hover:bg-gray-100{% if node.depth > 1 %} child level-{{node.depth}}{% endif %}">
<td class="hierarchy single_icon">
<span data-drag-id="{{ node.id }}" data-node-descendants="{{ node|get_descendants }}" class="drag text-gray-800 block py-3 pl-4 pr-2 cursor-move" draggable="true">
<span data-drag-id="{{ node.id }}" data-node-descendants="{{ node|get_descendant_ids }}" class="drag text-gray-800 block py-3 pl-4 pr-2 cursor-move" draggable="true">
<i data-feather="move" class="text-gray-800"></i>
</span>
</td>
Expand Down
2 changes: 1 addition & 1 deletion integreat_cms/cms/templates/pages/page_form.html
Expand Up @@ -354,7 +354,7 @@ <h1 class="heading">
<div class="bg-orange-100 border-l-4 border-orange-500 text-orange-500 px-4 py-3 mb-5" role="alert">
<p>
{% trans 'You cannot delete a page which has subpages.' %}<br>
{% blocktrans count counter=page.get_descendants.count trimmed %}
{% blocktrans count counter=page.get_descendants|length trimmed %}
To delete this page, you have to delete or move its subpage first:
{% plural %}
To delete this page, you have to delete or move its subpages first:
Expand Down
4 changes: 2 additions & 2 deletions integreat_cms/cms/templates/pages/page_tree_node.html
Expand Up @@ -11,7 +11,7 @@
</td>
<td class="hierarchy single_icon whitespace-nowrap">
<span data-drag-id="{{ page.id }}"
data-node-descendants="{{ page|get_descendants }}"
data-node-descendants="{{ page|get_descendant_ids }}"
class="{% if enable_drag_and_drop %}drag cursor-move{% else %}cursor-not-allowed{% endif %} text-gray-800 inline-block pl-4 align-middle"
{% if enable_drag_and_drop %}
draggable="true"
Expand All @@ -25,7 +25,7 @@
<span class="collapse-subpages cursor-pointer inline-block align-middle"
title="{% trans 'Collapse all subpages' %}"
data-page-id="{{ page.id }}"
data-page-children="{{ page|get_children }}">
data-page-children="{{ page|get_children_ids }}">
<i data-feather="chevron-right"></i>
</span>
{% endif %}
Expand Down
4 changes: 2 additions & 2 deletions integreat_cms/cms/templatetags/tree_filters.py
Expand Up @@ -10,7 +10,7 @@


@register.filter
def get_descendants(node):
def get_descendant_ids(node):
"""
This filter returns the ids of all the node's descendants.
Expand All @@ -24,7 +24,7 @@ def get_descendants(node):


@register.filter
def get_children(node):
def get_children_ids(node):
"""
This filter returns the ids of all the node's direct children.
Expand Down
2 changes: 1 addition & 1 deletion integreat_cms/cms/views/pages/page_tree_view.py
Expand Up @@ -155,7 +155,7 @@ def page_filter(page):
{
**context,
"current_menu_item": "pages",
"pages": pages,
"pages": pages.cache_tree(),
"archived_count": region.get_pages(archived=True).count(),
"language": language,
"languages": region.active_languages,
Expand Down
6 changes: 3 additions & 3 deletions integreat_cms/locale/de/LC_MESSAGES/django.po
Expand Up @@ -1818,11 +1818,11 @@ msgstr "Sie haben Ihr bisheriges Passwort falsch eingegeben."
msgid "The new passwords do not match."
msgstr "Die Passwörter stimmen nicht überein."

#: cms/models/abstract_tree_node.py:24
#: cms/models/abstract_tree_node.py:98
msgid "parent"
msgstr "übergeordneter Knoten"

#: cms/models/abstract_tree_node.py:29 cms/models/events/event.py:82
#: cms/models/abstract_tree_node.py:103 cms/models/events/event.py:82
#: cms/models/feedback/feedback.py:22 cms/models/media/directory.py:23
#: cms/models/media/media_file.py:135 cms/models/pages/imprint_page.py:22
#: cms/models/pois/poi.py:20
Expand All @@ -1831,7 +1831,7 @@ msgstr "übergeordneter Knoten"
msgid "region"
msgstr "Region"

#: cms/models/abstract_tree_node.py:178
#: cms/models/abstract_tree_node.py:322
msgid "The node \"{}\" in region \"{}\" cannot be moved to \"{}\"."
msgstr ""
"Der Knoten \"{}\" in Region \"{}\" kann nicht nach \"{}\" verschoben werden."
Expand Down

0 comments on commit cc5a3dc

Please sign in to comment.