2024-01-22 12:33:34 -05:00
from datetime import datetime , timedelta
2023-12-22 01:08:20 -05:00
from django . conf import settings
from django . contrib import messages
2024-01-17 11:16:43 -05:00
from django . contrib . auth . decorators import permission_required
2024-01-15 21:31:06 -05:00
from django . contrib . auth . mixins import PermissionRequiredMixin
2024-02-16 11:37:30 -05:00
from django . db . models import OuterRef , Q , Subquery
2024-01-19 15:33:54 -05:00
from django . db . models . functions import TruncMonth , TruncYear
2023-12-22 01:08:20 -05:00
from django . shortcuts import render
2024-01-22 12:33:34 -05:00
from django . template . defaultfilters import floatformat
2024-03-04 15:53:53 -05:00
from django . utils . html import format_html
from django . utils . safestring import SafeString
2024-02-02 19:26:06 -05:00
from django . views . generic import DetailView , ListView
2024-01-19 15:33:54 -05:00
from django . views . generic . dates import (
ArchiveIndexView ,
MonthArchiveView ,
YearArchiveView ,
)
2023-12-22 01:08:20 -05:00
2024-02-02 19:26:06 -05:00
import django_filters
2024-01-22 12:33:34 -05:00
import django_tables2 as tables
2022-05-05 17:21:48 -04:00
from dal import autocomplete
2024-02-02 19:26:06 -05:00
from django_filters . views import BaseFilterView
2024-02-16 11:37:30 -05:00
from django_mysql . models import GroupConcat
2024-01-22 12:33:34 -05:00
from django_tables2 import A , SingleTableMixin
2024-01-22 13:17:02 -05:00
from django_tables2 . export . views import ExportMixin
2022-02-03 13:45:58 -05:00
2024-01-17 21:17:24 -05:00
from membershipworks . membershipworks_api import MembershipWorks
2024-02-02 19:26:06 -05:00
from . models import EventAttendee , EventExt , Member
2022-05-05 17:21:48 -04:00
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 ( )
2023-12-22 01:08:20 -05:00
2024-01-17 11:16:43 -05:00
@permission_required ( " membershipworks.view_eventext " )
2023-12-22 01:08:20 -05:00
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 )
2024-01-15 21:31:06 -05:00
2024-01-22 12:33:34 -05:00
class DurationColumn ( tables . Column ) :
def render ( self , value : timedelta ) :
if value is None :
return None
return floatformat ( value . total_seconds ( ) / 60 / 60 , - 2 )
2024-01-22 13:17:02 -05:00
def value ( self , value : timedelta ) :
if value is None :
return None
return value . total_seconds ( ) / 60 / 60
2024-01-22 12:33:34 -05:00
class EventTable ( tables . Table ) :
2024-01-30 14:28:09 -05:00
title = tables . TemplateColumn (
template_code = (
' <a title= " MembershipWorks " href= " https://membershipworks.com/admin/#!event/admin/ {{ record.url }} " > {{ value }}</a> '
' <a title= " Admin " href= " { % u rl " admin:membershipworks_eventext_change " record.pk % } " ><i class= " bi bi-pencil-square " ></i></a> '
2024-01-31 20:18:46 -05:00
' <a title= " Invoice " href= " { % u rl " membershipworks:event-invoice " record.pk % } " ><i class= " bi bi-receipt " ></i></a> '
2024-01-30 14:28:09 -05:00
) ,
2024-04-04 00:23:06 -04:00
accessor = " unescaped_title " ,
2024-01-22 12:33:34 -05:00
)
2024-01-22 13:17:02 -05:00
occurred = tables . BooleanColumn ( visible = False )
2024-01-22 12:33:34 -05:00
start = tables . DateColumn ( " N d, Y " )
duration = DurationColumn ( )
person_hours = DurationColumn ( )
2024-01-29 14:04:35 -05:00
meetings = tables . Column ( )
2024-01-31 20:18:46 -05:00
gross_revenue = tables . Column ( )
total_due_to_instructor = tables . Column ( )
net_revenue = tables . Column ( )
2024-01-22 12:33:34 -05:00
class Meta :
model = EventExt
fields = (
" title " ,
2024-01-22 13:17:02 -05:00
" occurred " ,
2024-01-22 12:33:34 -05:00
" start " ,
" instructor " ,
" category " ,
" count " ,
" cap " ,
2024-01-29 14:04:35 -05:00
" meetings " ,
2024-01-22 12:33:34 -05:00
" duration " ,
" person_hours " ,
2024-01-31 20:18:46 -05:00
" gross_revenue " ,
" total_due_to_instructor " ,
" net_revenue " ,
2024-01-22 12:33:34 -05:00
)
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 " )
2024-01-29 14:04:35 -05:00
meetings__sum = tables . Column ( " Meetings " )
2024-01-22 12:33:34 -05:00
duration__sum = DurationColumn ( " Class Hours " )
person_hours__sum = DurationColumn ( " Person Hours " )
2024-01-31 20:18:46 -05:00
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 " )
2024-01-22 12:33:34 -05:00
2024-01-22 13:17:02 -05:00
class EventIndexReport (
ExportMixin , SingleTableMixin , PermissionRequiredMixin , ArchiveIndexView
) :
2024-01-19 15:33:54 -05:00
permission_required = " membershipworks.view_eventext "
queryset = EventExt . objects . all ( )
date_field = " start "
template_name = " membershipworks/event_index_report.dj.html "
make_object_list = True
2024-01-22 12:33:34 -05:00
table_class = EventSummaryTable
2024-01-22 13:17:02 -05:00
export_formats = ( " csv " , " xlsx " , " ods " )
export_name = " mw_events_index "
2024-01-22 12:33:34 -05:00
2024-01-29 14:13:00 -05:00
def get_table_data ( self ) :
return (
super ( )
. get_table_data ( )
2024-01-31 20:18:46 -05:00
. with_financials ( )
2024-01-29 14:13:00 -05:00
. values ( year = TruncYear ( " start " ) )
. summarize ( )
. order_by ( " year " )
)
2024-01-22 12:33:34 -05:00
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 ) , ) ,
}
2024-01-19 15:33:54 -05:00
2024-01-22 13:17:02 -05:00
class EventYearReport (
ExportMixin , SingleTableMixin , PermissionRequiredMixin , YearArchiveView
) :
2024-01-19 15:33:54 -05:00
permission_required = " membershipworks.view_eventext "
queryset = EventExt . objects . all ( )
date_field = " start "
template_name = " membershipworks/event_year_report.dj.html "
make_object_list = True
2024-01-22 12:33:34 -05:00
table_class = EventSummaryTable
2024-01-22 13:17:02 -05:00
export_formats = ( " csv " , " xlsx " , " ods " )
2024-01-29 14:13:00 -05:00
def get_table_data ( self ) :
return (
super ( )
. get_table_data ( )
2024-01-31 20:18:46 -05:00
. with_financials ( )
2024-01-29 14:13:00 -05:00
. values ( month = TruncMonth ( " start " ) )
. summarize ( )
. order_by ( " month " )
)
2024-01-22 13:17:02 -05:00
def get_export_filename ( self , export_format ) :
return f " mw_events_ { self . get_year ( ) } . { export_format } "
2024-01-22 12:33:34 -05:00
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 ) , ) ,
}
2024-01-19 15:33:54 -05:00
2024-01-22 13:17:02 -05:00
class EventMonthReport (
ExportMixin , SingleTableMixin , PermissionRequiredMixin , MonthArchiveView
) :
2024-01-15 21:31:06 -05:00
permission_required = " membershipworks.view_eventext "
2024-01-29 14:13:00 -05:00
queryset = EventExt . objects . all ( )
2024-01-15 21:31:06 -05:00
date_field = " start "
2024-01-17 11:15:57 -05:00
template_name = " membershipworks/event_month_report.dj.html "
2024-01-22 12:33:34 -05:00
table_class = EventTable
2024-01-22 13:17:02 -05:00
export_formats = ( " csv " , " xlsx " , " ods " )
2024-01-29 14:13:00 -05:00
def get_table_data ( self ) :
2024-01-31 20:18:46 -05:00
return (
super ( )
. get_table_data ( )
. select_related ( " category " , " instructor " )
. with_financials ( )
)
2024-01-29 14:13:00 -05:00
2024-01-22 13:17:02 -05:00
def get_export_filename ( self , export_format ) :
return f " mw_events_ { self . get_year ( ) } - { self . get_month ( ) : 02 } . { export_format } "
2024-01-31 20:18:46 -05:00
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 )
2024-03-04 15:53:53 -05:00
@staticmethod
def _math_header ( name : str , formula : str ) - > SafeString :
return format_html (
' {} <div class= " text-nowrap font-monospace fw-light " >[ {} ]</div> ' ,
name ,
formula ,
)
2024-01-31 20:18:46 -05:00
label = tables . Column ( " Ticket Type " , footer = " Subtotals " )
list_price = InvoiceMoneyColumn ( " Ticket Price " )
2024-03-04 15:53:53 -05:00
actual_price = InvoiceMoneyColumn ( _math_header ( " Actual Price " , " P " ) )
quantity = tables . Column (
_math_header ( " Quantity " , " Q " ) ,
footer = lambda table : table . event . quantity ,
)
amount = InvoiceMoneyFooterColumn ( _math_header ( " Amount " , " A=P*Q " ) )
materials = InvoiceMoneyFooterColumn (
_math_header ( " CMS Collected Materials Fee " , " M=m*Q " )
)
2024-01-31 20:18:46 -05:00
amount_without_materials = InvoiceMoneyFooterColumn (
2024-03-04 15:53:53 -05:00
_math_header ( " Event Revenue Base " , " B=A-M " )
)
instructor_revenue = InvoiceMoneyFooterColumn (
_math_header ( " Instructor Percentage Revenue " , " R=B*I " )
)
instructor_amount = InvoiceMoneyFooterColumn (
_math_header ( " Amount Due to Instructor " , " R+M " )
2024-01-31 20:18:46 -05:00
)
class Meta :
attrs = {
" tbody " : { " class " : " table-group-divider " } ,
" tfoot " : { " class " : " table-group-divider " } ,
}
orderable = False
class EventInvoiceView ( SingleTableMixin , PermissionRequiredMixin , DetailView ) :
permission_required = " membershipworks.view_eventext "
queryset = EventExt . objects . with_financials ( ) . all ( )
pk_url_kwarg = " eid "
context_object_name = " event "
template_name = " membershipworks/event_invoice.dj.html "
table_pagination = False
table_class = InvoiceTable
def get_table_data ( self ) :
return self . object . ticket_types . all ( )
def get_table_kwargs ( self ) :
return { " event " : self . object }
2024-02-02 19:26:06 -05:00
class EventAttendeeTable ( tables . Table ) :
class Meta :
model = EventAttendee
fields = ( " name " , " email " )
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 ( )
2024-02-16 11:37:30 -05:00
class MissingPaperworkTable ( tables . Table ) :
policy_agreement = tables . BooleanColumn ( )
authorize_charge = tables . BooleanColumn ( )
class Meta :
model = Member
fields = [
" first_name " ,
" last_name " ,
" membership " ,
" billing_method " ,
" join_date " ,
" membership_agreement_signed_and_on_file_date " ,
" waiver_form_signed_and_on_file_date " ,
" policy_agreement " ,
" authorize_charge " ,
]
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 " ) )
) ,
)
)