cmsmanage/membershipworks/views.py

544 lines
18 KiB
Python
Raw Permalink Normal View History

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):
2024-05-04 16:38:51 -04:00
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,
)
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", ", "))
),
)
)