Replace pages' use of the options call with directly building the HTML components using templates, as the syntax is much nicer. Use a common set of data provided to all modules via pagetemplate.py instead of passing things like web_ui_options around. Closes #153

This commit is contained in:
Ian Renton
2026-09-25 21:22:31 +01:00
parent 4df00c321c
commit 8de163469b
27 changed files with 493 additions and 534 deletions
+52 -62
View File
@@ -1,3 +1,4 @@
import functools
import logging
from typing import Any
@@ -7,6 +8,7 @@ from tornado.web import Application
from core.config import ALLOW_SPOTTING, MAX_SPOT_AGE
from core.constants import BANDS, PROPAGATION_MODES
from core.data_providers import DATA_PROVIDERS
from core.enums import Continent, Mode, ModeType
from core.utils import safe_json_dumps
from data.activities import ACTIVITIES
@@ -14,6 +16,54 @@ from data.activities import ACTIVITIES
logger = logging.getLogger(__name__)
@functools.cache
def build_options():
"""Build the options data that is returned by the /options API and used to populate the web UI templates. This
doesn't change at runtime, so it is built once on first use and then cached."""
# Build a map of activity name -> list of provider names that can submit spots for that activity
spot_submit_providers = {}
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
# for provider in DATA_PROVIDERS.spot_providers:
# if not provider.enabled:
# continue
# for activity in ACTIVITIES.values():
# if provider.can_submit_spot(activity.name):
# spot_submit_providers.setdefault(activity.name, []).append(provider.name)
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
# things that aren't even available.
spot_providers = [p.name for p in DATA_PROVIDERS.spot_providers if p.enabled]
alert_providers = [p.name for p in DATA_PROVIDERS.alert_providers if p.enabled]
callsign_data_providers = [p.name for p in DATA_PROVIDERS.callsign_data_providers if p.enabled]
spot_providers_enabled_by_default = [
p.name for p in DATA_PROVIDERS.spot_providers if p.enabled and p.enabled_by_default_in_web_ui
]
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
# one of our providers.
if ALLOW_SPOTTING:
spot_providers.append("API")
spot_providers_enabled_by_default.append("API")
return {
"bands": BANDS,
"modes": [m.value for m in Mode],
"mode_types": [t.value for t in ModeType],
"activities": list(ACTIVITIES.values()),
"spot_providers": spot_providers,
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
"alert_providers": alert_providers,
"callsign_data_providers": callsign_data_providers,
"continents": [c.value for c in Continent],
"propagation_modes": list(PROPAGATION_MODES.values()),
"max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING,
"spot_submit_providers": spot_submit_providers,
}
class APIOptionsHandler(tornado.web.RequestHandler):
"""API request handler for /api/v3/options"""
@@ -23,73 +73,13 @@ class APIOptionsHandler(tornado.web.RequestHandler):
request: httputil.HTTPServerRequest,
**kwargs: Any,
):
self._status_data = None
self._spot_providers = None
super().__init__(application, request, **kwargs)
def initialize(self, status_data, spot_providers=None):
self._status_data = status_data
self._spot_providers = spot_providers or []
def get(self):
try:
# Build a map of activity name -> list of provider names that can submit spots for that activity
spot_submit_providers = {}
# Spothole v2.0 - disable this for now, API changes are in but this functionality is not ready yet. TODO
# for provider in self._spot_providers:
# if not provider.enabled:
# continue
# for activity in ACTIVITIES.values():
# if provider.can_submit_spot(activity.name):
# spot_submit_providers.setdefault(activity.name, []).append(provider.name)
# Spot/alert sources are filtered for only ones that are enabled in config, no point letting the user toggle
# things that aren't even available.
spot_providers: list = [
p["name"] for p in filter(lambda p: p["enabled"], self._status_data["spot_providers"])
]
alert_providers = [p["name"] for p in filter(lambda p: p["enabled"], self._status_data["alert_providers"])]
callsign_data_providers = [
p["name"]
for p in filter(
lambda p: p["enabled"],
self._status_data["callsign_data_providers"],
)
]
spot_providers_enabled_by_default = [
p["name"]
for p in filter(
lambda p: p["enabled"] and p["enabled_by_default_in_web_ui"],
self._status_data["spot_providers"],
)
]
# If spotting to this server is enabled, "API" is another valid spot source even though it does not come from
# one of our providers.
if ALLOW_SPOTTING:
spot_providers.append("API")
spot_providers_enabled_by_default.append("API")
options = {
"bands": BANDS,
"modes": [m.value for m in Mode],
"mode_types": [t.value for t in ModeType],
"activities": list(ACTIVITIES.values()),
"spot_providers": spot_providers,
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
"alert_providers": alert_providers,
"callsign_data_providers": callsign_data_providers,
"continents": [c.value for c in Continent],
"propagation_modes": list(PROPAGATION_MODES.values()),
"max_spot_age": MAX_SPOT_AGE,
"spot_allowed": ALLOW_SPOTTING,
"spot_submit_providers": spot_submit_providers,
}
self.write(safe_json_dumps(options))
self.write(safe_json_dumps(build_options()))
self.set_status(200)
self.set_header("Cache-Control", "no-store")
self.set_header("Cache-Control", "max-age=3600")
self.set_header("Content-Type", "application/json")
except Exception:
+26 -8
View File
@@ -1,3 +1,4 @@
import re
from typing import Any
import tornado
@@ -5,7 +6,6 @@ from tornado import httputil
from tornado.web import Application
from core.config import (
ALLOW_SPOTTING,
BASE_URL,
SERVER_OWNER_CALLSIGN,
TELNET_SERVER_ADDRESS,
@@ -14,6 +14,14 @@ from core.config import (
WEB_UI_OPTIONS,
)
from core.constants import SOFTWARE_VERSION
from core.utils import infer_mode_type_from_mode, safe_json_dumps
from webserver.handlers.api.options import build_options
def dom_safe_name(name):
"""Strip characters from a name that are not allowed in HTML element IDs"""
return re.sub(r"^[^A-Za-z0-9]+|\W+", "", name, flags=re.ASCII)
class PageTemplateHandler(tornado.web.RequestHandler):
@@ -31,17 +39,27 @@ class PageTemplateHandler(tornado.web.RequestHandler):
def initialize(self, template_name):
self._template_name = template_name
def get(self):
# Load named template, and provide variables used in templates
self.render(
f"{self._template_name}.html",
def get_template_namespace(self):
"""Provide variables and helper functions to all page templates"""
namespace = super().get_template_namespace()
namespace.update(
# Various options
options=build_options(),
web_ui_options=WEB_UI_OPTIONS,
# Other single static parameters
software_version=SOFTWARE_VERSION,
server_owner_callsign=SERVER_OWNER_CALLSIGN,
allow_spotting=ALLOW_SPOTTING,
web_ui_options=WEB_UI_OPTIONS,
baseurl=BASE_URL,
telnet_server_enabled=TELNET_SERVER_ENABLED,
telnet_server_address=TELNET_SERVER_ADDRESS,
telnet_server_port=TELNET_SERVER_PORT,
current_path=self.request.path,
# Function references that templates need
dom_safe_name=dom_safe_name,
safe_json_dumps=safe_json_dumps,
infer_mode_type_from_mode=infer_mode_type_from_mode,
)
return namespace
def get(self):
self.render(f"{self._template_name}.html")
+3 -15
View File
@@ -161,11 +161,7 @@ class WebServer:
APIDxStatsHandler,
{"spots": self._data_store.spots},
),
(
r"/api/v3/options",
APIOptionsHandler,
{"status_data": self._data_store.status.get()},
),
(r"/api/v3/options", APIOptionsHandler),
(
r"/api/v3/status",
APIStatusHandler,
@@ -216,11 +212,7 @@ class WebServer:
V2APIDxStatsHandler,
{"spots": self._data_store.spots},
),
(
r"/api/v2/options",
V2APIOptionsHandler,
{"status_data": self._data_store.status.get()},
),
(r"/api/v2/options", V2APIOptionsHandler),
(
r"/api/v2/status",
V2APIStatusHandler,
@@ -271,11 +263,7 @@ class WebServer:
V1APIDxStatsHandler,
{"spots": self._data_store.spots},
),
(
r"/api/v1/options",
V1APIOptionsHandler,
{"status_data": self._data_store.status.get()},
),
(r"/api/v1/options", V1APIOptionsHandler),
(
r"/api/v1/status",
V1APIStatusHandler,