Split tables out of view modules into tables

This commit is contained in:
Adam Goldsmith 2024-04-18 11:30:18 -04:00
parent 0ce441336f
commit 029b4dff28
6 changed files with 347 additions and 309 deletions

63
doorcontrol/tables.py Normal file
View File

@ -0,0 +1,63 @@
import calendar
import django_tables2 as tables
from .models import HIDEvent
class UnitTimeTable(tables.Table):
members = tables.columns.Column()
members_delta = tables.columns.TemplateColumn(
"{{ value|floatformat:2}}%", verbose_name="Δ Members"
)
access_count = tables.columns.Column()
access_count_delta = tables.columns.TemplateColumn(
"{{ value|floatformat:2}}%", verbose_name="Δ Access Count"
)
class Meta:
fields = ("members", "members_delta", "access_count", "access_count_delta")
class DeniedAccessTable(tables.Table):
name = tables.TemplateColumn(
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
)
class Meta:
model = HIDEvent
fields = (
"timestamp",
"door",
"event_type",
"name",
"raw_card_number",
"decoded_card_number",
)
class MostActiveMembersTable(tables.Table):
name = tables.Column()
access_count = tables.Column()
class DetailByDayTable(tables.Table):
timestamp__date = tables.DateColumn(verbose_name="Date")
name = tables.Column()
access_count = tables.Column()
class BusiestDayOfWeekTable(tables.Table):
timestamp__week_day = tables.Column("Week Day")
events = tables.Column()
members = tables.Column()
def render_timestamp__week_day(self, value):
return calendar.day_name[(value - 2) % 7]
class BusiestTimeOfDayTable(tables.Table):
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
events = tables.Column()
members = tables.Column()

View File

@ -1,4 +1,3 @@
import calendar
import datetime
from django.contrib.auth.mixins import PermissionRequiredMixin
@ -19,6 +18,14 @@ from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin
from .models import Door, HIDEvent
from .tables import (
BusiestDayOfWeekTable,
BusiestTimeOfDayTable,
DeniedAccessTable,
DetailByDayTable,
MostActiveMembersTable,
UnitTimeTable,
)
REPORTS = []
@ -96,20 +103,6 @@ class BaseAccessReport(
return context
class UnitTimeTable(tables.Table):
members = tables.columns.Column()
members_delta = tables.columns.TemplateColumn(
"{{ value|floatformat:2}}%", verbose_name="Δ Members"
)
access_count = tables.columns.Column()
access_count_delta = tables.columns.TemplateColumn(
"{{ value|floatformat:2}}%", verbose_name="Δ Access Count"
)
class Meta:
fields = ("members", "members_delta", "access_count", "access_count_delta")
@register_report
class AccessPerUnitTime(BaseAccessReport):
table_class = UnitTimeTable
@ -209,24 +202,6 @@ class AccessPerUnitTime(BaseAccessReport):
)
class DeniedAccessTable(tables.Table):
name = tables.TemplateColumn(
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
)
class Meta:
model = HIDEvent
fields = (
"timestamp",
"door",
"event_type",
"name",
"raw_card_number",
"decoded_card_number",
)
@register_report
class DeniedAccess(BaseAccessReport):
_report_name = "Denied Access"
@ -244,11 +219,6 @@ class DeniedAccess(BaseAccessReport):
)
class MostActiveMembersTable(tables.Table):
name = tables.Column()
access_count = tables.Column()
@register_report
class MostActiveMembers(BaseAccessReport):
_report_name = "Most Active Members"
@ -271,12 +241,6 @@ class MostActiveMembers(BaseAccessReport):
)
class DetailByDayTable(tables.Table):
timestamp__date = tables.DateColumn(verbose_name="Date")
name = tables.Column()
access_count = tables.Column()
@register_report
class DetailByDay(BaseAccessReport):
_report_name = "Detail by Day"
@ -299,15 +263,6 @@ class DetailByDay(BaseAccessReport):
)
class BusiestDayOfWeekTable(tables.Table):
timestamp__week_day = tables.Column("Week Day")
events = tables.Column()
members = tables.Column()
def render_timestamp__week_day(self, value):
return calendar.day_name[(value - 2) % 7]
@register_report
class BusiestDayOfWeek(BaseAccessReport):
_report_name = "Busiest Day of the Week"
@ -326,12 +281,6 @@ class BusiestDayOfWeek(BaseAccessReport):
)
class BusiestTimeOfDayTable(tables.Table):
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
events = tables.Column()
members = tables.Column()
@register_report
class BusiestTimeOfDay(BaseAccessReport):
_report_name = "Busiest Time of Day"

168
membershipworks/tables.py Normal file
View File

@ -0,0 +1,168 @@
from datetime import timedelta
from django.template.defaultfilters import floatformat
from django.utils.html import format_html
from django.utils.safestring import SafeString
import django_tables2 as tables
from .models import EventAttendee, EventExt, Member
class DurationColumn(tables.Column):
def render(self, value: timedelta):
if value is None:
return None
return floatformat(value.total_seconds() / 60 / 60, -2)
def value(self, value: timedelta):
if value is None:
return None
return value.total_seconds() / 60 / 60
class EventTable(tables.Table):
title = tables.TemplateColumn(
template_code=(
'<a title="MembershipWorks" href="https://membershipworks.com/admin/#!event/admin/{{ record.url }}">{{ value }}</a> '
'<a title="Admin" href="{% url "admin:membershipworks_eventext_change" record.pk %}"><i class="bi bi-pencil-square"></i></a> '
'<a title="Details" href="{% url "membershipworks:event-detail" record.pk %}"><i class="bi bi-receipt"></i></a> '
),
accessor="unescaped_title",
)
occurred = tables.BooleanColumn(visible=False)
start = tables.DateColumn("N d, Y")
duration = DurationColumn()
person_hours = DurationColumn()
meetings = tables.Column()
gross_revenue = tables.Column()
total_due_to_instructor = tables.Column()
net_revenue = tables.Column()
invoice__date_submitted = tables.DateColumn(verbose_name="Invoice Submitted")
invoice__date_paid = tables.DateColumn(verbose_name="Invoice Paid")
class Meta:
model = EventExt
fields = (
"title",
"occurred",
"start",
"instructor",
"category",
"count",
"cap",
"meetings",
"duration",
"person_hours",
"gross_revenue",
"total_due_to_instructor",
"net_revenue",
)
row_attrs = {
"class": lambda record: (
"" if record.occurred else "text-decoration-line-through table-danger"
)
}
class EventSummaryTable(tables.Table):
event_count = tables.Column("Events")
canceled_event_count = tables.Column("Canceled Events")
count__sum = tables.Column("Tickets")
instructor__count = tables.Column("Unique Instructors")
meetings__sum = tables.Column("Meetings")
duration__sum = DurationColumn("Class Hours")
person_hours__sum = DurationColumn("Person Hours")
gross_revenue__sum = tables.Column("Gross Revenue")
total_due_to_instructor__sum = tables.Column("Total Due to Instructor")
net_revenue__sum = tables.Column("Net Revenue")
class UserEventTable(EventTable):
title = tables.Column(linkify=True, accessor="unescaped_title")
instructor = None
person_hours = None
gross_revenue = None
net_revenue = None
class InvoiceMoneyColumn(tables.columns.Column):
def render(self, value):
return f"${super().render(value):.2f}"
class InvoiceMoneyFooterColumn(InvoiceMoneyColumn):
def render_footer(self, bound_column, table):
value = getattr(table.event, bound_column.accessor)
if value is not None:
return f"${value:.2f}"
else:
return bound_column.default
class InvoiceTable(tables.Table):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event")
super().__init__(*args, **kwargs)
@staticmethod
def _math_header(name: str, formula: str) -> SafeString:
return format_html(
'{} <div class="text-nowrap font-monospace fw-light">[{}]</div>',
name,
formula,
)
label = tables.Column("Ticket Type", footer="Subtotals")
list_price = InvoiceMoneyColumn("Ticket Price")
actual_price = InvoiceMoneyColumn(_math_header("Actual Price", "P"))
quantity = tables.Column(
_math_header("Quantity", "Q"),
footer=lambda table: table.event.quantity,
)
amount = InvoiceMoneyFooterColumn(_math_header("Amount", "A=P*Q"))
materials = InvoiceMoneyFooterColumn(
_math_header("CMS Collected Materials Fee", "M=m*Q")
)
amount_without_materials = InvoiceMoneyFooterColumn(
_math_header("Event Revenue Base", "B=A-M")
)
instructor_revenue = InvoiceMoneyFooterColumn(
_math_header("Instructor Percentage Revenue", "R=B*I")
)
instructor_amount = InvoiceMoneyFooterColumn(
_math_header("Amount Due to Instructor", "R+M")
)
class Meta:
attrs = {
"class": "table table-sm mx-auto w-auto",
"tbody": {"class": "table-group-divider"},
"tfoot": {"class": "table-group-divider"},
}
orderable = False
class EventAttendeeTable(tables.Table):
class Meta:
model = EventAttendee
fields = ("name", "email")
class MissingPaperworkTable(tables.Table):
policy_agreement = tables.BooleanColumn()
authorize_charge = tables.BooleanColumn()
class Meta:
model = Member
fields = [
"first_name",
"last_name",
"membership",
"billing_method",
"join_date",
"membership_agreement_signed_and_on_file_date",
"waiver_form_signed_and_on_file_date",
"policy_agreement",
"authorize_charge",
]

View File

@ -1,5 +1,5 @@
import uuid
from datetime import datetime, timedelta
from datetime import datetime
from typing import Any
from django.conf import settings
@ -15,12 +15,9 @@ from django.db.models import OuterRef, Q, Subquery
from django.db.models.functions import TruncMonth, TruncYear
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render
from django.template.defaultfilters import floatformat
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.safestring import SafeString
from django.views.generic import DetailView, ListView
from django.views.generic.dates import (
ArchiveIndexView,
@ -46,6 +43,14 @@ from membershipworks.membershipworks_api import MembershipWorks
from .forms import EventInvoiceForm
from .invoice_email import make_invoice_emails
from .models import EventAttendee, EventExt, EventInvoice, Member
from .tables import (
EventAttendeeTable,
EventSummaryTable,
EventTable,
InvoiceTable,
MissingPaperworkTable,
UserEventTable,
)
class MemberAutocomplete(autocomplete.Select2QuerySetView):
@ -138,75 +143,6 @@ def upcoming_events(request):
return render(request, "membershipworks/upcoming_events.dj.html", context)
class DurationColumn(tables.Column):
def render(self, value: timedelta):
if value is None:
return None
return floatformat(value.total_seconds() / 60 / 60, -2)
def value(self, value: timedelta):
if value is None:
return None
return value.total_seconds() / 60 / 60
class EventTable(tables.Table):
title = tables.TemplateColumn(
template_code=(
'<a title="MembershipWorks" href="https://membershipworks.com/admin/#!event/admin/{{ record.url }}">{{ value }}</a> '
'<a title="Admin" href="{% url "admin:membershipworks_eventext_change" record.pk %}"><i class="bi bi-pencil-square"></i></a> '
'<a title="Details" href="{% url "membershipworks:event-detail" record.pk %}"><i class="bi bi-receipt"></i></a> '
),
accessor="unescaped_title",
)
occurred = tables.BooleanColumn(visible=False)
start = tables.DateColumn("N d, Y")
duration = DurationColumn()
person_hours = DurationColumn()
meetings = tables.Column()
gross_revenue = tables.Column()
total_due_to_instructor = tables.Column()
net_revenue = tables.Column()
invoice__date_submitted = tables.DateColumn(verbose_name="Invoice Submitted")
invoice__date_paid = tables.DateColumn(verbose_name="Invoice Paid")
class Meta:
model = EventExt
fields = (
"title",
"occurred",
"start",
"instructor",
"category",
"count",
"cap",
"meetings",
"duration",
"person_hours",
"gross_revenue",
"total_due_to_instructor",
"net_revenue",
)
row_attrs = {
"class": lambda record: (
"" if record.occurred else "text-decoration-line-through table-danger"
)
}
class EventSummaryTable(tables.Table):
event_count = tables.Column("Events")
canceled_event_count = tables.Column("Canceled Events")
count__sum = tables.Column("Tickets")
instructor__count = tables.Column("Unique Instructors")
meetings__sum = tables.Column("Meetings")
duration__sum = DurationColumn("Class Hours")
person_hours__sum = DurationColumn("Person Hours")
gross_revenue__sum = tables.Column("Gross Revenue")
total_due_to_instructor__sum = tables.Column("Total Due to Instructor")
net_revenue__sum = tables.Column("Net Revenue")
class EventIndexReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, ArchiveIndexView
):
@ -303,14 +239,6 @@ class EventMonthReport(
return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}"
class UserEventTable(EventTable):
title = tables.Column(linkify=True, accessor="unescaped_title")
instructor = None
person_hours = None
gross_revenue = None
net_revenue = None
class UserEventView(SingleTableMixin, ListView):
model = EventExt
table_class = UserEventTable
@ -339,63 +267,6 @@ class UserEventView(SingleTableMixin, ListView):
return f"my_events_{self.member.uid if self.member is not None else 'no-uid'}.{export_format}"
class InvoiceMoneyColumn(tables.columns.Column):
def render(self, value):
return f"${super().render(value):.2f}"
class InvoiceMoneyFooterColumn(InvoiceMoneyColumn):
def render_footer(self, bound_column, table):
value = getattr(table.event, bound_column.accessor)
if value is not None:
return f"${value:.2f}"
else:
return bound_column.default
class InvoiceTable(tables.Table):
def __init__(self, *args, **kwargs):
self.event = kwargs.pop("event")
super().__init__(*args, **kwargs)
@staticmethod
def _math_header(name: str, formula: str) -> SafeString:
return format_html(
'{} <div class="text-nowrap font-monospace fw-light">[{}]</div>',
name,
formula,
)
label = tables.Column("Ticket Type", footer="Subtotals")
list_price = InvoiceMoneyColumn("Ticket Price")
actual_price = InvoiceMoneyColumn(_math_header("Actual Price", "P"))
quantity = tables.Column(
_math_header("Quantity", "Q"),
footer=lambda table: table.event.quantity,
)
amount = InvoiceMoneyFooterColumn(_math_header("Amount", "A=P*Q"))
materials = InvoiceMoneyFooterColumn(
_math_header("CMS Collected Materials Fee", "M=m*Q")
)
amount_without_materials = InvoiceMoneyFooterColumn(
_math_header("Event Revenue Base", "B=A-M")
)
instructor_revenue = InvoiceMoneyFooterColumn(
_math_header("Instructor Percentage Revenue", "R=B*I")
)
instructor_amount = InvoiceMoneyFooterColumn(
_math_header("Amount Due to Instructor", "R+M")
)
class Meta:
attrs = {
"class": "table table-sm mx-auto w-auto",
"tbody": {"class": "table-group-divider"},
"tfoot": {"class": "table-group-divider"},
}
orderable = False
class EventDetailView(
SingleTableMixin, FormMixin, AccessMixin, DetailView, ProcessFormView
):
@ -527,12 +398,6 @@ class EventInvoicePDFView(AccessMixin, BaseDetailView):
return self.handle_no_permission()
class EventAttendeeTable(tables.Table):
class Meta:
model = EventAttendee
fields = ("name", "email")
class EventAttendeeFilters(django_filters.FilterSet):
new_since = django_filters.DateFilter(
field_name="event__start", method="filter_new_since"
@ -560,25 +425,6 @@ class EventAttendeeListView(
return super().get_table_data().values("name", "email").distinct()
class MissingPaperworkTable(tables.Table):
policy_agreement = tables.BooleanColumn()
authorize_charge = tables.BooleanColumn()
class Meta:
model = Member
fields = [
"first_name",
"last_name",
"membership",
"billing_method",
"join_date",
"membership_agreement_signed_and_on_file_date",
"waiver_form_signed_and_on_file_date",
"policy_agreement",
"authorize_charge",
]
class MissingPaperworkReport(
ExportMixin,
SingleTableMixin,

92
paperwork/tables.py Normal file
View File

@ -0,0 +1,92 @@
import django_tables2 as tables
from .models import (
InstructorOrVendor,
Waiver,
)
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 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 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 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 CertificationCountTable(tables.Table):
certification_version__definition__name = tables.Column("Certification")
certification_version__definition__department__name = tables.Column("Department")
total_issued = tables.Column()

View File

@ -19,7 +19,6 @@ from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFou
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
@ -35,6 +34,13 @@ from .models import (
InstructorOrVendor,
Waiver,
)
from .tables import (
AccessVerificationTable,
CertificationCountTable,
CertifiersTable,
InstructorOrVendorTable,
WaiverReportTable,
)
WIKI_URL = settings.WIKI_URL
@ -120,34 +126,6 @@ def certification_pdf(request, cert_name):
)
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"
@ -156,23 +134,6 @@ class WaiverReport(ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListV
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,
@ -203,33 +164,6 @@ 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,
@ -317,14 +251,6 @@ class AccessVerificationReport(
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,
@ -356,12 +282,6 @@ class CertifiersReport(
)
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,