Compare commits

...

9 Commits

10 changed files with 338 additions and 234 deletions

View File

@ -38,6 +38,7 @@ INSTALLED_APPS = [
"rest_framework.authtoken", "rest_framework.authtoken",
"django_q", "django_q",
"django_nh3", "django_nh3",
"django_tables2",
"tasks.apps.TasksConfig", "tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig", "rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig", "membershipworks.apps.MembershipworksConfig",
@ -134,3 +135,6 @@ Q_CLUSTER = {
}, },
}, },
} }
# Django-Tables2
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5-responsive.html"

View File

@ -1,5 +1,7 @@
{% extends "base.dj.html" %} {% extends "base.dj.html" %}
{% load render_table from django_tables2 %}
{% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %} {% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %}
{% block content %} {% block content %}
<div class="vstack align-items-center"> <div class="vstack align-items-center">
@ -11,9 +13,9 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<form method="get" class="container"> <form method="get" class="container-fluid">
<div class="row align-items-center"> <div class="row g-2 align-items-center justify-content-center">
<div class="col gy-1"> <div class="col-6 col-sm-auto">
<div class="form-floating"> <div class="form-floating">
<input type="date" <input type="date"
class="form-control" class="form-control"
@ -23,7 +25,7 @@
<label for="startDate">Start Date</label> <label for="startDate">Start Date</label>
</div> </div>
</div> </div>
<div class="col gy-1"> <div class="col-6 col-sm-auto">
<div class="form-floating"> <div class="form-floating">
<input type="date" <input type="date"
class="form-control" class="form-control"
@ -33,7 +35,7 @@
<label for="endDate">End Date</label> <label for="endDate">End Date</label>
</div> </div>
</div> </div>
<div class="col-md gy-1"> <div class="col-12 col-sm-auto">
<div class="form-floating"> <div class="form-floating">
<input type="number" <input type="number"
class="form-control" class="form-control"
@ -47,51 +49,15 @@
<label for="itemsPerPage">Items Per Page</label> <label for="itemsPerPage">Items Per Page</label>
</div> </div>
</div> </div>
<div class="col-6 col-md-auto gy-1"> <div class="btn-group col-auto" role="group" aria-label="Form Controls">
<button type="submit" class="btn btn-primary w-100">Submit</button> <button type="submit" class="btn btn-sm btn-primary">Submit</button>
<a href="?" class="btn btn-sm btn-warning">Reset</a>
</div> </div>
<div class="col-6 col-md-auto gy-1"> <div class="col-auto">
<a href="?" class="btn btn-warning w-100">Reset</a> {% include "membershipworks/components/download_table.dj.html" %}
</div> </div>
</div> </div>
</form> </form>
<div class="table-responsive mw-100"> {% render_table table %}
<table class="table table-striped table-hover mb-2 w-auto">
<thead>
<tr>
{% for column in object_list.0.keys %}<th>{{ column|title }}</th>{% endfor %}
</tr>
</thead>
<tbody>
{% for object in object_list %}
<tr>
{% for field in object.values %}<td>{{ field }}</td>{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<nav aria-label="Page navigation">
<div class="text-center mb-2">Showing {{ page_obj.object_list|length }} of {{ paginator.count }} results.</div>
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link"
href="?{{ query_params }}&page={{ page_obj.previous_page_number }}">Previous</a>
</li>
{% endif %}
{% for page_num in paginator_range %}
<li class="page-item {% if page_num == page_obj.number %} active {% elif page_num == paginator.ELLIPSIS %} disabled {% endif %}">
<a class="page-link" href="?{{ query_params }}&page={{ page_num }}">{{ page_num }}</a>
</li>
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link"
href="?{{ query_params }}&page={{ page_obj.next_page_number }}">Next</a>
</li>
{% endif %}
</ul>
</nav>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -8,11 +8,14 @@ from django.db.models import Count, F, FloatField, Window
from django.db.models.functions import Lead, Trunc from django.db.models.functions import Lead, Trunc
from django.urls import path, reverse_lazy from django.urls import path, reverse_lazy
from django.utils import dateparse from django.utils import dateparse
from django.utils.formats import date_format
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.timezone import localtime from django.utils.timezone import localtime
from django.views.generic.list import ListView from django.views.generic.list import ListView
import django_tables2 as tables
from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin
from .models import HIDEvent from .models import HIDEvent
REPORTS = [] REPORTS = []
@ -23,13 +26,17 @@ def register_report(cls: "BaseAccessReport"):
return cls return cls
class BaseAccessReport(PermissionRequiredMixin, ListView): class BaseAccessReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView
):
model = HIDEvent model = HIDEvent
permission_required = "doorcontrol.view_hidevent" permission_required = "doorcontrol.view_hidevent"
paginate_by = 20 paginate_by = 20
context_object_name = "object_list" context_object_name = "object_list"
template_name = "doorcontrol/access_report.dj.html" template_name = "doorcontrol/access_report.dj.html"
export_formats = ("csv", "xlsx", "ods")
_report_name = None _report_name = None
@classmethod @classmethod
@ -44,6 +51,10 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
slug = slugify(cls._report_name) slug = slugify(cls._report_name)
return path(f"reports/{slug}", cls.as_view(), name=slug) return path(f"reports/{slug}", cls.as_view(), name=slug)
@property
def export_name(self):
return slugify(self._report_name)
def _selected_report(self): def _selected_report(self):
return self._report_name return self._report_name
@ -95,8 +106,23 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
return context 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 @register_report
class AccessPerUnitTime(BaseAccessReport): class AccessPerUnitTime(BaseAccessReport):
table_class = UnitTimeTable
UNIT_TIMES = ["day", "week", "month", "year"] UNIT_TIMES = ["day", "week", "month", "year"]
@classmethod @classmethod
@ -115,23 +141,41 @@ class AccessPerUnitTime(BaseAccessReport):
name="access-per-unit-time", name="access-per-unit-time",
) )
@property
def _report_name(self):
unit_time = self.kwargs["unit_time"]
return "Access per " + unit_time.title()
def _selected_report(self) -> str: def _selected_report(self) -> str:
return "Access per " + self.kwargs["unit_time"].title() return "Access per " + self.kwargs["unit_time"].title()
def _format_date(self, date: datetime.datetime) -> str: def get_table_kwargs(self):
unit_time = self.kwargs["unit_time"] unit_time = self.kwargs["unit_time"]
if unit_time == "day": if unit_time == "week":
return date_format(date, "DATE_FORMAT") unit_time_column = tables.TemplateColumn(
elif unit_time == "week": verbose_name=unit_time.title(),
return ( template_code=(
date_format(date, "DATE_FORMAT") "{{ value|date|default:default }} - "
+ " - " "{{ value|add:one_week|date|default:default }}"
+ date_format(date + datetime.timedelta(weeks=1), "DATE_FORMAT") ),
extra_context={"one_week": datetime.timedelta(weeks=1)},
) )
else:
if unit_time == "day":
date_format = "DATE_FORMAT"
elif unit_time == "month": elif unit_time == "month":
return date_format(date, "N Y") date_format = "N Y"
elif unit_time == "year": elif unit_time == "year":
return date_format(date, "Y") date_format = "Y"
unit_time_column = tables.DateColumn(
date_format, verbose_name=unit_time.title()
)
return {
"sequence": ("unit_time", "..."),
"extra_columns": (("unit_time", unit_time_column),),
}
def get_queryset(self): def get_queryset(self):
unit_time = self.kwargs["unit_time"] unit_time = self.kwargs["unit_time"]
@ -141,7 +185,7 @@ class AccessPerUnitTime(BaseAccessReport):
granted_event_types = [ granted_event_types = [
t for t in HIDEvent.EventType if t.name.startswith("GRANTED_ACCESS") t for t in HIDEvent.EventType if t.name.startswith("GRANTED_ACCESS")
] ]
events = ( return (
super() super()
.get_queryset() .get_queryset()
.filter(event_type__in=granted_event_types) .filter(event_type__in=granted_event_types)
@ -172,64 +216,58 @@ class AccessPerUnitTime(BaseAccessReport):
) )
.order_by("-unit_time") .order_by("-unit_time")
) )
return [
{
unit_time: self._format_date(event["unit_time"]), class DeniedAccessTable(tables.Table):
"members": event["members"], name = tables.TemplateColumn(
"Δ members": ( "{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
f'{event["members_delta"]:.2f}%' )
if event["members_delta"] is not None
else "" class Meta:
), model = HIDEvent
"access count": event["access_count"],
"Δ access count": ( fields = (
f'{event["access_count_delta"]:.2f}%' "timestamp",
if event["access_count_delta"] is not None "door",
else "" "event_type",
), "name",
} "raw_card_number",
for event in events "decoded_card_number",
] )
@register_report @register_report
class DeniedAccess(BaseAccessReport): class DeniedAccess(BaseAccessReport):
_report_name = "Denied Access" _report_name = "Denied Access"
table_class = DeniedAccessTable
def get_queryset(self): def get_queryset(self):
denied_event_types = [ denied_event_types = [
t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS") t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS")
] ]
events = ( return (
super() super()
.get_queryset() .get_queryset()
.filter(event_type__in=denied_event_types) .filter(event_type__in=denied_event_types)
.with_decoded_card_number() .with_decoded_card_number()
) )
return [
{ class MostActiveMembersTable(tables.Table):
"timestamp": event.timestamp, cardholder_id = tables.Column()
"door name": event.door.name, name = tables.TemplateColumn(
"event type": HIDEvent.EventType(event.event_type).label, "{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
"name": " ".join( )
n for n in [event.forename, event.surname] if n is not None access_count = tables.Column()
),
"raw card number": (
event.raw_card_number if event.raw_card_number is not None else ""
),
"card number": event.decoded_card_number() or "",
}
for event in events
]
@register_report @register_report
class MostActiveMembers(BaseAccessReport): class MostActiveMembers(BaseAccessReport):
_report_name = "Most Active Members" _report_name = "Most Active Members"
table_class = MostActiveMembersTable
def get_queryset(self): def get_queryset(self):
counts = ( return (
super() super()
.get_queryset() .get_queryset()
.values("cardholder_id", "forename", "surname") .values("cardholder_id", "forename", "surname")
@ -238,45 +276,45 @@ class MostActiveMembers(BaseAccessReport):
.order_by("-access_count") .order_by("-access_count")
) )
return [
{ class BusiestDayOfWeekTable(tables.Table):
"cardholder id": count["cardholder_id"], timestamp__week_day = tables.Column("Week Day")
"name": " ".join( events = tables.Column()
n for n in [count["forename"], count["surname"]] if n is not None
), def render_timestamp__week_day(self, value):
"access count": count["access_count"], return calendar.day_name[(value - 2) % 7]
}
for count in counts
]
@register_report @register_report
class BusiestDayOfWeek(BaseAccessReport): class BusiestDayOfWeek(BaseAccessReport):
_report_name = "Busiest Day of the Week" _report_name = "Busiest Day of the Week"
table_pagination = False
table_class = BusiestDayOfWeekTable
def get_queryset(self): def get_queryset(self):
return [ return (
{ super()
"week day": calendar.day_name[(count["timestamp__week_day"] - 2) % 7],
"events": count["events"],
}
for count in super()
.get_queryset() .get_queryset()
.values("timestamp__week_day") .values("timestamp__week_day")
.annotate(events=Count("timestamp")) .annotate(events=Count("timestamp"))
] )
class BusiestTimeOfDayTable(tables.Table):
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
events = tables.Column()
@register_report @register_report
class BusiestTimeOfDay(BaseAccessReport): class BusiestTimeOfDay(BaseAccessReport):
_report_name = "Busiest Time of Day" _report_name = "Busiest Time of Day"
paginate_by = 24 table_pagination = False
table_class = BusiestTimeOfDayTable
def get_queryset(self): def get_queryset(self):
return [ return (
{"hour": f'{count["timestamp__hour"]}:00', "events": count["events"]} super()
for count in super()
.get_queryset() .get_queryset()
.values("timestamp__hour") .values("timestamp__hour")
.annotate(events=Count("timestamp")) .annotate(events=Count("timestamp"))
] )

View File

@ -433,7 +433,7 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
return method( return method(
count__sum=Sum("count", filter=F("occurred")), count__sum=Sum("count", filter=F("occurred")),
instructor__count=Count("instructor", distinct=True, filter=F("occurred")), instructor__count=Count("instructor", distinct=True, filter=F("occurred")),
meeting_times__count=Count("meeting_times__count", filter=F("occurred")), meeting_times__count__sum=Sum("meeting_times__count", filter=F("occurred")),
duration__sum=Sum("duration", filter=F("occurred")), duration__sum=Sum("duration", filter=F("occurred")),
person_hours__sum=Sum("person_hours", filter=F("occurred")), person_hours__sum=Sum("person_hours", filter=F("occurred")),
event_count=Count("eid", filter=F("occurred")), event_count=Count("eid", filter=F("occurred")),

View File

@ -1,42 +1,10 @@
{% extends "base.dj.html" %} {% extends "base.dj.html" %}
{% load membershipworks_tags %} {% load render_table from django_tables2 %}
{% block title %}Event Report{% endblock %} {% block title %}Event Report Index{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}<li class="breadcrumb-item active" aria-current="page">MW Event Reports</li>{% endblock %}
<li class="breadcrumb-item active" aria-current="page">MW Event Reports</li>
{% endblock %}
{% block content %} {% block content %}
<div class="table-responsive"> {% include "membershipworks/components/download_table.dj.html" %}
<table class="table"> {% render_table table %}
<thead>
<tr>
<th for="column">Month</th>
<th for="column">Events</th>
<th for="column">Canceled Events</th>
<th for="column">Tickets</th>
<th for="column">Unique Instructors</th>
<th for="column">Meetings</th>
<th for="column">Total Hours</th>
<th for="column">Total Person Hours</th>
</tr>
</thead>
<tbody>
{% for year in object_list %}
<tr>
<td>
<a href="{% url 'membershipworks:event-year-report' year.year|date:"Y" %}">{{ year.year|date:"Y" }}</a>
</td>
<td>{{ year.event_count }}</td>
<td>{{ year.canceled_event_count }}</td>
<td>{{ year.count__sum }}</td>
<td>{{ year.instructor__count }}</td>
<td>{{ year.meeting_times__count }}</td>
<td>{{ year.duration__sum|duration_as_hours }}</td>
<td>{{ year.person_hours__sum|duration_as_hours }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %} {% endblock %}

View File

@ -1,50 +1,20 @@
{% extends "base.dj.html" %} {% extends "base.dj.html" %}
{% load membershipworks_tags %} {% load render_table from django_tables2 %}
{% block title %}Event Report{% endblock %} {% block title %}Event Report {{ month|date:"N Y" }}{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'membershipworks:event-index-report' %}">MW Event Reports</a></li> <li class="breadcrumb-item">
<a href="{% url 'membershipworks:event-index-report' %}">MW Event Reports</a>
</li>
<li class="breadcrumb-item"> <li class="breadcrumb-item">
<a href="{% url 'membershipworks:event-year-report' month|date:"Y" %}">{{ month|date:"Y" }}</a> <a href="{% url 'membershipworks:event-year-report' month|date:"Y" %}">{{ month|date:"Y" }}</a>
</li> </li>
<li class="breadcrumb-item active" aria-current="page">{{ month|date:"F" }}</li> <li class="breadcrumb-item active" aria-current="page">{{ month|date:"F" }}</li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="table-responsive"> {% include "membershipworks/components/download_table.dj.html" %}
<table class="table"> {% render_table table %}
<thead>
<tr>
<th for="column">Title</th>
<th for="column">Date</th>
<th for="column">Instructor</th>
<th for="column">Category</th>
<th for="column">Ticket Count</th>
<th for="column">Ticket Cap</th>
<th for="column">Meetings</th>
<th for="column">Total Duration</th>
<th for="column">Person Hours</th>
</tr>
</thead>
<tbody>
{% for event in object_list %}
<tr {% if not event.occurred %}class="text-decoration-line-through table-danger"{% endif %}>
<td>
<a href="https://membershipworks.com/admin/#!event/admin/{{ event.url }}">{{ event.title }}</a>
</td>
<td>{{ event.start|date }}</td>
<td>{{ event.instructor }}</td>
<td>{{ event.category }}</td>
<td>{{ event.count }}</td>
<td>{{ event.cap }}</td>
<td>{{ event.meeting_times__count }}</td>
<td>{{ event.duration|duration_as_hours }}</td>
<td>{{ event.person_hours|duration_as_hours }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if previous_month %} {% if previous_month %}

View File

@ -1,45 +1,17 @@
{% extends "base.dj.html" %} {% extends "base.dj.html" %}
{% load membershipworks_tags %} {% load render_table from django_tables2 %}
{% block title %}Event Report{% endblock %} {% block title %}Event Report {{ year|date:"Y" }}{% endblock %}
{% block breadcrumbs %} {% block breadcrumbs %}
<li class="breadcrumb-item"><a href="{% url 'membershipworks:event-index-report' %}">MW Event Reports</a></li> <li class="breadcrumb-item">
<a href="{% url 'membershipworks:event-index-report' %}">MW Event Reports</a>
</li>
<li class="breadcrumb-item active" aria-current="page">{{ year|date:"Y" }}</li> <li class="breadcrumb-item active" aria-current="page">{{ year|date:"Y" }}</li>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="table-responsive"> {% include "membershipworks/components/download_table.dj.html" %}
<table class="table"> {% render_table table %}
<thead>
<tr>
<th for="column">Month</th>
<th for="column">Events</th>
<th for="column">Canceled Events</th>
<th for="column">Tickets</th>
<th for="column">Unique Instructors</th>
<th for="column">Meetings</th>
<th for="column">Total Hours</th>
<th for="column">Total Person Hours</th>
</tr>
</thead>
<tbody>
{% for month in object_list %}
<tr>
<td>
<a href="{% url 'membershipworks:event-month-report' month.month|date:"Y" month.month|date:"m" %}">{{ month.month|date:"F Y" }}</a>
</td>
<td>{{ month.event_count }}</td>
<td>{{ month.canceled_event_count }}</td>
<td>{{ month.count__sum }}</td>
<td>{{ month.instructor__count }}</td>
<td>{{ month.meeting_times__count }}</td>
<td>{{ month.duration__sum|duration_as_hours }}</td>
<td>{{ month.person_hours__sum|duration_as_hours }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if previous_year %} {% if previous_year %}

View File

@ -1,4 +1,4 @@
from datetime import datetime from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
@ -6,13 +6,17 @@ from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.db.models.functions import TruncMonth, TruncYear from django.db.models.functions import TruncMonth, TruncYear
from django.shortcuts import render from django.shortcuts import render
from django.template.defaultfilters import floatformat
from django.views.generic.dates import ( from django.views.generic.dates import (
ArchiveIndexView, ArchiveIndexView,
MonthArchiveView, MonthArchiveView,
YearArchiveView, YearArchiveView,
) )
import django_tables2 as tables
from dal import autocomplete from dal import autocomplete
from django_tables2 import A, SingleTableMixin
from django_tables2.export.views import ExportMixin
from membershipworks.membershipworks_api import MembershipWorks from membershipworks.membershipworks_api import MembershipWorks
@ -109,12 +113,83 @@ def upcoming_events(request):
return render(request, "membershipworks/upcoming_events.dj.html", context) return render(request, "membershipworks/upcoming_events.dj.html", context)
class EventIndexReport(PermissionRequiredMixin, ArchiveIndexView): 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.Column(
linkify=lambda record: f"https://membershipworks.com/admin/#!event/admin/{record.url}"
)
occurred = tables.BooleanColumn(visible=False)
start = tables.DateColumn("N d, Y")
duration = DurationColumn()
person_hours = DurationColumn()
meeting_times__count = tables.Column("Meetings")
class Meta:
model = EventExt
fields = (
"title",
"occurred",
"start",
"instructor",
"category",
"count",
"cap",
"meeting_times__count",
"duration",
"person_hours",
)
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")
meeting_times__count__sum = tables.Column("Meetings")
duration__sum = DurationColumn("Class Hours")
person_hours__sum = DurationColumn("Person Hours")
class EventIndexReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, ArchiveIndexView
):
permission_required = "membershipworks.view_eventext" permission_required = "membershipworks.view_eventext"
queryset = EventExt.objects.all() queryset = EventExt.objects.all()
date_field = "start" date_field = "start"
template_name = "membershipworks/event_index_report.dj.html" template_name = "membershipworks/event_index_report.dj.html"
make_object_list = True make_object_list = True
table_class = EventSummaryTable
export_formats = ("csv", "xlsx", "ods")
export_name = "mw_events_index"
def get_table_kwargs(self):
year_column = tables.DateColumn(
"Y",
linkify=(
"membershipworks:event-year-report",
[A("year__year")],
),
)
return {
"sequence": ("year", "..."),
"extra_columns": (("year", year_column),),
}
def get_dated_queryset(self, **lookup): def get_dated_queryset(self, **lookup):
return ( return (
@ -126,12 +201,32 @@ class EventIndexReport(PermissionRequiredMixin, ArchiveIndexView):
) )
class EventYearReport(PermissionRequiredMixin, YearArchiveView): class EventYearReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, YearArchiveView
):
permission_required = "membershipworks.view_eventext" permission_required = "membershipworks.view_eventext"
queryset = EventExt.objects.all() queryset = EventExt.objects.all()
date_field = "start" date_field = "start"
template_name = "membershipworks/event_year_report.dj.html" template_name = "membershipworks/event_year_report.dj.html"
make_object_list = True make_object_list = True
table_class = EventSummaryTable
export_formats = ("csv", "xlsx", "ods")
def get_export_filename(self, export_format):
return f"mw_events_{self.get_year()}.{export_format}"
def get_table_kwargs(self):
month_column = tables.DateColumn(
"F Y",
linkify=(
"membershipworks:event-month-report",
[A("month__year"), A("month__month")],
),
)
return {
"sequence": ("month", "..."),
"extra_columns": (("month", month_column),),
}
def get_dated_queryset(self, **lookup): def get_dated_queryset(self, **lookup):
return ( return (
@ -143,8 +238,15 @@ class EventYearReport(PermissionRequiredMixin, YearArchiveView):
) )
class EventMonthReport(PermissionRequiredMixin, MonthArchiveView): class EventMonthReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, MonthArchiveView
):
permission_required = "membershipworks.view_eventext" permission_required = "membershipworks.view_eventext"
queryset = EventExt.objects.select_related("category", "instructor").all() queryset = EventExt.objects.select_related("category", "instructor").all()
date_field = "start" date_field = "start"
template_name = "membershipworks/event_month_report.dj.html" template_name = "membershipworks/event_month_report.dj.html"
table_class = EventTable
export_formats = ("csv", "xlsx", "ods")
def get_export_filename(self, export_format):
return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}"

84
pdm.lock generated
View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "lint", "server", "typing", "dev"] groups = ["default", "debug", "lint", "server", "typing", "dev"]
strategy = ["cross_platform"] strategy = ["cross_platform"]
lock_version = "4.4.1" lock_version = "4.4.1"
content_hash = "sha256:73715d6c541091f09cb8dea4f2baba6b58b0972a615b44e6f85869f918fdb360" content_hash = "sha256:c0e5c80b47118152c5b0167a588d3d1d078b0f2a710b8fc97869052ca04e7874"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@ -368,6 +368,16 @@ files = [
{file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"},
] ]
[[package]]
name = "defusedxml"
version = "0.7.1"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
summary = "XML bomb protection for Python stdlib modules"
files = [
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]] [[package]]
name = "django" name = "django"
version = "5.0.1" version = "5.0.1"
@ -573,6 +583,18 @@ files = [
{file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"}, {file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"},
] ]
[[package]]
name = "django-tables2"
version = "2.7.0"
summary = "Table/data-grid framework for Django"
dependencies = [
"Django>=3.2",
]
files = [
{file = "django-tables2-2.7.0.tar.gz", hash = "sha256:4113fcc575eb438a12e83a4d4ea01452e4800d970e8bdd0e4122ac171af1900d"},
{file = "django_tables2-2.7.0-py2.py3-none-any.whl", hash = "sha256:99e06d966ca8ac69fd74092eb45c79a280dd5ca0ccb81395d96261f62128e1af"},
]
[[package]] [[package]]
name = "django-widget-tweaks" name = "django-widget-tweaks"
version = "1.5.0" version = "1.5.0"
@ -679,6 +701,16 @@ files = [
{file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"}, {file = "EditorConfig-0.12.3.tar.gz", hash = "sha256:57f8ce78afcba15c8b18d46b5170848c88d56fd38f05c2ec60dbbfcb8996e89e"},
] ]
[[package]]
name = "et-xmlfile"
version = "1.1.0"
requires_python = ">=3.6"
summary = "An implementation of lxml.xmlfile for the standard library"
files = [
{file = "et_xmlfile-1.1.0-py3-none-any.whl", hash = "sha256:a2ba85d1d6a74ef63837eed693bcb89c3f752169b0e3e7ae5b16ca5e1b3deada"},
{file = "et_xmlfile-1.1.0.tar.gz", hash = "sha256:8eb9e2bc2f8c97e37a2dc85a09ecdcdec9d8a396530a6d5a33b30b9a92da0c5c"},
]
[[package]] [[package]]
name = "executing" name = "executing"
version = "2.0.1" version = "2.0.1"
@ -1160,6 +1192,17 @@ files = [
{file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"}, {file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"},
] ]
[[package]]
name = "odfpy"
version = "1.4.1"
summary = "Python API and tools to manipulate OpenDocument files"
dependencies = [
"defusedxml",
]
files = [
{file = "odfpy-1.4.1.tar.gz", hash = "sha256:db766a6e59c5103212f3cc92ec8dd50a0f3a02790233ed0b52148b70d3c438ec"},
]
[[package]] [[package]]
name = "openapi-client-udm" name = "openapi-client-udm"
version = "1.0.2" version = "1.0.2"
@ -1175,6 +1218,19 @@ files = [
{file = "openapi_client_udm-1.0.2-py3-none-any.whl", hash = "sha256:453d4fe405542729cd307005e4b9710ae475693c6795616b1568e045114e1be5"}, {file = "openapi_client_udm-1.0.2-py3-none-any.whl", hash = "sha256:453d4fe405542729cd307005e4b9710ae475693c6795616b1568e045114e1be5"},
] ]
[[package]]
name = "openpyxl"
version = "3.1.2"
requires_python = ">=3.6"
summary = "A Python library to read/write Excel 2010 xlsx/xlsm files"
dependencies = [
"et-xmlfile",
]
files = [
{file = "openpyxl-3.1.2-py2.py3-none-any.whl", hash = "sha256:f91456ead12ab3c6c2e9491cf33ba6d08357d802192379bb482f1033ade496f5"},
{file = "openpyxl-3.1.2.tar.gz", hash = "sha256:a6f5977418eff3b2d5500d54d9db50c8277a368436f4e4f8ddb1be3422870184"},
]
[[package]] [[package]]
name = "packaging" name = "packaging"
version = "23.0" version = "23.0"
@ -1618,6 +1674,32 @@ files = [
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
] ]
[[package]]
name = "tablib"
version = "3.5.0"
requires_python = ">=3.8"
summary = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)"
files = [
{file = "tablib-3.5.0-py3-none-any.whl", hash = "sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9"},
{file = "tablib-3.5.0.tar.gz", hash = "sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33"},
]
[[package]]
name = "tablib"
version = "3.5.0"
extras = ["ods", "xlsx"]
requires_python = ">=3.8"
summary = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)"
dependencies = [
"odfpy",
"openpyxl>=2.6.0",
"tablib==3.5.0",
]
files = [
{file = "tablib-3.5.0-py3-none-any.whl", hash = "sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9"},
{file = "tablib-3.5.0.tar.gz", hash = "sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33"},
]
[[package]] [[package]]
name = "tinycss2" name = "tinycss2"
version = "1.1.1" version = "1.1.1"

View File

@ -29,6 +29,8 @@ dependencies = [
"openapi-client-udm~=1.0", "openapi-client-udm~=1.0",
"django-nh3~=0.1", "django-nh3~=0.1",
"nh3~=0.2", "nh3~=0.2",
"django-tables2~=2.7",
"tablib[ods,xlsx]~=3.5",
] ]
requires-python = ">=3.11" requires-python = ">=3.11"