Finish web UI for submitting spots. Closes #29

This commit is contained in:
Ian Renton
2025-10-14 19:10:26 +01:00
parent eb424145f6
commit 67a99a6d39
7 changed files with 103 additions and 19 deletions

View File

@@ -30,6 +30,7 @@ class HTTPAlertProvider(AlertProvider):
thread.start() thread.start()
def stop(self): def stop(self):
if self.poll_timer:
self.poll_timer.cancel() self.poll_timer.cancel()
def poll(self): def poll(self):

View File

@@ -96,10 +96,10 @@ class LookupHelper:
try: try:
logging.info("Downloading Clublog cty.xml...") logging.info("Downloading Clublog cty.xml...")
response = self.CLUBLOG_CTY_XML_CACHE.get("https://cdn.clublog.org/cty.php?api=" + self.CLUBLOG_API_KEY, response = self.CLUBLOG_CTY_XML_CACHE.get("https://cdn.clublog.org/cty.php?api=" + self.CLUBLOG_API_KEY,
headers=HTTP_HEADERS).raw headers=HTTP_HEADERS)
with gzip.GzipFile(fileobj=response) as uncompressed: open(self.CLUBLOG_XML_DOWNLOAD_LOCATION + ".gz", 'wb').write(response.content)
with gzip.open(self.CLUBLOG_XML_DOWNLOAD_LOCATION + ".gz", "rb") as uncompressed:
file_content = uncompressed.read() file_content = uncompressed.read()
with open(self.CLUBLOG_XML_DOWNLOAD_LOCATION, "wb") as f: with open(self.CLUBLOG_XML_DOWNLOAD_LOCATION, "wb") as f:
f.write(file_content) f.write(file_content)
f.flush() f.flush()

View File

@@ -96,7 +96,7 @@ class Alert:
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0]) self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_calls[0])
if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id: if self.dx_calls and self.dx_calls[0] and not self.dx_dxcc_id:
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0]) self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_calls[0])
if self.dx_dxcc_id and not self.dx_flag: if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag:
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id] self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id]
# DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from # DX operator details lookup, using QRZ.com. This should be the last resort compared to taking the data from

View File

@@ -124,7 +124,7 @@ class Spot:
self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call) self.dx_itu_zone = lookup_helper.infer_itu_zone_from_callsign(self.dx_call)
if self.dx_call and not self.dx_dxcc_id: if self.dx_call and not self.dx_dxcc_id:
self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call) self.dx_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.dx_call)
if self.dx_dxcc_id and DXCC_FLAGS[self.dx_dxcc_id] and not self.dx_flag: if self.dx_dxcc_id and self.dx_dxcc_id in DXCC_FLAGS and not self.dx_flag:
self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id] self.dx_flag = DXCC_FLAGS[self.dx_dxcc_id]
# Clean up spotter call if it has an SSID or -# from RBN # Clean up spotter call if it has an SSID or -# from RBN
@@ -138,7 +138,7 @@ class Spot:
self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call) self.de_continent = lookup_helper.infer_continent_from_callsign(self.de_call)
if self.de_call and not self.de_dxcc_id: if self.de_call and not self.de_dxcc_id:
self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call) self.de_dxcc_id = lookup_helper.infer_dxcc_id_from_callsign(self.de_call)
if self.de_dxcc_id and not self.de_flag: if self.de_dxcc_id and self.de_dxcc_id in DXCC_FLAGS and not self.de_flag:
self.de_flag = DXCC_FLAGS[self.de_dxcc_id] self.de_flag = DXCC_FLAGS[self.de_dxcc_id]
# Band from frequency # Band from frequency

View File

@@ -30,6 +30,7 @@ class HTTPSpotProvider(SpotProvider):
thread.start() thread.start()
def stop(self): def stop(self):
if self.poll_timer:
self.poll_timer.cancel() self.poll_timer.cancel()
def poll(self): def poll(self):

View File

@@ -1,13 +1,5 @@
% rebase('webpage_base.tpl') % rebase('webpage_base.tpl')
<!-- todo remove on release -->
<div class="mt-3">
<div class="alert alert-warning alert-dismissible fade show" role="alert">
<i class="fa-solid fa-triangle-exclamation"></i> <strong>This is a pre-release version of Spothole!</strong><br/>Several features of the user interface have not been completed yet, and the API could change between now and the time that the software is released. Please do not build anything on the Spothole API yet. You can <a href="https://git.ianrenton.com/ian/spothole/issues" class="alert-link">check the list of outstanding features here</a>. There are contact details on <a href="https://ianrenton.com/" class="alert-link">my website</a> if you need to get in touch. There is no timescale to complete it as it's just a hobby project, but it's open source so feel free to contribute!<br/>QRX QRX DE M0TRT SK :)
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div>
<div id="intro-box" class="mt-3"> <div id="intro-box" class="mt-3">
<div class="alert alert-primary alert-dismissible fade show" role="alert"> <div class="alert alert-primary alert-dismissible fade show" role="alert">
<i class="fa-solid fa-circle-info"></i> <strong>What is Spothole?</strong><br/>Spothole is an aggregator of amateur radio spots from DX clusters and outdoor activity programmes. It's free for anyone to use and includes an API that developers can build other applications on. For more information, check out the <a href="/about" class="alert-link">"About" page</a>. If that sounds like nonsense to you, you can visit <a href="/about#faq" class="alert-link">the FAQ section</a> to learn more. <i class="fa-solid fa-circle-info"></i> <strong>What is Spothole?</strong><br/>Spothole is an aggregator of amateur radio spots from DX clusters and outdoor activity programmes. It's free for anyone to use and includes an API that developers can build other applications on. For more information, check out the <a href="/about" class="alert-link">"About" page</a>. If that sounds like nonsense to you, you can visit <a href="/about#faq" class="alert-link">the FAQ section</a> to learn more.
@@ -22,7 +14,7 @@
</div> </div>
<div class="col-auto"> <div class="col-auto">
<p class="d-inline-flex gap-1"> <p class="d-inline-flex gap-1">
<button id="add-spot-button" type="button" class="btn btn-outline-success" data-bs-toggle="button" onclick="toggleAddSpotPanel();"><i class="fa-solid fa-comment"></i> Add Spot</button> <button id="add-spot-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleAddSpotPanel();"><i class="fa-solid fa-comment"></i> Add Spot</button>
<button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleFiltersPanel();"><i class="fa-solid fa-filter"></i> Filters</button> <button id="filters-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleFiltersPanel();"><i class="fa-solid fa-filter"></i> Filters</button>
<button id="display-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i> Display</button> <button id="display-button" type="button" class="btn btn-outline-primary" data-bs-toggle="button" onclick="toggleDisplayPanel();"><i class="fa-solid fa-desktop"></i> Display</button>
</p> </p>
@@ -193,7 +185,7 @@
</div> </div>
<div id="add-spot-area" class="appearing-panel card mb-3"> <div id="add-spot-area" class="appearing-panel card mb-3">
<div class="card-header text-white bg-success"> <div class="card-header text-white bg-primary">
<div class="row"> <div class="row">
<div class="col-auto me-auto"> <div class="col-auto me-auto">
Add a Spot Add a Spot
@@ -205,7 +197,39 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<p>Coming soon!</p> <form class="row g-2">
<div class="col-auto">
<label for="add-spot-dx-call" class="form-label">DX Call</label>
<input type="text" class="form-control" id="add-spot-dx-call" placeholder="N0CALL" style="max-width: 8em;">
</div>
<div class="col-auto">
<label for="add-spot-freq" class="form-label">Frequency (kHz)</label>
<input type="text" class="form-control" id="add-spot-freq" placeholder="14100" style="max-width: 8em;">
</div>
<div class="col-auto">
<label for="add-spot-mode" class="form-label">Mode</label>
<input type="text" class="form-control" id="add-spot-mode" placeholder="SSB" style="max-width: 6em;">
</div>
<div class="col-auto">
<label for="add-spot-comment" class="form-label">Comment</label>
<input type="text" class="form-control" id="add-spot-comment" placeholder="59 TNX QSO 73" style="max-width: 12em;">
</div>
<div class="col-auto">
<label for="add-spot-de-call" class="form-label">Your Call</label>
<input type="text" class="form-control" id="add-spot-de-call" placeholder="N0CALL" style="max-width: 8em;">
</div>
<div class="col-auto">
<button type="button" class="btn btn-primary" style="margin-top: 2em;" onclick="addSpot();">Spot</button>
<span id="post-spot-result-good"></span>
</div>
</form>
<div id="post-spot-result-bad"></div>
<div class="alert alert-warning alert-dismissible fade show mb-0 mt-4" role="alert">
Please note that spots added to Spothole are not currently sent "upstream" to DX clusters or xOTA spotting sites.
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
</div> </div>
</div> </div>

View File

@@ -337,6 +337,64 @@ function userGridUpdated() {
saveSettings(); saveSettings();
} }
// Method called to add a spot to the server
function addSpot() {
try {
var dx = $("#add-spot-dx-call").val().toUpperCase();
var freqStr = $("#add-spot-freq").val();
var mode = $("#add-spot-mode").val().toUpperCase();
var comment = $("#add-spot-comment").val();
var de = $("#add-spot-de-call").val().toUpperCase();
var spot = {}
if (dx != "") {
spot["dx_call"] = dx;
} else {
showAddSpotError("A DX callsign is required in order to spot.");
return;
}
if (freqStr != "") {
spot["freq"] = parseFloat(freqStr) * 1000;
} else {
showAddSpotError("A frequency is required in order to spot.");
return;
}
if (mode != "") {
spot["mode"] = mode;
}
if (comment != "") {
spot["comment"] = comment;
}
if (de != "") {
spot["de_call"] = de;
}
spot["time"] = moment.utc().valueOf() / 1000.0;
$.ajax("/api/v1/spot", {
data : JSON.stringify(spot),
contentType : 'application/json',
type : 'POST',
timeout: 10000,
success: async function (result) {
$("#post-spot-result-good").html("<button type='button' class='btn btn-success' style='margin-top: 2em;'><i class='fa-solid fa-check'></i> OK</button>");
setTimeout(() => $("#post-spot-result-good").hide(), 2000);
loadSpots();
},
error: function (result) {
showAddSpotError(result.responseText);
}
});
} catch (error) {
showAddSpotError(error);
}
return false;
}
// Show an "add spot" error.
function showAddSpotError(text) {
$("#post-spot-result-bad").html("<div class='alert alert-danger alert-dismissible fade show mb-0 mt-4' role='alert'><i class='fa-solid fa-triangle-exclamation'></i> " + text + "<button type='button' class='btn-close' data-bs-dismiss='alert' aria-label='Close'></button></div>");
}
// React to toggling/closing panels // React to toggling/closing panels
function toggleFiltersPanel() { function toggleFiltersPanel() {
// If we are going to show the filters panel, hide the display and add spot panels // If we are going to show the filters panel, hide the display and add spot panels