membershipworks: Add ability for instructors to generate and submit event invoices
Some checks failed
Ruff / ruff (push) Successful in 1m23s
Test / test (push) Failing after 6m7s

This commit is contained in:
Adam Goldsmith 2024-04-14 01:21:32 -04:00
parent 60e7fc90aa
commit 0ce441336f
19 changed files with 639 additions and 82 deletions

View File

@ -42,6 +42,7 @@ INSTALLED_APPS = [
"django_filters",
"django_db_views",
"django_mysql",
"django_sendfile",
"django_bootstrap5",
"tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig",
@ -121,6 +122,7 @@ LOGGING = {
MEDIA_ROOT = "media"
MEDIA_URL = "media/"
SENDFILE_ROOT = str(Path(__file__).parents[2] / "media" / "protected")
WIKI_URL = "https://wiki.claremontmakerspace.org"

View File

@ -21,3 +21,5 @@ MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa:
configure_hypothesis_profiles()
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "dev"))
SENDFILE_BACKEND = "django_sendfile.backends.development"

View File

@ -48,3 +48,6 @@ AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
)
AUTH_LDAP_GROUP_TYPE = PosixGroupType()
AUTH_LDAP_MIRROR_GROUPS = True
SENDFILE_BACKEND = "django_sendfile.backends.nginx"
SENDFILE_URL = "/media/protected"

View File

@ -2,32 +2,48 @@ from django.urls import reverse
import dashboard
from dashboard import Link
from membershipworks.models import EventExt, Member
@dashboard.register
class MembershipworksDashboardFragment(dashboard.LinksCardDashboardFragment):
name = "MembershipWorks"
links = [
Link(
"Upcoming Events",
reverse("membershipworks:upcoming-events"),
permission="membershipworks.view_event",
tooltip="Generator for Wordpress posts",
),
Link(
"Event Report",
reverse("membershipworks:event-index-report"),
permission="membershipworks.view_event",
),
Link(
"Event Attendees",
reverse("membershipworks:event-attendees"),
permission="membershipworks.view_event",
),
Link(
"Missing Paperwork",
reverse("membershipworks:missing-paperwork-report"),
permission="membershipworks.view_member",
),
]
@property
def links(self):
links = [
Link(
"Upcoming Events",
reverse("membershipworks:upcoming-events"),
permission="membershipworks.view_event",
tooltip="Generator for Wordpress posts",
),
Link(
"Event Report",
reverse("membershipworks:event-index-report"),
permission="membershipworks.view_event",
),
Link(
"Event Attendees",
reverse("membershipworks:event-attendees"),
permission="membershipworks.view_event",
),
Link(
"Missing Paperwork",
reverse("membershipworks:missing-paperwork-report"),
permission="membershipworks.view_member",
),
]
member = Member.from_user(self.request.user)
if (
member is not None
and EventExt.objects.filter(instructor__member=member).exists()
):
links.append(
Link(
"My Events", reverse("membershipworks:user-events"), permission=None
)
)
return links

38
membershipworks/forms.py Normal file
View File

@ -0,0 +1,38 @@
from django import forms
from django.contrib.auth.models import AbstractBaseUser
from membershipworks.models import EventExt
class EventInvoiceForm(forms.Form):
reviewed_invoice = forms.BooleanField(
label="I have reviewed this invoice and confirm that the course information, materials fee, course fees, enrollment information, invoice amount and all other details are correct.",
help_text='Please contact us at <a href="mailto:info@claremontmakerspace.org">info@claremontmakerspace.org</a> if corrections are required.',
required=True,
)
verified_payment_and_contact = forms.BooleanField(
label="I have verified my contact and payment information.",
help_text='You can <a href="https://claremontmakerspace.org/membersonly/">change your payment information here</a>. It may take up to an hour for your changes to take effect.',
required=True,
)
def __init__(self, *args, event: EventExt, user: AbstractBaseUser, **kwargs):
self.event = event
self.user = user
super().__init__(*args, **kwargs)
def clean(self):
if self.event.total_due_to_instructor is None:
raise forms.ValidationError(
"Event missing required information to generate invoice"
)
if not self.event.user_is_instructor(self.user):
raise forms.ValidationError(
"You are not the listed as the instructor for this event"
)
if hasattr(self.event, "eventinvoice"):
raise forms.ValidationError("Invoice already exists for this event")
return super().clean()

View File

@ -0,0 +1,65 @@
from django.conf import settings
from django.core.mail import EmailMessage, EmailMultiAlternatives
from django.template import loader
import mdformat
from markdownify import markdownify
from membershipworks.models import EventInvoice
def make_multipart_email(
subject: str, html_body: str, to: tuple[str]
) -> EmailMultiAlternatives:
plain_body = mdformat.text(markdownify(html_body), extensions={"tables"})
email = EmailMultiAlternatives(
subject,
plain_body,
from_email="CMS Invoices <invoices@claremontmakerspace.org>",
to=to,
reply_to=["Claremont MakerSpace <Info@ClaremontMakerSpace.org>"],
)
email.attach_alternative(html_body, "text/html")
return email
def make_instructor_email(
invoice: EventInvoice, pdf: bytes, event_url: str
) -> EmailMessage:
template = loader.get_template(
"membershipworks/email/event_invoice_instructor.dj.html"
)
html_body = template.render({"invoice": invoice, "event_url": event_url})
message = make_multipart_email(
f'Your CMS instructor invoice has been received for event "{invoice.event}" {invoice.event.start} - {invoice.event.end}',
html_body,
(invoice.event.instructor.member.sanitized_mailbox(),),
)
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
return message
def make_admin_email(invoice: EventInvoice, pdf: bytes, event_url: str) -> EmailMessage:
template = loader.get_template("membershipworks/email/event_invoice_admin.dj.html")
html_body = template.render({"invoice": invoice, "event_url": event_url})
message = make_multipart_email(
f'CMS instructor invoice created for event "{invoice.event}" {invoice.event.start} - {invoice.event.end}',
html_body,
# TODO: should this be in database instead?
settings.INVOICE_HANDLERS,
)
message.attach(f"CMS_event_invoice_{invoice.uuid}.pdf", pdf, "application/pdf")
return message
def make_invoice_emails(
invoice: EventInvoice, pdf: bytes, event_url: str
) -> list[EmailMessage]:
return [
make_instructor_email(invoice, pdf, event_url),
make_admin_email(invoice, pdf, event_url),
]

View File

@ -4,6 +4,7 @@ from typing import Optional
import django.core.mail.message
from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser
from django.db import models
from django.db.models import (
Case,
@ -20,6 +21,7 @@ from django.db.models import (
When,
)
from django.db.models.functions import Coalesce
from django.urls import reverse
from django.utils import timezone
import nh3
@ -550,6 +552,15 @@ class EventExt(Event):
)
details = models.JSONField(null=True, blank=True)
def get_absolute_url(self) -> str:
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})
def user_is_instructor(self, user: AbstractBaseUser) -> bool:
member = Member.from_user(user)
if member is not None:
return self.instructor.member == member
return False
class Meta:
verbose_name = "event"
ordering = ["-start"]

View File

@ -0,0 +1,82 @@
@page {
size: letter portrait;
margin: 1.2in 0.7in;
@top-center {
content: "Event Invoice";
font-size: 1.2em;
color: #444;
vertical-align: bottom;
}
@top-left {
vertical-align: bottom;
content: element(header);
}
@top-right {
content: "";
vertical-align: bottom;
background-position: bottom;
background-image: url("https://claremontmakerspace.org/wp-content/uploads/2018/06/cms_logo.png");
background-repeat: no-repeat;
background-size: 100%;
width: 7em;
}
@bottom-left {
content: element(footer-left);
}
@bottom-right {
content: element(footer-right);
}
}
footer {
font-size: 8pt;
}
footer .left {
position: running(footer-left);
margin-right: 1em;
}
footer .right {
position: running(footer-right);
text-align: right;
}
header {
position: running(header);
}
/* TODO: probably should just fix this server side */
:root {
--bs-font-sans-serif: roboto;
}
body {
margin-top: 1em;
}
p {
margin-bottom: 0.2em;
}
/* Bootstrap fixes for Weasyprint */
.table-group-divider {
border-top: 2px solid currentcolor;
}
.d-md-block {
display: block !important;
}
.col-md-7 {
flex: 0 0 auto;
width: 58.33333333%;
}
.col-md-4 {
flex: 0 0 auto;
width: 33.33333333%;
}

View File

@ -0,0 +1,6 @@
<p>
New invoice received from
<b>{{ invoice.event.instructor }}</b> on <b>{{ invoice.date_submitted }}</b>.
</p>
{% include "membershipworks/email/event_invoice_details_fragment.dj.html" %}

View File

@ -0,0 +1,19 @@
<p>
<div>
<b>Invoice #:</b> {{ invoice.uuid }}
</div>
<div>
<b>Event id:</b> {{ invoice.event.eid }}
</div>
<div>
<b>Event title:</b> {{ invoice.event }}
</div>
<div>
<b>Event duration:</b> {{ invoice.event.start }} - {{ invoice.event.end }}
</div>
<div>
<a href="{{ event_url }}">
<b>View this event and invoice in CMSManage</b>
</a>
</div>
</p>

View File

@ -0,0 +1,5 @@
<p>
Your invoice for <b>{{ invoice.event }}</b> has been received. A copy is attached for your records.
</p>
{% include "membershipworks/email/event_invoice_details_fragment.dj.html" %}

View File

@ -0,0 +1,46 @@
{% extends "base.dj.html" %}
{% load nh3_tags %}
{% load django_bootstrap5 %}
{% block title %}Event Invoice for {{ event.details.ttl|nh3 }}{% endblock %}
{% block admin_link %}
{% url 'admin:membershipworks_eventext_change' event.pk %}
{% endblock %}
{% block content %}
<div class="container">
{% include "membershipworks/event_invoice.dj.html" %}
<div class="card w-auto mt-5">
<div class="card-body">
{% if event.invoice %}
<div class="card-text text-center">
<p>
Invoice submitted on {{ event.invoice.date_submitted }}
for ${{ event.invoice.amount|floatformat:2 }},
{% if event.invoice.date_paid %}
paid on {{ event.invoice.date_paid }}
{% else %}
not paid yet
{% endif %}
</p>
<a class="btn btn-primary"
href="{% url 'membershipworks:event-invoice-pdf' event.invoice.pk %}">View PDF</a>
</div>
{% elif event.total_due_to_instructor is None %}
<p class="card-text text-center">
This event is missing required information to generate an invoice. Please contact us at <a href="mailto:info@claremontmakerspace.org">info@claremontmakerspace.org</a>.
</p>
{% elif user_is_instructor %}
<form method="post" class="card-text">
{% csrf_token %}
{% bootstrap_form form %}
<div class="text-center">{% bootstrap_button button_type="submit" content="Submit Invoice" %}</div>
</form>
{% else %}
<p>No invoice has been created for this event, and you are not listed as the the instructor.</p>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,52 +1,65 @@
{% extends "base.dj.html" %}
{% load nh3_tags %}
{% load render_table from django_tables2 %}
{% block title %}Event Invoice for {{ event.details.ttl|nh3 }}{% endblock %}
{% block admin_link %}
{% url 'admin:membershipworks_eventext_change' event.pk %}
{% endblock %}
{% block content %}
<p>
<b>Event Name:</b> {{ event.details.ttl|nh3 }}
</p>
<p>
<b>Instructor:</b> {{ event.instructor }}
</p>
<p>
{% with meeting_times=event.meeting_times.all %}
{% if meeting_times|length == 0 %}
<b>Dates of Event:</b> Not set
{% elif meeting_times|length == 1 %}
<b>Date of Event:</b> {{ meeting_times.0.start }} - {{ meeting_times.0.end }}
<div class="row">
<div class="col-12 col-md-7">
<p>
<b>Event Name:</b> {{ event.details.ttl|nh3 }}
</p>
<p>
<b>Event ID:</b> {{ event.eid }}
</p>
<p>
<b>Instructor:</b> {{ event.instructor }}
</p>
<p>
{% with meeting_times=event.meeting_times.all %}
{% if meeting_times|length == 0 %}
<b>Dates of Event:</b> Not set
{% elif meeting_times|length == 1 %}
<b>Date of Event:</b> {{ meeting_times.0.start }} {{ meeting_times.0.end }}
{% else %}
<b>Dates of Event:</b>
<ul>
{% for meeting_time in meeting_times %}<li>{{ meeting_time.start }} {{ meeting_time.end }}</li>{% endfor %}
</ul>
{% endif %}
{% endwith %}
</p>
<p>
<b>Attendees:</b> {{ event.details.cnt }}/{{ event.details.cap }}
</p>
<p>
<b>Materials Fee <span class="font-monospace fw-light">[m]</span>:</b>
{% if event.materials_fee_included_in_price is None %}
Unknown if included in price
{% elif event.materials_fee_included_in_price %}
{% if event.materials_fee is not None %}
${{ event.materials_fee|floatformat:2 }}
{% else %}
Not defined
{% endif %}
{% else %}
<b>Dates of Event:</b>
<ul>
{% for meeting_time in meeting_times %}<li>{{ meeting_time.start }} - {{ meeting_time.end }}</li>{% endfor %}
</ul>
Not collected by CMS
{% endif %}
{% endwith %}
</p>
<p>
<b>Attendees:</b> {{ event.details.cnt }}/{{ event.details.cap }}
</p>
<p>
<b>Materials Fee [m]:</b>
{% if event.materials_fee_included_in_price is None %}
Unknown if included in price
{% elif event.materials_fee_included_in_price %}
{% if event.materials_fee is not None %}
${{ event.materials_fee|floatformat:2 }}
{% else %}
Not defined
{% endif %}
{% else %}
Not collected by CMS
{% endif %}
</p>
<p>
<b>Instructor Percentage [I]:</b> {{ event.instructor_percentage }}
</p>
{% render_table table "membershipworks/tables/invoice_table.dj.html" %}
{% endblock %}
</p>
<p>
<b>Instructor Percentage <span class="font-monospace fw-light">[I]</span>:</b> {{ event.instructor_percentage }}
</p>
</div>
<div class="vr d-none d-md-block m-4 p-0"></div>
<div class="col-12 col-md-4">
<div>
<h3>Remit to:</h3>
<div>{{ event.instructor.member.account_name }}</div>
<div>{{ event.instructor.member.address_street }}</div>
<div>
{{ event.instructor.member.address_city }},
{{ event.instructor.member.address_state_province }}
{{ event.instructor.member.address_postal_code }}
</div>
<div>{{ event.instructor.member.email }}</div>
</div>
</div>
</div>
{% render_table table "membershipworks/tables/invoice_table.dj.html" %}

View File

@ -0,0 +1,29 @@
{% load static %}
{% load nh3_tags %}
<head>
<meta charset="utf-8">
<meta name="viewport"
content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link href="{% static 'bootstrap.min.css' %}" rel="stylesheet">
<link rel="stylesheet" href="{% static "membershipworks/css/event_invoice_pdf.css" %}" media="print">
<title>Event Invoice for {{ event.details.ttl|nh3 }}</title>
</head>
<body style="font-size: 10pt;">
<header>
<div>TwinState MakerSpaces, Inc.</div>
<div>PO Box 100</div>
<div>Lebanon, NH 03766-0100</div>
</header>
<footer>
<div class="left">Generated for {{ user }} on {{ now|date:"Y-m-d" }} {{ now|time:"H:i T" }} by CMSManage</div>
<div class="right">
Invoice ID: <span class="font-monospace text-nowrap">{{ invoice_uuid }}</span>
</div>
</footer>
{% include "membershipworks/event_invoice.dj.html" %}
</body>

View File

@ -0,0 +1,10 @@
{% extends "base.dj.html" %}
{% load render_table from django_tables2 %}
{% block title %}My Events{% endblock %}
{% block content %}
{% include "cmsmanage/components/download_table.dj.html" %}
{% render_table table %}
{% endblock %}

View File

@ -31,9 +31,19 @@ urlpatterns = [
name="event-month-report",
),
path(
"event-invoice/<eid>",
views.EventInvoiceView.as_view(),
name="event-invoice",
"my-events/",
views.UserEventView.as_view(),
name="user-events",
),
path(
"event/<eid>",
views.EventDetailView.as_view(),
name="event-detail",
),
path(
"event/invoice/<uuid:uuid>.pdf",
views.EventInvoicePDFView.as_view(),
name="event-invoice-pdf",
),
path(
"event-attendees",

View File

@ -1,13 +1,24 @@
import uuid
from datetime import datetime, timedelta
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 PermissionRequiredMixin
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.defaultfilters import floatformat
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.functional import cached_property
from django.utils.html import format_html
from django.utils.safestring import SafeString
from django.views.generic import DetailView, ListView
@ -16,18 +27,25 @@ from django.views.generic.dates import (
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 import GroupConcat
from django_sendfile import sendfile
from django_tables2 import A, SingleTableMixin
from django_tables2.export.views import ExportMixin
from django_weasyprint.utils import django_url_fetcher
from membershipworks.membershipworks_api import MembershipWorks
from .models import EventAttendee, EventExt, Member
from .forms import EventInvoiceForm
from .invoice_email import make_invoice_emails
from .models import EventAttendee, EventExt, EventInvoice, Member
class MemberAutocomplete(autocomplete.Select2QuerySetView):
@ -137,7 +155,7 @@ class EventTable(tables.Table):
template_code=(
'<a title="MembershipWorks" href="https://membershipworks.com/admin/#!event/admin/{{ record.url }}">{{ value }}</a> '
'<a title="Admin" href="{% url "admin:membershipworks_eventext_change" record.pk %}"><i class="bi bi-pencil-square"></i></a> '
'<a title="Invoice" href="{% url "membershipworks:event-invoice" record.pk %}"><i class="bi bi-receipt"></i></a> '
'<a title="Details" href="{% url "membershipworks:event-detail" record.pk %}"><i class="bi bi-receipt"></i></a> '
),
accessor="unescaped_title",
)
@ -149,6 +167,8 @@ class EventTable(tables.Table):
gross_revenue = tables.Column()
total_due_to_instructor = tables.Column()
net_revenue = tables.Column()
invoice__date_submitted = tables.DateColumn(verbose_name="Invoice Submitted")
invoice__date_paid = tables.DateColumn(verbose_name="Invoice Paid")
class Meta:
model = EventExt
@ -275,7 +295,7 @@ class EventMonthReport(
return (
super()
.get_table_data()
.select_related("category", "instructor")
.select_related("category", "instructor", "invoice")
.with_financials()
)
@ -283,6 +303,42 @@ class EventMonthReport(
return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}"
class UserEventTable(EventTable):
title = tables.Column(linkify=True, accessor="unescaped_title")
instructor = None
person_hours = None
gross_revenue = None
net_revenue = None
class UserEventView(SingleTableMixin, ListView):
model = 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 InvoiceMoneyColumn(tables.columns.Column):
def render(self, value):
return f"${super().render(value):.2f}"
@ -333,26 +389,142 @@ class InvoiceTable(tables.Table):
class Meta:
attrs = {
"class": "table table-sm mx-auto w-auto",
"tbody": {"class": "table-group-divider"},
"tfoot": {"class": "table-group-divider"},
}
orderable = False
class EventInvoiceView(SingleTableMixin, PermissionRequiredMixin, DetailView):
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_invoice.dj.html"
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):
return self.object.ticket_types.all()
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):
return {"event": self.object}
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,
# NOTE: this needs to be resolved before the object is
# saved, so cannot use the Now() db function
date_submitted=timezone.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 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 HttpResponse(invoice.pdf.path)
return sendfile(request, invoice.pdf.path, mimetype="application/pdf")
else:
return self.handle_no_permission()
class EventAttendeeTable(tables.Table):

View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "lint", "server", "typing", "dev"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:b6c73855da789a2433a99ac698bd3e7d16ad5a37170752043afd14ca9212f228"
content_hash = "sha256:8d03a3352b70059a88842e3f316ee61450b7690e39a4516b7ce8ebf22aaae5b2"
[[package]]
name = "aiohttp"
@ -659,6 +659,18 @@ files = [
{file = "django_recurrence-1.11.1-py3-none-any.whl", hash = "sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5"},
]
[[package]]
name = "django-sendfile2"
version = "0.7.1"
summary = "Abstraction to offload file uploads to web-server (e.g. Apache with mod_xsendfile) once Django has checked permissions etc."
dependencies = [
"django",
]
files = [
{file = "django-sendfile2-0.7.1.tar.gz", hash = "sha256:b5bec07f1c9b1875a60ea74beb306e9aba964bd8b54f00b4432cb77cc35bc58c"},
{file = "django_sendfile2-0.7.1-py3-none-any.whl", hash = "sha256:81971df1db77f688c4834b8540930902d0d929c64cf374bd604b81f5ac52c04a"},
]
[[package]]
name = "django-stubs"
version = "4.2.7"
@ -717,6 +729,20 @@ files = [
{file = "django_tables2-2.7.0-py2.py3-none-any.whl", hash = "sha256:99e06d966ca8ac69fd74092eb45c79a280dd5ca0ccb81395d96261f62128e1af"},
]
[[package]]
name = "django-weasyprint"
version = "2.3.0"
requires_python = ">=3.8"
summary = "Django WeasyPrint CBV"
dependencies = [
"Django>=3.2",
"WeasyPrint>=53",
]
files = [
{file = "django-weasyprint-2.3.0.tar.gz", hash = "sha256:2f849e15bfd6c1b2a58512097b9042eddf3533651d37d2e096cd6f7d8be6442b"},
{file = "django_weasyprint-2.3.0-py3-none-any.whl", hash = "sha256:807cb3b16332123d97c8bbe2ac9c70286103fe353235351803ffd33b67284735"},
]
[[package]]
name = "django-widget-tweaks"
version = "1.5.0"

View File

@ -35,6 +35,8 @@ dependencies = [
"django-filter~=24.2",
"django-db-views~=0.1",
"django-mysql~=4.12",
"django-weasyprint~=2.3",
"django-sendfile2~=0.7",
"django-bootstrap5~=23.4",
]
requires-python = ">=3.11"