Implement multi network support
This commit is contained in:
parent
aaaf26861d
commit
873b21f4d4
6 changed files with 181 additions and 45 deletions
|
|
@ -56,7 +56,7 @@ func runBMDeviceSync(db *sql.DB) {
|
|||
|
||||
// Only poll repeaters that haven't been polled in the last 3 days
|
||||
rows, err := db.Query(`SELECT id, callsign, freq_tx, freq_rx FROM repeaters
|
||||
WHERE network = 'Brandmeister' AND hotspot = 0
|
||||
WHERE networks @> ARRAY['Brandmeister'] AND hotspot = 0
|
||||
AND (last_polled IS NULL OR last_polled < NOW() - INTERVAL '3 days')
|
||||
ORDER BY CASE WHEN LEFT(id::text, 3) = '262' THEN 0 WHEN LEFT(id::text, 1) = '2' THEN 1 ELSE 2 END, callsign`)
|
||||
if err != nil {
|
||||
|
|
|
|||
131
db.go
131
db.go
|
|
@ -2,6 +2,7 @@ package main
|
|||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
|
|
@ -12,6 +13,84 @@ import (
|
|||
_ "github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
// StringArray maps a PostgreSQL TEXT[] column to a Go []string via database/sql.
|
||||
type StringArray []string
|
||||
|
||||
func (a *StringArray) Scan(src interface{}) error {
|
||||
if src == nil {
|
||||
*a = StringArray{}
|
||||
return nil
|
||||
}
|
||||
switch v := src.(type) {
|
||||
case []byte:
|
||||
parsed, err := parsePostgresArray(string(v))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*a = parsed
|
||||
case string:
|
||||
parsed, err := parsePostgresArray(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*a = parsed
|
||||
default:
|
||||
return fmt.Errorf("StringArray.Scan: unsupported type %T", src)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a StringArray) Value() (driver.Value, error) {
|
||||
if a == nil || len(a) == 0 {
|
||||
return "{}", nil
|
||||
}
|
||||
elems := make([]string, len(a))
|
||||
for i, s := range a {
|
||||
escaped := strings.ReplaceAll(s, `\`, `\\`)
|
||||
escaped = strings.ReplaceAll(escaped, `"`, `\"`)
|
||||
elems[i] = `"` + escaped + `"`
|
||||
}
|
||||
return "{" + strings.Join(elems, ",") + "}", nil
|
||||
}
|
||||
|
||||
func parsePostgresArray(s string) ([]string, error) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "{}" || s == "" {
|
||||
return []string{}, nil
|
||||
}
|
||||
if s[0] != '{' || s[len(s)-1] != '}' {
|
||||
return nil, fmt.Errorf("invalid postgres array literal: %q", s)
|
||||
}
|
||||
inner := s[1 : len(s)-1]
|
||||
var result []string
|
||||
var current strings.Builder
|
||||
inQuote := false
|
||||
escaped := false
|
||||
for _, r := range inner {
|
||||
if escaped {
|
||||
current.WriteRune(r)
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if r == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
if r == '"' {
|
||||
inQuote = !inQuote
|
||||
continue
|
||||
}
|
||||
if r == ',' && !inQuote {
|
||||
result = append(result, current.String())
|
||||
current.Reset()
|
||||
continue
|
||||
}
|
||||
current.WriteRune(r)
|
||||
}
|
||||
result = append(result, current.String())
|
||||
return result, nil
|
||||
}
|
||||
|
||||
type Repeater struct {
|
||||
ID int `json:"id"`
|
||||
Callsign string `json:"callsign"`
|
||||
|
|
@ -28,7 +107,7 @@ type Repeater struct {
|
|||
TsLinked string `json:"ts_linked"`
|
||||
Trustee string `json:"trustee"`
|
||||
IpscNetwork string `json:"ipsc_network"`
|
||||
Network string `json:"network"`
|
||||
Networks StringArray `json:"networks"`
|
||||
Hotspot int `json:"hotspot"`
|
||||
Status string `json:"status"`
|
||||
LastSeen *time.Time `json:"last_seen"`
|
||||
|
|
@ -82,7 +161,7 @@ func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band str
|
|||
}
|
||||
|
||||
query := `SELECT id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
|
||||
color_code, ts_linked, trustee, ipsc_network, network, hotspot, status,
|
||||
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status,
|
||||
last_seen, bm_status, bm_status_text, hardware, firmware, pep, agl, website, description,
|
||||
import_freq_inconsistent, last_polled
|
||||
FROM repeaters WHERE lat BETWEEN ` + nextParam() + ` AND ` + nextParam() + ` AND lng BETWEEN ` + nextParam() + ` AND ` + nextParam()
|
||||
|
|
@ -106,25 +185,29 @@ func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band str
|
|||
|
||||
// Network filter: only apply when not all 4 categories are selected
|
||||
if len(networks) > 0 && len(networks) < 4 {
|
||||
var placeholders []string
|
||||
var networkValues []string
|
||||
hasOther := false
|
||||
for _, n := range networks {
|
||||
switch n {
|
||||
case "BM":
|
||||
placeholders = append(placeholders, nextParam())
|
||||
args = append(args, "Brandmeister")
|
||||
networkValues = append(networkValues, "Brandmeister")
|
||||
case "DMR+":
|
||||
placeholders = append(placeholders, nextParam())
|
||||
args = append(args, "DMR+")
|
||||
networkValues = append(networkValues, "DMR+")
|
||||
case "TGIF":
|
||||
placeholders = append(placeholders, nextParam())
|
||||
args = append(args, "TGIF")
|
||||
networkValues = append(networkValues, "TGIF")
|
||||
case "Other":
|
||||
placeholders = append(placeholders, nextParam(), nextParam(), nextParam(), nextParam())
|
||||
args = append(args, "DMR-MARC", "FreeDMR", "Other", "")
|
||||
hasOther = true
|
||||
networkValues = append(networkValues, "DMR-MARC", "FreeDMR", "Other")
|
||||
}
|
||||
}
|
||||
if len(placeholders) > 0 {
|
||||
query += " AND network IN (" + strings.Join(placeholders, ",") + ")"
|
||||
if len(networkValues) > 0 {
|
||||
p := nextParam()
|
||||
if hasOther {
|
||||
query += " AND (networks && " + p + "::text[] OR networks = '{}')"
|
||||
} else {
|
||||
query += " AND networks && " + p + "::text[]"
|
||||
}
|
||||
args = append(args, StringArray(networkValues))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -147,7 +230,7 @@ func queryRepeaters(db *sql.DB, minLat, maxLat, minLng, maxLng float64, band str
|
|||
if err := rows.Scan(&r.ID, &r.Callsign, &r.FreqTx, &r.FreqRx, &r.FreqOffset, &r.Band,
|
||||
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
|
||||
&r.ColorCode, &r.TsLinked, &r.Trustee,
|
||||
&r.IpscNetwork, &r.Network, &r.Hotspot, &r.Status,
|
||||
&r.IpscNetwork, &r.Networks, &r.Hotspot, &r.Status,
|
||||
&r.LastSeen, &r.BmStatus, &r.BmStatusText, &r.Hardware,
|
||||
&r.Firmware, &r.Pep, &r.Agl, &r.Website, &r.Description,
|
||||
&r.ImportFreqInconsistent, &r.LastPolled); err != nil {
|
||||
|
|
@ -268,7 +351,7 @@ func queryAdminRepeaters(db *sql.DB, search string, page, perPage int) ([]Repeat
|
|||
" OR city ILIKE " + p +
|
||||
" OR state ILIKE " + p +
|
||||
" OR country ILIKE " + p +
|
||||
" OR network ILIKE " + p +
|
||||
" OR array_to_string(networks, ',') ILIKE " + p +
|
||||
" OR id::text = " + nextParam()
|
||||
pattern := "%" + search + "%"
|
||||
args = append(args, pattern, search)
|
||||
|
|
@ -282,7 +365,7 @@ func queryAdminRepeaters(db *sql.DB, search string, page, perPage int) ([]Repeat
|
|||
|
||||
offset := (page - 1) * perPage
|
||||
query := `SELECT id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
|
||||
color_code, ts_linked, trustee, ipsc_network, network, hotspot, status,
|
||||
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status,
|
||||
last_seen, bm_status, bm_status_text, hardware, firmware, pep, agl, website, description,
|
||||
import_freq_inconsistent, last_polled
|
||||
FROM repeaters` + where + ` ORDER BY callsign LIMIT ` + nextParam() + ` OFFSET ` + nextParam()
|
||||
|
|
@ -301,7 +384,7 @@ func queryAdminRepeaters(db *sql.DB, search string, page, perPage int) ([]Repeat
|
|||
if err := rows.Scan(&r.ID, &r.Callsign, &r.FreqTx, &r.FreqRx, &r.FreqOffset, &r.Band,
|
||||
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
|
||||
&r.ColorCode, &r.TsLinked, &r.Trustee,
|
||||
&r.IpscNetwork, &r.Network, &r.Hotspot, &r.Status,
|
||||
&r.IpscNetwork, &r.Networks, &r.Hotspot, &r.Status,
|
||||
&r.LastSeen, &r.BmStatus, &r.BmStatusText, &r.Hardware,
|
||||
&r.Firmware, &r.Pep, &r.Agl, &r.Website, &r.Description,
|
||||
&r.ImportFreqInconsistent, &r.LastPolled); err != nil {
|
||||
|
|
@ -373,7 +456,7 @@ func queryRepeaterSearch(db *sql.DB, search string, limit int) ([]Repeater, int,
|
|||
limit = 25
|
||||
}
|
||||
pattern := "%" + search + "%"
|
||||
where := `WHERE callsign ILIKE $1 OR city ILIKE $1 OR state ILIKE $1 OR country ILIKE $1 OR network ILIKE $1 OR id::text = $2`
|
||||
where := `WHERE callsign ILIKE $1 OR city ILIKE $1 OR state ILIKE $1 OR country ILIKE $1 OR array_to_string(networks, ',') ILIKE $1 OR id::text = $2`
|
||||
|
||||
var total int
|
||||
err := db.QueryRow("SELECT COUNT(*) FROM repeaters "+where, pattern, search).Scan(&total)
|
||||
|
|
@ -382,7 +465,7 @@ func queryRepeaterSearch(db *sql.DB, search string, limit int) ([]Repeater, int,
|
|||
}
|
||||
|
||||
rows, err := db.Query(`SELECT id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
|
||||
color_code, ts_linked, trustee, ipsc_network, network, hotspot, status,
|
||||
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status,
|
||||
last_seen, bm_status, bm_status_text, hardware, firmware, pep, agl, website, description,
|
||||
import_freq_inconsistent, last_polled
|
||||
FROM repeaters `+where+` ORDER BY callsign LIMIT $3`, pattern, search, limit)
|
||||
|
|
@ -398,7 +481,7 @@ func queryRepeaterSearch(db *sql.DB, search string, limit int) ([]Repeater, int,
|
|||
if err := rows.Scan(&r.ID, &r.Callsign, &r.FreqTx, &r.FreqRx, &r.FreqOffset, &r.Band,
|
||||
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
|
||||
&r.ColorCode, &r.TsLinked, &r.Trustee,
|
||||
&r.IpscNetwork, &r.Network, &r.Hotspot, &r.Status,
|
||||
&r.IpscNetwork, &r.Networks, &r.Hotspot, &r.Status,
|
||||
&r.LastSeen, &r.BmStatus, &r.BmStatusText, &r.Hardware,
|
||||
&r.Firmware, &r.Pep, &r.Agl, &r.Website, &r.Description,
|
||||
&r.ImportFreqInconsistent, &r.LastPolled); err != nil {
|
||||
|
|
@ -416,14 +499,14 @@ func queryRepeaterSearch(db *sql.DB, search string, limit int) ([]Repeater, int,
|
|||
func queryRepeaterByID(db *sql.DB, id int) (*Repeater, error) {
|
||||
var r Repeater
|
||||
err := db.QueryRow(`SELECT id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
|
||||
color_code, ts_linked, trustee, ipsc_network, network, hotspot, status,
|
||||
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status,
|
||||
last_seen, bm_status, bm_status_text, hardware, firmware, pep, agl, website, description,
|
||||
import_freq_inconsistent, last_polled
|
||||
FROM repeaters WHERE id = $1`, id).Scan(
|
||||
&r.ID, &r.Callsign, &r.FreqTx, &r.FreqRx, &r.FreqOffset, &r.Band,
|
||||
&r.Lat, &r.Lng, &r.City, &r.State, &r.Country,
|
||||
&r.ColorCode, &r.TsLinked, &r.Trustee,
|
||||
&r.IpscNetwork, &r.Network, &r.Hotspot, &r.Status,
|
||||
&r.IpscNetwork, &r.Networks, &r.Hotspot, &r.Status,
|
||||
&r.LastSeen, &r.BmStatus, &r.BmStatusText, &r.Hardware,
|
||||
&r.Firmware, &r.Pep, &r.Agl, &r.Website, &r.Description,
|
||||
&r.ImportFreqInconsistent, &r.LastPolled)
|
||||
|
|
@ -440,14 +523,14 @@ func updateRepeater(db *sql.DB, r Repeater) error {
|
|||
callsign=$2, freq_tx=$3, freq_rx=$4, freq_offset=$5, band=$6,
|
||||
lat=$7, lng=$8, city=$9, state=$10, country=$11,
|
||||
color_code=$12, ts_linked=$13, trustee=$14, ipsc_network=$15,
|
||||
network=$16, hotspot=$17, status=$18,
|
||||
networks=$16, hotspot=$17, status=$18,
|
||||
hardware=$19, firmware=$20, pep=$21, agl=$22,
|
||||
website=$23, description=$24
|
||||
WHERE id=$1`,
|
||||
r.ID, r.Callsign, r.FreqTx, r.FreqRx, r.FreqOffset, r.Band,
|
||||
r.Lat, r.Lng, r.City, r.State, r.Country,
|
||||
r.ColorCode, r.TsLinked, r.Trustee, r.IpscNetwork,
|
||||
r.Network, r.Hotspot, r.Status,
|
||||
r.Networks, r.Hotspot, r.Status,
|
||||
r.Hardware, r.Firmware, r.Pep, r.Agl,
|
||||
r.Website, r.Description)
|
||||
return err
|
||||
|
|
|
|||
13
migrations/003_networks_array.sql
Normal file
13
migrations/003_networks_array.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
-- +goose Up
|
||||
ALTER TABLE repeaters ADD COLUMN networks TEXT[] NOT NULL DEFAULT '{}';
|
||||
UPDATE repeaters SET networks = ARRAY[network] WHERE network != '';
|
||||
DROP INDEX IF EXISTS idx_repeaters_network;
|
||||
ALTER TABLE repeaters DROP COLUMN network;
|
||||
CREATE INDEX idx_repeaters_networks ON repeaters USING GIN (networks);
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE repeaters ADD COLUMN network TEXT NOT NULL DEFAULT '';
|
||||
UPDATE repeaters SET network = networks[1] WHERE array_length(networks, 1) > 0;
|
||||
DROP INDEX IF EXISTS idx_repeaters_networks;
|
||||
ALTER TABLE repeaters DROP COLUMN networks;
|
||||
CREATE INDEX idx_repeaters_network ON repeaters (network);
|
||||
42
seed.go
42
seed.go
|
|
@ -146,7 +146,7 @@ func seedDatabase(db *sql.DB, jsonPath, bmrptrsPath string) error {
|
|||
|
||||
stmt, err := tx.Prepare(`INSERT INTO repeaters
|
||||
(id, callsign, freq_tx, freq_rx, freq_offset, band, lat, lng, city, state, country,
|
||||
color_code, ts_linked, trustee, ipsc_network, network, hotspot, status)
|
||||
color_code, ts_linked, trustee, ipsc_network, networks, hotspot, status)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18)
|
||||
ON CONFLICT (id) DO NOTHING`)
|
||||
if err != nil {
|
||||
|
|
@ -175,18 +175,23 @@ func seedDatabase(db *sql.DB, jsonPath, bmrptrsPath string) error {
|
|||
freqRx = freq + off
|
||||
}
|
||||
|
||||
network := classifyNetwork(r.IpscNetwork)
|
||||
networks := classifyNetworks(r.IpscNetwork)
|
||||
callsignUpper := strings.ToUpper(strings.TrimSpace(r.Callsign))
|
||||
// If found in BrandMeister repeater list, ensure Brandmeister is in networks
|
||||
if len(bmSet) > 0 && bmSet[callsignUpper] && !containsNetwork(networks, "Brandmeister") {
|
||||
networks = append(networks, "Brandmeister")
|
||||
}
|
||||
hotspot := 0
|
||||
if isHotspot(r.Offset, r.City, r.MapInfo, r.Callsign, r.Country, r.Trustee) {
|
||||
hotspot = 1
|
||||
hotspots++
|
||||
} else if network == "Brandmeister" && len(bmSet) > 0 && !bmSet[strings.ToUpper(strings.TrimSpace(r.Callsign))] {
|
||||
} else if containsNetwork(networks, "Brandmeister") && len(bmSet) > 0 && !bmSet[callsignUpper] {
|
||||
hotspot = 1
|
||||
hotspots++
|
||||
}
|
||||
if _, err := stmt.Exec(r.ID, r.Callsign, freq, freqRx, r.Offset, band, lat, lng,
|
||||
r.City, r.State, r.Country, r.ColorCode,
|
||||
r.TsLinked, r.Trustee, r.IpscNetwork, network, hotspot, r.Status); err != nil {
|
||||
r.TsLinked, r.Trustee, r.IpscNetwork, StringArray(networks), hotspot, r.Status); err != nil {
|
||||
log.Printf("Warning: skipping repeater %d (%s): %v", r.ID, r.Callsign, err)
|
||||
skipped++
|
||||
continue
|
||||
|
|
@ -395,32 +400,45 @@ func isPersonalCallsign(callsign, country string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func classifyNetwork(raw string) string {
|
||||
func classifyNetworks(raw string) []string {
|
||||
s := strings.ToLower(strings.TrimSpace(raw))
|
||||
if s == "" || s == "none" || s == "n/a" || s == "?" || s == "-" || s == "no" {
|
||||
return ""
|
||||
return []string{}
|
||||
}
|
||||
var networks []string
|
||||
if strings.Contains(s, "brandm") || s == "bm" || strings.HasPrefix(s, "bm ") ||
|
||||
strings.HasPrefix(s, "bm,") || strings.HasPrefix(s, "bm/") ||
|
||||
strings.Contains(s, "brand m") || strings.Contains(s, "braindm") ||
|
||||
strings.Contains(s, "branme") || strings.Contains(s, "bramm") ||
|
||||
strings.Contains(s, "brewmister") || s == "bmr" {
|
||||
return "Brandmeister"
|
||||
networks = append(networks, "Brandmeister")
|
||||
}
|
||||
if strings.Contains(s, "dmr-plus") || strings.Contains(s, "dmr+") ||
|
||||
strings.Contains(s, "dmrplus") || s == "dmr plus" ||
|
||||
strings.HasPrefix(s, "ipsc2") || s == "ipsc" {
|
||||
return "DMR+"
|
||||
networks = append(networks, "DMR+")
|
||||
}
|
||||
if strings.Contains(s, "dmr-marc") || strings.Contains(s, "dmr marc") ||
|
||||
s == "marc" || strings.Contains(s, "dmrmarc") {
|
||||
return "DMR-MARC"
|
||||
networks = append(networks, "DMR-MARC")
|
||||
}
|
||||
if strings.Contains(s, "tgif") {
|
||||
return "TGIF"
|
||||
networks = append(networks, "TGIF")
|
||||
}
|
||||
if strings.Contains(s, "freedmr") || strings.Contains(s, "free-dmr") || strings.Contains(s, "free dmr") {
|
||||
return "FreeDMR"
|
||||
networks = append(networks, "FreeDMR")
|
||||
}
|
||||
return "Other"
|
||||
if len(networks) == 0 {
|
||||
return []string{"Other"}
|
||||
}
|
||||
return networks
|
||||
}
|
||||
|
||||
func containsNetwork(networks []string, name string) bool {
|
||||
for _, n := range networks {
|
||||
if n == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -431,6 +431,13 @@
|
|||
|
||||
.save-status.success { color: var(--tag-green-text); }
|
||||
.save-status.error { color: var(--danger); }
|
||||
|
||||
.multicheck-group { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.multicheck-label {
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
font-size: 13px; cursor: pointer; color: var(--text);
|
||||
}
|
||||
.multicheck-label input[type="checkbox"] { width: 16px; height: 16px; cursor: pointer; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -631,7 +638,7 @@
|
|||
'<td>' + (r.freq_rx ? r.freq_rx.toFixed(4) : '') + '</td>' +
|
||||
'<td>' + escapeHtml(r.freq_offset) + '</td>' +
|
||||
'<td>' + r.color_code + '</td>' +
|
||||
'<td>' + (r.network ? '<span class="tag tag-gray">' + escapeHtml(r.network) + '</span>' : '') + '</td>' +
|
||||
'<td>' + (r.networks && r.networks.length ? r.networks.map(function(n) { return '<span class="tag tag-gray">' + escapeHtml(n) + '</span>'; }).join(' ') : '') + '</td>' +
|
||||
'<td>' + escapeHtml(r.city) + '</td>' +
|
||||
'<td>' + escapeHtml(r.state) + '</td>' +
|
||||
'<td>' + escapeHtml(r.country) + '</td>' +
|
||||
|
|
@ -681,7 +688,7 @@
|
|||
{ key: "ts_linked", label: "Timeslots Linked", type: "text" },
|
||||
{ key: "trustee", label: "Trustee", type: "text" },
|
||||
{ key: "ipsc_network", label: "IPSC Network", type: "text" },
|
||||
{ key: "network", label: "Network", type: "select", options: [{ v: "", l: "(none)" }, "Brandmeister", "DMR+", "DMR-MARC", "TGIF", "FreeDMR", "Other"] },
|
||||
{ key: "networks", label: "Networks", type: "multicheck", options: ["Brandmeister", "DMR+", "DMR-MARC", "TGIF", "FreeDMR", "Other"] },
|
||||
{ key: "hotspot", label: "Hotspot", type: "select", options: [{ v: 0, l: "No" }, { v: 1, l: "Yes" }] },
|
||||
{ key: "status", label: "Status", type: "text" },
|
||||
{ key: "last_seen", label: "Last Seen", type: "readonly" },
|
||||
|
|
@ -738,6 +745,14 @@
|
|||
html += '<option value="' + escapeHtml(String(optVal)) + '"' + sel + '>' + escapeHtml(String(optLabel)) + '</option>';
|
||||
}
|
||||
html += '</select>';
|
||||
} else if (f.type === "multicheck") {
|
||||
var currentVals = val || [];
|
||||
html += '<div data-key="' + f.key + '" class="multicheck-group">';
|
||||
for (var j = 0; j < f.options.length; j++) {
|
||||
var checked = currentVals.indexOf(f.options[j]) !== -1 ? " checked" : "";
|
||||
html += '<label class="multicheck-label"><input type="checkbox" value="' + escapeHtml(f.options[j]) + '"' + checked + '> ' + escapeHtml(f.options[j]) + '</label>';
|
||||
}
|
||||
html += '</div>';
|
||||
} else if (f.type === "textarea") {
|
||||
html += '<textarea data-key="' + f.key + '" rows="3">' + escapeHtml(String(val)) + '</textarea>';
|
||||
} else {
|
||||
|
|
@ -790,23 +805,30 @@
|
|||
var inputs = detailBody.querySelectorAll("[data-key]");
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
var key = inputs[i].getAttribute("data-key");
|
||||
var val = inputs[i].value;
|
||||
var field = null;
|
||||
for (var j = 0; j < fields.length; j++) {
|
||||
if (fields[j].key === key) { field = fields[j]; break; }
|
||||
}
|
||||
if (!field) continue;
|
||||
|
||||
if (field.type === "number") {
|
||||
if (field.type === "multicheck") {
|
||||
var checkboxes = inputs[i].querySelectorAll('input[type="checkbox"]');
|
||||
var selected = [];
|
||||
for (var k = 0; k < checkboxes.length; k++) {
|
||||
if (checkboxes[k].checked) selected.push(checkboxes[k].value);
|
||||
}
|
||||
data[key] = selected;
|
||||
} else if (field.type === "number") {
|
||||
var val = inputs[i].value;
|
||||
if (field.step && field.step.indexOf(".") !== -1) {
|
||||
data[key] = parseFloat(val) || 0;
|
||||
} else {
|
||||
data[key] = parseInt(val, 10) || 0;
|
||||
}
|
||||
} else if (field.type === "select" && typeof field.options[0] === "object" && typeof field.options[0].v === "number") {
|
||||
data[key] = parseInt(val, 10) || 0;
|
||||
data[key] = parseInt(inputs[i].value, 10) || 0;
|
||||
} else {
|
||||
data[key] = val;
|
||||
data[key] = inputs[i].value;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
|
|
|
|||
|
|
@ -260,10 +260,10 @@
|
|||
if (r.state) loc += ", " + escapeHtml(r.state);
|
||||
if (r.country) loc += ", " + escapeHtml(r.country);
|
||||
html += "<tr><td>Location</td><td>" + loc + "</td></tr>";
|
||||
if (r.network)
|
||||
if (r.networks && r.networks.length)
|
||||
html +=
|
||||
"<tr><td>Network</td><td>" +
|
||||
escapeHtml(r.network) +
|
||||
r.networks.map(escapeHtml).join(", ") +
|
||||
"</td></tr>";
|
||||
if (r.trustee)
|
||||
html +=
|
||||
|
|
|
|||
Loading…
Reference in a new issue