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 """ +
+ {sections} +
+ +
+
+ """.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 """ -
- {sections} -
- -
-
- """.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)