Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions .github/workflows/ci-cd.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
name: CI/CD Pipeline

on:
push:
branches:
- "**"
tags:
- "*"
pull_request:
on: push

jobs:
pre-commit:
Expand Down Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
# Change Log

> **Notice**<br>
> [!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.

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
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/user.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
16 changes: 14 additions & 2 deletions structures/managers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
50 changes: 27 additions & 23 deletions structures/models/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down
10 changes: 6 additions & 4 deletions structures/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down
22 changes: 10 additions & 12 deletions structures/tests/models/test_notifications_2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
26 changes: 24 additions & 2 deletions structures/tests/test_managers_1.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading