Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/mptt/utils.py: 15%
88 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""
2Utilities for working with lists of model instances which represent
3trees.
4"""
5import copy
6import csv
7import itertools
8import sys
10from django.utils.translation import gettext as _
13__all__ = (
14 "previous_current_next",
15 "tree_item_iterator",
16 "drilldown_tree_for_node",
17 "get_cached_trees",
18)
21def previous_current_next(items):
22 """
23 From http://www.wordaligned.org/articles/zippy-triples-served-with-python
25 Creates an iterator which returns (previous, current, next) triples,
26 with ``None`` filling in when there is no previous or next
27 available.
28 """
29 extend = itertools.chain([None], items, [None])
30 prev, cur, nex = itertools.tee(extend, 3)
31 # Advancing an iterator twice when we know there are two items (the
32 # two Nones at the start and at the end) will never fail except if
33 # `items` is some funny StopIteration-raising generator. There's no point
34 # in swallowing this exception.
35 next(cur)
36 next(nex)
37 next(nex)
38 return zip(prev, cur, nex)
41def tree_item_iterator(items, ancestors=False, callback=str):
42 """
43 Given a list of tree items, iterates over the list, generating
44 two-tuples of the current tree item and a ``dict`` containing
45 information about the tree structure around the item, with the
46 following keys:
48 ``'new_level'``
49 ``True`` if the current item is the start of a new level in
50 the tree, ``False`` otherwise.
52 ``'closed_levels'``
53 A list of levels which end after the current item. This will
54 be an empty list if the next item is at the same level as the
55 current item.
57 If ``ancestors`` is ``True``, the following key will also be
58 available:
60 ``'ancestors'``
61 A list of representations of the ancestors of the current
62 node, in descending order (root node first, immediate parent
63 last).
65 For example: given the sample tree below, the contents of the
66 list which would be available under the ``'ancestors'`` key
67 are given on the right::
69 Books -> []
70 Sci-fi -> ['Books']
71 Dystopian Futures -> ['Books', 'Sci-fi']
73 You can overload the default representation by providing an
74 optional ``callback`` function which takes a single argument
75 and performs coersion as required.
77 """
78 structure = {}
79 opts = None
80 first_item_level = 0
81 for previous, current, next_ in previous_current_next(items):
82 if opts is None:
83 opts = current._mptt_meta
85 current_level = getattr(current, opts.level_attr)
86 if previous:
87 structure["new_level"] = getattr(previous, opts.level_attr) < current_level
88 if ancestors:
89 # If the previous node was the end of any number of
90 # levels, remove the appropriate number of ancestors
91 # from the list.
92 if structure["closed_levels"]:
93 structure["ancestors"] = structure["ancestors"][
94 : -len(structure["closed_levels"])
95 ]
96 # If the current node is the start of a new level, add its
97 # parent to the ancestors list.
98 if structure["new_level"]:
99 structure["ancestors"].append(callback(previous))
100 else:
101 structure["new_level"] = True
102 if ancestors:
103 # Set up the ancestors list on the first item
104 structure["ancestors"] = []
106 first_item_level = current_level
107 if next_:
108 structure["closed_levels"] = list(
109 range(current_level, getattr(next_, opts.level_attr), -1)
110 )
111 else:
112 # All remaining levels need to be closed
113 structure["closed_levels"] = list(
114 range(current_level, first_item_level - 1, -1)
115 )
117 # Return a deep copy of the structure dict so this function can
118 # be used in situations where the iterator is consumed
119 # immediately.
120 yield current, copy.deepcopy(structure)
123def drilldown_tree_for_node(
124 node,
125 rel_cls=None,
126 rel_field=None,
127 count_attr=None,
128 cumulative=False,
129 all_descendants=False,
130):
131 """
132 Creates a drilldown tree for the given node. A drilldown tree
133 consists of a node's ancestors, itself and its immediate children
134 or all descendants, all in tree order.
136 Optional arguments may be given to specify a ``Model`` class which
137 is related to the node's class, for the purpose of adding related
138 item counts to the node's children:
140 ``rel_cls``
141 A ``Model`` class which has a relation to the node's class.
143 ``rel_field``
144 The name of the field in ``rel_cls`` which holds the relation
145 to the node's class.
147 ``count_attr``
148 The name of an attribute which should be added to each child in
149 the drilldown tree, containing a count of how many instances
150 of ``rel_cls`` are related through ``rel_field``.
152 ``cumulative``
153 If ``True``, the count will be for each child and all of its
154 descendants, otherwise it will be for each child itself.
156 ``all_descendants``
157 If ``True``, return all descendants, not just immediate children.
158 """
159 if all_descendants:
160 children = node.get_descendants()
161 else:
162 children = node.get_children()
163 if rel_cls and rel_field and count_attr:
164 children = node._tree_manager.add_related_count(
165 children, rel_cls, rel_field, count_attr, cumulative
166 )
167 return itertools.chain(node.get_ancestors(), [node], children)
170def print_debug_info(qs, file=None):
171 """
172 Given an mptt queryset, prints some debug information to stdout.
173 Use this when things go wrong.
174 Please include the output from this method when filing bug issues.
175 """
176 opts = qs.model._mptt_meta
177 writer = csv.writer(sys.stdout if file is None else file)
178 header = (
179 "pk",
180 opts.level_attr,
181 "%s_id" % opts.parent_attr,
182 opts.tree_id_attr,
183 opts.left_attr,
184 opts.right_attr,
185 "pretty",
186 )
187 writer.writerow(header)
188 for n in qs.order_by("tree_id", "lft"):
189 level = getattr(n, opts.level_attr)
190 row = []
191 for field in header[:-1]:
192 row.append(getattr(n, field))
194 row_text = "%s%s" % ("- " * level, str(n))
195 row.append(row_text)
196 writer.writerow(row)
199def _get_tree_model(model_class):
200 # Find the model that contains the tree fields.
201 # This is a weird way of going about it, but Django doesn't let us access
202 # the fields list to detect where the tree fields actually are,
203 # because the app cache hasn't been loaded yet.
204 # So, it *should* be the *last* concrete MPTTModel subclass in the mro().
205 bases = list(model_class.mro())
206 while bases: 206 ↛ 212line 206 didn't jump to line 212, because the condition on line 206 was never false
207 b = bases.pop()
208 # NOTE can't use `issubclass(b, MPTTModel)` here because we can't
209 # import MPTTModel yet! So hasattr(b, '_mptt_meta') will have to do.
210 if hasattr(b, "_mptt_meta") and not (b._meta.abstract or b._meta.proxy):
211 return b
212 return None
215def get_cached_trees(queryset):
216 """
217 Takes a list/queryset of model objects in MPTT left (depth-first) order and
218 caches the children and parent on each node. This allows up and down
219 traversal through the tree without the need for further queries. Use cases
220 include using a recursively included template or arbitrarily traversing
221 trees.
223 NOTE: nodes _must_ be passed in the correct (depth-first) order. If they aren't,
224 a ValueError will be raised.
226 Returns a list of top-level nodes. If a single tree was provided in its
227 entirety, the list will of course consist of just the tree's root node.
229 For filtered querysets, if no ancestors for a node are included in the
230 queryset, it will appear in the returned list as a top-level node.
232 Aliases to this function are also available:
234 ``mptt.templatetags.mptt_tag.cache_tree_children``
235 Use for recursive rendering in templates.
237 ``mptt.querysets.TreeQuerySet.get_cached_trees``
238 Useful for chaining with queries; e.g.,
239 `Node.objects.filter(**kwargs).get_cached_trees()`
240 """
242 current_path = []
243 top_nodes = []
245 if queryset:
246 # Get the model's parent-attribute name
247 parent_attr = queryset[0]._mptt_meta.parent_attr
248 root_level = None
249 is_filtered = hasattr(queryset, "query") and queryset.query.has_filters()
250 for obj in queryset:
251 # Get the current mptt node level
252 node_level = obj.get_level()
254 if root_level is None or (is_filtered and node_level < root_level):
255 # First iteration, so set the root level to the top node level
256 root_level = node_level
258 elif node_level < root_level:
259 # ``queryset`` was a list or other iterable (unable to order),
260 # and was provided in an order other than depth-first
261 raise ValueError(
262 _("Node %s not in depth-first order") % (type(queryset),)
263 )
265 # Set up the attribute on the node that will store cached children,
266 # which is used by ``MPTTModel.get_children``
267 obj._cached_children = []
269 # Remove nodes not in the current branch
270 while len(current_path) > node_level - root_level:
271 current_path.pop(-1)
273 if node_level == root_level:
274 # Add the root to the list of top nodes, which will be returned
275 top_nodes.append(obj)
276 else:
277 # Cache the parent on the current node, and attach the current
278 # node to the parent's list of children
279 _parent = current_path[-1]
280 setattr(obj, parent_attr, _parent)
281 _parent._cached_children.append(obj)
283 if root_level == 0:
284 # get_ancestors() can use .parent.parent.parent...
285 setattr(obj, "_mptt_use_cached_ancestors", True)
287 # Add the current node to end of the current path - the last node
288 # in the current path is the parent for the next iteration, unless
289 # the next iteration is higher up the tree (a new branch), in which
290 # case the paths below it (e.g., this one) will be removed from the
291 # current path during the next iteration
292 current_path.append(obj)
294 return top_nodes