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

1""" 

2Utilities for working with lists of model instances which represent 

3trees. 

4""" 

5import copy 

6import csv 

7import itertools 

8import sys 

9 

10from django.utils.translation import gettext as _ 

11 

12 

13__all__ = ( 

14 "previous_current_next", 

15 "tree_item_iterator", 

16 "drilldown_tree_for_node", 

17 "get_cached_trees", 

18) 

19 

20 

21def previous_current_next(items): 

22 """ 

23 From http://www.wordaligned.org/articles/zippy-triples-served-with-python 

24 

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) 

39 

40 

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: 

47 

48 ``'new_level'`` 

49 ``True`` if the current item is the start of a new level in 

50 the tree, ``False`` otherwise. 

51 

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. 

56 

57 If ``ancestors`` is ``True``, the following key will also be 

58 available: 

59 

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). 

64 

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:: 

68 

69 Books -> [] 

70 Sci-fi -> ['Books'] 

71 Dystopian Futures -> ['Books', 'Sci-fi'] 

72 

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. 

76 

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 

84 

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"] = [] 

105 

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 ) 

116 

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) 

121 

122 

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. 

135 

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: 

139 

140 ``rel_cls`` 

141 A ``Model`` class which has a relation to the node's class. 

142 

143 ``rel_field`` 

144 The name of the field in ``rel_cls`` which holds the relation 

145 to the node's class. 

146 

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``. 

151 

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. 

155 

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) 

168 

169 

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)) 

193 

194 row_text = "%s%s" % ("- " * level, str(n)) 

195 row.append(row_text) 

196 writer.writerow(row) 

197 

198 

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 

213 

214 

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. 

222 

223 NOTE: nodes _must_ be passed in the correct (depth-first) order. If they aren't, 

224 a ValueError will be raised. 

225 

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. 

228 

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. 

231 

232 Aliases to this function are also available: 

233 

234 ``mptt.templatetags.mptt_tag.cache_tree_children`` 

235 Use for recursive rendering in templates. 

236 

237 ``mptt.querysets.TreeQuerySet.get_cached_trees`` 

238 Useful for chaining with queries; e.g., 

239 `Node.objects.filter(**kwargs).get_cached_trees()` 

240 """ 

241 

242 current_path = [] 

243 top_nodes = [] 

244 

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() 

253 

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 

257 

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 ) 

264 

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 = [] 

268 

269 # Remove nodes not in the current branch 

270 while len(current_path) > node_level - root_level: 

271 current_path.pop(-1) 

272 

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) 

282 

283 if root_level == 0: 

284 # get_ancestors() can use .parent.parent.parent... 

285 setattr(obj, "_mptt_use_cached_ancestors", True) 

286 

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) 

293 

294 return top_nodes