import uuid from datetime import datetime from typing import Any 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.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_mysql.models.aggregates import GroupConcat 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 ( EventAttendeeTable, 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(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) # 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", "invoice") .with_financials() ) def get_export_filename(self, export_format): return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}" 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", "instructor", "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 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=GroupConcat("flags__name")) ), ) )