diff --git a/src/octoprint/plugin/types.py b/src/octoprint/plugin/types.py index 6f72856a..aa13271b 100644 --- a/src/octoprint/plugin/types.py +++ b/src/octoprint/plugin/types.py @@ -746,6 +746,21 @@ class UiPlugin(OctoPrintPlugin, SortablePlugin): """ return None + def get_ui_additional_etag(self, default_additional): + """ + Allows to provide a list of additional fields to use for ETag generation. + + By default the same list will be returned that is also used in the stock UI (and injected + via the parameter ``default_additional``). + + Arguments: + default_additional (list): The list of default fields added to the ETag of the default UI + + Returns: + (list): A list of additional fields for the ETag generation, or None + """ + return default_additional + def get_ui_custom_lastmodified(self): """ Allows to calculate the LastModified differently than using the most recent modification @@ -806,6 +821,25 @@ class UiPlugin(OctoPrintPlugin, SortablePlugin): """ return False + def get_ui_custom_template_filter(self, default_template_filter): + """ + Allows to specify a custom template filter to use for filtering the template contained in the + ``render_kwargs`` provided to the templating sub system. + + Only relevant for UiPlugins that actually utilize the stock templates of OctoPrint. + + By default simply returns the provided ``default_template_filter``. + + Arguments: + default_template_filter (callable): The default template filter used by the default UI + + Returns: + (callable) A filter function accepting the ``template_type`` and ``template_key`` of a template + and returning ``True`` to keep it and ``False`` to filter it out. If ``None`` is returned, no + filtering will take place. + """ + return default_template_filter + class WizardPlugin(OctoPrintPlugin, ReloadNeedingPlugin): """ The ``WizardPlugin`` mixin allows plugins to report to OctoPrint whether diff --git a/src/octoprint/server/views.py b/src/octoprint/server/views.py index c25c748f..9e3afe3b 100644 --- a/src/octoprint/server/views.py +++ b/src/octoprint/server/views.py @@ -174,7 +174,27 @@ def index(): _templates[locale], _plugin_names, _plugin_vars = _process_templates() now = datetime.datetime.utcnow() - render_kwargs = _get_render_kwargs(_templates[locale], _plugin_names, _plugin_vars, now) + + enable_accesscontrol = userManager.enabled + enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"]) + enable_timelapse = bool(settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"])) + + def default_template_filter(template_type, template_key): + if template_type == "navbar": + return template_key != "login" or enable_accesscontrol + elif template_type == "tab": + return (template_key != "gcodeviewer" or enable_gcodeviewer) and \ + (template_key != "timelapse" or enable_timelapse) + elif template_type == "settings": + return template_key != "accesscontrol" or enable_accesscontrol + elif template_type == "usersettings": + return enable_accesscontrol + else: + return True + + default_additional_etag = [enable_accesscontrol, + enable_gcodeviewer, + enable_timelapse] def get_preemptively_cached_view(key, view, data=None, additional_request_data=None, additional_unless=None): if (data is None and additional_request_data is None) or g.locale is None: @@ -190,7 +210,10 @@ def index(): data=d, unless=unless)(view) - def get_cached_view(key, view, additional_key_data=None, additional_files=None, custom_files=None, custom_etag=None, custom_lastmodified=None): + def get_cached_view(key, view, additional_key_data=None, additional_files=None, additional_etag=None, custom_files=None, custom_etag=None, custom_lastmodified=None): + if additional_etag is None: + additional_etag = [] + def cache_key(): return _cache_key(key, additional_key_data=additional_key_data) @@ -200,11 +223,11 @@ def index(): lastmodified_ok = util.flask.check_lastmodified(lastmodified) etag_ok = util.flask.check_etag(compute_etag(files=files, lastmodified=lastmodified, - additional=cache_key())) + additional=[cache_key()] + additional_etag)) return lastmodified_ok and etag_ok def validate_cache(cached): - etag_different = compute_etag(additional=cache_key()) != cached.get_etag()[0] + etag_different = compute_etag(additional=[cache_key()] + additional_etag) != cached.get_etag()[0] return force_refresh or etag_different def collect_files(): @@ -273,7 +296,7 @@ def index(): if lastmodified: hash.update(lastmodified) for add in additional: - hash.update(add) + hash.update(str(add)) return hash.hexdigest() decorated_view = view @@ -293,7 +316,8 @@ def index(): additional_files=p.get_ui_additional_tracked_files, custom_files=p.get_ui_custom_tracked_files, custom_etag=p.get_ui_custom_etag, - custom_lastmodified=p.get_ui_custom_lastmodified) + custom_lastmodified=p.get_ui_custom_lastmodified, + additional_etag=p.get_ui_additional_etag(default_additional_etag)) if preemptive_cache_enabled and p.get_ui_preemptive_caching_enabled(): view = get_preemptively_cached_view(p._identifier, @@ -304,12 +328,30 @@ def index(): else: view = cached + template_filter = p.get_ui_custom_template_filter(default_template_filter) + if template_filter is not None and callable(template_filter): + filtered_templates = _filter_templates(_templates[locale], template_filter) + else: + filtered_templates = _templates[locale] + + render_kwargs = _get_render_kwargs(filtered_templates, + _plugin_names, + _plugin_vars, + now) + return view(now, request, render_kwargs) def default_view(): - wizard = wizard_active(_templates[locale]) - enable_accesscontrol = userManager.enabled + filtered_templates = _filter_templates(_templates[locale], default_template_filter) + + wizard = wizard_active(filtered_templates) accesscontrol_active = enable_accesscontrol and userManager.hasBeenCustomized() + + render_kwargs = _get_render_kwargs(filtered_templates, + _plugin_names, + _plugin_vars, + now) + render_kwargs.update(dict( webcamStream=settings().get(["webcam", "stream"]), enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]), @@ -331,7 +373,8 @@ def index(): return r cached = get_cached_view("_default", - make_default_ui) + make_default_ui, + additional_etag=default_additional_etag) preemptively_cached = get_preemptively_cached_view("_default", cached, dict(), @@ -396,12 +439,7 @@ def _get_render_kwargs(templates, plugin_names, plugin_vars, now): def _process_templates(): - enable_accesscontrol = userManager.enabled first_run = settings().getBoolean(["server", "firstRun"]) - enable_gcodeviewer = settings().getBoolean(["gcodeViewer", "enabled"]) - enable_timelapse = (settings().get(["webcam", "snapshot"]) and settings().get(["webcam", "ffmpeg"])) - enable_systemmenu = settings().get(["system"]) is not None and settings().get(["system", "actions"]) is not None - preferred_stylesheet = settings().get(["devel", "stylesheet"]) ##~~ prepare templates @@ -473,12 +511,10 @@ def _process_templates(): # navbar templates["navbar"]["entries"] = dict( - settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin") + settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"], data_bind="visible: loginState.isAdmin"), + systemmenu=dict(template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin", custom_bindings=False), + login=dict(template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"], custom_bindings=False), ) - if enable_accesscontrol: - templates["navbar"]["entries"]["login"] = dict(template="navbar/login.jinja2", _div="navbar_login", classes=["dropdown"], custom_bindings=False) - if enable_systemmenu: - templates["navbar"]["entries"]["systemmenu"] = dict(template="navbar/systemmenu.jinja2", _div="navbar_systemmenu", styles=["display: none"], classes=["dropdown"], data_bind="visible: loginState.isAdmin", custom_bindings=False) # sidebar @@ -493,12 +529,10 @@ def _process_templates(): templates["tab"]["entries"] = dict( temperature=(gettext("Temperature"), dict(template="tabs/temperature.jinja2", _div="temp")), control=(gettext("Control"), dict(template="tabs/control.jinja2", _div="control")), + gcodeviewer=(gettext("GCode Viewer"), dict(template="tabs/gcodeviewer.jinja2", _div="gcode")), terminal=(gettext("Terminal"), dict(template="tabs/terminal.jinja2", _div="term")), + timelapse=(gettext("Timelapse"), dict(template="tabs/timelapse.jinja2", _div="timelapse")) ) - if enable_gcodeviewer: - templates["tab"]["entries"]["gcodeviewer"] = (gettext("GCode Viewer"), dict(template="tabs/gcodeviewer.jinja2", _div="gcode")) - if enable_timelapse: - templates["tab"]["entries"]["timelapse"] = (gettext("Timelapse"), dict(template="tabs/timelapse.jinja2", _div="timelapse")) # settings dialog @@ -520,21 +554,19 @@ def _process_templates(): section_octoprint=(gettext("OctoPrint"), None), + accesscontrol=(gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False)), folders=(gettext("Folders"), dict(template="dialogs/settings/folders.jinja2", _div="settings_folders", custom_bindings=False)), appearance=(gettext("Appearance"), dict(template="dialogs/settings/appearance.jinja2", _div="settings_appearance", custom_bindings=False)), logs=(gettext("Logs"), dict(template="dialogs/settings/logs.jinja2", _div="settings_logs")), server=(gettext("Server"), dict(template="dialogs/settings/server.jinja2", _div="settings_server", custom_bindings=False)), ) - if enable_accesscontrol: - templates["settings"]["entries"]["accesscontrol"] = (gettext("Access Control"), dict(template="dialogs/settings/accesscontrol.jinja2", _div="settings_users", custom_bindings=False)) # user settings dialog - if enable_accesscontrol: - templates["usersettings"]["entries"] = dict( - access=(gettext("Access"), dict(template="dialogs/usersettings/access.jinja2", _div="usersettings_access", custom_bindings=False)), - interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)), - ) + templates["usersettings"]["entries"] = dict( + access=(gettext("Access"), dict(template="dialogs/usersettings/access.jinja2", _div="usersettings_access", custom_bindings=False)), + interface=(gettext("Interface"), dict(template="dialogs/usersettings/interface.jinja2", _div="usersettings_interface", custom_bindings=False)), + ) # wizard @@ -784,6 +816,19 @@ def _process_template_config(name, implementation, rule, config=None, counter=1) return data +def _filter_templates(templates, template_filter): + filtered_templates = dict() + for template_type, template_collection in templates.items(): + filtered_entries = dict() + for template_key, template_entry in template_collection["entries"].items(): + if template_filter(template_type, template_key): + filtered_entries[template_key] = template_entry + filtered_templates[template_type] = dict(order=filter(lambda x: x in filtered_entries, + template_collection["order"]), + entries=filtered_entries) + return filtered_templates + + @app.route("/robots.txt") @util.flask.cached(timeout=-1) def robotsTxt():