Exemple #1
0
def in_cache():
    url = request.base_url.replace("/cached.gif", "/")
    path = request.path.replace("/cached.gif", "/")
    base_url = request.url_root

    # select view from plugins and fall back on default view if no plugin will handle it
    ui_plugins = pluginManager.get_implementations(
        octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render")
    for plugin in ui_plugins:
        if plugin.will_handle_ui(request):
            ui = plugin._identifier
            key = _cache_key(plugin._identifier,
                             url=url,
                             additional_key_data=plugin.
                             get_ui_additional_key_data_for_cache)
            unless = _preemptive_unless(
                url,
                additional_unless=plugin.
                get_ui_preemptive_caching_additional_unless)
            data = _preemptive_data(
                plugin._identifier,
                path=path,
                base_url=base_url,
                data=plugin.get_ui_data_for_preemptive_caching,
                additional_request_data=plugin.
                get_ui_additional_request_data_for_preemptive_caching)
            break
    else:
        ui = "_default"
        key = _cache_key("_default", url=url)
        unless = _preemptive_unless(url)
        data = _preemptive_data("_default", path=path, base_url=base_url)

    response = make_response(
        bytes(
            base64.b64decode(
                "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")))
    response.headers["Content-Type"] = "image/gif"

    if unless or not preemptiveCache.has_record(data, root=path):
        _logger.info(
            "Preemptive cache not active for path {}, ui {} and data {!r}, signaling as cached"
            .format(path, ui, data))
        return response
    elif util.flask.is_in_cache(key):
        _logger.info(
            "Found path {} in cache (key: {}), signaling as cached".format(
                path, key))
        return response
    elif util.flask.is_cache_bypassed(key):
        _logger.info(
            "Path {} was bypassed from cache (key: {}), signaling as cached".
            format(path, key))
        return response
    else:
        _logger.debug(
            "Path {} not yet cached (key: {}), signaling as missing".format(
                path, key))
        return abort(404)
Exemple #2
0
def in_cache():
	url = request.base_url.replace("/cached.gif", "/")
	path = request.path.replace("/cached.gif", "/")
	base_url = request.url_root

	# select view from plugins and fall back on default view if no plugin will handle it
	ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin,
	                                               sorting_context="UiPlugin.on_ui_render")
	for plugin in ui_plugins:
		try:
			if plugin.will_handle_ui(request):
				ui = plugin._identifier
				key = _cache_key(plugin._identifier,
				                 url=url,
				                 additional_key_data=plugin.get_ui_additional_key_data_for_cache)
				unless = _preemptive_unless(url, additional_unless=plugin.get_ui_preemptive_caching_additional_unless)
				data = _preemptive_data(plugin._identifier,
				                        path=path,
				                        base_url=base_url,
				                        data=plugin.get_ui_data_for_preemptive_caching,
				                        additional_request_data=plugin.get_ui_additional_request_data_for_preemptive_caching)
				break
		except Exception:
			_logger.exception("Error while calling plugin {}, skipping it".format(plugin._identifier),
			                  extra=dict(plugin=plugin._identifier))
	else:
		ui = "_default"
		key = _cache_key("_default", url=url)
		unless = _preemptive_unless(url)
		data = _preemptive_data("_default", path=path, base_url=base_url)

	response = make_response(bytes(base64.b64decode("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")))
	response.headers["Content-Type"] = "image/gif"

	if unless or not preemptiveCache.has_record(data, root=path):
		_logger.info("Preemptive cache not active for path {}, ui {} and data {!r}, signaling as cached".format(path, ui, data))
		return response
	elif util.flask.is_in_cache(key):
		_logger.info("Found path {} in cache (key: {}), signaling as cached".format(path, key))
		return response
	elif util.flask.is_cache_bypassed(key):
		_logger.info("Path {} was bypassed from cache (key: {}), signaling as cached".format(path, key))
		return response
	else:
		_logger.debug("Path {} not yet cached (key: {}), signaling as missing".format(path, key))
		return abort(404)
Exemple #3
0
def _process_templates():
	first_run = settings().getBoolean(["server", "firstRun"])

	##~~ prepare templates

	templates = defaultdict(lambda: dict(order=[], entries=dict()))

	# rules for transforming template configs to template entries
	template_rules = dict(
		navbar=dict(div=lambda x: "navbar_plugin_" + x, template=lambda x: x + "_navbar.jinja2", to_entry=lambda data: data),
		sidebar=dict(div=lambda x: "sidebar_plugin_" + x, template=lambda x: x + "_sidebar.jinja2", to_entry=lambda data: (data["name"], data)),
		tab=dict(div=lambda x: "tab_plugin_" + x, template=lambda x: x + "_tab.jinja2", to_entry=lambda data: (data["name"], data)),
		settings=dict(div=lambda x: "settings_plugin_" + x, template=lambda x: x + "_settings.jinja2", to_entry=lambda data: (data["name"], data)),
		usersettings=dict(div=lambda x: "usersettings_plugin_" + x, template=lambda x: x + "_usersettings.jinja2", to_entry=lambda data: (data["name"], data)),
		wizard=dict(div=lambda x: "wizard_plugin_" + x, template=lambda x: x + "_wizard.jinja2", to_entry=lambda data: (data["name"], data)),
		about=dict(div=lambda x: "about_plugin_" + x, template=lambda x: x + "_about.jinja2", to_entry=lambda data: (data["name"], data)),
		generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data)
	)

	# sorting orders
	template_sorting = dict(
		navbar=dict(add="prepend", key=None),
		sidebar=dict(add="append", key="name"),
		tab=dict(add="append", key="name"),
		settings=dict(add="custom_append", key="name", custom_add_entries=lambda missing: dict(section_plugins=(gettext("Plugins"), None)), custom_add_order=lambda missing: ["section_plugins"] + missing),
		usersettings=dict(add="append", key="name"),
		wizard=dict(add="append", key="name", key_extractor=lambda d, k: "0:{}".format(d[0]) if "mandatory" in d[1] and d[1]["mandatory"] else "1:{}".format(d[0])),
		about=dict(add="append", key="name"),
		generic=dict(add="append", key=None)
	)

	hooks = pluginManager.get_hooks("octoprint.ui.web.templatetypes")
	for name, hook in hooks.items():
		try:
			result = hook(dict(template_sorting), dict(template_rules))
		except:
			_logger.exception("Error while retrieving custom template type definitions from plugin {name}".format(**locals()))
		else:
			if not isinstance(result, list):
				continue

			for entry in result:
				if not isinstance(entry, tuple) or not len(entry) == 3:
					continue

				key, order, rule = entry

				# order defaults
				if "add" not in order:
					order["add"] = "prepend"
				if "key" not in order:
					order["key"] = "name"

				# rule defaults
				if "div" not in rule:
					# default div name: <hook plugin>_<template_key>_plugin_<plugin>
					div = "{name}_{key}_plugin_".format(**locals())
					rule["div"] = lambda x: div + x
				if "template" not in rule:
					# default template name: <plugin>_plugin_<hook plugin>_<template key>.jinja2
					template = "_plugin_{name}_{key}.jinja2".format(**locals())
					rule["template"] = lambda x: x + template
				if "to_entry" not in rule:
					# default to_entry assumes existing "name" property to be used as label for 2-tuple entry data structure (<name>, <properties>)
					rule["to_entry"] = lambda data: (data["name"], data)

				template_rules["plugin_" + name + "_" + key] = rule
				template_sorting["plugin_" + name + "_" + key] = order
	template_types = template_rules.keys()

	# navbar

	templates["navbar"]["entries"] = dict(
		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),
	)

	# sidebar

	templates["sidebar"]["entries"]= dict(
		connection=(gettext("Connection"), dict(template="sidebar/connection.jinja2", _div="connection", icon="signal", styles_wrapper=["display: none"], data_bind="visible: loginState.isUser")),
		state=(gettext("State"), dict(template="sidebar/state.jinja2", _div="state", icon="info-sign")),
		files=(gettext("Files"), dict(template="sidebar/files.jinja2", _div="files", icon="list", classes_content=["overflow_visible"], template_header="sidebar/files_header.jinja2"))
	)

	# tabs

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

	# settings dialog

	templates["settings"]["entries"] = dict(
		section_printer=(gettext("Printer"), None),

		serial=(gettext("Serial Connection"), dict(template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection", custom_bindings=False)),
		printerprofiles=(gettext("Printer Profiles"), dict(template="dialogs/settings/printerprofiles.jinja2", _div="settings_printerProfiles", custom_bindings=False)),
		temperatures=(gettext("Temperatures"), dict(template="dialogs/settings/temperatures.jinja2", _div="settings_temperature", custom_bindings=False)),
		terminalfilters=(gettext("Terminal Filters"), dict(template="dialogs/settings/terminalfilters.jinja2", _div="settings_terminalFilters", custom_bindings=False)),
		gcodescripts=(gettext("GCODE Scripts"), dict(template="dialogs/settings/gcodescripts.jinja2", _div="settings_gcodeScripts", custom_bindings=False)),

		section_features=(gettext("Features"), None),

		features=(gettext("Features"), dict(template="dialogs/settings/features.jinja2", _div="settings_features", custom_bindings=False)),
		webcam=(gettext("Webcam & Timelapse"), dict(template="dialogs/settings/webcam.jinja2", _div="settings_webcam", custom_bindings=False)),
		gcodevisualizer=(gettext("GCODE Visualizer"), dict(template="dialogs/settings/gcodevisualizer.jinja2", _div="settings_gcodegcodevisualizer", custom_bindings=False)),
		api=(gettext("API"), dict(template="dialogs/settings/api.jinja2", _div="settings_api", custom_bindings=False)),

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

	# user settings dialog

	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

	if first_run:
		def custom_insert_order(existing, missing):
			if "firstrunstart" in missing:
				missing.remove("firstrunstart")
			if "firstrunend" in missing:
				missing.remove("firstrunend")

			return ["firstrunstart"] + existing + missing + ["firstrunend"]

		template_sorting["wizard"].update(dict(add="custom_insert", custom_insert_entries=lambda missing: dict(), custom_insert_order=custom_insert_order))
		templates["wizard"]["entries"] = dict(
			firstrunstart=(gettext("Start"), dict(template="dialogs/wizard/firstrun_start.jinja2", _div="wizard_firstrun_start")),
			firstrunend=(gettext("Finish"), dict(template="dialogs/wizard/firstrun_end.jinja2", _div="wizard_firstrun_end")),
		)

	# about dialog

	templates["about"]["entries"] = dict(
		about=("About OctoPrint", dict(template="dialogs/about/about.jinja2", _div="about_about", custom_bindings=False)),
		license=("OctoPrint License", dict(template="dialogs/about/license.jinja2", _div="about_license", custom_bindings=False)),
		thirdparty=("Third Party Licenses", dict(template="dialogs/about/thirdparty.jinja2", _div="about_thirdparty", custom_bindings=False)),
		authors=("Authors", dict(template="dialogs/about/authors.jinja2", _div="about_authors", custom_bindings=False)),
		changelog=("Changelog", dict(template="dialogs/about/changelog.jinja2", _div="about_changelog", custom_bindings=False)),
		supporters=("Supporters", dict(template="dialogs/about/supporters.jinja2", _div="about_sponsors", custom_bindings=False))
	)

	# extract data from template plugins

	template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)

	plugin_vars = dict()
	plugin_names = set()
	seen_wizards = settings().get(["server", "seenWizards"]) if not first_run else dict()
	for implementation in template_plugins:
		name = implementation._identifier
		plugin_names.add(name)
		wizard_required = False
		wizard_ignored = False

		try:
			vars = implementation.get_template_vars()
			configs = implementation.get_template_configs()
			if isinstance(implementation, octoprint.plugin.WizardPlugin):
				wizard_required = implementation.is_wizard_required()
				wizard_ignored = octoprint.plugin.WizardPlugin.is_wizard_ignored(seen_wizards, implementation)
		except:
			_logger.exception("Error while retrieving template data for plugin {}, ignoring it".format(name))
			continue

		if not isinstance(vars, dict):
			vars = dict()
		if not isinstance(configs, (list, tuple)):
			configs = []

		for var_name, var_value in vars.items():
			plugin_vars["plugin_" + name + "_" + var_name] = var_value

		includes = _process_template_configs(name, implementation, configs, template_rules)

		if not wizard_required or wizard_ignored:
			includes["wizard"] = list()

		for t in template_types:
			for include in includes[t]:
				if t == "navbar" or t == "generic":
					data = include
				else:
					data = include[1]

				key = data["_key"]
				if "replaces" in data:
					key = data["replaces"]
				templates[t]["entries"][key] = include

	#~~ order internal templates and plugins

	# make sure that
	# 1) we only have keys in our ordered list that we have entries for and
	# 2) we have all entries located somewhere within the order

	for t in template_types:
		default_order = settings().get(["appearance", "components", "order", t], merged=True, config=dict()) or []
		configured_order = settings().get(["appearance", "components", "order", t], merged=True) or []
		configured_disabled = settings().get(["appearance", "components", "disabled", t]) or []

		# first create the ordered list of all component ids according to the configured order
		templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled]

		# now append the entries from the default order that are not already in there
		templates[t]["order"] += [x for x in default_order if not x in templates[t]["order"] and x in templates[t]["entries"] and not x in configured_disabled]

		all_ordered = set(templates[t]["order"])
		all_disabled = set(configured_disabled)

		# check if anything is missing, if not we are done here
		missing_in_order = set(templates[t]["entries"].keys()).difference(all_ordered).difference(all_disabled)
		if len(missing_in_order) == 0:
			continue

		# works with entries that are dicts and entries that are 2-tuples with the
		# entry data at index 1
		def config_extractor(item, key, default_value=None):
			if isinstance(item, dict) and key in item:
				return item[key] if key in item else default_value
			elif isinstance(item, tuple) and len(item) > 1 and isinstance(item[1], dict) and key in item[1]:
				return item[1][key] if key in item[1] else default_value

			return default_value

		# finally add anything that's not included in our order yet
		if template_sorting[t]["key"] is not None:
			# we'll use our config extractor as default key extractor
			extractor = config_extractor

			# if template type provides custom extractor, make sure its exceptions are handled
			if "key_extractor" in template_sorting[t] and callable(template_sorting[t]["key_extractor"]):
				def create_safe_extractor(extractor):
					def f(x, k):
						try:
							return extractor(x, k)
						except:
							_logger.exception("Error while extracting sorting keys for template {}".format(t))
							return None
					return f
				extractor = create_safe_extractor(template_sorting[t]["key_extractor"])

			sort_key = template_sorting[t]["key"]

			def key_func(x):
				config = templates[t]["entries"][x]
				entry_order = config_extractor(config, "order", default_value=None)
				return entry_order is None, entry_order, extractor(config, sort_key)

			sorted_missing = sorted(missing_in_order, key=key_func)
		else:
			def key_func(x):
				config = templates[t]["entries"][x]
				entry_order = config_extractor(config, "order", default_value=None)
				return entry_order is None, entry_order

			sorted_missing = sorted(missing_in_order, key=key_func)

		if template_sorting[t]["add"] == "prepend":
			templates[t]["order"] = sorted_missing + templates[t]["order"]
		elif template_sorting[t]["add"] == "append":
			templates[t]["order"] += sorted_missing
		elif template_sorting[t]["add"] == "custom_prepend" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
			templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
			templates[t]["order"] = template_sorting[t]["custom_add_order"](sorted_missing) + templates[t]["order"]
		elif template_sorting[t]["add"] == "custom_append" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
			templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
			templates[t]["order"] += template_sorting[t]["custom_add_order"](sorted_missing)
		elif template_sorting[t]["add"] == "custom_insert" and "custom_insert_entries" in template_sorting[t] and "custom_insert_order" in template_sorting[t]:
			templates[t]["entries"].update(template_sorting[t]["custom_insert_entries"](sorted_missing))
			templates[t]["order"] = template_sorting[t]["custom_insert_order"](templates[t]["order"], sorted_missing)

	return templates, plugin_names, plugin_vars
Exemple #4
0
def index():
	global _templates, _plugin_names, _plugin_vars

	preemptive_cache_enabled = settings().getBoolean(["devel", "cache", "preemptive"])

	locale = g.locale.language if g.locale else "en"

	# helper to check if wizards are active
	def wizard_active(templates):
		return templates is not None and bool(templates["wizard"]["order"])

	# we force a refresh if the client forces one or if we have wizards cached
	force_refresh = util.flask.cache_check_headers() or "_refresh" in request.values or wizard_active(_templates.get(locale))

	# if we need to refresh our template cache or it's not yet set, process it
	if force_refresh or _templates.get(locale) is None or _plugin_names is None or _plugin_vars is None:
		_templates[locale], _plugin_names, _plugin_vars = _process_templates()

	now = datetime.datetime.utcnow()

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

		d = _preemptive_data(key, data=data, additional_request_data=additional_request_data)

		def unless():
			return _preemptive_unless(base_url=request.url_root, additional_unless=additional_unless)

		# finally decorate our view
		return util.flask.preemptively_cached(cache=preemptiveCache,
		                                      data=d,
		                                      unless=unless)(view)

	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)

		def check_etag_and_lastmodified():
			files = collect_files()
			lastmodified = compute_lastmodified(files)
			lastmodified_ok = util.flask.check_lastmodified(lastmodified)
			etag_ok = util.flask.check_etag(compute_etag(files=files,
			                                             lastmodified=lastmodified,
			                                             additional=[cache_key()] + additional_etag))
			return lastmodified_ok and etag_ok

		def validate_cache(cached):
			etag_different = compute_etag(additional=[cache_key()] + additional_etag) != cached.get_etag()[0]
			return force_refresh or etag_different

		def collect_files():
			if callable(custom_files):
				try:
					files = custom_files()
					if files:
						return files
				except:
					_logger.exception("Error while trying to retrieve tracked files for plugin {}".format(key))

			templates = _get_all_templates()
			assets = _get_all_assets()
			translations = _get_all_translationfiles(g.locale.language if g.locale else "en",
			                                         "messages")

			files = templates + assets + translations

			if callable(additional_files):
				try:
					af = additional_files()
					if af:
						files += af
				except:
					_logger.exception("Error while trying to retrieve additional tracked files for plugin {}".format(key))

			return sorted(set(files))

		def compute_lastmodified(files=None):
			if callable(custom_lastmodified):
				try:
					lastmodified = custom_lastmodified()
					if lastmodified:
						return lastmodified
				except:
					_logger.exception("Error while trying to retrieve custom LastModified value for plugin {}".format(key))

			if files is None:
				files = collect_files()
			return _compute_date(files)

		def compute_etag(files=None, lastmodified=None, additional=None):
			if callable(custom_etag):
				try:
					etag = custom_etag()
					if etag:
						return etag
				except:
					_logger.exception("Error while trying to retrieve custom ETag value for plugin {}".format(key))

			if files is None:
				files = collect_files()
			if lastmodified is None:
				lastmodified = compute_lastmodified(files)
			if lastmodified and not isinstance(lastmodified, basestring):
				from werkzeug.http import http_date
				lastmodified = http_date(lastmodified)
			if additional is None:
				additional = []

			import hashlib
			hash = hashlib.sha1()
			hash.update(octoprint.__version__)
			hash.update(octoprint.server.UI_API_KEY)
			hash.update(",".join(sorted(files)))
			if lastmodified:
				hash.update(lastmodified)
			for add in additional:
				hash.update(str(add))
			return hash.hexdigest()

		decorated_view = view
		decorated_view = util.flask.lastmodified(lambda _: compute_lastmodified())(decorated_view)
		decorated_view = util.flask.etagged(lambda _: compute_etag(additional=[cache_key()] + additional_etag))(decorated_view)
		decorated_view = util.flask.cached(timeout=-1,
		                                   refreshif=validate_cache,
		                                   key=cache_key,
		                                   unless_response=lambda response: util.flask.cache_check_response_headers(response) or util.flask.cache_check_status_code(response, _valid_status_for_cache))(decorated_view)
		decorated_view = util.flask.conditional(check_etag_and_lastmodified, NOT_MODIFIED)(decorated_view)
		return decorated_view

	def plugin_view(p):
		cached = get_cached_view(p._identifier,
		                         p.on_ui_render,
		                         additional_key_data=p.get_ui_additional_key_data_for_cache,
		                         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,
		                         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,
			                                    cached,
			                                    p.get_ui_data_for_preemptive_caching,
			                                    p.get_ui_additional_request_data_for_preemptive_caching,
			                                    p.get_ui_preemptive_caching_additional_unless)
		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():
		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"]),
			enableAccessControl=enable_accesscontrol,
			accessControlActive=accesscontrol_active,
			enableSdSupport=settings().get(["feature", "sdSupport"]),
			gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
			gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
			wizard=wizard,
			now=now,
		))

		# no plugin took an interest, we'll use the default UI
		def make_default_ui():
			r = make_response(render_template("index.jinja2", **render_kwargs))
			if wizard:
				# if we have active wizard dialogs, set non caching headers
				r = util.flask.add_non_caching_response_headers(r)
			return r

		cached = get_cached_view("_default",
		                         make_default_ui,
		                         additional_etag=default_additional_etag)
		preemptively_cached = get_preemptively_cached_view("_default",
		                                                   cached,
		                                                   dict(),
		                                                   dict())
		return preemptively_cached()

	response = None

	forced_view = request.headers.get("X-Force-View", None)

	if forced_view:
		# we have view forced by the preemptive cache
		_logger.debug("Forcing rendering of view {}".format(forced_view))
		if forced_view != "_default":
			plugin = pluginManager.get_plugin_info(forced_view, require_enabled=True)
			if plugin is not None and isinstance(plugin.implementation, octoprint.plugin.UiPlugin):
				response = plugin_view(plugin.implementation)
		else:
			response = default_view()

	else:
		# select view from plugins and fall back on default view if no plugin will handle it
		ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render")
		for plugin in ui_plugins:
			if plugin.will_handle_ui(request):
				# plugin claims responsibility, let it render the UI
				response = plugin_view(plugin)
				if response is not None:
					break
				else:
					_logger.warn("UiPlugin {} returned an empty response".format(plugin._identifier))
		else:
			response = default_view()

	if response is None:
		return abort(404)
	return response
Exemple #5
0
def index():

    #~~ a bunch of settings

    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 and len(settings().get(["system", "actions"])) > 0
    enable_accesscontrol = userManager is not None
    preferred_stylesheet = settings().get(["devel", "stylesheet"])
    locales = dict((l.language,
                    dict(language=l.language,
                         display=l.display_name,
                         english=l.english_name)) for l in LOCALES)

    ##~~ prepare templates

    templates = defaultdict(lambda: dict(order=[], entries=dict()))

    # rules for transforming template configs to template entries
    template_rules = dict(
        navbar=dict(div=lambda x: "navbar_plugin_" + x,
                    template=lambda x: x + "_navbar.jinja2",
                    to_entry=lambda data: data),
        sidebar=dict(div=lambda x: "sidebar_plugin_" + x,
                     template=lambda x: x + "_sidebar.jinja2",
                     to_entry=lambda data: (data["name"], data)),
        tab=dict(div=lambda x: "tab_plugin_" + x,
                 template=lambda x: x + "_tab.jinja2",
                 to_entry=lambda data: (data["name"], data)),
        settings=dict(div=lambda x: "settings_plugin_" + x,
                      template=lambda x: x + "_settings.jinja2",
                      to_entry=lambda data: (data["name"], data)),
        usersettings=dict(div=lambda x: "usersettings_plugin_" + x,
                          template=lambda x: x + "_usersettings.jinja2",
                          to_entry=lambda data: (data["name"], data)),
        generic=dict(template=lambda x: x + ".jinja2",
                     to_entry=lambda data: data))

    # sorting orders
    template_sorting = dict(
        navbar=dict(add="prepend", key=None),
        sidebar=dict(add="append", key="name"),
        tab=dict(add="append", key="name"),
        settings=dict(
            add="custom_append",
            key="name",
            custom_add_entries=lambda missing: dict(section_plugins=(gettext(
                "Plugins"), None)),
            custom_add_order=lambda missing: ["section_plugins"] + missing),
        usersettings=dict(add="append", key="name"),
        generic=dict(add="append", key=None))

    hooks = pluginManager.get_hooks("octoprint.ui.web.templatetypes")
    for name, hook in hooks.items():
        try:
            result = hook(dict(template_sorting), dict(template_rules))
        except:
            _logger.exception(
                "Error while retrieving custom template type definitions from plugin {name}"
                .format(**locals()))
        else:
            if not isinstance(result, list):
                continue

            for entry in result:
                if not isinstance(entry, tuple) or not len(entry) == 3:
                    continue

                key, order, rule = entry

                # order defaults
                if "add" not in order:
                    order["add"] = "prepend"
                if "key" not in order:
                    order["key"] = "name"

                # rule defaults
                if "div" not in rule:
                    # default div name: <hook plugin>_<template_key>_plugin_<plugin>
                    div = "{name}_{key}_plugin_".format(**locals())
                    rule["div"] = lambda x: div + x
                if "template" not in rule:
                    # default template name: <plugin>_plugin_<hook plugin>_<template key>.jinja2
                    template = "_plugin_{name}_{key}.jinja2".format(**locals())
                    rule["template"] = lambda x: x + template
                if "to_entry" not in rule:
                    # default to_entry assumes existing "name" property to be used as label for 2-tuple entry data structure (<name>, <properties>)
                    rule["to_entry"] = lambda data: (data["name"], data)

                template_rules["plugin_" + name + "_" + key] = rule
                template_sorting["plugin_" + name + "_" + key] = order
    template_types = template_rules.keys()

    # navbar

    templates["navbar"]["entries"] = dict(
        settings=dict(template="navbar/settings.jinja2",
                      _div="navbar_settings",
                      styles=["display: none"],
                      data_bind="visible: loginState.isAdmin"))
    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

    templates["sidebar"]["entries"] = dict(
        connection=(gettext("Connection"),
                    dict(template="sidebar/connection.jinja2",
                         _div="connection",
                         icon="signal",
                         styles_wrapper=["display: none"],
                         data_bind="visible: loginState.isAdmin")),
        state=(gettext("State"),
               dict(template="sidebar/state.jinja2",
                    _div="state",
                    icon="info-sign")),
        files=(gettext("Files"),
               dict(template="sidebar/files.jinja2",
                    _div="files",
                    icon="list",
                    classes_content=["overflow_visible"],
                    template_header="sidebar/files_header.jinja2")))

    # tabs

    templates["tab"]["entries"] = dict(
        temperature=(gettext("Temperature"),
                     dict(template="tabs/temperature.jinja2", _div="temp")),
        control=(gettext("Control"),
                 dict(template="tabs/control.jinja2", _div="control")),
        terminal=(gettext("Terminal"),
                  dict(template="tabs/terminal.jinja2", _div="term")),
    )
    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

    templates["settings"]["entries"] = dict(
        section_printer=(gettext("Printer"), None),
        serial=(gettext("Serial Connection"),
                dict(template="dialogs/settings/serialconnection.jinja2",
                     _div="settings_serialConnection",
                     custom_bindings=False)),
        printerprofiles=(
            gettext("Printer Profiles"),
            dict(template="dialogs/settings/printerprofiles.jinja2",
                 _div="settings_printerProfiles",
                 custom_bindings=False)),
        temperatures=(gettext("Temperatures"),
                      dict(template="dialogs/settings/temperatures.jinja2",
                           _div="settings_temperature",
                           custom_bindings=False)),
        terminalfilters=(
            gettext("Terminal Filters"),
            dict(template="dialogs/settings/terminalfilters.jinja2",
                 _div="settings_terminalFilters",
                 custom_bindings=False)),
        gcodescripts=(gettext("GCODE Scripts"),
                      dict(template="dialogs/settings/gcodescripts.jinja2",
                           _div="settings_gcodeScripts",
                           custom_bindings=False)),
        section_features=(gettext("Features"), None),
        features=(gettext("Features"),
                  dict(template="dialogs/settings/features.jinja2",
                       _div="settings_features",
                       custom_bindings=False)),
        webcam=(gettext("Webcam"),
                dict(template="dialogs/settings/webcam.jinja2",
                     _div="settings_webcam",
                     custom_bindings=False)),
        api=(gettext("API"),
             dict(template="dialogs/settings/api.jinja2",
                  _div="settings_api",
                  custom_bindings=False)),
        section_octoprint=(gettext("OctoPrint"), None),
        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)),
        )

    # extract data from template plugins

    template_plugins = pluginManager.get_implementations(
        octoprint.plugin.TemplatePlugin)

    plugin_vars = dict()
    plugin_names = set()
    for implementation in template_plugins:
        name = implementation._identifier
        plugin_names.add(name)

        try:
            vars = implementation.get_template_vars()
            configs = implementation.get_template_configs()
        except:
            _logger.exception(
                "Error while retrieving template data for plugin {}, ignoring it"
                .format(name))
            continue

        if not isinstance(vars, dict):
            vars = dict()
        if not isinstance(configs, (list, tuple)):
            configs = []

        for var_name, var_value in vars.items():
            plugin_vars["plugin_" + name + "_" + var_name] = var_value

        includes = _process_template_configs(name, implementation, configs,
                                             template_rules)

        for t in template_types:
            for include in includes[t]:
                if t == "navbar" or t == "generic":
                    data = include
                else:
                    data = include[1]

                key = data["_key"]
                if "replaces" in data:
                    key = data["replaces"]
                templates[t]["entries"][key] = include

    #~~ order internal templates and plugins

    # make sure that
    # 1) we only have keys in our ordered list that we have entries for and
    # 2) we have all entries located somewhere within the order

    for t in template_types:
        default_order = settings().get(
            ["appearance", "components", "order", t],
            merged=True,
            config=dict()) or []
        configured_order = settings().get(
            ["appearance", "components", "order", t], merged=True) or []
        configured_disabled = settings().get(
            ["appearance", "components", "disabled", t]) or []

        # first create the ordered list of all component ids according to the configured order
        templates[t]["order"] = [
            x for x in configured_order
            if x in templates[t]["entries"] and not x in configured_disabled
        ]

        # now append the entries from the default order that are not already in there
        templates[t]["order"] += [
            x for x in default_order if not x in templates[t]["order"]
            and x in templates[t]["entries"] and not x in configured_disabled
        ]

        all_ordered = set(templates[t]["order"])
        all_disabled = set(configured_disabled)

        # check if anything is missing, if not we are done here
        missing_in_order = set(templates[t]["entries"].keys()).difference(
            all_ordered).difference(all_disabled)
        if len(missing_in_order) == 0:
            continue

        # finally add anything that's not included in our order yet
        sorted_missing = list(missing_in_order)
        if template_sorting[t]["key"] is not None:
            # default extractor: works with entries that are dicts and entries that are 2-tuples with the
            # entry data at index 1
            def extractor(item, key):
                if isinstance(item, dict) and key in item:
                    return item[key]
                elif isinstance(item, tuple) and len(item) > 1 and isinstance(
                        item[1], dict) and key in item[1]:
                    return item[1][key]

                return None

            # if template type provides custom extractor, make sure its exceptions are handled
            if "key_extractor" in template_sorting[t] and callable(
                    template_sorting[t]["key_extractor"]):

                def create_safe_extractor(extractor):
                    def f(x, k):
                        try:
                            return extractor(x, k)
                        except:
                            _logger.exception(
                                "Error while extracting sorting keys for template {}"
                                .format(t))
                            return None

                    return f

                extractor = create_safe_extractor(
                    template_sorting[t]["key_extractor"])

            sort_key = template_sorting[t]["key"]
            sorted_missing = sorted(
                missing_in_order,
                key=lambda x: extractor(templates[t]["entries"][x], sort_key))

        if template_sorting[t]["add"] == "prepend":
            templates[t]["order"] = sorted_missing + templates[t]["order"]
        elif template_sorting[t]["add"] == "append":
            templates[t]["order"] += sorted_missing
        elif template_sorting[t][
                "add"] == "custom_prepend" and "custom_add_entries" in template_sorting[
                    t] and "custom_add_order" in template_sorting[t]:
            templates[t]["entries"].update(
                template_sorting[t]["custom_add_entries"](sorted_missing))
            templates[t]["order"] = template_sorting[t]["custom_add_order"](
                sorted_missing) + templates[t]["order"]
        elif template_sorting[t][
                "add"] == "custom_append" and "custom_add_entries" in template_sorting[
                    t] and "custom_add_order" in template_sorting[t]:
            templates[t]["entries"].update(
                template_sorting[t]["custom_add_entries"](sorted_missing))
            templates[t]["order"] += template_sorting[t]["custom_add_order"](
                sorted_missing)

    #~~ prepare full set of template vars for rendering

    first_run = settings().getBoolean([
        "server", "firstRun"
    ]) and (userManager is None or not userManager.hasBeenCustomized())
    render_kwargs = dict(
        webcamStream=settings().get(["webcam", "stream"]),
        enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
        enableAccessControl=userManager is not None,
        enableSdSupport=settings().get(["feature", "sdSupport"]),
        firstRun=first_run,
        debug=debug,
        version=VERSION,
        display_version=DISPLAY_VERSION,
        branch=BRANCH,
        gcodeMobileThreshold=settings().get(
            ["gcodeViewer", "mobileSizeThreshold"]),
        gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
        uiApiKey=UI_API_KEY,
        templates=templates,
        pluginNames=plugin_names,
        locales=locales)
    render_kwargs.update(plugin_vars)

    #~~ render!

    import datetime

    response = make_response(render_template("index.jinja2", **render_kwargs))
    response.headers["Last-Modified"] = datetime.datetime.now()

    if first_run:
        response = util.flask.add_non_caching_response_headers(response)

    return response
Exemple #6
0
def _process_templates():
	first_run = settings().getBoolean(["server", "firstRun"])

	##~~ prepare templates

	templates = defaultdict(lambda: dict(order=[], entries=dict()))

	# rules for transforming template configs to template entries
	template_rules = dict(
		navbar=dict(div=lambda x: "navbar_plugin_" + x, template=lambda x: x + "_navbar.jinja2", to_entry=lambda data: data),
		sidebar=dict(div=lambda x: "sidebar_plugin_" + x, template=lambda x: x + "_sidebar.jinja2", to_entry=lambda data: (data["name"], data)),
		tab=dict(div=lambda x: "tab_plugin_" + x, template=lambda x: x + "_tab.jinja2", to_entry=lambda data: (data["name"], data)),
		settings=dict(div=lambda x: "settings_plugin_" + x, template=lambda x: x + "_settings.jinja2", to_entry=lambda data: (data["name"], data)),
		usersettings=dict(div=lambda x: "usersettings_plugin_" + x, template=lambda x: x + "_usersettings.jinja2", to_entry=lambda data: (data["name"], data)),
		wizard=dict(div=lambda x: "wizard_plugin_" + x, template=lambda x: x + "_wizard.jinja2", to_entry=lambda data: (data["name"], data)),
		about=dict(div=lambda x: "about_plugin_" + x, template=lambda x: x + "_about.jinja2", to_entry=lambda data: (data["name"], data)),
		generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data)
	)

	# sorting orders
	def wizard_key_extractor(d, k):
		if d[1].get("_key", None) == "plugin_corewizard_acl":
			# Ultra special case - we MUST always have the ACL wizard first since otherwise any steps that follow and
			# that require to access APIs to function will run into errors since those APIs won't work before ACL
			# has been configured. See also #2140
			return u"0:{}".format(to_unicode(d[0]))
		elif d[1].get("mandatory", False):
			# Other mandatory steps come before the optional ones
			return u"1:{}".format(to_unicode(d[0]))
		else:
			# Finally everything else
			return u"2:{}".format(to_unicode(d[0]))

	template_sorting = dict(
		navbar=dict(add="prepend", key=None),
		sidebar=dict(add="append", key="name"),
		tab=dict(add="append", key="name"),
		settings=dict(add="custom_append", key="name", custom_add_entries=lambda missing: dict(section_plugins=(gettext("Plugins"), None)), custom_add_order=lambda missing: ["section_plugins"] + missing),
		usersettings=dict(add="append", key="name"),
		wizard=dict(add="append", key="name", key_extractor=wizard_key_extractor),
		about=dict(add="append", key="name"),
		generic=dict(add="append", key=None)
	)

	hooks = pluginManager.get_hooks("octoprint.ui.web.templatetypes")
	for name, hook in hooks.items():
		try:
			result = hook(dict(template_sorting), dict(template_rules))
		except:
			_logger.exception("Error while retrieving custom template type definitions from plugin {name}".format(**locals()))
		else:
			if not isinstance(result, list):
				continue

			for entry in result:
				if not isinstance(entry, tuple) or not len(entry) == 3:
					continue

				key, order, rule = entry

				# order defaults
				if "add" not in order:
					order["add"] = "prepend"
				if "key" not in order:
					order["key"] = "name"

				# rule defaults
				if "div" not in rule:
					# default div name: <hook plugin>_<template_key>_plugin_<plugin>
					div = "{name}_{key}_plugin_".format(**locals())
					rule["div"] = lambda x: div + x
				if "template" not in rule:
					# default template name: <plugin>_plugin_<hook plugin>_<template key>.jinja2
					template = "_plugin_{name}_{key}.jinja2".format(**locals())
					rule["template"] = lambda x: x + template
				if "to_entry" not in rule:
					# default to_entry assumes existing "name" property to be used as label for 2-tuple entry data structure (<name>, <properties>)
					rule["to_entry"] = lambda data: (data["name"], data)

				template_rules["plugin_" + name + "_" + key] = rule
				template_sorting["plugin_" + name + "_" + key] = order
	template_types = template_rules.keys()

	# navbar

	templates["navbar"]["entries"] = dict(
		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),
	)

	# sidebar

	templates["sidebar"]["entries"]= dict(
		connection=(gettext("Connection"), dict(template="sidebar/connection.jinja2", _div="connection", icon="signal", styles_wrapper=["display: none"], data_bind="visible: loginState.isUser", template_header="sidebar/connection_header.jinja2")),
		state=(gettext("State"), dict(template="sidebar/state.jinja2", _div="state", icon="info-circle")),
		files=(gettext("Files"), dict(template="sidebar/files.jinja2", _div="files", icon="list", classes_content=["overflow_visible"], template_header="sidebar/files_header.jinja2"))
	)

	# tabs

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

	# settings dialog

	templates["settings"]["entries"] = dict(
		section_printer=(gettext("Printer"), None),

		serial=(gettext("Serial Connection"), dict(template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection", custom_bindings=False)),
		printerprofiles=(gettext("Printer Profiles"), dict(template="dialogs/settings/printerprofiles.jinja2", _div="settings_printerProfiles", custom_bindings=False)),
		temperatures=(gettext("Temperatures"), dict(template="dialogs/settings/temperatures.jinja2", _div="settings_temperature", custom_bindings=False)),
		terminalfilters=(gettext("Terminal Filters"), dict(template="dialogs/settings/terminalfilters.jinja2", _div="settings_terminalFilters", custom_bindings=False)),
		gcodescripts=(gettext("GCODE Scripts"), dict(template="dialogs/settings/gcodescripts.jinja2", _div="settings_gcodeScripts", custom_bindings=False)),

		section_features=(gettext("Features"), None),

		features=(gettext("Features"), dict(template="dialogs/settings/features.jinja2", _div="settings_features", custom_bindings=False)),
		webcam=(gettext("Webcam & Timelapse"), dict(template="dialogs/settings/webcam.jinja2", _div="settings_webcam", custom_bindings=False)),
		gcodevisualizer=(gettext("GCODE Visualizer"), dict(template="dialogs/settings/gcodevisualizer.jinja2", _div="settings_gcodegcodevisualizer", custom_bindings=False)),
		api=(gettext("API"), dict(template="dialogs/settings/api.jinja2", _div="settings_api", custom_bindings=False)),

		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)),
		server=(gettext("Server"), dict(template="dialogs/settings/server.jinja2", _div="settings_server", custom_bindings=False)),
	)

	# user settings dialog

	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

	if first_run:
		def custom_insert_order(existing, missing):
			if "firstrunstart" in missing:
				missing.remove("firstrunstart")
			if "firstrunend" in missing:
				missing.remove("firstrunend")

			return ["firstrunstart"] + existing + missing + ["firstrunend"]

		template_sorting["wizard"].update(dict(add="custom_insert", custom_insert_entries=lambda missing: dict(), custom_insert_order=custom_insert_order))
		templates["wizard"]["entries"] = dict(
			firstrunstart=(gettext("Start"), dict(template="dialogs/wizard/firstrun_start.jinja2", _div="wizard_firstrun_start")),
			firstrunend=(gettext("Finish"), dict(template="dialogs/wizard/firstrun_end.jinja2", _div="wizard_firstrun_end")),
		)

	# about dialog

	templates["about"]["entries"] = dict(
		about=("About OctoPrint", dict(template="dialogs/about/about.jinja2", _div="about_about", custom_bindings=False)),
		license=("OctoPrint License", dict(template="dialogs/about/license.jinja2", _div="about_license", custom_bindings=False)),
		thirdparty=("Third Party Licenses", dict(template="dialogs/about/thirdparty.jinja2", _div="about_thirdparty", custom_bindings=False)),
		authors=("Authors", dict(template="dialogs/about/authors.jinja2", _div="about_authors", custom_bindings=False)),
		changelog=("Changelog", dict(template="dialogs/about/changelog.jinja2", _div="about_changelog", custom_bindings=False)),
		supporters=("Supporters", dict(template="dialogs/about/supporters.jinja2", _div="about_sponsors", custom_bindings=False))
	)

	# extract data from template plugins

	template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)

	plugin_vars = dict()
	plugin_names = set()
	seen_wizards = settings().get(["server", "seenWizards"]) if not first_run else dict()
	for implementation in template_plugins:
		name = implementation._identifier
		plugin_names.add(name)
		wizard_required = False
		wizard_ignored = False

		try:
			vars = implementation.get_template_vars()
			configs = implementation.get_template_configs()
			if isinstance(implementation, octoprint.plugin.WizardPlugin):
				wizard_required = implementation.is_wizard_required()
				wizard_ignored = octoprint.plugin.WizardPlugin.is_wizard_ignored(seen_wizards, implementation)
		except:
			_logger.exception("Error while retrieving template data for plugin {}, ignoring it".format(name))
			continue

		if not isinstance(vars, dict):
			vars = dict()
		if not isinstance(configs, (list, tuple)):
			configs = []

		for var_name, var_value in vars.items():
			plugin_vars["plugin_" + name + "_" + var_name] = var_value

		includes = _process_template_configs(name, implementation, configs, template_rules)

		if not wizard_required or wizard_ignored:
			includes["wizard"] = list()

		for t in template_types:
			for include in includes[t]:
				if t == "navbar" or t == "generic":
					data = include
				else:
					data = include[1]

				key = data["_key"]
				if "replaces" in data:
					key = data["replaces"]
				templates[t]["entries"][key] = include

	#~~ order internal templates and plugins

	# make sure that
	# 1) we only have keys in our ordered list that we have entries for and
	# 2) we have all entries located somewhere within the order

	for t in template_types:
		default_order = settings().get(["appearance", "components", "order", t], merged=True, config=dict()) or []
		configured_order = settings().get(["appearance", "components", "order", t], merged=True) or []
		configured_disabled = settings().get(["appearance", "components", "disabled", t]) or []

		# first create the ordered list of all component ids according to the configured order
		templates[t]["order"] = [x for x in configured_order if x in templates[t]["entries"] and not x in configured_disabled]

		# now append the entries from the default order that are not already in there
		templates[t]["order"] += [x for x in default_order if not x in templates[t]["order"] and x in templates[t]["entries"] and not x in configured_disabled]

		all_ordered = set(templates[t]["order"])
		all_disabled = set(configured_disabled)

		# check if anything is missing, if not we are done here
		missing_in_order = set(templates[t]["entries"].keys()).difference(all_ordered).difference(all_disabled)
		if len(missing_in_order) == 0:
			continue

		# works with entries that are dicts and entries that are 2-tuples with the
		# entry data at index 1
		def config_extractor(item, key, default_value=None):
			if isinstance(item, dict) and key in item:
				return item[key] if key in item else default_value
			elif isinstance(item, tuple) and len(item) > 1 and isinstance(item[1], dict) and key in item[1]:
				return item[1][key] if key in item[1] else default_value

			return default_value

		# finally add anything that's not included in our order yet
		if template_sorting[t]["key"] is not None:
			# we'll use our config extractor as default key extractor
			extractor = config_extractor

			# if template type provides custom extractor, make sure its exceptions are handled
			if "key_extractor" in template_sorting[t] and callable(template_sorting[t]["key_extractor"]):
				def create_safe_extractor(extractor):
					def f(x, k):
						try:
							return extractor(x, k)
						except:
							_logger.exception("Error while extracting sorting keys for template {}".format(t))
							return None
					return f
				extractor = create_safe_extractor(template_sorting[t]["key_extractor"])

			sort_key = template_sorting[t]["key"]

			def key_func(x):
				config = templates[t]["entries"][x]
				entry_order = config_extractor(config, "order", default_value=None)
				return entry_order is None, entry_order, extractor(config, sort_key)

			sorted_missing = sorted(missing_in_order, key=key_func)
		else:
			def key_func(x):
				config = templates[t]["entries"][x]
				entry_order = config_extractor(config, "order", default_value=None)
				return entry_order is None, entry_order

			sorted_missing = sorted(missing_in_order, key=key_func)

		if template_sorting[t]["add"] == "prepend":
			templates[t]["order"] = sorted_missing + templates[t]["order"]
		elif template_sorting[t]["add"] == "append":
			templates[t]["order"] += sorted_missing
		elif template_sorting[t]["add"] == "custom_prepend" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
			templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
			templates[t]["order"] = template_sorting[t]["custom_add_order"](sorted_missing) + templates[t]["order"]
		elif template_sorting[t]["add"] == "custom_append" and "custom_add_entries" in template_sorting[t] and "custom_add_order" in template_sorting[t]:
			templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
			templates[t]["order"] += template_sorting[t]["custom_add_order"](sorted_missing)
		elif template_sorting[t]["add"] == "custom_insert" and "custom_insert_entries" in template_sorting[t] and "custom_insert_order" in template_sorting[t]:
			templates[t]["entries"].update(template_sorting[t]["custom_insert_entries"](sorted_missing))
			templates[t]["order"] = template_sorting[t]["custom_insert_order"](templates[t]["order"], sorted_missing)

	return templates, plugin_names, plugin_vars
Exemple #7
0
def index():
	global _templates, _plugin_names, _plugin_vars

	preemptive_cache_enabled = settings().getBoolean(["devel", "cache", "preemptive"])

	locale = g.locale.language if g.locale else "en"

	# helper to check if wizards are active
	def wizard_active(templates):
		return templates is not None and bool(templates["wizard"]["order"])

	# we force a refresh if the client forces one or if we have wizards cached
	force_refresh = util.flask.cache_check_headers() or "_refresh" in request.values or wizard_active(_templates.get(locale))

	# if we need to refresh our template cache or it's not yet set, process it
	if force_refresh or _templates.get(locale) is None or _plugin_names is None or _plugin_vars is None:
		_templates[locale], _plugin_names, _plugin_vars = _process_templates()

	now = datetime.datetime.utcnow()

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

		d = _preemptive_data(key, data=data, additional_request_data=additional_request_data)

		def unless():
			return _preemptive_unless(base_url=request.url_root, additional_unless=additional_unless)

		# finally decorate our view
		return util.flask.preemptively_cached(cache=preemptiveCache,
		                                      data=d,
		                                      unless=unless)(view)

	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)

		def check_etag_and_lastmodified():
			files = collect_files()
			lastmodified = compute_lastmodified(files)
			lastmodified_ok = util.flask.check_lastmodified(lastmodified)
			etag_ok = util.flask.check_etag(compute_etag(files=files,
			                                             lastmodified=lastmodified,
			                                             additional=[cache_key()] + additional_etag))
			return lastmodified_ok and etag_ok

		def validate_cache(cached):
			etag_different = compute_etag(additional=[cache_key()] + additional_etag) != cached.get_etag()[0]
			return force_refresh or etag_different

		def collect_files():
			if callable(custom_files):
				try:
					files = custom_files()
					if files:
						return files
				except:
					_logger.exception("Error while trying to retrieve tracked files for plugin {}".format(key))

			templates = _get_all_templates()
			assets = _get_all_assets()
			translations = _get_all_translationfiles(g.locale.language if g.locale else "en",
			                                         "messages")

			files = templates + assets + translations

			if callable(additional_files):
				try:
					af = additional_files()
					if af:
						files += af
				except:
					_logger.exception("Error while trying to retrieve additional tracked files for plugin {}".format(key))

			return sorted(set(files))

		def compute_lastmodified(files=None):
			if callable(custom_lastmodified):
				try:
					lastmodified = custom_lastmodified()
					if lastmodified:
						return lastmodified
				except:
					_logger.exception("Error while trying to retrieve custom LastModified value for plugin {}".format(key))

			if files is None:
				files = collect_files()
			return _compute_date(files)

		def compute_etag(files=None, lastmodified=None, additional=None):
			if callable(custom_etag):
				try:
					etag = custom_etag()
					if etag:
						return etag
				except:
					_logger.exception("Error while trying to retrieve custom ETag value for plugin {}".format(key))

			if files is None:
				files = collect_files()
			if lastmodified is None:
				lastmodified = compute_lastmodified(files)
			if lastmodified and not isinstance(lastmodified, basestring):
				from werkzeug.http import http_date
				lastmodified = http_date(lastmodified)
			if additional is None:
				additional = []

			import hashlib
			hash = hashlib.sha1()
			hash.update(octoprint.__version__)
			hash.update(octoprint.server.UI_API_KEY)
			hash.update(",".join(sorted(files)))
			if lastmodified:
				hash.update(lastmodified)
			for add in additional:
				hash.update(str(add))
			return hash.hexdigest()

		decorated_view = view
		decorated_view = util.flask.lastmodified(lambda _: compute_lastmodified())(decorated_view)
		decorated_view = util.flask.etagged(lambda _: compute_etag(additional=[cache_key()] + additional_etag))(decorated_view)
		decorated_view = util.flask.cached(timeout=-1,
		                                   refreshif=validate_cache,
		                                   key=cache_key,
		                                   unless_response=lambda response: util.flask.cache_check_response_headers(response) or util.flask.cache_check_status_code(response, _valid_status_for_cache))(decorated_view)
		decorated_view = util.flask.conditional(check_etag_and_lastmodified, NOT_MODIFIED)(decorated_view)
		return decorated_view

	def plugin_view(p):
		cached = get_cached_view(p._identifier,
		                         p.on_ui_render,
		                         additional_key_data=p.get_ui_additional_key_data_for_cache,
		                         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,
		                         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,
			                                    cached,
			                                    p.get_ui_data_for_preemptive_caching,
			                                    p.get_ui_additional_request_data_for_preemptive_caching,
			                                    p.get_ui_preemptive_caching_additional_unless)
		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():
		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"]),
			enableAccessControl=enable_accesscontrol,
			accessControlActive=accesscontrol_active,
			enableSdSupport=settings().get(["feature", "sdSupport"]),
			gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
			gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
			wizard=wizard,
			now=now,
		))

		# no plugin took an interest, we'll use the default UI
		def make_default_ui():
			r = make_response(render_template("index.jinja2", **render_kwargs))
			if wizard:
				# if we have active wizard dialogs, set non caching headers
				r = util.flask.add_non_caching_response_headers(r)
			return r

		cached = get_cached_view("_default",
		                         make_default_ui,
		                         additional_etag=default_additional_etag)
		preemptively_cached = get_preemptively_cached_view("_default",
		                                                   cached,
		                                                   dict(),
		                                                   dict())
		return preemptively_cached()

	response = None

	forced_view = request.headers.get("X-Force-View", None)

	if forced_view:
		# we have view forced by the preemptive cache
		_logger.debug("Forcing rendering of view {}".format(forced_view))
		if forced_view != "_default":
			plugin = pluginManager.get_plugin_info(forced_view, require_enabled=True)
			if plugin is not None and isinstance(plugin.implementation, octoprint.plugin.UiPlugin):
				response = plugin_view(plugin.implementation)
		else:
			response = default_view()

	else:
		# select view from plugins and fall back on default view if no plugin will handle it
		ui_plugins = pluginManager.get_implementations(octoprint.plugin.UiPlugin, sorting_context="UiPlugin.on_ui_render")
		for plugin in ui_plugins:
			if plugin.will_handle_ui(request):
				# plugin claims responsibility, let it render the UI
				response = plugin_view(plugin)
				if response is not None:
					break
				else:
					_logger.warn("UiPlugin {} returned an empty response".format(plugin._identifier))
		else:
			response = default_view()

	if response is None:
		return abort(404)
	return response
Exemple #8
0
def index():
    # ~~ a bunch of settings

    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 and len(settings().get(["system", "actions"])) > 0
    enable_accesscontrol = userManager is not None
    preferred_stylesheet = settings().get(["devel", "stylesheet"])
    locales = dict(
        (l.language, dict(language=l.language, display=l.display_name, english=l.english_name)) for l in LOCALES)

    ##~~ prepare templates

    templates = defaultdict(lambda: dict(order=[], entries=dict()))

    # rules for transforming template configs to template entries
    template_rules = dict(
        navbar=dict(div=lambda x: "navbar_plugin_" + x, template=lambda x: x + "_navbar.jinja2",
                    to_entry=lambda data: data),
        sidebar=dict(div=lambda x: "sidebar_plugin_" + x, template=lambda x: x + "_sidebar.jinja2",
                     to_entry=lambda data: (data["name"], data)),
        tab=dict(div=lambda x: "tab_plugin_" + x, template=lambda x: x + "_tab.jinja2",
                 to_entry=lambda data: (data["name"], data)),
        settings=dict(div=lambda x: "settings_plugin_" + x, template=lambda x: x + "_settings.jinja2",
                      to_entry=lambda data: (data["name"], data)),
        usersettings=dict(div=lambda x: "usersettings_plugin_" + x, template=lambda x: x + "_usersettings.jinja2",
                          to_entry=lambda data: (data["name"], data)),
        generic=dict(template=lambda x: x + ".jinja2", to_entry=lambda data: data)
    )

    # sorting orders
    template_sorting = dict(
        navbar=dict(add="prepend", key=None),
        sidebar=dict(add="append", key="name"),
        tab=dict(add="append", key="name"),
        settings=dict(add="custom_append", key="name",
                      custom_add_entries=lambda missing: dict(section_plugins=(gettext("Plugins"), None)),
                      custom_add_order=lambda missing: ["section_plugins"] + missing),
        usersettings=dict(add="append", key="name"),
        generic=dict(add="append", key=None)
    )

    hooks = pluginManager.get_hooks("octoprint.ui.web.templatetypes")
    for name, hook in hooks.items():
        try:
            result = hook(dict(template_sorting), dict(template_rules))
        except:
            _logger.exception(
                "Error while retrieving custom template type definitions from plugin {name}".format(**locals()))
        else:
            if not isinstance(result, list):
                continue

            for entry in result:
                if not isinstance(entry, tuple) or not len(entry) == 3:
                    continue

                key, order, rule = entry

                # order defaults
                if "add" not in order:
                    order["add"] = "prepend"
                if "key" not in order:
                    order["key"] = "name"

                # rule defaults
                if "div" not in rule:
                    # default div name: <hook plugin>_<template_key>_plugin_<plugin>
                    div = "{name}_{key}_plugin_".format(**locals())
                    rule["div"] = lambda x: div + x
                if "template" not in rule:
                    # default template name: <plugin>_plugin_<hook plugin>_<template key>.jinja2
                    template = "_plugin_{name}_{key}.jinja2".format(**locals())
                    rule["template"] = lambda x: x + template
                if "to_entry" not in rule:
                    # default to_entry assumes existing "name" property to be used as label for 2-tuple entry data structure (<name>, <properties>)
                    rule["to_entry"] = lambda data: (data["name"], data)

                template_rules["plugin_" + name + "_" + key] = rule
                template_sorting["plugin_" + name + "_" + key] = order
    template_types = template_rules.keys()

    # navbar

    templates["navbar"]["entries"] = dict(
        settings=dict(template="navbar/settings.jinja2", _div="navbar_settings", styles=["display: none"],
                      data_bind="visible: loginState.isAdmin")
    )
    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

    templates["sidebar"]["entries"] = dict(
        connection=(gettext("Connection"), dict(template="sidebar/connection.jinja2", _div="connection", icon="signal",
                                                styles_wrapper=["display: none"],
                                                data_bind="visible: loginState.isAdmin")),
        state=(gettext("State"), dict(template="sidebar/state.jinja2", _div="state", icon="info-sign")),
        files=(gettext("Files"),
               dict(template="sidebar/files.jinja2", _div="files", icon="list", classes_content=["overflow_visible"],
                    template_header="sidebar/files_header.jinja2"))
    )

    # tabs

    templates["tab"]["entries"] = dict(
        temperature=(gettext("Temperature"), dict(template="tabs/temperature.jinja2", _div="temp")),
        control=(gettext("Control"), dict(template="tabs/control.jinja2", _div="control")),
        terminal=(gettext("Terminal"), dict(template="tabs/terminal.jinja2", _div="term")),
    )
    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

    templates["settings"]["entries"] = dict(
        section_printer=(gettext("Printer"), None),

        serial=(gettext("Serial Connection"),
                dict(template="dialogs/settings/serialconnection.jinja2", _div="settings_serialConnection",
                     custom_bindings=False)),
        printerprofiles=(gettext("Printer Profiles"),
                         dict(template="dialogs/settings/printerprofiles.jinja2", _div="settings_printerProfiles",
                              custom_bindings=False)),
        temperatures=(gettext("Temperatures"),
                      dict(template="dialogs/settings/temperatures.jinja2", _div="settings_temperature",
                           custom_bindings=False)),
        terminalfilters=(gettext("Terminal Filters"),
                         dict(template="dialogs/settings/terminalfilters.jinja2", _div="settings_terminalFilters",
                              custom_bindings=False)),
        gcodescripts=(gettext("GCODE Scripts"),
                      dict(template="dialogs/settings/gcodescripts.jinja2", _div="settings_gcodeScripts",
                           custom_bindings=False)),

        section_features=(gettext("Features"), None),

        features=(gettext("Features"),
                  dict(template="dialogs/settings/features.jinja2", _div="settings_features", custom_bindings=False)),
        webcam=(gettext("Webcam"),
                dict(template="dialogs/settings/webcam.jinja2", _div="settings_webcam", custom_bindings=False)),
        api=(gettext("API"), dict(template="dialogs/settings/api.jinja2", _div="settings_api", custom_bindings=False)),

        section_octoprint=(gettext("OctoPrint"), None),

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

    # extract data from template plugins

    template_plugins = pluginManager.get_implementations(octoprint.plugin.TemplatePlugin)

    plugin_vars = dict()
    plugin_names = set()
    for implementation in template_plugins:
        name = implementation._identifier
        plugin_names.add(name)

        try:
            vars = implementation.get_template_vars()
            configs = implementation.get_template_configs()
        except:
            _logger.exception("Error while retrieving template data for plugin {}, ignoring it".format(name))
            continue

        if not isinstance(vars, dict):
            vars = dict()
        if not isinstance(configs, (list, tuple)):
            configs = []

        for var_name, var_value in vars.items():
            plugin_vars["plugin_" + name + "_" + var_name] = var_value

        includes = _process_template_configs(name, implementation, configs, template_rules)

        for t in template_types:
            for include in includes[t]:
                if t == "navbar" or t == "generic":
                    data = include
                else:
                    data = include[1]

                key = data["_key"]
                if "replaces" in data:
                    key = data["replaces"]
                templates[t]["entries"][key] = include

    # ~~ order internal templates and plugins

    # make sure that
    # 1) we only have keys in our ordered list that we have entries for and
    # 2) we have all entries located somewhere within the order

    for t in template_types:
        default_order = settings().get(["appearance", "components", "order", t], merged=True, config=dict()) or []
        configured_order = settings().get(["appearance", "components", "order", t], merged=True) or []
        configured_disabled = settings().get(["appearance", "components", "disabled", t]) or []

        # first create the ordered list of all component ids according to the configured order
        templates[t]["order"] = [x for x in configured_order if
                                 x in templates[t]["entries"] and not x in configured_disabled]

        # now append the entries from the default order that are not already in there
        templates[t]["order"] += [x for x in default_order if not x in templates[t]["order"] and x in templates[t][
            "entries"] and not x in configured_disabled]

        all_ordered = set(templates[t]["order"])
        all_disabled = set(configured_disabled)

        # check if anything is missing, if not we are done here
        missing_in_order = set(templates[t]["entries"].keys()).difference(all_ordered).difference(all_disabled)
        if len(missing_in_order) == 0:
            continue

        # finally add anything that's not included in our order yet
        sorted_missing = list(missing_in_order)
        if template_sorting[t]["key"] is not None:
            # default extractor: works with entries that are dicts and entries that are 2-tuples with the
            # entry data at index 1
            def extractor(item, key):
                if isinstance(item, dict) and key in item:
                    return item[key]
                elif isinstance(item, tuple) and len(item) > 1 and isinstance(item[1], dict) and key in item[1]:
                    return item[1][key]

                return None

            # if template type provides custom extractor, make sure its exceptions are handled
            if "key_extractor" in template_sorting[t] and callable(template_sorting[t]["key_extractor"]):
                def create_safe_extractor(extractor):
                    def f(x, k):
                        try:
                            return extractor(x, k)
                        except:
                            _logger.exception("Error while extracting sorting keys for template {}".format(t))
                            return None

                    return f

                extractor = create_safe_extractor(template_sorting[t]["key_extractor"])

            sort_key = template_sorting[t]["key"]
            sorted_missing = sorted(missing_in_order, key=lambda x: extractor(templates[t]["entries"][x], sort_key))

        if template_sorting[t]["add"] == "prepend":
            templates[t]["order"] = sorted_missing + templates[t]["order"]
        elif template_sorting[t]["add"] == "append":
            templates[t]["order"] += sorted_missing
        elif template_sorting[t]["add"] == "custom_prepend" and "custom_add_entries" in template_sorting[
            t] and "custom_add_order" in template_sorting[t]:
            templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
            templates[t]["order"] = template_sorting[t]["custom_add_order"](sorted_missing) + templates[t]["order"]
        elif template_sorting[t]["add"] == "custom_append" and "custom_add_entries" in template_sorting[
            t] and "custom_add_order" in template_sorting[t]:
            templates[t]["entries"].update(template_sorting[t]["custom_add_entries"](sorted_missing))
            templates[t]["order"] += template_sorting[t]["custom_add_order"](sorted_missing)

    # ~~ prepare full set of template vars for rendering

    first_run = settings().getBoolean(["server", "firstRun"]) and (
        userManager is None or not userManager.hasBeenCustomized())
    render_kwargs = dict(
        webcamStream=settings().get(["webcam", "stream"]),
        enableTemperatureGraph=settings().get(["feature", "temperatureGraph"]),
        enableAccessControl=userManager is not None,
        enableSdSupport=settings().get(["feature", "sdSupport"]),
        firstRun=first_run,
        debug=debug,
        version=VERSION,
        display_version=DISPLAY_VERSION,
        branch=BRANCH,
        gcodeMobileThreshold=settings().get(["gcodeViewer", "mobileSizeThreshold"]),
        gcodeThreshold=settings().get(["gcodeViewer", "sizeThreshold"]),
        uiApiKey=UI_API_KEY,
        templates=templates,
        pluginNames=plugin_names,
        locales=locales
    )
    render_kwargs.update(plugin_vars)

    # ~~ render!

    import datetime

    response = make_response(render_template(
        "index.jinja2",
        **render_kwargs
    ))
    response.headers["Last-Modified"] = datetime.datetime.now()

    if first_run:
        response = util.flask.add_non_caching_response_headers(response)

    return response