diff --git a/htdocs/settings/general.html b/htdocs/settings/general.html
index 8b628b6..de0353e 100644
--- a/htdocs/settings/general.html
+++ b/htdocs/settings/general.html
@@ -13,7 +13,7 @@
${header}
-
General settings
+ ${title}
${sections}
diff --git a/owrx/controllers/settings/__init__.py b/owrx/controllers/settings/__init__.py
index 0970b3d..907ceda 100644
--- a/owrx/controllers/settings/__init__.py
+++ b/owrx/controllers/settings/__init__.py
@@ -1,6 +1,8 @@
from owrx.config import Config
from owrx.controllers.admin import AuthorizationMixin
from owrx.controllers.template import WebpageController
+from abc import ABCMeta, abstractmethod
+from urllib.parse import parse_qs
class Section(object):
@@ -31,3 +33,58 @@ class Section(object):
class SettingsController(AuthorizationMixin, WebpageController):
def indexAction(self):
self.serve_template("settings.html", **self.template_variables())
+
+
+class SettingsFormController(AuthorizationMixin, WebpageController, metaclass=ABCMeta):
+ @abstractmethod
+ def getSections(self):
+ pass
+
+ @abstractmethod
+ def getTitle(self):
+ pass
+
+ def render_sections(self):
+ sections = "".join(section.render() for section in self.getSections())
+ return """
+
+ """.format(
+ sections=sections
+ )
+
+ def indexAction(self):
+ self.serve_template("settings/general.html", **self.template_variables())
+
+ def header_variables(self):
+ variables = super().header_variables()
+ variables["assets_prefix"] = "../"
+ return variables
+
+ def template_variables(self):
+ variables = super().template_variables()
+ variables["sections"] = self.render_sections()
+ variables["title"] = self.getTitle()
+ return variables
+
+ def parseFormData(self):
+ data = parse_qs(self.get_body().decode("utf-8"), keep_blank_values=True)
+ return {k: v for i in self.getSections() for k, v in i.parse(data).items()}
+
+ def processFormData(self):
+ self.processData(self.parseFormData())
+
+ def processData(self, data):
+ config = Config.get()
+ for k, v in data.items():
+ if v is None:
+ if k in config:
+ del config[k]
+ else:
+ config[k] = v
+ config.store()
+ self.send_redirect(self.request.path)
diff --git a/owrx/controllers/settings/general.py b/owrx/controllers/settings/general.py
index c19a16a..2f1ff72 100644
--- a/owrx/controllers/settings/general.py
+++ b/owrx/controllers/settings/general.py
@@ -1,9 +1,5 @@
-from owrx.controllers.settings import Section
-from owrx.controllers.template import WebpageController
-from owrx.controllers.admin import AuthorizationMixin
+from owrx.controllers.settings import Section, SettingsFormController
from owrx.config.core import CoreConfig
-from owrx.config import Config
-from urllib.parse import parse_qs
from owrx.form import (
TextInput,
NumberInput,
@@ -33,292 +29,270 @@ import logging
logger = logging.getLogger(__name__)
-class GeneralSettingsController(AuthorizationMixin, WebpageController):
- sections = [
- Section(
- "Receiver information",
- TextInput("receiver_name", "Receiver name"),
- TextInput("receiver_location", "Receiver location"),
- NumberInput(
- "receiver_asl",
- "Receiver elevation",
- append="meters above mean sea level",
- ),
- TextInput("receiver_admin", "Receiver admin"),
- LocationInput("receiver_gps", "Receiver coordinates"),
- TextInput("photo_title", "Photo title"),
- TextAreaInput("photo_desc", "Photo description"),
- ),
- Section(
- "Receiver images",
- AvatarInput(
- "receiver_avatar",
- "Receiver Avatar",
- infotext="For performance reasons, images are cached. "
- + "It can take a few hours until they appear on the site.",
- ),
- TopPhotoInput(
- "receiver_top_photo",
- "Receiver Panorama",
- infotext="For performance reasons, images are cached. "
- + "It can take a few hours until they appear on the site.",
- ),
- ),
- Section(
- "Receiver limits",
- NumberInput(
- "max_clients",
- "Maximum number of clients",
- ),
- ),
- Section(
- "Receiver listings",
- TextAreaInput(
- "receiver_keys",
- "Receiver keys",
- converter=ReceiverKeysConverter(),
- infotext="Put the keys you receive on listing sites (e.g. "
- + 'Receiverbook) here, one per line',
- ),
- ),
- Section(
- "Waterfall settings",
- NumberInput(
- "fft_fps",
- "FFT speed",
- infotext="This setting specifies how many lines are being added to the waterfall per second. "
- + "Higher values will give you a faster waterfall, but will also use more CPU.",
- append="frames per second",
- ),
- NumberInput("fft_size", "FFT size", append="bins"),
- FloatInput(
- "fft_voverlap_factor",
- "FFT vertical overlap factor",
- infotext="If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the "
- + "diagram.",
- ),
- NumberInput("waterfall_min_level", "Lowest waterfall level", append="dBFS"),
- NumberInput("waterfall_max_level", "Highest waterfall level", append="dBFS"),
- ),
- Section(
- "Compression",
- DropdownInput(
- "audio_compression",
- "Audio compression",
- options=[
- Option("adpcm", "ADPCM"),
- Option("none", "None"),
- ],
- ),
- DropdownInput(
- "fft_compression",
- "Waterfall compression",
- options=[
- Option("adpcm", "ADPCM"),
- Option("none", "None"),
- ],
- ),
- ),
- Section(
- "Digimodes",
- CheckboxInput("digimodes_enable", "", checkboxText="Enable Digimodes"),
- NumberInput("digimodes_fft_size", "Digimodes FFT size", append="bins"),
- ),
- Section(
- "Demodulator settings",
- NumberInput(
- "squelch_auto_margin",
- "Auto-Squelch threshold",
- infotext="Offset to be added to the current signal level when using the auto-squelch",
- append="dB",
- ),
- DropdownInput(
- "wfm_deemphasis_tau",
- "Tau setting for WFM (broadcast FM) deemphasis",
- WfmTauValues,
- infotext='See '
- + "this Wikipedia article for more information",
- ),
- ),
- Section(
- "Display settings",
- NumberInput(
- "frequency_display_precision",
- "Frequency display precision",
- infotext="Number of decimal digits to show on the frequency display",
- ),
- ),
- Section(
- "Digital voice",
- NumberInput(
- "digital_voice_unvoiced_quality",
- "Quality of unvoiced sounds in synthesized voice",
- infotext="Determines the quality, and thus the cpu usage, for the ambe codec used by digital voice"
- + "modes.
If you're running on a Raspi (up to 3B+) you should leave this set at 1",
- ),
- CheckboxInput(
- "digital_voice_dmr_id_lookup",
- "DMR id lookup",
- checkboxText="Enable lookup of DMR ids in the radioid database to show callsigns and names",
- ),
- ),
- Section(
- "Map settings",
- TextInput(
- "google_maps_api_key",
- "Google Maps API key",
- infotext="Google Maps requires an API key, check out "
- + ''
- + "their documentation on how to obtain one.",
- ),
- NumberInput(
- "map_position_retention_time",
- "Map retention time",
- infotext="Specifies how log markers / grids will remain visible on the map",
- append="s",
- ),
- ),
- Section(
- "Decoding settings",
- NumberInput("decoding_queue_workers", "Number of decoding workers"),
- NumberInput("decoding_queue_length", "Maximum length of decoding job queue"),
- NumberInput(
- "wsjt_decoding_depth",
- "Default WSJT decoding depth",
- infotext="A higher decoding depth will allow more results, but will also consume more cpu",
- ),
- NumberInput(
- "js8_decoding_depth",
- "Js8Call decoding depth",
- infotext="A higher decoding depth will allow more results, but will also consume more cpu",
- ),
- Js8ProfileCheckboxInput("js8_enabled_profiles", "Js8Call enabled modes"),
- MultiCheckboxInput(
- "fst4_enabled_intervals",
- "Enabled FST4 intervals",
- [Option(v, "{}s".format(v)) for v in Fst4Profile.availableIntervals],
- ),
- MultiCheckboxInput(
- "fst4w_enabled_intervals",
- "Enabled FST4W intervals",
- [Option(v, "{}s".format(v)) for v in Fst4wProfile.availableIntervals],
- ),
- Q65ModeMatrix("q65_enabled_combinations", "Enabled Q65 Mode combinations"),
- ),
- Section(
- "Background decoding",
- CheckboxInput(
- "services_enabled",
- "Service",
- checkboxText="Enable background decoding services",
- ),
- ServicesCheckboxInput("services_decoders", "Enabled services"),
- ),
- Section(
- "APRS settings",
- TextInput(
- "aprs_callsign",
- "APRS callsign",
- infotext="This callsign will be used to send data to the APRS-IS network",
- ),
- CheckboxInput(
- "aprs_igate_enabled",
- "APRS I-Gate",
- checkboxText="Send received APRS data to APRS-IS",
- ),
- TextInput("aprs_igate_server", "APRS-IS server"),
- TextInput("aprs_igate_password", "APRS-IS network password"),
- CheckboxInput(
- "aprs_igate_beacon",
- "APRS beacon",
- checkboxText="Send the receiver position to the APRS-IS network",
- infotext="Please check that your receiver location is setup correctly before enabling the beacon",
- ),
- DropdownInput(
- "aprs_igate_symbol",
- "APRS beacon symbol",
- AprsBeaconSymbols,
- ),
- TextInput(
- "aprs_igate_comment",
- "APRS beacon text",
- infotext="This text will be sent as APRS comment along with your beacon",
- converter=OptionalConverter(),
- ),
- NumberInput(
- "aprs_igate_height",
- "Antenna height",
- infotext="Antenna height above average terrain (HAAT)",
- append="m",
- converter=OptionalConverter(),
- ),
- NumberInput(
- "aprs_igate_gain",
- "Antenna gain",
- append="dBi",
- converter=OptionalConverter(),
- ),
- DropdownInput("aprs_igate_dir", "Antenna direction", AprsAntennaDirections),
- ),
- Section(
- "pskreporter settings",
- CheckboxInput(
- "pskreporter_enabled",
- "Reporting",
- checkboxText="Enable sending spots to pskreporter.info",
- ),
- TextInput(
- "pskreporter_callsign",
- "pskreporter callsign",
- infotext="This callsign will be used to send spots to pskreporter.info",
- ),
- TextInput(
- "pskreporter_antenna_information",
- "Antenna information",
- infotext="Antenna description to be sent along with spots to pskreporter",
- converter=OptionalConverter(),
- ),
- ),
- Section(
- "WSPRnet settings",
- CheckboxInput(
- "wsprnet_enabled",
- "Reporting",
- checkboxText="Enable sending spots to wsprnet.org",
- ),
- TextInput(
- "wsprnet_callsign",
- "wsprnet callsign",
- infotext="This callsign will be used to send spots to wsprnet.org",
- ),
- ),
- ]
+class GeneralSettingsController(SettingsFormController):
+ def getTitle(self):
+ return "General Settings"
- def render_sections(self):
- sections = "".join(section.render() for section in GeneralSettingsController.sections)
- return """
-
- """.format(
- sections=sections
- )
-
- def indexAction(self):
- self.serve_template("settings/general.html", **self.template_variables())
-
- def header_variables(self):
- variables = super().header_variables()
- variables["assets_prefix"] = "../"
- return variables
-
- def template_variables(self):
- variables = super().template_variables()
- variables["sections"] = self.render_sections()
- return variables
+ def getSections(self):
+ return [
+ Section(
+ "Receiver information",
+ TextInput("receiver_name", "Receiver name"),
+ TextInput("receiver_location", "Receiver location"),
+ NumberInput(
+ "receiver_asl",
+ "Receiver elevation",
+ append="meters above mean sea level",
+ ),
+ TextInput("receiver_admin", "Receiver admin"),
+ LocationInput("receiver_gps", "Receiver coordinates"),
+ TextInput("photo_title", "Photo title"),
+ TextAreaInput("photo_desc", "Photo description"),
+ ),
+ Section(
+ "Receiver images",
+ AvatarInput(
+ "receiver_avatar",
+ "Receiver Avatar",
+ infotext="For performance reasons, images are cached. "
+ + "It can take a few hours until they appear on the site.",
+ ),
+ TopPhotoInput(
+ "receiver_top_photo",
+ "Receiver Panorama",
+ infotext="For performance reasons, images are cached. "
+ + "It can take a few hours until they appear on the site.",
+ ),
+ ),
+ Section(
+ "Receiver limits",
+ NumberInput(
+ "max_clients",
+ "Maximum number of clients",
+ ),
+ ),
+ Section(
+ "Receiver listings",
+ TextAreaInput(
+ "receiver_keys",
+ "Receiver keys",
+ converter=ReceiverKeysConverter(),
+ infotext="Put the keys you receive on listing sites (e.g. "
+ + 'Receiverbook) here, one per line',
+ ),
+ ),
+ Section(
+ "Waterfall settings",
+ NumberInput(
+ "fft_fps",
+ "FFT speed",
+ infotext="This setting specifies how many lines are being added to the waterfall per second. "
+ + "Higher values will give you a faster waterfall, but will also use more CPU.",
+ append="frames per second",
+ ),
+ NumberInput("fft_size", "FFT size", append="bins"),
+ FloatInput(
+ "fft_voverlap_factor",
+ "FFT vertical overlap factor",
+ infotext="If fft_voverlap_factor is above 0, multiple FFTs will be used for creating a line on the "
+ + "diagram.",
+ ),
+ NumberInput("waterfall_min_level", "Lowest waterfall level", append="dBFS"),
+ NumberInput("waterfall_max_level", "Highest waterfall level", append="dBFS"),
+ ),
+ Section(
+ "Compression",
+ DropdownInput(
+ "audio_compression",
+ "Audio compression",
+ options=[
+ Option("adpcm", "ADPCM"),
+ Option("none", "None"),
+ ],
+ ),
+ DropdownInput(
+ "fft_compression",
+ "Waterfall compression",
+ options=[
+ Option("adpcm", "ADPCM"),
+ Option("none", "None"),
+ ],
+ ),
+ ),
+ Section(
+ "Digimodes",
+ CheckboxInput("digimodes_enable", "", checkboxText="Enable Digimodes"),
+ NumberInput("digimodes_fft_size", "Digimodes FFT size", append="bins"),
+ ),
+ Section(
+ "Demodulator settings",
+ NumberInput(
+ "squelch_auto_margin",
+ "Auto-Squelch threshold",
+ infotext="Offset to be added to the current signal level when using the auto-squelch",
+ append="dB",
+ ),
+ DropdownInput(
+ "wfm_deemphasis_tau",
+ "Tau setting for WFM (broadcast FM) deemphasis",
+ WfmTauValues,
+ infotext='See '
+ + "this Wikipedia article for more information",
+ ),
+ ),
+ Section(
+ "Display settings",
+ NumberInput(
+ "frequency_display_precision",
+ "Frequency display precision",
+ infotext="Number of decimal digits to show on the frequency display",
+ ),
+ ),
+ Section(
+ "Digital voice",
+ NumberInput(
+ "digital_voice_unvoiced_quality",
+ "Quality of unvoiced sounds in synthesized voice",
+ infotext="Determines the quality, and thus the cpu usage, for the ambe codec used by digital voice"
+ + "modes.
If you're running on a Raspi (up to 3B+) you should leave this set at 1",
+ ),
+ CheckboxInput(
+ "digital_voice_dmr_id_lookup",
+ "DMR id lookup",
+ checkboxText="Enable lookup of DMR ids in the radioid database to show callsigns and names",
+ ),
+ ),
+ Section(
+ "Map settings",
+ TextInput(
+ "google_maps_api_key",
+ "Google Maps API key",
+ infotext="Google Maps requires an API key, check out "
+ + ''
+ + "their documentation on how to obtain one.",
+ ),
+ NumberInput(
+ "map_position_retention_time",
+ "Map retention time",
+ infotext="Specifies how log markers / grids will remain visible on the map",
+ append="s",
+ ),
+ ),
+ Section(
+ "Decoding settings",
+ NumberInput("decoding_queue_workers", "Number of decoding workers"),
+ NumberInput("decoding_queue_length", "Maximum length of decoding job queue"),
+ NumberInput(
+ "wsjt_decoding_depth",
+ "Default WSJT decoding depth",
+ infotext="A higher decoding depth will allow more results, but will also consume more cpu",
+ ),
+ NumberInput(
+ "js8_decoding_depth",
+ "Js8Call decoding depth",
+ infotext="A higher decoding depth will allow more results, but will also consume more cpu",
+ ),
+ Js8ProfileCheckboxInput("js8_enabled_profiles", "Js8Call enabled modes"),
+ MultiCheckboxInput(
+ "fst4_enabled_intervals",
+ "Enabled FST4 intervals",
+ [Option(v, "{}s".format(v)) for v in Fst4Profile.availableIntervals],
+ ),
+ MultiCheckboxInput(
+ "fst4w_enabled_intervals",
+ "Enabled FST4W intervals",
+ [Option(v, "{}s".format(v)) for v in Fst4wProfile.availableIntervals],
+ ),
+ Q65ModeMatrix("q65_enabled_combinations", "Enabled Q65 Mode combinations"),
+ ),
+ Section(
+ "Background decoding",
+ CheckboxInput(
+ "services_enabled",
+ "Service",
+ checkboxText="Enable background decoding services",
+ ),
+ ServicesCheckboxInput("services_decoders", "Enabled services"),
+ ),
+ Section(
+ "APRS settings",
+ TextInput(
+ "aprs_callsign",
+ "APRS callsign",
+ infotext="This callsign will be used to send data to the APRS-IS network",
+ ),
+ CheckboxInput(
+ "aprs_igate_enabled",
+ "APRS I-Gate",
+ checkboxText="Send received APRS data to APRS-IS",
+ ),
+ TextInput("aprs_igate_server", "APRS-IS server"),
+ TextInput("aprs_igate_password", "APRS-IS network password"),
+ CheckboxInput(
+ "aprs_igate_beacon",
+ "APRS beacon",
+ checkboxText="Send the receiver position to the APRS-IS network",
+ infotext="Please check that your receiver location is setup correctly before enabling the beacon",
+ ),
+ DropdownInput(
+ "aprs_igate_symbol",
+ "APRS beacon symbol",
+ AprsBeaconSymbols,
+ ),
+ TextInput(
+ "aprs_igate_comment",
+ "APRS beacon text",
+ infotext="This text will be sent as APRS comment along with your beacon",
+ converter=OptionalConverter(),
+ ),
+ NumberInput(
+ "aprs_igate_height",
+ "Antenna height",
+ infotext="Antenna height above average terrain (HAAT)",
+ append="m",
+ converter=OptionalConverter(),
+ ),
+ NumberInput(
+ "aprs_igate_gain",
+ "Antenna gain",
+ append="dBi",
+ converter=OptionalConverter(),
+ ),
+ DropdownInput("aprs_igate_dir", "Antenna direction", AprsAntennaDirections),
+ ),
+ Section(
+ "pskreporter settings",
+ CheckboxInput(
+ "pskreporter_enabled",
+ "Reporting",
+ checkboxText="Enable sending spots to pskreporter.info",
+ ),
+ TextInput(
+ "pskreporter_callsign",
+ "pskreporter callsign",
+ infotext="This callsign will be used to send spots to pskreporter.info",
+ ),
+ TextInput(
+ "pskreporter_antenna_information",
+ "Antenna information",
+ infotext="Antenna description to be sent along with spots to pskreporter",
+ converter=OptionalConverter(),
+ ),
+ ),
+ Section(
+ "WSPRnet settings",
+ CheckboxInput(
+ "wsprnet_enabled",
+ "Reporting",
+ checkboxText="Enable sending spots to wsprnet.org",
+ ),
+ TextInput(
+ "wsprnet_callsign",
+ "wsprnet callsign",
+ infotext="This callsign will be used to send spots to wsprnet.org",
+ ),
+ ),
+ ]
def handle_image(self, data, image_id):
if image_id in data:
@@ -344,18 +318,8 @@ class GeneralSettingsController(AuthorizationMixin, WebpageController):
for file in glob("{}/{}*".format(config.get_temporary_directory(), image_id)):
os.unlink(file)
- def processFormData(self):
- data = parse_qs(self.get_body().decode("utf-8"), keep_blank_values=True)
- data = {k: v for i in GeneralSettingsController.sections for k, v in i.parse(data).items()}
+ def processData(self, data):
# Image handling
for img in ["receiver_avatar", "receiver_top_photo"]:
self.handle_image(data, img)
- config = Config.get()
- for k, v in data.items():
- if v is None:
- if k in config:
- del config[k]
- else:
- config[k] = v
- config.store()
- self.send_redirect("/settings/general")
+ super().processData(data)