1
# This file is part of Checkbox.
3
# Copyright 2013-2014 Canonical Ltd.
5
# Sylvain Pineau <sylvain.pineau@canonical.com>
7
# Checkbox is free software: you can redistribute it and/or modify
8
# it under the terms of the GNU General Public License version 3,
9
# as published by the Free Software Foundation.
11
# Checkbox is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
# GNU General Public License for more details.
16
# You should have received a copy of the GNU General Public License
17
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
20
:mod:`checkbox_ng.misc` -- Other stuff
21
======================================
25
THIS MODULE DOES NOT HAVE STABLE PUBLIC API
28
from logging import getLogger
31
logger = getLogger("checkbox.ng.commands.cli")
37
JobTreeNode class is used to store a tree structure.
39
A tree consists of a collection of JobTreeNode instances connected in a
40
hierarchical way where nodes are used as categories, jobs belonging to a
41
category are listed in the node leaves.
51
def __init__(self, name=None):
52
""" Initialize the job tree node with a given name. """
53
self._name = name if name else 'Root'
60
""" name of this node. """
65
""" parent node for this node. """
70
""" list of sub categories. """
71
return self._categories
75
""" job(s) belonging to this node/category. """
80
""" level of depth for this node. """
81
return (self._parent.depth + 1) if self._parent else 0
84
""" same as self.name. """
88
""" Get a representation of this node for debugging. """
89
return "<JobTreeNode name:{!r}>".format(self.name)
91
def add_category(self, category):
93
Add a new category to this node.
96
The node instance to be added as a category.
98
self._categories.append(category)
99
# Always keep this list sorted to easily find a given child by index
100
self._categories.sort(key=lambda item: item.name)
101
category._parent = self
103
def add_job(self, job):
105
Add a new job to this node.
108
The job instance to be added to this node.
110
self._jobs.append(job)
111
# Always keep this list sorted to easily find a given leaf by index
112
# Note bisect.insort(a, x) cannot be used here as JobDefinition are
114
self._jobs.sort(key=lambda item: item.id)
116
def get_ancestors(self):
117
""" Get the list of ancestors from here to the root of the tree. """
120
while node.parent is not None:
121
ancestors.append(node.parent)
125
def get_descendants(self):
126
""" Return a list of all descendant category nodes. """
128
for category in self.categories:
129
descendants.append(category)
130
descendants.extend(category.get_descendants())
134
def create_tree(cls, session_state, job_list):
136
Build a rooted JobTreeNode from a job list.
138
:argument session_state:
139
A session state object
141
List of jobs to consider for building the tree.
143
builder = TreeBuilder(session_state, cls)
145
builder.auto_add_job(job)
146
return builder.root_node
149
def create_simple_tree(cls, sa, job_list):
151
Build a rooted JobTreeNode from a job list.
154
A session assistant object
156
List of jobs to consider for building the tree.
160
cat_name = sa.get_category(job.category_id).tr_name()
161
matches = [n for n in root_node.categories if n.name == cat_name]
164
root_node.add_category(node)
174
Builder for :class:`JobTreeNode`.
177
Helper class that assists in building a tree of :class:`JobTreeNode`
178
objects out of job definitions and their associations, as expressed by
179
:attr:`JobState.via_job` associated with each job.
181
The builder is a single-use object and should be re-created for each new
182
construct. Internally it stores the job_state_map of the
183
:class:`SessionState` it was created with as well as additional helper
187
def __init__(self, session_state: "SessionState", node_cls):
188
self._job_state_map = session_state.job_state_map
189
self._node_cls = node_cls
190
self._root_node = node_cls()
191
self._category_node_map = {} # id -> node
195
return self._root_node
197
def auto_add_job(self, job):
199
Add a job to the tree, automatically creating category nodes as needed.
202
The job definition to add.
204
if job.plugin == 'local':
205
# For local jobs, just create the category node but don't add the
206
# local job itself there.
207
self.get_or_create_category_node(job)
209
# For all other jobs, look at the parent job (if any) and create
210
# the category node out of that node. This never fails as "None" is
211
# the root_node object.
212
state = self._job_state_map[job.id]
213
node = self.get_or_create_category_node(state.via_job)
214
# Then add that job to the category node
217
def get_or_create_category_node(self, category_job):
219
Get a category node for a given job.
221
Get or create a :class:`JobTreeNode` that corresponds to the
222
category defined (somehow) by the job ``category_job``.
225
The job that describes the category. This is either a
226
plugin="local" job or a plugin="resource" job. This can also be
227
None, which is a shorthand to say "root node".
229
The ``root_node`` if ``category_job`` is None. A freshly
230
created node, created with :func:`create_category_node()` if
231
the category_job was never seen before (as recorded by the
234
logger.debug("get_or_create_category_node(%r)", category_job)
235
if category_job is None:
236
return self._root_node
237
if category_job.id not in self._category_node_map:
238
category_node = self.create_category_node(category_job)
239
# The category is added to its parent, that's either the root
240
# (if we're standalone) or the non-root category this one
242
category_state = self._job_state_map[category_job.id]
243
if category_state.via_job is not None:
244
parent_category_node = self.get_or_create_category_node(
245
category_state.via_job)
247
parent_category_node = self._root_node
248
parent_category_node.add_category(category_node)
250
category_node = self._category_node_map[category_job.id]
253
def create_category_node(self, category_job):
255
Create a category node for a given job.
257
Create a :class:`JobTreeNode` that corresponds to the category defined
258
(somehow) by the job ``category_job``.
261
The job that describes the node to create.
263
A fresh node with appropriate data.
265
logger.debug("create_category_node(%r)", category_job)
266
if category_job.summary == category_job.partial_id:
267
category_node = self._node_cls(category_job.description)
269
category_node = self._node_cls(category_job.summary)
270
self._category_node_map[category_job.id] = category_node
274
class SelectableJobTreeNode(JobTreeNode):
276
Implementation of a node in a tree that can be selected/deselected
278
def __init__(self, job=None):
279
super().__init__(job)
281
self.job_selection = {}
283
self.current_index = 0
284
self._resource_jobs = []
286
def get_node_by_index(self, index, tree=None):
288
Return the node found at the position given by index considering the
289
tree from a top-down list view.
294
for category in self.categories:
295
if index == tree.current_index:
296
tree.current_index = 0
297
return (category, None)
299
tree.current_index += 1
300
result = category.get_node_by_index(index, tree)
301
if result != (None, None):
303
for job in self.jobs:
304
if index == tree.current_index:
305
tree.current_index = 0
308
tree.current_index += 1
311
def render(self, cols=80, as_summary=True):
313
Return the tree as a simple list of categories and jobs suitable for
314
display. Jobs are properly indented to respect the tree hierarchy
315
and selection marks are added automatically at the beginning of each
318
The node titles should not exceed the width of a the terminal and
319
thus are cut to fit inside.
322
The number of columns to render.
324
Whether we display the job summaries or their partial IDs.
328
for category in self.categories:
330
if category.selected:
333
title = category.name
334
if category.jobs or category.categories:
335
if category.expanded:
336
line = prefix + self.depth * ' ' + ' - ' + title
338
line = prefix + self.depth * ' ' + ' + ' + title
340
line = prefix + self.depth * ' ' + ' ' + title
342
col_max = cols - 4 # includes len('...') + a space
343
line = line[:col_max] + '...'
344
self._flat_list.append(line)
345
self._flat_list.extend(category.render(cols, as_summary))
346
for job in self.jobs:
348
if self.job_selection[job]:
351
title = job.tr_summary()
353
title = job.partial_id
354
line = prefix + self.depth * ' ' + ' ' + title
356
col_max = cols - 4 # includes len('...') + a space
357
line = line[:col_max] + '...'
358
self._flat_list.append(line)
359
return self._flat_list
361
def add_job(self, job):
362
if job.plugin == 'resource':
363
# I don't want the user to see resources but I need to keep
364
# track of them to put them in the final selection. I also
365
# don't want to add them to the tree.
366
self._resource_jobs.append(job)
369
self.job_selection[job] = True
374
Return all the jobs currently selected
376
self._selection_list = []
377
for category in self.categories:
378
self._selection_list.extend(category.selection)
379
for job in self.job_selection:
380
if self.job_selection[job]:
381
self._selection_list.append(job)
382
return self._selection_list
385
def resource_jobs(self):
386
"""Return all the resource jobs."""
387
return self._resource_jobs
389
def set_ancestors_state(self, new_state):
391
Set the selection state of all ancestors consistently
393
# If child is set, then all ancestors must be set
397
parent.selected = new_state
398
parent = parent.parent
399
# If child is not set, then all ancestors mustn't be set
400
# unless another child of the ancestor is set
404
if any((category.selected
405
for category in parent.categories)):
407
if any((parent.job_selection[job]
408
for job in parent.job_selection)):
410
parent.selected = new_state
411
parent = parent.parent
413
def update_selected_state(self):
415
Update the category state according to its job selection
417
if any((self.job_selection[job] for job in self.job_selection)):
420
self.selected = False
422
def set_descendants_state(self, new_state):
424
Set the selection state of all descendants recursively
426
self.selected = new_state
427
for job in self.job_selection:
428
self.job_selection[job] = new_state
429
for category in self.categories:
430
category.set_descendants_state(new_state)