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, Count, Max, 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 import django_tables2 as tables import requests import weasyprint from django_mysql.models 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, ) 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): if request.user.is_superuser: departments = Department.objects.prefetch_related( "shop_lead_flag__members" ).all() elif 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 WarnEmptyColumn(tables.Column): attrs = { "td": { "class": lambda value, bound_column: "table-danger" if value == bound_column.default else "" } } class WaiverReportTable(tables.Table): emergency_contact_name = WarnEmptyColumn() emergency_contact_number = WarnEmptyColumn() class Meta: model = Waiver fields = [ "name", "date", "emergency_contact_name", "emergency_contact_number", "waiver_version", "guardian_name", "guardian_relation", "guardian_date", ] 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 InstructorOrVendorTable(tables.Table): instructor_agreement_date = WarnEmptyColumn( "Instructor Agreement Date(s)", default="Missing" ) w9_date = WarnEmptyColumn(default="Missing") class Meta: model = InstructorOrVendor fields = [ "name", "instructor_agreement_date", "w9_date", "phone", "email_address", ] 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 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 class CertifiersTable(tables.Table): certified_by = tables.Column() certification_version__definition__name = tables.Column("Certification") certification_version__definition__department__name = tables.Column("Department") number_issued_on_this_tool = tables.Column() last_issued_certification_date = tables.Column() 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 CertificationCountTable(tables.Table): certification_version__definition__name = tables.Column("Certification") certification_version__definition__department__name = tables.Column("Department") total_issued = tables.Column() 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") )