From 250c96f1c2f210a57f6f685606aa361b8287b54f Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Mon, 10 Apr 2023 13:12:45 -0400 Subject: [PATCH] paperwork: Add Audits for Waivers and Certifications --- paperwork/admin.py | 46 +++++++++- .../0012_waiveraudit_certificationaudit.py | 90 +++++++++++++++++++ paperwork/models.py | 27 ++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 paperwork/migrations/0012_waiveraudit_certificationaudit.py diff --git a/paperwork/admin.py b/paperwork/admin.py index ebc6f10..1be7af6 100644 --- a/paperwork/admin.py +++ b/paperwork/admin.py @@ -1,3 +1,4 @@ +from django import forms from django.core import mail from django.contrib import admin, messages from django.db.models import Value @@ -8,15 +9,34 @@ from .models import ( Department, CertificationDefinition, Certification, + CertificationAudit, CertificationVersion, InstructorOrVendor, SpecialProgram, Waiver, + WaiverAudit, ) from .forms import CertificationForm from .certification_emails import all_certification_emails +class AlwaysChangedModelForm(forms.models.ModelForm): + """By always returning true even unchanged inlines will get validated and saved.""" + + def has_changed(self): + return True + + +class AbstractAuditInline(admin.TabularInline): + extra = 0 + form = AlwaysChangedModelForm + + def get_formset(self, request, obj=None, **kwargs): + formset = super().get_formset(request, obj, **kwargs) + formset.form.base_fields["user"].initial = request.user + return formset + + @admin.register(Department) class DepartmentAdmin(admin.ModelAdmin): search_fields = ["name"] @@ -64,6 +84,10 @@ class CertificationDefinitionAdmin(admin.ModelAdmin): return obj.latest_version().semantic_version() +class CertificationAuditInline(AbstractAuditInline): + model = CertificationAudit + + @admin.register(Certification) class CertificationAdmin(admin.ModelAdmin): form = CertificationForm @@ -74,6 +98,7 @@ class CertificationAdmin(admin.ModelAdmin): ] autocomplete_fields = ["member"] exclude = ["shop_lead_notified"] + inlines = [CertificationAuditInline] def get_queryset(self, request): qs = super().get_queryset(request) @@ -112,6 +137,10 @@ class CertificationAdmin(admin.ModelAdmin): def certification_department(self, obj): return obj.certification_version.definition.department + @admin.display(description="Latest Audit") + def latest_audit(self, obj): + return obj.audits.latest() + list_display = [ "certification_name", "name", @@ -121,6 +150,7 @@ class CertificationAdmin(admin.ModelAdmin): "date", "shop_lead_notified", "certified_by", + "latest_audit", ] list_display_links = [ "certification_name", @@ -129,6 +159,7 @@ class CertificationAdmin(admin.ModelAdmin): list_filter = [ "certification_version__definition__department", ("shop_lead_notified", admin.EmptyFieldListFilter), + ("audits", admin.EmptyFieldListFilter), ] actions = ["send_notifications"] @@ -188,10 +219,13 @@ class SpecialProgramAdmin(admin.ModelAdmin): ] +class WaiverAuditInline(AbstractAuditInline): + model = WaiverAudit + + @admin.register(Waiver) class WaiverAdmin(admin.ModelAdmin): search_fields = ["name"] - list_display = [ "name", "date", @@ -201,7 +235,17 @@ class WaiverAdmin(admin.ModelAdmin): "guardian_name", "guardian_relation", "guardian_date", + "latest_audit", ] + list_filter = [ + "waiver_version", + ("audits", admin.EmptyFieldListFilter), + ] + inlines = [WaiverAuditInline] + + @admin.display(description="Latest Audit") + def latest_audit(self, obj): + return obj.audits.latest() admin.site.register(CmsRedRiverVeteransScholarship) diff --git a/paperwork/migrations/0012_waiveraudit_certificationaudit.py b/paperwork/migrations/0012_waiveraudit_certificationaudit.py new file mode 100644 index 0000000..18151af --- /dev/null +++ b/paperwork/migrations/0012_waiveraudit_certificationaudit.py @@ -0,0 +1,90 @@ +# Generated by Django 4.2 on 2023-04-10 05:34 + +import datetime +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("paperwork", "0011_alter_certificationversion_options"), + ] + + operations = [ + migrations.CreateModel( + name="WaiverAudit", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(default=datetime.date.today, unique=True)), + ("good", models.BooleanField(default=False)), + ("notes", models.CharField(blank=True, max_length=255)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "waiver", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="audits", + to="paperwork.waiver", + ), + ), + ], + options={ + "ordering": ["date"], + "get_latest_by": ["date"], + "abstract": False, + }, + ), + migrations.CreateModel( + name="CertificationAudit", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("date", models.DateField(default=datetime.date.today, unique=True)), + ("good", models.BooleanField(default=False)), + ("notes", models.CharField(blank=True, max_length=255)), + ( + "certification", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="audits", + to="paperwork.certification", + ), + ), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "ordering": ["date"], + "get_latest_by": ["date"], + "abstract": False, + }, + ), + ] diff --git a/paperwork/models.py b/paperwork/models.py index 6a4cdf0..7ea1d85 100644 --- a/paperwork/models.py +++ b/paperwork/models.py @@ -1,13 +1,30 @@ +import datetime import re from semver import VersionInfo from django.db import models from django.db.models import OuterRef, Q, ExpressionWrapper, Subquery +from django.conf import settings from django.core.validators import RegexValidator from membershipworks.models import Member, Flag as MembershipWorksFlag +class AbstractAudit(models.Model): + date = models.DateField(default=datetime.date.today, unique=True) + user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT) + good = models.BooleanField(default=False) + notes = models.CharField(max_length=255, blank=True) + + def __str__(self): + return f"{'Good' if self.good else 'Bad'} audit at {self.date} by {self.user}" + + class Meta: + abstract = True + ordering = ["date"] + get_latest_by = ["date"] + + class CmsRedRiverVeteransScholarship(models.Model): serial = models.AutoField(primary_key=True) program_name = models.CharField(db_column="Program Name", max_length=255) @@ -202,6 +219,12 @@ class Certification(models.Model): ] +class CertificationAudit(AbstractAudit): + certification = models.ForeignKey( + Certification, on_delete=models.CASCADE, related_name="audits" + ) + + class InstructorOrVendor(models.Model): serial = models.AutoField(primary_key=True) name = models.CharField(db_column="Name", max_length=255) @@ -282,3 +305,7 @@ class Waiver(models.Model): class Meta: db_table = "Waivers" + + +class WaiverAudit(AbstractAudit): + waiver = models.ForeignKey(Waiver, on_delete=models.CASCADE, related_name="audits")