diff --git a/.github/workflows/ci-cd.yaml b/.github/workflows/ci-cd.yaml index da83aaa..a0490d6 100644 --- a/.github/workflows/ci-cd.yaml +++ b/.github/workflows/ci-cd.yaml @@ -1,12 +1,6 @@ name: CI/CD Pipeline -on: - push: - branches: - - "**" - tags: - - "*" - pull_request: +on: push jobs: pre-commit: @@ -107,7 +101,7 @@ jobs: pip install tox tox -e docs - deploy-pypi: + publish: needs: [test, test-docs] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 93a1712..c9e4e08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -> **Notice**
+> [!WARNING] > This file has been decommissioned. Changes are now documented on the GitHub releases page. All notable changes to this project will be documented in this file. @@ -8,8 +8,6 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -## [Unreleased] - yyyy-mm-dd - ## [4.0.2] - 2026-07-13 ### Changed diff --git a/Makefile b/Makefile index c885211..2bfd281 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +# This makefile provides tools for developers. +# +# Note that it requires you to have a .env file defined with the path to your manage.py file +# The syntax is: MANAGE_PY_PATH = /path/to/manage.py + -include .env export diff --git a/README.md b/README.md index bdc76ac..adccf8d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Structures adds the following main features to Alliance Auth: - Sovereignty - Wars - Corporation membership changes -- Automatically adds timers from relevant notifications to [Alliance Auth Structure Timers](https://allianceauth.readthedocs.io/en/latest/features/apps/timerboard.html) or [Structure Timers II](https://gitlab.com/ErikKalkoken/aa-structuretimers) app (if installed) +- Automatically adds timers from relevant notifications to [Alliance Auth Structure Timers](https://allianceauth.readthedocs.io/en/latest/features/apps/timerboard.html) or [Structure Timers II](https://github.com/AllianceAuth-Apps/aa-structuretimers) app (if installed) - Fittings, contents of Upwell structures (e.g. fuel blocks and ammo) and current fuel usage - Additional structure notifications generated by the app: - Configurable alerts to warn about fuel running at in structures and POSes, and which can replace the ESI notifications diff --git a/docs/user.md b/docs/user.md index 0b0d598..903885f 100644 --- a/docs/user.md +++ b/docs/user.md @@ -125,7 +125,7 @@ Structures has the ability to measure the average notification delay of your sys ## Admin status notifications -Many alliances are relying that the structure services - i.e. getting attack and fuel notifications on Discord. However, outages can occur, e.g. when tokens become invalid or the Eve Online API server (ESI) has issues. To give alliances the ability to fix outages quickly, Structures has a build in service monitoring capability. Should an issue occur it will automatically send an Auth notification to admins. When combined with the app [Discord Notify](https://gitlab.com/ErikKalkoken/aa-discordnotify), those notifications will be forwarded immediately to Discord, allowing admins to take quick action to resolve any issues. +Many alliances are relying that the structure services - i.e. getting attack and fuel notifications on Discord. However, outages can occur, e.g. when tokens become invalid or the Eve Online API server (ESI) has issues. To give alliances the ability to fix outages quickly, Structures has a build in service monitoring capability. Should an issue occur it will automatically send an Auth notification to admins. When combined with the app [Discord Notify](https://github.com/AllianceAuth-Apps/aa-discordnotify), those notifications will be forwarded immediately to Discord, allowing admins to take quick action to resolve any issues. There are currently two types of issue related admin notifications: diff --git a/structures/managers.py b/structures/managers.py index 9c355b1..16484c9 100644 --- a/structures/managers.py +++ b/structures/managers.py @@ -6,6 +6,7 @@ import datetime as dt import itertools +import re from typing import TYPE_CHECKING, Any, Iterable, Optional, Set, Tuple from django.contrib.auth.models import User @@ -545,10 +546,21 @@ def update_or_create_for_sov(self) -> Tuple[Any, bool]: ) -class WebhookManager(WebhookBaseManager): +class WebhookQuerySet(models.QuerySet): + def filter_notification_type(self, notif_type: NotificationType): + """Return Queryset with a filter for a notification type.""" + return self.filter( + notification_types__regex=rf"(^|,){re.escape(notif_type)}(,|$)" + ) + + +class WebhookManagerBase(WebhookBaseManager): def enabled_notification_types(self) -> Set[str]: - """Set of all currently enabled notification types.""" + """Return currently enabled notification types.""" notif_types_list = list( self.filter(is_active=True).values_list("notification_types", flat=True) ) return set(itertools.chain(*notif_types_list)) + + +WebhookManager = WebhookManagerBase.from_queryset(WebhookQuerySet) diff --git a/structures/models/notifications.py b/structures/models/notifications.py index 1216f9c..ec4bc0c 100644 --- a/structures/models/notifications.py +++ b/structures/models/notifications.py @@ -309,18 +309,20 @@ def relevant_webhooks(self) -> models.QuerySet: structures_qs = Structure.objects.none() else: structures_qs = self.calc_related_structures() + if ( structures_qs.exists() and structures_qs.filter(webhooks__isnull=False).count() == 1 ): - webhooks_qs = structures_qs.first().webhooks.filter( - notification_types__contains=self.notif_type, is_active=True - ) + webhooks_qs = structures_qs.first().webhooks else: - webhooks_qs = self.owner.webhooks.filter( - notification_types__contains=self.notif_type, is_active=True - ) - return webhooks_qs + webhooks_qs = self.owner.webhooks + + qs = webhooks_qs.filter(is_active=True).filter_notification_type( + self.notif_type + ) + + return qs def calc_related_structures(self) -> models.QuerySet[Structure]: """Identify structures this notification is related to. @@ -739,8 +741,7 @@ def send_new_notifications(self, force: bool = False) -> None: """Send new fuel notifications based on this config.""" raise NotImplementedError() - @staticmethod - def relevant_webhooks() -> models.QuerySet: + def relevant_webhooks(self) -> models.QuerySet: """Webhooks relevant for processing fuel notifications based on this config.""" raise NotImplementedError() @@ -831,18 +832,19 @@ def send_new_notifications(self, force: bool = False) -> None: if created or force: notif.send_generated_notification() - @staticmethod - def relevant_webhooks() -> models.QuerySet: + def relevant_webhooks(self) -> models.QuerySet: """Webhooks relevant for processing fuel notifications based on this config.""" qs = ( - Webhook.objects.filter(is_active=True) - .filter(Q(owners__isnull=False) | Q(structures__isnull=False)) - .filter( - Q(notification_types__contains=NotificationType.STRUCTURE_FUEL_ALERT) - | Q( - notification_types__contains=NotificationType.TOWER_RESOURCE_ALERT_MSG + ( + Webhook.objects.filter_notification_type( + NotificationType.STRUCTURE_FUEL_ALERT + ) + | Webhook.objects.filter_notification_type( + NotificationType.TOWER_RESOURCE_ALERT_MSG ) ) + .filter(is_active=True) + .filter(Q(owners__isnull=False) | Q(structures__isnull=False)) .distinct() ) return qs @@ -887,13 +889,15 @@ def send_new_notifications(self, force: bool = False) -> None: if created or force: notif.send_generated_notification() - @staticmethod - def relevant_webhooks() -> models.QuerySet: + def relevant_webhooks(self) -> models.QuerySet: """Webhooks relevant for processing jump fuel notifications based on this config.""" - return Webhook.objects.filter( - is_active=True, - notification_types__contains=NotificationType.STRUCTURE_JUMP_FUEL_ALERT, - ).filter(Q(owners__isnull=False) | Q(structures__isnull=False)) + return ( + Webhook.objects.filter_notification_type( + NotificationType.STRUCTURE_JUMP_FUEL_ALERT + ) + .filter(is_active=True) + .filter(Q(owners__isnull=False) | Q(structures__isnull=False)) + ) class BaseFuelAlert(models.Model): diff --git a/structures/tasks.py b/structures/tasks.py index 54a90e0..8127715 100644 --- a/structures/tasks.py +++ b/structures/tasks.py @@ -215,15 +215,17 @@ def send_new_notifications_for_owner(owner_pk: int): @shared_task(time_limit=STRUCTURES_TASKS_TIME_LIMIT) def send_structure_fuel_notifications_for_config(config_pk: int): """Send structure fuel notifications for a config.""" - FuelAlertConfig.objects.get(pk=config_pk).send_new_notifications() - send_queued_messages_for_webhooks(FuelAlertConfig.relevant_webhooks()) + config = FuelAlertConfig.objects.get(pk=config_pk) + config.send_new_notifications() + send_queued_messages_for_webhooks(config.relevant_webhooks()) @shared_task(time_limit=STRUCTURES_TASKS_TIME_LIMIT) def send_jump_fuel_notifications_for_config(config_pk: int): """Send jump fuel notifications for a config.""" - JumpFuelAlertConfig.objects.get(pk=config_pk).send_new_notifications() - send_queued_messages_for_webhooks(JumpFuelAlertConfig.relevant_webhooks()) + config = JumpFuelAlertConfig.objects.get(pk=config_pk) + config.send_new_notifications() + send_queued_messages_for_webhooks(config.relevant_webhooks()) def send_queued_messages_for_webhooks(webhooks: Iterable[Webhook]): diff --git a/structures/tests/models/test_notifications_2.py b/structures/tests/models/test_notifications_2.py index fb9cd37..824bf29 100644 --- a/structures/tests/models/test_notifications_2.py +++ b/structures/tests/models/test_notifications_2.py @@ -357,14 +357,13 @@ def test_should_return_correct_webhooks(self, mock_send_message): notification_types=[NotificationType.STRUCTURE_FUEL_ALERT], name="wrong_other_owner", ) - config = FuelAlertConfigFactory(start=48, end=0, repeat=12) + config = FuelAlertConfigFactory() + # when - qs = config.relevant_webhooks() + got = config.relevant_webhooks() + # then - relevant_webhook_pks = qs.values_list("pk", flat=True) - self.assertSetEqual( - set(relevant_webhook_pks), {self.webhook.pk, webhook_structure.pk} - ) + self.assertCountEqual(got, [self.webhook, webhook_structure]) @patch(MODULE_PATH + ".Webhook.send_message", spec=True) @@ -587,14 +586,13 @@ def test_should_return_correct_webhooks(self, mock_send_message): ) structure.webhooks.add(webhook_structure) WebhookFactory(notification_types=[NotificationType.STRUCTURE_JUMP_FUEL_ALERT]) - config = JumpFuelAlertConfigFactory(threshold=100) + config = JumpFuelAlertConfigFactory() + # when - qs = config.relevant_webhooks() + got = config.relevant_webhooks() + # then - relevant_webhook_pks = qs.values_list("pk", flat=True) - self.assertSetEqual( - set(relevant_webhook_pks), {webhook.pk, webhook_structure.pk} - ) + self.assertCountEqual(got, [webhook, webhook_structure]) class TestNotificationRelatedStructures(NoSocketsTestCase): diff --git a/structures/tests/test_managers_1.py b/structures/tests/test_managers_1.py index b81a26a..df5e5cd 100644 --- a/structures/tests/test_managers_1.py +++ b/structures/tests/test_managers_1.py @@ -887,8 +887,30 @@ def test_should_raise_error_for_unknown_space_type_when_trying_to_update(self): # self.assertEqual(structure.order, 50) -class TestWebhookManager(NoSocketsTestCase): - def test_should_return_enabled_notification_types(self): +class TestWebhookQuerySetFilterNotificationType(NoSocketsTestCase): + def test_should_only_include_correct_matches(self): + # given + nt = NotificationType.STRUCTURE_DESTROYED + nt_embedded = NotificationType.SOV_STRUCTURE_DESTROYED + nt_other_1 = NotificationType.WAR_DECLARE_WAR + nt_other_2 = NotificationType.MOONMINING_LASER_FIRED + wh_1 = WebhookFactory(notification_types=[nt]) + wh_2 = WebhookFactory(notification_types=[nt, nt_other_1]) + wh_3 = WebhookFactory(notification_types=[nt_other_1, nt]) + wh_4 = WebhookFactory(notification_types=[nt_other_1, nt, nt_other_2]) + WebhookFactory(notification_types=[nt_embedded]) + WebhookFactory(notification_types=[nt_other_1, nt_embedded]) + WebhookFactory(notification_types=[nt_embedded, nt_other_2]) + + # when + qs = Webhook.objects.filter_notification_type(nt) + + # then + self.assertCountEqual(qs, [wh_1, wh_2, wh_3, wh_4]) + + +class TestWebhookManagerEnabledNotificationTypes(NoSocketsTestCase): + def test_should_return_types(self): # given WebhookFactory( is_active=True,