mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2025-10-27 08:49:27 +00:00
Implement templating to avoid copy/paste HTML code. #7
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,3 @@
|
||||
/.idea
|
||||
/.venv
|
||||
__pycache__
|
||||
*.pyc
|
||||
|
||||
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
10
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
<option name="processLiterals" value="true" />
|
||||
<option name="processComments" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
10
.idea/metaspot.iml
generated
Normal file
10
.idea/metaspot.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.13 virtualenv at ~/code/spothole/.venv" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
7
.idea/misc.xml
generated
Normal file
7
.idea/misc.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.13 (metaspot)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 virtualenv at ~/code/spothole/.venv" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/metaspot.iml" filepath="$PROJECT_DIR$/.idea/metaspot.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -17,7 +17,7 @@ from core.utils import infer_mode_type_from_mode, infer_band_from_freq, infer_co
|
||||
@dataclass
|
||||
class Spot:
|
||||
# Globally unique identifier for the spot
|
||||
guid: str = str(uuid.uuid4())
|
||||
guid: str = None
|
||||
# Callsign of the operator that has been spotted
|
||||
dx_call: str = None
|
||||
# Callsign of the operator that has spotted them
|
||||
@@ -91,6 +91,9 @@ class Spot:
|
||||
|
||||
# Infer missing parameters where possible
|
||||
def infer_missing(self):
|
||||
# Always create a GUID
|
||||
self.guid = str(uuid.uuid4())
|
||||
|
||||
# Clean up DX call if it has an SSID or -# from RBN
|
||||
if self.dx_call and "-" in self.dx_call:
|
||||
self.dx_call = self.dx_call.split("-")[0]
|
||||
|
||||
@@ -5,7 +5,7 @@ from threading import Thread
|
||||
|
||||
import bottle
|
||||
import pytz
|
||||
from bottle import run, response
|
||||
from bottle import run, response, template
|
||||
|
||||
from core.config import MAX_SPOT_AGE
|
||||
from core.constants import BANDS, ALL_MODES, MODE_TYPES, SIGS, SOURCES, CONTINENTS
|
||||
@@ -26,11 +26,15 @@ class WebServer:
|
||||
self.thread.daemon = True
|
||||
self.status = "Starting"
|
||||
|
||||
# Set up routing
|
||||
bottle.get("/api/spots")(self.serve_api_spots)
|
||||
bottle.get("/api/options")(self.serve_api_options)
|
||||
bottle.get("/api/status")(self.serve_api_status)
|
||||
bottle.get("/")(self.serve_index)
|
||||
# Routes for API calls
|
||||
bottle.get("/api/spots")(lambda: self.serve_api(self.get_spot_list_with_filters()))
|
||||
bottle.get("/api/options")(lambda: self.serve_api(self.get_options()))
|
||||
bottle.get("/api/status")(lambda: self.serve_api(self.status_data))
|
||||
# Routes for templated pages
|
||||
bottle.get("/")(lambda: self.serve_template('webpage_home'))
|
||||
bottle.get("/about")(lambda: self.serve_template('webpage_about'))
|
||||
bottle.get("/apidocs")(lambda: self.serve_template('webpage_apidocs'))
|
||||
# Default route to serve from "webassets"
|
||||
bottle.get("/<filepath:path>")(self.serve_static_file)
|
||||
|
||||
# Start the web server
|
||||
@@ -43,51 +47,29 @@ class WebServer:
|
||||
self.status = "Waiting"
|
||||
run(host='localhost', port=self.port)
|
||||
|
||||
# Main spots API
|
||||
def serve_api_spots(self):
|
||||
# Serve a JSON API endpoint
|
||||
def serve_api(self, data):
|
||||
self.last_api_access_time = datetime.now(pytz.UTC)
|
||||
self.status = "OK"
|
||||
spots_json = json.dumps(self.get_spot_list_with_filters(bottle.request.query), default=serialize_everything)
|
||||
response.content_type = 'application/json'
|
||||
return spots_json
|
||||
return json.dumps(data, default=serialize_everything)
|
||||
|
||||
# Options API
|
||||
def serve_api_options(self):
|
||||
self.last_api_access_time = datetime.now(pytz.UTC)
|
||||
self.status = "OK"
|
||||
status_json = json.dumps(self.get_options(), default=serialize_everything)
|
||||
response.content_type = 'application/json'
|
||||
return status_json
|
||||
|
||||
# Server status API
|
||||
def serve_api_status(self):
|
||||
self.last_api_access_time = datetime.now(pytz.UTC)
|
||||
self.status = "OK"
|
||||
status_json = json.dumps(self.status_data, default=serialize_everything)
|
||||
response.content_type = 'application/json'
|
||||
return status_json
|
||||
|
||||
# Serve the home page. This would be accessible as /index.html but we need this workaround to make it available as /
|
||||
def serve_index(self):
|
||||
return self.serve_static_file("")
|
||||
|
||||
# Serve general static files from "webassets" directory, along with some extra workarounds to make URLs such as
|
||||
# "/", "/about" and "/apidocs" work.
|
||||
def serve_static_file(self, filepath):
|
||||
# Serve a templated page
|
||||
def serve_template(self, template_name):
|
||||
self.last_page_access_time = datetime.now(pytz.UTC)
|
||||
self.status = "OK"
|
||||
if filepath == "":
|
||||
return bottle.static_file("index.html", root="webassets")
|
||||
elif filepath == "about":
|
||||
return bottle.static_file("about.html", root="webassets")
|
||||
elif filepath == "apidocs":
|
||||
return bottle.static_file("index.html", root="webassets/apidocs")
|
||||
else:
|
||||
return template(template_name)
|
||||
|
||||
# Serve general static files from "webassets" directory.
|
||||
def serve_static_file(self, filepath):
|
||||
return bottle.static_file(filepath, root="webassets")
|
||||
|
||||
# Utility method to apply filters to the overall spot list and return only a subset. Enables query parameters in
|
||||
# the main "spots" GET call. The "query" parameter should be the result of bottle's request.query, and is a MultiDict
|
||||
def get_spot_list_with_filters(self, query):
|
||||
# the main "spots" GET call.
|
||||
def get_spot_list_with_filters(self):
|
||||
# Get the query (and the right one, with Bottle magic. This is a MultiDict object
|
||||
query = bottle.request.query
|
||||
|
||||
# Create a shallow copy of the spot list, ordered by spot time. We'll then filter it accordingly.
|
||||
# We can filter by spot time and received time with "since" and "received_since", which take a UNIX timestamp
|
||||
# in seconds UTC.
|
||||
|
||||
12
views/webpage_about.tpl
Normal file
12
views/webpage_about.tpl
Normal file
@@ -0,0 +1,12 @@
|
||||
% rebase('webpage_base.tpl')
|
||||
|
||||
<div id="info-container" class="mt-4">
|
||||
<h3>About (S)pothole</h3>
|
||||
<p>(S)pothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data.</p>
|
||||
<p>While there are several other web-based interfaces to DX clusters, and sites that aggregate spots from various outfoor activity programmes for amateur radio, (S)pothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it.</p>
|
||||
<p>The API is deliberately well-defined with an <a href="/apidocs/openapi.yml">OpenAPI specification</a> and auto-generated <a href="/apidocs">API documentation</a>. The API delivers spots in a consistent format regardless of the data source, freeing developers from needing to know how each individual data source presents its data.</p>
|
||||
<p>(S)pothole itself is also open source, Public Domain licenced code that anyone can take and modify. <a href="https://git.ianrenton.com/ian/metaspot/">The source code is here</a>.</p>
|
||||
<p>Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, and Parks 'n' Peaks.</p>
|
||||
<p>The software was written by <a href="https://ianrenton.com">Ian Renton, MØTRT</a>.</p>
|
||||
<p><a href="/">« Back home</a></p>
|
||||
</div>
|
||||
4
views/webpage_apidocs.tpl
Normal file
4
views/webpage_apidocs.tpl
Normal file
@@ -0,0 +1,4 @@
|
||||
% rebase('webpage_base.tpl')
|
||||
|
||||
<redoc spec-url="/apidocs/openapi.yml"></redoc>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script>
|
||||
@@ -21,12 +21,12 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-FKyoEForCGlyvwx9Hj09JcYn3nv7wiPVlz7YYwJrWVcXK/BmnVDxM+D2scQbITxI"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="js/code.js"></script>
|
||||
<script src="/js/code.js"></script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg navbar-light justify-content-between border-bottom" style="background-color: white;">
|
||||
<nav class="navbar navbar-expand-lg navbar-light justify-content-between p-0 border-bottom" style="background-color: white;">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="/img/logo.png" width="192" height="60" alt="Spothole">
|
||||
</a>
|
||||
@@ -38,10 +38,7 @@
|
||||
|
||||
<main>
|
||||
|
||||
<div id="table-container">
|
||||
<p>Latest spots as of XXXX. Updating in XXX seconds...</p>
|
||||
<div id="table-container-inner"></div>
|
||||
</div>
|
||||
{{!base}}
|
||||
|
||||
</main>
|
||||
</div>
|
||||
6
views/webpage_home.tpl
Normal file
6
views/webpage_home.tpl
Normal file
@@ -0,0 +1,6 @@
|
||||
% rebase('webpage_base.tpl')
|
||||
|
||||
<div class="mt-3">
|
||||
<p>Latest spots as of XXXX. Updating in XXX seconds...</p>
|
||||
<div id="table-container"></div>
|
||||
</div>
|
||||
@@ -1,46 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title>(S)pothole</title>
|
||||
|
||||
<link rel="stylesheet" href="css/style.css" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||
integrity="sha384-sRIl4kxILFvY47J16cr9ZwB07vP4J8+LH7qKQnuqkuIAvNWLzeN8tE5YBujZqJLB" crossorigin="anonymous">
|
||||
|
||||
<link rel="icon" type="image/png" href="img/icon-512.png">
|
||||
<link rel="alternate icon" type="image/png" href="img/icon-192.png">
|
||||
<link rel="alternate icon" type="image/png" href="img/icon-32.png">
|
||||
<link rel="alternate icon" type="image/png" href="img/icon-16.png">
|
||||
<link rel="alternate icon" type="image/x-icon" href="img/favicon.ico">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<nav class="navbar navbar-expand-lg navbar-light justify-content-between border-bottom" style="background-color: white;">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="/img/logo.png" width="192" height="60" alt="Spothole">
|
||||
</a>
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item"><a href="/about" class="nav-link">About</a></li>
|
||||
<li class="nav-item"><a href="/apidocs" class="nav-link">API</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
<div id="info-container">
|
||||
<h3>About (S)pothole</h3>
|
||||
<p>(S)pothole is a utility to aggregate "spots" from amateur radio DX clusters and xOTA spotting sites, and provide an open JSON API as well as a website to browse the data.</p>
|
||||
<p>While there are several other web-based interfaces to DX clusters, and sites that aggregate spots from various outfoor activity programmes for amateur radio, (S)pothole differentiates itself by supporting a large number of data sources, and by being "API first" rather than just providing a web front-end. This allows other software to be built on top of it.</p>
|
||||
<p>The API is deliberately well-defined with an <a href="/apidocs/openapi.yml">OpenAPI specification</a> and auto-generated <a href="/apidocs">API documentation</a>. The API delivers spots in a consistent format regardless of the data source, freeing developers from needing to know how each individual data source presents its data.</p>
|
||||
<p>(S)pothole itself is also open source, Public Domain licenced code that anyone can take and modify. <a href="https://git.ianrenton.com/ian/metaspot/">The source code is here</a>.</p>
|
||||
<p>Supported data sources include DX Clusters, the Reverse Beacon Network (RBN), the APRS Internet Service (APRS-IS), POTA, SOTA, WWFF, GMA, WWBOTA, HEMA, and Parks 'n' Peaks.</p>
|
||||
<p>The software was written by <a href="https://ianrenton.com">Ian Renton, MØTRT</a>.</p>
|
||||
<p><a href="/">« Back home</a></p>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,19 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>API Documentation</title>
|
||||
</head>
|
||||
<body> <nav class="navbar navbar-expand-lg navbar-light justify-content-between border-bottom" style="background-color: white;">
|
||||
<a class="navbar-brand" href="/">
|
||||
<img src="/img/logo.png" width="192" height="60" alt="Spothole">
|
||||
</a>
|
||||
<ul class="nav nav-pills">
|
||||
<li class="nav-item"><a href="/about" class="nav-link">About</a></li>
|
||||
<li class="nav-item"><a href="/apidocs" class="nav-link">API</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<redoc spec-url="/apidocs/openapi.yml"></redoc>
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"> </script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -17,7 +17,7 @@ $.getJSON('/api/spots', function(jsonData) {
|
||||
table.find('tbody').append($tr);
|
||||
});
|
||||
|
||||
$('#table-container-inner').html(table);
|
||||
$('#table-container').html(table);
|
||||
});
|
||||
|
||||
function escapeHtml(str) {
|
||||
|
||||
Reference in New Issue
Block a user