From 18da34c4a145d1db236e3450e33563e5d9bbece7 Mon Sep 17 00:00:00 2001 From: Oli Date: Sun, 30 Apr 2023 10:13:22 +0100 Subject: [PATCH 1/2] feat(alerts): add alerts models, admin, script and service --- apps/alerts/__init__.py | 0 apps/alerts/admin.py | 29 +++++++++ apps/alerts/apps.py | 6 ++ apps/alerts/management/__init__.py | 0 apps/alerts/management/commands/__init__.py | 0 .../management/commands/scan_for_alerts.py | 27 ++++++++ apps/alerts/migrations/0001_initial.py | 42 ++++++++++++ apps/alerts/migrations/__init__.py | 0 apps/alerts/models.py | 46 +++++++++++++ apps/alerts/tests/__init__.py | 0 project/settings/base.py | 1 + services/alert_service.yaml | 52 +++++++++++++++ services/weather_service.yaml | 65 +++++++------------ 13 files changed, 228 insertions(+), 40 deletions(-) create mode 100644 apps/alerts/__init__.py create mode 100644 apps/alerts/admin.py create mode 100644 apps/alerts/apps.py create mode 100644 apps/alerts/management/__init__.py create mode 100644 apps/alerts/management/commands/__init__.py create mode 100644 apps/alerts/management/commands/scan_for_alerts.py create mode 100644 apps/alerts/migrations/0001_initial.py create mode 100644 apps/alerts/migrations/__init__.py create mode 100644 apps/alerts/models.py create mode 100644 apps/alerts/tests/__init__.py create mode 100644 services/alert_service.yaml diff --git a/apps/alerts/__init__.py b/apps/alerts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/alerts/admin.py b/apps/alerts/admin.py new file mode 100644 index 0000000..6a5251b --- /dev/null +++ b/apps/alerts/admin.py @@ -0,0 +1,29 @@ +from django.contrib import admin + +from alerts.models import Alert + + +# Register your models here. + + +@admin.register(Alert) +class AlertAdmin(admin.ModelAdmin): + list_filter = ["user", "plant", "sensor"] + readonly_fields = ["created", "updated"] + list_display = ["name", "user", "plant", "sensor"] + + fieldsets = [ + ( + "Alert Info", + { + "fields": ["user", "plant", "sensor", "name", "upper_threshold", "lower_threshold"], + }, + ), + ( + "Meta Data", + { + "classes": ["collapse"], + "fields": ["created", "updated"], + }, + ), + ] diff --git a/apps/alerts/apps.py b/apps/alerts/apps.py new file mode 100644 index 0000000..2be2313 --- /dev/null +++ b/apps/alerts/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AlertsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "alerts" diff --git a/apps/alerts/management/__init__.py b/apps/alerts/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/alerts/management/commands/__init__.py b/apps/alerts/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/alerts/management/commands/scan_for_alerts.py b/apps/alerts/management/commands/scan_for_alerts.py new file mode 100644 index 0000000..669abde --- /dev/null +++ b/apps/alerts/management/commands/scan_for_alerts.py @@ -0,0 +1,27 @@ +import logging + +from django.core.management.base import BaseCommand + +from alerts.models import Alert, AlertLog + +# Get an instance of a logger +logger = logging.getLogger(__name__) + + +class Command(BaseCommand): + """Command for scanning alerts in the system, and creating any logs needed.""" + + def handle(self, *args, **options): + alert_logs = [] + # TODO have a way of automatically resolving logs if condition is "unmet" + for alert in Alert.objects.all().select_related("user", "plant", "sensor"): + if alert.lower_threshold and alert.latest_data_point <= alert.latest_data_point.data: + if alert.alert_logs.order_by("created").addressed is True: + logger.info(f"Condition met for alert {alert}") + alert_logs.append(AlertLog(user=alert.user, alert=alert)) + elif alert.lower_threshold and alert.latest_data_point >= alert.latest_data_point.data: + if alert.alert_logs.order_by("created").addressed is True: + logger.info(f"Condition met for alert {alert}") + alert_logs.append(AlertLog(user=alert.user, alert=alert)) + + AlertLog.objects.bulk_create(alert_logs) diff --git a/apps/alerts/migrations/0001_initial.py b/apps/alerts/migrations/0001_initial.py new file mode 100644 index 0000000..457fa6f --- /dev/null +++ b/apps/alerts/migrations/0001_initial.py @@ -0,0 +1,42 @@ +# Generated by Django 4.2 on 2023-04-30 08:53 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("plants", "0009_alter_datapoint_options"), + ] + + operations = [ + migrations.CreateModel( + name="Alert", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("name", models.CharField(max_length=128)), + ("upper_threshold", models.FloatField(blank=True, null=True)), + ("lower_threshold", models.FloatField(blank=True, null=True)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ("plant", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="plants.plant")), + ("sensor", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="plants.sensor")), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name="AlertLog", + fields=[ + ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")), + ("addressed", models.BooleanField(default=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("updated", models.DateTimeField(auto_now=True)), + ("alert", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="alerts.alert")), + ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/apps/alerts/migrations/__init__.py b/apps/alerts/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/apps/alerts/models.py b/apps/alerts/models.py new file mode 100644 index 0000000..2c00bae --- /dev/null +++ b/apps/alerts/models.py @@ -0,0 +1,46 @@ +from functools import cached_property + +from django.contrib.auth import get_user_model +from django.db import models + +# Create your models here. + + +class Alert(models.Model): + """An alert for a user. + + An alert is a configurable model that users can create to get notified about changes to their plants. + """ + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE, related_name="alerts") + plant = models.ForeignKey("plants.Plant", on_delete=models.CASCADE, related_name="alerts") + sensor = models.ForeignKey("plants.Sensor", on_delete=models.CASCADE, related_name="alerts") + + name = models.CharField(max_length=128) + + upper_threshold = models.FloatField(blank=True, null=True) + lower_threshold = models.FloatField(blank=True, null=True) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) + + @cached_property + def latest_data_point(self): + return self.sensor.data_points.order_by("time").first() + + def __str__(self): + return self.name + + +class AlertLog(models.Model): + """An alert log from an alert. + + Created when the conditions of an alert are met. + """ + + user = models.ForeignKey(get_user_model(), on_delete=models.CASCADE) + alert = models.ForeignKey(Alert, on_delete=models.CASCADE, related_name="alert_logs") + addressed = models.BooleanField(default=False) + + created = models.DateTimeField(auto_now_add=True) + updated = models.DateTimeField(auto_now=True) diff --git a/apps/alerts/tests/__init__.py b/apps/alerts/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/project/settings/base.py b/project/settings/base.py index d77f51c..300d381 100644 --- a/project/settings/base.py +++ b/project/settings/base.py @@ -54,6 +54,7 @@ "api", "accounts", "weather", + "alerts", ] INSTALLED_APPS = DEFAULT_APPS + THIRD_PARTY_APPS + PROJECT_APPS diff --git a/services/alert_service.yaml b/services/alert_service.yaml new file mode 100644 index 0000000..1f70c64 --- /dev/null +++ b/services/alert_service.yaml @@ -0,0 +1,52 @@ +apiVersion: run.googleapis.com/v1 +kind: Job +metadata: + name: alert-scanner + labels: + cloud.googleapis.com/location: europe-west1 + +spec: + template: + spec: + parallelism: 1 + taskCount: 1 + template: + spec: + containers: + - image: gcr.io/garden-server-381815/garden-server:1.6.2 # x-release-please-version + command: + - ./manage.py + - scan_for_alerts + env: + - name: DJANGO_SETTINGS_MODULE + valueFrom: + secretKeyRef: + key: latest + name: DJANGO_SETTINGS_MODULE + - name: CSRF_TRUSTED_ORIGINS + valueFrom: + secretKeyRef: + key: latest + name: CSRF_TRUSTED_ORIGINS + - name: ALLOWED_HOST + valueFrom: + secretKeyRef: + key: latest + name: ALLOWED_HOST + - name: SECRET_KEY + valueFrom: + secretKeyRef: + key: latest + name: SECRET_KEY + - name: DATABASE_URL + valueFrom: + secretKeyRef: + key: latest + name: DATABASE_URL + resources: + limits: + cpu: 1000m + memory: 512Mi + maxRetries: 0 + timeoutSeconds: '600' + serviceAccountName: 400668159616-compute@developer.gserviceaccount.com diff --git a/services/weather_service.yaml b/services/weather_service.yaml index 4e3981e..193cc62 100644 --- a/services/weather_service.yaml +++ b/services/weather_service.yaml @@ -18,46 +18,31 @@ spec: - ./manage.py - populate_weather env: - - name: DJANGO_SETTINGS_MODULE - valueFrom: - secretKeyRef: - key: latest - name: DJANGO_SETTINGS_MODULE - - name: DJANGO_DATABASE_PASSWORD - valueFrom: - secretKeyRef: - key: latest - name: DJANGO_DATABASE_PASSWORD - - name: DJANGO_DATABASE_HOST - valueFrom: - secretKeyRef: - key: latest - name: DJANGO_DATABASE_HOST - - name: DJANGO_DATABASE_NAME - valueFrom: - secretKeyRef: - key: latest - name: DJANGO_DATABASE_NAME - - name: CSRF_TRUSTED_ORIGINS - valueFrom: - secretKeyRef: - key: latest - name: CSRF_TRUSTED_ORIGINS - - name: ALLOWED_HOST - valueFrom: - secretKeyRef: - key: latest - name: ALLOWED_HOST - - name: SECRET_KEY - valueFrom: - secretKeyRef: - key: latest - name: SECRET_KEY - - name: DJANGO_DATABASE_USER - valueFrom: - secretKeyRef: - key: latest - name: DJANGO_DATABASE_USER + - name: DJANGO_SETTINGS_MODULE + valueFrom: + secretKeyRef: + key: latest + name: DJANGO_SETTINGS_MODULE + - name: CSRF_TRUSTED_ORIGINS + valueFrom: + secretKeyRef: + key: latest + name: CSRF_TRUSTED_ORIGINS + - name: ALLOWED_HOST + valueFrom: + secretKeyRef: + key: latest + name: ALLOWED_HOST + - name: SECRET_KEY + valueFrom: + secretKeyRef: + key: latest + name: SECRET_KEY + - name: DATABASE_URL + valueFrom: + secretKeyRef: + key: latest + name: DATABASE_URL resources: limits: cpu: 1000m From 30a57442bc5adbad8616583d0b646cce17ff9203 Mon Sep 17 00:00:00 2001 From: Oli Date: Sun, 30 Apr 2023 10:20:06 +0100 Subject: [PATCH 2/2] fix: update migration --- apps/alerts/migrations/0001_initial.py | 32 +++++++++++++++++++++----- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/apps/alerts/migrations/0001_initial.py b/apps/alerts/migrations/0001_initial.py index 457fa6f..85f45f9 100644 --- a/apps/alerts/migrations/0001_initial.py +++ b/apps/alerts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2 on 2023-04-30 08:53 +# Generated by Django 4.2 on 2023-04-30 09:19 from django.conf import settings from django.db import migrations, models @@ -9,8 +9,8 @@ class Migration(migrations.Migration): initial = True dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ("plants", "0009_alter_datapoint_options"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -23,9 +23,24 @@ class Migration(migrations.Migration): ("lower_threshold", models.FloatField(blank=True, null=True)), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), - ("plant", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="plants.plant")), - ("sensor", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="plants.sensor")), - ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ( + "plant", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="alerts", to="plants.plant" + ), + ), + ( + "sensor", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="alerts", to="plants.sensor" + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="alerts", to=settings.AUTH_USER_MODEL + ), + ), ], ), migrations.CreateModel( @@ -35,7 +50,12 @@ class Migration(migrations.Migration): ("addressed", models.BooleanField(default=False)), ("created", models.DateTimeField(auto_now_add=True)), ("updated", models.DateTimeField(auto_now=True)), - ("alert", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="alerts.alert")), + ( + "alert", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, related_name="alert_logs", to="alerts.alert" + ), + ), ("user", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), ], ),