Strict adherence to canonical ActivityNames, to avoid the problem where Spothole was giving out activities like ["Towers", "TOWERS"]. All uses of ActivityName should now be canonical not arbitrary strings, and attempts to convert unknown strings to activitynames will be logged for me to check out.

This commit is contained in:
Ian Renton
2026-09-25 09:16:53 +01:00
parent 7417140a3d
commit 760c2412d3
17 changed files with 144 additions and 145 deletions
+9 -3
View File
@@ -1,7 +1,12 @@
import logging
from data.activities import ACTIVITIES from data.activities import ACTIVITIES
from data.activity import Activity
logger = logging.getLogger(__name__)
def get_activity_by_name(name): def get_activity_by_name(name: str | None) -> Activity | None:
"""Utility function to resolve an arbitrary, case-insensitive activity name string (e.g. from a spot comment, a """Utility function to resolve an arbitrary, case-insensitive activity name string (e.g. from a spot comment, a
provider, or an API request) to the matching known Activity. Returns None if no match is found.""" provider, or an API request) to the matching known Activity. Returns None if no match is found."""
@@ -10,10 +15,11 @@ def get_activity_by_name(name):
for activity_name, activity in ACTIVITIES.items(): for activity_name, activity in ACTIVITIES.items():
if activity_name.upper() == name.upper(): if activity_name.upper() == name.upper():
return activity return activity
logger.warning(f"Unknown activity name '{name}', developer may need to add support for this!")
return None return None
def get_ref_regex_for_activity(activity): def get_ref_regex_for_activity(activity: str | None) -> str | None:
"""Utility function to get the regex string for an activity reference for a named activity. If no match is """Utility function to get the regex string for an activity reference for a named activity. If no match is
found, None will be returned.""" found, None will be returned."""
@@ -21,7 +27,7 @@ def get_ref_regex_for_activity(activity):
return found.ref_regex if found else None return found.ref_regex if found else None
def get_icon_for_activity(activity): def get_icon_for_activity(activity: str | None) -> str | None:
"""Utility function to get the icon for a named activity. If no match is found, None will be returned.""" """Utility function to get the icon for a named activity. If no match is found, None will be returned."""
found = get_activity_by_name(activity) found = get_activity_by_name(activity)
+2 -2
View File
@@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from core.enums import ActivityRefType from core.enums import ActivityName, ActivityRefType
@dataclass @dataclass
@@ -9,7 +9,7 @@ class ActivityRef:
name and a lookup URL.""" name and a lookup URL."""
# Activity that this reference is in, e.g. "POTA". # Activity that this reference is in, e.g. "POTA".
activity: str activity: ActivityName
# 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.
+18 -10
View File
@@ -8,10 +8,11 @@ import pytz
from pyhamtools.locator import latlong_to_locator, locator_to_latlong 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_activity_by_name, get_icon_for_activity
from core.call_lookup_helper import get_call_info from core.call_lookup_helper import get_call_info
from core.enums import Continent from core.enums import ActivityName, Continent
from core.utils import get_flag_for_dxcc from core.utils import get_flag_for_dxcc
from data.activity_ref import ActivityRef
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -70,9 +71,9 @@ class Alert:
# e.g. a POTA and WWFF dual activation. This is a list so we can maintain the order items were added, but needs to # e.g. a POTA and WWFF dual activation. This is a list so we can maintain the order items were added, but needs to
# be set-like to avoid dupes, and there's no Python class that handles that properly. So we use a list, but handle # be set-like to avoid dupes, and there's no Python class that handles that properly. So we use a list, but handle
# the uniqueness logic manually, so you must use add_activity() to add to it instead of adding directly. # the uniqueness logic manually, so you must use add_activity() to add to it instead of adding directly.
activities: list = field(default_factory=list) activities: list[ActivityName] = field(default_factory=list)
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. # Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
activity_refs: list = field(default_factory=list) activity_refs: list[ActivityRef] = field(default_factory=list)
# Timing info # Timing info
@@ -96,9 +97,11 @@ class Alert:
icon: str | None = None icon: str | None = None
def __post_init__(self): def __post_init__(self):
"""Normalise the activities list, removing any duplicates while keeping the order.""" """Normalise the activities list, converting any activity names provided as strings to their canonical
ActivityName (dropping any we don't know about) and removing any duplicates while keeping the order."""
self.activities = list(dict.fromkeys(self.activities)) if self.activities else [] found_activities = [get_activity_by_name(activity) for activity in self.activities or []]
self.activities = list(dict.fromkeys(found.name for found in found_activities if found))
def infer_missing(self, credentials=None): def infer_missing(self, credentials=None):
"""Infer missing parameters where possible""" """Infer missing parameters where possible"""
@@ -155,7 +158,7 @@ class Alert:
# Add the activities of any activity refs we have to the alert's list of activities. # Add the activities of any activity refs we have to the alert's list of activities.
for activity_ref in self.activity_refs: for activity_ref in self.activity_refs:
if activity_ref and activity_ref.activity: if activity_ref:
self.add_activity(activity_ref.activity) self.add_activity(activity_ref.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
@@ -197,11 +200,16 @@ class Alert:
except Exception: except Exception:
logger.exception("Exception while inferring missing data from spot") logger.exception("Exception while inferring missing data from spot")
def add_activity(self, activity): def add_activity(self, activity: ActivityName | None):
"""Add an activity to the activities list, so long as it's not blank and not already there. The list is kept in """Add an activity to the activities list, so long as it's not blank and not already there. The list is kept in
insertion order, so the first activity added is treated as the "primary" one.""" insertion order, so the first activity added is treated as the "primary" one. Only canonical ActivityNames are
accepted otherwise we risk sending unknown stuff to API clients."""
if activity and activity not in self.activities: if not activity:
return
if not isinstance(activity, ActivityName):
raise TypeError(f"add_activity() requires an ActivityName, got {type(activity).__name__} {activity!r}")
if activity not in self.activities:
self.activities.append(activity) self.activities.append(activity)
def to_json(self): def to_json(self):
+30 -18
View File
@@ -5,6 +5,7 @@ import re
from dataclasses import dataclass, field from dataclasses import dataclass, field
from datetime import datetime, timedelta from datetime import datetime, timedelta
from math import isnan from math import isnan
from typing import cast
import pytz import pytz
from pyhamtools.locator import latlong_to_locator, locator_to_latlong from pyhamtools.locator import latlong_to_locator, locator_to_latlong
@@ -129,9 +130,9 @@ class Spot:
# e.g. a POTA and WWFF dual activation. This is a list so we can maintain the order items were added, but needs to # e.g. a POTA and WWFF dual activation. This is a list so we can maintain the order items were added, but needs to
# be set-like to avoid dupes, and there's no Python class that handles that properly. So we use a list, but handle # be set-like to avoid dupes, and there's no Python class that handles that properly. So we use a list, but handle
# the uniqueness logic manually, so you must use add_activity() to add to it instead of adding directly. # the uniqueness logic manually, so you must use add_activity() to add to it instead of adding directly.
activities: list = field(default_factory=list) activities: list[ActivityName] = field(default_factory=list)
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. # Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
activity_refs: list = field(default_factory=list) activity_refs: list[ActivityRef] = field(default_factory=list)
# Timing info # Timing info
@@ -161,14 +162,22 @@ 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 activity_refs list, and de-duplicating the activities list.""" objects such as the activity_refs list, and de-duplicating the activities list. Activity names provided as
strings are converted to their canonical ActivityName. Any activities, or activity refs, for an activity we
don't know about are dropped."""
self.activities = list(dict.fromkeys(self.activities)) if self.activities else [] found_activities = [get_activity_by_name(activity) for activity in self.activities or []]
if self.activity_refs: self.activities = list(dict.fromkeys(found.name for found in found_activities if found))
self.activity_refs = [ # When created from JSON, activity refs arrive as dicts rather than ActivityRef objects.
activity_ref if isinstance(activity_ref, ActivityRef) else ActivityRef(**activity_ref) activity_refs = []
for activity_ref in self.activity_refs for activity_ref in cast("list[ActivityRef | dict]", self.activity_refs or []):
] if isinstance(activity_ref, ActivityRef):
activity_refs.append(activity_ref)
elif found_activity := get_activity_by_name(activity_ref.get("activity")):
activity_ref_data = dict(activity_ref)
activity_ref_data["activity"] = found_activity.name
activity_refs.append(ActivityRef(**activity_ref_data))
self.activity_refs = activity_refs
def infer_missing(self, credentials=None): def infer_missing(self, credentials=None):
"""Infer missing parameters where possible""" """Infer missing parameters where possible"""
@@ -274,13 +283,12 @@ class Spot:
# Add the activities of any activity refs we have to the top-level activities list. # Add the activities of any activity refs we have to the top-level activities list.
for activity_ref in self.activity_refs: for activity_ref in self.activity_refs:
if activity_ref.activity: self.add_activity(activity_ref.activity)
self.add_activity(activity_ref.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.activity_refs and self.activity_refs[0].activity: if self.comment and self.activity_refs:
activity = self.activity_refs[0].activity.upper() activity = self.activity_refs[0].activity
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)
@@ -476,7 +484,7 @@ 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.activity_refs: if self.activity_refs:
qth = self.activity_refs[0].id qth = self.activity_refs[0].id or ""
if self.activity_refs[0].name: if self.activity_refs[0].name:
qth += f" {self.activity_refs[0].name}" qth += f" {self.activity_refs[0].name}"
self.dx_qth = qth self.dx_qth = qth
@@ -539,11 +547,16 @@ class Spot:
except Exception: except Exception:
logger.exception("Exception while inferring missing data from spot") logger.exception("Exception while inferring missing data from spot")
def add_activity(self, activity): def add_activity(self, activity: ActivityName | None):
"""Add an activity to the activities list, so long as it's not blank and not already there. The list is kept in """Add an activity to the activities list, so long as it's not blank and not already there. The list is kept in
insertion order, so the first activity added is treated as the "primary" one.""" insertion order, so the first activity added is treated as the "primary" one. Only canonical ActivityNames are
accepted otherwise we risk sending unknown stuff to API clients."""
if activity and activity not in self.activities: if not activity:
return
if not isinstance(activity, ActivityName):
raise TypeError(f"add_activity() requires an ActivityName, got {type(activity).__name__} {activity!r}")
if activity not in self.activities:
self.activities.append(activity) self.activities.append(activity)
def to_json(self): def to_json(self):
@@ -555,7 +568,6 @@ 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.activity = new_activity_ref.activity.strip().upper()
if new_activity_ref.id == "": if new_activity_ref.id == "":
return return
for activity_ref in self.activity_refs: for activity_ref in self.activity_refs:
+9 -17
View File
@@ -3,6 +3,7 @@ from datetime import datetime
import pytz import pytz
from core.activity_utils import get_activity_by_name
from core.enums import ActivityName from core.enums import ActivityName
from data.activity_ref import ActivityRef from data.activity_ref import ActivityRef
from data.alert import Alert from data.alert import Alert
@@ -37,7 +38,13 @@ 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, activity=activity, name=ref_name)] # We can only add a reference if we know the activity it's for
found_activity = get_activity_by_name(activity)
activities = []
activity_refs = []
if found_activity is not None:
activities = [found_activity.name]
activity_refs = [ActivityRef(id=ref_id, activity=found_activity.name, name=ref_name)]
# Convert to our alert format # Convert to our alert format
alert = Alert( alert = Alert(
@@ -46,26 +53,11 @@ 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"],
activities=[activity] if activity else [], activities=activities,
activity_refs=activity_refs, activity_refs=activity_refs,
start_time=start_time, start_time=start_time,
) )
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
if activity and activity not in [
ActivityName.POTA,
ActivityName.SOTA,
ActivityName.WWFF,
ActivityName.HEMA,
ActivityName.SIOTA,
ActivityName.ZLOTA,
ActivityName.KRMNPA,
ActivityName.SANPCPA,
ActivityName.LLOTA,
ActivityName.QRP,
]:
logger.warning(f"PNP alert found with activity {activity}, developer needs to add support for this!")
# If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to # If this is POTA, SOTA or WWFF data we already have it through other means, so ignore. Otherwise, add to
# the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that # the alert list. Note that while ZLOTA has its own spots API, it doesn't have its own alerts API. So that
# means the PnP *spot* provider rejects ZLOTA spots here, but the PnP *alerts* provider here allows ZLOTA. # means the PnP *spot* provider rejects ZLOTA spots here, but the PnP *alerts* provider here allows ZLOTA.
+28 -42
View File
@@ -68,15 +68,6 @@ 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"],
activity_refs=[
ActivityRef(
id=source_spot["REF"],
activity="",
name=source_spot["NAME"],
latitude=lat,
longitude=lon,
)
],
time=time, time=time,
dx_latitude=lat, dx_latitude=lat,
dx_longitude=lon, dx_longitude=lon,
@@ -98,57 +89,52 @@ 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.activity_refs and ref_info and "reftype" in ref_info: if ref_info and "reftype" in ref_info:
activity: ActivityName | None = None
ref_type: ActivityRefType | None = None
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.activity_refs[0].activity = ActivityName.SOTA activity, ref_type = ActivityName.SOTA, ActivityRefType.SUMMIT
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
spot.add_activity(ActivityName.SOTA)
else: else:
spot.activity_refs[0].activity = ActivityName.GMA activity, ref_type = ActivityName.GMA, ActivityRefType.SUMMIT
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
spot.add_activity(ActivityName.GMA)
case "POTA": case "POTA":
spot.activity_refs[0].activity = ActivityName.POTA activity, ref_type = ActivityName.POTA, ActivityRefType.PARK
spot.activity_refs[0].ref_type = ActivityRefType.PARK
spot.add_activity(ActivityName.POTA)
case "WWFF": case "WWFF":
spot.activity_refs[0].activity = ActivityName.WWFF activity, ref_type = ActivityName.WWFF, ActivityRefType.PARK
spot.activity_refs[0].ref_type = ActivityRefType.PARK
spot.add_activity(ActivityName.WWFF)
case "IOTA Island": case "IOTA Island":
spot.activity_refs[0].activity = ActivityName.IOTA activity, ref_type = ActivityName.IOTA, ActivityRefType.ISLAND
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
spot.add_activity(ActivityName.IOTA)
case "GMA Island": case "GMA Island":
spot.activity_refs[0].activity = ActivityName.GMA_ISLANDS activity, ref_type = ActivityName.GMA_ISLANDS, ActivityRefType.ISLAND
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
spot.add_activity(ActivityName.GMA_ISLANDS)
case "Lighthouse (ILLW)": case "Lighthouse (ILLW)":
spot.activity_refs[0].activity = ActivityName.ILLW activity, ref_type = ActivityName.ILLW, ActivityRefType.LIGHTHOUSE
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
spot.add_activity(ActivityName.ILLW)
case "Lighthouse (ARLHS)": case "Lighthouse (ARLHS)":
spot.activity_refs[0].activity = ActivityName.ARLHS activity, ref_type = ActivityName.ARLHS, ActivityRefType.LIGHTHOUSE
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
spot.add_activity(ActivityName.ARLHS)
case "Castle": case "Castle":
spot.activity_refs[0].activity = ActivityName.WCA activity, ref_type = ActivityName.WCA, ActivityRefType.CASTLE
spot.activity_refs[0].ref_type = ActivityRefType.CASTLE
spot.add_activity(ActivityName.WCA)
case "Mill": case "Mill":
spot.activity_refs[0].activity = ActivityName.MOTA activity, ref_type = ActivityName.MOTA, ActivityRefType.MILL
spot.activity_refs[0].ref_type = ActivityRefType.MILL
spot.add_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.activity_refs[0].activity = ref_info["reftype"]
spot.add_activity(ref_info["reftype"]) # Now we know the activity, add the reference to the spot. If it's an activity we
# don't know, there's no ActivityName for it, so we can't add a reference.
if activity is not None:
spot.activity_refs = [
ActivityRef(
id=source_spot["REF"],
activity=activity,
ref_type=ref_type,
name=source_spot["NAME"],
latitude=lat,
longitude=lon,
)
]
spot.add_activity(activity)
elif not ref_response.from_cache: elif not ref_response.from_cache:
if not ref_response.ok: if not ref_response.ok:
+7 -20
View File
@@ -6,6 +6,7 @@ from typing import ClassVar
import pytz import pytz
import requests import requests
from core.activity_utils import get_activity_by_name
from core.constants import HTTP_HEADERS from core.constants import HTTP_HEADERS
from core.enums import ActivityName, Mode from core.enums import ActivityName, Mode
from data.activity_ref import ActivityRef from data.activity_ref import ActivityRef
@@ -67,16 +68,19 @@ class ParksNPeaks(HTTPSpotProvider):
# Record activity information # Record activity information
activity = source_spot["actClass"].upper() activity = source_spot["actClass"].upper()
found_activity = get_activity_by_name(activity)
ref_id = source_spot["actSiteID"] ref_id = source_spot["actSiteID"]
if activity: if activity:
spot.add_activity(activity) if found_activity is not None:
spot.add_activity(found_activity.name)
if ref_id: # We can only add a reference if we know the activity it's for
if ref_id and found_activity is not None:
activity_refs = [ activity_refs = [
ActivityRef( ActivityRef(
id=ref_id, id=ref_id,
activity=activity, activity=found_activity.name,
# 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"] != ""
@@ -96,23 +100,6 @@ class ParksNPeaks(HTTPSpotProvider):
): ):
spot.comment = source_spot["actLocation"] spot.comment = source_spot["actLocation"]
# Log a warning for the developer if PnP gives us an unknown programme we've never seen before
if activity not in [
ActivityName.POTA,
ActivityName.SOTA,
ActivityName.WWFF,
ActivityName.HEMA,
ActivityName.SIOTA,
ActivityName.ZLOTA,
ActivityName.KRMNPA,
ActivityName.SANPCPA,
ActivityName.LLOTA,
ActivityName.QRP,
]:
logger.warning(
f"PNP spot found with activity {activity}, developer needs to add support for this!"
)
# Add new spot to the list # Add new spot to the list
new_spots.append(spot) new_spots.append(spot)
return new_spots return new_spots
+20 -11
View File
@@ -1,13 +1,17 @@
import json import json
import logging
from datetime import datetime from datetime import datetime
import pytz import pytz
from core.enums import Mode from core.activity_utils import get_activity_by_name
from core.enums import ActivityName, Mode
from data.activity_ref import ActivityRef from data.activity_ref import ActivityRef
from data.spot import Spot from data.spot import Spot
from providers.spot.websocket_spot_provider import WebsocketSpotProvider from providers.spot.websocket_spot_provider import WebsocketSpotProvider
logger = logging.getLogger(__name__)
class XOTA(WebsocketSpotProvider): class XOTA(WebsocketSpotProvider):
"""Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/ """Spot provider for servers based on the "xOTA" software at https://github.com/nischu/xOTA/
@@ -17,12 +21,17 @@ class XOTA(WebsocketSpotProvider):
is why we also provide an activity_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01" is why we also provide an activity_ref_prefix in our config. This is applied to the reference ID, so e.g. "T-01"
at C3 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: ActivityName | None = 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["activity"]) if "activity" in provider_config else None found_activity = get_activity_by_name(provider_config.get("activity"))
self.ACTIVITY = found_activity.name if found_activity else None
if not self.ACTIVITY:
logger.error(
"XOTA provider has no activity reference, this is a config problem - your config needs to specify a known activity type!"
)
self._activity_ref_prefix = ( self._activity_ref_prefix = (
str(provider_config["activity_ref_prefix"]) if "activity_ref_prefix" in provider_config else "" str(provider_config["activity_ref_prefix"]) if "activity_ref_prefix" in provider_config else ""
) )
@@ -31,20 +40,20 @@ class XOTA(WebsocketSpotProvider):
string = b.decode("utf-8") string = b.decode("utf-8")
source_spot = json.loads(string) source_spot = json.loads(string)
ref_id = f"{self._activity_ref_prefix} {source_spot['reference']['title']}" ref_id = f"{self._activity_ref_prefix} {source_spot['reference']['title']}"
activity = self.ACTIVITY
activities = []
activity_refs = []
if activity is not None:
activities = [activity]
activity_refs = [ActivityRef(id=ref_id, activity=activity, url=source_spot["reference"]["website"])]
spot = Spot( spot = Spot(
source=self.name, source=self.name,
source_id=source_spot["id"], source_id=source_spot["id"],
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()),
activities=[self.ACTIVITY] if self.ACTIVITY else [], activities=activities,
activity_refs=[ activity_refs=activity_refs,
ActivityRef(
id=ref_id,
activity=self.ACTIVITY or "",
url=source_spot["reference"]["website"],
)
],
time=datetime.now(pytz.UTC).timestamp(), time=datetime.now(pytz.UTC).timestamp(),
qrt=source_spot["state"] != "active", qrt=source_spot["state"] != "active",
) )
+1 -1
View File
@@ -77,7 +77,7 @@
</div> </div>
<script src="/static/js/add-spot.js?v=1790319283"></script> <script src="/static/js/add-spot.js?v=1790324213"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-add-spot").addClass("active"); $("#nav-link-add-spot").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -85,7 +85,7 @@
</div> </div>
<script src="/static/js/alerts.js?v=1790319283"></script> <script src="/static/js/alerts.js?v=1790324213"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-alerts").addClass("active"); $("#nav-link-alerts").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -76,8 +76,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1790319283"></script> <script src="/static/js/spotsbandsandmap.js?v=1790324213"></script>
<script src="/static/js/bands.js?v=1790319283"></script> <script src="/static/js/bands.js?v=1790324213"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-bands").addClass("active"); $("#nav-link-bands").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+5 -5
View File
@@ -1,6 +1,6 @@
{% extends "skeleton.html" %} {% extends "skeleton.html" %}
{% block head_extra %} {% block head_extra %}
<link rel="stylesheet" href="/static/css/style.css?v=1790319283" type="text/css"> <link rel="stylesheet" href="/static/css/style.css?v=1790324213" type="text/css">
<link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet"> <link href="/static/vendor/css/bootstrap-5.3.8.min.css" rel="stylesheet">
<link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/fontawesome-6.7.2.min.css" rel="stylesheet">
<link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet"> <link href="/static/vendor/css/solid-6.7.2.min.css" rel="stylesheet">
@@ -16,10 +16,10 @@
window.fetchEventSource = fetchEventSource; window.fetchEventSource = fetchEventSource;
</script> </script>
<script src="/static/js/utils.js?v=1790319283"></script> <script src="/static/js/utils.js?v=1790324213"></script>
<script src="/static/js/ui-ham.js?v=1790319283"></script> <script src="/static/js/ui-ham.js?v=1790324213"></script>
<script src="/static/js/geo.js?v=1790319283"></script> <script src="/static/js/geo.js?v=1790324213"></script>
<script src="/static/js/common.js?v=1790319283"></script> <script src="/static/js/common.js?v=1790324213"></script>
{% end %} {% end %}
{% block body %} {% block body %}
<div class="container"> <div class="container">
+1 -1
View File
@@ -284,7 +284,7 @@
</div> </div>
<script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script> <script src="/static/vendor/js/chart-4.4.9.umd.min.js"></script>
<script src="/static/js/conditions.js?v=1790319283"></script> <script src="/static/js/conditions.js?v=1790324213"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-conditions").addClass("active"); $("#nav-link-conditions").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -113,8 +113,8 @@
const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}"; const CARTODB_API_KEY = "{{ web_ui_options.get('cartodb_api_key', '') }}";
</script> </script>
<script src="/static/js/spotsbandsandmap.js?v=1790319283"></script> <script src="/static/js/spotsbandsandmap.js?v=1790324213"></script>
<script src="/static/js/map.js?v=1790319283"></script> <script src="/static/js/map.js?v=1790324213"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-map").addClass("active"); $("#nav-link-map").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+2 -2
View File
@@ -125,8 +125,8 @@
</div> </div>
<script src="/static/js/spotsbandsandmap.js?v=1790319283"></script> <script src="/static/js/spotsbandsandmap.js?v=1790324213"></script>
<script src="/static/js/spots.js?v=1790319283"></script> <script src="/static/js/spots.js?v=1790324213"></script>
<script>$(document).ready(function () { <script>$(document).ready(function () {
$("#nav-link-spots").addClass("active"); $("#nav-link-spots").addClass("active");
}); <!-- highlight active page in nav --></script> }); <!-- highlight active page in nav --></script>
+1 -1
View File
@@ -96,7 +96,7 @@
</div> </div>
</div> </div>
<script src="/static/js/status.js?v=1790319283"></script> <script src="/static/js/status.js?v=1790324213"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#nav-link-status").addClass("active"); $("#nav-link-status").addClass("active");
+6 -7
View File
@@ -7,7 +7,7 @@ from tornado import httputil
from tornado.web import Application from tornado.web import Application
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_activity_by_name, get_ref_regex_for_activity from core.activity_utils import get_activity_by_name
from core.call_lookup_helper import get_call_info from core.call_lookup_helper import get_call_info
from core.geo_utils import ( from core.geo_utils import (
lat_lon_for_grid_sw_corner_plus_size, lat_lon_for_grid_sw_corner_plus_size,
@@ -82,12 +82,11 @@ class APILookupActivityRefHandler(tornado.web.RequestHandler):
# "activity" 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 "activity" in query_params and "id" in query_params: if "activity" in query_params and "id" in query_params:
activity = str(query_params.get("activity")).upper() found_activity = get_activity_by_name(str(query_params.get("activity")))
ref_id = str(query_params.get("id")).upper() ref_id = str(query_params.get("id")).upper()
if get_activity_by_name(activity): if found_activity is not None:
if not get_ref_regex_for_activity(activity) or re.match( activity = found_activity.name
get_ref_regex_for_activity(activity), ref_id if not found_activity.ref_regex or re.match(found_activity.ref_regex, ref_id):
):
data = populate_missing_activity_ref_info(ActivityRef(id=ref_id, activity=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))
@@ -99,7 +98,7 @@ class APILookupActivityRefHandler(tornado.web.RequestHandler):
) )
self.set_status(422) self.set_status(422)
else: else:
self.write(safe_json_dumps(f"Error - activity '{activity}' is not known.")) self.write(safe_json_dumps(f"Error - activity '{query_params.get('activity')}' is not known."))
self.set_status(422) self.set_status(422)
else: else:
self.write(safe_json_dumps("Error - activity and id must be provided")) self.write(safe_json_dumps("Error - activity and id must be provided"))