2024-04-14 01:21:32 -04:00
import uuid
2024-04-18 11:30:18 -04:00
from datetime import datetime
2024-04-14 01:21:32 -04:00
from typing import Any
2024-05-05 23:20:25 -04:00
from urllib . parse import quote , urlencode
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-04-14 01:21:32 -04:00
from django . contrib . auth . mixins import (
AccessMixin ,
PermissionRequiredMixin ,
)
from django . core import mail
from django . core . files . base import ContentFile
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
2024-04-14 01:21:32 -04:00
from django . http import HttpRequest , HttpResponse
2023-12-22 01:08:20 -05:00
from django . shortcuts import render
2024-04-14 01:21:32 -04:00
from django . template . loader import render_to_string
from django . utils import timezone
from django . utils . functional import cached_property
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 ,
)
2024-04-14 01:21:32 -04:00
from django . views . generic . detail import BaseDetailView
from django . views . generic . edit import FormMixin , ProcessFormView
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
2024-04-14 01:21:32 -04:00
import weasyprint
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-05-04 16:38:51 -04:00
from django_mysql . models . aggregates import GroupConcat
2024-04-14 01:21:32 -04:00
from django_sendfile import sendfile
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
2024-04-27 10:53:37 -04:00
from django_weasyprint import WeasyTemplateResponseMixin
2024-04-14 01:21:32 -04:00
from django_weasyprint . utils import django_url_fetcher
2022-02-03 13:45:58 -05:00
2024-01-17 21:17:24 -05:00
from membershipworks . membershipworks_api import MembershipWorks
2024-04-14 01:21:32 -04:00
from . forms import EventInvoiceForm
from . invoice_email import make_invoice_emails
from . models import EventAttendee , EventExt , EventInvoice , Member
2024-04-18 11:30:18 -04:00
from . tables import (
2024-07-18 12:58:35 -04:00
CurrentAndUpcomingEventTable ,
2024-04-18 11:30:18 -04:00
EventAttendeeTable ,
2024-05-05 23:20:25 -04:00
EventRegistrationsTable ,
2024-04-18 11:30:18 -04:00
EventSummaryTable ,
EventTable ,
InvoiceTable ,
MissingPaperworkTable ,
UserEventTable ,
)
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 " )
2024-07-18 11:42:10 -04:00
def upcoming_events_wordpress ( request ) :
2023-12-22 01:08:20 -05:00
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 )
2024-07-18 22:28:00 -04:00
# canceled events
elif event_details [ " cap " ] == 0 :
continue
2023-12-22 01:08:20 -05:00
# 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 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 ( )
2024-07-18 12:51:51 -04:00
. select_related ( " category " , " instructor " , " instructor__member " , " invoice " )
2024-01-31 20:18:46 -05:00
. 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
2024-07-18 12:58:35 -04:00
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 ( )
)
2024-04-14 01:21:32 -04:00
class UserEventView ( SingleTableMixin , ListView ) :
2024-05-04 16:38:51 -04:00
model : type [ EventExt ] = EventExt
2024-04-14 01:21:32 -04:00
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 ( )
2024-07-18 12:51:51 -04:00
. select_related ( " category " , " invoice " )
2024-04-14 01:21:32 -04:00
. 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
) :
2024-01-31 20:18:46 -05:00
permission_required = " membershipworks.view_eventext "
queryset = EventExt . objects . with_financials ( ) . all ( )
pk_url_kwarg = " eid "
context_object_name = " event "
2024-04-14 01:21:32 -04:00
template_name = " membershipworks/event_detail.dj.html "
2024-01-31 20:18:46 -05:00
table_pagination = False
table_class = InvoiceTable
2024-04-14 01:21:32 -04:00
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 " )
)
2024-01-31 20:18:46 -05:00
def get_table_data ( self ) :
2024-04-14 01:21:32 -04:00
if self . display_instructor_version ( ) :
return self . object . ticket_types . group_by_ticket_type ( )
else :
return self . object . ticket_types . all ( )
2024-01-31 20:18:46 -05:00
def get_table_kwargs ( self ) :
2024-04-14 01:21:32 -04:00
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 ,
2024-04-27 10:57:04 -04:00
date_submitted = pdf_context [ " now " ] ,
2024-04-14 01:21:32 -04:00
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 )
2024-04-27 10:53:37 -04:00
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
2024-04-14 01:21:32 -04:00
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 ( )
2024-02-02 19:26:06 -05:00
2024-05-05 23:20:25 -04:00
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
2024-02-02 19:26:06 -05:00
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 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 " ) )
) ,
)
)