MariaDB has become far too annoying/buggy, and there are some neat features only available in PostgreSQL
545 lines
18 KiB
Python
545 lines
18 KiB
Python
import uuid
|
|
from datetime import datetime
|
|
from typing import Any
|
|
from urllib.parse import quote, urlencode
|
|
|
|
from django.conf import settings
|
|
from django.contrib import messages
|
|
from django.contrib.auth.decorators import permission_required
|
|
from django.contrib.auth.mixins import (
|
|
AccessMixin,
|
|
PermissionRequiredMixin,
|
|
)
|
|
from django.contrib.postgres.aggregates import StringAgg
|
|
from django.core import mail
|
|
from django.core.files.base import ContentFile
|
|
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.loader import render_to_string
|
|
from django.utils import timezone
|
|
from django.utils.functional import cached_property
|
|
from django.views.generic import DetailView, ListView
|
|
from django.views.generic.dates import (
|
|
ArchiveIndexView,
|
|
MonthArchiveView,
|
|
YearArchiveView,
|
|
)
|
|
from django.views.generic.detail import BaseDetailView
|
|
from django.views.generic.edit import FormMixin, ProcessFormView
|
|
|
|
import django_filters
|
|
import django_tables2 as tables
|
|
import weasyprint
|
|
from dal import autocomplete
|
|
from django_filters.views import BaseFilterView
|
|
from django_sendfile import sendfile
|
|
from django_tables2 import A, SingleTableMixin
|
|
from django_tables2.export.views import ExportMixin
|
|
from django_weasyprint import WeasyTemplateResponseMixin
|
|
from django_weasyprint.utils import django_url_fetcher
|
|
|
|
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 (
|
|
CurrentAndUpcomingEventTable,
|
|
EventAttendeeTable,
|
|
EventRegistrationsTable,
|
|
EventSummaryTable,
|
|
EventTable,
|
|
InvoiceTable,
|
|
MissingPaperworkTable,
|
|
UserEventTable,
|
|
)
|
|
|
|
|
|
class MemberAutocomplete(autocomplete.Select2QuerySetView):
|
|
model = Member
|
|
search_fields = ["account_name"]
|
|
|
|
def get_queryset(self):
|
|
if not self.request.user.has_perm("membershipworks.view_member"):
|
|
return Member.objects.none()
|
|
else:
|
|
return super().get_queryset()
|
|
|
|
|
|
@permission_required("membershipworks.view_eventext")
|
|
def upcoming_events_wordpress(request):
|
|
now = datetime.now()
|
|
|
|
membershipworks = MembershipWorks()
|
|
membershipworks.login(
|
|
settings.MEMBERSHIPWORKS_USERNAME, settings.MEMBERSHIPWORKS_PASSWORD
|
|
)
|
|
|
|
events = membershipworks.get_events_list(now)
|
|
if "error" in events:
|
|
messages.add_message(
|
|
request,
|
|
messages.ERROR,
|
|
f"MembershipWorks Error: {events['error']}",
|
|
)
|
|
# TODO: this should probably be an HTTP 500 response
|
|
return render(request, "base.dj.html")
|
|
|
|
ongoing_events = []
|
|
full_events = []
|
|
upcoming_events = []
|
|
for event in events["evt"]:
|
|
try:
|
|
# ignore hidden events
|
|
if event["cal"] == 0:
|
|
continue
|
|
event_details = membershipworks.get_event_by_eid(event["eid"])
|
|
|
|
# Convert timestamps to datetime objects
|
|
event_details["sdp_dt"] = datetime.fromtimestamp(event_details["sdp"])
|
|
event_details["edp_dt"] = datetime.fromtimestamp(event_details["edp"])
|
|
|
|
# registration has already ended
|
|
if (
|
|
"erd" in event_details
|
|
and datetime.fromtimestamp(event_details["erd"]) < now
|
|
):
|
|
ongoing_events.append(event_details)
|
|
# canceled events
|
|
elif event_details["cap"] == 0:
|
|
continue
|
|
# class is full
|
|
elif event_details["cnt"] >= event_details["cap"]:
|
|
full_events.append(event_details)
|
|
else:
|
|
upcoming_events.append(event_details)
|
|
|
|
except KeyError as e:
|
|
messages.add_message(
|
|
request,
|
|
messages.ERROR,
|
|
f"Event '{event.get('ttl')}' missing required property: '{e.args[0]}'",
|
|
)
|
|
# TODO: this should probably be an HTTP 500 response
|
|
return render(request, "base.dj.html")
|
|
|
|
context = {
|
|
"event_sections": [
|
|
{
|
|
"title": "Upcoming Events",
|
|
"blurb": "Events that are currently open for registration.",
|
|
"events": upcoming_events,
|
|
"truncate": False,
|
|
},
|
|
{
|
|
"title": "Just Missed",
|
|
"blurb": "These classes are currently full at time of writing. If you are interested, please check the event's page; spots occasionally open up. Keep an eye on this newsletter to see when these classes are offered again.",
|
|
"events": full_events,
|
|
"truncate": True,
|
|
},
|
|
{
|
|
"title": "Ongoing Events",
|
|
"blurb": "These events are ongoing. Registration is currently closed, but these events may be offered again in the future.",
|
|
"events": ongoing_events,
|
|
"truncate": True,
|
|
},
|
|
]
|
|
}
|
|
return render(request, "membershipworks/upcoming_events.dj.html", context)
|
|
|
|
|
|
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_data(self):
|
|
return (
|
|
super()
|
|
.get_table_data()
|
|
.with_financials()
|
|
.values(year=TruncYear("start"))
|
|
.summarize()
|
|
.order_by("year")
|
|
)
|
|
|
|
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),),
|
|
}
|
|
|
|
|
|
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_table_data(self):
|
|
return (
|
|
super()
|
|
.get_table_data()
|
|
.with_financials()
|
|
.values(month=TruncMonth("start"))
|
|
.summarize()
|
|
.order_by("month")
|
|
)
|
|
|
|
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),),
|
|
}
|
|
|
|
|
|
class EventMonthReport(
|
|
ExportMixin, SingleTableMixin, PermissionRequiredMixin, MonthArchiveView
|
|
):
|
|
permission_required = "membershipworks.view_eventext"
|
|
queryset = EventExt.objects.all()
|
|
date_field = "start"
|
|
template_name = "membershipworks/event_month_report.dj.html"
|
|
table_class = EventTable
|
|
export_formats = ("csv", "xlsx", "ods")
|
|
|
|
def get_table_data(self):
|
|
return (
|
|
super()
|
|
.get_table_data()
|
|
.select_related("category", "instructor", "instructor__member", "invoice")
|
|
.with_financials()
|
|
)
|
|
|
|
def get_export_filename(self, export_format):
|
|
return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}"
|
|
|
|
|
|
class CurrentAndUpcomingEventView(SingleTableMixin, PermissionRequiredMixin, ListView):
|
|
permission_required = "membershipworks.view_eventext"
|
|
queryset = EventExt.objects.all()
|
|
date_field = "start"
|
|
template_name = "membershipworks/current_and_upcoming_events.dj.html"
|
|
table_class = CurrentAndUpcomingEventTable
|
|
|
|
def get_table_data(self):
|
|
return (
|
|
super()
|
|
.get_table_data()
|
|
.order_by("next_meeting_start")
|
|
.filter(end__gt=timezone.now())
|
|
.select_related("category", "instructor", "instructor__member")
|
|
.with_financials()
|
|
)
|
|
|
|
|
|
class UserEventView(SingleTableMixin, ListView):
|
|
model: type[EventExt] = EventExt
|
|
table_class = UserEventTable
|
|
export_formats = ("csv", "xlsx", "ods")
|
|
template_name = "membershipworks/user_event_list.dj.html"
|
|
|
|
@cached_property
|
|
def member(self) -> Member | None:
|
|
return Member.from_user(self.request.user)
|
|
|
|
def get_queryset(self):
|
|
if self.member is None:
|
|
return self.model.objects.none()
|
|
else:
|
|
return super().get_queryset().filter(instructor__member=self.member)
|
|
|
|
def get_table_data(self):
|
|
return (
|
|
super()
|
|
.get_table_data()
|
|
.select_related("category", "invoice")
|
|
.with_financials()
|
|
)
|
|
|
|
def get_export_filename(self, export_format):
|
|
return f"my_events_{self.member.uid if self.member is not None else 'no-uid'}.{export_format}"
|
|
|
|
|
|
class EventDetailView(
|
|
SingleTableMixin, FormMixin, AccessMixin, DetailView, ProcessFormView
|
|
):
|
|
permission_required = "membershipworks.view_eventext"
|
|
queryset = EventExt.objects.with_financials().all()
|
|
pk_url_kwarg = "eid"
|
|
context_object_name = "event"
|
|
template_name = "membershipworks/event_detail.dj.html"
|
|
table_pagination = False
|
|
table_class = InvoiceTable
|
|
form_class = EventInvoiceForm
|
|
|
|
def render_to_response(
|
|
self, context: dict[str, Any], **response_kwargs: Any
|
|
) -> HttpResponse:
|
|
if self.request.user.has_perm(
|
|
self.permission_required
|
|
) or self.object.user_is_instructor(self.request.user):
|
|
return super().render_to_response(context, **response_kwargs)
|
|
else:
|
|
return self.handle_no_permission()
|
|
|
|
def display_instructor_version(self):
|
|
return (
|
|
self.request.method == "POST" # generating a PDF
|
|
or not self.request.user.has_perm(self.permission_required)
|
|
or self.request.GET.get("instructor_view")
|
|
)
|
|
|
|
def get_table_data(self):
|
|
if self.display_instructor_version():
|
|
return self.object.ticket_types.group_by_ticket_type()
|
|
else:
|
|
return self.object.ticket_types.all()
|
|
|
|
def get_table_kwargs(self):
|
|
kwargs = super().get_table_kwargs()
|
|
|
|
kwargs["event"] = self.object
|
|
if self.display_instructor_version():
|
|
kwargs["exclude"] = [
|
|
"list_price",
|
|
]
|
|
|
|
return kwargs
|
|
|
|
def get_success_url(self):
|
|
return self.request.build_absolute_uri()
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context["user_is_instructor"] = self.object.user_is_instructor(
|
|
self.request.user
|
|
)
|
|
return context
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
self.object = self.get_object()
|
|
return super().post(request, *args, **kwargs)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs["event"] = self.object
|
|
kwargs["user"] = self.request.user
|
|
return kwargs
|
|
|
|
def form_valid(self, form: EventInvoiceForm):
|
|
self.object = self.get_object()
|
|
event = self.object
|
|
|
|
invoice_uuid = uuid.uuid4()
|
|
|
|
pdf_context = self.get_context_data(object=event)
|
|
pdf_context.update({"now": timezone.now(), "invoice_uuid": invoice_uuid})
|
|
weasy_html = weasyprint.HTML(
|
|
string=render_to_string(
|
|
"membershipworks/event_invoice_pdf.dj.html",
|
|
context=pdf_context,
|
|
request=self.request,
|
|
),
|
|
url_fetcher=django_url_fetcher,
|
|
base_url="file://",
|
|
)
|
|
pdf = weasy_html.write_pdf()
|
|
# the result will be None only if a target was provided
|
|
assert pdf is not None
|
|
|
|
# NOTE: this is only saved AFTER the emails are successfully sent
|
|
invoice = EventInvoice(
|
|
uuid=invoice_uuid,
|
|
event=event,
|
|
date_submitted=pdf_context["now"],
|
|
amount=event.total_due_to_instructor,
|
|
)
|
|
# removed), currently used in event_invoice_admin.dj.html.
|
|
|
|
emails = make_invoice_emails(
|
|
invoice, pdf, self.request.build_absolute_uri(event.get_absolute_url())
|
|
)
|
|
with mail.get_connection() as conn:
|
|
conn.send_messages(emails)
|
|
|
|
# this also saves the invoice object
|
|
invoice.pdf.save(f"{event.eid}.pdf", ContentFile(pdf))
|
|
|
|
messages.success(
|
|
self.request,
|
|
"Created Invoice! You should receive a confirmation email shortly.",
|
|
)
|
|
|
|
return super().form_valid(form)
|
|
|
|
|
|
class EventInvoicePDFPreviewView(WeasyTemplateResponseMixin, EventDetailView):
|
|
template_name = "membershipworks/event_invoice_pdf.dj.html"
|
|
pdf_attachment = False
|
|
|
|
def display_instructor_version(self):
|
|
return True
|
|
|
|
def get_pdf_filename(self):
|
|
return f"event-invoice_{self.object.pk}.pdf"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context.update(
|
|
{
|
|
"now": timezone.now(),
|
|
"invoice_uuid": "00000000-0000-0000-0000-000000000000",
|
|
"preview": True,
|
|
}
|
|
)
|
|
return context
|
|
|
|
|
|
class EventInvoicePDFView(AccessMixin, BaseDetailView):
|
|
model = EventInvoice
|
|
pk_url_kwarg = "uuid"
|
|
|
|
def get(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
|
invoice = self.get_object()
|
|
if request.user.has_perm(
|
|
"membershipworks.view_eventinvoice"
|
|
) or invoice.event.user_is_instructor(request.user):
|
|
return sendfile(request, invoice.pdf.path, mimetype="application/pdf")
|
|
|
|
else:
|
|
return self.handle_no_permission()
|
|
|
|
|
|
class EventRegistrationsView(ExportMixin, SingleTableMixin, AccessMixin, DetailView):
|
|
permission_required = "membershipworks.view_eventext"
|
|
model = EventExt
|
|
pk_url_kwarg = "eid"
|
|
context_object_name = "event"
|
|
template_name = "membershipworks/event_registrations.dj.html"
|
|
table_class = EventRegistrationsTable
|
|
export_formats = ("csv", "xlsx", "ods")
|
|
|
|
def render_to_response(
|
|
self, context: dict[str, Any], **response_kwargs: Any
|
|
) -> HttpResponse:
|
|
if self.request.user.has_perm(
|
|
self.permission_required
|
|
) or self.object.user_is_instructor(self.request.user):
|
|
return super().render_to_response(context, **response_kwargs)
|
|
else:
|
|
return self.handle_no_permission()
|
|
|
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
|
context_data = super().get_context_data(**kwargs)
|
|
|
|
context_data["email_link"] = "mailto:?" + urlencode(
|
|
{
|
|
"subject": f"[CMS Event] {self.object.title}",
|
|
"bcc": ",".join(
|
|
mail.message.sanitize_address(
|
|
(reg["Full name"], reg["Email"]), settings.DEFAULT_CHARSET
|
|
)
|
|
for reg in self.object.registrations
|
|
if any(
|
|
int(v) > 0 for k, v in reg.items() if k.startswith("Ticket: ")
|
|
)
|
|
),
|
|
},
|
|
quote_via=quote,
|
|
)
|
|
|
|
return context_data
|
|
|
|
def get_table_data(self):
|
|
return self.object.registrations
|
|
|
|
|
|
class EventAttendeeFilters(django_filters.FilterSet):
|
|
new_since = django_filters.DateFilter(
|
|
field_name="event__start", method="filter_new_since"
|
|
)
|
|
|
|
def filter_new_since(self, queryset, name, value):
|
|
return queryset.filter(**{f"{name}__gte": value}).exclude(
|
|
email__in=Subquery(
|
|
queryset.filter(**{f"{name}__lt": value}).values("email")
|
|
)
|
|
)
|
|
|
|
|
|
class EventAttendeeListView(
|
|
BaseFilterView, ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView
|
|
):
|
|
permission_required = "membershipworks.view_eventext"
|
|
queryset = EventAttendee.objects.all()
|
|
table_class = EventAttendeeTable
|
|
template_name = "membershipworks/eventattendee_list.dj.html"
|
|
export_formats = ("csv", "xlsx", "ods")
|
|
filterset_class = EventAttendeeFilters
|
|
|
|
def get_table_data(self):
|
|
return super().get_table_data().values("name", "email").distinct()
|
|
|
|
|
|
class MissingPaperworkReport(
|
|
ExportMixin,
|
|
SingleTableMixin,
|
|
PermissionRequiredMixin,
|
|
ListView,
|
|
):
|
|
model = Member
|
|
permission_required = "membershipworks.view_member"
|
|
template_name = "membershipworks/missing_paperwork_report.dj.html"
|
|
table_class = MissingPaperworkTable
|
|
export_formats = ("csv", "xlsx", "ods")
|
|
|
|
def get_queryset(self):
|
|
qs = super().get_queryset()
|
|
return (
|
|
qs.with_is_active()
|
|
.filter(
|
|
Q(membership_agreement_signed_and_on_file_date__isnull=True)
|
|
| Q(waiver_form_signed_and_on_file_date__isnull=True),
|
|
is_active=True,
|
|
)
|
|
.annotate(
|
|
membership=Subquery(
|
|
qs.filter(
|
|
pk=OuterRef("pk"), flags__type__in=("level", "addon")
|
|
).values(m=StringAgg("flags__name", ", "))
|
|
),
|
|
)
|
|
)
|