from datetime import datetime, timedelta from django.conf import settings from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.contrib.auth.mixins import PermissionRequiredMixin from django.db.models import OuterRef, Q, Subquery from django.db.models.functions import TruncMonth, TruncYear from django.shortcuts import render from django.template.defaultfilters import floatformat from django.utils.html import format_html from django.utils.safestring import SafeString from django.views.generic import DetailView, ListView from django.views.generic.dates import ( ArchiveIndexView, MonthArchiveView, YearArchiveView, ) import django_filters import django_tables2 as tables from dal import autocomplete from django_filters.views import BaseFilterView from django_mysql.models import GroupConcat from django_tables2 import A, SingleTableMixin from django_tables2.export.views import ExportMixin from membershipworks.membershipworks_api import MembershipWorks from .models import EventAttendee, EventExt, Member 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 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.TemplateColumn( template_code=( '{{ value }} ' ' ' ' ' ), ) occurred = tables.BooleanColumn(visible=False) start = tables.DateColumn("N d, Y") duration = DurationColumn() person_hours = DurationColumn() meetings = tables.Column() gross_revenue = tables.Column() total_due_to_instructor = tables.Column() net_revenue = tables.Column() class Meta: model = EventExt fields = ( "title", "occurred", "start", "instructor", "category", "count", "cap", "meetings", "duration", "person_hours", "gross_revenue", "total_due_to_instructor", "net_revenue", ) 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") meetings__sum = tables.Column("Meetings") duration__sum = DurationColumn("Class Hours") person_hours__sum = DurationColumn("Person Hours") gross_revenue__sum = tables.Column("Gross Revenue") total_due_to_instructor__sum = tables.Column("Total Due to Instructor") net_revenue__sum = tables.Column("Net Revenue") 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") .with_financials() ) def get_export_filename(self, export_format): return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}" class InvoiceMoneyColumn(tables.columns.Column): def render(self, value): return f"${super().render(value):.2f}" class InvoiceMoneyFooterColumn(InvoiceMoneyColumn): def render_footer(self, bound_column, table): value = getattr(table.event, bound_column.accessor) if value is not None: return f"${value:.2f}" else: return bound_column.default class InvoiceTable(tables.Table): def __init__(self, *args, **kwargs): self.event = kwargs.pop("event") super().__init__(*args, **kwargs) @staticmethod def _math_header(name: str, formula: str) -> SafeString: return format_html( '{}