def _show_file_breadcrumb(host_name: HostName, title: str) -> Breadcrumb: breadcrumb = make_host_breadcrumb(host_name) breadcrumb.append( BreadcrumbItem( title=_("Log files of host %s") % host_name, url=makeuri(request, [("file", "")]), ) ) breadcrumb.append(make_current_page_breadcrumb_item(title)) return breadcrumb
def _show_audit_log_options_controls(self): html.open_div(class_="side_popup_controls") html.open_div(class_="update_buttons") html.button("apply", _("Apply"), "submit") html.buttonlink(makeuri(request, [], remove_prefix="options_"), _("Reset")) html.close_div() html.close_div()
def _show_status(self) -> None: job_status = self._get_job_status() html.h3(_("Job status")) if job_status["is_active"]: html.immediate_browser_redirect(0.8, makeuri(request, [])) job = FetchAgentOutputBackgroundJob(self._request) gui_background_job.JobRenderer.show_job_details( job.get_job_id(), job_status)
def _show_form(self, profile_changed: bool) -> None: assert config.user.id is not None users = userdb.load_users() if profile_changed: html.reload_sidebar() html.show_message(_("Successfully updated user profile.")) # Ensure theme changes are applied without additional user interaction html.immediate_browser_redirect(0.5, makeuri(global_request, [])) if html.has_user_errors(): html.show_user_errors() user = users.get(config.user.id) if user is None: html.show_warning(_("Sorry, your user account does not exist.")) html.footer() return html.begin_form("profile", method="POST") html.prevent_password_auto_completion() html.open_div(class_="wato") forms.header(_("Personal settings")) forms.section(_("Name"), simple=True) html.write_text(user.get("alias", config.user.id)) select_language(user) # Let the user configure how he wants to be notified rulebased_notifications = rulebased_notifications_enabled() if (not rulebased_notifications and config.user.may('general.edit_notifications') and user.get("notifications_enabled")): forms.section(_("Notifications")) html.help( _("Here you can configure how you want to be notified about host and service problems and " "other monitoring events.")) watolib.get_vs_flexible_notifications().render_input( "notification_method", user.get("notification_method")) if config.user.may('general.edit_user_attributes'): custom_user_attr_topics = get_user_attributes_by_topic() _show_custom_user_attr(user, custom_user_attr_topics.get("personal", [])) forms.header(_("Interface settings"), isopen=False) _show_custom_user_attr( user, custom_user_attr_topics.get("interface", [])) forms.end() html.close_div() html.hidden_fields() html.end_form() html.footer()
def _redirect_for_two_factor_authentication(user_id: UserId) -> None: if requested_file_name(request) in ( "user_login_two_factor", "user_webauthn_login_begin", "user_webauthn_login_complete", ): return if userdb.is_two_factor_login_enabled( user_id) and not userdb.is_two_factor_completed(): raise HTTPRedirect("user_login_two_factor.py?_origtarget=%s" % urlencode(makeuri(request, [])))
def query_limit_exceeded_warn(limit: Optional[int], user_config: 'LoggedInUser') -> None: """Compare query reply against limits, warn in the GUI about incompleteness""" text = HTML(_("Your query produced more than %d results. ") % limit) if html.request.get_ascii_input( "limit", "soft") == "soft" and user_config.may("general.ignore_soft_limit"): text += html.render_a(_('Repeat query and allow more results.'), target="_self", href=makeuri(request, [("limit", "hard")])) elif html.request.get_ascii_input("limit") == "hard" and user_config.may( "general.ignore_hard_limit"): text += html.render_a(_('Repeat query without limit.'), target="_self", href=makeuri(request, [("limit", "none")])) text += " " + _( "<b>Note:</b> the shown results are incomplete and do not reflect the sort order." ) html.show_warning(text)
def _extend_display_dropdown(self, menu: PageMenu) -> None: display_dropdown = menu.get_dropdown_by_name( "display", make_display_options_dropdown()) display_dropdown.topics.insert( 0, PageMenuTopic( title=_("Filter"), entries=[ PageMenuEntry( title=_("Filter view"), icon_name="filters_set" if html.form_submitted("options") else "filters", item=PageMenuSidePopup(self._render_filter_form()), name="filters", is_shortcut=True, ), ], )) display_dropdown.topics.insert( 0, PageMenuTopic( title=_("Details"), entries=[ PageMenuEntry( title=_("Show details"), icon_name="trans", item=PageMenuCheckbox( is_checked=self._show_details, check_url=makeuri(request, [("show_details", "1")]), uncheck_url=makeuri(request, [("show_details", "0")]), ), name="show_details", css_classes=["toggle"], ) ], ))
def page_menu_entries_related_monitoring( self, crash_info: CrashInfo, site_id: SiteId) -> Iterator[PageMenuEntry]: host = crash_info["details"]["host"] service = crash_info["details"]["description"] host_url = makeuri( request, [ ("view_name", "hoststatus"), ("host", host), ("site", site_id), ], filename="view.py", ) yield PageMenuEntry( title=_("Host status"), icon_name="status", item=make_simple_link(host_url), ) service_url = makeuri( request, [ ("view_name", "service"), ("host", host), ("service", service), ( "site", site_id, ), ], filename="view.py", ) yield PageMenuEntry( title=_("Service status"), icon_name="status", item=make_simple_link(service_url), )
def _page_menu_entries_details(self) -> Iterator[PageMenuEntry]: for toggle_id, title, setting in [ ("_show_host_tags", _("host tags"), config.user.wato_folders_show_tags), ("_show_explicit_labels", _("explicit host labels"), config.user.wato_folders_show_labels), ]: yield PageMenuEntry( title=_("Show %s" % title), icon_name="checked_checkbox" if setting else "checkbox", item=make_simple_link( makeuri(global_request, [ (toggle_id, "" if setting else "1"), ])), )
def _render_manpage_list(titles, manpage_list, path_comp, heading): def translate(t): return titles.get(t, t) html.h3(heading) with table_element(searchable=False, sortable=False, css="check_catalog") as table: for entry in sorted(manpage_list, key=lambda x: x["title"]): if not isinstance(entry, dict): continue table.row() url = makeuri( request, [ ("mode", "check_manpage"), ("check_type", entry["name"]), ("back", makeuri(request, [])), ], ) table.cell(_("Type of Check"), html.render_a(entry["title"], href=url), css="title") table.cell(_("Plugin Name"), html.render_tt(entry["name"]), css="name") table.cell( _("Agents"), ", ".join(map(translate, sorted(entry["agents"]))), css="agents" )
def create_data_for_single_metric(cls, properties, context): columns, data_rows = cls._get_data(properties, context) data = [] used_metrics = [] for idx, row in enumerate(data_rows): d_row = dict(zip(columns, row)) translated_metrics = translate_perf_data(d_row["service_perf_data"], d_row["service_check_command"]) metric = translated_metrics.get(properties['metric']) if metric is None: continue series = merge_multicol(d_row, columns, properties) site = d_row['site'] host = d_row["host_name"] svc_url = makeuri( request, [("view_name", "service"), ("site", site), ("host", host), ("service", d_row['service_description'])], filename="view.py", ) row_id = "row_%d" % idx # Historic values for ts, elem in series.time_data_pairs(): if elem: data.append({ "tag": row_id, "timestamp": ts, "value": elem, "label": host, }) # Live value data.append({ "tag": row_id, "timestamp": int(time.time()), "value": metric['value'], "formatted_value": metric['unit']['render'](metric['value']), "url": svc_url, "label": host, }) used_metrics.append((row_id, metric, d_row)) return data, used_metrics
def show_create_view_dialog(next_url=None): vs_ds = DatasourceSelection() ds = "services" # Default selection title = _("Create view") breadcrumb = visuals.visual_page_breadcrumb("views", title, "create") make_header( html, title, breadcrumb, make_simple_form_page_menu( _("View"), breadcrumb, form_name="create_view", button_name="_save", save_title=_("Continue"), ), ) if request.var("_save") and transactions.check_transaction(): try: ds = vs_ds.from_html_vars("ds") vs_ds.validate_value(ds, "ds") if not next_url: next_url = makeuri( request, [("datasource", ds)], filename="create_view_infos.py", ) else: next_url = next_url + "&datasource=%s" % ds raise HTTPRedirect(next_url) except MKUserError as e: html.user_error(e) html.begin_form("create_view") html.hidden_field("mode", "create") forms.header(_("Select Datasource")) forms.section(vs_ds.title()) vs_ds.render_input("ds", ds) html.help(vs_ds.help()) forms.end() html.hidden_fields() html.end_form() html.footer()
def _show_crash_dump_message( crash: "GUICrashReport", plain_text: bool, fail_silently: bool, show_crash_link: Optional[bool] ) -> None: """Create a crash dump from a GUI exception and display a message to the user""" if show_crash_link is None: show_crash_link = user.may("general.see_crash_reports") title = _("Internal error") message = "%s: %s<br>\n<br>\n" % (title, crash.crash_info["exc_value"]) # Do not reveal crash context information to unauthenticated users or not permitted # users to prevent disclosure of internal information if not show_crash_link: message += _( "An internal error occurred while processing your request. " "You can report this issue to your Checkmk administrator. " "Detailed information can be found on the crash report page " "or in <tt>var/log/web.log</tt>." ) else: crash_url = makeuri( request, [ ("site", omd_site()), ("crash_id", crash.ident_to_text()), ], filename="crash.py", ) message += ( _( "An internal error occured while processing your request. " "You can report this issue to the Checkmk team to help " 'fixing this issue. Please open the <a href="%s">crash report page</a> ' "and use the form for reporting the problem." ) % crash_url ) if plain_text: response.set_content_type("text/plain") response.set_data("%s\n" % escaping.strip_tags(message)) return if fail_silently: return html.header(title, Breadcrumb()) html.show_error(message) html.footer()
def make_display_options_dropdown() -> PageMenuDropdown: return PageMenuDropdown( name="display", title=_("Display"), topics=[ PageMenuTopic( title=_("General display options"), entries=[ PageMenuEntry( title=_("This page without navigation"), icon_name="frameurl", item=PageMenuLink( Link( url=makeuri(request, []), target="_top", ) ), ), PageMenuEntry( title=_("This page with navigation"), icon_name="pageurl", item=PageMenuLink( Link( url=makeuri_contextless( request, [("start_url", makeuri(request, []))], filename="index.py", ), target="_top", ) ), ), ], ), ], )
def _handle_not_authenticated() -> Response: if fail_silently(): # While api call don't show the login dialog raise MKUnauthenticatedException(_('You are not authenticated.')) # Redirect to the login-dialog with the current url as original target # Never render the login form directly when accessing urls like "index.py" # or "dashboard.py". This results in strange problems. if html.myfile != 'login': raise HTTPRedirect('%scheck_mk/login.py?_origtarget=%s' % (config.url_prefix(), html.urlencode(makeuri(request, [])))) # This either displays the login page or validates the information submitted # to the login form. After successful login a http redirect to the originally # requested page is performed. login_page = login.LoginPage() login_page.set_no_html_output(plain_error()) login_page.handle_page() return html.response
def page(self): html.help( _("This catalog of check plugins gives you a complete listing of all plugins " "that are shipped with your Check_MK installation. It also allows you to " "access the rule sets for configuring the parameters of the checks and to " "manually create services in case you cannot or do not want to rely on the " "automatic service discovery.")) menu = MainMenu() for topic, _has_second_level, title, helptext in _man_page_catalog_topics(): menu.add_item( MenuItem(mode_or_url=makeuri( request, [("mode", "check_plugin_topic"), ("topic", topic)], ), title=title, icon="plugins_" + topic, permission=None, description=helptext)) menu.show()
def _page_menu(self, breadcrumb: Breadcrumb, crash_info: CrashInfo) -> PageMenu: return PageMenu( dropdowns=[ PageMenuDropdown( name="crash_reports", title=_("Crash reports"), topics=[ PageMenuTopic( title=_("This crash report"), entries=[ PageMenuEntry( title=_("Download"), icon_name="download", item=make_simple_link( makeuri( request, [], filename="download_crash_report.py" )), is_shortcut=True, is_suggested=True, ), ], ), ], ), PageMenuDropdown( name="related", title=_("Related"), topics=[ PageMenuTopic( title=_("Monitoring"), entries=list( self._page_menu_entries_related_monitoring( crash_info)), ), ], ), ], breadcrumb=breadcrumb, )
def _page_menu( self, breadcrumb: Breadcrumb, site_id: SiteId, host_name: HostName, service_description: str, ) -> PageMenu: return PageMenu( dropdowns=[ PageMenuDropdown( name="robotmk_reports", title=_("RobotMK reports"), topics=[ PageMenuTopic( title=_("This RobotMK report"), entries=[ PageMenuEntry( title=_("Download"), icon_name="download", item=make_simple_link( makeuri( request, [ ("site", site_id), ("host", host_name), ("service", service_description), ], filename= "download_robotmk_report.py", )), is_shortcut=True, is_suggested=True, ), ], ), ], ), ], breadcrumb=breadcrumb, )
def page_login() -> None: title = _("Checkmk Mobile") mobile_html_head(title) jqm_page_header(title, id_="login") html.div(_("Welcome to Checkmk Mobile."), id_="loginhead") html.begin_form("login", method="POST", add_transid=False) # Keep information about original target URL default_origtarget = ("index.py" if requested_file_name(request) in ["login", "logout"] else makeuri(request, [])) origtarget = request.get_url_input("_origtarget", default_origtarget) html.hidden_field("_origtarget", escaping.escape_attribute(origtarget)) html.text_input("_username", label=_("Username:"******"username", id_="input_user") html.password_input( "_password", size=None, label=_("Password:"******"current-password", id_="input_pass", ) html.br() html.button("_login", _("Login")) html.set_focus("_username") html.end_form() html.open_div(id_="loginfoot") html.img("themes/facelift/images/logo_cmk_small.png", class_="logomk") html.div( HTML( _('© <a target="_blank" href="https://checkmk.com">tribe29 GmbH</a>' )), class_="copyright", ) html.close_div() # close content-div html.close_div() html.close_div() # close page-div mobile_html_foot()
def _paint_download_host_info(what, row, tags, host_custom_vars, ty): if ((what == "host" or (what == "service" and row["service_description"] == "Check_MK")) and user.may("wato.download_agent_output") and not row["host_check_type"] == 2): # Not for shadow hosts # Not 100% acurate to use the tags here, but this is the best we can do # with the available information. # Render "download agent output" for non agent hosts, because there might # be piggyback data available which should be downloadable. if ty == "walk" and "snmp" not in tags: return None if ty == "agent" and "snmp" in tags and "tcp" not in tags: return None params = [ ("host", row["host_name"]), ("folder", _wato_folder_from_filename(row["host_filename"])), ("type", ty), ("_start", "1"), ] # When the download icon is part of the host/service action menu, then # the _back_url set in paint_action_menu() needs to be used. Otherwise # makeuri(request, []) (not request.requested_uri()) is the right choice. back_url = request.get_url_input("_back_url", makeuri(request, [])) if back_url: params.append(("back_url", back_url)) if ty == "agent": title = _("Download agent output") else: title = _("Download SNMP walk") url = makeuri_contextless(request, params, filename="fetch_agent_output.py") return "agents", title, url return None
def _check_auth_cookie(cookie_name: str) -> Optional[UserId]: username, session_id, cookie_hash = _parse_auth_cookie(cookie_name) _check_parsed_auth_cookie(username, session_id, cookie_hash) try: userdb.on_access(username, session_id) except MKAuthException: del_auth_cookie() raise # Once reached this the cookie is a good one. Renew it! _renew_cookie(cookie_name, username, session_id) if html.myfile != 'user_change_pw': result = userdb.need_to_change_pw(username) if result: raise HTTPRedirect( 'user_change_pw.py?_origtarget=%s&reason=%s' % (html.urlencode(makeuri(global_request, [])), result)) # Return the authenticated username return username
def _check_auth_cookie(cookie_name: str) -> Optional[UserId]: username, session_id, cookie_hash = user_from_cookie( _fetch_cookie(cookie_name)) check_parsed_auth_cookie(username, session_id, cookie_hash) try: userdb.on_access(username, session_id) except MKAuthException: del_auth_cookie() raise # Once reached this the cookie is a good one. Renew it! _renew_cookie(cookie_name, username, session_id) if requested_file_name(request) != "user_change_pw": result = userdb.need_to_change_pw(username) if result: raise HTTPRedirect("user_change_pw.py?_origtarget=%s&reason=%s" % (urlencode(makeuri(request, [])), result)) # Return the authenticated username return username
def render(self, what, row, tags, custom_vars): url_vars = [ ('host', row['host_name']), ] if row.get('site'): url_vars.append(('site', row['site'])) if what == 'service': url_vars.append(('service', row['service_description'])) if html.request.has_var('display_options'): url_vars.append(('display_options', html.request.var('display_options'))) if html.request.has_var('_display_options'): url_vars.append(('_display_options', html.request.var('_display_options'))) url_vars.append(('_back_url', makeuri(request, []))) return html.render_popup_trigger( html.render_icon('menu', _('Open the action menu'), cssclass="iconbutton"), 'action_menu', MethodAjax(endpoint='action_menu', url_vars=url_vars), )
def render_input(self, varprefix, value): # The display mode differs when the valuespec is activated vars_copy = dict(request.itervars()) # The timeperiod mode can be set by either the GUI switch or by the value itself # GUI switch overrules the information stored in the value if request.has_var(self.tp_toggle_var): is_active = self._is_switched_on() else: is_active = self.is_active(value) # Set the actual used mode html.hidden_field(self.tp_current_mode, "%d" % is_active) vars_copy[self.tp_toggle_var] = "%d" % (not is_active) url_vars: HTTPVariables = [] url_vars += vars_copy.items() toggle_url = makeuri(request, url_vars) if is_active: value = self._get_timeperiod_value(value) self._get_timeperiod_valuespec().render_input(varprefix, value) html.buttonlink( toggle_url, _("Disable timespecific parameters"), class_=["toggle_timespecific_parameter"], ) else: value = self._get_timeless_value(value) r = self._enclosed_valuespec.render_input(varprefix, value) html.buttonlink( toggle_url, _("Enable timespecific parameters"), class_=["toggle_timespecific_parameter"], ) return r
def _back_url(self): return makeuri(request, [])
def _page_menu( self, breadcrumb: Breadcrumb, site_id: SiteId, host_name: HostName, service_description: str, ) -> PageMenu: return PageMenu( dropdowns=[ PageMenuDropdown( name="robotmk_logs", title=_("RobotMK logs"), topics=[ PageMenuTopic( title=_("This log"), entries=[ PageMenuEntry( title=_("Download"), icon_name="download", item=make_simple_link( makeuri( request, [ ("report_type", self._report_type()), ("site", site_id), ("host", host_name), ("service", service_description), ], filename= "download_robotmk_report.py", )), is_shortcut=True, is_suggested=True, ), ], ), PageMenuTopic( title=_("Other logs for this service"), entries=[ PageMenuEntry( title=_("Last error log") if self._is_last_log() else _("Last log"), icon_name="robotmk_error" if self._is_last_log() else "robotmk", item=make_simple_link( makeuri( request, [ ( "report_type", "robotmk_error" if self._is_last_log() else "robotmk", ), ("site", site_id), ("host", host_name), ("service", service_description), ], filename="robotmk.py", )), is_shortcut=True, is_suggested=True, ), ], ), ], ), ], breadcrumb=breadcrumb, )
def _show_login_page(self) -> None: html.set_render_headfoot(False) html.add_body_css_class("login") html.header(config.get_page_heading(), Breadcrumb(), javascripts=[]) default_origtarget = ("index.py" if html.myfile in ["login", "logout"] else makeuri(global_request, [])) origtarget = html.get_url_input("_origtarget", default_origtarget) # Never allow the login page to be opened in the iframe. Redirect top page to login page. # This will result in a full screen login page. html.javascript('''if(top != self) { window.top.location.href = location; }''') # When someone calls the login page directly and is already authed redirect to main page if html.myfile == 'login' and _check_auth(html.request): raise HTTPRedirect(origtarget) html.open_div(id_="login") html.open_div(id_="login_window") html.div("" if "hide_version" in config.login_screen else cmk_version.__version__, id_="version") html.begin_form("login", method='POST', add_transid=False, action='login.py') html.hidden_field('_login', '1') html.hidden_field('_origtarget', origtarget) html.label("%s:" % _('Username'), id_="label_user", class_=["legend"], for_="_username") html.br() html.text_input("_username", id_="input_user") html.label("%s:" % _('Password'), id_="label_pass", class_=["legend"], for_="_password") html.br() html.password_input("_password", id_="input_pass", size=None) if html.has_user_errors(): html.open_div(id_="login_error") html.show_user_errors() html.close_div() html.open_div(id_="button_text") html.button("_login", _('Login')) html.close_div() html.close_div() html.open_div(id_="foot") if config.login_screen.get("login_message"): html.open_div(id_="login_message") html.show_message(config.login_screen["login_message"]) html.close_div() footer: List[Union[HTML, str]] = [] for title, url, target in config.login_screen.get("footer_links", []): footer.append(html.render_a(title, href=url, target=target)) if "hide_version" not in config.login_screen: footer.append("Version: %s" % cmk_version.__version__) footer.append("© %s" % html.render_a( "tribe29 GmbH", href="https://checkmk.com", target="_blank")) html.write(HTML(" - ").join(footer)) if cmk_version.is_raw_edition(): html.br() html.br() html.write( _('You can use, modify and distribute Check_MK under the terms of the <a href="%s" target="_blank">' 'GNU GPL Version 2</a>.') % "https://checkmk.com/gpl.html") html.close_div() html.set_focus('_username') html.hidden_fields() html.end_form() html.close_div() html.footer()
def add_url(cls): return 'create_view_dashlet.py?name=%s&mode=create&back=%s' % \ (urlencode(html.request.var('name')), urlencode(makeuri(request, [('edit', '1')])))
def _render_headers(self, actions_enabled: bool, actions_visible: bool, empty_columns: List[bool]) -> None: if self.options["omit_headers"]: return table_id = self.id html.open_tr() first_col = True for nr, header in enumerate(self.headers): if self.options["omit_empty_columns"] and empty_columns[nr]: continue if header.help_txt: header_title: HTML = html.render_span(header.title, title=header.help_txt) else: header_title = header.title if not isinstance(header.css, list): css_class: "CSSSpec" = [header.css] else: css_class = header.css assert isinstance(css_class, list) css_class = [("header_%s" % c) for c in css_class if c is not None] if not self.options["sortable"] or not header.sortable: html.open_th(class_=css_class) else: css_class.insert(0, "sort") reverse = 0 sort = request.get_ascii_input("_%s_sort" % table_id) if sort: sort_col, sort_reverse = map(int, sort.split(",", 1)) if sort_col == nr: reverse = 1 if sort_reverse == 0 else 0 action_uri = makeactionuri(request, transactions, [("_%s_sort" % table_id, "%d,%d" % (nr, reverse))]) html.open_th( class_=css_class, title=_("Sort by %s") % header.title, onclick="location.href='%s'" % action_uri, ) # Add the table action link if first_col: first_col = False if actions_enabled: if not header_title: header_title = HTML( " " ) # Fixes layout problem with white triangle if actions_visible: state = "0" help_txt = _("Hide table actions") img = "table_actions_on" else: state = "1" help_txt = _("Display table actions") img = "table_actions_off" html.open_div(class_=["toggle_actions"]) html.icon_button( makeuri(request, [("_%s_actions" % table_id, state)]), help_txt, img, cssclass="toggle_actions", ) html.span(header_title) html.close_div() else: html.write_text(header_title) else: html.write_text(header_title) html.close_th() html.close_tr()
def _end(self) -> None: if not self.rows and self.options["omit_if_empty"]: return if self.options["output_format"] == "csv": self._write_csv(csv_separator=request.get_str_input_mandatory( "csv_separator", ";")) return container: ContextManager[bool] = nullcontext(False) if self.title: if self.options["foldable"] in [ Foldable.FOLDABLE_SAVE_STATE, Foldable.FOLDABLE_STATELESS, ]: html.open_div(class_="foldable_wrapper") container = foldable_container( treename="table", id_=self.id, isopen=True, indent=False, title=html.render_h3(self.title, class_=["treeangle", "title"]), save_state=self.options["foldable"] == Foldable.FOLDABLE_SAVE_STATE, ) else: html.h3(self.title, class_="table") with container: if self.help: html.help(self.help) if not self.rows: html.div(self.empty_text, class_="info") return # Controls whether or not actions are available for a table rows, actions_visible, search_term = self._evaluate_user_opts() # Apply limit after search / sorting etc. num_rows_unlimited = len(rows) limit = self.limit if limit: # only use rows up to the limit plus the fixed rows limited_rows = [] for index in range(num_rows_unlimited): row = rows[index] if index < limit or isinstance(row, GroupHeader) or row.fixed: limited_rows.append(row) # Display corrected number of rows num_rows_unlimited -= len([ r for r in limited_rows if isinstance(row, GroupHeader) or r.fixed ]) rows = limited_rows # Render header if self.limit_hint is not None: num_rows_unlimited = self.limit_hint if limit and num_rows_unlimited > limit: html.show_message( _("This table is limited to show only %d of %d rows. " 'Click <a href="%s">here</a> to disable the limitation.') % (limit, num_rows_unlimited, makeuri(request, [("limit", "none")]))) self._write_table(rows, num_rows_unlimited, self._show_action_row(), actions_visible, search_term) if self.title and self.options["foldable"] in [ Foldable.FOLDABLE_SAVE_STATE, Foldable.FOLDABLE_STATELESS, ]: html.close_div() return