Compare commits
9 Commits
df621988f6
...
19884bcbc4
Author | SHA1 | Date | |
---|---|---|---|
19884bcbc4 | |||
a2950c0279 | |||
97e9a3c9d5 | |||
1f3cd94601 | |||
1dd2fa521a | |||
0a3a709ab5 | |||
8155402dbb | |||
c489c492b2 | |||
599b7f7785 |
@ -38,6 +38,7 @@ INSTALLED_APPS = [
|
||||
"rest_framework.authtoken",
|
||||
"django_q",
|
||||
"django_nh3",
|
||||
"django_tables2",
|
||||
"tasks.apps.TasksConfig",
|
||||
"rentals.apps.RentalsConfig",
|
||||
"membershipworks.apps.MembershipworksConfig",
|
||||
@ -134,3 +135,6 @@ Q_CLUSTER = {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
# Django-Tables2
|
||||
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5-responsive.html"
|
||||
|
@ -1,5 +1,7 @@
|
||||
{% extends "base.dj.html" %}
|
||||
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %}
|
||||
{% block content %}
|
||||
<div class="vstack align-items-center">
|
||||
@ -11,9 +13,9 @@
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<form method="get" class="container">
|
||||
<div class="row align-items-center">
|
||||
<div class="col gy-1">
|
||||
<form method="get" class="container-fluid">
|
||||
<div class="row g-2 align-items-center justify-content-center">
|
||||
<div class="col-6 col-sm-auto">
|
||||
<div class="form-floating">
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
@ -23,7 +25,7 @@
|
||||
<label for="startDate">Start Date</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col gy-1">
|
||||
<div class="col-6 col-sm-auto">
|
||||
<div class="form-floating">
|
||||
<input type="date"
|
||||
class="form-control"
|
||||
@ -33,7 +35,7 @@
|
||||
<label for="endDate">End Date</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md gy-1">
|
||||
<div class="col-12 col-sm-auto">
|
||||
<div class="form-floating">
|
||||
<input type="number"
|
||||
class="form-control"
|
||||
@ -47,51 +49,15 @@
|
||||
<label for="itemsPerPage">Items Per Page</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-6 col-md-auto gy-1">
|
||||
<button type="submit" class="btn btn-primary w-100">Submit</button>
|
||||
<div class="btn-group col-auto" role="group" aria-label="Form Controls">
|
||||
<button type="submit" class="btn btn-sm btn-primary">Submit</button>
|
||||
<a href="?" class="btn btn-sm btn-warning">Reset</a>
|
||||
</div>
|
||||
<div class="col-6 col-md-auto gy-1">
|
||||
<a href="?" class="btn btn-warning w-100">Reset</a>
|
||||
<div class="col-auto">
|
||||
{% include "membershipworks/components/download_table.dj.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="table-responsive mw-100">
|
||||
<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>
|
||||
{% render_table table %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -8,11 +8,14 @@ from django.db.models import Count, F, FloatField, Window
|
||||
from django.db.models.functions import Lead, Trunc
|
||||
from django.urls import path, reverse_lazy
|
||||
from django.utils import dateparse
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.text import slugify
|
||||
from django.utils.timezone import localtime
|
||||
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
|
||||
|
||||
REPORTS = []
|
||||
@ -23,13 +26,17 @@ def register_report(cls: "BaseAccessReport"):
|
||||
return cls
|
||||
|
||||
|
||||
class BaseAccessReport(PermissionRequiredMixin, ListView):
|
||||
class BaseAccessReport(
|
||||
ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView
|
||||
):
|
||||
model = HIDEvent
|
||||
permission_required = "doorcontrol.view_hidevent"
|
||||
paginate_by = 20
|
||||
context_object_name = "object_list"
|
||||
template_name = "doorcontrol/access_report.dj.html"
|
||||
|
||||
export_formats = ("csv", "xlsx", "ods")
|
||||
|
||||
_report_name = None
|
||||
|
||||
@classmethod
|
||||
@ -44,6 +51,10 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
|
||||
slug = slugify(cls._report_name)
|
||||
return path(f"reports/{slug}", cls.as_view(), name=slug)
|
||||
|
||||
@property
|
||||
def export_name(self):
|
||||
return slugify(self._report_name)
|
||||
|
||||
def _selected_report(self):
|
||||
return self._report_name
|
||||
|
||||
@ -95,8 +106,23 @@ class BaseAccessReport(PermissionRequiredMixin, ListView):
|
||||
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
|
||||
UNIT_TIMES = ["day", "week", "month", "year"]
|
||||
|
||||
@classmethod
|
||||
@ -115,23 +141,41 @@ class AccessPerUnitTime(BaseAccessReport):
|
||||
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:
|
||||
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"]
|
||||
if unit_time == "day":
|
||||
return date_format(date, "DATE_FORMAT")
|
||||
elif unit_time == "week":
|
||||
return (
|
||||
date_format(date, "DATE_FORMAT")
|
||||
+ " - "
|
||||
+ date_format(date + datetime.timedelta(weeks=1), "DATE_FORMAT")
|
||||
if unit_time == "week":
|
||||
unit_time_column = tables.TemplateColumn(
|
||||
verbose_name=unit_time.title(),
|
||||
template_code=(
|
||||
"{{ value|date|default:default }} - "
|
||||
"{{ value|add:one_week|date|default:default }}"
|
||||
),
|
||||
extra_context={"one_week": datetime.timedelta(weeks=1)},
|
||||
)
|
||||
elif unit_time == "month":
|
||||
return date_format(date, "N Y")
|
||||
elif unit_time == "year":
|
||||
return date_format(date, "Y")
|
||||
else:
|
||||
if unit_time == "day":
|
||||
date_format = "DATE_FORMAT"
|
||||
elif unit_time == "month":
|
||||
date_format = "N Y"
|
||||
elif unit_time == "year":
|
||||
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):
|
||||
unit_time = self.kwargs["unit_time"]
|
||||
@ -141,7 +185,7 @@ class AccessPerUnitTime(BaseAccessReport):
|
||||
granted_event_types = [
|
||||
t for t in HIDEvent.EventType if t.name.startswith("GRANTED_ACCESS")
|
||||
]
|
||||
events = (
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(event_type__in=granted_event_types)
|
||||
@ -172,64 +216,58 @@ class AccessPerUnitTime(BaseAccessReport):
|
||||
)
|
||||
.order_by("-unit_time")
|
||||
)
|
||||
return [
|
||||
{
|
||||
unit_time: self._format_date(event["unit_time"]),
|
||||
"members": event["members"],
|
||||
"Δ members": (
|
||||
f'{event["members_delta"]:.2f}%'
|
||||
if event["members_delta"] is not None
|
||||
else ""
|
||||
),
|
||||
"access count": event["access_count"],
|
||||
"Δ access count": (
|
||||
f'{event["access_count_delta"]:.2f}%'
|
||||
if event["access_count_delta"] is not None
|
||||
else ""
|
||||
),
|
||||
}
|
||||
for event in events
|
||||
]
|
||||
|
||||
|
||||
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"
|
||||
table_class = DeniedAccessTable
|
||||
|
||||
def get_queryset(self):
|
||||
denied_event_types = [
|
||||
t for t in HIDEvent.EventType if t.name.startswith("DENIED_ACCESS")
|
||||
]
|
||||
events = (
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(event_type__in=denied_event_types)
|
||||
.with_decoded_card_number()
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"timestamp": event.timestamp,
|
||||
"door name": event.door.name,
|
||||
"event type": HIDEvent.EventType(event.event_type).label,
|
||||
"name": " ".join(
|
||||
n for n in [event.forename, event.surname] if n is not None
|
||||
),
|
||||
"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
|
||||
]
|
||||
|
||||
class MostActiveMembersTable(tables.Table):
|
||||
cardholder_id = tables.Column()
|
||||
name = tables.TemplateColumn(
|
||||
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
|
||||
)
|
||||
access_count = tables.Column()
|
||||
|
||||
|
||||
@register_report
|
||||
class MostActiveMembers(BaseAccessReport):
|
||||
_report_name = "Most Active Members"
|
||||
table_class = MostActiveMembersTable
|
||||
|
||||
def get_queryset(self):
|
||||
counts = (
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.values("cardholder_id", "forename", "surname")
|
||||
@ -238,45 +276,45 @@ class MostActiveMembers(BaseAccessReport):
|
||||
.order_by("-access_count")
|
||||
)
|
||||
|
||||
return [
|
||||
{
|
||||
"cardholder id": count["cardholder_id"],
|
||||
"name": " ".join(
|
||||
n for n in [count["forename"], count["surname"]] if n is not None
|
||||
),
|
||||
"access count": count["access_count"],
|
||||
}
|
||||
for count in counts
|
||||
]
|
||||
|
||||
class BusiestDayOfWeekTable(tables.Table):
|
||||
timestamp__week_day = tables.Column("Week Day")
|
||||
events = 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"
|
||||
table_pagination = False
|
||||
table_class = BusiestDayOfWeekTable
|
||||
|
||||
def get_queryset(self):
|
||||
return [
|
||||
{
|
||||
"week day": calendar.day_name[(count["timestamp__week_day"] - 2) % 7],
|
||||
"events": count["events"],
|
||||
}
|
||||
for count in super()
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.values("timestamp__week_day")
|
||||
.annotate(events=Count("timestamp"))
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class BusiestTimeOfDayTable(tables.Table):
|
||||
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
|
||||
events = tables.Column()
|
||||
|
||||
|
||||
@register_report
|
||||
class BusiestTimeOfDay(BaseAccessReport):
|
||||
_report_name = "Busiest Time of Day"
|
||||
paginate_by = 24
|
||||
table_pagination = False
|
||||
table_class = BusiestTimeOfDayTable
|
||||
|
||||
def get_queryset(self):
|
||||
return [
|
||||
{"hour": f'{count["timestamp__hour"]}:00', "events": count["events"]}
|
||||
for count in super()
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.values("timestamp__hour")
|
||||
.annotate(events=Count("timestamp"))
|
||||
]
|
||||
)
|
||||
|
@ -433,7 +433,7 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
|
||||
return method(
|
||||
count__sum=Sum("count", 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")),
|
||||
person_hours__sum=Sum("person_hours", filter=F("occurred")),
|
||||
event_count=Count("eid", filter=F("occurred")),
|
||||
|
@ -1,42 +1,10 @@
|
||||
{% extends "base.dj.html" %}
|
||||
|
||||
{% load membershipworks_tags %}
|
||||
{% load render_table from django_tables2 %}
|
||||
|
||||
{% block title %}Event Report{% endblock %}
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item active" aria-current="page">MW Event Reports</li>
|
||||
{% endblock %}
|
||||
{% block title %}Event Report Index{% endblock %}
|
||||
{% block breadcrumbs %}<li class="breadcrumb-item active" aria-current="page">MW Event Reports</li>{% endblock %}
|
||||
{% block content %}
|
||||
<div class="table-responsive">
|
||||
<table class="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>
|
||||
{% include "membershipworks/components/download_table.dj.html" %}
|
||||
{% render_table table %}
|
||||
{% endblock %}
|
||||
|
@ -1,50 +1,20 @@
|
||||
{% 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 %}
|
||||
<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">
|
||||
<a href="{% url 'membershipworks:event-year-report' month|date:"Y" %}">{{ month|date:"Y" }}</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ month|date:"F" }}</li>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="table-responsive">
|
||||
<table class="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>
|
||||
{% include "membershipworks/components/download_table.dj.html" %}
|
||||
{% render_table table %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if previous_month %}
|
||||
|
@ -1,45 +1,17 @@
|
||||
{% 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 %}
|
||||
<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>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="table-responsive">
|
||||
<table class="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>
|
||||
{% include "membershipworks/components/download_table.dj.html" %}
|
||||
{% render_table table %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination justify-content-center">
|
||||
{% if previous_year %}
|
||||
|
@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django.conf import settings
|
||||
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.db.models.functions import TruncMonth, TruncYear
|
||||
from django.shortcuts import render
|
||||
from django.template.defaultfilters import floatformat
|
||||
from django.views.generic.dates import (
|
||||
ArchiveIndexView,
|
||||
MonthArchiveView,
|
||||
YearArchiveView,
|
||||
)
|
||||
|
||||
import django_tables2 as tables
|
||||
from dal import autocomplete
|
||||
from django_tables2 import A, SingleTableMixin
|
||||
from django_tables2.export.views import ExportMixin
|
||||
|
||||
from membershipworks.membershipworks_api import MembershipWorks
|
||||
|
||||
@ -109,12 +113,83 @@ def upcoming_events(request):
|
||||
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"
|
||||
queryset = EventExt.objects.all()
|
||||
date_field = "start"
|
||||
template_name = "membershipworks/event_index_report.dj.html"
|
||||
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):
|
||||
return (
|
||||
@ -126,12 +201,32 @@ class EventIndexReport(PermissionRequiredMixin, ArchiveIndexView):
|
||||
)
|
||||
|
||||
|
||||
class EventYearReport(PermissionRequiredMixin, YearArchiveView):
|
||||
class EventYearReport(
|
||||
ExportMixin, SingleTableMixin, PermissionRequiredMixin, YearArchiveView
|
||||
):
|
||||
permission_required = "membershipworks.view_eventext"
|
||||
queryset = EventExt.objects.all()
|
||||
date_field = "start"
|
||||
template_name = "membershipworks/event_year_report.dj.html"
|
||||
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):
|
||||
return (
|
||||
@ -143,8 +238,15 @@ class EventYearReport(PermissionRequiredMixin, YearArchiveView):
|
||||
)
|
||||
|
||||
|
||||
class EventMonthReport(PermissionRequiredMixin, MonthArchiveView):
|
||||
class EventMonthReport(
|
||||
ExportMixin, SingleTableMixin, PermissionRequiredMixin, MonthArchiveView
|
||||
):
|
||||
permission_required = "membershipworks.view_eventext"
|
||||
queryset = EventExt.objects.select_related("category", "instructor").all()
|
||||
date_field = "start"
|
||||
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
84
pdm.lock
@ -5,7 +5,7 @@
|
||||
groups = ["default", "debug", "lint", "server", "typing", "dev"]
|
||||
strategy = ["cross_platform"]
|
||||
lock_version = "4.4.1"
|
||||
content_hash = "sha256:73715d6c541091f09cb8dea4f2baba6b58b0972a615b44e6f85869f918fdb360"
|
||||
content_hash = "sha256:c0e5c80b47118152c5b0167a588d3d1d078b0f2a710b8fc97869052ca04e7874"
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp"
|
||||
@ -368,6 +368,16 @@ files = [
|
||||
{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]]
|
||||
name = "django"
|
||||
version = "5.0.1"
|
||||
@ -573,6 +583,18 @@ files = [
|
||||
{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]]
|
||||
name = "django-widget-tweaks"
|
||||
version = "1.5.0"
|
||||
@ -679,6 +701,16 @@ files = [
|
||||
{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]]
|
||||
name = "executing"
|
||||
version = "2.0.1"
|
||||
@ -1160,6 +1192,17 @@ files = [
|
||||
{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]]
|
||||
name = "openapi-client-udm"
|
||||
version = "1.0.2"
|
||||
@ -1175,6 +1218,19 @@ files = [
|
||||
{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]]
|
||||
name = "packaging"
|
||||
version = "23.0"
|
||||
@ -1618,6 +1674,32 @@ files = [
|
||||
{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]]
|
||||
name = "tinycss2"
|
||||
version = "1.1.1"
|
||||
|
@ -29,6 +29,8 @@ dependencies = [
|
||||
"openapi-client-udm~=1.0",
|
||||
"django-nh3~=0.1",
|
||||
"nh3~=0.2",
|
||||
"django-tables2~=2.7",
|
||||
"tablib[ods,xlsx]~=3.5",
|
||||
]
|
||||
requires-python = ">=3.11"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user