diff --git a/core/activity_utils.py b/core/activity_utils.py index 1b5cfaf..d641360 100644 --- a/core/activity_utils.py +++ b/core/activity_utils.py @@ -11,6 +11,15 @@ def get_ref_regex_for_activity(activity): return None +def get_icon_for_activity(activity): + """Utility function to get the icon for a named activity. If no match is found, None will be returned.""" + + for a in ACTIVITIES: + if a.name.upper() == activity.upper(): + return a.icon + return None + + def get_activity_name_from_comment_name(activity): """Utility function to get the name of an activity from its "comment name". Generally these will be the same but there are some cases (e.g. is "TOTA" Towers, Tiles or Toilets?) where we need to transform one to the diff --git a/core/constants.py b/core/constants.py index 15c84e7..8e1f934 100644 --- a/core/constants.py +++ b/core/constants.py @@ -12,6 +12,22 @@ HAMQTH_PRG = f"Spothole v{SOFTWARE_VERSION} operated by {SERVER_OWNER_CALLSIGN}" # Activities ACTIVITIES = [ + Activity( + name="Contest", + comment_names=["CONTEST"], + description="Contest", + sig_type=ActivityType.TRADITIONAL, + icon="fa-trophy", + refs_globally_unique=False, + ), + Activity( + name="DXpedition", + comment_names=[], + description="Radio expedition to a remote location", + sig_type=ActivityType.TRADITIONAL, + icon="fa-book-atlas", + refs_globally_unique=False, + ), Activity( name="Satellite", comment_names=[], diff --git a/core/enums.py b/core/enums.py index 9d1c70f..e985ac7 100644 --- a/core/enums.py +++ b/core/enums.py @@ -112,15 +112,6 @@ class ActivityRefType(str, Enum): TOILET = "TOILET" -class AlertType(str, Enum): - """Type of an alert.""" - - XOTA = "XOTA" - SATELLITE = "SATELLITE" - DXPEDITION = "DXPEDITION" - CONTEST = "CONTEST" - - class ActivityType(str, Enum): """Type of an activity. Used to group them in the web UI.""" diff --git a/data/alert.py b/data/alert.py index 4086df2..aba9cfb 100644 --- a/data/alert.py +++ b/data/alert.py @@ -7,8 +7,9 @@ from datetime import datetime, timedelta import pytz from core.activity_lookup_helper import populate_missing_activity_ref_info +from core.activity_utils import get_icon_for_activity from core.call_lookup_helper import get_call_info -from core.enums import AlertType, Continent +from core.enums import Continent from core.utils import get_flag_for_dxcc logger = logging.getLogger(__name__) @@ -54,8 +55,6 @@ class Alert: end_time_iso: str | None = None # Comment made by the alerter, if any comment: str | None = None - # The type of alert this is: xOTA, DXpedition, or Contest. - alert_type: AlertType | None = None # A URL link to more information, if any url: str | None = None @@ -153,16 +152,10 @@ 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 spot should be the icon of the first activity ref if present, otherwise a radio tower + # Icon for the alert should be the icon of its activity if known, otherwise a radio tower self.icon = "fa-tower-cell" - if self.alert_type == AlertType.DXPEDITION: - self.icon = "fa-globe-africa" - elif self.alert_type == AlertType.CONTEST: - self.icon = "fa-trophy" - elif self.alert_type == AlertType.SATELLITE: - self.icon = "fa-satellite" - elif self.sig_refs and self.sig_refs[0].icon: - self.icon = self.sig_refs[0].icon + if self.sig and (activity_icon := get_icon_for_activity(self.sig)): + self.icon = activity_icon except Exception: logger.exception("Exception while inferring missing data from spot") diff --git a/data/spot.py b/data/spot.py index 0a27103..78a52d3 100644 --- a/data/spot.py +++ b/data/spot.py @@ -13,6 +13,7 @@ from core.activity_lookup_helper import populate_missing_activity_ref_info from core.activity_utils import ( ANY_ACTIVITY_REGEX, get_activity_name_from_comment_name, + get_icon_for_activity, get_ref_regex_for_activity, ) from core.call_lookup_helper import get_call_info @@ -378,16 +379,10 @@ class Spot: logger.info(f"Seen a new propagation mode tag not yet in the system: {mode_tag}") # Set activities based on propagation mode - if self.propagation_mode == "Satellite": - if not self.sig: - self.sig = "AMSAT" - if not any(activity_ref.sig == "AMSAT" for activity_ref in self.sig_refs): - self.sig_refs.append(ActivityRef(sig="AMSAT")) - if self.propagation_mode == "Earth-Moon-Earth": - if not self.sig: - self.sig = "EME" - if not any(activity_ref.sig == "EME" for activity_ref in self.sig_refs): - self.sig_refs.append(ActivityRef(sig="EME")) + if self.propagation_mode == "Satellite" and not self.sig: + self.sig = "Satellite" + if self.propagation_mode == "Earth-Moon-Earth" and not self.sig: + self.sig = "EME" # Parse "de_grid -> dx_grid" structures from the comment if self.comment: @@ -499,10 +494,10 @@ 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 the first activity ref if present, otherwise a radio tower + # Icon for the spot should be the icon of its activity if known, otherwise a radio tower self.icon = "fa-tower-cell" - if self.sig_refs and self.sig_refs[0].icon: - self.icon = self.sig_refs[0].icon + if self.sig and (activity_icon := get_icon_for_activity(self.sig)): + self.icon = activity_icon except Exception: logger.exception("Exception while inferring missing data from spot") diff --git a/providers/alert/bota.py b/providers/alert/bota.py index c278801..3a4bcf7 100644 --- a/providers/alert/bota.py +++ b/providers/alert/bota.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta import pytz from bs4 import BeautifulSoup -from core.enums import AlertType from data.activity_ref import ActivityRef from data.alert import Alert from providers.alert.http_alert_provider import HTTPAlertProvider @@ -56,9 +55,9 @@ class BOTA(HTTPAlertProvider): alert = Alert( source=self.name, dx_calls=[dx_call], + sig="BOTA", sig_refs=[ActivityRef(id=ref_name, sig="BOTA")], start_time=date_time.timestamp(), - alert_type=AlertType.XOTA, ) new_alerts.append(alert) diff --git a/providers/alert/hamsat.py b/providers/alert/hamsat.py index 6c297c3..3b3a688 100644 --- a/providers/alert/hamsat.py +++ b/providers/alert/hamsat.py @@ -2,7 +2,6 @@ from datetime import datetime import pytz -from core.enums import AlertType from data.activity_ref import ActivityRef from data.alert import Alert from providers.alert.http_alert_provider import HTTPAlertProvider @@ -35,10 +34,11 @@ class Hamsat(HTTPAlertProvider): dx_calls=[source_alert["callsign"].upper()], freqs_modes=freqs_modes, comment=source_alert["comment"], + sig="Satellite", # Fudge an activity ref to provide the remaining bits of data we need: the satellite and the operator's grid sig_refs=[ ActivityRef( - sig="AMSAT", + sig="Satellite", id=f"{source_alert['satellite']['name']} from {source_alert['grids'][0]}", ) ], @@ -48,7 +48,6 @@ class Hamsat(HTTPAlertProvider): end_time=datetime.strptime(source_alert["los_at"], "%Y-%m-%dT%H:%M:%SZ") .replace(tzinfo=pytz.UTC) .timestamp(), - alert_type=AlertType.SATELLITE, ) # Add to our list diff --git a/providers/alert/ng3k.py b/providers/alert/ng3k.py index 03170f5..a18ae36 100644 --- a/providers/alert/ng3k.py +++ b/providers/alert/ng3k.py @@ -6,7 +6,6 @@ import pytz from rss_parser import Parser from rss_parser.models.rss import RSS -from core.enums import AlertType from data.alert import Alert from providers.alert.http_alert_provider import HTTPAlertProvider @@ -89,7 +88,7 @@ class NG3K(HTTPAlertProvider): comment=f"{by}; {comment}; {qsl_info}", start_time=start_timestamp, end_time=end_timestamp, - alert_type=AlertType.DXPEDITION, + sig="DXpedition", ) # Add to our list. diff --git a/providers/alert/parksnpeaks.py b/providers/alert/parksnpeaks.py index cb326c1..4fad3a2 100644 --- a/providers/alert/parksnpeaks.py +++ b/providers/alert/parksnpeaks.py @@ -3,7 +3,6 @@ from datetime import datetime import pytz -from core.enums import AlertType from data.activity_ref import ActivityRef from data.alert import Alert from providers.alert.http_alert_provider import HTTPAlertProvider @@ -50,9 +49,9 @@ class ParksNPeaks(HTTPAlertProvider): dx_calls=[source_alert["CallSign"].upper()], freqs_modes=f"{source_alert['Freq']} {source_alert['MODE']}", comment=source_alert["Comments"], + sig=activity, sig_refs=activity_refs, start_time=start_time, - alert_type=AlertType.XOTA, ) # Log a warning for the developer if PnP gives us an unknown programme we've never seen before diff --git a/providers/alert/pota.py b/providers/alert/pota.py index aebbcde..4e79984 100644 --- a/providers/alert/pota.py +++ b/providers/alert/pota.py @@ -2,7 +2,6 @@ from datetime import datetime import pytz -from core.enums import AlertType from data.activity_ref import ActivityRef from data.alert import Alert from providers.alert.http_alert_provider import HTTPAlertProvider @@ -28,6 +27,7 @@ class POTA(HTTPAlertProvider): dx_calls=[source_alert["activator"].upper()], freqs_modes=source_alert["frequencies"], comment=source_alert["comments"], + sig="POTA", sig_refs=[ ActivityRef( id=source_alert["reference"], @@ -45,7 +45,6 @@ class POTA(HTTPAlertProvider): end_time=datetime.strptime(source_alert["endDate"] + source_alert["endTime"], "%Y-%m-%d%H:%M") .replace(tzinfo=pytz.UTC) .timestamp(), - alert_type=AlertType.XOTA, ) # Add to our list, but exclude any old spots that POTA can sometimes give us where even the end time is diff --git a/providers/alert/rsgb_ical_alert_provider.py b/providers/alert/rsgb_ical_alert_provider.py index 16d56b2..d4aa528 100644 --- a/providers/alert/rsgb_ical_alert_provider.py +++ b/providers/alert/rsgb_ical_alert_provider.py @@ -2,7 +2,7 @@ import re from icalendar import Event -from core.enums import AlertType, Continent +from core.enums import Continent from data.alert import Alert from providers.alert.ical_alert_provider import ICALAlertProvider @@ -69,7 +69,7 @@ class RSGBICALAlertProvider(ICALAlertProvider): comment=summary, start_time=start_timestamp, end_time=end_timestamp, - alert_type=AlertType.CONTEST, + sig="Contest", ) return alert diff --git a/providers/alert/sota.py b/providers/alert/sota.py index f260634..a234261 100644 --- a/providers/alert/sota.py +++ b/providers/alert/sota.py @@ -2,7 +2,6 @@ from datetime import datetime import pytz -from core.enums import AlertType from data.activity_ref import ActivityRef from data.alert import Alert from providers.alert.http_alert_provider import HTTPAlertProvider @@ -34,6 +33,7 @@ class SOTA(HTTPAlertProvider): dx_names=[source_alert["activatorName"].upper()], freqs_modes=source_alert["frequency"], comment=source_alert["comments"], + sig="SOTA", sig_refs=[ ActivityRef( id=f"{source_alert['associationCode']}/{source_alert['summitCode']}", @@ -45,7 +45,6 @@ class SOTA(HTTPAlertProvider): start_time=datetime.strptime(source_alert["dateActivated"], "%Y-%m-%dT%H:%M:%SZ") .replace(tzinfo=pytz.UTC) .timestamp(), - alert_type=AlertType.XOTA, ) # Add to our list diff --git a/providers/alert/wa7bnm.py b/providers/alert/wa7bnm.py index 498c194..a2c3b75 100644 --- a/providers/alert/wa7bnm.py +++ b/providers/alert/wa7bnm.py @@ -1,6 +1,5 @@ from icalendar import Event -from core.enums import AlertType from data.alert import Alert from providers.alert.ical_alert_provider import ICALAlertProvider @@ -35,7 +34,7 @@ class WA7BNM(ICALAlertProvider): url=url, start_time=start_timestamp, end_time=end_timestamp, - alert_type=AlertType.CONTEST, + sig="Contest", ) return alert diff --git a/providers/alert/wwff.py b/providers/alert/wwff.py index d3b32f0..1bea3a4 100644 --- a/providers/alert/wwff.py +++ b/providers/alert/wwff.py @@ -2,7 +2,6 @@ from datetime import datetime import pytz -from core.enums import AlertType from data.activity_ref import ActivityRef from data.alert import Alert from providers.alert.http_alert_provider import HTTPAlertProvider @@ -28,6 +27,7 @@ class WWFF(HTTPAlertProvider): dx_calls=[source_alert["activator_call"].upper()], freqs_modes=f"{source_alert['band']} {source_alert['mode']}", comment=source_alert["remarks"], + sig="WWFF", sig_refs=[ActivityRef(id=source_alert["reference"], sig="WWFF")], start_time=datetime.strptime(source_alert["utc_start"], "%Y-%m-%d %H:%M:%S") .replace(tzinfo=pytz.UTC) @@ -35,7 +35,6 @@ class WWFF(HTTPAlertProvider): end_time=datetime.strptime(source_alert["utc_end"], "%Y-%m-%d %H:%M:%S") .replace(tzinfo=pytz.UTC) .timestamp(), - alert_type=AlertType.XOTA, ) # Add to our list diff --git a/static/apidocs/openapi.yml b/static/apidocs/openapi.yml index 4a03b3e..b65f878 100644 --- a/static/apidocs/openapi.yml +++ b/static/apidocs/openapi.yml @@ -14,7 +14,12 @@ info: Spothole's source code is located at https://git.ianrenton.com/ian/spothole and the README there provides setup instructions if you would like to run your own copy. A demonstration server of Spothole is located at https://spothole.app. The README also contains some examples of how you could query the API of the demonstration server to integrate the data into your own apps. ## Changelog + + ### 2.2 + * Renamed AMSAT SIG to "Satellite" as AMSAT is a specific organisation not just a general term for satellite QSOs + * Removed `alert_type` from alert data. Contest, DXpedition and Satellite alerts now give those values in `sig` instead, alongside the existing outdoor activity programmes. Teeeeechnically a breaking change but AlertType is so new I doubt anyone is using it yet, so slipped this one in anyway. Sorry :) + ### 2.1 * Added AMSAT, EME, DTMBA, FEA, BIWOTA, COTA & PGA SIGs @@ -952,14 +957,6 @@ components: - TOILET example: PARK - AlertType: - type: string - enum: - - XOTA - - DXPEDITION - - CONTEST - example: XOTA - Continent: type: string enum: @@ -1489,9 +1486,6 @@ components: items: $ref: '#/components/schemas/ActivityRef' description: Activity references. We allow multiple here for e.g. n-fer activations, unlike ADIF SIG_INFO. Still named "sig_refs" in the API for backwards compatibility. - alert_type: - description: "The type of alert this is: xOTA, DXpedition, or Contest." - $ref: "#/components/schemas/AlertType" url: type: string description: A URL linking to more information about the alert, e.g. DXpedition or contest info. diff --git a/static/js/alerts.js b/static/js/alerts.js index fa73104..085c4d5 100644 --- a/static/js/alerts.js +++ b/static/js/alerts.js @@ -54,7 +54,7 @@ function updateTable() { const showDX = $("#tableShowDX")[0].checked; const showFreqsModes = $("#tableShowFreqsModes")[0].checked; const showComment = $("#tableShowComment")[0].checked; - const showType = $("#tableShowType")[0].checked; + const showActivity = $("#tableShowActivity")[0].checked; const showRef = $("#tableShowRef")[0].checked; // Populate table with headers @@ -75,11 +75,11 @@ function updateTable() { if (showComment) { table.find('thead tr').append(`Comment`); } - if (showType) { - table.find('thead tr').append(`Type`); + if (showActivity) { + table.find('thead tr').append(`Activity`); } if (showRef) { - table.find('thead tr').append(`Ref.`); + table.find('thead tr').append(`Reference`); } table.find('tbody').empty(); @@ -151,7 +151,7 @@ function addAlertRowsToTable(tbody, alerts) { const showDX = $("#tableShowDX")[0].checked; const showFreqsModes = $("#tableShowFreqsModes")[0].checked; const showComment = $("#tableShowComment")[0].checked; - const showType = $("#tableShowType")[0].checked; + const showActivity = $("#tableShowActivity")[0].checked; const showRef = $("#tableShowRef")[0].checked; // Get times for the alert, and convert to local time if necessary. @@ -210,14 +210,14 @@ function addAlertRowsToTable(tbody, alerts) { if (a["dx_calls"] != null) { dx_calls_html = a["dx_calls"].map(call => `${call}`).join(", "); } - if (dx_calls_html === "" && a["alert_type"] === "CONTEST") { + if (dx_calls_html === "" && a["sig"] === "Contest") { // Contest = true and no DX callsigns, so display "Contest" dx_calls_html = "Contest" } // Format DXpedition country let dx_country_html = ""; - if (a["alert_type"] === "DXPEDITION" && a["dx_country"] != null && a["dx_country"] !== "") { + if (a["sig"] === "DXpedition" && a["dx_country"] != null && a["dx_country"] !== "") { dx_country_html = `
${a["dx_country"]}`; } @@ -250,20 +250,10 @@ function addAlertRowsToTable(tbody, alerts) { } - // Type, activity or fallback to source - let activityTypeText = a["source"]; - if (a["alert_type"] === "CONTEST") { - activityTypeText = "Contest"; - } else if (a["alert_type"] === "DXPEDITION") { - activityTypeText = "DXpedition"; - } else if (a["alert_type"] === "SATELLITE") { - activityTypeText = "Satellite"; - } else if (a["alert_type"] === "XOTA") { - if (a["sig"]) { - activityTypeText = a["sig"]; - } else { - activityTypeText = "xOTA"; - } + // Activity or fallback to "General DX" + let activityText = "General DX"; + if (a["sig"]) { + activityText = a["sig"]; } // Format activity refs @@ -296,8 +286,8 @@ function addAlertRowsToTable(tbody, alerts) { if (showComment) { $tr.append(`${commentText}`); } - if (showType) { - $tr.append(` ${activityTypeText}`); + if (showActivity) { + $tr.append(` ${activityText}`); } if (showRef) { $tr.append(`${activityRefs}`); @@ -314,7 +304,7 @@ function addAlertRowsToTable(tbody, alerts) { } const $td2 = $(""); - if (showType) { + if (showActivity) { $td2.append(` `); } if (showRef) { diff --git a/static/js/spots.js b/static/js/spots.js index 649d5f7..31be50d 100644 --- a/static/js/spots.js +++ b/static/js/spots.js @@ -128,7 +128,7 @@ function updateTable() { const showComment = $("#tableShowComment")[0].checked; const showBearing = $("#tableShowBearing")[0].checked && userPos != null; const showDistance = $("#tableShowDistance")[0].checked && userPos != null; - const showType = $("#tableShowType")[0].checked; + const showActivity = $("#tableShowActivity")[0].checked; const showRef = $("#tableShowRef")[0].checked; const showDE = $("#tableShowDE")[0].checked; const showWorkedCheckbox = $("#tableShowWorkedCheckbox")[0].checked; @@ -157,11 +157,11 @@ function updateTable() { if (showDistance) { table.find('thead tr').append(`Distance`); } - if (showType) { - table.find('thead tr').append(`Type`); + if (showActivity) { + table.find('thead tr').append(`Activity`); } if (showRef) { - table.find('thead tr').append(`Ref.`); + table.find('thead tr').append(`Reference`); } if (showDE) { table.find('thead tr').append(`DE`); @@ -208,7 +208,7 @@ function createNewTableRowsForSpot(s, highlightNew) { const showComment = $("#tableShowComment")[0].checked; const showBearing = $("#tableShowBearing")[0].checked && userPos != null; const showDistance = $("#tableShowDistance")[0].checked && userPos != null; - const showType = $("#tableShowType")[0].checked; + const showActivity = $("#tableShowActivity")[0].checked; const showRef = $("#tableShowRef")[0].checked; const showDE = $("#tableShowDE")[0].checked; const showWorkedCheckbox = $("#tableShowWorkedCheckbox")[0].checked; @@ -327,10 +327,10 @@ function createNewTableRowsForSpot(s, highlightNew) { } } - // Format "type" (activity or fallback to source) - let typeText = s["source"]; + // Format activity + let activityText = "General DX"; if (s["sig"]) { - typeText = s["sig"]; + activityText = s["sig"]; } // Format activity refs @@ -397,8 +397,8 @@ function createNewTableRowsForSpot(s, highlightNew) { if (showDistance) { $tr.append(`${distanceText}`); } - if (showType) { - $tr.append(` ${typeText}`); + if (showActivity) { + $tr.append(` ${activityText}`); } if (showRef) { $tr.append(`${activityRefs}`); @@ -410,7 +410,7 @@ function createNewTableRowsForSpot(s, highlightNew) { $tr.append(`${workedCheckbox}`); } - // Second row for mobile view only, containing type, ref & comment + // Second row for mobile view only, containing activity, ref & comment const $tr2 = $(""); // Apply styles as per the first row @@ -429,8 +429,8 @@ function createNewTableRowsForSpot(s, highlightNew) { const $td2 = $(""); const $td2floatleft = $(`
`); - if (showType) { - $td2floatleft.append(` ${typeText} `); + if (showActivity) { + $td2floatleft.append(` ${activityText} `); } if (showRef) { $td2floatleft.append(`${activityRefs} `); diff --git a/static/js/spotsbandsandmap.js b/static/js/spotsbandsandmap.js index ca06e73..7854a32 100644 --- a/static/js/spotsbandsandmap.js +++ b/static/js/spotsbandsandmap.js @@ -60,7 +60,7 @@ function generateActivitiesMultiToggleFilterCard(activity_options) { const domSafeName = o["name"].replace(/^[^A-Za-z0-9]+|[^\w]+/gi, ""); $grid.append(`
`); }); - $body.append(`
`); + $body.append(`
`); $body.append($grid); }); diff --git a/templates/cards/table_columns_alerts.html b/templates/cards/table_columns_alerts.html index fb07cb6..5cddf7d 100644 --- a/templates/cards/table_columns_alerts.html +++ b/templates/cards/table_columns_alerts.html @@ -39,16 +39,16 @@
- - + +
- +
diff --git a/templates/cards/table_columns_spots.html b/templates/cards/table_columns_spots.html index ba032e1..e23fde7 100644 --- a/templates/cards/table_columns_spots.html +++ b/templates/cards/table_columns_spots.html @@ -53,16 +53,16 @@
- - + +
- +
diff --git a/webserver/handlers/api/alerts.py b/webserver/handlers/api/alerts.py index 3420c49..4861333 100644 --- a/webserver/handlers/api/alerts.py +++ b/webserver/handlers/api/alerts.py @@ -9,7 +9,6 @@ import tornado_eventsource.handler from tornado import httputil from tornado.web import Application -from core.enums import AlertType from core.utils import safe_json_dumps from data.lookup_credentials import extract_credentials @@ -169,13 +168,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 # always passes the check. if ( - alert.alert_type == AlertType.DXPEDITION + alert.sig == "DXpedition" and "dxpeditions_skip_max_duration_check" in query and query.get("dxpeditions_skip_max_duration_check").upper() == "TRUE" ): continue if ( - alert.alert_type == AlertType.CONTEST + alert.sig == "Contest" and "contests_skip_max_duration_check" in query and query.get("contests_skip_max_duration_check").upper() == "TRUE" ):