mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-24 16:24:32 +00:00
Compare commits
2
Commits
2.2
..
3.0-breaking
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ce3ca8d29 | ||
|
|
d91fa70655 |
+7
-7
@@ -130,25 +130,25 @@ spot_providers:
|
|||||||
url: "wss://39c3.totawatch.de/api/spot/live"
|
url: "wss://39c3.totawatch.de/api/spot/live"
|
||||||
# For the "XOTA" provider, an activity must be set manually here because xOTA is a generic backend for xOTA
|
# For the "XOTA" provider, an activity must be set manually here because xOTA is a generic backend for xOTA
|
||||||
# programmes and so different URLs potentially provide different programmes.
|
# programmes and so different URLs potentially provide different programmes.
|
||||||
sig: "Toilets"
|
activity: "Toilets"
|
||||||
# For Toilets on the Air, we prefix the activity references (T-01 etc) with some characters that define the
|
# For Toilets on the Air, we prefix the activity references (T-01 etc) with some characters that define the
|
||||||
# conference: C3, EH or HOPE - so we can look up the correct locations in our database, because each conference
|
# conference: C3, EH or HOPE - so we can look up the correct locations in our database, because each conference
|
||||||
# starts from T-01 but refers to a toilet in a different building (or continent!)
|
# starts from T-01 but refers to a toilet in a different building (or continent!)
|
||||||
sig_ref_prefix: "C3"
|
activity_ref_prefix: "C3"
|
||||||
|
|
||||||
- class: "XOTA"
|
- class: "XOTA"
|
||||||
name: "EH23 TOTA"
|
name: "EH23 TOTA"
|
||||||
enabled: false
|
enabled: false
|
||||||
url: "wss://eh23.totawatch.de/api/spot/live"
|
url: "wss://eh23.totawatch.de/api/spot/live"
|
||||||
sig: "Toilets"
|
activity: "Toilets"
|
||||||
sig_ref_prefix: "EH"
|
activity_ref_prefix: "EH"
|
||||||
|
|
||||||
- class: "XOTA"
|
- class: "XOTA"
|
||||||
name: "HOPE26 TOTA"
|
name: "HOPE26 TOTA"
|
||||||
enabled: false
|
enabled: false
|
||||||
url: "wss://hope-26.totawatch.de/api/spot/live"
|
url: "wss://hope-26.totawatch.de/api/spot/live"
|
||||||
sig: "Toilets"
|
activity: "Toilets"
|
||||||
sig_ref_prefix: "HOPE"
|
activity_ref_prefix: "HOPE"
|
||||||
|
|
||||||
|
|
||||||
# Alert providers to use. Same setup as the spot providers list above.
|
# Alert providers to use. Same setup as the spot providers list above.
|
||||||
@@ -218,7 +218,7 @@ static_data_providers:
|
|||||||
|
|
||||||
# Activity reference data providers to use. These allow Spothole to download, for example, the WWFF directory that
|
# Activity reference data providers to use. These allow Spothole to download, for example, the WWFF directory that
|
||||||
# maps WWFF park IDs to their name and location.
|
# maps WWFF park IDs to their name and location.
|
||||||
sig_ref_data_providers:
|
activity_ref_data_providers:
|
||||||
- class: "POTA"
|
- class: "POTA"
|
||||||
enabled: true
|
enabled: true
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def get_activity_ref_info(activity_name, ref_id):
|
|||||||
ref_id = ref_id.replace(" ", "-")
|
ref_id = ref_id.replace(" ", "-")
|
||||||
|
|
||||||
# Prepare the object to be returned
|
# Prepare the object to be returned
|
||||||
activity_ref = ActivityRef(sig=activity_name, id=ref_id)
|
activity_ref = ActivityRef(activity=activity_name, id=ref_id)
|
||||||
|
|
||||||
# We can always get the reference type and the icon from the activity itself
|
# We can always get the reference type and the icon from the activity itself
|
||||||
activity = get_activity_by_name(activity_name)
|
activity = get_activity_by_name(activity_name)
|
||||||
@@ -141,11 +141,11 @@ def get_activity_ref_info(activity_name, ref_id):
|
|||||||
|
|
||||||
def populate_missing_activity_ref_info(activity_ref):
|
def populate_missing_activity_ref_info(activity_ref):
|
||||||
"""Look up details of an activity reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in an
|
"""Look up details of an activity reference (e.g. POTA park) such as name, lat/lon, and grid. Takes in an
|
||||||
activity_ref object which must at minimum have a "sig" and an "id". The rest of the object will be populated
|
activity_ref object which must at minimum have an "activity" and an "id". The rest of the object will be populated
|
||||||
and returned. Any data currently in the object will be kept, only missing data in the object will be populated
|
and returned. Any data currently in the object will be kept, only missing data in the object will be populated
|
||||||
if it can be determined."""
|
if it can be determined."""
|
||||||
|
|
||||||
lookup_data = get_activity_ref_info(activity_ref.sig, activity_ref.id)
|
lookup_data = get_activity_ref_info(activity_ref.activity, activity_ref.id)
|
||||||
|
|
||||||
if lookup_data:
|
if lookup_data:
|
||||||
# Copy new activity ref data into existing object where data was previously missing
|
# Copy new activity ref data into existing object where data was previously missing
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ from core.config import SERVER_OWNER_CALLSIGN
|
|||||||
from data.band import Band
|
from data.band import Band
|
||||||
|
|
||||||
# General software
|
# General software
|
||||||
SOFTWARE_VERSION = "2.2"
|
SOFTWARE_VERSION = "3.0-pre"
|
||||||
|
|
||||||
# HTTP headers used for spot providers that use HTTP
|
# HTTP headers used for spot providers that use HTTP
|
||||||
HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"}
|
HTTP_HEADERS = {"User-Agent": f"Spothole v{SOFTWARE_VERSION} (operated by {SERVER_OWNER_CALLSIGN})"}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class DataProviders:
|
|||||||
self.alert_providers = []
|
self.alert_providers = []
|
||||||
self.solar_condition_providers = []
|
self.solar_condition_providers = []
|
||||||
self.static_data_providers = []
|
self.static_data_providers = []
|
||||||
self.sig_ref_data_providers = []
|
self.activity_ref_data_providers = []
|
||||||
self.callsign_data_providers = []
|
self.callsign_data_providers = []
|
||||||
self._startup_timers = []
|
self._startup_timers = []
|
||||||
|
|
||||||
@@ -28,8 +28,8 @@ class DataProviders:
|
|||||||
self.solar_condition_providers.append(create_provider_from_config("providers.solarconditions", entry))
|
self.solar_condition_providers.append(create_provider_from_config("providers.solarconditions", entry))
|
||||||
for entry in config.get("static_data_providers", []):
|
for entry in config.get("static_data_providers", []):
|
||||||
self.static_data_providers.append(create_provider_from_config("providers.staticdata", entry))
|
self.static_data_providers.append(create_provider_from_config("providers.staticdata", entry))
|
||||||
for entry in config.get("sig_ref_data_providers", []):
|
for entry in config.get("activity_ref_data_providers", []):
|
||||||
self.sig_ref_data_providers.append(create_provider_from_config("providers.activityrefdata", entry))
|
self.activity_ref_data_providers.append(create_provider_from_config("providers.activityrefdata", entry))
|
||||||
for entry in config.get("callsign_data_providers", []):
|
for entry in config.get("callsign_data_providers", []):
|
||||||
self.callsign_data_providers.append(create_provider_from_config("providers.callsigndata", entry))
|
self.callsign_data_providers.append(create_provider_from_config("providers.callsigndata", entry))
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ class DataProviders:
|
|||||||
25.0,
|
25.0,
|
||||||
lambda: self.start_providers(self.solar_condition_providers, "solar condition"),
|
lambda: self.start_providers(self.solar_condition_providers, "solar condition"),
|
||||||
),
|
),
|
||||||
threading.Timer(30.0, lambda: self.start_providers(self.sig_ref_data_providers, "activity ref data")),
|
threading.Timer(30.0, lambda: self.start_providers(self.activity_ref_data_providers, "activity ref data")),
|
||||||
]
|
]
|
||||||
for t in self._startup_timers:
|
for t in self._startup_timers:
|
||||||
t.daemon = True
|
t.daemon = True
|
||||||
@@ -72,7 +72,7 @@ class DataProviders:
|
|||||||
self.spot_providers
|
self.spot_providers
|
||||||
+ self.alert_providers
|
+ self.alert_providers
|
||||||
+ self.solar_condition_providers
|
+ self.solar_condition_providers
|
||||||
+ self.sig_ref_data_providers
|
+ self.activity_ref_data_providers
|
||||||
+ self.static_data_providers
|
+ self.static_data_providers
|
||||||
+ self.callsign_data_providers
|
+ self.callsign_data_providers
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-1
@@ -77,7 +77,7 @@ class LocationSourceForSpot(str, Enum):
|
|||||||
"""Where the location data came from in a spot."""
|
"""Where the location data came from in a spot."""
|
||||||
|
|
||||||
SPOT = "SPOT"
|
SPOT = "SPOT"
|
||||||
SIG_REF_LOOKUP = "SIG REF LOOKUP"
|
ACTIVITY_REF_LOOKUP = "ACTIVITY REF LOOKUP"
|
||||||
GRID = "GRID"
|
GRID = "GRID"
|
||||||
HOME_QTH = "HOME QTH"
|
HOME_QTH = "HOME QTH"
|
||||||
DXCC = "DXCC"
|
DXCC = "DXCC"
|
||||||
|
|||||||
@@ -111,9 +111,9 @@ class StatusReporter:
|
|||||||
}
|
}
|
||||||
for p in DATA_PROVIDERS.static_data_providers
|
for p in DATA_PROVIDERS.static_data_providers
|
||||||
]
|
]
|
||||||
DATA_STORE.status.get()["sig_ref_data_providers"] = [
|
DATA_STORE.status.get()["activity_ref_data_providers"] = [
|
||||||
{
|
{
|
||||||
"sig_name": p.sig_name,
|
"activity_name": p.activity_name,
|
||||||
"enabled": p.enabled,
|
"enabled": p.enabled,
|
||||||
"status": p.status,
|
"status": p.status,
|
||||||
"last_updated": p.last_update_time.replace(tzinfo=pytz.UTC).timestamp()
|
"last_updated": p.last_update_time.replace(tzinfo=pytz.UTC).timestamp()
|
||||||
@@ -121,7 +121,7 @@ class StatusReporter:
|
|||||||
else 0,
|
else 0,
|
||||||
"reference_count": p.reference_count,
|
"reference_count": p.reference_count,
|
||||||
}
|
}
|
||||||
for p in DATA_PROVIDERS.sig_ref_data_providers
|
for p in DATA_PROVIDERS.activity_ref_data_providers
|
||||||
]
|
]
|
||||||
DATA_STORE.status.get()["callsign_data_providers"] = [
|
DATA_STORE.status.get()["callsign_data_providers"] = [
|
||||||
{
|
{
|
||||||
|
|||||||
+43
-43
@@ -5,7 +5,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.CONTEST: Activity(
|
ActivityName.CONTEST: Activity(
|
||||||
name=ActivityName.CONTEST,
|
name=ActivityName.CONTEST,
|
||||||
description="Contest",
|
description="Contest",
|
||||||
sig_type=ActivityType.TRADITIONAL,
|
activity_type=ActivityType.TRADITIONAL,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
# No sensible way to determine *which* contest, but if we set comment_names=["CONTEST"] then at least
|
# No sensible way to determine *which* contest, but if we set comment_names=["CONTEST"] then at least
|
||||||
@@ -17,7 +17,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.DXPEDITION: Activity(
|
ActivityName.DXPEDITION: Activity(
|
||||||
name=ActivityName.DXPEDITION,
|
name=ActivityName.DXPEDITION,
|
||||||
description="Radio expedition to a remote location",
|
description="Radio expedition to a remote location",
|
||||||
sig_type=ActivityType.TRADITIONAL,
|
activity_type=ActivityType.TRADITIONAL,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
comment_names=["DXPEDITION"],
|
comment_names=["DXPEDITION"],
|
||||||
@@ -27,7 +27,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.SATELLITE: Activity(
|
ActivityName.SATELLITE: Activity(
|
||||||
name=ActivityName.SATELLITE,
|
name=ActivityName.SATELLITE,
|
||||||
description="Amateur Radio Satellite",
|
description="Amateur Radio Satellite",
|
||||||
sig_type=ActivityType.TRADITIONAL,
|
activity_type=ActivityType.TRADITIONAL,
|
||||||
# Satellite "references" are the names of the satellites themselves. This is not an exhaustive list, it just
|
# Satellite "references" are the names of the satellites themselves. This is not an exhaustive list, it just
|
||||||
# matches some of the most commonly used amateur radio satellites so they can be picked out of spot comments.
|
# matches some of the most commonly used amateur radio satellites so they can be picked out of spot comments.
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
@@ -41,7 +41,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.EME: Activity(
|
ActivityName.EME: Activity(
|
||||||
name=ActivityName.EME,
|
name=ActivityName.EME,
|
||||||
description="Earth-Moon-Earth (Moonbounce)",
|
description="Earth-Moon-Earth (Moonbounce)",
|
||||||
sig_type=ActivityType.TRADITIONAL,
|
activity_type=ActivityType.TRADITIONAL,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
comment_names=[],
|
comment_names=[],
|
||||||
@@ -50,7 +50,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.AERONAUTICAL_MOBILE: Activity(
|
ActivityName.AERONAUTICAL_MOBILE: Activity(
|
||||||
name=ActivityName.AERONAUTICAL_MOBILE,
|
name=ActivityName.AERONAUTICAL_MOBILE,
|
||||||
description="Aeronautical Mobile",
|
description="Aeronautical Mobile",
|
||||||
sig_type=ActivityType.TRADITIONAL,
|
activity_type=ActivityType.TRADITIONAL,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
# Don't pick /AM out of comments, spot.py will handle picking it out of the callsign
|
# Don't pick /AM out of comments, spot.py will handle picking it out of the callsign
|
||||||
@@ -60,7 +60,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.MARITIME_MOBILE: Activity(
|
ActivityName.MARITIME_MOBILE: Activity(
|
||||||
name=ActivityName.MARITIME_MOBILE,
|
name=ActivityName.MARITIME_MOBILE,
|
||||||
description="Maritime Mobile",
|
description="Maritime Mobile",
|
||||||
sig_type=ActivityType.TRADITIONAL,
|
activity_type=ActivityType.TRADITIONAL,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
# Don't pick /MM out of comments, spot.py will handle picking it out of the callsign
|
# Don't pick /MM out of comments, spot.py will handle picking it out of the callsign
|
||||||
@@ -70,7 +70,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.QRP: Activity(
|
ActivityName.QRP: Activity(
|
||||||
name=ActivityName.QRP,
|
name=ActivityName.QRP,
|
||||||
description="Low power",
|
description="Low power",
|
||||||
sig_type=ActivityType.TRADITIONAL,
|
activity_type=ActivityType.TRADITIONAL,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
comment_names=["QRP"],
|
comment_names=["QRP"],
|
||||||
@@ -80,7 +80,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.POTA: Activity(
|
ActivityName.POTA: Activity(
|
||||||
name=ActivityName.POTA,
|
name=ActivityName.POTA,
|
||||||
description="Parks on the Air",
|
description="Parks on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
@@ -92,7 +92,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.SOTA: Activity(
|
ActivityName.SOTA: Activity(
|
||||||
name=ActivityName.SOTA,
|
name=ActivityName.SOTA,
|
||||||
description="Summits on the Air",
|
description="Summits on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
@@ -104,7 +104,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.WWFF: Activity(
|
ActivityName.WWFF: Activity(
|
||||||
name=ActivityName.WWFF,
|
name=ActivityName.WWFF,
|
||||||
description="World Wide Flora & Fauna",
|
description="World Wide Flora & Fauna",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
@@ -116,7 +116,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.GMA: Activity(
|
ActivityName.GMA: Activity(
|
||||||
name=ActivityName.GMA,
|
name=ActivityName.GMA,
|
||||||
description="Global Mountain Activity",
|
description="Global Mountain Activity",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
@@ -127,7 +127,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.WWBOTA: Activity(
|
ActivityName.WWBOTA: Activity(
|
||||||
name=ActivityName.WWBOTA,
|
name=ActivityName.WWBOTA,
|
||||||
description="Worldwide Bunkers on the Air",
|
description="Worldwide Bunkers on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.BUNKER,
|
ref_type=ActivityRefType.BUNKER,
|
||||||
@@ -138,7 +138,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.HEMA: Activity(
|
ActivityName.HEMA: Activity(
|
||||||
name=ActivityName.HEMA,
|
name=ActivityName.HEMA,
|
||||||
description="HuMPs Excluding Marilyns Award",
|
description="HuMPs Excluding Marilyns Award",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
@@ -150,7 +150,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.IOTA: Activity(
|
ActivityName.IOTA: Activity(
|
||||||
name=ActivityName.IOTA,
|
name=ActivityName.IOTA,
|
||||||
description="Islands on the Air",
|
description="Islands on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.ISLAND,
|
ref_type=ActivityRefType.ISLAND,
|
||||||
@@ -161,7 +161,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.GMA_ISLANDS: Activity(
|
ActivityName.GMA_ISLANDS: Activity(
|
||||||
name=ActivityName.GMA_ISLANDS,
|
name=ActivityName.GMA_ISLANDS,
|
||||||
description="Global Mountain Activity - Islands",
|
description="Global Mountain Activity - Islands",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.ISLAND,
|
ref_type=ActivityRefType.ISLAND,
|
||||||
@@ -172,7 +172,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.ARLHS: Activity(
|
ActivityName.ARLHS: Activity(
|
||||||
name=ActivityName.ARLHS,
|
name=ActivityName.ARLHS,
|
||||||
description="Amateur Radio Lighthouse Society",
|
description="Amateur Radio Lighthouse Society",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
@@ -183,7 +183,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.ILLW: Activity(
|
ActivityName.ILLW: Activity(
|
||||||
name=ActivityName.ILLW,
|
name=ActivityName.ILLW,
|
||||||
description="International Lighthouse & Lightship Weekend",
|
description="International Lighthouse & Lightship Weekend",
|
||||||
sig_type=ActivityType.EVENT,
|
activity_type=ActivityType.EVENT,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
@@ -194,7 +194,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.MOTA: Activity(
|
ActivityName.MOTA: Activity(
|
||||||
name=ActivityName.MOTA,
|
name=ActivityName.MOTA,
|
||||||
description="Mills on the Air",
|
description="Mills on the Air",
|
||||||
sig_type=ActivityType.EVENT,
|
activity_type=ActivityType.EVENT,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.MILL,
|
ref_type=ActivityRefType.MILL,
|
||||||
@@ -205,7 +205,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.SIOTA: Activity(
|
ActivityName.SIOTA: Activity(
|
||||||
name=ActivityName.SIOTA,
|
name=ActivityName.SIOTA,
|
||||||
description="Silos on the Air",
|
description="Silos on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.SILO,
|
ref_type=ActivityRefType.SILO,
|
||||||
@@ -217,7 +217,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.WCA: Activity(
|
ActivityName.WCA: Activity(
|
||||||
name=ActivityName.WCA,
|
name=ActivityName.WCA,
|
||||||
description="World Castles Award",
|
description="World Castles Award",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
@@ -228,7 +228,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.ZLOTA: Activity(
|
ActivityName.ZLOTA: Activity(
|
||||||
name=ActivityName.ZLOTA,
|
name=ActivityName.ZLOTA,
|
||||||
description="New Zealand on the Air",
|
description="New Zealand on the Air",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=None,
|
ref_type=None,
|
||||||
@@ -241,7 +241,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.WOTA: Activity(
|
ActivityName.WOTA: Activity(
|
||||||
name=ActivityName.WOTA,
|
name=ActivityName.WOTA,
|
||||||
description="Wainwrights on the Air",
|
description="Wainwrights on the Air",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
@@ -254,7 +254,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.BOTA: Activity(
|
ActivityName.BOTA: Activity(
|
||||||
name=ActivityName.BOTA,
|
name=ActivityName.BOTA,
|
||||||
description="Beaches on the Air",
|
description="Beaches on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.BEACH,
|
ref_type=ActivityRefType.BEACH,
|
||||||
@@ -265,7 +265,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.KRMNPA: Activity(
|
ActivityName.KRMNPA: Activity(
|
||||||
name=ActivityName.KRMNPA,
|
name=ActivityName.KRMNPA,
|
||||||
description="Keith Roget Memorial National Parks Award",
|
description="Keith Roget Memorial National Parks Award",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
@@ -278,7 +278,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.SANPCPA: Activity(
|
ActivityName.SANPCPA: Activity(
|
||||||
name=ActivityName.SANPCPA,
|
name=ActivityName.SANPCPA,
|
||||||
description="South Australian National Parks and Conservation Parks Award",
|
description="South Australian National Parks and Conservation Parks Award",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
@@ -291,7 +291,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.LLOTA: Activity(
|
ActivityName.LLOTA: Activity(
|
||||||
name=ActivityName.LLOTA,
|
name=ActivityName.LLOTA,
|
||||||
description="Lagos y Lagunas on the Air",
|
description="Lagos y Lagunas on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.LAKE,
|
ref_type=ActivityRefType.LAKE,
|
||||||
@@ -303,7 +303,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.TOWERS: Activity(
|
ActivityName.TOWERS: Activity(
|
||||||
name=ActivityName.TOWERS,
|
name=ActivityName.TOWERS,
|
||||||
description="Towers on the Air",
|
description="Towers on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.TOWER,
|
ref_type=ActivityRefType.TOWER,
|
||||||
@@ -314,7 +314,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.TILES: Activity(
|
ActivityName.TILES: Activity(
|
||||||
name=ActivityName.TILES,
|
name=ActivityName.TILES,
|
||||||
description="Tiles on the Air",
|
description="Tiles on the Air",
|
||||||
sig_type=ActivityType.ADVENTURE,
|
activity_type=ActivityType.ADVENTURE,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.GRID,
|
ref_type=ActivityRefType.GRID,
|
||||||
@@ -325,7 +325,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.RADAR_RALLY: Activity(
|
ActivityName.RADAR_RALLY: Activity(
|
||||||
name=ActivityName.RADAR_RALLY,
|
name=ActivityName.RADAR_RALLY,
|
||||||
description="RaDAR Rally",
|
description="RaDAR Rally",
|
||||||
sig_type=ActivityType.EVENT,
|
activity_type=ActivityType.EVENT,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
comment_names=["RaDAR"],
|
comment_names=["RaDAR"],
|
||||||
@@ -334,7 +334,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.WAB: Activity(
|
ActivityName.WAB: Activity(
|
||||||
name=ActivityName.WAB,
|
name=ActivityName.WAB,
|
||||||
description="Worked All Britain",
|
description="Worked All Britain",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.GRID,
|
ref_type=ActivityRefType.GRID,
|
||||||
@@ -346,7 +346,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.WAI: Activity(
|
ActivityName.WAI: Activity(
|
||||||
name=ActivityName.WAI,
|
name=ActivityName.WAI,
|
||||||
description="Worked All Ireland",
|
description="Worked All Ireland",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.GRID,
|
ref_type=ActivityRefType.GRID,
|
||||||
@@ -358,7 +358,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.DMF: Activity(
|
ActivityName.DMF: Activity(
|
||||||
name=ActivityName.DMF,
|
name=ActivityName.DMF,
|
||||||
description="Diplôme des Moulins de France",
|
description="Diplôme des Moulins de France",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.MILL,
|
ref_type=ActivityRefType.MILL,
|
||||||
@@ -369,7 +369,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.DME: Activity(
|
ActivityName.DME: Activity(
|
||||||
name=ActivityName.DME,
|
name=ActivityName.DME,
|
||||||
description="Diploma Municipios de España",
|
description="Diploma Municipios de España",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.TOWN,
|
ref_type=ActivityRefType.TOWN,
|
||||||
@@ -381,7 +381,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.FEA: Activity(
|
ActivityName.FEA: Activity(
|
||||||
name=ActivityName.FEA,
|
name=ActivityName.FEA,
|
||||||
description="Diploma Faros de España",
|
description="Diploma Faros de España",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
@@ -396,7 +396,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.DMUE: Activity(
|
ActivityName.DMUE: Activity(
|
||||||
name=ActivityName.DMUE,
|
name=ActivityName.DMUE,
|
||||||
description="Diploma Museos de España",
|
description="Diploma Museos de España",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.BUILDING,
|
ref_type=ActivityRefType.BUILDING,
|
||||||
@@ -408,7 +408,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.DMVE: Activity(
|
ActivityName.DMVE: Activity(
|
||||||
name=ActivityName.DMVE,
|
name=ActivityName.DMVE,
|
||||||
description="Diploma Monumentos y Vestigios de España",
|
description="Diploma Monumentos y Vestigios de España",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.BUILDING,
|
ref_type=ActivityRefType.BUILDING,
|
||||||
@@ -420,7 +420,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.DCE: Activity(
|
ActivityName.DCE: Activity(
|
||||||
name=ActivityName.DCE,
|
name=ActivityName.DCE,
|
||||||
description="Diploma Castillos de España",
|
description="Diploma Castillos de España",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
@@ -432,7 +432,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.DEFE: Activity(
|
ActivityName.DEFE: Activity(
|
||||||
name=ActivityName.DEFE,
|
name=ActivityName.DEFE,
|
||||||
description="Diploma Estaciones de Ferrocarril de España",
|
description="Diploma Estaciones de Ferrocarril de España",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.BUILDING,
|
ref_type=ActivityRefType.BUILDING,
|
||||||
@@ -444,7 +444,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.DTMBA: Activity(
|
ActivityName.DTMBA: Activity(
|
||||||
name=ActivityName.DTMBA,
|
name=ActivityName.DTMBA,
|
||||||
description="Diploma Teatri Musei e Belle Arti",
|
description="Diploma Teatri Musei e Belle Arti",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.BUILDING,
|
ref_type=ActivityRefType.BUILDING,
|
||||||
@@ -456,7 +456,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.BIWOTA: Activity(
|
ActivityName.BIWOTA: Activity(
|
||||||
name=ActivityName.BIWOTA,
|
name=ActivityName.BIWOTA,
|
||||||
description="British Inland Waterways on the Air",
|
description="British Inland Waterways on the Air",
|
||||||
sig_type=ActivityType.EVENT,
|
activity_type=ActivityType.EVENT,
|
||||||
has_refs=False,
|
has_refs=False,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.WATERWAY,
|
ref_type=ActivityRefType.WATERWAY,
|
||||||
@@ -467,7 +467,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.COTA: Activity(
|
ActivityName.COTA: Activity(
|
||||||
name=ActivityName.COTA,
|
name=ActivityName.COTA,
|
||||||
description="Castles on the Air",
|
description="Castles on the Air",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
@@ -479,7 +479,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.PGA: Activity(
|
ActivityName.PGA: Activity(
|
||||||
name=ActivityName.PGA,
|
name=ActivityName.PGA,
|
||||||
description="Polish Gmina Award",
|
description="Polish Gmina Award",
|
||||||
sig_type=ActivityType.REGIONAL,
|
activity_type=ActivityType.REGIONAL,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=False,
|
refs_globally_unique=False,
|
||||||
ref_type=ActivityRefType.REGION,
|
ref_type=ActivityRefType.REGION,
|
||||||
@@ -491,7 +491,7 @@ ACTIVITIES: dict[ActivityName, Activity] = {
|
|||||||
ActivityName.TOILETS: Activity(
|
ActivityName.TOILETS: Activity(
|
||||||
name=ActivityName.TOILETS,
|
name=ActivityName.TOILETS,
|
||||||
description="Toilets on the Air",
|
description="Toilets on the Air",
|
||||||
sig_type=ActivityType.EVENT,
|
activity_type=ActivityType.EVENT,
|
||||||
has_refs=True,
|
has_refs=True,
|
||||||
refs_globally_unique=True,
|
refs_globally_unique=True,
|
||||||
ref_type=ActivityRefType.TOILET,
|
ref_type=ActivityRefType.TOILET,
|
||||||
|
|||||||
+8
-10
@@ -5,22 +5,20 @@ from core.enums import ActivityName, ActivityRefType, ActivityType
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Activity:
|
class Activity:
|
||||||
"""Data class that defines an Activity (formerly referred to as a "Special Interest Group" or "SIG", a term
|
"""Data class that defines an Activity. Each contains a name and a longer form description. They also contain
|
||||||
which is still used for the `sig` field name in the API for backwards compatibility). Each contains a name and
|
comment_names which attempts to separate out the way people might refer to it in cluster comments from how it is
|
||||||
a longer form description. They also contain comment_names which attempts to separate out the way people might
|
referred to in the UI & API. (For example, "TOTA" in cluster spot comments almost always means Towers on the Air,
|
||||||
refer to it in cluster comments from how it is referred to in the UI & API. (For example, "TOTA" in cluster
|
but no single programme is referred to in the UI as "TOTA" as it's ambiguous between Towers, Toilets and Tiles.
|
||||||
spot comments almost always means Towers on the Air, but no single programme is referred to in the UI as "TOTA"
|
And while Beaches got the name "BOTA" first, "BOTA" spots are much more likely to be bunkers.) Finally, there is a
|
||||||
as it's ambiguous between Towers, Toilets and Tiles. And while Beaches got the name "BOTA" first, "BOTA" spots
|
ref_regex which provides a regular expression to match what references (such as parks and summits) look like for
|
||||||
are much more likely to be bunkers.) Finally, there is a ref_regex which provides a regular expression to
|
that programme."""
|
||||||
match what references (such as parks and summits) look like for that programme."""
|
|
||||||
|
|
||||||
# Activity name as used in the UI and API, e.g. "Towers"
|
# Activity name as used in the UI and API, e.g. "Towers"
|
||||||
name: ActivityName
|
name: ActivityName
|
||||||
# Description, e.g. "Towers on the Air"
|
# Description, e.g. "Towers on the Air"
|
||||||
description: str
|
description: str
|
||||||
# Type, either Worldwide, Regional or Event. Used for sorting in the web UI.
|
# Type, either Worldwide, Regional or Event. Used for sorting in the web UI.
|
||||||
# Note: this field is still named "sig_type" in the API for backwards compatibility.
|
activity_type: ActivityType
|
||||||
sig_type: ActivityType
|
|
||||||
# Whether this activity has a fixed set of references (e.g. parks) with some sort of ID to "activate"
|
# Whether this activity has a fixed set of references (e.g. parks) with some sort of ID to "activate"
|
||||||
has_refs: bool
|
has_refs: bool
|
||||||
# Identifies that the activity's reference ID structure defined by its regex is unique across all programmes and
|
# Identifies that the activity's reference ID structure defined by its regex is unique across all programmes and
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ class ActivityRef:
|
|||||||
"""Data class that defines an Activity "info" or reference. As well as the basic reference ID we include a
|
"""Data class that defines an Activity "info" or reference. As well as the basic reference ID we include a
|
||||||
name and a lookup URL."""
|
name and a lookup URL."""
|
||||||
|
|
||||||
# Activity that this reference is in, e.g. "POTA". Still named "sig" for backwards compatibility with the API.
|
# Activity that this reference is in, e.g. "POTA".
|
||||||
sig: str
|
activity: str
|
||||||
# Reference ID, e.g. "GB-0001".
|
# Reference ID, e.g. "GB-0001".
|
||||||
id: str | None = None
|
id: str | None = None
|
||||||
# Name of the reference, e.g. "Null Country Park", if known.
|
# Name of the reference, e.g. "Null Country Park", if known.
|
||||||
|
|||||||
+10
-11
@@ -5,7 +5,7 @@ from dataclasses import dataclass, field
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
from pyhamtools.locator import locator_to_latlong, latlong_to_locator
|
from pyhamtools.locator import latlong_to_locator, locator_to_latlong
|
||||||
|
|
||||||
from core.activity_lookup_helper import populate_missing_activity_ref_info
|
from core.activity_lookup_helper import populate_missing_activity_ref_info
|
||||||
from core.activity_utils import get_icon_for_activity
|
from core.activity_utils import get_icon_for_activity
|
||||||
@@ -66,11 +66,10 @@ class Alert:
|
|||||||
|
|
||||||
# Activity info
|
# Activity info
|
||||||
|
|
||||||
# Activity (e.g. outdoor activity programme such as POTA). Still named "sig" for API backwards compatibility.
|
# Activity (e.g. outdoor activity programme such as POTA).
|
||||||
sig: str | None = None
|
activity: str | None = None
|
||||||
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named
|
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
|
||||||
# "sig_refs" for API backwards compatibility.
|
activity_refs: list = field(default_factory=list)
|
||||||
sig_refs: list = field(default_factory=list)
|
|
||||||
|
|
||||||
# Timing info
|
# Timing info
|
||||||
|
|
||||||
@@ -131,8 +130,8 @@ class Alert:
|
|||||||
self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id)
|
self.dx_flag = get_flag_for_dxcc(self.dx_dxcc_id)
|
||||||
|
|
||||||
# Fetch activity data, and set a real position if we can get one.
|
# Fetch activity data, and set a real position if we can get one.
|
||||||
if self.sig_refs:
|
if self.activity_refs:
|
||||||
for activity_ref in self.sig_refs:
|
for activity_ref in self.activity_refs:
|
||||||
activity_ref = populate_missing_activity_ref_info(activity_ref)
|
activity_ref = populate_missing_activity_ref_info(activity_ref)
|
||||||
# If the alert itself doesn't have location yet, but the activity ref does, extract it
|
# If the alert itself doesn't have location yet, but the activity ref does, extract it
|
||||||
if activity_ref.grid and not self.dx_grid:
|
if activity_ref.grid and not self.dx_grid:
|
||||||
@@ -148,8 +147,8 @@ class Alert:
|
|||||||
|
|
||||||
# If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
|
# If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
|
||||||
# reference's activity and apply it to the whole spot.
|
# reference's activity and apply it to the whole spot.
|
||||||
if self.sig_refs and self.sig_refs[0] and not self.sig:
|
if self.activity_refs and self.activity_refs[0] and not self.activity:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.activity = self.activity_refs[0].activity
|
||||||
|
|
||||||
# DX Grid to lat/lon and vice versa in case one is missing
|
# DX Grid to lat/lon and vice versa in case one is missing
|
||||||
if self.dx_grid and (not self.dx_latitude or not self.dx_longitude):
|
if self.dx_grid and (not self.dx_latitude or not self.dx_longitude):
|
||||||
@@ -182,7 +181,7 @@ class Alert:
|
|||||||
|
|
||||||
# Icon for the alert should be the icon of its activity if known, otherwise a radio tower
|
# Icon for the alert should be the icon of its activity if known, otherwise a radio tower
|
||||||
self.icon = "fa-tower-cell"
|
self.icon = "fa-tower-cell"
|
||||||
if self.sig and (activity_icon := get_icon_for_activity(self.sig)):
|
if self.activity and (activity_icon := get_icon_for_activity(self.activity)):
|
||||||
self.icon = activity_icon
|
self.icon = activity_icon
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
+50
-49
@@ -76,8 +76,8 @@ class Spot:
|
|||||||
# DX Location source. Indicates how accurate the location might be.
|
# DX Location source. Indicates how accurate the location might be.
|
||||||
dx_location_source: LocationSourceForSpot | None = None
|
dx_location_source: LocationSourceForSpot | None = None
|
||||||
# DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is
|
# DX Location good. Indicates that the software thinks the location data is good enough to plot on a map. This is
|
||||||
# true if the location source is "SPOT", "SIG REF LOOKUP" or "GRID", or if the location source is "HOME QTH" and
|
# true if the location source is "SPOT", "ACTIVITY REF LOOKUP" or "GRID", or if the location source is "HOME QTH"
|
||||||
# the DX callsign doesn't have a suffix like /P. (Location source retains "SIG" wording for API compatibility.)
|
# and the DX callsign doesn't have a suffix like /P.
|
||||||
dx_location_good: bool = False
|
dx_location_good: bool = False
|
||||||
|
|
||||||
# DE (Spotter) info
|
# DE (Spotter) info
|
||||||
@@ -125,11 +125,10 @@ class Spot:
|
|||||||
|
|
||||||
# Activity info
|
# Activity info
|
||||||
|
|
||||||
# Activity (e.g. outdoor activity programme such as POTA). Still named "sig" for API backwards compatibility.
|
# Activity (e.g. outdoor activity programme such as POTA).
|
||||||
sig: str | None = None
|
activity: str | None = None
|
||||||
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named
|
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
|
||||||
# "sig_refs" for API backwards compatibility.
|
activity_refs: list = field(default_factory=list)
|
||||||
sig_refs: list = field(default_factory=list)
|
|
||||||
|
|
||||||
# Timing info
|
# Timing info
|
||||||
|
|
||||||
@@ -159,12 +158,12 @@ class Spot:
|
|||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
"""Normalise fields that don't survive a plain dict to Spot conversion. This is used in the "add spot" API
|
"""Normalise fields that don't survive a plain dict to Spot conversion. This is used in the "add spot" API
|
||||||
endpoint where the client is submitting JSON, and we want to recreate a full Spot object, including nested
|
endpoint where the client is submitting JSON, and we want to recreate a full Spot object, including nested
|
||||||
objects such as the sig_refs list.."""
|
objects such as the activity_refs list.."""
|
||||||
|
|
||||||
if self.sig_refs:
|
if self.activity_refs:
|
||||||
self.sig_refs = [
|
self.activity_refs = [
|
||||||
activity_ref if isinstance(activity_ref, ActivityRef) else ActivityRef(**activity_ref)
|
activity_ref if isinstance(activity_ref, ActivityRef) else ActivityRef(**activity_ref)
|
||||||
for activity_ref in self.sig_refs
|
for activity_ref in self.activity_refs
|
||||||
]
|
]
|
||||||
|
|
||||||
def infer_missing(self, credentials=None):
|
def infer_missing(self, credentials=None):
|
||||||
@@ -270,18 +269,20 @@ class Spot:
|
|||||||
self.dx_location_source = LocationSourceForSpot.SPOT
|
self.dx_location_source = LocationSourceForSpot.SPOT
|
||||||
|
|
||||||
# Set the top-level activity if it is missing but we have at least one activity ref.
|
# Set the top-level activity if it is missing but we have at least one activity ref.
|
||||||
if not self.sig and self.sig_refs:
|
if not self.activity and self.activity_refs:
|
||||||
self.sig = self.sig_refs[0].sig.upper()
|
self.activity = self.activity_refs[0].activity.upper()
|
||||||
|
|
||||||
# See if we already have an activity reference, but the comment looks like it contains more for the same
|
# See if we already have an activity reference, but the comment looks like it contains more for the same
|
||||||
# activity. This should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
|
# activity. This should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
|
||||||
if self.comment and self.sig_refs and self.sig_refs[0].sig:
|
if self.comment and self.activity_refs and self.activity_refs[0].activity:
|
||||||
activity = self.sig_refs[0].sig.upper()
|
activity = self.activity_refs[0].activity.upper()
|
||||||
regex = get_ref_regex_for_activity(activity)
|
regex = get_ref_regex_for_activity(activity)
|
||||||
if regex:
|
if regex:
|
||||||
all_comment_ref_matches = re.finditer(r"(^|\W)(" + regex + r")($|\W)", self.comment, re.IGNORECASE)
|
all_comment_ref_matches = re.finditer(r"(^|\W)(" + regex + r")($|\W)", self.comment, re.IGNORECASE)
|
||||||
for ref_match in all_comment_ref_matches:
|
for ref_match in all_comment_ref_matches:
|
||||||
self._append_activity_ref_if_missing(ActivityRef(id=ref_match.group(2).upper(), sig=activity))
|
self._append_activity_ref_if_missing(
|
||||||
|
ActivityRef(id=ref_match.group(2).upper(), activity=activity)
|
||||||
|
)
|
||||||
|
|
||||||
# See if the comment looks like it contains any activities (and optionally activity references) that we
|
# See if the comment looks like it contains any activities (and optionally activity references) that we
|
||||||
# can add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g.
|
# can add to the spot. This should catch cluster spot comments like "POTA GB-0001 WWFF GFF-0001" and e.g.
|
||||||
@@ -292,11 +293,11 @@ class Spot:
|
|||||||
# First of all, if we haven't got an activity for this spot set yet, now we have. This covers
|
# First of all, if we haven't got an activity for this spot set yet, now we have. This covers
|
||||||
# things like cluster spots where the comment is just "POTA".
|
# things like cluster spots where the comment is just "POTA".
|
||||||
found_activity = get_activity_name_from_comment_name(activity_match.group(2))
|
found_activity = get_activity_name_from_comment_name(activity_match.group(2))
|
||||||
if not self.sig:
|
if not self.activity:
|
||||||
self.sig = found_activity
|
self.activity = found_activity
|
||||||
|
|
||||||
# Now look to see if that activity name was followed by something that looks like a reference ID
|
# Now look to see if that activity name was followed by something that looks like a reference ID
|
||||||
# for that activity. If so, add that to the sig_refs list for this spot.
|
# for that activity. If so, add that to the activity_refs list for this spot.
|
||||||
found_activity_info = get_activity_by_name(found_activity)
|
found_activity_info = get_activity_by_name(found_activity)
|
||||||
if found_activity_info and found_activity_info.has_refs and found_activity_info.ref_regex:
|
if found_activity_info and found_activity_info.has_refs and found_activity_info.ref_regex:
|
||||||
ref_matches = re.finditer(
|
ref_matches = re.finditer(
|
||||||
@@ -306,7 +307,7 @@ class Spot:
|
|||||||
)
|
)
|
||||||
for ref_match in ref_matches:
|
for ref_match in ref_matches:
|
||||||
self._append_activity_ref_if_missing(
|
self._append_activity_ref_if_missing(
|
||||||
ActivityRef(id=ref_match.group(3).upper(), sig=found_activity)
|
ActivityRef(id=ref_match.group(3).upper(), activity=found_activity)
|
||||||
)
|
)
|
||||||
|
|
||||||
# See if the comment looks like it contains any activity references *without* the corresponding activity
|
# See if the comment looks like it contains any activity references *without* the corresponding activity
|
||||||
@@ -322,17 +323,17 @@ class Spot:
|
|||||||
# First of all, if we haven't got an activity for this spot set yet, now we have. This
|
# First of all, if we haven't got an activity for this spot set yet, now we have. This
|
||||||
# covers things like cluster spots where the comment is just "OHFF-1234", now we know
|
# covers things like cluster spots where the comment is just "OHFF-1234", now we know
|
||||||
# it's WWFF.
|
# it's WWFF.
|
||||||
if not self.sig:
|
if not self.activity:
|
||||||
self.sig = activity.name
|
self.activity = activity.name
|
||||||
self._append_activity_ref_if_missing(
|
self._append_activity_ref_if_missing(
|
||||||
ActivityRef(id=ref_match.group(2).upper(), sig=activity.name)
|
ActivityRef(id=ref_match.group(2).upper(), activity=activity.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a
|
# Fetch activity data. In case a particular API doesn't provide a full set of name, lat, lon & grid for a
|
||||||
# reference in its initial call, we use this code to populate the rest of the data. This includes working
|
# reference in its initial call, we use this code to populate the rest of the data. This includes working
|
||||||
# out grid refs from WAB and WAI, which count as an activity even though there's no real lookup, just maths
|
# out grid refs from WAB and WAI, which count as an activity even though there's no real lookup, just maths
|
||||||
if self.sig_refs:
|
if self.activity_refs:
|
||||||
for activity_ref in self.sig_refs:
|
for activity_ref in self.activity_refs:
|
||||||
activity_ref = populate_missing_activity_ref_info(activity_ref)
|
activity_ref = populate_missing_activity_ref_info(activity_ref)
|
||||||
# If the spot itself doesn't have location yet, but the activity ref does, extract it
|
# If the spot itself doesn't have location yet, but the activity ref does, extract it
|
||||||
if activity_ref.grid and not self.dx_grid:
|
if activity_ref.grid and not self.dx_grid:
|
||||||
@@ -345,15 +346,15 @@ class Spot:
|
|||||||
):
|
):
|
||||||
self.dx_latitude = activity_ref.latitude
|
self.dx_latitude = activity_ref.latitude
|
||||||
self.dx_longitude = activity_ref.longitude
|
self.dx_longitude = activity_ref.longitude
|
||||||
if self.sig in (ActivityName.WAB, ActivityName.WAI, ActivityName.TILES):
|
if self.activity in (ActivityName.WAB, ActivityName.WAI, ActivityName.TILES):
|
||||||
self.dx_location_source = LocationSourceForSpot.GRID
|
self.dx_location_source = LocationSourceForSpot.GRID
|
||||||
else:
|
else:
|
||||||
self.dx_location_source = LocationSourceForSpot.SIG_REF_LOOKUP
|
self.dx_location_source = LocationSourceForSpot.ACTIVITY_REF_LOOKUP
|
||||||
|
|
||||||
# If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
|
# If the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
|
||||||
# reference's activity and apply it to the whole spot.
|
# reference's activity and apply it to the whole spot.
|
||||||
if self.sig_refs and not self.sig:
|
if self.activity_refs and not self.activity:
|
||||||
self.sig = self.sig_refs[0].sig
|
self.activity = self.activity_refs[0].activity
|
||||||
|
|
||||||
# Parse "de_grid<prop_mode>dx_grid" structures from the comment, e.g. "JN61ES(ES)JM56XT" or "JO02GQ<>KN17LG".
|
# Parse "de_grid<prop_mode>dx_grid" structures from the comment, e.g. "JN61ES(ES)JM56XT" or "JO02GQ<>KN17LG".
|
||||||
# These are common on cluster spots and can provide grid references in preference to e.g. QRZ lookup, as well as
|
# These are common on cluster spots and can provide grid references in preference to e.g. QRZ lookup, as well as
|
||||||
@@ -406,32 +407,32 @@ class Spot:
|
|||||||
self.dx_location_source = LocationSourceForSpot.GRID
|
self.dx_location_source = LocationSourceForSpot.GRID
|
||||||
|
|
||||||
# Set activities based on propagation mode
|
# Set activities based on propagation mode
|
||||||
if self.propagation_mode == "Satellite" and not self.sig:
|
if self.propagation_mode == "Satellite" and not self.activity:
|
||||||
self.sig = ActivityName.SATELLITE
|
self.activity = ActivityName.SATELLITE
|
||||||
if self.propagation_mode == "Earth-Moon-Earth" and not self.sig:
|
if self.propagation_mode == "Earth-Moon-Earth" and not self.activity:
|
||||||
self.sig = ActivityName.EME
|
self.activity = ActivityName.EME
|
||||||
|
|
||||||
# Set activities based on the DX callsign suffix
|
# Set activities based on the DX callsign suffix
|
||||||
if self.dx_call and not self.sig:
|
if self.dx_call and not self.activity:
|
||||||
if self.dx_call.upper().endswith(ActivityName.AERONAUTICAL_MOBILE):
|
if self.dx_call.upper().endswith(ActivityName.AERONAUTICAL_MOBILE):
|
||||||
self.sig = ActivityName.AERONAUTICAL_MOBILE
|
self.activity = ActivityName.AERONAUTICAL_MOBILE
|
||||||
elif self.dx_call.upper().endswith(ActivityName.MARITIME_MOBILE):
|
elif self.dx_call.upper().endswith(ActivityName.MARITIME_MOBILE):
|
||||||
self.sig = ActivityName.MARITIME_MOBILE
|
self.activity = ActivityName.MARITIME_MOBILE
|
||||||
|
|
||||||
# Alright, now let's get really fancy. Check if the DX callsign matches one taking part in a currently
|
# Alright, now let's get really fancy. Check if the DX callsign matches one taking part in a currently
|
||||||
# running DXpedition which we know from the alerts list.
|
# running DXpedition which we know from the alerts list.
|
||||||
if self.dx_call and not self.sig:
|
if self.dx_call and not self.activity:
|
||||||
now = datetime.now(pytz.UTC).timestamp()
|
now = datetime.now(pytz.UTC).timestamp()
|
||||||
for alert in DATA_STORE.alerts.values():
|
for alert in DATA_STORE.alerts.values():
|
||||||
if (
|
if (
|
||||||
alert.sig == ActivityName.DXPEDITION
|
alert.activity == ActivityName.DXPEDITION
|
||||||
and alert.dx_calls
|
and alert.dx_calls
|
||||||
and alert.start_time
|
and alert.start_time
|
||||||
and alert.end_time
|
and alert.end_time
|
||||||
and alert.start_time < now < alert.end_time
|
and alert.start_time < now < alert.end_time
|
||||||
and self.dx_call.upper() in [c.upper() for c in alert.dx_calls if c]
|
and self.dx_call.upper() in [c.upper() for c in alert.dx_calls if c]
|
||||||
):
|
):
|
||||||
self.sig = ActivityName.DXPEDITION
|
self.activity = ActivityName.DXPEDITION
|
||||||
break
|
break
|
||||||
|
|
||||||
# DX Grid to lat/lon and vice versa in case one is missing
|
# DX Grid to lat/lon and vice versa in case one is missing
|
||||||
@@ -476,10 +477,10 @@ class Spot:
|
|||||||
|
|
||||||
# Determine a "QTH" string. If we have an activity ref, pick the first one and turn it into a suitable
|
# Determine a "QTH" string. If we have an activity ref, pick the first one and turn it into a suitable
|
||||||
# string, otherwise see what they have set on an online lookup service.
|
# string, otherwise see what they have set on an online lookup service.
|
||||||
if self.sig_refs:
|
if self.activity_refs:
|
||||||
qth = self.sig_refs[0].id
|
qth = self.activity_refs[0].id
|
||||||
if self.sig_refs[0].name:
|
if self.activity_refs[0].name:
|
||||||
qth += f" {self.sig_refs[0].name}"
|
qth += f" {self.activity_refs[0].name}"
|
||||||
self.dx_qth = qth
|
self.dx_qth = qth
|
||||||
else:
|
else:
|
||||||
self.dx_qth = dx_call_info.qth
|
self.dx_qth = dx_call_info.qth
|
||||||
@@ -512,7 +513,7 @@ class Spot:
|
|||||||
and self.dx_longitude
|
and self.dx_longitude
|
||||||
and (
|
and (
|
||||||
self.dx_location_source == LocationSourceForSpot.SPOT
|
self.dx_location_source == LocationSourceForSpot.SPOT
|
||||||
or self.dx_location_source == LocationSourceForSpot.SIG_REF_LOOKUP
|
or self.dx_location_source == LocationSourceForSpot.ACTIVITY_REF_LOOKUP
|
||||||
or self.dx_location_source == LocationSourceForSpot.GRID
|
or self.dx_location_source == LocationSourceForSpot.GRID
|
||||||
or (self.dx_location_source == LocationSourceForSpot.HOME_QTH and "/" not in (self.dx_call or ""))
|
or (self.dx_location_source == LocationSourceForSpot.HOME_QTH and "/" not in (self.dx_call or ""))
|
||||||
)
|
)
|
||||||
@@ -532,7 +533,7 @@ class Spot:
|
|||||||
|
|
||||||
# Icon for the spot should be the icon of its activity if known, otherwise a radio tower
|
# Icon for the spot should be the icon of its activity if known, otherwise a radio tower
|
||||||
self.icon = "fa-tower-cell"
|
self.icon = "fa-tower-cell"
|
||||||
if self.sig and (activity_icon := get_icon_for_activity(self.sig)):
|
if self.activity and (activity_icon := get_icon_for_activity(self.activity)):
|
||||||
self.icon = activity_icon
|
self.icon = activity_icon
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -547,13 +548,13 @@ class Spot:
|
|||||||
"""Append an activity ref to the list, so long as it's not already there."""
|
"""Append an activity ref to the list, so long as it's not already there."""
|
||||||
|
|
||||||
new_activity_ref.id = new_activity_ref.id.strip().upper()
|
new_activity_ref.id = new_activity_ref.id.strip().upper()
|
||||||
new_activity_ref.sig = new_activity_ref.sig.strip().upper()
|
new_activity_ref.activity = new_activity_ref.activity.strip().upper()
|
||||||
if new_activity_ref.id == "":
|
if new_activity_ref.id == "":
|
||||||
return
|
return
|
||||||
for activity_ref in self.sig_refs:
|
for activity_ref in self.activity_refs:
|
||||||
if activity_ref.id == new_activity_ref.id and activity_ref.sig == new_activity_ref.sig:
|
if activity_ref.id == new_activity_ref.id and activity_ref.activity == new_activity_ref.activity:
|
||||||
return
|
return
|
||||||
self.sig_refs.append(new_activity_ref)
|
self.activity_refs.append(new_activity_ref)
|
||||||
|
|
||||||
def expired(self):
|
def expired(self):
|
||||||
"""Decide if this spot has expired (in which case it should not be added to the system in the first place, and not
|
"""Decide if this spot has expired (in which case it should not be added to the system in the first place, and not
|
||||||
|
|||||||
@@ -13,11 +13,10 @@ class ActivityRefDataProvider:
|
|||||||
"""Generic activity reference data provider class. Subclasses of this query the individual URLs or files for
|
"""Generic activity reference data provider class. Subclasses of this query the individual URLs or files for
|
||||||
data."""
|
data."""
|
||||||
|
|
||||||
def __init__(self, sig_name, provider_config):
|
def __init__(self, activity_name, provider_config):
|
||||||
"""Constructor. Note the parameter and attribute are still named "sig_name" for consistency with the API's
|
"""Constructor"""
|
||||||
"sig" field name."""
|
|
||||||
|
|
||||||
self.sig_name = sig_name
|
self.activity_name = activity_name
|
||||||
self.enabled = provider_config["enabled"]
|
self.enabled = provider_config["enabled"]
|
||||||
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
self.last_update_time = datetime.min.replace(tzinfo=pytz.UTC)
|
||||||
self.status = "Not Started" if self.enabled else "Disabled"
|
self.status = "Not Started" if self.enabled else "Disabled"
|
||||||
@@ -43,7 +42,7 @@ class ActivityRefDataProvider:
|
|||||||
# transact()s is to fail if they can't get the lock (?!). This behaviour is fixed by retry=True.
|
# transact()s is to fail if they can't get the lock (?!). This behaviour is fixed by retry=True.
|
||||||
with DATA_STORE.activity_refs.transact(retry=True):
|
with DATA_STORE.activity_refs.transact(retry=True):
|
||||||
for d in new_data:
|
for d in new_data:
|
||||||
DATA_STORE.activity_refs.set(f"{self.sig_name}:{d.id}", d)
|
DATA_STORE.activity_refs.set(f"{self.activity_name}:{d.id}", d)
|
||||||
|
|
||||||
# For the big data sources, loading will take a few minutes. If we want to shut down the software neatly
|
# For the big data sources, loading will take a few minutes. If we want to shut down the software neatly
|
||||||
# within the first few minutes of startup, we need a way to abort this expensive process of filling up the
|
# within the first few minutes of startup, we need a way to abort this expensive process of filling up the
|
||||||
@@ -52,4 +51,4 @@ class ActivityRefDataProvider:
|
|||||||
break
|
break
|
||||||
|
|
||||||
self.reference_count = len(new_data)
|
self.reference_count = len(new_data)
|
||||||
logger.info(f"Loaded {self.reference_count} references for {self.sig_name} into the data store.")
|
logger.info(f"Loaded {self.reference_count} references for {self.activity_name} into the data store.")
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ARLHS(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["ARLHS"]
|
ref_id = row["ARLHS"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=ActivityRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class COTA(FileDownloadActivityRefDataProvider):
|
|||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=name,
|
name=name,
|
||||||
ref_type=ActivityRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
|
|||||||
@@ -27,7 +27,12 @@ class DCE(FileDownloadActivityRefDataProvider):
|
|||||||
for index, row in df.iterrows():
|
for index, row in df.iterrows():
|
||||||
if row.iloc[0] and row.iloc[2]:
|
if row.iloc[0] and row.iloc[2]:
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(sig=self.ACTIVITY, id=row.iloc[0].strip(), name=row.iloc[2].strip(), ref_type=ActivityRefType.CASTLE)
|
ActivityRef(
|
||||||
|
activity=self.ACTIVITY,
|
||||||
|
id=row.iloc[0].strip(),
|
||||||
|
name=row.iloc[2].strip(),
|
||||||
|
ref_type=ActivityRefType.CASTLE,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
|
|||||||
@@ -31,7 +31,12 @@ class DEFE(FileDownloadActivityRefDataProvider):
|
|||||||
|
|
||||||
if row.iloc[0] and row.iloc[1]:
|
if row.iloc[0] and row.iloc[1]:
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(sig=self.ACTIVITY, id=row.iloc[0].strip(), name=row.iloc[1].strip(), ref_type=ActivityRefType.BUILDING)
|
ActivityRef(
|
||||||
|
activity=self.ACTIVITY,
|
||||||
|
id=row.iloc[0].strip(),
|
||||||
|
name=row.iloc[1].strip(),
|
||||||
|
ref_type=ActivityRefType.BUILDING,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class DME(LocalFileActivityRefDataProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
ref = ActivityRef(
|
ref = ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
ref_type=ActivityRefType.TOWN,
|
ref_type=ActivityRefType.TOWN,
|
||||||
name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}",
|
name=f"{row['NOMBRE_ACTUAL']}, {row['PROVINCIA']}",
|
||||||
|
|||||||
@@ -22,7 +22,12 @@ class DMUE(FileDownloadActivityRefDataProvider):
|
|||||||
for row in csv.reader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
|
for row in csv.reader(http_response.content.decode("utf-8-sig").splitlines(), delimiter=";"):
|
||||||
if len(row) > 1 and row[0] and row[1]:
|
if len(row) > 1 and row[0] and row[1]:
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(sig=self.ACTIVITY, id=row[0].strip(), name=row[1].strip(), ref_type=ActivityRefType.BUILDING)
|
ActivityRef(
|
||||||
|
activity=self.ACTIVITY,
|
||||||
|
id=row[0].strip(),
|
||||||
|
name=row[1].strip(),
|
||||||
|
ref_type=ActivityRefType.BUILDING,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
|
|||||||
@@ -36,7 +36,11 @@ class DMVE(FileDownloadActivityRefDataProvider):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
if ref and name:
|
if ref and name:
|
||||||
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref.strip(), name=name.strip(), ref_type=ActivityRefType.BUILDING))
|
new_data.append(
|
||||||
|
ActivityRef(
|
||||||
|
activity=self.ACTIVITY, id=ref.strip(), name=name.strip(), ref_type=ActivityRefType.BUILDING
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
# the data in this case
|
# the data in this case
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ class DTMBA(FileDownloadActivityRefDataProvider):
|
|||||||
split = row.split(";")
|
split = row.split(";")
|
||||||
ref_id = split[0]
|
ref_id = split[0]
|
||||||
ref_name = split[1]
|
ref_name = split[1]
|
||||||
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id, name=ref_name, ref_type=ActivityRefType.BUILDING))
|
new_data.append(
|
||||||
|
ActivityRef(activity=self.ACTIVITY, id=ref_id, name=ref_name, ref_type=ActivityRefType.BUILDING)
|
||||||
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
# the data in this case
|
# the data in this case
|
||||||
|
|||||||
@@ -41,8 +41,16 @@ class FEA(FileDownloadActivityRefDataProvider):
|
|||||||
# prefix and just use FEA-1234 or FEA 1234, so we add both copies to the database.
|
# prefix and just use FEA-1234 or FEA 1234, so we add both copies to the database.
|
||||||
ref_id_1 = row[0].strip()
|
ref_id_1 = row[0].strip()
|
||||||
ref_id_2 = ref_id_1.replace("D-", "FEA-").replace("E-", "FEA-")
|
ref_id_2 = ref_id_1.replace("D-", "FEA-").replace("E-", "FEA-")
|
||||||
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id_1, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE))
|
new_data.append(
|
||||||
new_data.append(ActivityRef(sig=self.ACTIVITY, id=ref_id_2, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE))
|
ActivityRef(
|
||||||
|
activity=self.ACTIVITY, id=ref_id_1, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
new_data.append(
|
||||||
|
ActivityRef(
|
||||||
|
activity=self.ACTIVITY, id=ref_id_2, name=row[1].strip(), ref_type=ActivityRefType.LIGHTHOUSE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
# Bail out if a stop has been requested, i.e. the program is shutting down - no need to parse the rest of
|
||||||
# the data in this case
|
# the data in this case
|
||||||
|
|||||||
@@ -16,19 +16,21 @@ class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
|
|||||||
"""Generic activity ref data provider class for providers that fetch their data from the web by downloading a
|
"""Generic activity ref data provider class for providers that fetch their data from the web by downloading a
|
||||||
file."""
|
file."""
|
||||||
|
|
||||||
def __init__(self, sig_name, provider_config, url, poll_interval):
|
def __init__(self, activity_name, provider_config, url, poll_interval):
|
||||||
"""Set up the provider, note poll_interval is in *days*."""
|
"""Set up the provider, note poll_interval is in *days*."""
|
||||||
super().__init__(sig_name, provider_config)
|
super().__init__(activity_name, provider_config)
|
||||||
self._url = url
|
self._url = url
|
||||||
self._poll_interval = poll_interval
|
self._poll_interval = poll_interval
|
||||||
self._thread = None
|
self._thread = None
|
||||||
self._url_data_cache = URLDataCache(f"activity_ref_data_{sig_name}")
|
self._url_data_cache = URLDataCache(f"activity_ref_data_{activity_name}")
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
|
# Fire off the polling thread. It will poll immediately on startup, then sleep for poll_interval between
|
||||||
# subsequent polls, so start() returns immediately and the application can continue starting.
|
# subsequent polls, so start() returns immediately and the application can continue starting.
|
||||||
logger.info(f"Set up query of {self.sig_name} activity ref data every {self._poll_interval!s} days.")
|
logger.info(f"Set up query of {self.activity_name} activity ref data every {self._poll_interval!s} days.")
|
||||||
self._thread = Thread(target=self._run, name=f"FileDownloadActivityRefDataProvider-{self.sig_name}", daemon=True)
|
self._thread = Thread(
|
||||||
|
target=self._run, name=f"FileDownloadActivityRefDataProvider-{self.activity_name}", daemon=True
|
||||||
|
)
|
||||||
self._thread.start()
|
self._thread.start()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
@@ -36,7 +38,9 @@ class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
|
|||||||
if self._thread:
|
if self._thread:
|
||||||
self._thread.join(timeout=12)
|
self._thread.join(timeout=12)
|
||||||
if self._thread.is_alive():
|
if self._thread.is_alive():
|
||||||
logger.warning(f"{self.sig_name} activity ref data worker thread did not exit on time and will be killed.")
|
logger.warning(
|
||||||
|
f"{self.activity_name} activity ref data worker thread did not exit on time and will be killed."
|
||||||
|
)
|
||||||
|
|
||||||
def _run(self):
|
def _run(self):
|
||||||
while True:
|
while True:
|
||||||
@@ -48,7 +52,7 @@ class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
|
|||||||
try:
|
try:
|
||||||
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
|
# Request data from API. Use the data cache (with a TTL of 1 day) here, not as the main mechanism for
|
||||||
# caching, but just so continual restarts of the software during testing don't hammer the servers.
|
# caching, but just so continual restarts of the software during testing don't hammer the servers.
|
||||||
logger.debug(f"Downloading {self.sig_name} activity ref data...")
|
logger.debug(f"Downloading {self.activity_name} activity ref data...")
|
||||||
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
|
http_response = self._url_data_cache.get(self._url, headers=HTTP_HEADERS)
|
||||||
# Check response code was good
|
# Check response code was good
|
||||||
if http_response.ok:
|
if http_response.ok:
|
||||||
@@ -60,20 +64,22 @@ class FileDownloadActivityRefDataProvider(ActivityRefDataProvider):
|
|||||||
|
|
||||||
self.status = "OK"
|
self.status = "OK"
|
||||||
self.last_update_time = datetime.now(pytz.UTC)
|
self.last_update_time = datetime.now(pytz.UTC)
|
||||||
logger.debug(f"Received activity ref data for {self.sig_name}")
|
logger.debug(f"Received activity ref data for {self.activity_name}")
|
||||||
else:
|
else:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.warning(f"HTTP {http_response.status_code} when downloading activity ref data for {self.sig_name}.")
|
logger.warning(
|
||||||
|
f"HTTP {http_response.status_code} when downloading activity ref data for {self.activity_name}."
|
||||||
|
)
|
||||||
|
|
||||||
except ConnectionError:
|
except ConnectionError:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.warning(f"Connection error when downloading activity ref data for {self.sig_name}.")
|
logger.warning(f"Connection error when downloading activity ref data for {self.activity_name}.")
|
||||||
except (ConnectTimeout, ReadTimeout):
|
except (ConnectTimeout, ReadTimeout):
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.warning(f"Timeout when downloading activity ref data for {self.sig_name}.")
|
logger.warning(f"Timeout when downloading activity ref data for {self.activity_name}.")
|
||||||
except Exception:
|
except Exception:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.exception(f"Exception in HTTP Activity Ref Data Provider ({self.sig_name})")
|
logger.exception(f"Exception in HTTP Activity Ref Data Provider ({self.activity_name})")
|
||||||
self._stop_event.wait(timeout=1)
|
self._stop_event.wait(timeout=1)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class GMA(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["Reference"]
|
ref_id = row["Reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=ActivityRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class ILLW(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["ILLW"]
|
ref_id = row["ILLW"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=ActivityRefType.LIGHTHOUSE,
|
ref_type=ActivityRefType.LIGHTHOUSE,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class IOTA(FileDownloadActivityRefDataProvider):
|
|||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=ref["name"],
|
name=ref["name"],
|
||||||
ref_type=ActivityRefType.ISLAND,
|
ref_type=ActivityRefType.ISLAND,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class LLOTA(FileDownloadActivityRefDataProvider):
|
|||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=str(ref["name"]),
|
name=str(ref["name"]),
|
||||||
ref_type=ActivityRefType.LAKE,
|
ref_type=ActivityRefType.LAKE,
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ logger = logging.getLogger(__name__)
|
|||||||
class LocalFileActivityRefDataProvider(ActivityRefDataProvider):
|
class LocalFileActivityRefDataProvider(ActivityRefDataProvider):
|
||||||
"""Generic activity ref data provider class for providers that fetch their data from a local file on startup."""
|
"""Generic activity ref data provider class for providers that fetch their data from a local file on startup."""
|
||||||
|
|
||||||
def __init__(self, sig_name, provider_config, path):
|
def __init__(self, activity_name, provider_config, path):
|
||||||
super().__init__(sig_name, provider_config)
|
super().__init__(activity_name, provider_config)
|
||||||
self._path = path
|
self._path = path
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
logger.debug(f"Loading {self.sig_name} activity ref data from file.")
|
logger.debug(f"Loading {self.activity_name} activity ref data from file.")
|
||||||
try:
|
try:
|
||||||
new_data = self._file_to_data(self._path)
|
new_data = self._file_to_data(self._path)
|
||||||
if new_data:
|
if new_data:
|
||||||
@@ -25,10 +25,10 @@ class LocalFileActivityRefDataProvider(ActivityRefDataProvider):
|
|||||||
self.last_update_time = datetime.now(pytz.UTC)
|
self.last_update_time = datetime.now(pytz.UTC)
|
||||||
else:
|
else:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.info(f"Failed to load activity ref data for {self.sig_name}")
|
logger.info(f"Failed to load activity ref data for {self.activity_name}")
|
||||||
except Exception:
|
except Exception:
|
||||||
self.status = "Error"
|
self.status = "Error"
|
||||||
logger.exception(f"Exception in local file Activity Ref Data Provider ({self.sig_name})")
|
logger.exception(f"Exception in local file Activity Ref Data Provider ({self.activity_name})")
|
||||||
|
|
||||||
def _file_to_data(self, path):
|
def _file_to_data(self, path):
|
||||||
"""Load a file on the given path and turn it into activity ref data."""
|
"""Load a file on the given path and turn it into activity ref data."""
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class MOTA(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["Reference"]
|
ref_id = row["Reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=ActivityRefType.MILL,
|
ref_type=ActivityRefType.MILL,
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class PGA(FileDownloadActivityRefDataProvider):
|
|||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=name,
|
name=name,
|
||||||
ref_type=ActivityRefType.REGION,
|
ref_type=ActivityRefType.REGION,
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ class ParksNPeaksKMLActivityRefDataProvider(FileDownloadActivityRefDataProvider)
|
|||||||
|
|
||||||
REF_PATTERN = re.compile(r"VKFF-\d+")
|
REF_PATTERN = re.compile(r"VKFF-\d+")
|
||||||
|
|
||||||
def __init__(self, sig_name, provider_config, url, poll_interval):
|
def __init__(self, activity_name, provider_config, url, poll_interval):
|
||||||
"""Set up the provider, note poll_interval is in *days*."""
|
"""Set up the provider, note poll_interval is in *days*."""
|
||||||
super().__init__(sig_name, provider_config, url, poll_interval)
|
super().__init__(activity_name, provider_config, url, poll_interval)
|
||||||
|
|
||||||
def _http_response_to_data(self, http_response):
|
def _http_response_to_data(self, http_response):
|
||||||
new_data = []
|
new_data = []
|
||||||
@@ -41,7 +41,7 @@ class ParksNPeaksKMLActivityRefDataProvider(FileDownloadActivityRefDataProvider)
|
|||||||
longitude, latitude = placemark.geometry.x, placemark.geometry.y
|
longitude, latitude = placemark.geometry.x, placemark.geometry.y
|
||||||
|
|
||||||
ref = ActivityRef(
|
ref = ActivityRef(
|
||||||
sig=self.sig_name,
|
activity=self.activity_name,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=placemark.name,
|
name=placemark.name,
|
||||||
ref_type=ActivityRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class POTA(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["reference"]
|
ref_id = row["reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("name", None),
|
name=row.get("name", None),
|
||||||
ref_type=ActivityRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class SIOTA(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["SILO_CODE"]
|
ref_id = row["SILO_CODE"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("NAME", None),
|
name=row.get("NAME", None),
|
||||||
ref_type=ActivityRefType.SILO,
|
ref_type=ActivityRefType.SILO,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ class SOTA(FileDownloadActivityRefDataProvider):
|
|||||||
longitude = float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None
|
longitude = float(row["Longitude"]) if "Longitude" in row and row["Longitude"] != "" else None
|
||||||
altitude = float(row["AltM"]) if "AltM" in row and row["AltM"] != "" else None
|
altitude = float(row["AltM"]) if "AltM" in row and row["AltM"] != "" else None
|
||||||
ref = ActivityRef(
|
ref = ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("SummitName", None),
|
name=row.get("SummitName", None),
|
||||||
ref_type=ActivityRefType.SUMMIT,
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class Toilets(LocalFileActivityRefDataProvider):
|
|||||||
for row in dr:
|
for row in dr:
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=row["ref"],
|
id=row["ref"],
|
||||||
name=row["ref"],
|
name=row["ref"],
|
||||||
ref_type=ActivityRefType.TOILET,
|
ref_type=ActivityRefType.TOILET,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class Towers(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["Ref"]
|
ref_id = row["Ref"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Nazev", None),
|
name=row.get("Nazev", None),
|
||||||
ref_type=ActivityRefType.TOWER,
|
ref_type=ActivityRefType.TOWER,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class WCA(FileDownloadActivityRefDataProvider):
|
|||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("CLEAN NAME", None),
|
name=row.get("CLEAN NAME", None),
|
||||||
ref_type=ActivityRefType.CASTLE,
|
ref_type=ActivityRefType.CASTLE,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ class WOTA(FileDownloadActivityRefDataProvider):
|
|||||||
|
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=feature["properties"]["title"],
|
name=feature["properties"]["title"],
|
||||||
url=url,
|
url=url,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class WWBOTA(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["Reference"]
|
ref_id = row["Reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("Name", None),
|
name=row.get("Name", None),
|
||||||
ref_type=ActivityRefType.BUNKER,
|
ref_type=ActivityRefType.BUNKER,
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class WWFF(FileDownloadActivityRefDataProvider):
|
|||||||
ref_id = row["reference"]
|
ref_id = row["reference"]
|
||||||
new_data.append(
|
new_data.append(
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=row.get("name", None),
|
name=row.get("name", None),
|
||||||
ref_type=ActivityRefType.PARK,
|
ref_type=ActivityRefType.PARK,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class ZLOTA(FileDownloadActivityRefDataProvider):
|
|||||||
ref_type = None
|
ref_type = None
|
||||||
|
|
||||||
new_ref = ActivityRef(
|
new_ref = ActivityRef(
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
name=ref["name"],
|
name=ref["name"],
|
||||||
ref_type=ref_type,
|
ref_type=ref_type,
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ class BOTA(HTTPAlertProvider):
|
|||||||
alert = Alert(
|
alert = Alert(
|
||||||
source=self.name,
|
source=self.name,
|
||||||
dx_calls=[dx_call],
|
dx_calls=[dx_call],
|
||||||
sig=ActivityName.BOTA,
|
activity=ActivityName.BOTA,
|
||||||
sig_refs=[ActivityRef(id=ref_name, sig=ActivityName.BOTA)],
|
activity_refs=[ActivityRef(id=ref_name, activity=ActivityName.BOTA)],
|
||||||
start_time=date_time.timestamp(),
|
start_time=date_time.timestamp(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ class Hamsat(HTTPAlertProvider):
|
|||||||
dx_grid=source_alert["grids"][0],
|
dx_grid=source_alert["grids"][0],
|
||||||
freqs_modes=freqs_modes,
|
freqs_modes=freqs_modes,
|
||||||
comment=source_alert["comment"],
|
comment=source_alert["comment"],
|
||||||
sig=ActivityName.SATELLITE,
|
activity=ActivityName.SATELLITE,
|
||||||
# Fudge an activity ref to provide the remaining bits of data we need: the satellite and the operator's grid
|
# Fudge an activity ref to provide the remaining bits of data we need: the satellite and the operator's grid
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
sig=ActivityName.SATELLITE,
|
activity=ActivityName.SATELLITE,
|
||||||
id=source_alert["satellite"]["name"],
|
id=source_alert["satellite"]["name"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ class NG3K(HTTPAlertProvider):
|
|||||||
comment=f"{by}; {comment}; {qsl_info}",
|
comment=f"{by}; {comment}; {qsl_info}",
|
||||||
start_time=start_timestamp,
|
start_time=start_timestamp,
|
||||||
end_time=end_timestamp,
|
end_time=end_timestamp,
|
||||||
sig=ActivityName.DXPEDITION,
|
activity=ActivityName.DXPEDITION,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to our list.
|
# Add to our list.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class ParksNPeaks(HTTPAlertProvider):
|
|||||||
datetime.strptime(source_alert["alTime"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp()
|
datetime.strptime(source_alert["alTime"], "%Y-%m-%d %H:%M:%S").replace(tzinfo=pytz.UTC).timestamp()
|
||||||
)
|
)
|
||||||
|
|
||||||
activity_refs = [ActivityRef(id=ref_id, sig=activity, name=ref_name)]
|
activity_refs = [ActivityRef(id=ref_id, activity=activity, name=ref_name)]
|
||||||
|
|
||||||
# Convert to our alert format
|
# Convert to our alert format
|
||||||
alert = Alert(
|
alert = Alert(
|
||||||
@@ -46,8 +46,8 @@ class ParksNPeaks(HTTPAlertProvider):
|
|||||||
dx_calls=[source_alert["CallSign"].upper()],
|
dx_calls=[source_alert["CallSign"].upper()],
|
||||||
freqs_modes=f"{source_alert['Freq']} {source_alert['MODE']}",
|
freqs_modes=f"{source_alert['Freq']} {source_alert['MODE']}",
|
||||||
comment=source_alert["Comments"],
|
comment=source_alert["Comments"],
|
||||||
sig=activity,
|
activity=activity,
|
||||||
sig_refs=activity_refs,
|
activity_refs=activity_refs,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ class POTA(HTTPAlertProvider):
|
|||||||
dx_calls=[source_alert["activator"].upper()],
|
dx_calls=[source_alert["activator"].upper()],
|
||||||
freqs_modes=source_alert["frequencies"],
|
freqs_modes=source_alert["frequencies"],
|
||||||
comment=source_alert["comments"],
|
comment=source_alert["comments"],
|
||||||
sig=ActivityName.POTA,
|
activity=ActivityName.POTA,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_alert["reference"],
|
id=source_alert["reference"],
|
||||||
sig=ActivityName.POTA,
|
activity=ActivityName.POTA,
|
||||||
name=source_alert["name"],
|
name=source_alert["name"],
|
||||||
url=f"https://pota.app/#/park/{source_alert['reference']}",
|
url=f"https://pota.app/#/park/{source_alert['reference']}",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ class RSGBICALAlertProvider(ICALAlertProvider):
|
|||||||
comment=summary,
|
comment=summary,
|
||||||
start_time=start_timestamp,
|
start_time=start_timestamp,
|
||||||
end_time=end_timestamp,
|
end_time=end_timestamp,
|
||||||
sig=ActivityName.CONTEST,
|
activity=ActivityName.CONTEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
return alert
|
return alert
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ class SOTA(HTTPAlertProvider):
|
|||||||
dx_names=[source_alert["activatorName"].upper()],
|
dx_names=[source_alert["activatorName"].upper()],
|
||||||
freqs_modes=source_alert["frequency"],
|
freqs_modes=source_alert["frequency"],
|
||||||
comment=source_alert["comments"],
|
comment=source_alert["comments"],
|
||||||
sig=ActivityName.SOTA,
|
activity=ActivityName.SOTA,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=f"{source_alert['associationCode']}/{source_alert['summitCode']}",
|
id=f"{source_alert['associationCode']}/{source_alert['summitCode']}",
|
||||||
sig=ActivityName.SOTA,
|
activity=ActivityName.SOTA,
|
||||||
name=summit_name,
|
name=summit_name,
|
||||||
activation_score=summit_points,
|
activation_score=summit_points,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ class WA7BNM(ICALAlertProvider):
|
|||||||
url=url,
|
url=url,
|
||||||
start_time=start_timestamp,
|
start_time=start_timestamp,
|
||||||
end_time=end_timestamp,
|
end_time=end_timestamp,
|
||||||
sig=ActivityName.CONTEST,
|
activity=ActivityName.CONTEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
return alert
|
return alert
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ class WOTA(HTTPAlertProvider):
|
|||||||
dx_calls=[dx_call],
|
dx_calls=[dx_call],
|
||||||
freqs_modes=freqs_modes,
|
freqs_modes=freqs_modes,
|
||||||
comment=comment,
|
comment=comment,
|
||||||
sig_refs=[ActivityRef(id=ref, sig=ActivityName.WOTA, name=ref_name)] if ref else [],
|
activity_refs=[ActivityRef(id=ref, activity=ActivityName.WOTA, name=ref_name)] if ref else [],
|
||||||
start_time=time.timestamp(),
|
start_time=time.timestamp(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ class WWFF(HTTPAlertProvider):
|
|||||||
dx_calls=[source_alert["activator_call"].upper()],
|
dx_calls=[source_alert["activator_call"].upper()],
|
||||||
freqs_modes=f"{source_alert['band']} {source_alert['mode']}",
|
freqs_modes=f"{source_alert['band']} {source_alert['mode']}",
|
||||||
comment=source_alert["remarks"],
|
comment=source_alert["remarks"],
|
||||||
sig=ActivityName.WWFF,
|
activity=ActivityName.WWFF,
|
||||||
sig_refs=[ActivityRef(id=source_alert["reference"], sig=ActivityName.WWFF)],
|
activity_refs=[ActivityRef(id=source_alert["reference"], activity=ActivityName.WWFF)],
|
||||||
start_time=datetime.strptime(source_alert["utc_start"], "%Y-%m-%d %H:%M:%S")
|
start_time=datetime.strptime(source_alert["utc_start"], "%Y-%m-%d %H:%M:%S")
|
||||||
.replace(tzinfo=pytz.UTC)
|
.replace(tzinfo=pytz.UTC)
|
||||||
.timestamp(),
|
.timestamp(),
|
||||||
|
|||||||
+35
-35
@@ -68,10 +68,10 @@ class GMA(HTTPSpotProvider):
|
|||||||
# Filter out some weird mode strings
|
# Filter out some weird mode strings
|
||||||
mode=Mode.from_name(source_spot["MODE"].upper()) if "<>" not in source_spot["MODE"] else None,
|
mode=Mode.from_name(source_spot["MODE"].upper()) if "<>" not in source_spot["MODE"] else None,
|
||||||
comment=source_spot["TEXT"],
|
comment=source_spot["TEXT"],
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["REF"],
|
id=source_spot["REF"],
|
||||||
sig="",
|
activity="",
|
||||||
name=source_spot["NAME"],
|
name=source_spot["NAME"],
|
||||||
latitude=lat,
|
latitude=lat,
|
||||||
longitude=lon,
|
longitude=lon,
|
||||||
@@ -98,57 +98,57 @@ class GMA(HTTPSpotProvider):
|
|||||||
and ref_response.text != "\n"
|
and ref_response.text != "\n"
|
||||||
):
|
):
|
||||||
ref_info = ref_response.json()
|
ref_info = ref_response.json()
|
||||||
if spot.sig_refs and ref_info and "reftype" in ref_info:
|
if spot.activity_refs and ref_info and "reftype" in ref_info:
|
||||||
match ref_info["reftype"]:
|
match ref_info["reftype"]:
|
||||||
case "Summit":
|
case "Summit":
|
||||||
# Summits are a bit complicated, they can be SOTA or GMA depending on the
|
# Summits are a bit complicated, they can be SOTA or GMA depending on the
|
||||||
# separate "sota" field:
|
# separate "sota" field:
|
||||||
if "sota" in ref_info and ref_info["sota"] != "":
|
if "sota" in ref_info and ref_info["sota"] != "":
|
||||||
spot.sig_refs[0].sig = ActivityName.SOTA
|
spot.activity_refs[0].activity = ActivityName.SOTA
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.SUMMIT
|
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||||
spot.sig = ActivityName.SOTA
|
spot.activity = ActivityName.SOTA
|
||||||
else:
|
else:
|
||||||
spot.sig_refs[0].sig = ActivityName.GMA
|
spot.activity_refs[0].activity = ActivityName.GMA
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.SUMMIT
|
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||||
spot.sig = ActivityName.GMA
|
spot.activity = ActivityName.GMA
|
||||||
case "POTA":
|
case "POTA":
|
||||||
spot.sig_refs[0].sig = ActivityName.POTA
|
spot.activity_refs[0].activity = ActivityName.POTA
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.PARK
|
spot.activity_refs[0].ref_type = ActivityRefType.PARK
|
||||||
spot.sig = ActivityName.POTA
|
spot.activity = ActivityName.POTA
|
||||||
case "WWFF":
|
case "WWFF":
|
||||||
spot.sig_refs[0].sig = ActivityName.WWFF
|
spot.activity_refs[0].activity = ActivityName.WWFF
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.PARK
|
spot.activity_refs[0].ref_type = ActivityRefType.PARK
|
||||||
spot.sig = ActivityName.WWFF
|
spot.activity = ActivityName.WWFF
|
||||||
case "IOTA Island":
|
case "IOTA Island":
|
||||||
spot.sig_refs[0].sig = ActivityName.IOTA
|
spot.activity_refs[0].activity = ActivityName.IOTA
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.ISLAND
|
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
|
||||||
spot.sig = ActivityName.IOTA
|
spot.activity = ActivityName.IOTA
|
||||||
case "GMA Island":
|
case "GMA Island":
|
||||||
spot.sig_refs[0].sig = ActivityName.GMA_ISLANDS
|
spot.activity_refs[0].activity = ActivityName.GMA_ISLANDS
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.ISLAND
|
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
|
||||||
spot.sig = ActivityName.GMA_ISLANDS
|
spot.activity = ActivityName.GMA_ISLANDS
|
||||||
case "Lighthouse (ILLW)":
|
case "Lighthouse (ILLW)":
|
||||||
spot.sig_refs[0].sig = ActivityName.ILLW
|
spot.activity_refs[0].activity = ActivityName.ILLW
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||||
spot.sig = ActivityName.ILLW
|
spot.activity = ActivityName.ILLW
|
||||||
case "Lighthouse (ARLHS)":
|
case "Lighthouse (ARLHS)":
|
||||||
spot.sig_refs[0].sig = ActivityName.ARLHS
|
spot.activity_refs[0].activity = ActivityName.ARLHS
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||||
spot.sig = ActivityName.ARLHS
|
spot.activity = ActivityName.ARLHS
|
||||||
case "Castle":
|
case "Castle":
|
||||||
spot.sig_refs[0].sig = ActivityName.WCA
|
spot.activity_refs[0].activity = ActivityName.WCA
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.CASTLE
|
spot.activity_refs[0].ref_type = ActivityRefType.CASTLE
|
||||||
spot.sig = ActivityName.WCA
|
spot.activity = ActivityName.WCA
|
||||||
case "Mill":
|
case "Mill":
|
||||||
spot.sig_refs[0].sig = ActivityName.MOTA
|
spot.activity_refs[0].activity = ActivityName.MOTA
|
||||||
spot.sig_refs[0].ref_type = ActivityRefType.MILL
|
spot.activity_refs[0].ref_type = ActivityRefType.MILL
|
||||||
spot.sig = ActivityName.MOTA
|
spot.activity = ActivityName.MOTA
|
||||||
case _:
|
case _:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!"
|
f"GMA spot found with ref type {ref_info['reftype']}, developer needs to add support for this!"
|
||||||
)
|
)
|
||||||
spot.sig_refs[0].sig = ref_info["reftype"]
|
spot.activity_refs[0].activity = ref_info["reftype"]
|
||||||
spot.sig = ref_info["reftype"]
|
spot.activity = ref_info["reftype"]
|
||||||
|
|
||||||
elif not ref_response.from_cache:
|
elif not ref_response.from_cache:
|
||||||
if not ref_response.ok:
|
if not ref_response.ok:
|
||||||
|
|||||||
@@ -62,11 +62,11 @@ class HEMA(HTTPSpotProvider):
|
|||||||
freq=float(freq_mode_match.group(1)) * 1000000,
|
freq=float(freq_mode_match.group(1)) * 1000000,
|
||||||
mode=Mode.from_name(freq_mode_match.group(2).upper()),
|
mode=Mode.from_name(freq_mode_match.group(2).upper()),
|
||||||
comment=spotter_comment_match.group(2),
|
comment=spotter_comment_match.group(2),
|
||||||
sig=ActivityName.HEMA,
|
activity=ActivityName.HEMA,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=spot_items[3].upper(),
|
id=spot_items[3].upper(),
|
||||||
sig=ActivityName.HEMA,
|
activity=ActivityName.HEMA,
|
||||||
name=spot_items[4],
|
name=spot_items[4],
|
||||||
latitude=float(spot_items[7]),
|
latitude=float(spot_items[7]),
|
||||||
longitude=float(spot_items[8]),
|
longitude=float(spot_items[8]),
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ class LLOTA(HTTPSpotProvider):
|
|||||||
freq=float(source_spot["frequency"]) * 1000000,
|
freq=float(source_spot["frequency"]) * 1000000,
|
||||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||||
comment=comment,
|
comment=comment,
|
||||||
sig=ActivityName.LLOTA,
|
activity=ActivityName.LLOTA,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
sig=ActivityName.LLOTA,
|
activity=ActivityName.LLOTA,
|
||||||
name=source_spot["reference_name"],
|
name=source_spot["reference_name"],
|
||||||
ref_type=ActivityRefType.LAKE,
|
ref_type=ActivityRefType.LAKE,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -70,20 +70,20 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
ref_id = source_spot["actSiteID"]
|
ref_id = source_spot["actSiteID"]
|
||||||
|
|
||||||
if activity:
|
if activity:
|
||||||
spot.sig = activity
|
spot.activity = activity
|
||||||
|
|
||||||
if ref_id:
|
if ref_id:
|
||||||
activity_refs = [
|
activity_refs = [
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
sig=activity,
|
activity=activity,
|
||||||
# Free text location is not present in all spots, so only add it if it's set
|
# Free text location is not present in all spots, so only add it if it's set
|
||||||
name=source_spot["actLocation"]
|
name=source_spot["actLocation"]
|
||||||
if "actLocation" in source_spot and source_spot["actLocation"] != ""
|
if "actLocation" in source_spot and source_spot["actLocation"] != ""
|
||||||
else None,
|
else None,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
spot.sig_refs = activity_refs
|
spot.activity_refs = activity_refs
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# If no actSiteID is set, e.g. because actClass is "QRP", sometimes we still have an actLocation
|
# If no actSiteID is set, e.g. because actClass is "QRP", sometimes we still have an actLocation
|
||||||
@@ -128,9 +128,9 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account."
|
"Parks N Peaks user ID and API key are required. Get yours from your Parks N Peaks account."
|
||||||
)
|
)
|
||||||
ref_id = spot.sig_refs[0].id if spot.sig_refs else ""
|
ref_id = spot.activity_refs[0].id if spot.activity_refs else ""
|
||||||
body = {
|
body = {
|
||||||
"actClass": spot.sig or "",
|
"actClass": spot.activity or "",
|
||||||
"actCallsign": spot.dx_call,
|
"actCallsign": spot.dx_call,
|
||||||
"actSite": ref_id,
|
"actSite": ref_id,
|
||||||
"mode": spot.mode or "",
|
"mode": spot.mode or "",
|
||||||
|
|||||||
@@ -33,11 +33,11 @@ class POTA(HTTPSpotProvider):
|
|||||||
freq=float(source_spot["frequency"]) * 1000 if source_spot["frequency"] != "INVALID" else None,
|
freq=float(source_spot["frequency"]) * 1000 if source_spot["frequency"] != "INVALID" else None,
|
||||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||||
comment=source_spot["comments"],
|
comment=source_spot["comments"],
|
||||||
sig=ActivityName.POTA,
|
activity=ActivityName.POTA,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
sig=ActivityName.POTA,
|
activity=ActivityName.POTA,
|
||||||
name=source_spot["name"],
|
name=source_spot["name"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"],
|
longitude=source_spot["longitude"],
|
||||||
@@ -61,14 +61,14 @@ class POTA(HTTPSpotProvider):
|
|||||||
return activity == ActivityName.POTA
|
return activity == ActivityName.POTA
|
||||||
|
|
||||||
def submit_spot(self, spot, credentials):
|
def submit_spot(self, spot, credentials):
|
||||||
sig_ref = spot.sig_refs[0].id if spot.sig_refs else None
|
ref_id = spot.activity_refs[0].id if spot.activity_refs else None
|
||||||
if sig_ref:
|
if ref_id:
|
||||||
body = {
|
body = {
|
||||||
"activator": spot.dx_call,
|
"activator": spot.dx_call,
|
||||||
"spotter": spot.de_call,
|
"spotter": spot.de_call,
|
||||||
"frequency": str(spot.freq / 1000.0),
|
"frequency": str(spot.freq / 1000.0),
|
||||||
"mode": spot.mode or "",
|
"mode": spot.mode or "",
|
||||||
"reference": sig_ref,
|
"reference": ref_id,
|
||||||
"comments": spot.comment or "",
|
"comments": spot.comment or "",
|
||||||
"source": "Spothole",
|
"source": "Spothole",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,11 +57,11 @@ class SOTA(HTTPSpotProvider):
|
|||||||
# Seen SOTA spots with no frequency!
|
# Seen SOTA spots with no frequency!
|
||||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||||
comment=source_spot["comments"],
|
comment=source_spot["comments"],
|
||||||
sig=ActivityName.SOTA,
|
activity=ActivityName.SOTA,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["summitCode"],
|
id=source_spot["summitCode"],
|
||||||
sig=ActivityName.SOTA,
|
activity=ActivityName.SOTA,
|
||||||
name=source_spot["summitName"],
|
name=source_spot["summitName"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"],
|
longitude=source_spot["longitude"],
|
||||||
@@ -92,10 +92,10 @@ class SOTA(HTTPSpotProvider):
|
|||||||
id_token = credentials.get("id_token", "")
|
id_token = credentials.get("id_token", "")
|
||||||
if not access_token or not id_token:
|
if not access_token or not id_token:
|
||||||
raise ValueError("SOTA API tokens are required. Please log into SOTA in order to spot to it.")
|
raise ValueError("SOTA API tokens are required. Please log into SOTA in order to spot to it.")
|
||||||
sig_ref = spot.sig_refs[0].id if spot.sig_refs else ""
|
ref_id = spot.activity_refs[0].id if spot.activity_refs else ""
|
||||||
if sig_ref:
|
if ref_id:
|
||||||
# Split reference into association and summit codes
|
# Split reference into association and summit codes
|
||||||
ref_split = sig_ref.split("/")
|
ref_split = ref_id.split("/")
|
||||||
|
|
||||||
# Figure out a valid mode. Borrowed this from PoLo :)
|
# Figure out a valid mode. Borrowed this from PoLo :)
|
||||||
# https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAPostSelfSpot.js
|
# https://github.com/ham2k/app-polo/blob/main/src/extensions/activities/sota/SOTAPostSelfSpot.js
|
||||||
|
|||||||
@@ -59,13 +59,13 @@ class Tiles(HTTPSpotProvider):
|
|||||||
freq=freq,
|
freq=freq,
|
||||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||||
comment=source_spot["notes"],
|
comment=source_spot["notes"],
|
||||||
sig=ActivityName.TILES,
|
activity=ActivityName.TILES,
|
||||||
# Tiles spots can include POTA & SOTA references, but ignore those on the basis that we will get them separately from the POTA/SOTA providers anyway.
|
# Tiles spots can include POTA & SOTA references, but ignore those on the basis that we will get them separately from the POTA/SOTA providers anyway.
|
||||||
# Just take the grid reference itself as the single Tiles activity reference.
|
# Just take the grid reference itself as the single Tiles activity reference.
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["maidenhead_grid"],
|
id=source_spot["maidenhead_grid"],
|
||||||
sig=ActivityName.TILES,
|
activity=ActivityName.TILES,
|
||||||
name=source_spot["maidenhead_grid"],
|
name=source_spot["maidenhead_grid"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"],
|
longitude=source_spot["longitude"],
|
||||||
|
|||||||
@@ -34,8 +34,10 @@ class Towers(HTTPSpotProvider):
|
|||||||
dx_call=source_spot["call"].upper(),
|
dx_call=source_spot["call"].upper(),
|
||||||
freq=likely_freq,
|
freq=likely_freq,
|
||||||
comment=source_spot["comment"],
|
comment=source_spot["comment"],
|
||||||
sig=ActivityName.TOWERS,
|
activity=ActivityName.TOWERS,
|
||||||
sig_refs=[ActivityRef(id=source_spot["ref"], sig=ActivityName.TOWERS, ref_type=ActivityRefType.TOWER)],
|
activity_refs=[
|
||||||
|
ActivityRef(id=source_spot["ref"], activity=ActivityName.TOWERS, ref_type=ActivityRefType.TOWER)
|
||||||
|
],
|
||||||
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M")
|
time=datetime.strptime(response_json["updated"][:10] + source_spot["time"], "%Y-%m-%d%H:%M")
|
||||||
.replace(tzinfo=pytz.utc)
|
.replace(tzinfo=pytz.utc)
|
||||||
.timestamp(),
|
.timestamp(),
|
||||||
|
|||||||
@@ -92,9 +92,13 @@ class WOTA(HTTPSpotProvider):
|
|||||||
freq=freq_hz,
|
freq=freq_hz,
|
||||||
mode=Mode.from_name(mode),
|
mode=Mode.from_name(mode),
|
||||||
comment=comment,
|
comment=comment,
|
||||||
sig=ActivityName.WOTA,
|
activity=ActivityName.WOTA,
|
||||||
sig_refs=(
|
activity_refs=(
|
||||||
[ActivityRef(id=ref, sig=ActivityName.WOTA, name=ref_name, ref_type=ActivityRefType.SUMMIT)]
|
[
|
||||||
|
ActivityRef(
|
||||||
|
id=ref, activity=ActivityName.WOTA, name=ref_name, ref_type=ActivityRefType.SUMMIT
|
||||||
|
)
|
||||||
|
]
|
||||||
if ref
|
if ref
|
||||||
else []
|
else []
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class WWBOTA(SSESpotProvider):
|
|||||||
for ref in source_spot["references"]:
|
for ref in source_spot["references"]:
|
||||||
activity_ref = ActivityRef(
|
activity_ref = ActivityRef(
|
||||||
id=ref["reference"],
|
id=ref["reference"],
|
||||||
sig=ActivityName.WWBOTA,
|
activity=ActivityName.WWBOTA,
|
||||||
name=ref["name"],
|
name=ref["name"],
|
||||||
latitude=ref["lat"],
|
latitude=ref["lat"],
|
||||||
longitude=ref["long"],
|
longitude=ref["long"],
|
||||||
@@ -38,8 +38,8 @@ class WWBOTA(SSESpotProvider):
|
|||||||
freq=float(source_spot["freq"]) * 1000000,
|
freq=float(source_spot["freq"]) * 1000000,
|
||||||
mode=Mode.from_name(source_spot["mode"].upper()) if source_spot.get("mode") else None,
|
mode=Mode.from_name(source_spot["mode"].upper()) if source_spot.get("mode") else None,
|
||||||
comment=source_spot["comment"],
|
comment=source_spot["comment"],
|
||||||
sig=ActivityName.WWBOTA,
|
activity=ActivityName.WWBOTA,
|
||||||
sig_refs=refs,
|
activity_refs=refs,
|
||||||
time=datetime.fromisoformat(source_spot["time"].replace("Z", "+00:00")).timestamp(),
|
time=datetime.fromisoformat(source_spot["time"].replace("Z", "+00:00")).timestamp(),
|
||||||
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For
|
# WWBOTA spots can contain multiple references for bunkers being activated simultaneously. For
|
||||||
# now, we will just pick the first one to use as our grid, latitude and longitude.
|
# now, we will just pick the first one to use as our grid, latitude and longitude.
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ class WWFF(HTTPSpotProvider):
|
|||||||
freq=float(source_spot["frequency_khz"]) * 1000,
|
freq=float(source_spot["frequency_khz"]) * 1000,
|
||||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||||
comment=source_spot["remarks"],
|
comment=source_spot["remarks"],
|
||||||
sig=ActivityName.WWFF,
|
activity=ActivityName.WWFF,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
sig=ActivityName.WWFF,
|
activity=ActivityName.WWFF,
|
||||||
name=source_spot["reference_name"],
|
name=source_spot["reference_name"],
|
||||||
latitude=source_spot["latitude"],
|
latitude=source_spot["latitude"],
|
||||||
longitude=source_spot["longitude"],
|
longitude=source_spot["longitude"],
|
||||||
|
|||||||
@@ -14,16 +14,18 @@ class XOTA(WebsocketSpotProvider):
|
|||||||
The provider typically doesn't give us a lat/lon or activity explicitly, so our own config provides an activity
|
The provider typically doesn't give us a lat/lon or activity explicitly, so our own config provides an activity
|
||||||
which we can then use for lookups. This functionality is implemented for Toilets on the Air events, of which
|
which we can then use for lookups. This functionality is implemented for Toilets on the Air events, of which
|
||||||
there are several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA, which
|
there are several - so a plain lookup of a "TOTA reference" doesn't make sense, it depends on which TOTA, which
|
||||||
is why we also provide a sig_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01" at C3
|
is why we also provide an activity_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01"
|
||||||
might become "C3 T-01". This allows us to provide location lookups for TOTA at several conferences."""
|
at C3 might become "C3 T-01". This allows us to provide location lookups for TOTA at several conferences."""
|
||||||
|
|
||||||
ACTIVITY = None
|
ACTIVITY = None
|
||||||
|
|
||||||
def __init__(self, provider_config):
|
def __init__(self, provider_config):
|
||||||
name = provider_config.get("name", "xOTA")
|
name = provider_config.get("name", "xOTA")
|
||||||
super().__init__(name, provider_config, provider_config["url"])
|
super().__init__(name, provider_config, provider_config["url"])
|
||||||
self.ACTIVITY = str(provider_config["sig"]) if "sig" in provider_config else None
|
self.ACTIVITY = str(provider_config["activity"]) if "activity" in provider_config else None
|
||||||
self._activity_ref_prefix = str(provider_config["sig_ref_prefix"]) if "sig_ref_prefix" in provider_config else ""
|
self._activity_ref_prefix = (
|
||||||
|
str(provider_config["activity_ref_prefix"]) if "activity_ref_prefix" in provider_config else ""
|
||||||
|
)
|
||||||
|
|
||||||
def _ws_message_to_spot(self, b):
|
def _ws_message_to_spot(self, b):
|
||||||
string = b.decode("utf-8")
|
string = b.decode("utf-8")
|
||||||
@@ -35,11 +37,11 @@ class XOTA(WebsocketSpotProvider):
|
|||||||
dx_call=source_spot["stationCallSign"].upper(),
|
dx_call=source_spot["stationCallSign"].upper(),
|
||||||
freq=float(source_spot["freq"]) * 1000,
|
freq=float(source_spot["freq"]) * 1000,
|
||||||
mode=Mode.from_name(source_spot["mode"].upper()),
|
mode=Mode.from_name(source_spot["mode"].upper()),
|
||||||
sig=self.ACTIVITY,
|
activity=self.ACTIVITY,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
sig=self.ACTIVITY or "",
|
activity=self.ACTIVITY or "",
|
||||||
url=source_spot["reference"]["website"],
|
url=source_spot["reference"]["website"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -35,11 +35,11 @@ class ZLOTA(HTTPSpotProvider):
|
|||||||
freq=freq_hz,
|
freq=freq_hz,
|
||||||
mode=Mode.from_name(source_spot["mode"].upper().strip()),
|
mode=Mode.from_name(source_spot["mode"].upper().strip()),
|
||||||
comment=source_spot["comments"],
|
comment=source_spot["comments"],
|
||||||
sig=ActivityName.ZLOTA,
|
activity=ActivityName.ZLOTA,
|
||||||
sig_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
sig=ActivityName.ZLOTA,
|
activity=ActivityName.ZLOTA,
|
||||||
name=source_spot["name"],
|
name=source_spot["name"],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "spothole"
|
name = "spothole"
|
||||||
version = "2.2"
|
version = "3.0-pre"
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Ian Renton", email = "ian@ianrenton.com" },
|
{ name = "Ian Renton", email = "ian@ianrenton.com" },
|
||||||
]
|
]
|
||||||
|
|||||||
+67
-49
@@ -15,6 +15,25 @@ info:
|
|||||||
|
|
||||||
## Changelog
|
## Changelog
|
||||||
|
|
||||||
|
### 3.0
|
||||||
|
|
||||||
|
The term "SIG" (Special Interest Group), which Spothole inherited from ADIF, has been replaced with "activity" throughout the API.
|
||||||
|
|
||||||
|
* **Breaking change:** In spot and alert data, `sig` has been renamed to `activity` and `sig_refs` to `activity_refs`.
|
||||||
|
* **Breaking change:** In activity reference data (i.e. each entry in `activity_refs`, and the response of the activity reference lookup), `sig` has been renamed to `activity`.
|
||||||
|
* **Breaking change:** The `dx_location_source` value "SIG REF LOOKUP" has been renamed to "ACTIVITY REF LOOKUP".
|
||||||
|
* **Breaking change:** The `/spots`, `/spots/stream`, `/alerts` and `/alerts/stream` query parameter `sig` has been renamed to `activity`, and its special value `NO_SIG` to `NO_ACTIVITY`. The `/spots` and `/spots/stream` query parameters `needs_sig` and `needs_sig_ref` have been renamed to `needs_activity` and `needs_activity_ref`. When using the `fields` query parameter, use the new field names `activity` and `activity_refs`.
|
||||||
|
* **Breaking change:** `/lookup/sigref` has been renamed to `/lookup/activityref`, and its `sig` query parameter has been renamed to `activity`.
|
||||||
|
* **Breaking change:** POST `/spot` now expects `activity` and `activity_refs` in the `spot` object, rather than `sig` and `sig_refs`.
|
||||||
|
* **Breaking change:** In the `/options` response, `sigs` has been renamed to `activities`, and within each activity, `sig_type` has been renamed to `activity_type`.
|
||||||
|
* **Breaking change:** In the `/status` response, `sig_ref_data_providers` has been renamed to `activity_ref_data_providers`, and within each provider, `sig_name` has been renamed to `activity_name`.
|
||||||
|
|
||||||
|
#### Upgrading a client from v2 to v3 API endpoints
|
||||||
|
|
||||||
|
In v3.0 of Spothole, the `v2` (and `v1`) API endpoints will be maintained for backwards compatibility, so if you have written a client against the `v2` API, it will continue to receive `sig`, `sig_refs` etc. as before. However, you are encouraged to move to the `v3` API endpoints as soon as possible.
|
||||||
|
|
||||||
|
To upgrade, replace `v2` with `v3` in the URLs your code calls, then rename any use of the fields, query parameters and values listed above. If you use the activity reference lookup, call `/lookup/activityref?activity=...&id=...` instead of `/lookup/sigref?sig=...&id=...`.
|
||||||
|
|
||||||
### 2.2
|
### 2.2
|
||||||
|
|
||||||
* Renamed AMSAT SIG to "Satellite" as AMSAT is a specific organisation not just a general term for satellite QSOs
|
* Renamed AMSAT SIG to "Satellite" as AMSAT is a specific organisation not just a general term for satellite QSOs
|
||||||
@@ -102,10 +121,10 @@ info:
|
|||||||
license:
|
license:
|
||||||
name: The Unlicense
|
name: The Unlicense
|
||||||
url: https://unlicense.org/#the-unlicense
|
url: https://unlicense.org/#the-unlicense
|
||||||
version: 2.0
|
version: 3.0
|
||||||
|
|
||||||
servers:
|
servers:
|
||||||
- url: https://spothole.app/api/v2
|
- url: https://spothole.app/api/v3
|
||||||
|
|
||||||
tags:
|
tags:
|
||||||
- name: Spots
|
- name: Spots
|
||||||
@@ -396,7 +415,7 @@ paths:
|
|||||||
example: "Failed"
|
example: "Failed"
|
||||||
|
|
||||||
|
|
||||||
/lookup/sigref:
|
/lookup/activityref:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
- Utilities
|
- Utilities
|
||||||
@@ -405,7 +424,7 @@ paths:
|
|||||||
Perform a lookup of data about a certain reference, providing the activity and the ID of the
|
Perform a lookup of data about a certain reference, providing the activity and the ID of the
|
||||||
reference. An ActivityRef structure will be returned containing the activity and ID, plus any other
|
reference. An ActivityRef structure will be returned containing the activity and ID, plus any other
|
||||||
information Spothole could find about it.
|
information Spothole could find about it.
|
||||||
operationId: sigref
|
operationId: activityref
|
||||||
parameters:
|
parameters:
|
||||||
- $ref: '#/components/parameters/ActivityRefLookupActivity'
|
- $ref: '#/components/parameters/ActivityRefLookupActivity'
|
||||||
- $ref: '#/components/parameters/ActivityRefLookupId'
|
- $ref: '#/components/parameters/ActivityRefLookupId'
|
||||||
@@ -461,7 +480,7 @@ paths:
|
|||||||
Supply a JSON object containing a `spot` sub-object (the spot data) and an optional `handling` sub-object
|
Supply a JSON object containing a `spot` sub-object (the spot data) and an optional `handling` sub-object
|
||||||
containing server-side instructions such as upstream submission). Check `spot_submit_providers` in the
|
containing server-side instructions such as upstream submission). Check `spot_submit_providers` in the
|
||||||
`/options` response to see which activities and providers support upstream submission. cURL example:
|
`/options` response to see which activities and providers support upstream submission. cURL example:
|
||||||
`curl --request POST --header \"Content-Type: application/json\" --data '{\"spot\":{\"dx_call\":\"M0TRT\",\"time\":1760019539,\"freq\":14200000,\"comment\":\"Test spot please ignore\",\"de_call\":\"M0TRT\"}}' https://spothole.app/api/v2/spot`"
|
`curl --request POST --header \"Content-Type: application/json\" --data '{\"spot\":{\"dx_call\":\"M0TRT\",\"time\":1760019539,\"freq\":14200000,\"comment\":\"Test spot please ignore\",\"de_call\":\"M0TRT\"}}' https://spothole.app/api/v3/spot`"
|
||||||
operationId: spot
|
operationId: spot
|
||||||
requestBody:
|
requestBody:
|
||||||
description: Object containing a "spot" sub-object with the spot data, and an optional "handling" sub-object with server-side instructions of what to do with it.
|
description: Object containing a "spot" sub-object with the spot data, and an optional "handling" sub-object with server-side instructions of what to do with it.
|
||||||
@@ -559,32 +578,32 @@ components:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Source"
|
$ref: "#/components/schemas/Source"
|
||||||
SpotActivity:
|
SpotActivity:
|
||||||
name: sig
|
name: activity
|
||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the spots to only ones from one or more activities provided as an argument.
|
Limit the spots to only ones from one or more activities provided as an argument.
|
||||||
To select more than one activity, supply a comma-separated list. The special `sig` name `NO_SIG`
|
To select more than one activity, supply a comma-separated list. The special `activity` name `NO_ACTIVITY`
|
||||||
matches spots with no activity set. You can use `sig=NO_SIG` to specifically only return generic
|
matches spots with no activity set. You can use `activity=NO_ACTIVITY` to specifically only return generic
|
||||||
spots with no associated activity. You can also use combinations to request for example POTA + no
|
spots with no associated activity. You can also use combinations to request for example POTA + no
|
||||||
activity, but reject other activities. If you want to request 'every activity and not No Activity', see the
|
activity, but reject other activities. If you want to request 'every activity and not No Activity', see the
|
||||||
`needs_sig` query parameter for a shortcut.
|
`needs_activity` query parameter for a shortcut.
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ActivityNameIncludingNoSig"
|
$ref: "#/components/schemas/ActivityNameIncludingNoActivity"
|
||||||
SpotNeedsActivity:
|
SpotNeedsActivity:
|
||||||
name: needs_sig
|
name: needs_activity
|
||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the spots to only ones with an activity such as POTA. Because supplying all
|
Limit the spots to only ones with an activity such as POTA. Because supplying all
|
||||||
known activities as a `sigs` parameter is unwieldy, and leaving `sigs` blank will also return spots
|
known activities as an `activity` parameter is unwieldy, and leaving `activity` blank will also return spots
|
||||||
with *no* activity, this parameter can be set true to return only spots with an activity, regardless of
|
with *no* activity, this parameter can be set true to return only spots with an activity, regardless of
|
||||||
what it is, so long as it's not blank. This is the equivalent of supplying the `sig` query
|
what it is, so long as it's not blank. This is the equivalent of supplying the `activity` query
|
||||||
param with a list of every known activity apart from the special `NO_SIG` value. This is what Field
|
param with a list of every known activity apart from the special `NO_ACTIVITY` value. This is what Field
|
||||||
Spotter uses to exclude generic cluster spots and only retrieve xOTA things.
|
Spotter uses to exclude generic cluster spots and only retrieve xOTA things.
|
||||||
schema:
|
schema:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
SpotNeedsActivityRef:
|
SpotNeedsActivityRef:
|
||||||
name: needs_sig_ref
|
name: needs_activity_ref
|
||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the spots to only ones which have at least one reference (e.g. a park reference) for
|
Limit the spots to only ones which have at least one reference (e.g. a park reference) for
|
||||||
@@ -723,14 +742,14 @@ components:
|
|||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/Source"
|
$ref: "#/components/schemas/Source"
|
||||||
AlertActivity:
|
AlertActivity:
|
||||||
name: sig
|
name: activity
|
||||||
in: query
|
in: query
|
||||||
description: >
|
description: >
|
||||||
Limit the alerts to only ones from one or more activities. To select more than one
|
Limit the alerts to only ones from one or more activities. To select more than one
|
||||||
activity, supply a comma-separated list. The special value 'NO_SIG' can be included to return alerts
|
activity, supply a comma-separated list. The special value 'NO_ACTIVITY' can be included to return alerts
|
||||||
specifically without an associated activity (i.e. general DXpeditions).
|
specifically without an associated activity.
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ActivityNameIncludingNoSig"
|
$ref: "#/components/schemas/ActivityNameIncludingNoActivity"
|
||||||
AlertDxContinent:
|
AlertDxContinent:
|
||||||
name: dx_continent
|
name: dx_continent
|
||||||
in: query
|
in: query
|
||||||
@@ -839,9 +858,9 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
example: M0TRT
|
example: M0TRT
|
||||||
ActivityRefLookupActivity:
|
ActivityRefLookupActivity:
|
||||||
name: sig
|
name: activity
|
||||||
in: query
|
in: query
|
||||||
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
|
description: Activity, e.g. outdoor activity programme such as POTA
|
||||||
required: true
|
required: true
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ActivityName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
@@ -938,11 +957,11 @@ components:
|
|||||||
- EVENT
|
- EVENT
|
||||||
example: TRADITIONAL
|
example: TRADITIONAL
|
||||||
|
|
||||||
ActivityNameIncludingNoSig:
|
ActivityNameIncludingNoActivity:
|
||||||
oneOf:
|
oneOf:
|
||||||
- $ref: "#/components/schemas/ActivityName"
|
- $ref: "#/components/schemas/ActivityName"
|
||||||
- type: string
|
- type: string
|
||||||
enum: [ NO_SIG ]
|
enum: [ NO_ACTIVITY ]
|
||||||
example: POTA
|
example: POTA
|
||||||
|
|
||||||
ActivityRefType:
|
ActivityRefType:
|
||||||
@@ -1068,7 +1087,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
enum:
|
enum:
|
||||||
- SPOT
|
- SPOT
|
||||||
- "SIG REF LOOKUP"
|
- "ACTIVITY REF LOOKUP"
|
||||||
- "GRID"
|
- "GRID"
|
||||||
- "HOME QTH"
|
- "HOME QTH"
|
||||||
- DXCC
|
- DXCC
|
||||||
@@ -1090,8 +1109,8 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Activity reference ID.
|
description: Activity reference ID.
|
||||||
example: GB-0001
|
example: GB-0001
|
||||||
sig:
|
activity:
|
||||||
description: Activity that this reference is in. Still named "sig" in the API for backwards compatibility.
|
description: Activity that this reference is in.
|
||||||
$ref: "#/components/schemas/ActivityName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
@@ -1205,7 +1224,7 @@ components:
|
|||||||
itself, or from a lookup of the activity ref (e.g. park) it's likely quite accurate, but if
|
itself, or from a lookup of the activity ref (e.g. park) it's likely quite accurate, but if
|
||||||
we had to fall back to QRZ lookup, or even a location based on the DXCC itself, it will
|
we had to fall back to QRZ lookup, or even a location based on the DXCC itself, it will
|
||||||
be a lot less accurate. "SPOT" indicates the location source was the spot itself from the
|
be a lot less accurate. "SPOT" indicates the location source was the spot itself from the
|
||||||
spotting service. "SIG REF LOOKUP" indicates that the spot didn't provide a location,
|
spotting service. "ACTIVITY REF LOOKUP" indicates that the spot didn't provide a location,
|
||||||
but we looked it up from reference data. "GRID" indicates that the spot provided some
|
but we looked it up from reference data. "GRID" indicates that the spot provided some
|
||||||
location data such as a Maidenhead, UK Ordnance Survey or Irish grid reference, but the
|
location data such as a Maidenhead, UK Ordnance Survey or Irish grid reference, but the
|
||||||
location is likely less accurate than "SPOT". "HOME QTH" indicates we looked up the DX
|
location is likely less accurate than "SPOT". "HOME QTH" indicates we looked up the DX
|
||||||
@@ -1218,7 +1237,7 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: >
|
description: >
|
||||||
Does the software think the location is good enough to put a marker on a map? This is
|
Does the software think the location is good enough to put a marker on a map? This is
|
||||||
true if the source is "SPOT", "SIG REF LOOKUP" or "GRID", or alternatively if
|
true if the source is "SPOT", "ACTIVITY REF LOOKUP" or "GRID", or alternatively if
|
||||||
the source is "HOME QTH" and the callsign doesn't have a slash in it (i.e. operator
|
the source is "HOME QTH" and the callsign doesn't have a slash in it (i.e. operator
|
||||||
likely at home).
|
likely at home).
|
||||||
example: true
|
example: true
|
||||||
@@ -1315,14 +1334,14 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Comment left by the spotter, if any
|
description: Comment left by the spotter, if any
|
||||||
example: "59 in NY 73"
|
example: "59 in NY 73"
|
||||||
sig:
|
activity:
|
||||||
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
|
description: Activity, e.g. outdoor activity programme such as POTA
|
||||||
$ref: "#/components/schemas/ActivityName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
sig_refs:
|
activity_refs:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/ActivityRef'
|
$ref: '#/components/schemas/ActivityRef'
|
||||||
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility.
|
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
|
||||||
qrt:
|
qrt:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
|
description: QRT state. Some APIs return spots marked as QRT. Otherwise we can check the comments.
|
||||||
@@ -1364,7 +1383,7 @@ components:
|
|||||||
type: boolean
|
type: boolean
|
||||||
description: >
|
description: >
|
||||||
If true, forward the spot to an external upstream provider (e.g. POTA, SOTA) rather
|
If true, forward the spot to an external upstream provider (e.g. POTA, SOTA) rather
|
||||||
than only adding it to this Spothole server. Requires `sig`, at least one `sig_refs`
|
than only adding it to this Spothole server. Requires `activity`, at least one `activity_refs`
|
||||||
entry, and `upstream_provider` to be set. Check `spot_submit_providers` in the
|
entry, and `upstream_provider` to be set. Check `spot_submit_providers` in the
|
||||||
/options response to see which activities and providers support this.
|
/options response to see which activities and providers support this.
|
||||||
default: false
|
default: false
|
||||||
@@ -1503,14 +1522,14 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: Comment made by the activator, if any
|
description: Comment made by the activator, if any
|
||||||
example: "2025 DXpedition to null island"
|
example: "2025 DXpedition to null island"
|
||||||
sig:
|
activity:
|
||||||
description: Activity, e.g. outdoor activity programme such as POTA (still named "sig" in the API for backwards compatibility)
|
description: Activity, e.g. outdoor activity programme such as POTA
|
||||||
$ref: "#/components/schemas/ActivityName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
sig_refs:
|
activity_refs:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/ActivityRef'
|
$ref: '#/components/schemas/ActivityRef'
|
||||||
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility.
|
description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
|
||||||
url:
|
url:
|
||||||
type: string
|
type: string
|
||||||
description: A URL linking to more information about the alert, e.g. DXpedition or contest info.
|
description: A URL linking to more information about the alert, e.g. DXpedition or contest info.
|
||||||
@@ -1604,8 +1623,8 @@ components:
|
|||||||
Activity:
|
Activity:
|
||||||
type: object
|
type: object
|
||||||
description: >
|
description: >
|
||||||
Represents an activity (a term which replaces the older "Special Interest Group" or "SIG" terminology,
|
Represents an activity, such as an outdoor activity programme (e.g. POTA), or another kind of operating
|
||||||
though `sig`-prefixed field names remain for API backwards compatibility).
|
(e.g. Contest, DXpedition).
|
||||||
properties:
|
properties:
|
||||||
name:
|
name:
|
||||||
description: The abbreviated name of the activity
|
description: The abbreviated name of the activity
|
||||||
@@ -1614,12 +1633,11 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: The full name of the activity
|
description: The full name of the activity
|
||||||
example: Parks on the Air
|
example: Parks on the Air
|
||||||
sig_type:
|
activity_type:
|
||||||
type: boolean
|
|
||||||
description: >
|
description: >
|
||||||
Whether the activity is traditional (e.g. EME), adventure (e.g. POTA), regional (e.g. WAB), or for a
|
Whether the activity is traditional (e.g. EME), adventure (e.g. POTA), regional (e.g. WAB), or for a
|
||||||
specific event (e.g. MOTA). Generally for Spothole's own internal use, clients probably won't need this.
|
specific event (e.g. MOTA). Generally for Spothole's own internal use, clients probably won't need this.
|
||||||
Used to group them in the web UI. Still named "sig_type" in the API for backwards compatibility.
|
Used to group them in the web UI.
|
||||||
$ref: "#/components/schemas/ActivityType"
|
$ref: "#/components/schemas/ActivityType"
|
||||||
has_refs:
|
has_refs:
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -1998,7 +2016,7 @@ components:
|
|||||||
StaticDataProviderStatus:
|
StaticDataProviderStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sig_name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: The name of the provider.
|
description: The name of the provider.
|
||||||
example: K0SWE
|
example: K0SWE
|
||||||
@@ -2020,9 +2038,9 @@ components:
|
|||||||
ActivityRefDataProviderStatus:
|
ActivityRefDataProviderStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sig_name:
|
activity_name:
|
||||||
type: string
|
type: string
|
||||||
description: The name of the activity. Still named "sig_name" in the API for backwards compatibility.
|
description: The name of the activity.
|
||||||
example: WWFF
|
example: WWFF
|
||||||
enabled:
|
enabled:
|
||||||
type: boolean
|
type: boolean
|
||||||
@@ -2046,7 +2064,7 @@ components:
|
|||||||
CallsignDataProviderStatus:
|
CallsignDataProviderStatus:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
sig_name:
|
name:
|
||||||
type: string
|
type: string
|
||||||
description: The name of the provider.
|
description: The name of the provider.
|
||||||
example: Country Files
|
example: Country Files
|
||||||
@@ -2185,7 +2203,7 @@ components:
|
|||||||
description: An array of all the static reference data providers.
|
description: An array of all the static reference data providers.
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/StaticDataProviderStatus'
|
$ref: '#/components/schemas/StaticDataProviderStatus'
|
||||||
sig_ref_data_providers:
|
activity_ref_data_providers:
|
||||||
type: array
|
type: array
|
||||||
description: An array of all the activity reference data providers.
|
description: An array of all the activity reference data providers.
|
||||||
items:
|
items:
|
||||||
@@ -2216,7 +2234,7 @@ components:
|
|||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
example: "PHONE"
|
example: "PHONE"
|
||||||
sigs:
|
activities:
|
||||||
type: array
|
type: array
|
||||||
description: An array of all the supported activities.
|
description: An array of all the supported activities.
|
||||||
items:
|
items:
|
||||||
@@ -2254,7 +2272,7 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
description: >
|
description: >
|
||||||
The maximum age, in seconds, of any spot before it will be deleted by the system. When
|
The maximum age, in seconds, of any spot before it will be deleted by the system. When
|
||||||
querying the /api/v2/spots endpoint and providing a "max_age" or "since" parameter, there
|
querying the /api/v3/spots endpoint and providing a "max_age" or "since" parameter, there
|
||||||
is no point providing a number larger than this, because the system drops all spots older
|
is no point providing a number larger than this, because the system drops all spots older
|
||||||
than this.
|
than this.
|
||||||
example: 3600
|
example: 3600
|
||||||
|
|||||||
+13
-13
@@ -29,7 +29,7 @@ const PROVIDER_CREDENTIAL_SCHEMAS = {
|
|||||||
// Load server options. Once a successful callback is made from this, we can populate the choice boxes in the form and load
|
// Load server options. Once a successful callback is made from this, we can populate the choice boxes in the form and load
|
||||||
// any saved values from local storage.
|
// any saved values from local storage.
|
||||||
function loadOptions() {
|
function loadOptions() {
|
||||||
$.getJSON('/api/v2/options', function (jsonData) {
|
$.getJSON('/api/v3/options', function (jsonData) {
|
||||||
// Store options
|
// Store options
|
||||||
options = jsonData;
|
options = jsonData;
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ function loadOptions() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Populate activity drop-down
|
// Populate activity drop-down
|
||||||
$.each(options["sigs"], function (i, activity) {
|
$.each(options["activities"], function (i, activity) {
|
||||||
$('#sig').append($('<option>', {
|
$('#activity').append($('<option>', {
|
||||||
value: activity.name,
|
value: activity.name,
|
||||||
text: activity.name
|
text: activity.name
|
||||||
}));
|
}));
|
||||||
@@ -91,8 +91,8 @@ function updateUpstreamArea() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sig = $("#sig").val();
|
const activity = $("#activity").val();
|
||||||
const providers = (sig && options["spot_submit_providers"][sig]) ? options["spot_submit_providers"][sig] : [];
|
const providers = (activity && options["spot_submit_providers"][activity]) ? options["spot_submit_providers"][activity] : [];
|
||||||
|
|
||||||
if (providers.length === 0) {
|
if (providers.length === 0) {
|
||||||
$("#upstream-area").hide();
|
$("#upstream-area").hide();
|
||||||
@@ -131,8 +131,8 @@ function updateCredentialsButton() {
|
|||||||
|
|
||||||
// Get the currently selected upstream provider name
|
// Get the currently selected upstream provider name
|
||||||
function getSelectedUpstreamProvider() {
|
function getSelectedUpstreamProvider() {
|
||||||
const providers = (options && options["spot_submit_providers"] && $("#sig").val())
|
const providers = (options && options["spot_submit_providers"] && $("#activity").val())
|
||||||
? (options["spot_submit_providers"][$("#sig").val()] || [])
|
? (options["spot_submit_providers"][$("#activity").val()] || [])
|
||||||
: [];
|
: [];
|
||||||
if (providers.length === 0) return null;
|
if (providers.length === 0) return null;
|
||||||
if (providers.length === 1) return providers[0];
|
if (providers.length === 1) return providers[0];
|
||||||
@@ -197,8 +197,8 @@ function addSpot() {
|
|||||||
const dx = $("#dx-call").val().toUpperCase();
|
const dx = $("#dx-call").val().toUpperCase();
|
||||||
const freqStr = $("#freq").val();
|
const freqStr = $("#freq").val();
|
||||||
const mode = $("#mode")[0].value;
|
const mode = $("#mode")[0].value;
|
||||||
const sig = $("#sig")[0].value;
|
const activity = $("#activity")[0].value;
|
||||||
const sigRef = $("#sig-ref").val();
|
const activityRef = $("#activity-ref").val();
|
||||||
const dxGrid = $("#dx-grid").val();
|
const dxGrid = $("#dx-grid").val();
|
||||||
const comment = $("#comment").val();
|
const comment = $("#comment").val();
|
||||||
const de = $("#de-call").val().toUpperCase();
|
const de = $("#de-call").val().toUpperCase();
|
||||||
@@ -208,8 +208,8 @@ function addSpot() {
|
|||||||
spot["dx_call"] = dx;
|
spot["dx_call"] = dx;
|
||||||
spot["freq"] = parseFloat(freqStr) * 1000;
|
spot["freq"] = parseFloat(freqStr) * 1000;
|
||||||
if (mode !== "") spot["mode"] = mode;
|
if (mode !== "") spot["mode"] = mode;
|
||||||
if (sig !== "") spot["sig"] = sig;
|
if (activity !== "") spot["activity"] = activity;
|
||||||
if (sigRef !== "") spot["sig_refs"] = [{sig: sig, id: sigRef}];
|
if (activityRef !== "") spot["activity_refs"] = [{activity: activity, id: activityRef}];
|
||||||
if (dxGrid !== "") spot["dx_grid"] = dxGrid;
|
if (dxGrid !== "") spot["dx_grid"] = dxGrid;
|
||||||
if (comment !== "") spot["comment"] = comment;
|
if (comment !== "") spot["comment"] = comment;
|
||||||
spot["de_call"] = de;
|
spot["de_call"] = de;
|
||||||
@@ -232,7 +232,7 @@ function addSpot() {
|
|||||||
handling["upstream_credentials"] = loadCredentials(upstreamProviderName);
|
handling["upstream_credentials"] = loadCredentials(upstreamProviderName);
|
||||||
}
|
}
|
||||||
|
|
||||||
$.ajax("/api/v2/spot", {
|
$.ajax("/api/v3/spot", {
|
||||||
data: JSON.stringify({spot, handling}),
|
data: JSON.stringify({spot, handling}),
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
@@ -291,7 +291,7 @@ $("#mode").change(function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Update upstream area and credentials button when activity changes
|
// Update upstream area and credentials button when activity changes
|
||||||
$("#sig").change(function () {
|
$("#activity").change(function () {
|
||||||
updateUpstreamArea();
|
updateUpstreamArea();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+14
-14
@@ -10,7 +10,7 @@ let alerts = [];
|
|||||||
// to alerts
|
// to alerts
|
||||||
function loadAlerts() {
|
function loadAlerts() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v2/alerts' + buildQueryString(), dataType: 'json', success: function (jsonData) {
|
url: '/api/v3/alerts' + buildQueryString(), dataType: 'json', success: function (jsonData) {
|
||||||
// Store last updated time
|
// Store last updated time
|
||||||
lastUpdateTime = moment.utc();
|
lastUpdateTime = moment.utc();
|
||||||
// Store data
|
// Store data
|
||||||
@@ -24,7 +24,7 @@ function loadAlerts() {
|
|||||||
// Build a query string for the API, based on the filters that the user has selected.
|
// Build a query string for the API, based on the filters that the user has selected.
|
||||||
function buildQueryString() {
|
function buildQueryString() {
|
||||||
let str = "?";
|
let str = "?";
|
||||||
["dx_continent", "source", "sig"].forEach(fn => {
|
["dx_continent", "source", "activity"].forEach(fn => {
|
||||||
if (!allFilterOptionsSelected(fn)) {
|
if (!allFilterOptionsSelected(fn)) {
|
||||||
str = str + getQueryStringFor(fn) + "&";
|
str = str + getQueryStringFor(fn) + "&";
|
||||||
}
|
}
|
||||||
@@ -210,14 +210,14 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
if (a["dx_calls"] != null) {
|
if (a["dx_calls"] != null) {
|
||||||
dx_calls_html = a["dx_calls"].map(call => `<a class='dx-link' href='https://qrz.com/db/${call}' target='_new'>${call}</a>`).join(", ");
|
dx_calls_html = a["dx_calls"].map(call => `<a class='dx-link' href='https://qrz.com/db/${call}' target='_new'>${call}</a>`).join(", ");
|
||||||
}
|
}
|
||||||
if (dx_calls_html === "" && a["sig"] === "Contest") {
|
if (dx_calls_html === "" && a["activity"] === "Contest") {
|
||||||
// Contest = true and no DX callsigns, so display "Contest"
|
// Contest = true and no DX callsigns, so display "Contest"
|
||||||
dx_calls_html = "Contest"
|
dx_calls_html = "Contest"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format DXpedition country
|
// Format DXpedition country
|
||||||
let dx_country_html = "";
|
let dx_country_html = "";
|
||||||
if (a["sig"] === "DXpedition" && a["dx_country"] != null && a["dx_country"] !== "") {
|
if (a["activity"] === "DXpedition" && a["dx_country"] != null && a["dx_country"] !== "") {
|
||||||
dx_country_html = `<br/>${a["dx_country"]}`;
|
dx_country_html = `<br/>${a["dx_country"]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,23 +252,23 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
|
|
||||||
// Activity or fallback to "General DX"
|
// Activity or fallback to "General DX"
|
||||||
let activityText = "General DX";
|
let activityText = "General DX";
|
||||||
if (a["sig"]) {
|
if (a["activity"]) {
|
||||||
activityText = a["sig"];
|
activityText = a["activity"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format activity refs
|
// Format activity refs
|
||||||
let activityRefs = "";
|
let activityRefs = "";
|
||||||
if (a["sig_refs"] != null) {
|
if (a["activity_refs"] != null) {
|
||||||
const items = [];
|
const items = [];
|
||||||
for (let i = 0; i < a["sig_refs"].length; i++) {
|
for (let i = 0; i < a["activity_refs"].length; i++) {
|
||||||
if (a["sig_refs"][i]["url"] != null) {
|
if (a["activity_refs"][i]["url"] != null) {
|
||||||
items[i] = `<a href='${encodeURI(a["sig_refs"][i]["url"])}' title='${escapeHtml(a["sig_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(a["sig_refs"][i]["id"])}</a>`
|
items[i] = `<a href='${encodeURI(a["activity_refs"][i]["url"])}' title='${escapeHtml(a["activity_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(a["activity_refs"][i]["id"])}</a>`
|
||||||
} else {
|
} else {
|
||||||
items[i] = `${escapeHtml(a["sig_refs"][i]["id"])}`
|
items[i] = `${escapeHtml(a["activity_refs"][i]["id"])}`
|
||||||
}
|
}
|
||||||
// If this is a satellite alert the ref will just be the satellite, but DX grid is also important, so
|
// If this is a satellite alert the ref will just be the satellite, but DX grid is also important, so
|
||||||
// show that if we can.
|
// show that if we can.
|
||||||
if (a["sig_refs"][i]["sig"] === "Satellite" && a["dx_grid"] != null) {
|
if (a["activity_refs"][i]["activity"] === "Satellite" && a["dx_grid"] != null) {
|
||||||
items[i] += " from " + a["dx_grid"];
|
items[i] += " from " + a["dx_grid"];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,7 +330,7 @@ function addAlertRowsToTable(tbody, alerts) {
|
|||||||
|
|
||||||
// Load server options. Once a successful callback is made from this, we then query alerts.
|
// Load server options. Once a successful callback is made from this, we then query alerts.
|
||||||
function loadOptions() {
|
function loadOptions() {
|
||||||
$.getJSON('/api/v2/options', function (jsonData) {
|
$.getJSON('/api/v3/options', function (jsonData) {
|
||||||
// Store options
|
// Store options
|
||||||
options = jsonData;
|
options = jsonData;
|
||||||
|
|
||||||
@@ -338,7 +338,7 @@ function loadOptions() {
|
|||||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||||
generateMultiToggleFilterCard("#source-options", "source", options["alert_providers"]);
|
generateMultiToggleFilterCard("#source-options", "source", options["alert_providers"]);
|
||||||
// Alerts can only ever be tagged with activities that have an alert source, so only offer those as filters here
|
// Alerts can only ever be tagged with activities that have an alert source, so only offer those as filters here
|
||||||
generateActivitiesMultiToggleFilterCard(options["sigs"].filter(o => o["alerts_possible"]), false);
|
generateActivitiesMultiToggleFilterCard(options["activities"].filter(o => o["alerts_possible"]), false);
|
||||||
|
|
||||||
// Load URL params. These may select things from the various filter & display options, so the function needs
|
// Load URL params. These may select things from the various filter & display options, so the function needs
|
||||||
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
// to be called after these are set up, but if the URL params ask for "embedded mode", this will suppress
|
||||||
|
|||||||
+5
-5
@@ -20,7 +20,7 @@ function loadSpots() {
|
|||||||
sseAbortController.abort();
|
sseAbortController.abort();
|
||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v2/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
|
url: '/api/v3/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
|
||||||
// Store data
|
// Store data
|
||||||
spots = jsonData;
|
spots = jsonData;
|
||||||
// Update bands display
|
// Update bands display
|
||||||
@@ -39,7 +39,7 @@ function startSSEConnection() {
|
|||||||
sseAbortController = new AbortController();
|
sseAbortController = new AbortController();
|
||||||
|
|
||||||
// No need to include QRZ/HamQTH credentials because the information wouldn't be displayed on the bands panel anyway
|
// No need to include QRZ/HamQTH credentials because the information wouldn't be displayed on the bands panel anyway
|
||||||
fetchEventSource('/api/v2/spots/stream' + buildQueryString(), {
|
fetchEventSource('/api/v3/spots/stream' + buildQueryString(), {
|
||||||
signal: sseAbortController.signal,
|
signal: sseAbortController.signal,
|
||||||
openWhenHidden: true,
|
openWhenHidden: true,
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ function expireOldSpots() {
|
|||||||
// in the bands page's version of this, because nothing QRZ.com/HamQTH can provide will affect the display.
|
// in the bands page's version of this, because nothing QRZ.com/HamQTH can provide will affect the display.
|
||||||
function buildQueryString() {
|
function buildQueryString() {
|
||||||
let str = "?";
|
let str = "?";
|
||||||
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
|
["dx_continent", "de_continent", "mode", "source", "band", "activity"].forEach(fn => {
|
||||||
if (!allFilterOptionsSelected(fn)) {
|
if (!allFilterOptionsSelected(fn)) {
|
||||||
str = str + getQueryStringFor(fn) + "&";
|
str = str + getQueryStringFor(fn) + "&";
|
||||||
}
|
}
|
||||||
@@ -281,7 +281,7 @@ function removeDuplicatesForBandPanel(spotList) {
|
|||||||
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
||||||
// spots repeatedly.
|
// spots repeatedly.
|
||||||
function loadOptions() {
|
function loadOptions() {
|
||||||
$.getJSON('/api/v2/options', function (jsonData) {
|
$.getJSON('/api/v3/options', function (jsonData) {
|
||||||
// Store options
|
// Store options
|
||||||
options = jsonData;
|
options = jsonData;
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ function loadOptions() {
|
|||||||
|
|
||||||
// Populate the filters panel
|
// Populate the filters panel
|
||||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||||
generateActivitiesMultiToggleFilterCard(options["sigs"]);
|
generateActivitiesMultiToggleFilterCard(options["activities"]);
|
||||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||||
generateModesMultiToggleFilterCard(options["modes"]);
|
generateModesMultiToggleFilterCard(options["modes"]);
|
||||||
|
|||||||
+9
-9
@@ -67,7 +67,7 @@ function loadURLParams() {
|
|||||||
updateSelectFromParam(params, "limit", "alerts_to_fetch"); // Only on Alerts page
|
updateSelectFromParam(params, "limit", "alerts_to_fetch"); // Only on Alerts page
|
||||||
updateSelectFromParam(params, "max_age", "max_spot_age"); // Only on Map & Bands pages
|
updateSelectFromParam(params, "max_age", "max_spot_age"); // Only on Map & Bands pages
|
||||||
updateFilterFromParam(params, "band", "band");
|
updateFilterFromParam(params, "band", "band");
|
||||||
updateFilterFromParam(params, "sig", "sig");
|
updateFilterFromParam(params, "activity", "activity");
|
||||||
updateFilterFromParam(params, "source", "source");
|
updateFilterFromParam(params, "source", "source");
|
||||||
updateFilterFromParam(params, "mode", "mode");
|
updateFilterFromParam(params, "mode", "mode");
|
||||||
updateFilterFromParam(params, "dx_continent", "dx_continent");
|
updateFilterFromParam(params, "dx_continent", "dx_continent");
|
||||||
@@ -156,41 +156,41 @@ function buildActivityFilterGrid(activity_options) {
|
|||||||
const $grid = $('<div class="row row-cols-2 g-1 mb-1">');
|
const $grid = $('<div class="row row-cols-2 g-1 mb-1">');
|
||||||
activity_options.forEach(o => {
|
activity_options.forEach(o => {
|
||||||
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, "");
|
||||||
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-${domSafeName}" for="filter-button-sig-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
$grid.append(`<div class="col"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-activity storeable-checkbox" id="filter-button-activity-${domSafeName}" value="${o['name']}" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-activity-${domSafeName}" for="filter-button-activity-${domSafeName}" title="${o['description']}"><i class="fa-solid ${o['icon']}"></i> ${o['name']} ${(o["region_flag"] != null) ? o['region_flag'] : ''}</label></div></div>`);
|
||||||
});
|
});
|
||||||
return $grid;
|
return $grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate activities filter card. This one is also a special case. includeGeneralDX controls whether the "General
|
// Generate activities filter card. This one is also a special case. includeGeneralDX controls whether the "General
|
||||||
// DX" (NO_SIG) option is offered - this doesn't apply on the alerts page, where every alert has an activity.
|
// DX" (NO_ACTIVITY) option is offered - this doesn't apply on the alerts page, where every alert has an activity.
|
||||||
function generateActivitiesMultiToggleFilterCard(activity_options, includeGeneralDX = true) {
|
function generateActivitiesMultiToggleFilterCard(activity_options, includeGeneralDX = true) {
|
||||||
const $list = $('<ul class="list-unstyled filter-section-list ps-0">');
|
const $list = $('<ul class="list-unstyled filter-section-list ps-0">');
|
||||||
|
|
||||||
const traditional = activity_options.filter(o => o["sig_type"] === "TRADITIONAL");
|
const traditional = activity_options.filter(o => o["activity_type"] === "TRADITIONAL");
|
||||||
appendActivityFilterSection($list, 'traditional', 'Traditional', true, includeGeneralDX || traditional.length > 0, $body => {
|
appendActivityFilterSection($list, 'traditional', 'Traditional', true, includeGeneralDX || traditional.length > 0, $body => {
|
||||||
if (includeGeneralDX) {
|
if (includeGeneralDX) {
|
||||||
$body.append(`<div class="w-100 mb-1"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-sig storeable-checkbox" id="filter-button-sig-NO_SIG" value="NO_SIG" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-sig-NO_SIG" for="filter-button-sig-NO_SIG"><i class="fa-solid fa-tower-cell"></i> General DX</label></div></div>`);
|
$body.append(`<div class="w-100 mb-1"><div class="form-check"><input type="checkbox" class="form-check-input filter-button-activity storeable-checkbox" id="filter-button-activity-NO_ACTIVITY" value="NO_ACTIVITY" autocomplete="off" onClick="filtersUpdated()" checked><label class="form-check-label" id="filter-button-label-activity-NO_ACTIVITY" for="filter-button-activity-NO_ACTIVITY"><i class="fa-solid fa-tower-cell"></i> General DX</label></div></div>`);
|
||||||
}
|
}
|
||||||
$body.append(buildActivityFilterGrid(traditional));
|
$body.append(buildActivityFilterGrid(traditional));
|
||||||
});
|
});
|
||||||
|
|
||||||
const adventure = activity_options.filter(o => o["sig_type"] === "ADVENTURE");
|
const adventure = activity_options.filter(o => o["activity_type"] === "ADVENTURE");
|
||||||
appendActivityFilterSection($list, 'adventure', 'Adventure', true, adventure.length > 0, $body => {
|
appendActivityFilterSection($list, 'adventure', 'Adventure', true, adventure.length > 0, $body => {
|
||||||
$body.append(buildActivityFilterGrid(adventure));
|
$body.append(buildActivityFilterGrid(adventure));
|
||||||
});
|
});
|
||||||
|
|
||||||
const regional = activity_options.filter(o => o["sig_type"] === "REGIONAL");
|
const regional = activity_options.filter(o => o["activity_type"] === "REGIONAL");
|
||||||
appendActivityFilterSection($list, 'regional', 'Regional', false, regional.length > 0, $body => {
|
appendActivityFilterSection($list, 'regional', 'Regional', false, regional.length > 0, $body => {
|
||||||
$body.append(buildActivityFilterGrid(regional));
|
$body.append(buildActivityFilterGrid(regional));
|
||||||
});
|
});
|
||||||
|
|
||||||
const event = activity_options.filter(o => o["sig_type"] === "EVENT");
|
const event = activity_options.filter(o => o["activity_type"] === "EVENT");
|
||||||
appendActivityFilterSection($list, 'event', 'Event', false, event.length > 0, $body => {
|
appendActivityFilterSection($list, 'event', 'Event', false, event.length > 0, $body => {
|
||||||
$body.append(buildActivityFilterGrid(event));
|
$body.append(buildActivityFilterGrid(event));
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#activity-options").append($list);
|
$("#activity-options").append($list);
|
||||||
$("#activity-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('sig', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('sig', false); return false;">None</a></div>`);
|
$("#activity-options").append(`<div class="mt-1"><a href="#" onclick="toggleFilterButtons('activity', true); return false;">All</a> <a href="#" onclick="toggleFilterButtons('activity', false); return false;">None</a></div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Method called when "All" or "None" is clicked
|
// Method called when "All" or "None" is clicked
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ let ionosondeChart = null;
|
|||||||
|
|
||||||
// Load solar conditions
|
// Load solar conditions
|
||||||
function loadSolarConditions() {
|
function loadSolarConditions() {
|
||||||
$.getJSON('/api/v2/solar', function (jsonData) {
|
$.getJSON('/api/v3/solar', function (jsonData) {
|
||||||
|
|
||||||
// HF
|
// HF
|
||||||
|
|
||||||
@@ -660,7 +660,7 @@ function dxStatsContientChanged() {
|
|||||||
|
|
||||||
// Fetch DX stats from the API and render
|
// Fetch DX stats from the API and render
|
||||||
function loadDxStats() {
|
function loadDxStats() {
|
||||||
$.getJSON('/api/v2/dxstats', function (jsonData) {
|
$.getJSON('/api/v3/dxstats', function (jsonData) {
|
||||||
dxStatsData = jsonData;
|
dxStatsData = jsonData;
|
||||||
renderDxStats();
|
renderDxStats();
|
||||||
});
|
});
|
||||||
|
|||||||
+13
-13
@@ -45,7 +45,7 @@ function loadSpots() {
|
|||||||
// 3) Subscribe to the SSE endpoint (with credentials if we have them) so that updates come with augmented
|
// 3) Subscribe to the SSE endpoint (with credentials if we have them) so that updates come with augmented
|
||||||
// data if they can.
|
// data if they can.
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v2/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
|
url: '/api/v3/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
|
||||||
// Store data
|
// Store data
|
||||||
spots = jsonData;
|
spots = jsonData;
|
||||||
// Update map
|
// Update map
|
||||||
@@ -58,7 +58,7 @@ function loadSpots() {
|
|||||||
// OK, we have credentials and have loaded once without them so the user has a basic map. Now reload
|
// OK, we have credentials and have loaded once without them so the user has a basic map. Now reload
|
||||||
// with the credentials and replace what's on the map, so we can improve the data.
|
// with the credentials and replace what's on the map, so we can improve the data.
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v2/spots' + buildQueryString(),
|
url: '/api/v3/spots' + buildQueryString(),
|
||||||
dataType: 'json',
|
dataType: 'json',
|
||||||
headers: getCredentialHeaders(),
|
headers: getCredentialHeaders(),
|
||||||
success: function (jsonData2) {
|
success: function (jsonData2) {
|
||||||
@@ -85,7 +85,7 @@ function startSSEConnection() {
|
|||||||
sseAbortController = new AbortController();
|
sseAbortController = new AbortController();
|
||||||
|
|
||||||
// SSE is going to fetch only a few spots at a time, so now we include QRZ/HamQTH credentials because the delay won't be significant.
|
// SSE is going to fetch only a few spots at a time, so now we include QRZ/HamQTH credentials because the delay won't be significant.
|
||||||
fetchEventSource('/api/v2/spots/stream' + buildQueryString(), {
|
fetchEventSource('/api/v3/spots/stream' + buildQueryString(), {
|
||||||
headers: getCredentialHeaders(),
|
headers: getCredentialHeaders(),
|
||||||
signal: sseAbortController.signal,
|
signal: sseAbortController.signal,
|
||||||
openWhenHidden: true,
|
openWhenHidden: true,
|
||||||
@@ -183,7 +183,7 @@ function removeSpotFromMap(key) {
|
|||||||
// Build a query string for the API, based on the filters that the user has selected.
|
// Build a query string for the API, based on the filters that the user has selected.
|
||||||
function buildQueryString() {
|
function buildQueryString() {
|
||||||
let str = "?";
|
let str = "?";
|
||||||
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
|
["dx_continent", "de_continent", "mode", "source", "band", "activity"].forEach(fn => {
|
||||||
if (!allFilterOptionsSelected(fn)) {
|
if (!allFilterOptionsSelected(fn)) {
|
||||||
str = str + getQueryStringFor(fn) + "&";
|
str = str + getQueryStringFor(fn) + "&";
|
||||||
}
|
}
|
||||||
@@ -276,19 +276,19 @@ function getTooltipText(s) {
|
|||||||
|
|
||||||
// Activity or fallback to source
|
// Activity or fallback to source
|
||||||
let activitySourceText = s["source"];
|
let activitySourceText = s["source"];
|
||||||
if (s["sig"]) {
|
if (s["activity"]) {
|
||||||
activitySourceText = s["sig"];
|
activitySourceText = s["activity"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format activity refs
|
// Format activity refs
|
||||||
let activityRefs = "";
|
let activityRefs = "";
|
||||||
if (s["sig_refs"] != null) {
|
if (s["activity_refs"] != null) {
|
||||||
const items = [];
|
const items = [];
|
||||||
for (let i = 0; i < s["sig_refs"].length; i++) {
|
for (let i = 0; i < s["activity_refs"].length; i++) {
|
||||||
if (s["sig_refs"][i]["url"] != null) {
|
if (s["activity_refs"][i]["url"] != null) {
|
||||||
items[i] = `<a href='${s["sig_refs"][i]["url"]}' title='${s["sig_refs"][i]["name"]}' target='_new' class='activity-ref-link'>${s["sig_refs"][i]["id"]}</a>`
|
items[i] = `<a href='${s["activity_refs"][i]["url"]}' title='${s["activity_refs"][i]["name"]}' target='_new' class='activity-ref-link'>${s["activity_refs"][i]["id"]}</a>`
|
||||||
} else {
|
} else {
|
||||||
items[i] = `${s["sig_refs"][i]["id"]}`
|
items[i] = `${s["activity_refs"][i]["id"]}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activityRefs = items.join(", ");
|
activityRefs = items.join(", ");
|
||||||
@@ -325,7 +325,7 @@ function getTooltipText(s) {
|
|||||||
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
||||||
// spots repeatedly.
|
// spots repeatedly.
|
||||||
function loadOptions() {
|
function loadOptions() {
|
||||||
$.getJSON('/api/v2/options', function (jsonData) {
|
$.getJSON('/api/v3/options', function (jsonData) {
|
||||||
// Store options
|
// Store options
|
||||||
options = jsonData;
|
options = jsonData;
|
||||||
|
|
||||||
@@ -339,7 +339,7 @@ function loadOptions() {
|
|||||||
|
|
||||||
// Populate the filters panel
|
// Populate the filters panel
|
||||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||||
generateActivitiesMultiToggleFilterCard(options["sigs"]);
|
generateActivitiesMultiToggleFilterCard(options["activities"]);
|
||||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||||
generateModesMultiToggleFilterCard(options["modes"]);
|
generateModesMultiToggleFilterCard(options["modes"]);
|
||||||
|
|||||||
+12
-12
@@ -20,7 +20,7 @@ function loadSpots() {
|
|||||||
|
|
||||||
// Make the new query. No credential headers on the first load to keep things quick
|
// Make the new query. No credential headers on the first load to keep things quick
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '/api/v2/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
|
url: '/api/v3/spots' + buildQueryString(), dataType: 'json', success: function (jsonData) {
|
||||||
// Store data
|
// Store data
|
||||||
spots = jsonData;
|
spots = jsonData;
|
||||||
// Update table
|
// Update table
|
||||||
@@ -43,7 +43,7 @@ function startSSEConnection() {
|
|||||||
sseAbortController = new AbortController();
|
sseAbortController = new AbortController();
|
||||||
|
|
||||||
// SSE is going to fetch only a few spots at a time, so now we include QRZ/HamQTH credentials because the delay won't be significant.
|
// SSE is going to fetch only a few spots at a time, so now we include QRZ/HamQTH credentials because the delay won't be significant.
|
||||||
fetchEventSource('/api/v2/spots/stream' + buildQueryString(), {
|
fetchEventSource('/api/v3/spots/stream' + buildQueryString(), {
|
||||||
headers: getCredentialHeaders(),
|
headers: getCredentialHeaders(),
|
||||||
signal: sseAbortController.signal,
|
signal: sseAbortController.signal,
|
||||||
openWhenHidden: true,
|
openWhenHidden: true,
|
||||||
@@ -100,7 +100,7 @@ function startSSEConnection() {
|
|||||||
// Build a query string for the API, based on the filters that the user has selected.
|
// Build a query string for the API, based on the filters that the user has selected.
|
||||||
function buildQueryString() {
|
function buildQueryString() {
|
||||||
let str = "?";
|
let str = "?";
|
||||||
["dx_continent", "de_continent", "mode", "source", "band", "sig"].forEach(fn => {
|
["dx_continent", "de_continent", "mode", "source", "band", "activity"].forEach(fn => {
|
||||||
if (!allFilterOptionsSelected(fn)) {
|
if (!allFilterOptionsSelected(fn)) {
|
||||||
str = str + getQueryStringFor(fn) + "&";
|
str = str + getQueryStringFor(fn) + "&";
|
||||||
}
|
}
|
||||||
@@ -329,19 +329,19 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
|
|
||||||
// Format activity
|
// Format activity
|
||||||
let activityText = "General DX";
|
let activityText = "General DX";
|
||||||
if (s["sig"]) {
|
if (s["activity"]) {
|
||||||
activityText = s["sig"];
|
activityText = s["activity"];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format activity refs
|
// Format activity refs
|
||||||
let activityRefs = "";
|
let activityRefs = "";
|
||||||
if (s["sig_refs"] != null) {
|
if (s["activity_refs"] != null) {
|
||||||
const items = [];
|
const items = [];
|
||||||
for (let i = 0; i < s["sig_refs"].length; i++) {
|
for (let i = 0; i < s["activity_refs"].length; i++) {
|
||||||
if (s["sig_refs"][i]["url"] != null) {
|
if (s["activity_refs"][i]["url"] != null) {
|
||||||
items[i] = `<span style="white-space: nowrap;"><a href='${encodeURI(s["sig_refs"][i]["url"])}' title='${escapeHtml(s["sig_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(s["sig_refs"][i]["id"])}</a></span>`
|
items[i] = `<span style="white-space: nowrap;"><a href='${encodeURI(s["activity_refs"][i]["url"])}' title='${escapeHtml(s["activity_refs"][i]["name"])}' target='_new' class='activity-ref-link'>${escapeHtml(s["activity_refs"][i]["id"])}</a></span>`
|
||||||
} else {
|
} else {
|
||||||
items[i] = `<span style="white-space: nowrap;">${escapeHtml(s["sig_refs"][i]["id"])}</span>`
|
items[i] = `<span style="white-space: nowrap;">${escapeHtml(s["activity_refs"][i]["id"])}</span>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
activityRefs = items.join(", ");
|
activityRefs = items.join(", ");
|
||||||
@@ -464,7 +464,7 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
// Load server options. Once a successful callback is made from this, we then query spots and set up the timer to query
|
||||||
// spots repeatedly.
|
// spots repeatedly.
|
||||||
function loadOptions() {
|
function loadOptions() {
|
||||||
$.getJSON('/api/v2/options', function (jsonData) {
|
$.getJSON('/api/v3/options', function (jsonData) {
|
||||||
// Store options
|
// Store options
|
||||||
options = jsonData;
|
options = jsonData;
|
||||||
|
|
||||||
@@ -478,7 +478,7 @@ function loadOptions() {
|
|||||||
|
|
||||||
// Populate the filters panel
|
// Populate the filters panel
|
||||||
generateBandsMultiToggleFilterCard(options["bands"]);
|
generateBandsMultiToggleFilterCard(options["bands"]);
|
||||||
generateActivitiesMultiToggleFilterCard(options["sigs"]);
|
generateActivitiesMultiToggleFilterCard(options["activities"]);
|
||||||
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
generateMultiToggleFilterCard("#dx_continent_options", "dx_continent", options["continents"]);
|
||||||
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
generateMultiToggleFilterCard("#de_continent_options", "de_continent", options["continents"]);
|
||||||
generateModesMultiToggleFilterCard(options["modes"]);
|
generateModesMultiToggleFilterCard(options["modes"]);
|
||||||
|
|||||||
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
// Load server status
|
// Load server status
|
||||||
function loadStatus() {
|
function loadStatus() {
|
||||||
$.getJSON('/api/v2/status', function (jsonData) {
|
$.getJSON('/api/v3/status', function (jsonData) {
|
||||||
$("#software_version").text(jsonData["software_version"]);
|
$("#software_version").text(jsonData["software_version"]);
|
||||||
$("#server_owner_callsign").text(jsonData["server_owner_callsign"]);
|
$("#server_owner_callsign").text(jsonData["server_owner_callsign"]);
|
||||||
$("#up-since").text(moment().subtract(jsonData["uptime"], 'seconds').fromNow());
|
$("#up-since").text(moment().subtract(jsonData["uptime"], 'seconds').fromNow());
|
||||||
@@ -54,10 +54,10 @@ function loadStatus() {
|
|||||||
</div>`);
|
</div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
jsonData["sig_ref_data_providers"].forEach(p => {
|
jsonData["activity_ref_data_providers"].forEach(p => {
|
||||||
$("#sig_ref_data_providers-status-container").append(`
|
$("#activity_ref_data_providers-status-container").append(`
|
||||||
<div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
|
<div class="row row-cols-1 row-cols-md-4 g-4 mb-4 mb-md-2">
|
||||||
<div class="col"><strong>${p["sig_name"]}</strong></div>
|
<div class="col"><strong>${p["activity_name"]}</strong></div>
|
||||||
<div class="col">Status: ${p["status"]}</div>
|
<div class="col">Status: ${p["status"]}</div>
|
||||||
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
|
<div class="col">Last updated: ${(p["enabled"] && p["last_updated"] > 0) ? moment.unix(p["last_updated"]).utc().fromNow() : "N/A"}</div>
|
||||||
<div class="col">References: ${p["enabled"] ? p["reference_count"] : "N/A"}</div>
|
<div class="col">References: ${p["enabled"] ? p["reference_count"] : "N/A"}</div>
|
||||||
|
|||||||
@@ -41,14 +41,14 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="sig" class="form-label">Activity</label>
|
<label for="activity" class="form-label">Activity</label>
|
||||||
<select id="sig" class="form-select">
|
<select id="activity" class="form-select">
|
||||||
<option value="" selected></option>
|
<option value="" selected></option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="sig-ref" class="form-label">Activity Reference</label>
|
<label for="activity-ref" class="form-label">Activity Reference</label>
|
||||||
<input type="text" class="form-control input-narrow" id="sig-ref" placeholder="e.g. GB-0001">
|
<input type="text" class="form-control input-narrow" id="activity-ref" placeholder="e.g. GB-0001">
|
||||||
</div>
|
</div>
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="dx-grid" class="form-label">DX Grid</label>
|
<label for="dx-grid" class="form-label">DX Grid</label>
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
which you can automatically use to generate a client skeleton using various software.
|
which you can automatically use to generate a client skeleton using various software.
|
||||||
</li>
|
</li>
|
||||||
<li>Call the main "spots" or "alerts" API endpoints to get the data you want. For example, your app could call
|
<li>Call the main "spots" or "alerts" API endpoints to get the data you want. For example, your app could call
|
||||||
<code>https://spothole.app/api/v2/spots</code> once every few minutes. Apply filters if necessary.
|
<code>https://spothole.app/api/v3/spots</code> once every few minutes. Apply filters if necessary.
|
||||||
</li>
|
</li>
|
||||||
<li>Call the "options" API to get an idea of which bands, modes etc. the server knows about. You might want to do
|
<li>Call the "options" API to get an idea of which bands, modes etc. the server knows about. You might want to do
|
||||||
that first before calling the spots/alerts APIs, to allow you to populate your filters correctly.
|
that first before calling the spots/alerts APIs, to allow you to populate your filters correctly.
|
||||||
@@ -38,11 +38,11 @@
|
|||||||
once every two minutes, so if your client is interested in POTA data there's no need to poll Spothole any more often
|
once every two minutes, so if your client is interested in POTA data there's no need to poll Spothole any more often
|
||||||
than that.</p>
|
than that.</p>
|
||||||
<p>If you absolutely must be informed within seconds of a spot arriving in Spothole, please use the SSE endpoints
|
<p>If you absolutely must be informed within seconds of a spot arriving in Spothole, please use the SSE endpoints
|
||||||
instead, e.g. <code>https://spothole.app/api/v2/spots/stream</code>.</p>
|
instead, e.g. <code>https://spothole.app/api/v3/spots/stream</code>.</p>
|
||||||
<p>If you want to handle different types of spot or alert differently within your client, please consider making a
|
<p>If you want to handle different types of spot or alert differently within your client, please consider making a
|
||||||
single request to the Spothole API to retrieve all the data, then filtering on your side. For example, call
|
single request to the Spothole API to retrieve all the data, then filtering on your side. For example, call
|
||||||
<code>https://spothole.app/api/v2/spots?sig=POTA,SOTA</code> rather than making two separate calls to
|
<code>https://spothole.app/api/v3/spots?activity=POTA,SOTA</code> rather than making two separate calls to
|
||||||
<code>https://spothole.app/api/v2/spots?sig=POTA</code> and <code>https://spothole.app/api/v2/spots?sig=SOTA</code>.
|
<code>https://spothole.app/api/v3/spots?activity=POTA</code> and <code>https://spothole.app/api/v3/spots?activity=SOTA</code>.
|
||||||
</p>
|
</p>
|
||||||
<p>Remember, here at Spothole Inc. we offer an industry-standard "five nines" uptime on our server, with our own unique
|
<p>Remember, here at Spothole Inc. we offer an industry-standard "five nines" uptime on our server, with our own unique
|
||||||
twist: we don't tell you which side of the decimal point the nines start! (Translation: This is a hobby project.
|
twist: we don't tell you which side of the decimal point the nines start! (Translation: This is a hobby project.
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<p>These are supplied with the URL to the page you want to embed, for example for an embedded version of the band map in
|
<p>These are supplied with the URL to the page you want to embed, for example for an embedded version of the band map in
|
||||||
dark mode, use <code>https://spothole.app/bands?embedded=true&dark-mode=true</code>. For an embedded version of
|
dark mode, use <code>https://spothole.app/bands?embedded=true&dark-mode=true</code>. For an embedded version of
|
||||||
the main spots/home page in the system light/dark mode, use <code>https://spothole.app/?embedded=true</code>. For
|
the main spots/home page in the system light/dark mode, use <code>https://spothole.app/?embedded=true</code>. For
|
||||||
dark mode showing 70cm TOTA spots only, use <code>https://spothole.app/?embedded=true&dark-mode=true&sig=TOTA&band=70cm</code>.
|
dark mode showing 70cm TOTA spots only, use <code>https://spothole.app/?embedded=true&dark-mode=true&activity=TOTA&band=70cm</code>.
|
||||||
Providing no URL params causes the page to be loaded in the normal way it would when accessed directly in the user's
|
Providing no URL params causes the page to be loaded in the normal way it would when accessed directly in the user's
|
||||||
browser.</p>
|
browser.</p>
|
||||||
<p>The supported parameters are as follows. Generally these match the equivalent parameters in the real Spothole API,
|
<p>The supported parameters are as follows. Generally these match the equivalent parameters in the real Spothole API,
|
||||||
@@ -82,10 +82,10 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td><code>sig</code></td>
|
<td><code>activity</code></td>
|
||||||
<td>Comma-separated list</td>
|
<td>Comma-separated list</td>
|
||||||
<td>(all)</td>
|
<td>(all)</td>
|
||||||
<td><code>?sig=POTA,SOTA,NO_SIG</code></td>
|
<td><code>?activity=POTA,SOTA,NO_ACTIVITY</code></td>
|
||||||
<td>Sets the list of activities that will be shown on the spots, bands and map pages. Available options
|
<td>Sets the list of activities that will be shown on the spots, bands and map pages. Available options
|
||||||
match the labels of the buttons in the standard web interface.
|
match the labels of the buttons in the standard web interface.
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
Activity Reference Data Providers
|
Activity Reference Data Providers
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body" id="sig_ref_data_providers-status-container">
|
<div class="card-body" id="activity_ref_data_providers-status-container">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ RECAPTCHA_VERIFY_URL = "https://www.google.com/recaptcha/api/siteverify"
|
|||||||
|
|
||||||
|
|
||||||
class APISpotHandler(tornado.web.RequestHandler):
|
class APISpotHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/spot (POST)"""
|
"""API request handler for /api/v3/spot (POST)"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -144,17 +144,17 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
# Reject if activity ref format incorrect for activity
|
# Reject if activity ref format incorrect for activity
|
||||||
if (
|
if (
|
||||||
spot.sig
|
spot.activity
|
||||||
and spot.sig_refs
|
and spot.activity_refs
|
||||||
and len(spot.sig_refs) > 0
|
and len(spot.activity_refs) > 0
|
||||||
and spot.sig_refs[0].id
|
and spot.activity_refs[0].id
|
||||||
and get_ref_regex_for_activity(spot.sig)
|
and get_ref_regex_for_activity(spot.activity)
|
||||||
and not re.match(get_ref_regex_for_activity(spot.sig), spot.sig_refs[0].id)
|
and not re.match(get_ref_regex_for_activity(spot.activity), spot.activity_refs[0].id)
|
||||||
):
|
):
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(
|
self.write(
|
||||||
safe_json_dumps(
|
safe_json_dumps(
|
||||||
f"Error - '{spot.sig_refs[0].id}' does not look like a valid reference for {spot.sig}."
|
f"Error - '{spot.activity_refs[0].id}' does not look like a valid reference for {spot.activity}."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
@@ -171,13 +171,13 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
# Validate upstream submission requirements
|
# Validate upstream submission requirements
|
||||||
if submit_upstream and upstream_provider_name:
|
if submit_upstream and upstream_provider_name:
|
||||||
if not spot.sig:
|
if not spot.activity:
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(safe_json_dumps("Error - an activity must be selected to submit upstream."))
|
self.write(safe_json_dumps("Error - an activity must be selected to submit upstream."))
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
if not spot.sig_refs and upstream_provider_name != "Tiles":
|
if not spot.activity_refs and upstream_provider_name != "Tiles":
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(safe_json_dumps("Error - an activity reference is required to submit upstream."))
|
self.write(safe_json_dumps("Error - an activity reference is required to submit upstream."))
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
@@ -201,7 +201,7 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
# Submit upstream if requested
|
# Submit upstream if requested
|
||||||
upstream_warning = None
|
upstream_warning = None
|
||||||
if submit_upstream and upstream_provider_name:
|
if submit_upstream and upstream_provider_name:
|
||||||
provider = self._find_provider(upstream_provider_name, spot.sig)
|
provider = self._find_provider(upstream_provider_name, spot.activity)
|
||||||
if provider:
|
if provider:
|
||||||
try:
|
try:
|
||||||
# Submit spot to the upstream provider
|
# Submit spot to the upstream provider
|
||||||
@@ -216,7 +216,7 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
f"Spot was saved locally but upstream submission to {upstream_provider_name} failed."
|
f"Spot was saved locally but upstream submission to {upstream_provider_name} failed."
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
upstream_warning = f"No enabled provider named '{upstream_provider_name}' supports upstream submission for {spot.sig if spot.sig else ''} spots."
|
upstream_warning = f"No enabled provider named '{upstream_provider_name}' supports upstream submission for {spot.activity if spot.activity else ''} spots."
|
||||||
|
|
||||||
# If we successfully submitted the spot upstream, don't add it direct to Spothole, otherwise it will be a
|
# If we successfully submitted the spot upstream, don't add it direct to Spothole, otherwise it will be a
|
||||||
# duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or
|
# duplicate with what immediately comes back from the API. But if we weren't asked to send it upstream, or
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class APIAlertsHandler(tornado.web.RequestHandler):
|
class APIAlertsHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/alerts"""
|
"""API request handler for /api/v3/alerts"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -69,7 +69,7 @@ class APIAlertsHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
class APIAlertsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
||||||
"""API request handler for /api/v2/alerts/stream"""
|
"""API request handler for /api/v3/alerts/stream"""
|
||||||
|
|
||||||
def __init__(self, application, request, **kwargs: Any):
|
def __init__(self, application, request, **kwargs: Any):
|
||||||
self._sse_alert_broadcaster = None
|
self._sse_alert_broadcaster = None
|
||||||
@@ -169,13 +169,13 @@ def alert_allowed_by_query(alert, query):
|
|||||||
# the alert is a dxpedition, or contests_skip_max_duration_check and the alert is a contest, it also
|
# the alert is a dxpedition, or contests_skip_max_duration_check and the alert is a contest, it also
|
||||||
# always passes the check.
|
# always passes the check.
|
||||||
if (
|
if (
|
||||||
alert.sig == ActivityName.DXPEDITION
|
alert.activity == ActivityName.DXPEDITION
|
||||||
and "dxpeditions_skip_max_duration_check" in query
|
and "dxpeditions_skip_max_duration_check" in query
|
||||||
and query.get("dxpeditions_skip_max_duration_check").upper() == "TRUE"
|
and query.get("dxpeditions_skip_max_duration_check").upper() == "TRUE"
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if (
|
if (
|
||||||
alert.sig == ActivityName.CONTEST
|
alert.activity == ActivityName.CONTEST
|
||||||
and "contests_skip_max_duration_check" in query
|
and "contests_skip_max_duration_check" in query
|
||||||
and query.get("contests_skip_max_duration_check").upper() == "TRUE"
|
and query.get("contests_skip_max_duration_check").upper() == "TRUE"
|
||||||
):
|
):
|
||||||
@@ -186,14 +186,14 @@ def alert_allowed_by_query(alert, query):
|
|||||||
sources = query.get(k).split(",")
|
sources = query.get(k).split(",")
|
||||||
if not alert.source or alert.source not in sources:
|
if not alert.source or alert.source not in sources:
|
||||||
return False
|
return False
|
||||||
case "sig":
|
case "activity":
|
||||||
# If a list of activities is provided, the alert must have an activity and it must match one of them.
|
# If a list of activities is provided, the alert must have an activity and it must match one of them.
|
||||||
# The special activity "NO_SIG", when supplied in the list, matches alerts with no activity.
|
# The special activity "NO_ACTIVITY", when supplied in the list, matches alerts with no activity.
|
||||||
activities = query.get(k).split(",")
|
activities = query.get(k).split(",")
|
||||||
include_no_activity = "NO_SIG" in activities
|
include_no_activity = "NO_ACTIVITY" in activities
|
||||||
if not alert.sig and not include_no_activity:
|
if not alert.activity and not include_no_activity:
|
||||||
return False
|
return False
|
||||||
if alert.sig and alert.sig not in activities:
|
if alert.activity and alert.activity not in activities:
|
||||||
return False
|
return False
|
||||||
case "dx_continent":
|
case "dx_continent":
|
||||||
dxconts = query.get(k).split(",")
|
dxconts = query.get(k).split(",")
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ HF_BANDS = [b.name for b in BANDS if b.is_ham_hf]
|
|||||||
|
|
||||||
|
|
||||||
class APIDxStatsHandler(tornado.web.RequestHandler):
|
class APIDxStatsHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/dxstats"""
|
"""API request handler for /api/v3/dxstats"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class APILookupCallHandler(tornado.web.RequestHandler):
|
class APILookupCallHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/lookup/call"""
|
"""API request handler for /api/v3/lookup/call"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -63,7 +63,7 @@ class APILookupCallHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class APILookupActivityRefHandler(tornado.web.RequestHandler):
|
class APILookupActivityRefHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/lookup/sigref"""
|
"""API request handler for /api/v3/lookup/activityref"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -79,16 +79,16 @@ class APILookupActivityRefHandler(tornado.web.RequestHandler):
|
|||||||
# reduce that to just the first entry, and convert bytes to string
|
# reduce that to just the first entry, and convert bytes to string
|
||||||
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
query_params = {k: v[0].decode("utf-8") for k, v in self.request.arguments.items()}
|
||||||
|
|
||||||
# "sig" and "id" query params must exist, the activity must be known, and if we have a reference regex for
|
# "activity" and "id" query params must exist, the activity must be known, and if we have a reference regex for
|
||||||
# that activity, the provided id must match it.
|
# that activity, the provided id must match it.
|
||||||
if "sig" in query_params and "id" in query_params:
|
if "activity" in query_params and "id" in query_params:
|
||||||
activity = str(query_params.get("sig")).upper()
|
activity = str(query_params.get("activity")).upper()
|
||||||
ref_id = str(query_params.get("id")).upper()
|
ref_id = str(query_params.get("id")).upper()
|
||||||
if get_activity_by_name(activity):
|
if get_activity_by_name(activity):
|
||||||
if not get_ref_regex_for_activity(activity) or re.match(
|
if not get_ref_regex_for_activity(activity) or re.match(
|
||||||
get_ref_regex_for_activity(activity), ref_id
|
get_ref_regex_for_activity(activity), ref_id
|
||||||
):
|
):
|
||||||
data = populate_missing_activity_ref_info(ActivityRef(id=ref_id, sig=activity))
|
data = populate_missing_activity_ref_info(ActivityRef(id=ref_id, activity=activity))
|
||||||
self.write(safe_json_dumps(data))
|
self.write(safe_json_dumps(data))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -99,10 +99,10 @@ class APILookupActivityRefHandler(tornado.web.RequestHandler):
|
|||||||
)
|
)
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
else:
|
else:
|
||||||
self.write(safe_json_dumps(f"Error - sig '{activity}' is not known."))
|
self.write(safe_json_dumps(f"Error - activity '{activity}' is not known."))
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
else:
|
else:
|
||||||
self.write(safe_json_dumps("Error - sig and id must be provided"))
|
self.write(safe_json_dumps("Error - activity and id must be provided"))
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -115,7 +115,7 @@ class APILookupActivityRefHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class APILookupGridHandler(tornado.web.RequestHandler):
|
class APILookupGridHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/lookup/grid"""
|
"""API request handler for /api/v3/lookup/grid"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class APIOptionsHandler(tornado.web.RequestHandler):
|
class APIOptionsHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/options"""
|
"""API request handler for /api/v3/options"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -75,7 +75,7 @@ class APIOptionsHandler(tornado.web.RequestHandler):
|
|||||||
"bands": BANDS,
|
"bands": BANDS,
|
||||||
"modes": [m.value for m in Mode],
|
"modes": [m.value for m in Mode],
|
||||||
"mode_types": [t.value for t in ModeType],
|
"mode_types": [t.value for t in ModeType],
|
||||||
"sigs": list(ACTIVITIES.values()),
|
"activities": list(ACTIVITIES.values()),
|
||||||
"spot_providers": spot_providers,
|
"spot_providers": spot_providers,
|
||||||
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
|
"spot_providers_enabled_by_default": spot_providers_enabled_by_default,
|
||||||
"alert_providers": alert_providers,
|
"alert_providers": alert_providers,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class APISolarConditionsHandler(tornado.web.RequestHandler):
|
class APISolarConditionsHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/solar"""
|
"""API request handler for /api/v3/solar"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class APISpotsHandler(tornado.web.RequestHandler):
|
class APISpotsHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/spots"""
|
"""API request handler for /api/v3/spots"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -68,7 +68,7 @@ class APISpotsHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
|
|
||||||
class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
class APISpotsStreamHandler(tornado_eventsource.handler.EventSourceHandler):
|
||||||
"""API request handler for /api/v2/spots/stream"""
|
"""API request handler for /api/v3/spots/stream"""
|
||||||
|
|
||||||
def __init__(self, application, request, **kwargs: Any):
|
def __init__(self, application, request, **kwargs: Any):
|
||||||
self._sse_spot_broadcaster = None
|
self._sse_spot_broadcaster = None
|
||||||
@@ -196,25 +196,25 @@ def spot_allowed_by_query(spot, query):
|
|||||||
sources = query.get(k).split(",")
|
sources = query.get(k).split(",")
|
||||||
if not spot.source or spot.source not in sources:
|
if not spot.source or spot.source not in sources:
|
||||||
return False
|
return False
|
||||||
case "sig":
|
case "activity":
|
||||||
# If a list of activities is provided, the spot must have an activity and it must match one of them.
|
# If a list of activities is provided, the spot must have an activity and it must match one of them.
|
||||||
# The special activity "NO_SIG", when supplied in the list, matches spots with no activity.
|
# The special activity "NO_ACTIVITY", when supplied in the list, matches spots with no activity.
|
||||||
activities = query.get(k).split(",")
|
activities = query.get(k).split(",")
|
||||||
include_no_activity = "NO_SIG" in activities
|
include_no_activity = "NO_ACTIVITY" in activities
|
||||||
if not spot.sig and not include_no_activity:
|
if not spot.activity and not include_no_activity:
|
||||||
return False
|
return False
|
||||||
if spot.sig and spot.sig not in activities:
|
if spot.activity and spot.activity not in activities:
|
||||||
return False
|
return False
|
||||||
case "needs_sig":
|
case "needs_activity":
|
||||||
# If true, an activity is required, regardless of what it is, it just can't be missing. Mutually
|
# If true, an activity is required, regardless of what it is, it just can't be missing. Mutually
|
||||||
# exclusive with supplying the special "NO_SIG" parameter to the "sig" query param.
|
# exclusive with supplying the special "NO_ACTIVITY" parameter to the "activity" query param.
|
||||||
needs_activity = query.get(k).upper() == "TRUE"
|
needs_activity = query.get(k).upper() == "TRUE"
|
||||||
if needs_activity and not spot.sig:
|
if needs_activity and not spot.activity:
|
||||||
return False
|
return False
|
||||||
case "needs_sig_ref":
|
case "needs_activity_ref":
|
||||||
# If true, at least one activity ref is required, regardless of what it is, it just can't be missing.
|
# If true, at least one activity ref is required, regardless of what it is, it just can't be missing.
|
||||||
needs_activity_ref = query.get(k).upper() == "TRUE"
|
needs_activity_ref = query.get(k).upper() == "TRUE"
|
||||||
if needs_activity_ref and (not spot.sig_refs or len(spot.sig_refs) == 0):
|
if needs_activity_ref and (not spot.activity_refs or len(spot.activity_refs) == 0):
|
||||||
return False
|
return False
|
||||||
case "band":
|
case "band":
|
||||||
bands = query.get(k).split(",")
|
bands = query.get(k).split(",")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class APIStatusHandler(tornado.web.RequestHandler):
|
class APIStatusHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v2/status"""
|
"""API request handler for /api/v3/status"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ from core.config import ALLOW_SPOTTING
|
|||||||
from core.constants import UNKNOWN_BAND
|
from core.constants import UNKNOWN_BAND
|
||||||
from core.utils import infer_band_from_freq, safe_json_dumps
|
from core.utils import infer_band_from_freq, safe_json_dumps
|
||||||
from data.spot import Spot
|
from data.spot import Spot
|
||||||
|
from webserver.handlers.api.v2_compatibility import translate_v2_spot
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class V1APISpotHandler(tornado.web.RequestHandler):
|
class V1APISpotHandler(tornado.web.RequestHandler):
|
||||||
"""API request handler for /api/v1/spot (POST). Included in early Spothole v2 for backwards compatibility."""
|
"""API request handler for /api/v1/spot (POST). Included in Spothole v2 onwards for backwards
|
||||||
|
compatibility."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -57,8 +59,9 @@ class V1APISpotHandler(tornado.web.RequestHandler):
|
|||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Read in the request body as JSON then convert to a Spot object
|
# Read in the request body as JSON then convert to a Spot object. The v1 spot format uses the same field
|
||||||
json_spot = tornado.escape.json_decode(post_data)
|
# names as v2, so needs the same translation to v3 field names.
|
||||||
|
json_spot = translate_v2_spot(tornado.escape.json_decode(post_data))
|
||||||
spot = Spot(**json_spot)
|
spot = Spot(**json_spot)
|
||||||
|
|
||||||
# Reject if no timestamp, frequency, dx_call or de_call
|
# Reject if no timestamp, frequency, dx_call or de_call
|
||||||
@@ -106,17 +109,17 @@ class V1APISpotHandler(tornado.web.RequestHandler):
|
|||||||
|
|
||||||
# Reject if activity ref format incorrect for activity
|
# Reject if activity ref format incorrect for activity
|
||||||
if (
|
if (
|
||||||
spot.sig
|
spot.activity
|
||||||
and spot.sig_refs
|
and spot.activity_refs
|
||||||
and len(spot.sig_refs) > 0
|
and len(spot.activity_refs) > 0
|
||||||
and spot.sig_refs[0].id
|
and spot.activity_refs[0].id
|
||||||
and get_ref_regex_for_activity(spot.sig)
|
and get_ref_regex_for_activity(spot.activity)
|
||||||
and not re.match(get_ref_regex_for_activity(spot.sig), spot.sig_refs[0].id)
|
and not re.match(get_ref_regex_for_activity(spot.activity), spot.activity_refs[0].id)
|
||||||
):
|
):
|
||||||
self.set_status(422)
|
self.set_status(422)
|
||||||
self.write(
|
self.write(
|
||||||
safe_json_dumps(
|
safe_json_dumps(
|
||||||
f"Error - '{spot.sig_refs[0].id}' does not look like a valid reference for {spot.sig}."
|
f"Error - '{spot.activity_refs[0].id}' does not look like a valid reference for {spot.activity}."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from webserver.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
from webserver.handlers.api.v2_compatibility import V2APISpotsHandler, V2APISpotsStreamHandler
|
||||||
|
|
||||||
_GRID_SOURCE_RE = re.compile(r'"dx_location_source":\s*"GRID"')
|
_GRID_SOURCE_RE = re.compile(r'"dx_location_source":\s*"GRID"')
|
||||||
_LEGACY_PARAM_TO_HEADER_MAP = {
|
_LEGACY_PARAM_TO_HEADER_MAP = {
|
||||||
@@ -24,8 +24,9 @@ def _handle_legacy_params(handler):
|
|||||||
handler.request.headers[header] = value
|
handler.request.headers[header] = value
|
||||||
|
|
||||||
|
|
||||||
class V1APISpotsHandler(APISpotsHandler):
|
class V1APISpotsHandler(V2APISpotsHandler):
|
||||||
"""API request handler for /api/v1/spots (GET). Included in early Spothole v2 for backwards compatibility."""
|
"""API request handler for /api/v1/spots (GET). Included in Spothole v2 onwards for backwards
|
||||||
|
compatibility."""
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
_handle_legacy_params(self)
|
_handle_legacy_params(self)
|
||||||
@@ -37,8 +38,9 @@ class V1APISpotsHandler(APISpotsHandler):
|
|||||||
super().write(chunk)
|
super().write(chunk)
|
||||||
|
|
||||||
|
|
||||||
class V1APISpotsStreamHandler(APISpotsStreamHandler):
|
class V1APISpotsStreamHandler(V2APISpotsStreamHandler):
|
||||||
"""API request handler for /api/v1/spots/stream (SSE). Included in early Spothole v2 for backwards compatibility."""
|
"""API request handler for /api/v1/spots/stream (SSE). Included in Spothole v2 onwards for backwards
|
||||||
|
compatibility."""
|
||||||
|
|
||||||
def prepare(self):
|
def prepare(self):
|
||||||
_handle_legacy_params(self)
|
_handle_legacy_params(self)
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import tornado
|
||||||
|
|
||||||
|
from core.utils import safe_json_dumps
|
||||||
|
from webserver.handlers.api.addspot import APISpotHandler
|
||||||
|
from webserver.handlers.api.alerts import APIAlertsHandler, APIAlertsStreamHandler
|
||||||
|
from webserver.handlers.api.lookups import APILookupActivityRefHandler
|
||||||
|
from webserver.handlers.api.options import APIOptionsHandler
|
||||||
|
from webserver.handlers.api.spots import APISpotsHandler, APISpotsStreamHandler
|
||||||
|
from webserver.handlers.api.status import APIStatusHandler
|
||||||
|
|
||||||
|
# Query parameters renamed in v3
|
||||||
|
_V2_TO_V3_QUERY_PARAMS = {
|
||||||
|
"sig": "activity",
|
||||||
|
"needs_sig": "needs_activity",
|
||||||
|
"needs_sig_ref": "needs_activity_ref",
|
||||||
|
}
|
||||||
|
# Values of query parameters renamed in v3
|
||||||
|
_V2_TO_V3_QUERY_VALUES = {
|
||||||
|
"activity": {"NO_SIG": "NO_ACTIVITY"},
|
||||||
|
"fields": {"sig": "activity", "sig_refs": "activity_refs"},
|
||||||
|
}
|
||||||
|
# Keys of JSON objects in API responses renamed in v3
|
||||||
|
_V3_TO_V2_RESPONSE_KEYS = {
|
||||||
|
"activity": "sig",
|
||||||
|
"activity_refs": "sig_refs",
|
||||||
|
"activity_type": "sig_type",
|
||||||
|
"activities": "sigs",
|
||||||
|
"activity_ref_data_providers": "sig_ref_data_providers",
|
||||||
|
"activity_name": "sig_name",
|
||||||
|
}
|
||||||
|
# Values of JSON objects in API responses renamed in v3
|
||||||
|
_V3_TO_V2_RESPONSE_VALUES = {
|
||||||
|
"dx_location_source": {"ACTIVITY REF LOOKUP": "SIG REF LOOKUP"},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _translate_v2_query_params(handler):
|
||||||
|
"""Rename any v2 query parameters (and values) in the request to their v3 equivalents, so the v3 handler can
|
||||||
|
understand them."""
|
||||||
|
|
||||||
|
for arguments in (handler.request.arguments, handler.request.query_arguments):
|
||||||
|
for v2_name, v3_name in _V2_TO_V3_QUERY_PARAMS.items():
|
||||||
|
if v2_name in arguments:
|
||||||
|
arguments[v3_name] = arguments.pop(v2_name)
|
||||||
|
for name, value_map in _V2_TO_V3_QUERY_VALUES.items():
|
||||||
|
if name in arguments:
|
||||||
|
arguments[name] = [
|
||||||
|
",".join(value_map.get(item.strip(), item) for item in v.decode("utf-8").split(",")).encode("utf-8")
|
||||||
|
for v in arguments[name]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _translate_v3_response_object(obj):
|
||||||
|
"""Rename keys and values in an object from their v3 names to their v2 names."""
|
||||||
|
|
||||||
|
if isinstance(obj, dict):
|
||||||
|
translated = {}
|
||||||
|
for k, v in obj.items():
|
||||||
|
if k in _V3_TO_V2_RESPONSE_VALUES and isinstance(v, str):
|
||||||
|
v = _V3_TO_V2_RESPONSE_VALUES[k].get(v, v)
|
||||||
|
translated[_V3_TO_V2_RESPONSE_KEYS.get(k, k)] = _translate_v3_response_object(v)
|
||||||
|
return translated
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [_translate_v3_response_object(i) for i in obj]
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def translate_v3_response(chunk):
|
||||||
|
"""Translate a JSON string output by a v3 handler into its v2 equivalent"""
|
||||||
|
|
||||||
|
if not isinstance(chunk, str):
|
||||||
|
return chunk
|
||||||
|
try:
|
||||||
|
return safe_json_dumps(_translate_v3_response_object(json.loads(chunk)))
|
||||||
|
except ValueError:
|
||||||
|
return chunk
|
||||||
|
|
||||||
|
|
||||||
|
def translate_v2_spot(spot_data):
|
||||||
|
"""Translate a spot provided by a client in v2 format into v3 format"""
|
||||||
|
|
||||||
|
spot_data = dict(spot_data)
|
||||||
|
if "sig" in spot_data:
|
||||||
|
spot_data["activity"] = spot_data.pop("sig")
|
||||||
|
if "sig_refs" in spot_data:
|
||||||
|
spot_data["activity_refs"] = spot_data.pop("sig_refs")
|
||||||
|
if isinstance(spot_data.get("activity_refs"), list):
|
||||||
|
refs = []
|
||||||
|
for ref in spot_data["activity_refs"]:
|
||||||
|
if isinstance(ref, dict) and "sig" in ref:
|
||||||
|
ref = dict(ref)
|
||||||
|
ref["activity"] = ref.pop("sig")
|
||||||
|
refs.append(ref)
|
||||||
|
spot_data["activity_refs"] = refs
|
||||||
|
return spot_data
|
||||||
|
|
||||||
|
|
||||||
|
class _V2ResponseTranslationMixin:
|
||||||
|
"""Mixin for request handlers that translates v2 query params to v3 on the way in, and v3 JSON responses to v2 on
|
||||||
|
the way out"""
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
_translate_v2_query_params(self)
|
||||||
|
super().prepare()
|
||||||
|
|
||||||
|
def write(self, chunk):
|
||||||
|
super().write(translate_v3_response(chunk))
|
||||||
|
|
||||||
|
|
||||||
|
class _V2StreamTranslationMixin:
|
||||||
|
"""Mixin for SSE handlers that translates v2 query params to v3 on the way in, and v3 JSON messages to v2 on the way
|
||||||
|
out"""
|
||||||
|
|
||||||
|
def prepare(self):
|
||||||
|
_translate_v2_query_params(self)
|
||||||
|
super().prepare()
|
||||||
|
|
||||||
|
def write_message(self, name=None, msg=True, wait=None, evt_id=None):
|
||||||
|
if not name:
|
||||||
|
msg = translate_v3_response(msg)
|
||||||
|
return super().write_message(name=name, msg=msg, wait=wait, evt_id=evt_id)
|
||||||
|
|
||||||
|
|
||||||
|
class V2APISpotsHandler(_V2ResponseTranslationMixin, APISpotsHandler):
|
||||||
|
"""API request handler for /api/v2/spots (GET). Included in Spothole v3 for backwards compatibility."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APISpotsStreamHandler(_V2StreamTranslationMixin, APISpotsStreamHandler):
|
||||||
|
"""API request handler for /api/v2/spots/stream (SSE). Included in Spothole v3 for backwards compatibility."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIAlertsHandler(_V2ResponseTranslationMixin, APIAlertsHandler):
|
||||||
|
"""API request handler for /api/v2/alerts (GET). Included in Spothole v3 for backwards compatibility."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIAlertsStreamHandler(_V2StreamTranslationMixin, APIAlertsStreamHandler):
|
||||||
|
"""API request handler for /api/v2/alerts/stream (SSE). Included in Spothole v3 for backwards compatibility."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIOptionsHandler(_V2ResponseTranslationMixin, APIOptionsHandler):
|
||||||
|
"""API request handler for /api/v2/options (GET). Included in Spothole v3 for backwards compatibility."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APIStatusHandler(_V2ResponseTranslationMixin, APIStatusHandler):
|
||||||
|
"""API request handler for /api/v2/status (GET). Included in Spothole v3 for backwards compatibility."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APILookupSigRefHandler(_V2ResponseTranslationMixin, APILookupActivityRefHandler):
|
||||||
|
"""API request handler for /api/v2/lookup/sigref (GET). Included in Spothole v3 for backwards compatibility. This
|
||||||
|
is the v2 equivalent of /api/v3/lookup/activityref."""
|
||||||
|
|
||||||
|
|
||||||
|
class V2APISpotHandler(APISpotHandler):
|
||||||
|
"""API request handler for /api/v2/spot (POST). Included in Spothole v3 for backwards compatibility. Translates the
|
||||||
|
spot in the request body from v2 to v3 format. The response is a plain status message so needs no translation."""
|
||||||
|
|
||||||
|
def post(self):
|
||||||
|
# Translate the request body if we can. If the body is empty or invalid JSON, leave it alone and let the v3
|
||||||
|
# handler return the appropriate error.
|
||||||
|
try:
|
||||||
|
json_body = tornado.escape.json_decode(self.request.body)
|
||||||
|
if isinstance(json_body, dict) and isinstance(json_body.get("spot"), dict):
|
||||||
|
json_body["spot"] = translate_v2_spot(json_body["spot"])
|
||||||
|
self.request.body = json.dumps(json_body).encode("utf-8")
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
super().post()
|
||||||
+76
-11
@@ -30,6 +30,16 @@ from webserver.handlers.api.status import APIStatusHandler
|
|||||||
from webserver.handlers.api.v1_addspot import V1APISpotHandler
|
from webserver.handlers.api.v1_addspot import V1APISpotHandler
|
||||||
from webserver.handlers.api.v1_compatability import V1RedirectHandler
|
from webserver.handlers.api.v1_compatability import V1RedirectHandler
|
||||||
from webserver.handlers.api.v1_spots import V1APISpotsHandler, V1APISpotsStreamHandler
|
from webserver.handlers.api.v1_spots import V1APISpotsHandler, V1APISpotsStreamHandler
|
||||||
|
from webserver.handlers.api.v2_compatibility import (
|
||||||
|
V2APIAlertsHandler,
|
||||||
|
V2APIAlertsStreamHandler,
|
||||||
|
V2APILookupSigRefHandler,
|
||||||
|
V2APIOptionsHandler,
|
||||||
|
V2APISpotHandler,
|
||||||
|
V2APISpotsHandler,
|
||||||
|
V2APISpotsStreamHandler,
|
||||||
|
V2APIStatusHandler,
|
||||||
|
)
|
||||||
from webserver.handlers.manifesthandler import ManifestHandler
|
from webserver.handlers.manifesthandler import ManifestHandler
|
||||||
from webserver.handlers.metrics import PrometheusMetricsHandler
|
from webserver.handlers.metrics import PrometheusMetricsHandler
|
||||||
from webserver.handlers.pagetemplate import PageTemplateHandler
|
from webserver.handlers.pagetemplate import PageTemplateHandler
|
||||||
@@ -107,25 +117,80 @@ class WebServer:
|
|||||||
# API endpoints are always enabled
|
# API endpoints are always enabled
|
||||||
api_routes = [
|
api_routes = [
|
||||||
(
|
(
|
||||||
r"/api/v2/spots",
|
r"/api/v3/spots",
|
||||||
APISpotsHandler,
|
APISpotsHandler,
|
||||||
{"spots": self._data_store.spots},
|
{"spots": self._data_store.spots},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/alerts",
|
r"/api/v3/alerts",
|
||||||
APIAlertsHandler,
|
APIAlertsHandler,
|
||||||
{"alerts": self._data_store.alerts},
|
{"alerts": self._data_store.alerts},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/spots/stream",
|
r"/api/v3/spots/stream",
|
||||||
APISpotsStreamHandler,
|
APISpotsStreamHandler,
|
||||||
{"sse_spot_broadcaster": self._spot_broadcaster},
|
{"sse_spot_broadcaster": self._spot_broadcaster},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/alerts/stream",
|
r"/api/v3/alerts/stream",
|
||||||
APIAlertsStreamHandler,
|
APIAlertsStreamHandler,
|
||||||
{"sse_alert_broadcaster": self._alert_broadcaster},
|
{"sse_alert_broadcaster": self._alert_broadcaster},
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
r"/api/v3/solar",
|
||||||
|
APISolarConditionsHandler,
|
||||||
|
{"solar_conditions": self._data_store.solar_conditions.get()},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v3/dxstats",
|
||||||
|
APIDxStatsHandler,
|
||||||
|
{"spots": self._data_store.spots},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v3/options",
|
||||||
|
APIOptionsHandler,
|
||||||
|
{"status_data": self._data_store.status.get()},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v3/status",
|
||||||
|
APIStatusHandler,
|
||||||
|
{"status_data": self._data_store.status.get()},
|
||||||
|
),
|
||||||
|
(r"/api/v3/lookup/call", APILookupCallHandler),
|
||||||
|
(r"/api/v3/lookup/activityref", APILookupActivityRefHandler),
|
||||||
|
(r"/api/v3/lookup/grid", APILookupGridHandler),
|
||||||
|
(
|
||||||
|
r"/api/v3/spot",
|
||||||
|
APISpotHandler,
|
||||||
|
{
|
||||||
|
"spots": self._data_store.spots,
|
||||||
|
"spot_providers": self._data_providers,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
# v2 API compatibility routes
|
||||||
|
v2_compat_routes = [
|
||||||
|
(
|
||||||
|
r"/api/v2/spots",
|
||||||
|
V2APISpotsHandler,
|
||||||
|
{"spots": self._data_store.spots},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v2/alerts",
|
||||||
|
V2APIAlertsHandler,
|
||||||
|
{"alerts": self._data_store.alerts},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v2/spots/stream",
|
||||||
|
V2APISpotsStreamHandler,
|
||||||
|
{"sse_spot_broadcaster": self._spot_broadcaster},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v2/alerts/stream",
|
||||||
|
V2APIAlertsStreamHandler,
|
||||||
|
{"sse_alert_broadcaster": self._alert_broadcaster},
|
||||||
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/solar",
|
r"/api/v2/solar",
|
||||||
APISolarConditionsHandler,
|
APISolarConditionsHandler,
|
||||||
@@ -138,20 +203,20 @@ class WebServer:
|
|||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/options",
|
r"/api/v2/options",
|
||||||
APIOptionsHandler,
|
V2APIOptionsHandler,
|
||||||
{"status_data": self._data_store.status.get()},
|
{"status_data": self._data_store.status.get()},
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/status",
|
r"/api/v2/status",
|
||||||
APIStatusHandler,
|
V2APIStatusHandler,
|
||||||
{"status_data": self._data_store.status.get()},
|
{"status_data": self._data_store.status.get()},
|
||||||
),
|
),
|
||||||
(r"/api/v2/lookup/call", APILookupCallHandler),
|
(r"/api/v2/lookup/call", APILookupCallHandler),
|
||||||
(r"/api/v2/lookup/sigref", APILookupActivityRefHandler),
|
(r"/api/v2/lookup/sigref", V2APILookupSigRefHandler),
|
||||||
(r"/api/v2/lookup/grid", APILookupGridHandler),
|
(r"/api/v2/lookup/grid", APILookupGridHandler),
|
||||||
(
|
(
|
||||||
r"/api/v2/spot",
|
r"/api/v2/spot",
|
||||||
APISpotHandler,
|
V2APISpotHandler,
|
||||||
{
|
{
|
||||||
"spots": self._data_store.spots,
|
"spots": self._data_store.spots,
|
||||||
"spot_providers": self._data_providers,
|
"spot_providers": self._data_providers,
|
||||||
@@ -159,8 +224,8 @@ class WebServer:
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
# v1 API redirects. Most v1 enpoints are unchanged in v2, and get an HTTP 308 redirect to the v2 API. The ones
|
# v1 API redirects. Most v1 enpoints are unchanged in v2, and are proxied to the v2 API (which in turn is
|
||||||
# that have the major breaking changes get a bespoke handler.
|
# translated from v3). The ones that have the major breaking changes get a bespoke handler.
|
||||||
v1_compat_routes = [
|
v1_compat_routes = [
|
||||||
(
|
(
|
||||||
r"/api/v1/spots",
|
r"/api/v1/spots",
|
||||||
@@ -253,7 +318,7 @@ class WebServer:
|
|||||||
]
|
]
|
||||||
|
|
||||||
app = tornado.web.Application(
|
app = tornado.web.Application(
|
||||||
api_routes + v1_compat_routes + ui_routes + misc_routes,
|
api_routes + v2_compat_routes + v1_compat_routes + ui_routes + misc_routes,
|
||||||
template_path=os.path.join(_HERE, "../templates"),
|
template_path=os.path.join(_HERE, "../templates"),
|
||||||
log_function=request_log,
|
log_function=request_log,
|
||||||
debug=False,
|
debug=False,
|
||||||
|
|||||||
Reference in New Issue
Block a user