Skip to content

Commit

Permalink
feat: added notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
c0rydoras committed Oct 24, 2023
1 parent 2b26d7a commit 575b4a8
Show file tree
Hide file tree
Showing 29 changed files with 660 additions and 261 deletions.
1 change: 1 addition & 0 deletions .envs/.local/.api
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ ENV=dev
OIDC_VERIFY_SSL=False
OIDC_OP_BASE_ENDPOINT=https://outdated.local/auth/realms/outdated/protocol/openid-connect
GITHUB_API_TOKEN=your_token
# NOTIFICATIONS=first-warning=180;final-warning=10;final-alert=-1
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ migrate: ## Migrate django
.PHONY: migrate-zero
migrate-zero: ## Unapply all django migrations
@docker compose run --rm api python ./manage.py migrate outdated zero
@docker compose run --rm api python ./manage.py migrate notifications zero
@docker compose run --rm api python ./manage.py migrate user zero

.PHONY: keycloak-import
Expand Down
2 changes: 1 addition & 1 deletion api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ COPY . $APP_HOME

EXPOSE 8000

CMD /bin/sh -c "wait-for-it.sh ${DATABASE_HOST:-db}:${DATABASE_PORT:-5432} -- ./manage.py migrate && gunicorn --bind :8000 outdated.wsgi"
CMD /bin/sh -c "wait-for-it.sh ${DATABASE_HOST:-db}:${DATABASE_PORT:-5432} -- ./manage.py migrate && ./manage.py update-notifications && gunicorn --bind :8000 outdated.wsgi"
15 changes: 15 additions & 0 deletions api/outdated/jinja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import jinja2

OVERWRITES = {
"trim_blocks": True,
"lstrip_blocks": True,
}


def environment(**options):
return jinja2.Environment(
**{
**options,
**OVERWRITES,
}
)
Empty file.
5 changes: 5 additions & 0 deletions api/outdated/notifications/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class NotificationConfig(AppConfig):
name = "outdated.notifications"
49 changes: 49 additions & 0 deletions api/outdated/notifications/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Generated by Django 4.1.10 on 2023-08-09 09:44

from django.db import migrations, models
import uuid


class Migration(migrations.Migration):

initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Notification",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("schedule", models.DurationField()),
(
"template",
models.CharField(
choices=[
("first-warning", "first-warning"),
("second-warning", "second-warning"),
("third-warning", "third-warning"),
("final-warning", "final-warning"),
("first-alert", "first-alert"),
("second-alert", "second-alert"),
("third-alert", "third-alert"),
("final-alert", "final-alert"),
],
max_length=50,
),
),
],
options={
"ordering": ("-schedule",),
"unique_together": {("schedule", "template")},
},
),
]
Empty file.
60 changes: 60 additions & 0 deletions api/outdated/notifications/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from django.conf import settings
from django.db import models
from django.db.models.signals import m2m_changed, post_save
from django.dispatch import receiver

from outdated.models import UUIDModel
from outdated.outdated.models import Project, ReleaseVersion

TEMPLATE_CHOICES = [(template, template) for template, _ in settings.NOTIFICATIONS]


class Notification(UUIDModel):
schedule = models.DurationField()
template = models.CharField(max_length=50, choices=TEMPLATE_CHOICES)

def __str__(self) -> str:
return f"{self.template} ({self.schedule.days} days before EOL)"

class Meta:
unique_together = ("schedule", "template")
ordering = ("-schedule",)


def build_notification_queue(project: Project):
duration_until_outdated = project.duration_until_outdated
notifications = project.notification_queue
unsent_notifications = Notification.objects.filter(
schedule__gte=duration_until_outdated
)
notifications.set(
[
*list(unsent_notifications)[-1:],
*Notification.objects.filter(schedule__lte=duration_until_outdated),
]
)
project.save()


@receiver(post_save, sender=ReleaseVersion)
def release_version_changed(instance: ReleaseVersion, **kwargs):
if not instance.end_of_life:
return
concerned_projects = []
for version in instance.versions.all():
concerned_projects.extend(version.projects.all())

for project in set(concerned_projects):
if project.duration_until_outdated is not None:
build_notification_queue(project)


@receiver(m2m_changed, sender=Project.versioned_dependencies.through)
def versioned_dependencies_changed(action: str, instance: Project, **kwargs):
if (
action.startswith("pre")
or action.endswith("clear")
or instance.duration_until_outdated is None
):
return
build_notification_queue(instance)
28 changes: 28 additions & 0 deletions api/outdated/notifications/notifier.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from django.core.mail import EmailMessage
from django.template.base import Template
from django.template.loader import get_template

from outdated.outdated.models import Project

from .models import Notification


class Notifier:
def __init__(self, project: Project) -> None:
self.project = project

def notify(self) -> None:
try:
notification: Notification = self.project.notification_queue.get(
schedule__gte=self.project.duration_until_outdated
)
except Notification.DoesNotExist:
return

template: Template = get_template(notification.template + ".txt", using="text")
subject, _, body = template.render({"project": self.project}).partition("\n")
maintainers = [m.user.email for m in self.project.maintainers.all()]
message = EmailMessage(subject, body, to=maintainers[:1], cc=maintainers[1:])
message.send()
self.project.notification_queue.remove(notification)
self.project.save()
5 changes: 5 additions & 0 deletions api/outdated/notifications/templates/base-alert.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends 'base.txt' %}

{% block description %}
Outdated since {{ project.duration_until_outdated.days * -1 }} days
{% endblock %}
5 changes: 5 additions & 0 deletions api/outdated/notifications/templates/base-warning.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{% extends 'base.txt' %}

{% block description %}
Outdated in {{project.duration_until_outdated.days}} days
{% endblock %}
10 changes: 10 additions & 0 deletions api/outdated/notifications/templates/base.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% block subject required %}
{% endblock %}
Project: {{ project.name }}
Repo: {{ project.repo }}

{% block description %}
{% endblock %}

{% block content %}
{% endblock %}
9 changes: 9 additions & 0 deletions api/outdated/notifications/templates/final-alert.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base-alert.txt' %}

{% block subject %}
Your Project has reached EOL
{% endblock %}

{% block content %}
final-alert.txt contents
{% endblock %}
9 changes: 9 additions & 0 deletions api/outdated/notifications/templates/final-warning.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base-warning.txt' %}

{% block subject %}
Your project will be out of date shortly!
{% endblock %}

{% block content %}
final warning content
{% endblock %}
9 changes: 9 additions & 0 deletions api/outdated/notifications/templates/first-alert.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base-alert.txt' %}

{% block subject %}
Your project is now outdated
{% endblock %}

{% block content %}
first-alert.txt contents
{% endblock %}
9 changes: 9 additions & 0 deletions api/outdated/notifications/templates/first-warning.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base-warning.txt' %}

{% block subject %}
Your project will go out of date soon
{% endblock %}

{% block content %}
first warning text
{% endblock %}
9 changes: 9 additions & 0 deletions api/outdated/notifications/templates/second-alert.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base-alert.txt' %}

{% block subject %}
Your project is using outdated dependencies!
{% endblock %}

{% block content %}
second-alert.txt contents
{% endblock %}
9 changes: 9 additions & 0 deletions api/outdated/notifications/templates/second-warning.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base-warning.txt' %}

{% block subject %}
Your project is approaching it's EOL
{% endblock %}

{% block content %}
second warning text
{% endblock %}
9 changes: 9 additions & 0 deletions api/outdated/notifications/templates/third-alert.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base-alert.txt' %}

{% block subject %}
Your Project has outdated!
{% endblock %}

{% block content %}
third-alert.txt contents
{% endblock %}
9 changes: 9 additions & 0 deletions api/outdated/notifications/templates/third-warning.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% extends 'base-warning.txt' %}

{% block subject %}
Your project will soon be EOL!
{% endblock %}

{% block content %}
third warning text
{% endblock %}
15 changes: 13 additions & 2 deletions api/outdated/outdated/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 4.1.9 on 2023-09-18 08:51
# Generated by Django 4.1.10 on 2023-08-09 09:44

from django.db import migrations, models
import django.db.models.deletion
Expand All @@ -13,6 +13,7 @@ class Migration(migrations.Migration):

dependencies = [
("user", "0001_initial"),
("notifications", "0001_initial"),
]

operations = [
Expand Down Expand Up @@ -92,6 +93,7 @@ class Migration(migrations.Migration):
"release_version",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="versions",
to="outdated.releaseversion",
),
),
Expand Down Expand Up @@ -124,9 +126,15 @@ class Migration(migrations.Migration):
"repo",
outdated.models.RepositoryURLField(max_length=100, unique=True),
),
(
"notification_queue",
models.ManyToManyField(blank=True, to="notifications.notification"),
),
(
"versioned_dependencies",
models.ManyToManyField(blank=True, to="outdated.version"),
models.ManyToManyField(
blank=True, related_name="projects", to="outdated.version"
),
),
],
options={
Expand Down Expand Up @@ -161,6 +169,9 @@ class Migration(migrations.Migration):
),
),
],
options={
"ordering": ("-is_primary",),
},
),
migrations.AddConstraint(
model_name="project",
Expand Down
27 changes: 24 additions & 3 deletions api/outdated/outdated/models.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from datetime import date, timedelta
from typing import Optional

from django.db import models
from django.db.models.functions import Lower
Expand Down Expand Up @@ -70,7 +71,9 @@ def status(self):


class Version(UUIDModel):
release_version = models.ForeignKey(ReleaseVersion, on_delete=models.CASCADE)
release_version = models.ForeignKey(
ReleaseVersion, on_delete=models.CASCADE, related_name="versions"
)
patch_version = models.IntegerField()
release_date = models.DateField(null=True, blank=True)

Expand All @@ -91,12 +94,20 @@ def __str__(self):
def version(self):
return f"{self.release_version.version}.{self.patch_version}"

@property
def end_of_life(self):
return self.release_version.end_of_life


class Project(UUIDModel):
name = models.CharField(max_length=100, db_index=True)

versioned_dependencies = models.ManyToManyField(Version, blank=True)
repo = RepositoryURLField(max_length=100, unique=True)
versioned_dependencies = models.ManyToManyField(
Version, blank=True, related_name="projects"
)
notification_queue = models.ManyToManyField(
"notifications.Notification", blank=True
)

class Meta:
ordering = ["name", "id"]
Expand All @@ -116,6 +127,12 @@ def status(self) -> str:
first = self.versioned_dependencies.first()
return first.release_version.status if first else STATUS_OPTIONS["undefined"]

@property
def duration_until_outdated(self) -> Optional[timedelta]:
if not self.status or self.status == STATUS_OPTIONS["undefined"]:
return
return self.versioned_dependencies.first().end_of_life - date.today()

def __str__(self):
return self.name

Expand All @@ -127,5 +144,9 @@ class Maintainer(UUIDModel):
)
is_primary = UniqueBooleanField(default=False, together=["project"])

def __str__(self):
return self.user.email

class Meta:
unique_together = ("user", "project")
ordering = ("-is_primary",)
Loading

0 comments on commit 575b4a8

Please sign in to comment.