mirror of
https://git.ianrenton.com/ian/spothole.git
synced 2026-09-25 00:34:32 +00:00
Allow spots and alerts to have multiple activities #143
This commit is contained in:
+26
-9
@@ -66,8 +66,11 @@ class Alert:
|
||||
|
||||
# Activity info
|
||||
|
||||
# Activity (e.g. outdoor activity programme such as POTA).
|
||||
activity: str | None = None
|
||||
# Activities (e.g. outdoor activity programmes such as POTA). An alert can be for several activities at once,
|
||||
# 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
|
||||
# the uniqueness logic manually, so you must use add_activity() to add to it instead of adding directly.
|
||||
activities: list = field(default_factory=list)
|
||||
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
|
||||
activity_refs: list = field(default_factory=list)
|
||||
|
||||
@@ -92,6 +95,11 @@ class Alert:
|
||||
# Icon to use when displaying this alert in the web UI. Chosen from the Font Awesome set.
|
||||
icon: str | None = None
|
||||
|
||||
def __post_init__(self):
|
||||
"""Normalise the activities list, removing any duplicates while keeping the order."""
|
||||
|
||||
self.activities = list(dict.fromkeys(self.activities)) if self.activities else []
|
||||
|
||||
def infer_missing(self, credentials=None):
|
||||
"""Infer missing parameters where possible"""
|
||||
|
||||
@@ -145,10 +153,10 @@ class Alert:
|
||||
self.dx_latitude = activity_ref.latitude
|
||||
self.dx_longitude = activity_ref.longitude
|
||||
|
||||
# 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.
|
||||
if self.activity_refs and self.activity_refs[0] and not self.activity:
|
||||
self.activity = self.activity_refs[0].activity
|
||||
# Add the activities of any activity refs we have to the alert's list of activities.
|
||||
for activity_ref in self.activity_refs:
|
||||
if activity_ref and activity_ref.activity:
|
||||
self.add_activity(activity_ref.activity)
|
||||
|
||||
# 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):
|
||||
@@ -179,14 +187,23 @@ class Alert:
|
||||
if self.dx_calls and not self.dx_names:
|
||||
self.dx_names = [get_call_info(c, credentials).name for c in self.dx_calls]
|
||||
|
||||
# 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 first activity that has one, otherwise a radio tower
|
||||
self.icon = "fa-tower-cell"
|
||||
if self.activity and (activity_icon := get_icon_for_activity(self.activity)):
|
||||
self.icon = activity_icon
|
||||
for activity in self.activities:
|
||||
if activity_icon := get_icon_for_activity(activity):
|
||||
self.icon = activity_icon
|
||||
break
|
||||
|
||||
except Exception:
|
||||
logger.exception("Exception while inferring missing data from spot")
|
||||
|
||||
def add_activity(self, activity):
|
||||
"""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."""
|
||||
|
||||
if activity and activity not in self.activities:
|
||||
self.activities.append(activity)
|
||||
|
||||
def to_json(self):
|
||||
"""JSON serialise"""
|
||||
|
||||
|
||||
+40
-33
@@ -125,8 +125,11 @@ class Spot:
|
||||
|
||||
# Activity info
|
||||
|
||||
# Activity (e.g. outdoor activity programme such as POTA).
|
||||
activity: str | None = None
|
||||
# Activities (e.g. outdoor activity programmes such as POTA). An alert can be for several activities at once,
|
||||
# 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
|
||||
# the uniqueness logic manually, so you must use add_activity() to add to it instead of adding directly.
|
||||
activities: list = field(default_factory=list)
|
||||
# Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO.
|
||||
activity_refs: list = field(default_factory=list)
|
||||
|
||||
@@ -158,8 +161,9 @@ class Spot:
|
||||
def __post_init__(self):
|
||||
"""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
|
||||
objects such as the activity_refs list.."""
|
||||
objects such as the activity_refs list, and de-duplicating the activities list."""
|
||||
|
||||
self.activities = list(dict.fromkeys(self.activities)) if self.activities else []
|
||||
if self.activity_refs:
|
||||
self.activity_refs = [
|
||||
activity_ref if isinstance(activity_ref, ActivityRef) else ActivityRef(**activity_ref)
|
||||
@@ -268,9 +272,10 @@ class Spot:
|
||||
if self.dx_latitude or self.dx_grid:
|
||||
self.dx_location_source = LocationSourceForSpot.SPOT
|
||||
|
||||
# Set the top-level activity if it is missing but we have at least one activity ref.
|
||||
if not self.activity and self.activity_refs:
|
||||
self.activity = self.activity_refs[0].activity.upper()
|
||||
# Add the activities of any activity refs we have to the top-level activities list.
|
||||
for activity_ref in self.activity_refs:
|
||||
if 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
|
||||
# activity. This should catch e.g. POTA comments like "2-fer: GB-0001 GB-0002".
|
||||
@@ -290,11 +295,10 @@ class Spot:
|
||||
if self.comment:
|
||||
activity_matches = re.finditer(r"(^|\W)" + ANY_ACTIVITY_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
||||
for activity_match in activity_matches:
|
||||
# First of all, if we haven't got an activity for this spot set yet, now we have. This covers
|
||||
# First of all, add the activity to this spot's list of activities. This covers
|
||||
# things like cluster spots where the comment is just "POTA".
|
||||
found_activity = get_activity_name_from_comment_name(activity_match.group(2))
|
||||
if not self.activity:
|
||||
self.activity = found_activity
|
||||
self.add_activity(found_activity)
|
||||
|
||||
# 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 activity_refs list for this spot.
|
||||
@@ -320,11 +324,9 @@ class Spot:
|
||||
r"(^|\W)(" + activity.ref_regex + r")($|\W)", self.comment, re.IGNORECASE
|
||||
)
|
||||
for ref_match in ref_matches:
|
||||
# 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
|
||||
# it's WWFF.
|
||||
if not self.activity:
|
||||
self.activity = activity.name
|
||||
# First of all, add the activity to this spot's list of activities. This covers things
|
||||
# like cluster spots where the comment is just "OHFF-1234", now we know it's WWFF.
|
||||
self.add_activity(activity.name)
|
||||
self._append_activity_ref_if_missing(
|
||||
ActivityRef(id=ref_match.group(2).upper(), activity=activity.name)
|
||||
)
|
||||
@@ -346,16 +348,11 @@ class Spot:
|
||||
):
|
||||
self.dx_latitude = activity_ref.latitude
|
||||
self.dx_longitude = activity_ref.longitude
|
||||
if self.activity in (ActivityName.WAB, ActivityName.WAI, ActivityName.TILES):
|
||||
if activity_ref.activity in (ActivityName.WAB, ActivityName.WAI, ActivityName.TILES):
|
||||
self.dx_location_source = LocationSourceForSpot.GRID
|
||||
else:
|
||||
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
|
||||
# reference's activity and apply it to the whole spot.
|
||||
if self.activity_refs and not self.activity:
|
||||
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".
|
||||
# These are common on cluster spots and can provide grid references in preference to e.g. QRZ lookup, as well as
|
||||
# being the only source we have for propagation mode. Brace for nightmare regex from hell.
|
||||
@@ -407,32 +404,33 @@ class Spot:
|
||||
self.dx_location_source = LocationSourceForSpot.GRID
|
||||
|
||||
# Set activities based on propagation mode
|
||||
if self.propagation_mode == "Satellite" and not self.activity:
|
||||
self.activity = ActivityName.SATELLITE
|
||||
if self.propagation_mode == "Earth-Moon-Earth" and not self.activity:
|
||||
self.activity = ActivityName.EME
|
||||
if self.propagation_mode == "Satellite":
|
||||
self.add_activity(ActivityName.SATELLITE)
|
||||
if self.propagation_mode == "Earth-Moon-Earth":
|
||||
self.add_activity(ActivityName.EME)
|
||||
|
||||
# Set activities based on the DX callsign suffix
|
||||
if self.dx_call and not self.activity:
|
||||
if self.dx_call:
|
||||
if self.dx_call.upper().endswith(ActivityName.AERONAUTICAL_MOBILE):
|
||||
self.activity = ActivityName.AERONAUTICAL_MOBILE
|
||||
self.add_activity(ActivityName.AERONAUTICAL_MOBILE)
|
||||
elif self.dx_call.upper().endswith(ActivityName.MARITIME_MOBILE):
|
||||
self.activity = ActivityName.MARITIME_MOBILE
|
||||
self.add_activity(ActivityName.MARITIME_MOBILE)
|
||||
|
||||
# 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.
|
||||
if self.dx_call and not self.activity:
|
||||
if self.dx_call and not self.activities:
|
||||
now = datetime.now(pytz.UTC).timestamp()
|
||||
for alert in DATA_STORE.alerts.values():
|
||||
if (
|
||||
alert.activity == ActivityName.DXPEDITION
|
||||
alert.activities
|
||||
and ActivityName.DXPEDITION in alert.activities
|
||||
and alert.dx_calls
|
||||
and alert.start_time
|
||||
and 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]
|
||||
):
|
||||
self.activity = ActivityName.DXPEDITION
|
||||
self.add_activity(ActivityName.DXPEDITION)
|
||||
break
|
||||
|
||||
# DX Grid to lat/lon and vice versa in case one is missing
|
||||
@@ -531,14 +529,23 @@ class Spot:
|
||||
self.de_longitude = de_call_info.longitude
|
||||
self.de_grid = de_call_info.grid
|
||||
|
||||
# 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 first activity that has one, otherwise a radio tower
|
||||
self.icon = "fa-tower-cell"
|
||||
if self.activity and (activity_icon := get_icon_for_activity(self.activity)):
|
||||
self.icon = activity_icon
|
||||
for activity in self.activities:
|
||||
if activity_icon := get_icon_for_activity(activity):
|
||||
self.icon = activity_icon
|
||||
break
|
||||
|
||||
except Exception:
|
||||
logger.exception("Exception while inferring missing data from spot")
|
||||
|
||||
def add_activity(self, activity):
|
||||
"""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."""
|
||||
|
||||
if activity and activity not in self.activities:
|
||||
self.activities.append(activity)
|
||||
|
||||
def to_json(self):
|
||||
"""JSON serialise"""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user