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:
+25
-8
@@ -66,8 +66,11 @@ class Alert:
|
|||||||
|
|
||||||
# Activity info
|
# Activity info
|
||||||
|
|
||||||
# Activity (e.g. outdoor activity programme such as POTA).
|
# Activities (e.g. outdoor activity programmes such as POTA). An alert can be for several activities at once,
|
||||||
activity: str | None = None
|
# 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 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 = 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 to use when displaying this alert in the web UI. Chosen from the Font Awesome set.
|
||||||
icon: str | None = None
|
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):
|
def infer_missing(self, credentials=None):
|
||||||
"""Infer missing parameters where possible"""
|
"""Infer missing parameters where possible"""
|
||||||
|
|
||||||
@@ -145,10 +153,10 @@ class Alert:
|
|||||||
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 the spot itself doesn't have an activity yet, but we have at least one activity reference, take that
|
# Add the activities of any activity refs we have to the alert's list of activities.
|
||||||
# reference's activity and apply it to the whole spot.
|
for activity_ref in self.activity_refs:
|
||||||
if self.activity_refs and self.activity_refs[0] and not self.activity:
|
if activity_ref and activity_ref.activity:
|
||||||
self.activity = self.activity_refs[0].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
|
||||||
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):
|
||||||
@@ -179,14 +187,23 @@ class Alert:
|
|||||||
if self.dx_calls and not self.dx_names:
|
if self.dx_calls and not self.dx_names:
|
||||||
self.dx_names = [get_call_info(c, credentials).name for c in self.dx_calls]
|
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"
|
self.icon = "fa-tower-cell"
|
||||||
if self.activity and (activity_icon := get_icon_for_activity(self.activity)):
|
for activity in self.activities:
|
||||||
|
if activity_icon := get_icon_for_activity(activity):
|
||||||
self.icon = activity_icon
|
self.icon = activity_icon
|
||||||
|
break
|
||||||
|
|
||||||
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):
|
||||||
|
"""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):
|
def to_json(self):
|
||||||
"""JSON serialise"""
|
"""JSON serialise"""
|
||||||
|
|
||||||
|
|||||||
+39
-32
@@ -125,8 +125,11 @@ class Spot:
|
|||||||
|
|
||||||
# Activity info
|
# Activity info
|
||||||
|
|
||||||
# Activity (e.g. outdoor activity programme such as POTA).
|
# Activities (e.g. outdoor activity programmes such as POTA). An alert can be for several activities at once,
|
||||||
activity: str | None = None
|
# 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 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 = field(default_factory=list)
|
||||||
|
|
||||||
@@ -158,8 +161,9 @@ 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.."""
|
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:
|
if self.activity_refs:
|
||||||
self.activity_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)
|
||||||
@@ -268,9 +272,10 @@ class Spot:
|
|||||||
if self.dx_latitude or self.dx_grid:
|
if self.dx_latitude or self.dx_grid:
|
||||||
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.
|
# Add the activities of any activity refs we have to the top-level activities list.
|
||||||
if not self.activity and self.activity_refs:
|
for activity_ref in self.activity_refs:
|
||||||
self.activity = self.activity_refs[0].activity.upper()
|
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
|
# 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".
|
||||||
@@ -290,11 +295,10 @@ class Spot:
|
|||||||
if self.comment:
|
if self.comment:
|
||||||
activity_matches = re.finditer(r"(^|\W)" + ANY_ACTIVITY_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
activity_matches = re.finditer(r"(^|\W)" + ANY_ACTIVITY_REGEX + r"($|\W)", self.comment, re.IGNORECASE)
|
||||||
for activity_match in activity_matches:
|
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".
|
# 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.activity:
|
self.add_activity(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 activity_refs list for this spot.
|
# 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
|
r"(^|\W)(" + activity.ref_regex + r")($|\W)", self.comment, re.IGNORECASE
|
||||||
)
|
)
|
||||||
for ref_match in ref_matches:
|
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
|
# First of all, add the activity to this spot's list of activities. This covers things
|
||||||
# covers things like cluster spots where the comment is just "OHFF-1234", now we know
|
# like cluster spots where the comment is just "OHFF-1234", now we know it's WWFF.
|
||||||
# it's WWFF.
|
self.add_activity(activity.name)
|
||||||
if not self.activity:
|
|
||||||
self.activity = activity.name
|
|
||||||
self._append_activity_ref_if_missing(
|
self._append_activity_ref_if_missing(
|
||||||
ActivityRef(id=ref_match.group(2).upper(), activity=activity.name)
|
ActivityRef(id=ref_match.group(2).upper(), activity=activity.name)
|
||||||
)
|
)
|
||||||
@@ -346,16 +348,11 @@ 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.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
|
self.dx_location_source = LocationSourceForSpot.GRID
|
||||||
else:
|
else:
|
||||||
self.dx_location_source = LocationSourceForSpot.ACTIVITY_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
|
|
||||||
# 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".
|
# 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
|
||||||
# being the only source we have for propagation mode. Brace for nightmare regex from hell.
|
# 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
|
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.activity:
|
if self.propagation_mode == "Satellite":
|
||||||
self.activity = ActivityName.SATELLITE
|
self.add_activity(ActivityName.SATELLITE)
|
||||||
if self.propagation_mode == "Earth-Moon-Earth" and not self.activity:
|
if self.propagation_mode == "Earth-Moon-Earth":
|
||||||
self.activity = ActivityName.EME
|
self.add_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.activity:
|
if self.dx_call:
|
||||||
if self.dx_call.upper().endswith(ActivityName.AERONAUTICAL_MOBILE):
|
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):
|
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
|
# 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.activity:
|
if self.dx_call and not self.activities:
|
||||||
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.activity == ActivityName.DXPEDITION
|
alert.activities
|
||||||
|
and ActivityName.DXPEDITION in alert.activities
|
||||||
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.activity = ActivityName.DXPEDITION
|
self.add_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
|
||||||
@@ -531,14 +529,23 @@ class Spot:
|
|||||||
self.de_longitude = de_call_info.longitude
|
self.de_longitude = de_call_info.longitude
|
||||||
self.de_grid = de_call_info.grid
|
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"
|
self.icon = "fa-tower-cell"
|
||||||
if self.activity and (activity_icon := get_icon_for_activity(self.activity)):
|
for activity in self.activities:
|
||||||
|
if activity_icon := get_icon_for_activity(activity):
|
||||||
self.icon = activity_icon
|
self.icon = activity_icon
|
||||||
|
break
|
||||||
|
|
||||||
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):
|
||||||
|
"""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):
|
def to_json(self):
|
||||||
"""JSON serialise"""
|
"""JSON serialise"""
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timedelta
|
|||||||
import pytz
|
import pytz
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from core.enums import ActivityName
|
from core.enums import ActivityName, ActivityRefType
|
||||||
from data.activity_ref import ActivityRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
@@ -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],
|
||||||
activity=ActivityName.BOTA,
|
activities=[ActivityName.BOTA],
|
||||||
activity_refs=[ActivityRef(id=ref_name, activity=ActivityName.BOTA)],
|
activity_refs=[ActivityRef(id=ref_name, activity=ActivityName.BOTA, ref_type=ActivityRefType.BEACH)],
|
||||||
start_time=date_time.timestamp(),
|
start_time=date_time.timestamp(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import ActivityName
|
from core.enums import ActivityName, ActivityRefType
|
||||||
from data.activity_ref import ActivityRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
@@ -38,12 +38,13 @@ 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"],
|
||||||
activity=ActivityName.SATELLITE,
|
activities=[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
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
activity=ActivityName.SATELLITE,
|
activity=ActivityName.SATELLITE,
|
||||||
id=source_alert["satellite"]["name"],
|
id=source_alert["satellite"]["name"],
|
||||||
|
ref_type=ActivityRefType.SATELLITE
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
start_time=datetime.strptime(source_alert["aos_at"], "%Y-%m-%dT%H:%M:%SZ")
|
start_time=datetime.strptime(source_alert["aos_at"], "%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|||||||
@@ -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,
|
||||||
activity=ActivityName.DXPEDITION,
|
activities=[ActivityName.DXPEDITION],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add to our list.
|
# Add to our list.
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ 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"],
|
||||||
activity=activity,
|
activities=[activity] if activity else [],
|
||||||
activity_refs=activity_refs,
|
activity_refs=activity_refs,
|
||||||
start_time=start_time,
|
start_time=start_time,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import ActivityName
|
from core.enums import ActivityName, ActivityRefType
|
||||||
from data.activity_ref import ActivityRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
@@ -28,11 +28,12 @@ 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"],
|
||||||
activity=ActivityName.POTA,
|
activities=[ActivityName.POTA],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_alert["reference"],
|
id=source_alert["reference"],
|
||||||
activity=ActivityName.POTA,
|
activity=ActivityName.POTA,
|
||||||
|
ref_type=ActivityRefType.PARK,
|
||||||
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,
|
||||||
activity=ActivityName.CONTEST,
|
activities=[ActivityName.CONTEST],
|
||||||
)
|
)
|
||||||
|
|
||||||
return alert
|
return alert
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import ActivityName
|
from core.enums import ActivityName, ActivityRefType
|
||||||
from data.activity_ref import ActivityRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
@@ -34,11 +34,12 @@ 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"],
|
||||||
activity=ActivityName.SOTA,
|
activities=[ActivityName.SOTA],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=f"{source_alert['associationCode']}/{source_alert['summitCode']}",
|
id=f"{source_alert['associationCode']}/{source_alert['summitCode']}",
|
||||||
activity=ActivityName.SOTA,
|
activity=ActivityName.SOTA,
|
||||||
|
ref_type=ActivityRefType.SUMMIT,
|
||||||
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,
|
||||||
activity=ActivityName.CONTEST,
|
activities=[ActivityName.CONTEST],
|
||||||
)
|
)
|
||||||
|
|
||||||
return alert
|
return alert
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import pytz
|
|||||||
from rss_parser import Parser as RSSParser
|
from rss_parser import Parser as RSSParser
|
||||||
from rss_parser.models.rss import RSS
|
from rss_parser.models.rss import RSS
|
||||||
|
|
||||||
from core.enums import ActivityName
|
from core.enums import ActivityName, ActivityRefType
|
||||||
from data.activity_ref import ActivityRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
@@ -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,
|
||||||
activity_refs=[ActivityRef(id=ref, activity=ActivityName.WOTA, name=ref_name)] if ref else [],
|
activity_refs=[ActivityRef(id=ref, activity=ActivityName.WOTA, name=ref_name, ref_type=ActivityRefType.SUMMIT)] if ref else [],
|
||||||
start_time=time.timestamp(),
|
start_time=time.timestamp(),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import datetime
|
|||||||
|
|
||||||
import pytz
|
import pytz
|
||||||
|
|
||||||
from core.enums import ActivityName
|
from core.enums import ActivityName, ActivityRefType
|
||||||
from data.activity_ref import ActivityRef
|
from data.activity_ref import ActivityRef
|
||||||
from data.alert import Alert
|
from data.alert import Alert
|
||||||
from providers.alert.http_alert_provider import HTTPAlertProvider
|
from providers.alert.http_alert_provider import HTTPAlertProvider
|
||||||
@@ -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"],
|
||||||
activity=ActivityName.WWFF,
|
activities=[ActivityName.WWFF],
|
||||||
activity_refs=[ActivityRef(id=source_alert["reference"], activity=ActivityName.WWFF)],
|
activity_refs=[ActivityRef(id=source_alert["reference"], activity=ActivityName.WWFF, ref_type=ActivityRefType.PARK)],
|
||||||
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(),
|
||||||
|
|||||||
+11
-11
@@ -106,49 +106,49 @@ class GMA(HTTPSpotProvider):
|
|||||||
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
|
spot.activity_refs[0].activity = ActivityName.SOTA
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
|
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||||
spot.activity = ActivityName.SOTA
|
spot.add_activity(ActivityName.SOTA)
|
||||||
else:
|
else:
|
||||||
spot.activity_refs[0].activity = ActivityName.GMA
|
spot.activity_refs[0].activity = ActivityName.GMA
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
|
spot.activity_refs[0].ref_type = ActivityRefType.SUMMIT
|
||||||
spot.activity = ActivityName.GMA
|
spot.add_activity(ActivityName.GMA)
|
||||||
case "POTA":
|
case "POTA":
|
||||||
spot.activity_refs[0].activity = ActivityName.POTA
|
spot.activity_refs[0].activity = ActivityName.POTA
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.PARK
|
spot.activity_refs[0].ref_type = ActivityRefType.PARK
|
||||||
spot.activity = ActivityName.POTA
|
spot.add_activity(ActivityName.POTA)
|
||||||
case "WWFF":
|
case "WWFF":
|
||||||
spot.activity_refs[0].activity = ActivityName.WWFF
|
spot.activity_refs[0].activity = ActivityName.WWFF
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.PARK
|
spot.activity_refs[0].ref_type = ActivityRefType.PARK
|
||||||
spot.activity = ActivityName.WWFF
|
spot.add_activity(ActivityName.WWFF)
|
||||||
case "IOTA Island":
|
case "IOTA Island":
|
||||||
spot.activity_refs[0].activity = ActivityName.IOTA
|
spot.activity_refs[0].activity = ActivityName.IOTA
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
|
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
|
||||||
spot.activity = ActivityName.IOTA
|
spot.add_activity(ActivityName.IOTA)
|
||||||
case "GMA Island":
|
case "GMA Island":
|
||||||
spot.activity_refs[0].activity = ActivityName.GMA_ISLANDS
|
spot.activity_refs[0].activity = ActivityName.GMA_ISLANDS
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
|
spot.activity_refs[0].ref_type = ActivityRefType.ISLAND
|
||||||
spot.activity = ActivityName.GMA_ISLANDS
|
spot.add_activity(ActivityName.GMA_ISLANDS)
|
||||||
case "Lighthouse (ILLW)":
|
case "Lighthouse (ILLW)":
|
||||||
spot.activity_refs[0].activity = ActivityName.ILLW
|
spot.activity_refs[0].activity = ActivityName.ILLW
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||||
spot.activity = ActivityName.ILLW
|
spot.add_activity(ActivityName.ILLW)
|
||||||
case "Lighthouse (ARLHS)":
|
case "Lighthouse (ARLHS)":
|
||||||
spot.activity_refs[0].activity = ActivityName.ARLHS
|
spot.activity_refs[0].activity = ActivityName.ARLHS
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
spot.activity_refs[0].ref_type = ActivityRefType.LIGHTHOUSE
|
||||||
spot.activity = ActivityName.ARLHS
|
spot.add_activity(ActivityName.ARLHS)
|
||||||
case "Castle":
|
case "Castle":
|
||||||
spot.activity_refs[0].activity = ActivityName.WCA
|
spot.activity_refs[0].activity = ActivityName.WCA
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.CASTLE
|
spot.activity_refs[0].ref_type = ActivityRefType.CASTLE
|
||||||
spot.activity = ActivityName.WCA
|
spot.add_activity(ActivityName.WCA)
|
||||||
case "Mill":
|
case "Mill":
|
||||||
spot.activity_refs[0].activity = ActivityName.MOTA
|
spot.activity_refs[0].activity = ActivityName.MOTA
|
||||||
spot.activity_refs[0].ref_type = ActivityRefType.MILL
|
spot.activity_refs[0].ref_type = ActivityRefType.MILL
|
||||||
spot.activity = ActivityName.MOTA
|
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.activity_refs[0].activity = ref_info["reftype"]
|
||||||
spot.activity = ref_info["reftype"]
|
spot.add_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,7 +62,7 @@ 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),
|
||||||
activity=ActivityName.HEMA,
|
activities=[ActivityName.HEMA],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=spot_items[3].upper(),
|
id=spot_items[3].upper(),
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ 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,
|
||||||
activity=ActivityName.LLOTA,
|
activities=[ActivityName.LLOTA],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
ref_id = source_spot["actSiteID"]
|
ref_id = source_spot["actSiteID"]
|
||||||
|
|
||||||
if activity:
|
if activity:
|
||||||
spot.activity = activity
|
spot.add_activity(activity)
|
||||||
|
|
||||||
if ref_id:
|
if ref_id:
|
||||||
activity_refs = [
|
activity_refs = [
|
||||||
@@ -130,7 +130,7 @@ class ParksNPeaks(HTTPSpotProvider):
|
|||||||
)
|
)
|
||||||
ref_id = spot.activity_refs[0].id if spot.activity_refs else ""
|
ref_id = spot.activity_refs[0].id if spot.activity_refs else ""
|
||||||
body = {
|
body = {
|
||||||
"actClass": spot.activity or "",
|
"actClass": next((a for a in spot.activities if self.can_submit_spot(a)), ""),
|
||||||
"actCallsign": spot.dx_call,
|
"actCallsign": spot.dx_call,
|
||||||
"actSite": ref_id,
|
"actSite": ref_id,
|
||||||
"mode": spot.mode or "",
|
"mode": spot.mode or "",
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ 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"],
|
||||||
activity=ActivityName.POTA,
|
activities=[ActivityName.POTA],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ 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"],
|
||||||
activity=ActivityName.SOTA,
|
activities=[ActivityName.SOTA],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["summitCode"],
|
id=source_spot["summitCode"],
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ 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"],
|
||||||
activity=ActivityName.TILES,
|
activities=[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.
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ 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"],
|
||||||
activity=ActivityName.TOWERS,
|
activities=[ActivityName.TOWERS],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(id=source_spot["ref"], activity=ActivityName.TOWERS, ref_type=ActivityRefType.TOWER)
|
ActivityRef(id=source_spot["ref"], activity=ActivityName.TOWERS, ref_type=ActivityRefType.TOWER)
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class WOTA(HTTPSpotProvider):
|
|||||||
freq=freq_hz,
|
freq=freq_hz,
|
||||||
mode=Mode.from_name(mode),
|
mode=Mode.from_name(mode),
|
||||||
comment=comment,
|
comment=comment,
|
||||||
activity=ActivityName.WOTA,
|
activities=[ActivityName.WOTA],
|
||||||
activity_refs=(
|
activity_refs=(
|
||||||
[
|
[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ 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"],
|
||||||
activity=ActivityName.WWBOTA,
|
activities=[ActivityName.WWBOTA],
|
||||||
activity_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
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ 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"],
|
||||||
activity=ActivityName.WWFF,
|
activities=[ActivityName.WWFF],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ 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()),
|
||||||
activity=self.ACTIVITY,
|
activities=[self.ACTIVITY] if self.ACTIVITY else [],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=ref_id,
|
id=ref_id,
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ 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"],
|
||||||
activity=ActivityName.ZLOTA,
|
activities=[ActivityName.ZLOTA],
|
||||||
activity_refs=[
|
activity_refs=[
|
||||||
ActivityRef(
|
ActivityRef(
|
||||||
id=source_spot["reference"],
|
id=source_spot["reference"],
|
||||||
|
|||||||
+33
-21
@@ -19,20 +19,20 @@ info:
|
|||||||
|
|
||||||
The term "SIG" (Special Interest Group), which Spothole inherited from ADIF, has been replaced with "activity" throughout the API.
|
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 spot and alert data, the single `sig` value has been replaced with `activities`, a list of unique activity names. A spot or alert can now be associated with more than one activity (e.g. a POTA and WWFF dual activation, a /MM activation via satellite, etc). The array is empty if there is no associated activity. `sig_refs` has been renamed 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:** In activity reference data (i.e. each entry in `activity_refs` of a spot or alert, 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 `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:** 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`. The `activity` filter now matches any spot or alert that has at least one of the requested activities.
|
||||||
* **Breaking change:** `/lookup/sigref` has been renamed to `/lookup/activityref`, and its `sig` query parameter has been renamed to `activity`.
|
* **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:** POST `/spot` now expects `activities` (a list) 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 `/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`.
|
* **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
|
#### 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.
|
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. Where a spot or alert has more than one activity, the `v2` and `v1` APIs will return only the first one as `sig`.
|
||||||
|
|
||||||
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=...`.
|
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, and handle `activities` being a list rather than a single `sig` value. If you use the activity reference lookup, call `/lookup/activityref?activity=...&id=...` instead of `/lookup/sigref?sig=...&id=...`.
|
||||||
|
|
||||||
### 2.2
|
### 2.2
|
||||||
|
|
||||||
@@ -582,11 +582,11 @@ components:
|
|||||||
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 `activity` name `NO_ACTIVITY`
|
To select more than one activity, supply a comma-separated list. A spot matches if any of its activities are
|
||||||
matches spots with no activity set. You can use `activity=NO_ACTIVITY` to specifically only return generic
|
in the list. The special `activity` name `NO_ACTIVITY` matches spots with no activity set. You can use
|
||||||
spots with no associated activity. You can also use combinations to request for example POTA + no
|
`activity=NO_ACTIVITY` to specifically only return generic spots with no associated activity. You can also use
|
||||||
activity, but reject other activities. If you want to request 'every activity and not No Activity', see the
|
combinations to request for example POTA + no activity, but reject other activities. If you want to request
|
||||||
`needs_activity` query parameter for a shortcut.
|
'every activity but not No Activity', see the `needs_activity` query parameter for a shortcut.
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ActivityNameIncludingNoActivity"
|
$ref: "#/components/schemas/ActivityNameIncludingNoActivity"
|
||||||
SpotNeedsActivity:
|
SpotNeedsActivity:
|
||||||
@@ -746,8 +746,7 @@ components:
|
|||||||
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_ACTIVITY' can be included to return alerts
|
activity, supply a comma-separated list. An alert matches if any of its activities are in the list.
|
||||||
specifically without an associated activity.
|
|
||||||
schema:
|
schema:
|
||||||
$ref: "#/components/schemas/ActivityNameIncludingNoActivity"
|
$ref: "#/components/schemas/ActivityNameIncludingNoActivity"
|
||||||
AlertDxContinent:
|
AlertDxContinent:
|
||||||
@@ -1334,9 +1333,16 @@ 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"
|
||||||
activity:
|
activities:
|
||||||
description: Activity, e.g. outdoor activity programme such as POTA
|
type: array
|
||||||
|
uniqueItems: true
|
||||||
|
items:
|
||||||
$ref: "#/components/schemas/ActivityName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
|
description: >
|
||||||
|
Activities, e.g. outdoor activity programmes such as POTA. There may be more than one, e.g. for a POTA plus
|
||||||
|
WWFF dual activation, or none. Each activity appears at most once. The first activity is the "primary" one,
|
||||||
|
e.g. the activity of the programme the spot came from, and is the one used to choose the icon.
|
||||||
|
example: [ "POTA", "WWFF" ]
|
||||||
activity_refs:
|
activity_refs:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
@@ -1382,10 +1388,9 @@ components:
|
|||||||
submit_upstream:
|
submit_upstream:
|
||||||
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
|
||||||
than only adding it to this Spothole server. Requires `activity`, at least one `activity_refs`
|
to this Spothole server. Requires `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
|
||||||
upstream_provider:
|
upstream_provider:
|
||||||
type: string
|
type: string
|
||||||
@@ -1522,9 +1527,16 @@ 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"
|
||||||
activity:
|
activities:
|
||||||
description: Activity, e.g. outdoor activity programme such as POTA
|
type: array
|
||||||
|
uniqueItems: true
|
||||||
|
items:
|
||||||
$ref: "#/components/schemas/ActivityName"
|
$ref: "#/components/schemas/ActivityName"
|
||||||
|
description: >
|
||||||
|
Activities, e.g. outdoor activity programmes such as POTA. There may be more than one, e.g. for a POTA and
|
||||||
|
WWFF dual activation, or none. Each activity appears at most once. The first activity is the "primary" one,
|
||||||
|
e.g. the activity of the programme the spot came from, and is the one used to choose the icon.
|
||||||
|
example: [ "POTA", "WWFF" ]
|
||||||
activity_refs:
|
activity_refs:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ 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 (activity !== "") spot["activity"] = activity;
|
if (activity !== "") spot["activities"] = [activity];
|
||||||
if (activityRef !== "") spot["activity_refs"] = [{activity: activity, id: activityRef}];
|
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;
|
||||||
|
|||||||
+4
-4
@@ -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["activity"] === "Contest") {
|
if (dx_calls_html === "" && a["activities"] != null && a["activities"].includes("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["activity"] === "DXpedition" && a["dx_country"] != null && a["dx_country"] !== "") {
|
if (a["activities"] != null && a["activities"].includes("DXpedition") && a["dx_country"] != null && a["dx_country"] !== "") {
|
||||||
dx_country_html = `<br/>${a["dx_country"]}`;
|
dx_country_html = `<br/>${a["dx_country"]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,8 +252,8 @@ 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["activity"]) {
|
if (a["activities"] != null && a["activities"].length > 0) {
|
||||||
activityText = a["activity"];
|
activityText = a["activities"].join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format activity refs
|
// Format activity refs
|
||||||
|
|||||||
+2
-2
@@ -276,8 +276,8 @@ function getTooltipText(s) {
|
|||||||
|
|
||||||
// Activity or fallback to source
|
// Activity or fallback to source
|
||||||
let activitySourceText = s["source"];
|
let activitySourceText = s["source"];
|
||||||
if (s["activity"]) {
|
if (s["activities"] != null && s["activities"].length > 0) {
|
||||||
activitySourceText = s["activity"];
|
activitySourceText = s["activities"].join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format activity refs
|
// Format activity refs
|
||||||
|
|||||||
+2
-2
@@ -329,8 +329,8 @@ function createNewTableRowsForSpot(s, highlightNew) {
|
|||||||
|
|
||||||
// Format activity
|
// Format activity
|
||||||
let activityText = "General DX";
|
let activityText = "General DX";
|
||||||
if (s["activity"]) {
|
if (s["activities"] != null && s["activities"].length > 0) {
|
||||||
activityText = s["activity"];
|
activityText = s["activities"].join(", ");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format activity refs
|
// Format activity refs
|
||||||
|
|||||||
@@ -142,19 +142,14 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
self.set_header("Content-Type", "application/json")
|
self.set_header("Content-Type", "application/json")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Reject if activity ref format incorrect for activity
|
# Reject if any activity ref format is incorrect for its activity
|
||||||
if (
|
for activity_ref in spot.activity_refs:
|
||||||
spot.activity
|
ref_regex = get_ref_regex_for_activity(activity_ref.activity) if activity_ref.activity else None
|
||||||
and spot.activity_refs
|
if activity_ref.id and ref_regex and not re.match(ref_regex, activity_ref.id):
|
||||||
and len(spot.activity_refs) > 0
|
|
||||||
and spot.activity_refs[0].id
|
|
||||||
and get_ref_regex_for_activity(spot.activity)
|
|
||||||
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.activity_refs[0].id}' does not look like a valid reference for {spot.activity}."
|
f"Error - '{activity_ref.id}' does not look like a valid reference for {activity_ref.activity}."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.set_header("Cache-Control", "no-store")
|
self.set_header("Cache-Control", "no-store")
|
||||||
@@ -171,7 +166,8 @@ 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.activity:
|
if not spot.activities:
|
||||||
|
# TODO when we allow spotting to cluster upstream, we need to remove this restriction
|
||||||
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")
|
||||||
@@ -201,7 +197,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.activity)
|
provider = self._find_provider(upstream_provider_name, spot.activities)
|
||||||
if provider:
|
if provider:
|
||||||
try:
|
try:
|
||||||
# Submit spot to the upstream provider
|
# Submit spot to the upstream provider
|
||||||
@@ -216,7 +212,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.activity if spot.activity else ''} spots."
|
upstream_warning = f"No enabled provider named '{upstream_provider_name}' supports upstream submission for {', '.join(spot.activities)} 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
|
||||||
@@ -242,11 +238,11 @@ class APISpotHandler(tornado.web.RequestHandler):
|
|||||||
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")
|
||||||
|
|
||||||
def _find_provider(self, provider_name, activity) -> SpotProvider | None:
|
def _find_provider(self, provider_name, activities) -> SpotProvider | None:
|
||||||
"""Find an enabled provider by name that can submit spots for the given activity."""
|
"""Find an enabled provider by name that can submit spots for at least one of the given activities."""
|
||||||
|
|
||||||
for p in self._spot_providers:
|
for p in self._spot_providers:
|
||||||
if p.enabled and p.name == provider_name and p.can_submit_spot(activity):
|
if p.enabled and p.name == provider_name and any(p.can_submit_spot(a) for a in activities):
|
||||||
return p
|
return p
|
||||||
return None
|
return 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.activity == ActivityName.DXPEDITION
|
ActivityName.DXPEDITION in alert.activities
|
||||||
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.activity == ActivityName.CONTEST
|
ActivityName.CONTEST in alert.activities
|
||||||
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"
|
||||||
):
|
):
|
||||||
@@ -187,13 +187,13 @@ def alert_allowed_by_query(alert, query):
|
|||||||
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 "activity":
|
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 at least one activity that matches one of
|
||||||
# The special activity "NO_ACTIVITY", when supplied in the list, matches alerts with no activity.
|
# them. 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_ACTIVITY" in activities
|
include_no_activity = "NO_ACTIVITY" in activities
|
||||||
if not alert.activity and not include_no_activity:
|
if not alert.activities and not include_no_activity:
|
||||||
return False
|
return False
|
||||||
if alert.activity and alert.activity not in activities:
|
if alert.activities and not any(a in activities for a in alert.activities):
|
||||||
return False
|
return False
|
||||||
case "dx_continent":
|
case "dx_continent":
|
||||||
dxconts = query.get(k).split(",")
|
dxconts = query.get(k).split(",")
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ _V2_TO_V3_QUERY_PARAMS = {
|
|||||||
# Values of query parameters renamed in v3
|
# Values of query parameters renamed in v3
|
||||||
_V2_TO_V3_QUERY_VALUES = {
|
_V2_TO_V3_QUERY_VALUES = {
|
||||||
"activity": {"NO_SIG": "NO_ACTIVITY"},
|
"activity": {"NO_SIG": "NO_ACTIVITY"},
|
||||||
"fields": {"sig": "activity", "sig_refs": "activity_refs"},
|
"fields": {"sig": "activities", "sig_refs": "activity_refs"},
|
||||||
}
|
}
|
||||||
# Keys of JSON objects in API responses renamed in v3
|
# Keys of JSON objects in API responses renamed in v3
|
||||||
_V3_TO_V2_RESPONSE_KEYS = {
|
_V3_TO_V2_RESPONSE_KEYS = {
|
||||||
@@ -54,19 +54,28 @@ class V2CompatibilityWrapper(CompatibilityWrapper):
|
|||||||
return rename_keys_and_values(obj, _V3_TO_V2_RESPONSE_KEYS, _V3_TO_V2_RESPONSE_VALUES)
|
return rename_keys_and_values(obj, _V3_TO_V2_RESPONSE_KEYS, _V3_TO_V2_RESPONSE_VALUES)
|
||||||
|
|
||||||
|
|
||||||
class V2APISpotsHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APISpotsHandler):
|
class V2SpotsAlertsCompatibilityWrapper(V2CompatibilityWrapper):
|
||||||
|
"""Extra translation for spots and alerts. In v3 these have a list of "activities" rather than a single activity,
|
||||||
|
so for v2 we collapse this back down to a single value using the first activity in the list. This must happen
|
||||||
|
before the generic key renaming, which would otherwise rename "activities" to "sigs" as it does for /options."""
|
||||||
|
|
||||||
|
def translate_response_object(self, obj):
|
||||||
|
return super().translate_response_object(collapse_activities(obj))
|
||||||
|
|
||||||
|
|
||||||
|
class V2APISpotsHandler(V2SpotsAlertsCompatibilityWrapper, RequestCompatibilityWrapper, APISpotsHandler):
|
||||||
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
class V2APISpotsStreamHandler(V2CompatibilityWrapper, StreamCompatibilityWrapper, APISpotsStreamHandler):
|
class V2APISpotsStreamHandler(V2SpotsAlertsCompatibilityWrapper, StreamCompatibilityWrapper, APISpotsStreamHandler):
|
||||||
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
class V2APIAlertsHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIAlertsHandler):
|
class V2APIAlertsHandler(V2SpotsAlertsCompatibilityWrapper, RequestCompatibilityWrapper, APIAlertsHandler):
|
||||||
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
class V2APIAlertsStreamHandler(V2CompatibilityWrapper, StreamCompatibilityWrapper, APIAlertsStreamHandler):
|
class V2APIAlertsStreamHandler(V2SpotsAlertsCompatibilityWrapper, StreamCompatibilityWrapper, APIAlertsStreamHandler):
|
||||||
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
"""No special handling for this, the compatibility wrapper will handle translation to and from the later version."""
|
||||||
|
|
||||||
|
|
||||||
@@ -118,7 +127,8 @@ class V2APISpotHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIS
|
|||||||
|
|
||||||
spot_data = dict(spot_data)
|
spot_data = dict(spot_data)
|
||||||
if "sig" in spot_data:
|
if "sig" in spot_data:
|
||||||
spot_data["activity"] = spot_data.pop("sig")
|
sig = spot_data.pop("sig")
|
||||||
|
spot_data["activities"] = [sig] if sig else []
|
||||||
if "sig_refs" in spot_data:
|
if "sig_refs" in spot_data:
|
||||||
spot_data["activity_refs"] = spot_data.pop("sig_refs")
|
spot_data["activity_refs"] = spot_data.pop("sig_refs")
|
||||||
if isinstance(spot_data.get("activity_refs"), list):
|
if isinstance(spot_data.get("activity_refs"), list):
|
||||||
@@ -130,3 +140,18 @@ class V2APISpotHandler(V2CompatibilityWrapper, RequestCompatibilityWrapper, APIS
|
|||||||
refs.append(ref)
|
refs.append(ref)
|
||||||
spot_data["activity_refs"] = refs
|
spot_data["activity_refs"] = refs
|
||||||
return spot_data
|
return spot_data
|
||||||
|
|
||||||
|
|
||||||
|
def collapse_activities(obj):
|
||||||
|
"""Utility method to replace the "activities" list in a spot or alert JSON object with a single "activity" value,
|
||||||
|
being the first activity in the list, or None if there are none. The object can be a single spot/alert dict, or a
|
||||||
|
list of them. Anything else is returned untouched. Used to translate v3's list of activities to the single sig
|
||||||
|
expected in v2 API calls."""
|
||||||
|
|
||||||
|
if isinstance(obj, list):
|
||||||
|
return [collapse_activities(i) for i in obj]
|
||||||
|
if isinstance(obj, dict) and "activities" in obj:
|
||||||
|
obj = dict(obj)
|
||||||
|
activities = obj.pop("activities")
|
||||||
|
obj["activity"] = activities[0] if activities else None
|
||||||
|
return obj
|
||||||
@@ -197,19 +197,19 @@ def spot_allowed_by_query(spot, query):
|
|||||||
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 "activity":
|
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 at least one activity that matches one of
|
||||||
# The special activity "NO_ACTIVITY", when supplied in the list, matches spots with no activity.
|
# them. 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_ACTIVITY" in activities
|
include_no_activity = "NO_ACTIVITY" in activities
|
||||||
if not spot.activity and not include_no_activity:
|
if not spot.activities and not include_no_activity:
|
||||||
return False
|
return False
|
||||||
if spot.activity and spot.activity not in activities:
|
if spot.activities and not any(a in activities for a in spot.activities):
|
||||||
return False
|
return False
|
||||||
case "needs_activity":
|
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_ACTIVITY" parameter to the "activity" 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.activity:
|
if needs_activity and not spot.activities:
|
||||||
return False
|
return False
|
||||||
case "needs_activity_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.
|
||||||
|
|||||||
Reference in New Issue
Block a user