mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-25 08:44:33 +00:00
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:
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
Reference in New Issue
Block a user