from typing import TYPE_CHECKING from django.conf import settings from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.staticfiles import finders as staticfiles_finders from django.db import models from django.db.models import ( Case, Count, Exists, Max, OuterRef, Q, Subquery, 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 import requests import weasyprint from django_mysql.models.aggregates import GroupConcat from django_tables2 import SingleTableMixin from django_tables2.export.views import ExportMixin from membershipworks.models import Member from .models import ( Certification, CertificationVersion, Department, InstructorOrVendor, Waiver, ) from .tables import ( AccessVerificationTable, CertificationCountTable, CertifiersTable, InstructorOrVendorTable, WaiverReportTable, ) if TYPE_CHECKING: from collections.abc import Iterable WIKI_URL = settings.WIKI_URL class MemberCertificationListView(ListView): template_name = "paperwork/member_certifications.dj.html" context_object_name = "certifications" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context["show_outdated"] = ( self.request.GET.get("show_outdated", "false").lower() == "true" ) return context def get_queryset(self): self.member = get_object_or_404(Member, uid=self.kwargs["uid"]) return Certification.objects.filter(member=self.member) @login_required def department_certifications(request): departments: Iterable[Department] if (member := Member.from_user(request.user)) is not None: departments = Department.objects.filter_by_shop_lead(member) else: departments = [] certifications = Certification.objects.filter( certification_version__definition__department__in=departments ).prefetch_related( "certification_version__definition__department", "member", ) return render( request, "paperwork/department_certifications.dj.html", {"departments": departments, "certifications": certifications}, ) def certification_pdf(request, cert_name): wiki_page = f"{cert_name.replace('_', ' ')} Certification" r = requests.get( WIKI_URL + "/api.php", params={ "action": "askargs", "conditions": wiki_page, "printouts": "Version|Approval Date|Approval status", "format": "json", "api_version": "2", "origin": "*", }, ) results = r.json()["query"]["results"] if wiki_page not in results: return HttpResponseNotFound( f'No such certification found on wiki: {wiki_page}' ) printouts = results[wiki_page]["printouts"] if printouts["Approval status"] != ["approve"]: return HttpResponseBadRequest( f'Certification is not yet approved on wiki: {wiki_page}' ) filename = ( f'{wiki_page}_v{printouts["Version"][0]} - {printouts["Approval Date"][0]}.pdf' ) html = weasyprint.HTML(f"{WIKI_URL}/index.php?title={wiki_page}") stylesheet = staticfiles_finders.find("paperwork/certification-print.css") pdf = html.write_pdf(stylesheets=[stylesheet]) return HttpResponse( pdf, headers={ "Content-Type": "application/pdf", "Content-Disposition": f'inline; filename="{filename}"', }, ) class WaiverReport(ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView): permission_required = "paperwork.view_waiver" template_name = "paperwork/waiver_report.dj.html" queryset = Waiver.objects.order_by("name").all() table_class = WaiverReportTable table_pagination = False class InstructorOrVendorReport( ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView, ): permission_required = "paperwork.view_instructororvendor" template_name = "paperwork/instructor_or_vendor_report.dj.html" queryset = InstructorOrVendor.objects.order_by("name").all() table_class = InstructorOrVendorTable export_formats = ("csv", "xlsx", "ods") def get_table_data(self): return ( super() .get_table_data() .values("name") .annotate( instructor_agreement_date=GroupConcat( "instructor_agreement_date", distinct=True, ordering="asc" ), w9_date=GroupConcat("w9_date", distinct=True, ordering="asc"), phone=GroupConcat("phone", distinct=True, ordering="asc"), email_address=GroupConcat( "email_address", distinct=True, ordering="asc" ), ) ) 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): def shop_error(access_field: str, shop_name: str): member_list = CertificationVersion.objects.filter( is_current=True, definition__department__name=shop_name, certification__member__pk__isnull=False, ).values("certification__member__pk") return Case( When( Q(**{access_field: True}) & ~Q(uid__in=Subquery(member_list)), Value("Has access but no cert"), ), When( Q(**{access_field: False}) & Q(uid__in=Subquery(member_list)), Value("Has cert but no access"), ), default=None, ) 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) & ~( Member.objects.has_flag("label", "Volunteer: Desker") | Q(billing_method__startswith="Desker") | Q( Exists( Department.objects.filter( shop_lead_flag__members=OuterRef("pk") ) ) ) ), 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 class CertifiersReport( ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView, ): model = Certification permission_required = "paperwork.view_certification" template_name = "paperwork/certifiers_report.dj.html" table_class = CertifiersTable export_formats = ("csv", "xlsx", "ods") def get_queryset(self): qs = super().get_queryset() return ( qs.values( "certification_version__definition__department__name", "certification_version__definition__name", "certified_by", ) .annotate( number_issued_on_this_tool=Count("*"), last_issued_certification_date=Max("date"), ) .order_by( "certification_version__definition__name", "last_issued_certification_date", ) ) class CertificationCountReport( ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView, ): model = Certification permission_required = "paperwork.view_certification" template_name = "paperwork/certification_count_report.dj.html" table_class = CertificationCountTable export_formats = ("csv", "xlsx", "ods") def get_queryset(self): qs = super().get_queryset() return ( qs.values( "certification_version__definition__name", "certification_version__definition__department__name", ) .annotate(total_issued=Count("*")) .order_by("certification_version__definition__name") )