From c07e3ac07a3da91aa81837c9ec6b37148a2d6c14 Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Wed, 7 Feb 2024 21:03:19 -0500 Subject: [PATCH] paperwork: Add access verification report --- paperwork/dashboard.py | 5 + .../access_verification_report.dj.html | 12 ++ paperwork/urls.py | 5 + paperwork/views.py | 140 +++++++++++++++++- 4 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 paperwork/templates/paperwork/access_verification_report.dj.html diff --git a/paperwork/dashboard.py b/paperwork/dashboard.py index 2b1f999..f3e4dab 100644 --- a/paperwork/dashboard.py +++ b/paperwork/dashboard.py @@ -24,6 +24,11 @@ class PaperworkDashboardFragment(dashboard.LinksCardDashboardFragment): reverse("paperwork:instructors-and-vendor-report"), permission="paperwork.view_instructororvendor", ), + Link( + "Access Verification", + reverse("paperwork:access-verification-report"), + permission="paperwork.view_certification", + ), ] member = Member.from_user(self.request.user) diff --git a/paperwork/templates/paperwork/access_verification_report.dj.html b/paperwork/templates/paperwork/access_verification_report.dj.html new file mode 100644 index 0000000..6d1c5c4 --- /dev/null +++ b/paperwork/templates/paperwork/access_verification_report.dj.html @@ -0,0 +1,12 @@ +{% extends "base.dj.html" %} + +{% load render_table from django_tables2 %} + +{% block title %}Access Verification{% endblock %} +{% block admin_link %} + {% url 'admin:paperwork_certification_changelist' %} +{% endblock %} +{% block content %} + {% include "cmsmanage/components/download_table.dj.html" %} + {% render_table table %} +{% endblock %} diff --git a/paperwork/urls.py b/paperwork/urls.py index 2a276a2..de14918 100644 --- a/paperwork/urls.py +++ b/paperwork/urls.py @@ -35,4 +35,9 @@ urlpatterns = [ views.InstructorOrVendorReport.as_view(), name="instructors-and-vendor-report", ), + path( + "access-verification", + views.AccessVerificationReport.as_view(), + name="access-verification-report", + ), ] diff --git a/paperwork/views.py b/paperwork/views.py index a5dd099..2562774 100644 --- a/paperwork/views.py +++ b/paperwork/views.py @@ -2,6 +2,9 @@ from django.conf import settings from django.contrib import staticfiles from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import PermissionRequiredMixin +from django.db import models +from django.db.models import Case, Q, Value, When +from django.db.models.functions import Concat from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFound from django.shortcuts import get_object_or_404, render from django.views.generic import ListView @@ -15,7 +18,13 @@ from django_tables2.export.views import ExportMixin from membershipworks.models import Member -from .models import Certification, Department, InstructorOrVendor, Waiver +from .models import ( + Certification, + CertificationVersion, + Department, + InstructorOrVendor, + Waiver, +) WIKI_URL = settings.WIKI_URL @@ -186,3 +195,132 @@ class InstructorOrVendorReport( ), ) ) + + +class ShopAccessErrorColumn(tables.Column): + def td_class(value): + if value.startswith("Has access but"): + return "table-danger" + elif value.startswith("Has cert but"): + return "table-warning" + else: + return "" + + attrs = {"td": {"class": td_class}} + + +class AccessVerificationTable(tables.Table): + account_name = tables.Column() + access_card = tables.Column() + billing_method = tables.Column() + join_date = tables.DateColumn() + renewal_date = tables.DateColumn() + access_front_door = tables.BooleanColumn(verbose_name="Front Door") + access_studio_space = tables.BooleanColumn(verbose_name="Studio Space") + wood_shop_error = ShopAccessErrorColumn() + metal_shop_error = ShopAccessErrorColumn() + extended_hours_error = ShopAccessErrorColumn() + extended_hours_shops_error = ShopAccessErrorColumn() + storage_closet_error = ShopAccessErrorColumn() + + +class AccessVerificationReport( + ExportMixin, + SingleTableMixin, + PermissionRequiredMixin, + ListView, +): + permission_required = "paperwork.view_certification" + template_name = "paperwork/access_verification_report.dj.html" + table_class = AccessVerificationTable + export_formats = ("csv", "xlsx", "ods") + + def get_queryset(self): + # TODO: could be done with subqueries if membershipworks was not a separate DB + def shop_error(access_field: str, shop_name: str): + member_list = list( + CertificationVersion.objects.filter( + is_current=True, + definition__department__name=shop_name, + certification__member__pk__isnull=False, + ) + .values_list( + "certification__member__pk", + flat=True, + ) + .distinct() + ) + return Case( + When( + Q(**{access_field: True}) & ~Q(uid__in=member_list), + Value("Has access but no cert"), + ), + When( + Q(**{access_field: False}) & Q(uid__in=member_list), + Value("Has cert but no access"), + ), + default=None, + ) + + # TODO: could be a lot cleaner if membershipworks was not a separate DB + storage_closet_members = ( + Member.objects.filter( + Member.objects.has_flag("label", "Volunteer: Desker") + | Q(billing_method__startswith="Desker") + ) + .union( + *[ + department.shop_lead_flag.members.all() + for department in Department.objects.filter( + shop_lead_flag__isnull=False + ) + ] + ) + .values_list("pk", flat=True) + ) + + qs = ( + Member.objects.with_is_active() + .filter(is_active=True) + .values( + "account_name", + "billing_method", + "join_date", + "renewal_date", + "access_front_door", + "access_studio_space", + access_card=Concat( + "access_card_facility_code", + Value("-", models.TextField()), + "access_card_number", + ), + wood_shop_error=shop_error("access_wood_shop", "Wood Shop"), + metal_shop_error=shop_error("access_metal_shop", "Metal Shop"), + extended_hours_error=shop_error( + "access_front_door_and_studio_space_during_extended_hours", + "Closure/Lock-Up", + ), + extended_hours_shops_error=shop_error( + "access_permitted_shops_during_extended_hours", + "Closure/Lock-Up", + ), + storage_closet_error=Case( + When( + Q(access_storage_closet=True) + & ~Q(uid__in=storage_closet_members), + Value("Has access but not shop lead or desker"), + ), + default=None, + ), + ) + .filter( + Q(access_front_door=False) + | Q(access_studio_space=False) + | Q(wood_shop_error__isnull=False) + | Q(metal_shop_error__isnull=False) + | Q(extended_hours_error__isnull=False) + | Q(extended_hours_shops_error__isnull=False) + | Q(storage_closet_error__isnull=False) + ) + ) + return qs