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 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