Skip to content

Commit

Permalink
feat(certificates): added certificate expiry warnings to the dashboard (
Browse files Browse the repository at this point in the history
closes #4)
  • Loading branch information
zyv committed Nov 1, 2023
1 parent aa36b42 commit 90c603f
Show file tree
Hide file tree
Showing 11 changed files with 144 additions and 20 deletions.
2 changes: 1 addition & 1 deletion logbook/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,5 @@ class PilotAdmin(admin.ModelAdmin):

@admin.register(Certificate)
class CertificateAdmin(admin.ModelAdmin):
list_display = ("name", "issue_date", "valid_until", "authority")
list_display = ("name", "number", "issue_date", "valid_until", "authority", "supersedes")
save_as = True
24 changes: 24 additions & 0 deletions logbook/migrations/0017_certificate_supersedes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.7 on 2023-11-01 16:01

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("logbook", "0016_aircraft_speed_unit_aircraft_v_app_aircraft_v_bg_and_more"),
]

operations = [
migrations.AddField(
model_name="certificate",
name="supersedes",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.PROTECT,
related_name="supersedes_set",
to="logbook.certificate",
),
),
]
16 changes: 16 additions & 0 deletions logbook/migrations/0018_remove_certificate_relinquished.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Generated by Django 4.2.7 on 2023-11-01 16:01

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("logbook", "0017_certificate_supersedes"),
]

operations = [
migrations.RemoveField(
model_name="certificate",
name="relinquished",
),
]
22 changes: 22 additions & 0 deletions logbook/migrations/0019_alter_certificate_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Generated by Django 4.2.7 on 2023-11-01 16:12

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("logbook", "0018_remove_certificate_relinquished"),
]

operations = [
migrations.AlterModelOptions(
name="certificate",
options={
"ordering": (
"name",
models.OrderBy(models.F("supersedes"), descending=True, nulls_last=True),
"-valid_until",
),
},
),
]
18 changes: 13 additions & 5 deletions logbook/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,13 @@ class Certificate(models.Model):
valid_until = models.DateField(blank=True, null=True)
authority = models.CharField(max_length=255)
remarks = models.CharField(max_length=255, blank=True)
relinquished = models.BooleanField(default=False)
supersedes = models.ForeignKey(
"self",
on_delete=models.PROTECT,
blank=True,
null=True,
related_name="supersedes_set",
)

class Meta:
constraints = (
Expand All @@ -183,11 +189,13 @@ class Meta:
name="validity_after_issue",
),
)
ordering = ("name", "-valid_until")
ordering = ("name", F("supersedes").desc(nulls_last=True), "-valid_until")

def __str__(self):
return f"{self.name}{' ({})'.format(self.number) if self.number else ''}"
return f"{self.name}{' / {}'.format(self.number) if self.number else ''} ({self.issue_date})"

@property
def is_valid(self) -> bool:
return (self.valid_until is None or self.valid_until >= datetime.now(tz=UTC).date()) and not self.relinquished
def valid(self) -> bool:
return (
self.valid_until is None or self.valid_until >= datetime.now(tz=UTC).date()
) and not self.supersedes_set.count()
5 changes: 4 additions & 1 deletion logbook/templates/logbook/certificate_list.html
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
{% extends "logbook/base.html" %}
{% load django_bootstrap5 %}
{% load logbook_utils %}

{% block content %}

<h1>Certificates</h1>

{% bootstrap_messages %}

<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
Expand All @@ -19,7 +22,7 @@ <h1>Certificates</h1>
</thead>
<tbody>
{% for obj in object_list %}
<tr class="{% if obj.is_valid %}table-light{% else %}text-muted{% endif %}">
<tr class="{% if obj.valid %}table-light{% else %}text-muted{% endif %}">
<td>{{ obj.name }}</td>
<td>{{ obj.number | default:"-" }}</td>
<td>{{ obj.issue_date }}</td>
Expand Down
3 changes: 3 additions & 0 deletions logbook/templates/logbook/dashboard.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
{% extends "logbook/base.html" %}
{% load django_bootstrap5 %}
{% load logbook_utils %}

{% block content %}
<h1>Dashboard</h1>

{% bootstrap_messages %}

<h2>Currency status</h2>

<table class="table w-25">
Expand Down
6 changes: 5 additions & 1 deletion logbook/views/certificates.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from ..models import Certificate
from .utils import AuthenticatedListView
from .utils import AuthenticatedListView, check_certificates_expiry


class CertificateIndexView(AuthenticatedListView):
model = Certificate

def get(self, request, *args, **kwargs):
check_certificates_expiry(request)
return super().get(request, *args, **kwargs)
12 changes: 8 additions & 4 deletions logbook/views/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
from ..statistics.currency import CURRENCY_REQUIRED_LANDINGS_NIGHT, get_ninety_days_currency
from ..statistics.experience import compute_totals
from .utils import (
AuthenticatedListView,
AuthenticatedTemplateView,
check_certificates_expiry,
)


class DashboardView(AuthenticatedListView):
queryset = AircraftType
class DashboardView(AuthenticatedTemplateView):
template_name = "logbook/dashboard.html"

def get(self, request, *args, **kwargs):
check_certificates_expiry(request)
return super().get(request, *args, **kwargs)

def get_context_data(self, *args, **kwargs):
def totals_per_function(log_entries: QuerySet[LogEntry]):
return {function: compute_totals(log_entries.filter(time_function=function)) for function in FunctionType}
Expand Down Expand Up @@ -66,7 +70,7 @@ def totals_per_function(log_entries: QuerySet[LogEntry]):
],
},
}
for aircraft_type in self.queryset
for aircraft_type in AircraftType
},
"grand_total": compute_totals(LogEntry.objects.all()),
}
34 changes: 34 additions & 0 deletions logbook/views/utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
from datetime import UTC, datetime

from dateutil.relativedelta import relativedelta
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, UserPassesTestMixin
from django.urls import reverse_lazy
from django.utils.safestring import mark_safe
from django.views.generic import ListView, TemplateView

from ..models import Certificate

EXPIRY_WARNING_THRESHOLD = relativedelta(months=3)


def check_certificates_expiry(request):
today = datetime.now(tz=UTC).date()
certificates = Certificate.objects.filter(
valid_until__lte=today + EXPIRY_WARNING_THRESHOLD,
supersedes_set=None,
)
if certificates.exists():
for certificate in certificates:
if not certificate.valid:
messages.error(
request,
mark_safe(
f'Certificate <strong>"{certificate}"</strong> has expired on {certificate.valid_until}!',
),
)
else:
messages.warning(
request,
mark_safe(
f'Certificate <strong>"{certificate}"</strong> expires in '
f"<strong>{(certificate.valid_until - today).days}</strong> days!",
),
)


class AuthenticatedView(UserPassesTestMixin, LoginRequiredMixin):
login_url = reverse_lazy("admin:login")
Expand Down
22 changes: 14 additions & 8 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@


class ModelsTest(TestCase):
def test_is_valid(self):
certificate = Certificate.objects.create(name="Test", issue_date=datetime.now(tz=UTC))
self.assertTrue(certificate.is_valid)
def test_certificate_valid(self):
certificate_old = Certificate.objects.create(
name="Test",
issue_date=datetime.now(tz=UTC),
)
certificate_new = Certificate.objects.create(
name="Test",
issue_date=datetime.now(tz=UTC),
supersedes=certificate_old,
)

certificate.valid_until = (datetime.now(tz=UTC) - timedelta(days=1)).date()
self.assertFalse(certificate.is_valid)
self.assertFalse(certificate_old.valid)
self.assertTrue(certificate_new.valid)

certificate.relinquished = True
certificate.valid_until = None
self.assertFalse(certificate.is_valid)
certificate_new.valid_until = (datetime.now(tz=UTC) - timedelta(days=1)).date()
self.assertFalse(certificate_new.valid)

0 comments on commit 90c603f

Please sign in to comment.