2 # -*- encoding: utf-8; py-indent-offset: 4 -*-
3 # +------------------------------------------------------------------+
4 # | ____ _ _ __ __ _ __ |
5 # | / ___| |__ ___ ___| | __ | \/ | |/ / |
6 # | | | | '_ \ / _ \/ __| |/ / | |\/| | ' / |
7 # | | |___| | | | __/ (__| < | | | | . \ |
8 # | \____|_| |_|\___|\___|_|\_\___|_| |_|_|\_\ |
10 # | Copyright Mathias Kettner 2014 mk@mathias-kettner.de |
11 # +------------------------------------------------------------------+
13 # This file is part of Check_MK.
14 # The official homepage is at http://mathias-kettner.de/check_mk.
16 # check_mk is free software; you can redistribute it and/or modify it
17 # under the terms of the GNU General Public License as published by
18 # the Free Software Foundation in version 2. check_mk is distributed
19 # in the hope that it will be useful, but WITHOUT ANY WARRANTY; with-
20 # out even the implied warranty of MERCHANTABILITY or FITNESS FOR A
21 # PARTICULAR PURPOSE. See the GNU General Public License for more de-
22 # tails. You should have received a copy of the GNU General Public
23 # License along with GNU Make; see the file COPYING. If not, write
24 # to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
25 # Boston, MA 02110-1301 USA.
27 from contextlib
import contextmanager
31 import cmk
.gui
.utils
as utils
32 import cmk
.gui
.config
as config
33 from cmk
.gui
.i18n
import _
34 from cmk
.gui
.globals import html
35 from cmk
.gui
.htmllib
import HTML
39 def table_element(table_id
=None, title
=None, **kwargs
):
41 table
= Table(table_id
, title
, **kwargs
)
45 table
._finish
_previous
()
50 # .--Table---------------------------------------------------------------.
52 # | |_ _|_ _| |__ | | ___ |
53 # | | |/ _` | '_ \| |/ _ \ |
54 # | | | (_| | |_) | | __/ |
55 # | |_|\__,_|_.__/|_|\___| |
57 # +----------------------------------------------------------------------+
61 # | with table_element() as table: |
63 # | table.cell("header", "content") |
65 # '----------------------------------------------------------------------'
69 def __init__(self
, table_id
=None, title
=None, **kwargs
):
70 super(Table
, self
).__init
__()
71 self
.next_func
= lambda: None
72 self
.next_header
= None
74 # Use our pagename as table id if none is specified
75 table_id
= table_id
if table_id
is not None else html
.myfile
79 limit
= config
.table_row_limit
82 limit
= kwargs
.get('limit', limit
)
83 if html
.request
.var('limit') == 'none' or kwargs
.get("output_format", "html") != "html":
92 "collect_headers": False, # also: True, "finished"
93 "omit_if_empty": kwargs
.get("omit_if_empty", False),
94 "omit_headers": kwargs
.get("omit_headers", False),
95 "searchable": kwargs
.get("searchable", True),
96 "sortable": kwargs
.get("sortable", True),
97 "foldable": kwargs
.get("foldable", False),
98 "output_format": kwargs
.get("output_format", "html"), # possible: html, csv, fetch
101 self
.empty_text
= kwargs
.get("empty_text", _("No entries."))
102 self
.help = kwargs
.get("help", None)
103 self
.css
= kwargs
.get("css", None)
106 def row(self
, *posargs
, **kwargs
):
107 self
._finish
_previous
()
108 self
.next_func
= lambda: self
._add
_row
(*posargs
, **kwargs
)
110 def text_cell(self
, *args
, **kwargs
):
111 self
.cell(*args
, escape_text
=True, **kwargs
)
113 def cell(self
, *posargs
, **kwargs
):
114 self
._finish
_previous
()
115 self
.next_func
= lambda: self
._add
_cell
(*posargs
, **kwargs
)
117 def _finish_previous(self
):
119 self
.next_func
= lambda: None
121 def _add_row(self
, css
=None, state
=0, collect_headers
=True, fixed
=False, **attrs
):
123 self
.rows
.append((self
.next_header
, None, "header", True, attrs
))
124 self
.next_header
= None
125 self
.rows
.append(([], css
, state
, fixed
, attrs
))
127 if self
.options
["collect_headers"] is False:
128 self
.options
["collect_headers"] = True
129 elif self
.options
["collect_headers"] is True:
130 self
.options
["collect_headers"] = "finished"
131 elif not collect_headers
and self
.options
["collect_headers"] is True:
132 self
.options
["collect_headers"] = False
143 text
= html
.permissive_attrencode(text
)
145 if isinstance(text
, HTML
):
147 if not isinstance(text
, unicode):
150 htmlcode
= text
+ html
.drain()
152 if self
.options
["collect_headers"] is True:
153 # small helper to make sorting introducion easier. Cells which contain
154 # buttons are never sortable
155 if css
and 'buttons' in css
and sortable
:
157 self
.headers
.append((title
, css
, help_txt
, sortable
))
159 self
.rows
[-1][0].append((htmlcode
, css
, colspan
))
161 # Intermediate title, shown as soon as there is a following row.
162 # We store the group headers in the list of rows, with css None
163 # and state set to "header"
164 def groupheader(self
, title
):
165 self
.next_header
= title
168 if not self
.rows
and self
.options
["omit_if_empty"]:
171 if self
.options
["output_format"] == "csv":
172 self
._write
_csv
(csv_separator
=html
.request
.var("csv_separator", ";"))
176 if self
.options
["foldable"]:
177 html
.begin_foldable_container(
182 title
=html
.render_h3(self
.title
, class_
=["treeangle", "title"]))
185 html
.write(self
.title
)
192 html
.div(self
.empty_text
, class_
="info")
195 # Controls whether or not actions are available for a table
196 rows
, actions_enabled
, actions_visible
, search_term
, user_opts
= self
._evaluate
_user
_opts
()
198 # Apply limit after search / sorting etc.
199 num_rows_unlimited
= len(rows
)
201 if limit
is not None:
202 # only use rows up to the limit plus the fixed rows
203 rows
= [rows
[i
] for i
in range(num_rows_unlimited
) if i
< limit
or rows
[i
][3]]
204 # Display corrected number of rows
205 num_rows_unlimited
-= len([r
for r
in rows
if r
[3]])
208 self
._write
_table
(rows
, actions_enabled
, actions_visible
, search_term
)
210 if self
.title
and self
.options
["foldable"]:
211 html
.end_foldable_container()
213 if limit
is not None and num_rows_unlimited
> limit
:
215 _('This table is limited to show only %d of %d rows. '
216 'Click <a href="%s">here</a> to disable the limitation.') %
217 (limit
, num_rows_unlimited
, html
.makeuri([('limit', 'none')])))
220 config
.user
.save_file("tableoptions", user_opts
)
223 def _evaluate_user_opts(self
):
229 actions_enabled
= (self
.options
["searchable"] or self
.options
["sortable"])
231 if not actions_enabled
:
232 return rows
, False, False, None, None
234 user_opts
= config
.user
.load_file("tableoptions", {})
235 user_opts
.setdefault(table_id
, {})
236 table_opts
= user_opts
[table_id
]
238 # Handle the initial visibility of the actions
239 actions_visible
= user_opts
[table_id
].get('actions_visible', False)
240 if html
.request
.var('_%s_actions' % table_id
):
241 actions_visible
= html
.request
.var('_%s_actions' % table_id
) == '1'
242 user_opts
[table_id
]['actions_visible'] = actions_visible
244 if html
.request
.var('_%s_reset' % table_id
):
245 html
.request
.del_var('_%s_search' % table_id
)
246 if 'search' in table_opts
:
247 del table_opts
['search'] # persist
249 if self
.options
["searchable"]:
250 # Search is always lower case -> case insensitive
251 search_term
= html
.get_unicode_input('_%s_search' % table_id
,
252 table_opts
.get('search', '')).lower()
254 html
.request
.set_var('_%s_search' % table_id
, search_term
)
255 table_opts
['search'] = search_term
# persist
256 rows
= _filter_rows(rows
, search_term
)
258 if html
.request
.var('_%s_reset_sorting' % table_id
):
259 html
.request
.del_var('_%s_sort' % table_id
)
260 if 'sort' in table_opts
:
261 del table_opts
['sort'] # persist
263 if self
.options
["sortable"]:
264 # Now apply eventual sorting settings
265 sort
= html
.request
.var('_%s_sort' % table_id
, table_opts
.get('sort'))
267 html
.request
.set_var('_%s_sort' % table_id
, sort
)
268 table_opts
['sort'] = sort
# persist
269 sort_col
, sort_reverse
= map(int, sort
.split(',', 1))
270 rows
= _sort_rows(rows
, sort_col
, sort_reverse
)
272 return rows
, actions_enabled
, actions_visible
, search_term
, user_opts
274 def _write_table(self
, rows
, actions_enabled
, actions_visible
, search_term
):
275 headinfo
= _("1 row") if len(rows
) == 1 else _("%d rows") % len(rows
)
276 html
.javascript("cmk.utils.update_header_info(%s);" % json
.dumps(headinfo
))
279 num_cols
= len(self
.headers
)
281 html
.open_table(class_
=["data", "oddeven", self
.css
])
283 # If we have no group headers then paint the headers now
284 if self
.rows
and self
.rows
[0][2] != "header":
285 self
._render
_headers
(actions_enabled
, actions_visible
)
287 if actions_enabled
and actions_visible
:
288 html
.open_tr(class_
=["data", "even0", "actions"])
289 html
.open_td(colspan
=num_cols
)
290 if not html
.in_form():
291 html
.begin_form("%s_actions" % table_id
)
293 if self
.options
["searchable"]:
294 html
.open_div(class_
="search")
295 html
.text_input("_%s_search" % table_id
)
296 html
.button("_%s_submit" % table_id
, _("Search"))
297 html
.button("_%s_reset" % table_id
, _("Reset search"))
298 html
.set_focus("_%s_search" % table_id
)
301 if html
.request
.has_var('_%s_sort' % table_id
):
302 html
.open_div(class_
=["sort"])
303 html
.button("_%s_reset_sorting" % table_id
, _("Reset sorting"))
306 if not html
.in_form():
307 html
.begin_form("%s_actions" % table_id
)
313 for nr
, (row_spec
, css
, state
, _fixed
, attrs
) in enumerate(rows
):
315 if not css
and "class_" in attrs
:
316 css
= attrs
.pop("class_")
317 if not css
and "class" in attrs
:
318 css
= attrs
.pop("class")
320 # Intermediate header
321 if state
== "header":
322 # Show the header only, if at least one (non-header) row follows
323 if nr
< len(rows
) - 1 and rows
[nr
+ 1][2] != "header":
324 html
.open_tr(class_
="groupheader")
325 html
.open_td(colspan
=num_cols
)
332 self
._render
_headers
(actions_enabled
, actions_visible
)
335 oddeven_name
= "even" if (nr
- 1) % 2 == 0 else "odd"
338 class_
=["data", "%s%d" % (oddeven_name
, state
), css
if css
else None], **attrs
)
339 for cell_content
, css_classes
, colspan
in row_spec
:
341 class_
=css_classes
if css_classes
else None,
342 colspan
=colspan
if colspan
else None)
343 html
.write(cell_content
)
347 if not rows
and search_term
:
348 html
.open_tr(class_
=["data", "odd0", "no_match"])
349 html
.td(_('Found no matching rows. Please try another search term.'), colspan
=num_cols
)
354 def _write_csv(self
, csv_separator
):
357 headers
= self
.headers
359 omit_headers
= self
.options
["omit_headers"]
361 # Apply limit after search / sorting etc.
362 if limit
is not None:
365 # If we have no group headers then paint the headers now
366 if not omit_headers
and self
.rows
and self
.rows
[0][2] != "header":
369 [html
.strip_tags(header
) or ""
370 for (header
, _css
, _help
, _sortable
) in headers
]) + "\n")
372 for row_spec
, _css
, _state
, _fixed
, _attrs
in rows
:
375 html
.strip_tags(cell_content
)
376 for cell_content
, _css_classes
, _colspan
in row_spec
380 def _render_headers(self
, actions_enabled
, actions_visible
):
381 if self
.options
["omit_headers"]:
388 for nr
, (header
, css
, help_txt
, sortable
) in enumerate(self
.headers
):
392 header
= '<span title="%s">%s</span>' % (html
.attrencode(help_txt
), header
)
394 css_class
= "header_%s" % css
if css
else None
396 if not self
.options
["sortable"] or not sortable
:
397 html
.open_th(class_
=css_class
)
400 sort
= html
.request
.var('_%s_sort' % table_id
)
402 sort_col
, sort_reverse
= map(int, sort
.split(',', 1))
404 reverse
= 1 if sort_reverse
== 0 else 0
406 action_uri
= html
.makeactionuri([('_%s_sort' % table_id
, '%d,%d' % (nr
, reverse
))])
408 class_
=["sort", css_class
],
409 title
=_("Sort by %s") % text
,
410 onclick
="location.href='%s'" % action_uri
)
412 # Add the table action link
417 header
= " " # Fixes layout problem with white triangle
420 help_txt
= _('Hide table actions')
421 img
= 'table_actions_on'
424 help_txt
= _('Display table actions')
425 img
= 'table_actions_off'
426 html
.open_div(class_
=["toggle_actions"])
428 html
.makeuri([('_%s_actions' % table_id
, state
)]),
431 cssclass
='toggle_actions')
445 def _filter_rows(rows
, search_term
):
447 match_regex
= re
.compile(search_term
, re
.IGNORECASE
)
449 for row_spec
, css
, state
, fixed
, attrs
in rows
:
450 if state
== "header" or fixed
:
451 filtered_rows
.append((row_spec
, css
, state
, fixed
, attrs
))
452 continue # skip filtering of headers or fixed rows
454 for cell_content
, _css_classes
, _colspan
in row_spec
:
455 if match_regex
.search(cell_content
):
456 filtered_rows
.append((row_spec
, css
, state
, fixed
, attrs
))
457 break # skip other cells when matched
461 def _sort_rows(rows
, sort_col
, sort_reverse
):
463 # remove and remind fixed rows, add to separate list
465 for index
, row_spec
in enumerate(rows
[:]):
466 if row_spec
[3] is True:
467 rows
.remove(row_spec
)
468 fixed_rows
.append((index
, row_spec
))
470 # Then use natural sorting to sort the list. Note: due to a
471 # change in the number of columns of a table in different software
472 # versions the cmp-function might fail. This is because the sorting
473 # column is persisted in a user file. So we ignore exceptions during
474 # sorting. This gives the user the chance to change the sorting and
475 # see the table in the first place.
478 cmp=lambda a
, b
: utils
.cmp_num_split(a
[0][sort_col
][0], b
[0][sort_col
][0]),
479 reverse
=sort_reverse
== 1)
483 # Now re-add the removed "fixed" rows to the list again
485 for index
, row_spec
in fixed_rows
:
486 rows
.insert(index
, row_spec
)