refactor owrx.form -> owrx.form.input

This commit is contained in:
Jakob Ketterl
2021-04-29 15:17:21 +02:00
parent bc193c834c
commit 35dcff90ea
28 changed files with 49 additions and 48 deletions

409
owrx/form/input/__init__.py Normal file
View File

@ -0,0 +1,409 @@
from abc import ABC
from owrx.modes import Modes
from owrx.config import Config
from owrx.form.input.validator import Validator
from owrx.form.input.converter import Converter, NullConverter, IntConverter, FloatConverter, EnumConverter
from enum import Enum
class Input(ABC):
def __init__(self, id, label, infotext=None, converter: Converter = None, validator: Validator = None, disabled=False, removable=False):
self.id = id
self.label = label
self.infotext = infotext
self.converter = self.defaultConverter() if converter is None else converter
self.validator = validator
self.disabled = disabled
self.removable = removable
def setDisabled(self, disabled=True):
self.disabled = disabled
def setRemovable(self, removable=True):
self.removable = removable
def defaultConverter(self):
return NullConverter()
def bootstrap_decorate(self, input):
return """
<div class="form-group row" data-field="{id}">
<label class="col-form-label col-form-label-sm col-3" for="{id}">{label}</label>
<div class="col-9 p-0 removable-group {removable}">
<div class="removable-item">
{input}
{infotext}
</div>
{removebutton}
</div>
</div>
""".format(
id=self.id,
label=self.label,
input=input,
infotext="<small>{text}</small>".format(text=self.infotext) if self.infotext else "",
removable="removable" if self.removable else "",
removebutton='<button type="button" class="btn btn-sm btn-danger option-remove-button">Remove</button>'
if self.removable
else "",
)
def input_classes(self, errors):
classes = ["form-control", "form-control-sm"]
if errors:
classes.append("is-invalid")
return " ".join(classes)
def input_properties(self, value, errors):
props = {
"class": self.input_classes(errors),
"id": self.id,
"name": self.id,
"placeholder": self.label,
"value": value,
}
if self.disabled:
props["disabled"] = "disabled"
return props
def render_input_properties(self, value, error):
return " ".join('{}="{}"'.format(prop, value) for prop, value in self.input_properties(value, error).items())
def render_errors(self, errors):
return "".join("""<div class="invalid-feedback">{msg}</div>""".format(msg=e) for e in errors)
def render_input_group(self, value, errors):
return """
{input}
{errors}
""".format(
input=self.render_input(value, errors),
errors=self.render_errors(errors)
)
def render_input(self, value, errors):
return "<input {properties} />".format(properties=self.render_input_properties(value, errors))
def render(self, config, errors):
value = config[self.id] if self.id in config else None
error = errors[self.id] if self.id in errors else []
return self.bootstrap_decorate(self.render_input_group(self.converter.convert_to_form(value), error))
def parse(self, data):
value = self.converter.convert_from_form(data[self.id][0])
if self.validator is not None:
self.validator.validate(self.id, value)
return {self.id: value} if self.id in data else {}
def getLabel(self):
return self.label
class TextInput(Input):
def input_properties(self, value, errors):
props = super().input_properties(value, errors)
props["type"] = "text"
return props
class NumberInput(Input):
def __init__(self, id, label, infotext=None, append="", converter: Converter = None):
super().__init__(id, label, infotext, converter=converter)
self.step = None
self.append = append
def defaultConverter(self):
return IntConverter()
def input_properties(self, value, errors):
props = super().input_properties(value, errors)
props["type"] = "number"
if self.step:
props["step"] = self.step
return props
def render_input_group(self, value, errors):
if self.append:
append = """
<div class="input-group-append">
<span class="input-group-text">{append}</span>
</div>
""".format(
append=self.append
)
else:
append = ""
return """
<div class="input-group input-group-sm">
{input}
{append}
{errors}
</div>
""".format(
input=self.render_input(value, errors),
append=append,
errors=self.render_errors(errors)
)
class FloatInput(NumberInput):
def __init__(self, id, label, infotext=None, converter: Converter = None):
super().__init__(id, label, infotext, converter=converter)
self.step = "any"
def defaultConverter(self):
return FloatConverter()
class LocationInput(Input):
def render_input_group(self, value, errors):
return """
<div class="row {rowclass}">
{inputs}
</div>
{errors}
<div class="row">
<div class="col map-input" data-key="{key}" for="{id}"></div>
</div>
""".format(
id=self.id,
rowclass="is-invalid" if errors else "",
inputs=self.render_input(value, errors),
errors=self.render_errors(errors),
key=Config.get()["google_maps_api_key"],
)
def render_input(self, value, errors):
return "".join(self.render_sub_input(value, id, errors) for id in ["lat", "lon"])
def render_sub_input(self, value, id, errors):
return """
<div class="col">
<input type="number" class="{classes}" id="{id}" name="{id}" placeholder="{label}" value="{value}"
step="any" {disabled}>
</div>
""".format(
id="{0}-{1}".format(self.id, id),
label=self.label,
classes=self.input_classes(errors),
value=value[id],
disabled="disabled" if self.disabled else "",
)
def parse(self, data):
return {self.id: {k: float(data["{0}-{1}".format(self.id, k)][0]) for k in ["lat", "lon"]}}
class TextAreaInput(Input):
def render_input(self, value, errors):
return """
<textarea class="{classes}" id="{id}" name="{id}" style="height:200px;" {disabled}>{value}</textarea>
""".format(
id=self.id,
classes=self.input_classes(errors),
value=value,
disabled="disabled" if self.disabled else "",
)
class CheckboxInput(Input):
def __init__(self, id, checkboxText, infotext=None, converter: Converter = None):
super().__init__(id, "", infotext=infotext, converter=converter)
self.checkboxText = checkboxText
def render_input(self, value, errors):
return """
<div class="{classes}">
<input type="hidden" name="{id}" value="0" {disabled}>
<input class="form-check-input" type="checkbox" id="{id}" name="{id}" value="1" {checked} {disabled}>
<label class="form-check-label" for="{id}">
{checkboxText}
</label>
</div>
""".format(
id=self.id,
classes=self.input_classes(errors),
checked="checked" if value else "",
disabled="disabled" if self.disabled else "",
checkboxText=self.checkboxText,
)
def input_classes(self, error):
classes = ["form-check", "form-control-sm"]
if error:
classes.append("is-invalid")
return " ".join(classes)
def parse(self, data):
if self.id in data:
return {self.id: self.converter.convert_from_form("1" in data[self.id])}
return {}
def getLabel(self):
return self.checkboxText
class Option(object):
# used for both MultiCheckboxInput and DropdownInput
def __init__(self, value, text):
self.value = value
self.text = text
class MultiCheckboxInput(Input):
def __init__(self, id, label, options, infotext=None):
super().__init__(id, label, infotext=infotext)
self.options = options
def render_input(self, value, errors):
return "".join(self.render_checkbox(o, value, errors) for o in self.options)
def checkbox_id(self, option):
return "{0}-{1}".format(self.id, option.value)
def render_checkbox(self, option, value, errors):
return """
<div class="{classes}">
<input class="form-check-input" type="checkbox" id="{id}" name="{id}" {checked} {disabled}>
<label class="form-check-label" for="{id}">
{checkboxText}
</label>
</div>
""".format(
id=self.checkbox_id(option),
classes=self.input_classes(errors),
checked="checked" if option.value in value else "",
checkboxText=option.text,
disabled="disabled" if self.disabled else "",
)
def parse(self, data):
def in_response(option):
boxid = self.checkbox_id(option)
return boxid in data and data[boxid][0] == "on"
return {self.id: [o.value for o in self.options if in_response(o)]}
def input_classes(self, error):
classes = ["form-check", "form-control-sm"]
if error:
classes.append("is-invalid")
return " ".join(classes)
class ServicesCheckboxInput(MultiCheckboxInput):
def __init__(self, id, label, infotext=None):
services = [Option(s.modulation, s.name) for s in Modes.getAvailableServices()]
super().__init__(id, label, services, infotext)
class Js8ProfileCheckboxInput(MultiCheckboxInput):
def __init__(self, id, label, infotext=None):
profiles = [
Option("normal", "Normal (15s, 50Hz, ~16WPM)"),
Option("slow", "Slow (30s, 25Hz, ~8WPM"),
Option("fast", "Fast (10s, 80Hz, ~24WPM"),
Option("turbo", "Turbo (6s, 160Hz, ~40WPM"),
]
super().__init__(id, label, profiles, infotext)
class DropdownInput(Input):
def __init__(self, id, label, options, infotext=None, converter: Converter = None):
try:
isEnum = issubclass(options, DropdownEnum)
except TypeError:
isEnum = False
if isEnum:
self.options = [o.toOption() for o in options]
if converter is None:
converter = EnumConverter(options)
else:
self.options = options
super().__init__(id, label, infotext=infotext, converter=converter)
def render_input(self, value, errors):
return """
<select class="{classes}" id="{id}" name="{id}" {disabled}>{options}</select>
""".format(
classes=self.input_classes(errors),
id=self.id,
options=self.render_options(value),
disabled="disabled" if self.disabled else "",
)
def render_options(self, value):
options = [
"""
<option value="{value}" {selected}>{text}</option>
""".format(
text=o.text,
value=o.value,
selected="selected" if o.value == value else "",
)
for o in self.options
]
return "".join(options)
class DropdownEnum(Enum):
def toOption(self):
return Option(self.name, str(self))
class ModesInput(DropdownInput):
def __init__(self, id, label):
options = [Option(m.modulation, m.name) for m in Modes.getAvailableModes()]
super().__init__(id, label, options)
class ExponentialInput(Input):
def __init__(self, id, label, unit, infotext=None):
super().__init__(id, label, infotext=infotext)
self.unit = unit
def defaultConverter(self):
return IntConverter()
def input_properties(self, value, errors):
props = super().input_properties(value, errors)
props["type"] = "number"
props["step"] = "any"
return props
def render_input_group(self, value, errors):
append = """
<div class="input-group-append">
<select class="input-group-text exponent" name="{id}-exponent" tabindex="-1" {disabled}>
<option value="0" selected>{unit}</option>
<option value="3">k{unit}</option>
<option value="6">M{unit}</option>
<option value="9">G{unit}</option>
<option value="12">T{unit}</option>
</select>
</div>
""".format(
id=self.id,
disabled="disabled" if self.disabled else "",
unit=self.unit,
)
return """
<div class="input-group input-group-sm exponential-input">
{input}
{append}
{errors}
</div>
""".format(
input=self.render_input(value, errors),
append=append,
errors=self.render_errors(errors)
)
def parse(self, data):
exponent_id = "{}-exponent".format(self.id)
if self.id in data and exponent_id in data:
value = int(float(data[self.id][0]) * 10 ** int(data[exponent_id][0]))
return {self.id: value}
return {}

36
owrx/form/input/aprs.py Normal file
View File

@ -0,0 +1,36 @@
from owrx.form.input import DropdownEnum
class AprsBeaconSymbols(DropdownEnum):
BEACON_RECEIVE_ONLY = ("R&", "Receive only IGate")
BEACON_HF_GATEWAY = ("/&", "HF Gateway")
BEACON_IGATE_GENERIC = ("I&", "Igate Generic (please use more specific overlay)")
BEACON_PSKMAIL = ("P&", "PSKmail node")
BEACON_TX_1 = ("T&", "TX IGate with path set to 1 hop")
BEACON_WIRES_X = ("W&", "Wires-X")
BEACON_TX_2 = ("2&", "TX IGate with path set to 2 hops")
def __new__(cls, *args, **kwargs):
value, description = args
obj = object.__new__(cls)
obj._value_ = value
obj.description = description
return obj
def __str__(self):
return "{description} ({symbol})".format(description=self.description, symbol=self.value)
class AprsAntennaDirections(DropdownEnum):
DIRECTION_OMNI = None
DIRECTION_N = "N"
DIRECTION_NE = "NE"
DIRECTION_E = "E"
DIRECTION_SE = "SE"
DIRECTION_S = "S"
DIRECTION_SW = "SW"
DIRECTION_W = "W"
DIRECTION_NW = "NW"
def __str__(self):
return "omnidirectional" if self.value is None else self.value

View File

@ -0,0 +1,96 @@
from abc import ABC, abstractmethod
from owrx.jsons import Encoder
import json
class Converter(ABC):
@abstractmethod
def convert_to_form(self, value):
pass
@abstractmethod
def convert_from_form(self, value):
pass
class NullConverter(Converter):
def convert_to_form(self, value):
return value
def convert_from_form(self, value):
return value
class OptionalConverter(Converter):
"""
Transforms a special form value to None
The default is look for an empty string, but this can be used to adopt to other types.
If the default is not found, the actual value is passed to the sub_converter for further transformation.
useful for optional fields since None is not stored in the configuration
"""
def __init__(self, sub_converter: Converter = None, defaultFormValue=""):
self.sub_converter = NullConverter() if sub_converter is None else sub_converter
self.defaultFormValue = defaultFormValue
def convert_to_form(self, value):
return self.defaultFormValue if value is None else self.sub_converter.convert_to_form(value)
def convert_from_form(self, value):
return None if value == self.defaultFormValue else self.sub_converter.convert_from_form(value)
class IntConverter(Converter):
def convert_to_form(self, value):
return str(value)
def convert_from_form(self, value):
return int(value)
class FloatConverter(Converter):
def convert_to_form(self, value):
return str(value)
def convert_from_form(self, value):
return float(value)
class EnumConverter(Converter):
def __init__(self, enumCls):
self.enumCls = enumCls
def convert_to_form(self, value):
return None if value is None else self.enumCls(value).name
def convert_from_form(self, value):
return self.enumCls[value].value
class JsonConverter(Converter):
def convert_to_form(self, value):
return json.dumps(value, cls=Encoder)
def convert_from_form(self, value):
return json.loads(value)
class WaterfallColorsConverter(Converter):
def convert_to_form(self, value):
if value is None:
return ""
return "\n".join("#{:06x}".format(v) for v in value)
def convert_from_form(self, value):
def parseString(s):
try:
if s.startswith("#"):
return int(s[1:], 16)
# int() with base 0 can accept "0x" prefixed hex strings, or int numbers
return int(s, 0)
except ValueError:
return None
# \r\n or \n? this should work with both.
values = [parseString(v.strip("\r ")) for v in value.split("\n")]
return [v for v in values if v is not None]

418
owrx/form/input/device.py Normal file
View File

@ -0,0 +1,418 @@
from owrx.form.input import Input, CheckboxInput, DropdownInput, DropdownEnum, TextInput
from owrx.soapy import SoapySettings
from functools import reduce
from operator import and_
class GainInput(Input):
def __init__(self, id, label, has_agc, gain_stages=None):
super().__init__(id, label)
self.has_agc = has_agc
self.gain_stages = gain_stages
def render_input(self, value, errors):
try:
display_value = float(value)
except (ValueError, TypeError):
display_value = "0.0"
return """
<select class="{classes}" id="{id}-select" name="{id}-select" {disabled}>
{options}
</select>
<div class="option manual" style="display: none;">
<input type="number" id="{id}-manual" name="{id}-manual" value="{value}" class="{classes}"
placeholder="Manual device gain" step="any" {disabled}>
</div>
{stageoption}
""".format(
id=self.id,
classes=self.input_classes(errors),
value=display_value,
label=self.label,
options=self.render_options(value),
stageoption="" if self.gain_stages is None else self.render_stage_option(value, errors),
disabled="disabled" if self.disabled else "",
)
def render_input_group(self, value, errors):
return """
<div id="{id}">
{input}
{errors}
</div>
""".format(
id=self.id,
input=self.render_input(value, errors),
errors=self.render_errors(errors)
)
def render_options(self, value):
options = []
if self.has_agc:
options.append(("auto", "Enable hardware AGC"))
options.append(("manual", "Specify manual gain")),
if self.gain_stages:
options.append(("stages", "Specify gain stages individually"))
mode = self.getMode(value)
return "".join(
"""
<option value="{value}" {selected}>{text}</option>
""".format(
value=v[0], text=v[1], selected="selected" if mode == v[0] else ""
)
for v in options
)
def getMode(self, value):
if value is None:
return "auto" if self.has_agc else "manual"
if value == "auto":
return "auto"
try:
float(value)
return "manual"
except (ValueError, TypeError):
pass
return "stages"
def render_stage_option(self, value, errors):
try:
value_dict = {k: v for item in SoapySettings.parse(value) for k, v in item.items()}
except (AttributeError, ValueError):
value_dict = {}
return """
<div class="option stages container container-fluid" style="display: none;">
{inputs}
</div>
""".format(
inputs="".join(
"""
<div class="row">
<label class="col-form-label col-form-label-sm col-3">{stage}</label>
<input type="number" id="{id}-{stage}" name="{id}-{stage}" value="{value}"
class="col-9 {classes}" placeholder="{stage}" step="any" {disabled}>
</div>
""".format(
id=self.id,
stage=stage,
value=value_dict[stage] if stage in value_dict else "",
classes=self.input_classes(errors),
disabled="disabled" if self.disabled else "",
)
for stage in self.gain_stages
)
)
def parse(self, data):
def getStageValue(stage):
input_id = "{id}-{stage}".format(id=self.id, stage=stage)
if input_id in data:
return data[input_id][0]
else:
return None
select_id = "{id}-select".format(id=self.id)
if select_id in data:
if self.has_agc and data[select_id][0] == "auto":
return {self.id: "auto"}
if data[select_id][0] == "manual":
input_id = "{id}-manual".format(id=self.id)
value = 0.0
if input_id in data:
try:
value = float(data[input_id][0])
except ValueError:
pass
return {self.id: value}
if self.gain_stages is not None and data[select_id][0] == "stages":
settings_dict = [{s: getStageValue(s)} for s in self.gain_stages]
# filter out empty ones
settings_dict = [s for s in settings_dict if next(iter(s.values()))]
return {self.id: SoapySettings.encode(settings_dict)}
return {}
class BiasTeeInput(CheckboxInput):
def __init__(self):
super().__init__("bias_tee", "Enable Bias-Tee power supply")
class DirectSamplingOptions(DropdownEnum):
DIRECT_SAMPLING_OFF = (0, "Off")
DIRECT_SAMPLING_I = (1, "Direct Sampling (I branch)")
DIRECT_SAMPLING_Q = (2, "Direct Sampling (Q branch)")
def __new__(cls, *args, **kwargs):
value, description = args
obj = object.__new__(cls)
obj._value_ = value
obj.description = description
return obj
def __str__(self):
return self.description
class DirectSamplingInput(DropdownInput):
def __init__(self):
super().__init__(
"direct_sampling",
"Direct Sampling",
DirectSamplingOptions,
)
class RemoteInput(TextInput):
def __init__(self):
super().__init__(
"remote", "Remote IP and Port", infotext="Remote hostname or IP and port to connect to. Format = IP:Port"
)
class SchedulerInput(Input):
def __init__(self, id, label):
super().__init__(id, label)
self.profiles = {}
def render(self, config, errors):
if "profiles" in config:
self.profiles = config["profiles"]
return super().render(config, errors)
def render_profiles_select(self, value, errors, config_key, stage, extra_classes=""):
stage_value = ""
if value and "schedule" in value and config_key in value["schedule"]:
stage_value = value["schedule"][config_key]
return """
<select class="{extra_classes} {classes}" id="{id}" name="{id}" {disabled}>
{options}
</select>
""".format(
id="{}-{}".format(self.id, stage),
classes=self.input_classes(errors),
extra_classes=extra_classes,
disabled="disabled" if self.disabled else "",
options="".join(
"""
<option value={id} {selected}>{name}</option>
""".format(
id=p_id,
name=p["name"],
selected="selected" if stage_value == p_id else "",
)
for p_id, p in self.profiles.items()
),
)
def render_static_entires(self, value, errors):
def render_time_inputs(v):
values = ["{}:{}".format(x[0:2], x[2:4]) for x in [v[0:4], v[5:9]]]
return '<div class="p-1">-</div>'.join(
"""
<input type="time" class="{classes}" id="{id}" name="{id}" {disabled} value="{value}">
""".format(
id="{}-{}-{}".format(self.id, "time", "start" if i == 0 else "end"),
classes=self.input_classes(errors),
disabled="disabled" if self.disabled else "",
value=v,
)
for i, v in enumerate(values)
)
schedule = {"0000-0000": ""}
if value is not None and value and "schedule" in value and "type" in value and value["type"] == "static":
schedule = value["schedule"]
rows = "".join(
"""
<div class="row scheduler-static-time-inputs">
{time_inputs}
{select}
<button type="button" class="btn btn-sm btn-danger remove-button">X</button>
</div>
""".format(
time_inputs=render_time_inputs(slot),
select=self.render_profiles_select(value, errors, slot, "profile"),
)
for slot, entry in schedule.items()
)
return """
{rows}
<div class="row scheduler-static-time-inputs template" style="display: none;">
{time_inputs}
{select}
<button type="button" class="btn btn-sm btn-danger remove-button">X</button>
</div>
<div class="row">
<button type="button" class="btn btn-sm btn-primary col-12 add-button">Add...</button>
</div>
""".format(
rows=rows,
time_inputs=render_time_inputs("0000-0000"),
select=self.render_profiles_select("", errors, "0000-0000", "profile"),
)
def render_daylight_entries(self, value, errors):
return "".join(
"""
<div class="row">
<label class="col-form-label col-form-label-sm col-3">{name}</label>
{select}
</div>
""".format(
name=name,
select=self.render_profiles_select(value, errors, stage, stage, extra_classes="col-9"),
)
for stage, name in [("day", "Day"), ("night", "Night"), ("greyline", "Greyline")]
)
def render_input(self, value, errors):
return """
<div id="{id}">
<select class="{classes} mode" id="{id}-select" name="{id}-select" {disabled}>
{options}
</select>
<div class="option static container container-fluid" style="display: none;">
{entries}
</div>
<div class="option daylight container container-fluid" style="display: None;">
{stages}
</div>
</div>
""".format(
id=self.id,
classes=self.input_classes(errors),
disabled="disabled" if self.disabled else "",
options=self.render_options(value),
entries=self.render_static_entires(value, errors),
stages=self.render_daylight_entries(value, errors),
)
def _get_mode(self, value):
if value is not None and "type" in value:
return value["type"]
return ""
def render_options(self, value):
options = [
("static", "Static scheduler"),
("daylight", "Daylight scheduler"),
]
mode = self._get_mode(value)
return "".join(
"""
<option value="{value}" {selected}>{name}</option>
""".format(
value=value, name=name, selected="selected" if mode == value else ""
)
for value, name in options
)
def parse(self, data):
def getStageValue(stage):
input_id = "{id}-{stage}".format(id=self.id, stage=stage)
if input_id in data:
return data[input_id][0]
else:
return None
select_id = "{id}-select".format(id=self.id)
if select_id in data:
if data[select_id][0] == "static":
keys = ["{}-{}".format(self.id, x) for x in ["time-start", "time-end", "profile"]]
keys_present = reduce(and_, [key in data for key in keys], True)
if not keys_present:
return {}
lists = [data[key] for key in keys]
settings_dict = {
"{}{}-{}{}".format(start[0:2], start[3:5], end[0:2], end[3:5]): profile
for start, end, profile in zip(*lists)
}
return {self.id: {"type": "static", "schedule": settings_dict}}
elif data[select_id][0] == "daylight":
settings_dict = {s: getStageValue(s) for s in ["day", "night", "greyline"]}
# filter out empty ones
settings_dict = {s: v for s, v in settings_dict.items() if v}
return {self.id: {"type": "daylight", "schedule": settings_dict}}
return {}
class WaterfallLevelsInput(Input):
def __init__(self, id, label, infotext=None):
super().__init__(id, label, infotext=infotext)
def render_input_group(self, value, errors):
return """
<div class="row {rowclass}" id="{id}">
{input}
</div>
{errors}
""".format(
rowclass="is-invalid" if errors else "",
id=self.id,
input=self.render_input(value, errors),
errors=self.render_errors(errors),
)
def getUnit(self):
return "dBFS"
def getFields(self):
return {"min": "Minimum", "max": "Maximum"}
def render_input(self, value, errors):
return "".join(
"""
<div class="col row">
<label class="col-3 col-form-label col-form-label-sm" for="{id}-{name}">{label}</label>
<div class="col-9 input-group input-group-sm">
<input type="number" step="any" class="{classes}" name="{id}-{name}" value="{value}" {disabled}>
<div class="input-group-append">
<span class="input-group-text">{unit}</span>
</div>
</div>
</div>
""".format(
id=self.id,
name=name,
label=label,
value=value[name] if value and name in value else "0",
classes=self.input_classes(errors),
disabled="disabled" if self.disabled else "",
unit=self.getUnit(),
)
for name, label in self.getFields().items()
)
def parse(self, data):
def getValue(name):
key = "{}-{}".format(self.id, name)
if key in data:
return {name: float(data[key][0])}
raise KeyError("waterfall key not found")
try:
return {self.id: {k: v for name in ["min", "max"] for k, v in getValue(name).items()}}
except KeyError:
return {}
class WaterfallAutoLevelsInput(WaterfallLevelsInput):
def getUnit(self):
return "dB"
def getFields(self):
return {"min": "Lower", "max": "Upper"}

51
owrx/form/input/gfx.py Normal file
View File

@ -0,0 +1,51 @@
from abc import ABCMeta, abstractmethod
from owrx.form.input import Input
from datetime import datetime
class ImageInput(Input, metaclass=ABCMeta):
def render_input(self, value, errors):
# TODO display errors
return """
<div class="imageupload">
<input type="hidden" id="{id}" name="{id}">
<div class="image-container">
<img class="{classes}" src="{url}" alt="{label}"/>
</div>
<button type="button" class="btn btn-primary upload">Upload new image...</button>
<button type="button" class="btn btn-secondary restore">Restore original image</button>
</div>
""".format(
id=self.id, label=self.label, url=self.cachebuster(self.getUrl()), classes=" ".join(self.getImgClasses())
)
def cachebuster(self, url: str):
return "{url}{separator}cb={cachebuster}".format(
url=url,
cachebuster=datetime.now().timestamp(),
separator="&" if "?" in url else "?",
)
@abstractmethod
def getUrl(self) -> str:
pass
@abstractmethod
def getImgClasses(self) -> list:
pass
class AvatarInput(ImageInput):
def getUrl(self) -> str:
return "../static/gfx/openwebrx-avatar.png"
def getImgClasses(self) -> list:
return ["webrx-rx-avatar"]
class TopPhotoInput(ImageInput):
def getUrl(self) -> str:
return "../static/gfx/openwebrx-top-photo.jpg"
def getImgClasses(self) -> list:
return ["webrx-top-photo"]

View File

@ -0,0 +1,10 @@
from owrx.form.input.converter import Converter
class ReceiverKeysConverter(Converter):
def convert_to_form(self, value):
return "" if value is None else "\n".join(value)
def convert_from_form(self, value):
# \r\n or \n? this should work with both.
return [v.strip("\r ") for v in value.split("\n")]

View File

@ -0,0 +1,14 @@
from abc import ABC, abstractmethod
from owrx.form.error import ValidationError
class Validator(ABC):
@abstractmethod
def validate(self, key, value):
pass
class RequiredValidator(Validator):
def validate(self, key, value):
if value is None or value == "":
raise ValidationError(key, "Field is required")

16
owrx/form/input/wfm.py Normal file
View File

@ -0,0 +1,16 @@
from owrx.form.input import DropdownEnum
class WfmTauValues(DropdownEnum):
TAU_50_MICRO = (50e-6, "most regions")
TAU_75_MICRO = (75e-6, "Americas and South Korea")
def __new__(cls, *args, **kwargs):
value, description = args
obj = object.__new__(cls)
obj._value_ = value
obj.description = description
return obj
def __str__(self):
return "{}µs ({})".format(int(self.value * 1e6), self.description)

93
owrx/form/input/wsjt.py Normal file
View File

@ -0,0 +1,93 @@
from owrx.form.input import Input
from owrx.form.input.converter import JsonConverter
from owrx.wsjt import Q65Mode, Q65Interval
from owrx.modes import Modes, WsjtMode
import html
class Q65ModeMatrix(Input):
def checkbox_id(self, mode, interval):
return "{0}-{1}-{2}".format(self.id, mode.value, interval.value)
def render_checkbox(self, mode: Q65Mode, interval: Q65Interval, value, errors):
return """
<div class="{classes}">
<input class="form-check-input" type="checkbox" id="{id}" name="{id}" {checked} {disabled}>
<label class="form-check-label" for="{id}">
{checkboxText}
</label>
</div>
""".format(
classes=self.input_classes(errors),
id=self.checkbox_id(mode, interval),
checked="checked" if "{}{}".format(mode.name, interval.value) in value else "",
checkboxText="Mode {} interval {}s".format(mode.name, interval.value),
disabled="" if interval.is_available(mode) and not self.disabled else "disabled",
)
def render_input_group(self, value, errors):
return """
<div class="matrix q65-matrix">
{checkboxes}
{errors}
</div>
""".format(
checkboxes=self.render_input(value, errors),
errors=self.render_errors(errors),
)
def render_input(self, value, errors):
return "".join(
self.render_checkbox(mode, interval, value, errors) for interval in Q65Interval for mode in Q65Mode
)
def input_classes(self, error):
classes = ["form-check", "form-control-sm"]
if error:
classes.append("is-invalid")
return " ".join(classes)
def parse(self, data):
def in_response(mode, interval):
boxid = self.checkbox_id(mode, interval)
return boxid in data and data[boxid][0] == "on"
return {
self.id: [
"{}{}".format(mode.name, interval.value)
for interval in Q65Interval
for mode in Q65Mode
if in_response(mode, interval)
],
}
class WsjtDecodingDepthsInput(Input):
def defaultConverter(self):
return JsonConverter()
def render_input(self, value, errors):
def render_mode(m):
return """
<option value={mode}>{name}</option>
""".format(
mode=m.modulation,
name=m.name,
)
return """
<input type="hidden" class="{classes}" id="{id}" name="{id}" value="{value}" {disabled}>
<div class="inputs" style="display:none;">
<select class="form-control form-control-sm">{options}</select>
<input class="form-control form-control-sm" type="number" step="1">
</div>
""".format(
id=self.id,
classes=self.input_classes(errors),
value=html.escape(value),
options="".join(render_mode(m) for m in Modes.getAvailableModes() if isinstance(m, WsjtMode)),
disabled="disabled" if self.disabled else ""
)
def input_classes(self, error):
return super().input_classes(error) + " wsjt-decoding-depths"