Compare commits

..

No commits in common. "175c3b2c5a07b66a319f8bcc519c1aede5eeac48" and "10cfc151e1aeff0e49faca9606dee50708329378" have entirely different histories.

29 changed files with 515 additions and 1269 deletions

View File

@ -13,6 +13,8 @@ jobs:
image: mariadb:latest image: mariadb:latest
env: env:
MARIADB_ROOT_PASSWORD: whatever MARIADB_ROOT_PASSWORD: whatever
ports:
- 3306:3306
healthcheck: healthcheck:
test: ["CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized"] test: ["CMD", "healthcheck.sh", "--su-mysql", "--connect", "--innodb_initialized"]
steps: steps:
@ -30,4 +32,4 @@ jobs:
- name: Install python dependencies - name: Install python dependencies
run: pdm sync -d run: pdm sync -d
- name: Run tests - name: Run tests
run: pdm run -v ./manage.py test run: pdm run -v ./manage.py test --parallel auto

1
.gitignore vendored
View File

@ -2,4 +2,3 @@ __pycache__/
*.sqlite3 *.sqlite3
/__pypackages__/ /__pypackages__/
/markdownx/ /markdownx/
/media/

View File

@ -42,8 +42,6 @@ INSTALLED_APPS = [
"django_filters", "django_filters",
"django_db_views", "django_db_views",
"django_mysql", "django_mysql",
"django_sendfile",
"django_bootstrap5",
"tasks.apps.TasksConfig", "tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig", "rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig", "membershipworks.apps.MembershipworksConfig",
@ -96,7 +94,6 @@ TIME_ZONE = "America/New_York"
USE_I18N = False USE_I18N = False
USE_L10N = True USE_L10N = True
USE_TZ = True USE_TZ = True
USE_DEPRECATED_PYTZ = False
# Static files (CSS, JavaScript, Images) # Static files (CSS, JavaScript, Images)
@ -121,10 +118,6 @@ LOGGING = {
}, },
} }
MEDIA_ROOT = "media"
MEDIA_URL = "media/"
SENDFILE_ROOT = str(Path(__file__).parents[2] / "media" / "protected")
WIKI_URL = "https://wiki.claremontmakerspace.org" WIKI_URL = "https://wiki.claremontmakerspace.org"
# Django Rest Framework # Django Rest Framework

View File

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

View File

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

View File

@ -15,7 +15,6 @@ Including another URLconf
""" """
from django.conf import settings from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin from django.contrib import admin
from django.contrib.auth.views import LoginView, LogoutView from django.contrib.auth.views import LoginView, LogoutView
from django.urls import include, path from django.urls import include, path
@ -64,4 +63,3 @@ urlpatterns = [
if settings.DEBUG: if settings.DEBUG:
urlpatterns.append(path("__debug__/", include("debug_toolbar.urls"))) urlpatterns.append(path("__debug__/", include("debug_toolbar.urls")))
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View File

@ -1,63 +0,0 @@
import calendar
import django_tables2 as tables
from .models import HIDEvent
class UnitTimeTable(tables.Table):
members = tables.columns.Column()
members_delta = tables.columns.TemplateColumn(
"{{ value|floatformat:2}}%", verbose_name="Δ Members"
)
access_count = tables.columns.Column()
access_count_delta = tables.columns.TemplateColumn(
"{{ value|floatformat:2}}%", verbose_name="Δ Access Count"
)
class Meta:
fields = ("members", "members_delta", "access_count", "access_count_delta")
class DeniedAccessTable(tables.Table):
name = tables.TemplateColumn(
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
)
class Meta:
model = HIDEvent
fields = (
"timestamp",
"door",
"event_type",
"name",
"raw_card_number",
"decoded_card_number",
)
class MostActiveMembersTable(tables.Table):
name = tables.Column()
access_count = tables.Column()
class DetailByDayTable(tables.Table):
timestamp__date = tables.DateColumn(verbose_name="Date")
name = tables.Column()
access_count = tables.Column()
class BusiestDayOfWeekTable(tables.Table):
timestamp__week_day = tables.Column("Week Day")
events = tables.Column()
members = tables.Column()
def render_timestamp__week_day(self, value):
return calendar.day_name[(value - 2) % 7]
class BusiestTimeOfDayTable(tables.Table):
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
events = tables.Column()
members = tables.Column()

View File

@ -1,3 +1,4 @@
import calendar
import datetime import datetime
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
@ -18,14 +19,6 @@ from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
from .models import Door, HIDEvent from .models import Door, HIDEvent
from .tables import (
BusiestDayOfWeekTable,
BusiestTimeOfDayTable,
DeniedAccessTable,
DetailByDayTable,
MostActiveMembersTable,
UnitTimeTable,
)
REPORTS = [] REPORTS = []
@ -103,6 +96,20 @@ class BaseAccessReport(
return context return context
class UnitTimeTable(tables.Table):
members = tables.columns.Column()
members_delta = tables.columns.TemplateColumn(
"{{ value|floatformat:2}}%", verbose_name="Δ Members"
)
access_count = tables.columns.Column()
access_count_delta = tables.columns.TemplateColumn(
"{{ value|floatformat:2}}%", verbose_name="Δ Access Count"
)
class Meta:
fields = ("members", "members_delta", "access_count", "access_count_delta")
@register_report @register_report
class AccessPerUnitTime(BaseAccessReport): class AccessPerUnitTime(BaseAccessReport):
table_class = UnitTimeTable table_class = UnitTimeTable
@ -202,6 +209,24 @@ class AccessPerUnitTime(BaseAccessReport):
) )
class DeniedAccessTable(tables.Table):
name = tables.TemplateColumn(
"{{ record.forename|default:'' }} {{ record.surname|default:'' }}"
)
class Meta:
model = HIDEvent
fields = (
"timestamp",
"door",
"event_type",
"name",
"raw_card_number",
"decoded_card_number",
)
@register_report @register_report
class DeniedAccess(BaseAccessReport): class DeniedAccess(BaseAccessReport):
_report_name = "Denied Access" _report_name = "Denied Access"
@ -219,6 +244,11 @@ class DeniedAccess(BaseAccessReport):
) )
class MostActiveMembersTable(tables.Table):
name = tables.Column()
access_count = tables.Column()
@register_report @register_report
class MostActiveMembers(BaseAccessReport): class MostActiveMembers(BaseAccessReport):
_report_name = "Most Active Members" _report_name = "Most Active Members"
@ -241,6 +271,12 @@ class MostActiveMembers(BaseAccessReport):
) )
class DetailByDayTable(tables.Table):
timestamp__date = tables.DateColumn(verbose_name="Date")
name = tables.Column()
access_count = tables.Column()
@register_report @register_report
class DetailByDay(BaseAccessReport): class DetailByDay(BaseAccessReport):
_report_name = "Detail by Day" _report_name = "Detail by Day"
@ -263,6 +299,15 @@ class DetailByDay(BaseAccessReport):
) )
class BusiestDayOfWeekTable(tables.Table):
timestamp__week_day = tables.Column("Week Day")
events = tables.Column()
members = tables.Column()
def render_timestamp__week_day(self, value):
return calendar.day_name[(value - 2) % 7]
@register_report @register_report
class BusiestDayOfWeek(BaseAccessReport): class BusiestDayOfWeek(BaseAccessReport):
_report_name = "Busiest Day of the Week" _report_name = "Busiest Day of the Week"
@ -281,6 +326,12 @@ class BusiestDayOfWeek(BaseAccessReport):
) )
class BusiestTimeOfDayTable(tables.Table):
timestamp__hour = tables.TemplateColumn("{{ value }}:00", verbose_name="Hour")
events = tables.Column()
members = tables.Column()
@register_report @register_report
class BusiestTimeOfDay(BaseAccessReport): class BusiestTimeOfDay(BaseAccessReport):
_report_name = "Busiest Time of Day" _report_name = "Busiest Time of Day"

View File

@ -14,7 +14,6 @@ from .models import (
Event, Event,
EventExt, EventExt,
EventInstructor, EventInstructor,
EventInvoice,
EventMeetingTime, EventMeetingTime,
Flag, Flag,
Member, Member,
@ -120,15 +119,11 @@ class EventInstructorAdmin(admin.ModelAdmin):
search_fields = ["name", "member__account_name"] search_fields = ["name", "member__account_name"]
class EventInvoiceInline(admin.StackedInline):
model = EventInvoice
@admin.register(EventExt) @admin.register(EventExt)
class EventAdmin(DjangoObjectActions, admin.ModelAdmin): class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
inlines = [EventInvoiceInline, EventMeetingTimeInline] inlines = [EventMeetingTimeInline]
list_display = [ list_display = [
"unescaped_title", "title",
"start", "start",
"duration", "duration",
"count", "count",
@ -163,10 +158,6 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
fields.append("_details_timestamp") fields.append("_details_timestamp")
return fields return fields
@admin.display(ordering="title")
def unescaped_title(self, obj):
return obj.unescaped_title
@admin.display(ordering="duration") @admin.display(ordering="duration")
def duration(self, obj): def duration(self, obj):
return obj.duration return obj.duration

View File

@ -2,48 +2,32 @@ from django.urls import reverse
import dashboard import dashboard
from dashboard import Link from dashboard import Link
from membershipworks.models import EventExt, Member
@dashboard.register @dashboard.register
class MembershipworksDashboardFragment(dashboard.LinksCardDashboardFragment): class MembershipworksDashboardFragment(dashboard.LinksCardDashboardFragment):
name = "MembershipWorks" name = "MembershipWorks"
@property links = [
def links(self): Link(
links = [ "Upcoming Events",
Link( reverse("membershipworks:upcoming-events"),
"Upcoming Events", permission="membershipworks.view_event",
reverse("membershipworks:upcoming-events"), tooltip="Generator for Wordpress posts",
permission="membershipworks.view_event", ),
tooltip="Generator for Wordpress posts", Link(
), "Event Report",
Link( reverse("membershipworks:event-index-report"),
"Event Report", permission="membershipworks.view_event",
reverse("membershipworks:event-index-report"), ),
permission="membershipworks.view_event", Link(
), "Event Attendees",
Link( reverse("membershipworks:event-attendees"),
"Event Attendees", permission="membershipworks.view_event",
reverse("membershipworks:event-attendees"), ),
permission="membershipworks.view_event", Link(
), "Missing Paperwork",
Link( reverse("membershipworks:missing-paperwork-report"),
"Missing Paperwork", permission="membershipworks.view_member",
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

View File

@ -1,38 +0,0 @@
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

@ -1,65 +0,0 @@
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

@ -1,41 +0,0 @@
# Generated by Django 5.0.2 on 2024-03-08 21:30
import uuid
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0015_eventmeetingtime_end_after_start"),
]
operations = [
migrations.CreateModel(
name="EventInvoice",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
("date_submitted", models.DateField()),
("date_paid", models.DateField(blank=True, null=True)),
("pdf", models.FileField(upload_to="invoices/%Y/%m/%d/")),
("amount", models.DecimalField(decimal_places=4, max_digits=13)),
(
"event",
models.OneToOneField(
on_delete=django.db.models.deletion.PROTECT,
related_name="invoice",
to="membershipworks.eventext",
),
),
],
),
]

View File

@ -1,10 +1,8 @@
import uuid
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
import django.core.mail.message import django.core.mail.message
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import AbstractBaseUser
from django.db import models from django.db import models
from django.db.models import ( from django.db.models import (
Case, Case,
@ -17,14 +15,11 @@ from django.db.models import (
Q, Q,
Subquery, Subquery,
Sum, Sum,
Value,
When, When,
) )
from django.db.models.functions import Coalesce from django.db.models.functions import Coalesce
from django.urls import reverse
from django.utils import timezone from django.utils import timezone
import nh3
from django_db_views.db_view import DBView from django_db_views.db_view import DBView
@ -429,12 +424,8 @@ class Event(BaseModel):
_allowed_missing_fields = ["cap", "edp", "adn"] _allowed_missing_fields = ["cap", "edp", "adn"]
@property
def unescaped_title(self):
return nh3.clean(self.title, tags=set())
def __str__(self): def __str__(self):
return self.unescaped_title return self.title
class EventInstructor(models.Model): class EventInstructor(models.Model):
@ -480,7 +471,7 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
"amount", "amount",
"materials", "materials",
"amount_without_materials", "amount_without_materials",
"instructor_revenue", "instructor_fee",
"instructor_amount", "instructor_amount",
] ]
}, },
@ -552,15 +543,6 @@ class EventExt(Event):
) )
details = models.JSONField(null=True, blank=True) 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: class Meta:
verbose_name = "event" verbose_name = "event"
ordering = ["-start"] ordering = ["-start"]
@ -588,42 +570,6 @@ class EventMeetingTime(models.Model):
] ]
class EventInvoice(models.Model):
uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
event = models.OneToOneField(
EventExt, on_delete=models.PROTECT, related_name="invoice"
)
date_submitted = models.DateField()
date_paid = models.DateField(blank=True, null=True)
pdf = models.FileField(upload_to="protected/invoices/%Y/%m/%d/")
amount = models.DecimalField(max_digits=13, decimal_places=4)
def __str__(self) -> str:
return f'"{self.event}" submitted={self.date_submitted} paid:{self.date_paid} ${self.amount}'
class EventTicketTypeQuerySet(models.QuerySet["EventTicketType"]):
def group_by_ticket_type(self):
return self.values("is_members_ticket").annotate(
label=Case(
When(Q(is_members_ticket=True), Value("Members")),
default=Value("Non-Members"),
),
actual_price=F("actual_price"),
**{
field: Sum(field)
for field in [
"quantity",
"materials",
"amount",
"amount_without_materials",
"instructor_revenue",
"instructor_amount",
]
},
)
class EventTicketTypeManager(models.Manager["EventTicketType"]): class EventTicketTypeManager(models.Manager["EventTicketType"]):
def get_queryset(self) -> models.QuerySet["EventTicketType"]: def get_queryset(self) -> models.QuerySet["EventTicketType"]:
members_folder = Subquery( members_folder = Subquery(
@ -669,19 +615,18 @@ class EventTicketTypeManager(models.Manager["EventTicketType"]):
amount_without_materials=ExpressionWrapper( amount_without_materials=ExpressionWrapper(
F("amount") - F("materials"), output_field=models.DecimalField() F("amount") - F("materials"), output_field=models.DecimalField()
), ),
instructor_revenue=ExpressionWrapper( instructor_fee=ExpressionWrapper(
F("amount_without_materials") * F("event__instructor_percentage"), F("amount_without_materials") * F("event__instructor_percentage"),
output_field=models.DecimalField(), output_field=models.DecimalField(),
), ),
instructor_amount=ExpressionWrapper( instructor_amount=ExpressionWrapper(
F("instructor_revenue") + F("materials"), F("instructor_fee") + F("materials"), output_field=models.DecimalField()
output_field=models.DecimalField(),
), ),
) )
class EventTicketType(DBView): class EventTicketType(DBView):
objects = EventTicketTypeManager.from_queryset(EventTicketTypeQuerySet)() objects = EventTicketTypeManager()
event = models.ForeignKey( event = models.ForeignKey(
EventExt, on_delete=models.CASCADE, related_name="ticket_types" EventExt, on_delete=models.CASCADE, related_name="ticket_types"

View File

@ -1,82 +0,0 @@
@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

@ -1,168 +0,0 @@
from datetime import timedelta
from django.template.defaultfilters import floatformat
from django.utils.html import format_html
from django.utils.safestring import SafeString
import django_tables2 as tables
from .models import EventAttendee, EventExt, Member
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=(
'<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="Details" href="{% url "membershipworks:event-detail" record.pk %}"><i class="bi bi-receipt"></i></a> '
),
accessor="unescaped_title",
)
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()
invoice__date_submitted = tables.DateColumn(verbose_name="Invoice Submitted")
invoice__date_paid = tables.DateColumn(verbose_name="Invoice Paid")
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 UserEventTable(EventTable):
title = tables.Column(linkify=True, accessor="unescaped_title")
instructor = None
person_hours = None
gross_revenue = None
net_revenue = None
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(
'{} <div class="text-nowrap font-monospace fw-light">[{}]</div>',
name,
formula,
)
label = tables.Column("Ticket Type", footer="Subtotals")
list_price = InvoiceMoneyColumn("Ticket Price")
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")
)
amount_without_materials = InvoiceMoneyFooterColumn(
_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")
)
class Meta:
attrs = {
"class": "table table-sm mx-auto w-auto",
"tbody": {"class": "table-group-divider"},
"tfoot": {"class": "table-group-divider"},
}
orderable = False
class EventAttendeeTable(tables.Table):
class Meta:
model = EventAttendee
fields = ("name", "email")
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",
]

View File

@ -1,6 +0,0 @@
<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

@ -1,19 +0,0 @@
<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

@ -1,5 +0,0 @@
<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

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

View File

@ -1,29 +0,0 @@
{% 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

@ -1,10 +0,0 @@
{% 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,19 +31,9 @@ urlpatterns = [
name="event-month-report", name="event-month-report",
), ),
path( path(
"my-events/", "event-invoice/<eid>",
views.UserEventView.as_view(), views.EventInvoiceView.as_view(),
name="user-events", name="event-invoice",
),
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( path(
"event-attendees", "event-attendees",

View File

@ -1,56 +1,31 @@
import uuid from datetime import datetime, timedelta
from datetime import datetime
from typing import Any
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import permission_required from django.contrib.auth.decorators import permission_required
from django.contrib.auth.mixins import ( from django.contrib.auth.mixins import PermissionRequiredMixin
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 import OuterRef, Q, Subquery
from django.db.models.functions import TruncMonth, TruncYear from django.db.models.functions import TruncMonth, TruncYear
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render from django.shortcuts import render
from django.template.loader import render_to_string from django.template.defaultfilters import floatformat
from django.utils import timezone
from django.utils.functional import cached_property
from django.views.generic import DetailView, ListView from django.views.generic import DetailView, ListView
from django.views.generic.dates import ( from django.views.generic.dates import (
ArchiveIndexView, ArchiveIndexView,
MonthArchiveView, MonthArchiveView,
YearArchiveView, YearArchiveView,
) )
from django.views.generic.detail import BaseDetailView
from django.views.generic.edit import FormMixin, ProcessFormView
import django_filters import django_filters
import django_tables2 as tables import django_tables2 as tables
import weasyprint
from dal import autocomplete from dal import autocomplete
from django_filters.views import BaseFilterView from django_filters.views import BaseFilterView
from django_mysql.models import GroupConcat from django_mysql.models import GroupConcat
from django_sendfile import sendfile
from django_tables2 import A, SingleTableMixin from django_tables2 import A, SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
from django_weasyprint.utils import django_url_fetcher
from membershipworks.membershipworks_api import MembershipWorks from membershipworks.membershipworks_api import MembershipWorks
from .forms import EventInvoiceForm from .models import EventAttendee, EventExt, Member
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): class MemberAutocomplete(autocomplete.Select2QuerySetView):
@ -143,6 +118,72 @@ def upcoming_events(request):
return render(request, "membershipworks/upcoming_events.dj.html", context) 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=(
'<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> '
),
)
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( class EventIndexReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, ArchiveIndexView ExportMixin, SingleTableMixin, PermissionRequiredMixin, ArchiveIndexView
): ):
@ -231,7 +272,7 @@ class EventMonthReport(
return ( return (
super() super()
.get_table_data() .get_table_data()
.select_related("category", "instructor", "invoice") .select_related("category", "instructor")
.with_financials() .with_financials()
) )
@ -239,163 +280,65 @@ class EventMonthReport(
return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}" return f"mw_events_{self.get_year()}-{self.get_month():02}.{export_format}"
class UserEventView(SingleTableMixin, ListView): class InvoiceMoneyColumn(tables.columns.Column):
model = EventExt def render(self, value):
table_class = UserEventTable return f"${super().render(value):.2f}"
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): class InvoiceMoneyFooterColumn(InvoiceMoneyColumn):
if self.member is None: def render_footer(self, bound_column, table):
return self.model.objects.none() value = getattr(table.event, bound_column.accessor)
if value is not None:
return f"${value:.2f}"
else: else:
return super().get_queryset().filter(instructor__member=self.member) return bound_column.default
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( class InvoiceTable(tables.Table):
SingleTableMixin, FormMixin, AccessMixin, DetailView, ProcessFormView def __init__(self, *args, **kwargs):
): self.event = kwargs.pop("event")
super().__init__(*args, **kwargs)
label = tables.Column("Ticket Type", footer="Subtotals")
list_price = InvoiceMoneyColumn("Ticket Price")
actual_price = InvoiceMoneyColumn("Actual Price [P]")
quantity = tables.Column("Quantity [Q]", footer=lambda table: table.event.quantity)
amount = InvoiceMoneyFooterColumn("Amount [A = P * Q]")
materials = InvoiceMoneyFooterColumn("CMS Collected Materials Fee [M = m * Q]")
amount_without_materials = InvoiceMoneyFooterColumn(
"Event Revenue Base [R = A - M]"
)
instructor_fee = InvoiceMoneyFooterColumn("Instructor Fee [F = R * I]")
instructor_amount = InvoiceMoneyFooterColumn("Amount Due to Instructor [F + M]")
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" permission_required = "membershipworks.view_eventext"
queryset = EventExt.objects.with_financials().all() queryset = EventExt.objects.with_financials().all()
pk_url_kwarg = "eid" pk_url_kwarg = "eid"
context_object_name = "event" context_object_name = "event"
template_name = "membershipworks/event_detail.dj.html" template_name = "membershipworks/event_invoice.dj.html"
table_pagination = False table_pagination = False
table_class = InvoiceTable 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): def get_table_data(self):
if self.display_instructor_version(): return self.object.ticket_types.all()
return self.object.ticket_types.group_by_ticket_type()
else:
return self.object.ticket_types.all()
def get_table_kwargs(self): def get_table_kwargs(self):
kwargs = super().get_table_kwargs() return {"event": self.object}
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): class EventAttendeeTable(tables.Table):
model = EventInvoice class Meta:
pk_url_kwarg = "uuid" model = EventAttendee
fields = ("name", "email")
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 EventAttendeeFilters(django_filters.FilterSet): class EventAttendeeFilters(django_filters.FilterSet):
@ -425,6 +368,25 @@ class EventAttendeeListView(
return super().get_table_data().values("name", "email").distinct() return super().get_table_data().values("name", "email").distinct()
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( class MissingPaperworkReport(
ExportMixin, ExportMixin,
SingleTableMixin, SingleTableMixin,

View File

@ -1,92 +0,0 @@
import django_tables2 as tables
from .models import (
InstructorOrVendor,
Waiver,
)
class WarnEmptyColumn(tables.Column):
attrs = {
"td": {
"class": lambda value, bound_column: (
"table-danger" if value == bound_column.default else ""
)
}
}
class WaiverReportTable(tables.Table):
emergency_contact_name = WarnEmptyColumn()
emergency_contact_number = WarnEmptyColumn()
class Meta:
model = Waiver
fields = [
"name",
"date",
"emergency_contact_name",
"emergency_contact_number",
"waiver_version",
"guardian_name",
"guardian_relation",
"guardian_date",
]
class InstructorOrVendorTable(tables.Table):
instructor_agreement_date = WarnEmptyColumn(
"Instructor Agreement Date(s)", default="Missing"
)
w9_date = WarnEmptyColumn(default="Missing")
class Meta:
model = InstructorOrVendor
fields = [
"name",
"instructor_agreement_date",
"w9_date",
"phone",
"email_address",
]
class ShopAccessErrorColumn(tables.Column):
def td_class(value):
if value.startswith("Has access but"):
return "table-danger"
elif value.startswith("Has cert but"):
return "table-warning"
else:
return ""
attrs = {"td": {"class": td_class}}
class AccessVerificationTable(tables.Table):
account_name = tables.Column()
access_card = tables.Column()
billing_method = tables.Column()
join_date = tables.DateColumn()
renewal_date = tables.DateColumn()
access_front_door = tables.BooleanColumn(verbose_name="Front Door")
access_studio_space = tables.BooleanColumn(verbose_name="Studio Space")
wood_shop_error = ShopAccessErrorColumn()
metal_shop_error = ShopAccessErrorColumn()
extended_hours_error = ShopAccessErrorColumn()
extended_hours_shops_error = ShopAccessErrorColumn()
storage_closet_error = ShopAccessErrorColumn()
class CertifiersTable(tables.Table):
certified_by = tables.Column()
certification_version__definition__name = tables.Column("Certification")
certification_version__definition__department__name = tables.Column("Department")
number_issued_on_this_tool = tables.Column()
last_issued_certification_date = tables.Column()
class CertificationCountTable(tables.Table):
certification_version__definition__name = tables.Column("Certification")
certification_version__definition__department__name = tables.Column("Department")
total_issued = tables.Column()

View File

@ -19,6 +19,7 @@ from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotFou
from django.shortcuts import get_object_or_404, render from django.shortcuts import get_object_or_404, render
from django.views.generic import ListView from django.views.generic import ListView
import django_tables2 as tables
import requests import requests
import weasyprint import weasyprint
from django_mysql.models import GroupConcat from django_mysql.models import GroupConcat
@ -34,13 +35,6 @@ from .models import (
InstructorOrVendor, InstructorOrVendor,
Waiver, Waiver,
) )
from .tables import (
AccessVerificationTable,
CertificationCountTable,
CertifiersTable,
InstructorOrVendorTable,
WaiverReportTable,
)
WIKI_URL = settings.WIKI_URL WIKI_URL = settings.WIKI_URL
@ -126,6 +120,34 @@ def certification_pdf(request, cert_name):
) )
class WarnEmptyColumn(tables.Column):
attrs = {
"td": {
"class": lambda value, bound_column: "table-danger"
if value == bound_column.default
else ""
}
}
class WaiverReportTable(tables.Table):
emergency_contact_name = WarnEmptyColumn()
emergency_contact_number = WarnEmptyColumn()
class Meta:
model = Waiver
fields = [
"name",
"date",
"emergency_contact_name",
"emergency_contact_number",
"waiver_version",
"guardian_name",
"guardian_relation",
"guardian_date",
]
class WaiverReport(ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView): class WaiverReport(ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView):
permission_required = "paperwork.view_waiver" permission_required = "paperwork.view_waiver"
template_name = "paperwork/waiver_report.dj.html" template_name = "paperwork/waiver_report.dj.html"
@ -134,6 +156,23 @@ class WaiverReport(ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListV
table_pagination = False table_pagination = False
class InstructorOrVendorTable(tables.Table):
instructor_agreement_date = WarnEmptyColumn(
"Instructor Agreement Date(s)", default="Missing"
)
w9_date = WarnEmptyColumn(default="Missing")
class Meta:
model = InstructorOrVendor
fields = [
"name",
"instructor_agreement_date",
"w9_date",
"phone",
"email_address",
]
class InstructorOrVendorReport( class InstructorOrVendorReport(
ExportMixin, ExportMixin,
SingleTableMixin, SingleTableMixin,
@ -164,6 +203,33 @@ class InstructorOrVendorReport(
) )
class ShopAccessErrorColumn(tables.Column):
def td_class(value):
if value.startswith("Has access but"):
return "table-danger"
elif value.startswith("Has cert but"):
return "table-warning"
else:
return ""
attrs = {"td": {"class": td_class}}
class AccessVerificationTable(tables.Table):
account_name = tables.Column()
access_card = tables.Column()
billing_method = tables.Column()
join_date = tables.DateColumn()
renewal_date = tables.DateColumn()
access_front_door = tables.BooleanColumn(verbose_name="Front Door")
access_studio_space = tables.BooleanColumn(verbose_name="Studio Space")
wood_shop_error = ShopAccessErrorColumn()
metal_shop_error = ShopAccessErrorColumn()
extended_hours_error = ShopAccessErrorColumn()
extended_hours_shops_error = ShopAccessErrorColumn()
storage_closet_error = ShopAccessErrorColumn()
class AccessVerificationReport( class AccessVerificationReport(
ExportMixin, ExportMixin,
SingleTableMixin, SingleTableMixin,
@ -251,6 +317,14 @@ class AccessVerificationReport(
return qs return qs
class CertifiersTable(tables.Table):
certified_by = tables.Column()
certification_version__definition__name = tables.Column("Certification")
certification_version__definition__department__name = tables.Column("Department")
number_issued_on_this_tool = tables.Column()
last_issued_certification_date = tables.Column()
class CertifiersReport( class CertifiersReport(
ExportMixin, ExportMixin,
SingleTableMixin, SingleTableMixin,
@ -282,6 +356,12 @@ class CertifiersReport(
) )
class CertificationCountTable(tables.Table):
certification_version__definition__name = tables.Column("Certification")
certification_version__definition__department__name = tables.Column("Department")
total_issued = tables.Column()
class CertificationCountReport( class CertificationCountReport(
ExportMixin, ExportMixin,
SingleTableMixin, SingleTableMixin,

342
pdm.lock
View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "lint", "server", "typing", "dev"] groups = ["default", "debug", "lint", "server", "typing", "dev"]
strategy = ["cross_platform"] strategy = ["cross_platform"]
lock_version = "4.4.1" lock_version = "4.4.1"
content_hash = "sha256:d2a8240d2754416c9c2c277832f3b8a2401eae3b56bda6e412aaf285c1c3b955" content_hash = "sha256:0232aa2ec5a45feca37f676076b9caa5f56cf12f411c5c151eb7ef781e508dca"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@ -451,7 +451,7 @@ files = [
[[package]] [[package]]
name = "django" name = "django"
version = "5.0.4" version = "5.0.2"
requires_python = ">=3.10" requires_python = ">=3.10"
summary = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." summary = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
dependencies = [ dependencies = [
@ -460,26 +460,26 @@ dependencies = [
"tzdata; sys_platform == \"win32\"", "tzdata; sys_platform == \"win32\"",
] ]
files = [ files = [
{file = "Django-5.0.4-py3-none-any.whl", hash = "sha256:916423499d75d62da7aa038d19aef23d23498d8df229775eb0a6309ee1013775"}, {file = "Django-5.0.2-py3-none-any.whl", hash = "sha256:56ab63a105e8bb06ee67381d7b65fe6774f057e41a8bab06c8020c8882d8ecd4"},
{file = "Django-5.0.4.tar.gz", hash = "sha256:4bd01a8c830bb77a8a3b0e7d8b25b887e536ad17a81ba2dce5476135c73312bd"}, {file = "Django-5.0.2.tar.gz", hash = "sha256:b5bb1d11b2518a5f91372a282f24662f58f66749666b0a286ab057029f728080"},
] ]
[[package]] [[package]]
name = "django-admin-logs" name = "django-admin-logs"
version = "1.2.0" version = "1.1.0"
requires_python = ">=3.6" requires_python = ">=3.6"
summary = "View, delete or disable Django admin log entries." summary = "View, delete or disable Django admin log entries."
dependencies = [ dependencies = [
"Django>=3.2", "Django>=3.2",
] ]
files = [ files = [
{file = "django-admin-logs-1.2.0.tar.gz", hash = "sha256:4bb69c6e2bfaa7bd47ecf5c13674623e2be3b39c1550f39b2500450e6b2bdc62"}, {file = "django-admin-logs-1.1.0.tar.gz", hash = "sha256:bb87cd944cfa14b6d90c93584fbcdc3ffde9410fd999c65a0b524b94518a5c64"},
{file = "django_admin_logs-1.2.0-py3-none-any.whl", hash = "sha256:251614a2aa15d5bdd57fe90f9f263d38e6123ea77f52af07393eecdae684e05a"}, {file = "django_admin_logs-1.1.0-py3-none-any.whl", hash = "sha256:bb139a99a08a4b08a98731efe9112a6ba269ab0af5efcdba435d1c79706fde16"},
] ]
[[package]] [[package]]
name = "django-auth-ldap" name = "django-auth-ldap"
version = "4.8.0" version = "4.6.0"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Django LDAP authentication backend" summary = "Django LDAP authentication backend"
dependencies = [ dependencies = [
@ -487,8 +487,8 @@ dependencies = [
"python-ldap>=3.1", "python-ldap>=3.1",
] ]
files = [ files = [
{file = "django-auth-ldap-4.8.0.tar.gz", hash = "sha256:604250938ddc9fda619f247c7a59b0b2f06e53a7d3f46a156f28aa30dd71a738"}, {file = "django-auth-ldap-4.6.0.tar.gz", hash = "sha256:9ae2bf87f9b6367b6cfd94a0451896cbc728e5400ed81cbfbd58ce743c0909a2"},
{file = "django_auth_ldap-4.8.0-py3-none-any.whl", hash = "sha256:4b4b944f3c28bce362f33fb6e8db68429ed8fd8f12f0c0c4b1a4344a7ef225ce"}, {file = "django_auth_ldap-4.6.0-py3-none-any.whl", hash = "sha256:4e82ded9292dc6ac7a75784d81b95174b72ca5e650a76e11317e3b68008e56d8"},
] ]
[[package]] [[package]]
@ -502,19 +502,6 @@ files = [
{file = "django-autocomplete-light-3.11.0.tar.gz", hash = "sha256:212576a17e3308ef7ca77e280b86684167916d2091d4b73640f38845d9516328"}, {file = "django-autocomplete-light-3.11.0.tar.gz", hash = "sha256:212576a17e3308ef7ca77e280b86684167916d2091d4b73640f38845d9516328"},
] ]
[[package]]
name = "django-bootstrap5"
version = "24.1"
requires_python = ">=3.8"
summary = "Bootstrap 5 for Django"
dependencies = [
"Django>=3.2",
]
files = [
{file = "django-bootstrap5-24.1.tar.gz", hash = "sha256:fc272b5bb218690fe6f32d52b23c155ebb46fbc5a2856c84eb353c1bf5fc49ea"},
{file = "django_bootstrap5-24.1-py3-none-any.whl", hash = "sha256:c42b4f6e673d35af847486733da77104e1e98e7fd5caecc6d56f32a96cc77479"},
]
[[package]] [[package]]
name = "django-db-views" name = "django-db-views"
version = "0.1.6" version = "0.1.6"
@ -557,15 +544,15 @@ files = [
[[package]] [[package]]
name = "django-filter" name = "django-filter"
version = "24.2" version = "23.5"
requires_python = ">=3.8" requires_python = ">=3.7"
summary = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically." summary = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
dependencies = [ dependencies = [
"Django>=4.2", "Django>=3.2",
] ]
files = [ files = [
{file = "django-filter-24.2.tar.gz", hash = "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e"}, {file = "django-filter-23.5.tar.gz", hash = "sha256:67583aa43b91fe8c49f74a832d95f4d8442be628fd4c6d65e9f811f5153a4e5c"},
{file = "django_filter-24.2-py3-none-any.whl", hash = "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48"}, {file = "django_filter-23.5-py3-none-any.whl", hash = "sha256:99122a201d83860aef4fe77758b69dda913e874cc5e0eaa50a86b0b18d708400"},
] ]
[[package]] [[package]]
@ -633,7 +620,7 @@ files = [
[[package]] [[package]]
name = "django-q2" name = "django-q2"
version = "1.6.2" version = "1.6.1"
requires_python = ">=3.8,<4" requires_python = ">=3.8,<4"
summary = "A multiprocessing distributed task queue for Django" summary = "A multiprocessing distributed task queue for Django"
dependencies = [ dependencies = [
@ -641,8 +628,8 @@ dependencies = [
"django<6,>=3.2", "django<6,>=3.2",
] ]
files = [ files = [
{file = "django_q2-1.6.2-py3-none-any.whl", hash = "sha256:c2d75552c80b83ca0d8c0b0db7db4f17e9f43ee131a46d0ddd514c5f5fc603cb"}, {file = "django_q2-1.6.1-py3-none-any.whl", hash = "sha256:0944b6cbb73671d471fa970a3807e294d750a224764986cff5bff0fe6daa7f7d"},
{file = "django_q2-1.6.2.tar.gz", hash = "sha256:cd83c16b5791cd99f83a8d106d2447305d73c6c8ed8ec22c7cb954fe0e814284"}, {file = "django_q2-1.6.1.tar.gz", hash = "sha256:38fa67e5f75d172ef59a1edcc891cb5f8f0fb863f95a6ada4c44dd3f2d5bda53"},
] ]
[[package]] [[package]]
@ -659,18 +646,6 @@ files = [
{file = "django_recurrence-1.11.1-py3-none-any.whl", hash = "sha256:0c65f30872599b5813a9bab6952dada23c55894f28674490a753ada559f14bc5"}, {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]] [[package]]
name = "django-stubs" name = "django-stubs"
version = "4.2.7" version = "4.2.7"
@ -729,20 +704,6 @@ files = [
{file = "django_tables2-2.7.0-py2.py3-none-any.whl", hash = "sha256:99e06d966ca8ac69fd74092eb45c79a280dd5ca0ccb81395d96261f62128e1af"}, {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]] [[package]]
name = "django-widget-tweaks" name = "django-widget-tweaks"
version = "1.5.0" version = "1.5.0"
@ -755,15 +716,16 @@ files = [
[[package]] [[package]]
name = "djangorestframework" name = "djangorestframework"
version = "3.15.1" version = "3.14.0"
requires_python = ">=3.6" requires_python = ">=3.6"
summary = "Web APIs for Django, made easy." summary = "Web APIs for Django, made easy."
dependencies = [ dependencies = [
"django>=3.0", "django>=3.0",
"pytz",
] ]
files = [ files = [
{file = "djangorestframework-3.15.1-py3-none-any.whl", hash = "sha256:3ccc0475bce968608cf30d07fb17d8e52d1d7fc8bfe779c905463200750cbca6"}, {file = "djangorestframework-3.14.0-py3-none-any.whl", hash = "sha256:eb63f58c9f218e1a7d064d17a70751f528ed4e1d35547fdade9aaf4cd103fd08"},
{file = "djangorestframework-3.15.1.tar.gz", hash = "sha256:f88fad74183dfc7144b2756d0d2ac716ea5b4c7c9840995ac3bfd8ec034333c1"}, {file = "djangorestframework-3.14.0.tar.gz", hash = "sha256:579a333e6256b09489cbe0a067e66abe55c6595d8926be6b99423786334350c8"},
] ]
[[package]] [[package]]
@ -1018,7 +980,7 @@ files = [
[[package]] [[package]]
name = "hypothesis" name = "hypothesis"
version = "6.100.1" version = "6.98.15"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "A library for property-based testing" summary = "A library for property-based testing"
dependencies = [ dependencies = [
@ -1026,23 +988,23 @@ dependencies = [
"sortedcontainers<3.0.0,>=2.1.0", "sortedcontainers<3.0.0,>=2.1.0",
] ]
files = [ files = [
{file = "hypothesis-6.100.1-py3-none-any.whl", hash = "sha256:3dacf6ec90e8d14aaee02cde081ac9a17d5b70105e45e6ac822db72052c0195b"}, {file = "hypothesis-6.98.15-py3-none-any.whl", hash = "sha256:5b40fd81fce9e0b35f0a47e10eb41f375a6b9e8551d0e1084c83b8b0d0d1bb6b"},
{file = "hypothesis-6.100.1.tar.gz", hash = "sha256:ebff09d7fa4f1fb6a855a812baf17e578b4481b7b70ec6d96496210d1a4c6c35"}, {file = "hypothesis-6.98.15.tar.gz", hash = "sha256:1e31210951511b24ce8b3b6e04d791c466385a30ac3af571bf2223954b025d77"},
] ]
[[package]] [[package]]
name = "hypothesis" name = "hypothesis"
version = "6.100.1" version = "6.98.15"
extras = ["django"] extras = ["django"]
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "A library for property-based testing" summary = "A library for property-based testing"
dependencies = [ dependencies = [
"django>=3.2", "django>=3.2",
"hypothesis==6.100.1", "hypothesis==6.98.15",
] ]
files = [ files = [
{file = "hypothesis-6.100.1-py3-none-any.whl", hash = "sha256:3dacf6ec90e8d14aaee02cde081ac9a17d5b70105e45e6ac822db72052c0195b"}, {file = "hypothesis-6.98.15-py3-none-any.whl", hash = "sha256:5b40fd81fce9e0b35f0a47e10eb41f375a6b9e8551d0e1084c83b8b0d0d1bb6b"},
{file = "hypothesis-6.100.1.tar.gz", hash = "sha256:ebff09d7fa4f1fb6a855a812baf17e578b4481b7b70ec6d96496210d1a4c6c35"}, {file = "hypothesis-6.98.15.tar.gz", hash = "sha256:1e31210951511b24ce8b3b6e04d791c466385a30ac3af571bf2223954b025d77"},
] ]
[[package]] [[package]]
@ -1057,7 +1019,7 @@ files = [
[[package]] [[package]]
name = "ipython" name = "ipython"
version = "8.23.0" version = "8.22.1"
requires_python = ">=3.10" requires_python = ">=3.10"
summary = "IPython: Productive Interactive Computing" summary = "IPython: Productive Interactive Computing"
dependencies = [ dependencies = [
@ -1070,11 +1032,10 @@ dependencies = [
"pygments>=2.4.0", "pygments>=2.4.0",
"stack-data", "stack-data",
"traitlets>=5.13.0", "traitlets>=5.13.0",
"typing-extensions; python_version < \"3.12\"",
] ]
files = [ files = [
{file = "ipython-8.23.0-py3-none-any.whl", hash = "sha256:07232af52a5ba146dc3372c7bf52a0f890a23edf38d77caef8d53f9cdc2584c1"}, {file = "ipython-8.22.1-py3-none-any.whl", hash = "sha256:869335e8cded62ffb6fac8928e5287a05433d6462e3ebaac25f4216474dd6bc4"},
{file = "ipython-8.23.0.tar.gz", hash = "sha256:7468edaf4f6de3e1b912e57f66c241e6fd3c7099f2ec2136e239e142e800274d"}, {file = "ipython-8.22.1.tar.gz", hash = "sha256:39c6f9efc079fb19bfb0f17eee903978fe9a290b1b82d68196c641cecb76ea22"},
] ]
[[package]] [[package]]
@ -1113,77 +1074,43 @@ files = [
[[package]] [[package]]
name = "lxml" name = "lxml"
version = "5.2.1" version = "5.1.0"
requires_python = ">=3.6" requires_python = ">=3.6"
summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." summary = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API."
files = [ files = [
{file = "lxml-5.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:70ac664a48aa64e5e635ae5566f5227f2ab7f66a3990d67566d9907edcbbf867"}, {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"},
{file = "lxml-5.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1ae67b4e737cddc96c99461d2f75d218bdf7a0c3d3ad5604d1f5e7464a2f9ffe"}, {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f18a5a84e16886898e51ab4b1d43acb3083c39b14c8caeb3589aabff0ee0b270"}, {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6f2c8372b98208ce609c9e1d707f6918cc118fea4e2c754c9f0812c04ca116d"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:acb6b2f96f60f70e7f34efe0c3ea34ca63f19ca63ce90019c6cbca6b676e81fa"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:394ed3924d7a01b5bd9a0d9d946136e1c2f7b3dc337196d99e61740ed4bc6fe1"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d077bc40a1fe984e1a9931e801e42959a1e6598edc8a3223b061d30fbd26bbc"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:764b521b75701f60683500d8621841bec41a65eb739b8466000c6fdbc256c240"}, {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3a6b45da02336895da82b9d472cd274b22dc27a5cea1d4b793874eead23dd14f"}, {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:5ea7b6766ac2dfe4bcac8b8595107665a18ef01f8c8343f00710b85096d1b53a"}, {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:e196a4ff48310ba62e53a8e0f97ca2bca83cdd2fe2934d8b5cb0df0a841b193a"}, {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"},
{file = "lxml-5.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:200e63525948e325d6a13a76ba2911f927ad399ef64f57898cf7c74e69b71095"}, {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"},
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dae0ed02f6b075426accbf6b2863c3d0a7eacc1b41fb40f2251d931e50188dad"}, {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"},
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:ab31a88a651039a07a3ae327d68ebdd8bc589b16938c09ef3f32a4b809dc96ef"}, {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"},
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:df2e6f546c4df14bc81f9498bbc007fbb87669f1bb707c6138878c46b06f6510"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f643ffd2669ffd4b5a3e9b41c909b72b2a1d5e4915da90a77e119b8d48ce867a"},
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5dd1537e7cc06efd81371f5d1a992bd5ab156b2b4f88834ca852de4a8ea523fa"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"},
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b9ec9c9978b708d488bec36b9e4c94d88fd12ccac3e62134a9d17ddba910ea9"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"},
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8e77c69d5892cb5ba71703c4057091e31ccf534bd7f129307a4d084d90d014b8"}, {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"},
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a8d5c70e04aac1eda5c829a26d1f75c6e5286c74743133d9f742cda8e53b9c2f"}, {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"},
{file = "lxml-5.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c94e75445b00319c1fad60f3c98b09cd63fe1134a8a953dcd48989ef42318534"}, {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"},
{file = "lxml-5.2.1-cp311-cp311-win32.whl", hash = "sha256:4951e4f7a5680a2db62f7f4ab2f84617674d36d2d76a729b9a8be4b59b3659be"}, {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"},
{file = "lxml-5.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c670c0406bdc845b474b680b9a5456c561c65cf366f8db5a60154088c92d102"}, {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"},
{file = "lxml-5.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:abc25c3cab9ec7fcd299b9bcb3b8d4a1231877e425c650fa1c7576c5107ab851"}, {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"},
{file = "lxml-5.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6935bbf153f9a965f1e07c2649c0849d29832487c52bb4a5c5066031d8b44fd5"}, {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d793bebb202a6000390a5390078e945bbb49855c29c7e4d56a85901326c3b5d9"}, {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd5562927cdef7c4f5550374acbc117fd4ecc05b5007bdfa57cc5355864e0a4"}, {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e7259016bc4345a31af861fdce942b77c99049d6c2107ca07dc2bba2435c1d9"}, {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:530e7c04f72002d2f334d5257c8a51bf409db0316feee7c87e4385043be136af"}, {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59689a75ba8d7ffca577aefd017d08d659d86ad4585ccc73e43edbfc7476781a"}, {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f9737bf36262046213a28e789cc82d82c6ef19c85a0cf05e75c670a33342ac2c"}, {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:3a74c4f27167cb95c1d4af1c0b59e88b7f3e0182138db2501c353555f7ec57f4"}, {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:68a2610dbe138fa8c5826b3f6d98a7cfc29707b850ddcc3e21910a6fe51f6ca0"}, {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"},
{file = "lxml-5.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f0a1bc63a465b6d72569a9bba9f2ef0334c4e03958e043da1920299100bc7c08"}, {file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"},
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c2d35a1d047efd68027817b32ab1586c1169e60ca02c65d428ae815b593e65d4"}, {file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"},
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:79bd05260359170f78b181b59ce871673ed01ba048deef4bf49a36ab3e72e80b"},
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:865bad62df277c04beed9478fe665b9ef63eb28fe026d5dedcb89b537d2e2ea6"},
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:44f6c7caff88d988db017b9b0e4ab04934f11e3e72d478031efc7edcac6c622f"},
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:71e97313406ccf55d32cc98a533ee05c61e15d11b99215b237346171c179c0b0"},
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:057cdc6b86ab732cf361f8b4d8af87cf195a1f6dc5b0ff3de2dced242c2015e0"},
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f3bbbc998d42f8e561f347e798b85513ba4da324c2b3f9b7969e9c45b10f6169"},
{file = "lxml-5.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:491755202eb21a5e350dae00c6d9a17247769c64dcf62d8c788b5c135e179dc4"},
{file = "lxml-5.2.1-cp312-cp312-win32.whl", hash = "sha256:8de8f9d6caa7f25b204fc861718815d41cbcf27ee8f028c89c882a0cf4ae4134"},
{file = "lxml-5.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f2a9efc53d5b714b8df2b4b3e992accf8ce5bbdfe544d74d5c6766c9e1146a3a"},
{file = "lxml-5.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9b0ff53900566bc6325ecde9181d89afadc59c5ffa39bddf084aaedfe3b06a11"},
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6037392f2d57793ab98d9e26798f44b8b4da2f2464388588f48ac52c489ea1"},
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b9c07e7a45bb64e21df4b6aa623cb8ba214dfb47d2027d90eac197329bb5e94"},
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3249cc2989d9090eeac5467e50e9ec2d40704fea9ab72f36b034ea34ee65ca98"},
{file = "lxml-5.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f42038016852ae51b4088b2862126535cc4fc85802bfe30dea3500fdfaf1864e"},
{file = "lxml-5.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:533658f8fbf056b70e434dff7e7aa611bcacb33e01f75de7f821810e48d1bb66"},
{file = "lxml-5.2.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:622020d4521e22fb371e15f580d153134bfb68d6a429d1342a25f051ec72df1c"},
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efa7b51824aa0ee957ccd5a741c73e6851de55f40d807f08069eb4c5a26b2baa"},
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c6ad0fbf105f6bcc9300c00010a2ffa44ea6f555df1a2ad95c88f5656104817"},
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:e233db59c8f76630c512ab4a4daf5a5986da5c3d5b44b8e9fc742f2a24dbd460"},
{file = "lxml-5.2.1-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6a014510830df1475176466b6087fc0c08b47a36714823e58d8b8d7709132a96"},
{file = "lxml-5.2.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:d38c8f50ecf57f0463399569aa388b232cf1a2ffb8f0a9a5412d0db57e054860"},
{file = "lxml-5.2.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5aea8212fb823e006b995c4dda533edcf98a893d941f173f6c9506126188860d"},
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff097ae562e637409b429a7ac958a20aab237a0378c42dabaa1e3abf2f896e5f"},
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f5d65c39f16717a47c36c756af0fb36144069c4718824b7533f803ecdf91138"},
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3d0c3dd24bb4605439bf91068598d00c6370684f8de4a67c2992683f6c309d6b"},
{file = "lxml-5.2.1-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e32be23d538753a8adb6c85bd539f5fd3b15cb987404327c569dfc5fd8366e85"},
{file = "lxml-5.2.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:cc518cea79fd1e2f6c90baafa28906d4309d24f3a63e801d855e7424c5b34144"},
{file = "lxml-5.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a0af35bd8ebf84888373630f73f24e86bf016642fb8576fba49d3d6b560b7cbc"},
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8aca2e3a72f37bfc7b14ba96d4056244001ddcc18382bd0daa087fd2e68a354"},
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ca1e8188b26a819387b29c3895c47a5e618708fe6f787f3b1a471de2c4a94d9"},
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c8ba129e6d3b0136a0f50345b2cb3db53f6bda5dd8c7f5d83fbccba97fb5dcb5"},
{file = "lxml-5.2.1-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e998e304036198b4f6914e6a1e2b6f925208a20e2042563d9734881150c6c246"},
{file = "lxml-5.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d3be9b2076112e51b323bdf6d5a7f8a798de55fb8d95fcb64bd179460cdc0704"},
{file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"},
] ]
[[package]] [[package]]
@ -1220,15 +1147,15 @@ files = [
[[package]] [[package]]
name = "markdownify" name = "markdownify"
version = "0.12.1" version = "0.11.6"
summary = "Convert HTML to markdown." summary = "Convert HTML to markdown."
dependencies = [ dependencies = [
"beautifulsoup4<5,>=4.9", "beautifulsoup4<5,>=4.9",
"six<2,>=1.15", "six<2,>=1.15",
] ]
files = [ files = [
{file = "markdownify-0.12.1-py3-none-any.whl", hash = "sha256:a3805abd8166dbb7b27783c5599d91f54f10d79894b2621404d85b333c7ce561"}, {file = "markdownify-0.11.6-py3-none-any.whl", hash = "sha256:ba35fe289d5e9073bcd7d2cad629278fe25f1a93741fcdc0bfb4f009076d8324"},
{file = "markdownify-0.12.1.tar.gz", hash = "sha256:1fb08c618b30e0ee7a31a39b998f44a18fb28ab254f55f4af06b6d35a2179e27"}, {file = "markdownify-0.11.6.tar.gz", hash = "sha256:009b240e0c9f4c8eaf1d085625dcd4011e12f0f8cec55dedf9ea6f7655e49bfe"},
] ]
[[package]] [[package]]
@ -1384,25 +1311,25 @@ files = [
[[package]] [[package]]
name = "nh3" name = "nh3"
version = "0.2.17" version = "0.2.15"
summary = "Python bindings to the ammonia HTML sanitization library." summary = "Python bindings to the ammonia HTML sanitization library."
files = [ files = [
{file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9"}, {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9c0d415f6b7f2338f93035bba5c0d8c1b464e538bfbb1d598acd47d7969284f0"},
{file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a"}, {file = "nh3-0.2.15-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:6f42f99f0cf6312e470b6c09e04da31f9abaadcd3eb591d7d1a88ea931dca7f3"},
{file = "nh3-0.2.17-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3"}, {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac19c0d68cd42ecd7ead91a3a032fdfff23d29302dbb1311e641a130dfefba97"},
{file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a"}, {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f0d77272ce6d34db6c87b4f894f037d55183d9518f948bba236fe81e2bb4e28"},
{file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a"}, {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8d595df02413aa38586c24811237e95937ef18304e108b7e92c890a06793e3bf"},
{file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351"}, {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86e447a63ca0b16318deb62498db4f76fc60699ce0a1231262880b38b6cff911"},
{file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc"}, {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3277481293b868b2715907310c7be0f1b9d10491d5adf9fce11756a97e97eddf"},
{file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f"}, {file = "nh3-0.2.15-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60684857cfa8fdbb74daa867e5cad3f0c9789415aba660614fe16cd66cbb9ec7"},
{file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b"}, {file = "nh3-0.2.15-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3b803a5875e7234907f7d64777dfde2b93db992376f3d6d7af7f3bc347deb305"},
{file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a"}, {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0d02d0ff79dfd8208ed25a39c12cbda092388fff7f1662466e27d97ad011b770"},
{file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062"}, {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:f3b53ba93bb7725acab1e030bc2ecd012a817040fd7851b332f86e2f9bb98dc6"},
{file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71"}, {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b1e97221cedaf15a54f5243f2c5894bb12ca951ae4ddfd02a9d4ea9df9e1a29d"},
{file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10"}, {file = "nh3-0.2.15-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a5167a6403d19c515217b6bcaaa9be420974a6ac30e0da9e84d4fc67a5d474c5"},
{file = "nh3-0.2.17-cp37-abi3-win32.whl", hash = "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911"}, {file = "nh3-0.2.15-cp37-abi3-win32.whl", hash = "sha256:427fecbb1031db085eaac9931362adf4a796428ef0163070c484b5a768e71601"},
{file = "nh3-0.2.17-cp37-abi3-win_amd64.whl", hash = "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb"}, {file = "nh3-0.2.15-cp37-abi3-win_amd64.whl", hash = "sha256:bc2d086fb540d0fa52ce35afaded4ea526b8fc4d3339f783db55c95de40ef02e"},
{file = "nh3-0.2.17.tar.gz", hash = "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028"}, {file = "nh3-0.2.15.tar.gz", hash = "sha256:d1e30ff2d8d58fb2a14961f7aac1bbb1c51f9bdd7da727be35c63826060b0bf3"},
] ]
[[package]] [[package]]
@ -1663,6 +1590,15 @@ files = [
{file = "python-ldap-3.4.3.tar.gz", hash = "sha256:ab26c519a0ef2a443a2a10391fa3c5cb52d7871323399db949ebfaa9f25ee2a0"}, {file = "python-ldap-3.4.3.tar.gz", hash = "sha256:ab26c519a0ef2a443a2a10391fa3c5cb52d7871323399db949ebfaa9f25ee2a0"},
] ]
[[package]]
name = "pytz"
version = "2022.7"
summary = "World timezone definitions, modern and historical"
files = [
{file = "pytz-2022.7-py2.py3-none-any.whl", hash = "sha256:93007def75ae22f7cd991c84e02d434876818661f8df9ad5df9e950ff4e52cfd"},
{file = "pytz-2022.7.tar.gz", hash = "sha256:7ccfae7b4b2c067464a6733c6261673fdb8fd1be905460396b97a073e9fa683a"},
]
[[package]] [[package]]
name = "pywin32" name = "pywin32"
version = "306" version = "306"
@ -1781,27 +1717,27 @@ files = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.3.7" version = "0.3.0"
requires_python = ">=3.7" requires_python = ">=3.7"
summary = "An extremely fast Python linter and code formatter, written in Rust." summary = "An extremely fast Python linter and code formatter, written in Rust."
files = [ files = [
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"}, {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"},
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"}, {file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"}, {file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"}, {file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"}, {file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"}, {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"}, {file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"}, {file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"},
{file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"}, {file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"}, {file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"}, {file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"}, {file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"},
{file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"}, {file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"},
{file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"}, {file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"},
{file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"}, {file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"},
{file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"}, {file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"},
{file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"}, {file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"},
] ]
[[package]] [[package]]
@ -1816,12 +1752,12 @@ files = [
[[package]] [[package]]
name = "setuptools" name = "setuptools"
version = "69.5.1" version = "69.1.1"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Easily download, build, install, upgrade, and uninstall Python packages" summary = "Easily download, build, install, upgrade, and uninstall Python packages"
files = [ files = [
{file = "setuptools-69.5.1-py3-none-any.whl", hash = "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32"}, {file = "setuptools-69.1.1-py3-none-any.whl", hash = "sha256:02fa291a0471b3a18b2b2481ed902af520c69e8ae0919c13da936542754b4c56"},
{file = "setuptools-69.5.1.tar.gz", hash = "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987"}, {file = "setuptools-69.1.1.tar.gz", hash = "sha256:5c0806c7d9af348e6dd3777b4f4dbb42c7ad85b190104837488eab9a7c945cf8"},
] ]
[[package]] [[package]]
@ -1889,28 +1825,28 @@ files = [
[[package]] [[package]]
name = "tablib" name = "tablib"
version = "3.6.1" version = "3.5.0"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)" summary = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)"
files = [ files = [
{file = "tablib-3.6.1-py3-none-any.whl", hash = "sha256:c771d38ed1d74350a69873db43e0afb7f1cca0ed2915a7243094463eb6789207"}, {file = "tablib-3.5.0-py3-none-any.whl", hash = "sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9"},
{file = "tablib-3.6.1.tar.gz", hash = "sha256:040685fde11e9237675f43e985edb94b63250a5e9236f89d561ce6fb1465b839"}, {file = "tablib-3.5.0.tar.gz", hash = "sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33"},
] ]
[[package]] [[package]]
name = "tablib" name = "tablib"
version = "3.6.1" version = "3.5.0"
extras = ["ods", "xlsx"] extras = ["ods", "xlsx"]
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)" summary = "Format agnostic tabular data library (XLS, JSON, YAML, CSV, etc.)"
dependencies = [ dependencies = [
"odfpy", "odfpy",
"openpyxl>=2.6.0", "openpyxl>=2.6.0",
"tablib==3.6.1", "tablib==3.5.0",
] ]
files = [ files = [
{file = "tablib-3.6.1-py3-none-any.whl", hash = "sha256:c771d38ed1d74350a69873db43e0afb7f1cca0ed2915a7243094463eb6789207"}, {file = "tablib-3.5.0-py3-none-any.whl", hash = "sha256:9821caa9eca6062ff7299fa645e737aecff982e6b2b42046928a6413c8dabfd9"},
{file = "tablib-3.6.1.tar.gz", hash = "sha256:040685fde11e9237675f43e985edb94b63250a5e9236f89d561ce6fb1465b839"}, {file = "tablib-3.5.0.tar.gz", hash = "sha256:f6661dfc45e1d4f51fa8a6239f9c8349380859a5bfaa73280645f046d6c96e33"},
] ]
[[package]] [[package]]
@ -1961,15 +1897,15 @@ files = [
[[package]] [[package]]
name = "types-bleach" name = "types-bleach"
version = "6.1.0.20240331" version = "6.1.0.20240222"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Typing stubs for bleach" summary = "Typing stubs for bleach"
dependencies = [ dependencies = [
"types-html5lib", "types-html5lib",
] ]
files = [ files = [
{file = "types-bleach-6.1.0.20240331.tar.gz", hash = "sha256:2ee858a84fb06fc2225ff56ba2f7f6c88b65638659efae0d7bfd6b24a1b5a524"}, {file = "types-bleach-6.1.0.20240222.tar.gz", hash = "sha256:1299f06b5ef80e2d4f25fac11f613033c9bc35bad116413cb320d0c0c1188466"},
{file = "types_bleach-6.1.0.20240331-py3-none-any.whl", hash = "sha256:399bc59bfd20a36a56595f13f805e56c8a08e5a5c07903e5cf6fafb5a5107dd4"}, {file = "types_bleach-6.1.0.20240222-py3-none-any.whl", hash = "sha256:78a1c39484c8949030a0931076eeb69b03a0e08a4fafec5b3764e142929859d4"},
] ]
[[package]] [[package]]
@ -2002,15 +1938,15 @@ files = [
[[package]] [[package]]
name = "types-requests" name = "types-requests"
version = "2.31.0.20240406" version = "2.31.0.20240218"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "Typing stubs for requests" summary = "Typing stubs for requests"
dependencies = [ dependencies = [
"urllib3>=2", "urllib3>=2",
] ]
files = [ files = [
{file = "types-requests-2.31.0.20240406.tar.gz", hash = "sha256:4428df33c5503945c74b3f42e82b181e86ec7b724620419a2966e2de604ce1a1"}, {file = "types-requests-2.31.0.20240218.tar.gz", hash = "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"},
{file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, {file = "types_requests-2.31.0.20240218-py3-none-any.whl", hash = "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b"},
] ]
[[package]] [[package]]
@ -2071,7 +2007,7 @@ files = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.29.0" version = "0.27.1"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "The lightning-fast ASGI server." summary = "The lightning-fast ASGI server."
dependencies = [ dependencies = [
@ -2079,13 +2015,13 @@ dependencies = [
"h11>=0.8", "h11>=0.8",
] ]
files = [ files = [
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"},
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"},
] ]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.29.0" version = "0.27.1"
extras = ["standard"] extras = ["standard"]
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "The lightning-fast ASGI server." summary = "The lightning-fast ASGI server."
@ -2094,14 +2030,14 @@ dependencies = [
"httptools>=0.5.0", "httptools>=0.5.0",
"python-dotenv>=0.13", "python-dotenv>=0.13",
"pyyaml>=5.1", "pyyaml>=5.1",
"uvicorn==0.29.0", "uvicorn==0.27.1",
"uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"", "uvloop!=0.15.0,!=0.15.1,>=0.14.0; (sys_platform != \"cygwin\" and sys_platform != \"win32\") and platform_python_implementation != \"PyPy\"",
"watchfiles>=0.13", "watchfiles>=0.13",
"websockets>=10.4", "websockets>=10.4",
] ]
files = [ files = [
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"},
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"},
] ]
[[package]] [[package]]
@ -2186,7 +2122,7 @@ files = [
[[package]] [[package]]
name = "weasyprint" name = "weasyprint"
version = "61.2" version = "61.1"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "The Awesome Document Factory" summary = "The Awesome Document Factory"
dependencies = [ dependencies = [
@ -2200,8 +2136,8 @@ dependencies = [
"tinycss2>=1.0.0", "tinycss2>=1.0.0",
] ]
files = [ files = [
{file = "weasyprint-61.2-py3-none-any.whl", hash = "sha256:76c6dc0e75e09182d5645d92c66ddf86b1b992c9420235b723fb374b584e5bf4"}, {file = "weasyprint-61.1-py3-none-any.whl", hash = "sha256:a5622f2c67d408e5d2e94df0ae07b9bd0fa9bb5e1ee466e2211391807f1ece41"},
{file = "weasyprint-61.2.tar.gz", hash = "sha256:47df6cfeeff8c6c28cf2e4caf837cde17715efe462708ada74baa2eb391b6059"}, {file = "weasyprint-61.1.tar.gz", hash = "sha256:7cbc824dc4026d82a97362647755f837dd15799949639cc8a428dd33d385009a"},
] ]
[[package]] [[package]]

View File

@ -7,23 +7,23 @@ authors = [
] ]
dependencies = [ dependencies = [
"django~=5.0", "django~=5.0",
"django-admin-logs~=1.2", "django-admin-logs~=1.1",
"django-auth-ldap~=4.8", "django-auth-ldap~=4.6",
"django-markdownx~=4.0", "django-markdownx~=4.0",
"django-recurrence~=1.11", "django-recurrence~=1.11",
"django-widget-tweaks~=1.5", "django-widget-tweaks~=1.5",
"django-stubs-ext~=4.2", "django-stubs-ext~=4.2",
"markdownify~=0.12", "markdownify~=0.11",
"mdformat~=0.7", "mdformat~=0.7",
"mdformat-tables~=0.4", "mdformat-tables~=0.4",
"mysqlclient~=2.2", "mysqlclient~=2.2",
"django-autocomplete-light~=3.11", "django-autocomplete-light~=3.11",
"weasyprint~=61.2", "weasyprint~=61.1",
"requests~=2.31", "requests~=2.31",
"semver~=3.0", "semver~=3.0",
"djangorestframework~=3.15", "djangorestframework~=3.14",
"django-q2~=1.6", "django-q2~=1.6",
"lxml~=5.2", "lxml~=5.1",
"django-object-actions~=4.2", "django-object-actions~=4.2",
"bitstring~=4.1", "bitstring~=4.1",
"udm-rest-client~=1.2", "udm-rest-client~=1.2",
@ -31,20 +31,17 @@ dependencies = [
"django-nh3~=0.1", "django-nh3~=0.1",
"nh3~=0.2", "nh3~=0.2",
"django-tables2~=2.7", "django-tables2~=2.7",
"tablib[ods,xlsx]~=3.6", "tablib[ods,xlsx]~=3.5",
"django-filter~=24.2", "django-filter~=23.5",
"django-db-views~=0.1", "django-db-views~=0.1",
"django-mysql~=4.12", "django-mysql~=4.12",
"django-weasyprint~=2.3",
"django-sendfile2~=0.7",
"django-bootstrap5~=24.1",
] ]
requires-python = ">=3.11" requires-python = ">=3.11"
[project.optional-dependencies] [project.optional-dependencies]
server = [ server = [
"uvicorn[standard]~=0.29", "uvicorn[standard]~=0.27",
"setuptools~=69.5", "setuptools~=69.1",
] ]
[project.entry-points."djangoq.errorreporters"] [project.entry-points."djangoq.errorreporters"]
@ -54,7 +51,7 @@ admin_email = "cmsmanage.django_q2_admin_email_reporter:AdminEmailReporter"
line-length = 88 line-length = 88
[tool.ruff.lint] [tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "C4", "UP", "PERF", "PL", "SIM", "FIX003"] select = ["E4", "E7", "E9", "F", "I", "C4", "UP", "PERF", "PL", "SIM"]
[tool.ruff.lint.isort] [tool.ruff.lint.isort]
known-first-party = [ known-first-party = [
@ -116,7 +113,7 @@ lint = [
typing = [ typing = [
"mypy~=1.7", "mypy~=1.7",
"django-stubs~=4.2", "django-stubs~=4.2",
"setuptools~=69.5", "setuptools~=69.1",
"types-bleach~=6.1", "types-bleach~=6.1",
"types-requests~=2.31", "types-requests~=2.31",
"types-urllib3~=1.26", "types-urllib3~=1.26",
@ -128,14 +125,14 @@ debug = [
] ]
dev = [ dev = [
"django-extensions~=3.2", "django-extensions~=3.2",
"ipython~=8.23", "ipython~=8.22",
"hypothesis[django]~=6.100", "hypothesis[django]~=6.98",
"tblib~=3.0", "tblib~=3.0",
] ]
[tool.pdm.scripts] [tool.pdm.scripts]
start = "./manage.py runserver" start = "./manage.py runserver"
fmt.shell = "ruff check --fix ; ruff format . ; djlint --reformat ." fmt.shell = "ruff check --fix && ruff format . && djlint --reformat ."
[build-system] [build-system]
requires = ["pdm-backend"] requires = ["pdm-backend"]