Compare commits

...

14 Commits

Author SHA1 Message Date
eec1420ee9 membershipworks: Fix EventInstructor search_fields
All checks were successful
Ruff / ruff (push) Successful in 1m16s
2024-01-29 21:49:45 -05:00
d3215a6588 paperwork: Fix typo in dashboard 2024-01-29 21:48:55 -05:00
e10bcd5b49 membershipworks: Add storage and scraping for Event details 2024-01-29 21:48:19 -05:00
334f8d4bf8 membershipworks: Move table-specific queryset operations into get_table_data 2024-01-29 14:18:00 -05:00
29e87c4468 membershipworks: Change name for EventExt.meetings to avoid extra queries 2024-01-29 14:18:00 -05:00
dafd8cc620 membershipworks: Add default ordering by start field to EventExt 2024-01-29 14:17:58 -05:00
21fe7f66cd membershipworks: Expose EventExtQuerySet methods on EventExtManager 2024-01-28 23:54:55 -05:00
0eab482893 Allow empty lines in templates 2024-01-26 14:13:46 -05:00
60b8510961 Enable djlint formatting pre-commit hook 2024-01-26 14:04:32 -05:00
240ebc710d Apply missed djlint format 2024-01-26 14:03:46 -05:00
a1a9cb2890 Bump dependencies 2024-01-26 14:03:04 -05:00
9f732d9b43 doorcontrol: Use input group for start date/end date in access reports 2024-01-26 14:00:30 -05:00
968a47c723 doorcontrol: Use django-filters for date range filter in access reports 2024-01-26 14:00:27 -05:00
f20d85e2e4 Add django-filters 2024-01-26 13:56:27 -05:00
16 changed files with 286 additions and 171 deletions

View File

@ -11,6 +11,7 @@ repos:
rev: v1.34.1 rev: v1.34.1
hooks: hooks:
- id: djlint-django - id: djlint-django
- id: djlint-reformat-django
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.13 rev: v0.1.13

View File

@ -39,6 +39,7 @@ INSTALLED_APPS = [
"django_q", "django_q",
"django_nh3", "django_nh3",
"django_tables2", "django_tables2",
"django_filters",
"tasks.apps.TasksConfig", "tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig", "rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig", "membershipworks.apps.MembershipworksConfig",

View File

@ -3,6 +3,7 @@
{% load render_table from django_tables2 %} {% load render_table from django_tables2 %}
{% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %} {% block title %}{{ selected_report }} | Door Controls | CMS{% endblock %}
{% block content %} {% block content %}
<div class="vstack align-items-center"> <div class="vstack align-items-center">
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
@ -13,28 +14,30 @@
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<form method="get" class="container-fluid"> <form method="get" class="container-fluid">
<div class="row g-2 align-items-center justify-content-center"> <div class="row g-2 align-items-center justify-content-center">
<div class="col-6 col-sm-auto"> <div class="col-auto">
<div class="form-floating"> <div class="input-group">
<input type="date" <div class="form-floating">
class="form-control" <input type="date"
id="startDate" class="form-control"
name="timestamp__gte" id="startDate"
value="{{ timestamp__gte|date:'Y-m-d' }}"> name="timestamp_after"
<label for="startDate">Start Date</label> value="{{ filter.form.timestamp.value.0 }}">
</div> <label for="startDate">Start Date</label>
</div> </div>
<div class="col-6 col-sm-auto"> <div class="form-floating">
<div class="form-floating"> <input type="date"
<input type="date" class="form-control"
class="form-control" id="endDate"
id="endDate" name="timestamp_before"
name="timestamp__lte" value="{{ filter.form.timestamp.value.1 }}">
value="{{ timestamp__lte|date:'Y-m-d' }}"> <label for="endDate">End Date</label>
<label for="endDate">End Date</label> </div>
</div> </div>
</div> </div>
<div class="col-12 col-sm-auto"> <div class="col-12 col-sm-auto">
<div class="form-floating"> <div class="form-floating">
<input type="number" <input type="number"
@ -49,15 +52,16 @@
<label for="itemsPerPage">Items Per Page</label> <label for="itemsPerPage">Items Per Page</label>
</div> </div>
</div> </div>
<div class="btn-group col-auto" role="group" aria-label="Form Controls"> <div class="btn-group col-auto" role="group" aria-label="Form Controls">
<button type="submit" class="btn btn-sm btn-primary">Submit</button> <button type="submit" class="btn btn-sm btn-primary">Submit</button>
<a href="?" class="btn btn-sm btn-warning">Reset</a> <a href="?" class="btn btn-sm btn-warning">Reset</a>
</div> </div>
<div class="col-auto">
{% include "cmsmanage/components/download_table.dj.html" %} <div class="col-auto">{% include "cmsmanage/components/download_table.dj.html" %}</div>
</div>
</div> </div>
</form> </form>
{% render_table table %} {% render_table table %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -7,12 +7,12 @@ from django.core.paginator import Page
from django.db.models import Count, F, FloatField, Window from django.db.models import Count, F, FloatField, Window
from django.db.models.functions import Lead, Trunc from django.db.models.functions import Lead, Trunc
from django.urls import path, reverse_lazy from django.urls import path, reverse_lazy
from django.utils import dateparse
from django.utils.text import slugify from django.utils.text import slugify
from django.utils.timezone import localtime
from django.views.generic.list import ListView from django.views.generic.list import ListView
import django_filters
import django_tables2 as tables import django_tables2 as tables
from django_filters.views import BaseFilterView
from django_tables2 import SingleTableMixin from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
@ -26,8 +26,12 @@ def register_report(cls: "BaseAccessReport"):
return cls return cls
class DateTimeFilters(django_filters.FilterSet):
timestamp = django_filters.DateFromToRangeFilter()
class BaseAccessReport( class BaseAccessReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView BaseFilterView, ExportMixin, SingleTableMixin, PermissionRequiredMixin, ListView
): ):
model = HIDEvent model = HIDEvent
permission_required = "doorcontrol.view_hidevent" permission_required = "doorcontrol.view_hidevent"
@ -37,6 +41,8 @@ class BaseAccessReport(
export_formats = ("csv", "xlsx", "ods") export_formats = ("csv", "xlsx", "ods")
filterset_class = DateTimeFilters
_report_name = None _report_name = None
@classmethod @classmethod
@ -58,30 +64,13 @@ class BaseAccessReport(
def _selected_report(self): def _selected_report(self):
return self._report_name return self._report_name
def _get_timestamp_range(self):
timestamp__gte = dateparse.parse_datetime(
self.request.GET.get("timestamp__gte") or "2019-01-01"
)
timestamp__lte = self.request.GET.get("timestamp__lte")
if timestamp__lte:
timestamp__lte = dateparse.parse_datetime(timestamp__lte)
else:
timestamp__lte = localtime()
return timestamp__gte, timestamp__lte
def get_paginate_by(self, queryset) -> int: def get_paginate_by(self, queryset) -> int:
if "items_per_page" in self.request.GET: if "items_per_page" in self.request.GET:
return int(self.request.GET.get("items_per_page")) return int(self.request.GET.get("items_per_page"))
return super().get_paginate_by(queryset) return super().get_paginate_by(queryset)
def get_queryset(self): def get_queryset(self):
return ( return super().get_queryset().select_related("door")
super()
.get_queryset()
.filter(timestamp__range=self._get_timestamp_range())
.select_related("door")
)
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs) context = super().get_context_data(**kwargs)
@ -94,10 +83,6 @@ class BaseAccessReport(
context["selected_report"] = self._selected_report() context["selected_report"] = self._selected_report()
context["items_per_page"] = self.get_paginate_by(None) context["items_per_page"] = self.get_paginate_by(None)
timestamp__gte, timestamp__lte = self._get_timestamp_range()
context["timestamp__gte"] = timestamp__gte
context["timestamp__lte"] = timestamp__lte
query_params = self.request.GET.copy() query_params = self.request.GET.copy()
if "page" in query_params: if "page" in query_params:
query_params.pop("page") query_params.pop("page")

View File

@ -2,7 +2,11 @@ from django.contrib import admin
from django.contrib.humanize.templatetags.humanize import naturaltime from django.contrib.humanize.templatetags.humanize import naturaltime
from django.utils.html import format_html from django.utils.html import format_html
from django_object_actions import DjangoObjectActions, action from django_object_actions import (
DjangoObjectActions,
action,
takes_instance_or_queryset,
)
from django_q.models import Task from django_q.models import Task
from django_q.tasks import async_task from django_q.tasks import async_task
@ -15,7 +19,10 @@ from .models import (
Member, Member,
Transaction, Transaction,
) )
from .tasks.scrape import scrape_membershipworks from .tasks.scrape import (
scrape_event_details,
scrape_membershipworks,
)
class ReadOnlyAdmin(admin.ModelAdmin): class ReadOnlyAdmin(admin.ModelAdmin):
@ -100,11 +107,11 @@ class EventMeetingTimeInline(admin.TabularInline):
@admin.register(EventInstructor) @admin.register(EventInstructor)
class EventInstructorAdmin(admin.ModelAdmin): class EventInstructorAdmin(admin.ModelAdmin):
autocomplete_fields = ["member"] autocomplete_fields = ["member"]
search_fields = ["name", "member"] search_fields = ["name", "member__account_name"]
@admin.register(EventExt) @admin.register(EventExt)
class EventAdmin(admin.ModelAdmin): class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
inlines = [EventMeetingTimeInline] inlines = [EventMeetingTimeInline]
list_display = [ list_display = [
"title", "title",
@ -123,8 +130,10 @@ class EventAdmin(admin.ModelAdmin):
show_facets = admin.ShowFacets.ALWAYS show_facets = admin.ShowFacets.ALWAYS
search_fields = ["eid", "title", "url"] search_fields = ["eid", "title", "url"]
date_hierarchy = "start" date_hierarchy = "start"
exclude = ["url"] exclude = ["url", "details"]
autocomplete_fields = ["instructor"] autocomplete_fields = ["instructor"]
change_actions = ["fetch_details"]
actions = ["fetch_details"]
@property @property
def readonly_fields(self): def readonly_fields(self):
@ -137,6 +146,7 @@ class EventAdmin(admin.ModelAdmin):
else: else:
fields.append(field.name) fields.append(field.name)
fields.insert(fields.index("end") + 1, "duration") fields.insert(fields.index("end") + 1, "duration")
fields.append("details_timestamp")
return fields return fields
@admin.display(ordering="duration") @admin.display(ordering="duration")
@ -150,6 +160,10 @@ class EventAdmin(admin.ModelAdmin):
obj.url, obj.url,
) )
@takes_instance_or_queryset
def fetch_details(self, request, queryset):
scrape_event_details(queryset)
def has_add_permission(self, request, obj=None): def has_add_permission(self, request, obj=None):
return False return False

View File

@ -0,0 +1,16 @@
# Generated by Django 5.0.1 on 2024-01-29 19:17
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0009_eventext_materials_fee_included_in_price"),
]
operations = [
migrations.AlterModelOptions(
name="eventext",
options={"ordering": ["-start"], "verbose_name": "event"},
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 5.0.1 on 2024-01-29 19:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0010_alter_eventext_options"),
]
operations = [
migrations.AddField(
model_name="eventext",
name="details",
field=models.JSONField(blank=True, null=True),
),
migrations.AddField(
model_name="eventext",
name="details_timestamp",
field=models.GeneratedField(
db_persist=False,
expression=models.Func(
models.Func(models.F("details___ts"), function="FROM_UNIXTIME"),
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
),
output_field=models.DateTimeField(),
),
),
]

View File

@ -9,6 +9,7 @@ from django.db.models import (
Exists, Exists,
ExpressionWrapper, ExpressionWrapper,
F, F,
Func,
OuterRef, OuterRef,
Q, Q,
Subquery, Subquery,
@ -438,7 +439,7 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
return method( return method(
count__sum=Sum("count", filter=F("occurred")), count__sum=Sum("count", filter=F("occurred")),
instructor__count=Count("instructor", distinct=True, filter=F("occurred")), instructor__count=Count("instructor", distinct=True, filter=F("occurred")),
meeting_times__count__sum=Sum("meeting_times__count", filter=F("occurred")), meetings__sum=Sum("meetings", filter=F("occurred")),
duration__sum=Sum("duration", filter=F("occurred")), duration__sum=Sum("duration", filter=F("occurred")),
person_hours__sum=Sum("person_hours", filter=F("occurred")), person_hours__sum=Sum("person_hours", filter=F("occurred")),
event_count=Count("eid", filter=F("occurred")), event_count=Count("eid", filter=F("occurred")),
@ -448,32 +449,37 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
class EventExtManager(models.Manager["EventExt"]): class EventExtManager(models.Manager["EventExt"]):
def get_queryset(self) -> models.QuerySet["EventExt"]: def get_queryset(self) -> models.QuerySet["EventExt"]:
return EventExtQuerySet(self.model, using=self._db).annotate( return (
meeting_times__count=Subquery( super()
EventMeetingTime.objects.filter(event=OuterRef("pk")) .get_queryset()
.values("event__pk") .annotate(
.annotate(d=Count("pk")) meetings=Subquery(
.values("d")[:1], EventMeetingTime.objects.filter(event=OuterRef("pk"))
output_field=models.IntegerField(), .values("event__pk")
), .annotate(d=Count("pk"))
duration=Subquery( .values("d")[:1],
EventMeetingTime.objects.filter(event=OuterRef("pk")) output_field=models.IntegerField(),
.values("event__pk") ),
.annotate(d=Sum("duration")) duration=Subquery(
.values("d")[:1], EventMeetingTime.objects.filter(event=OuterRef("pk"))
output_field=models.DurationField(), .values("event__pk")
), .annotate(d=Sum("duration"))
person_hours=ExpressionWrapper( .values("d")[:1],
ExpressionWrapper(F("duration"), models.IntegerField()) * F("count"), output_field=models.DurationField(),
models.DurationField(), ),
), person_hours=ExpressionWrapper(
ExpressionWrapper(F("duration"), models.IntegerField())
* F("count"),
models.DurationField(),
),
)
) )
class EventExt(Event): class EventExt(Event):
"""Extension of `Event` to capture some fields not supported in MembershipWorks""" """Extension of `Event` to capture some fields not supported in MembershipWorks"""
objects = EventExtManager() objects = EventExtManager.from_queryset(EventExtQuerySet)()
instructor = models.ForeignKey( instructor = models.ForeignKey(
EventInstructor, on_delete=models.PROTECT, null=True, blank=True EventInstructor, on_delete=models.PROTECT, null=True, blank=True
@ -488,9 +494,19 @@ class EventExt(Event):
instructor_flat_rate = models.DecimalField( instructor_flat_rate = models.DecimalField(
max_digits=13, decimal_places=4, default=0 max_digits=13, decimal_places=4, default=0
) )
details = models.JSONField(null=True, blank=True)
details_timestamp = models.GeneratedField(
expression=Func(
Func(F("details___ts"), function="FROM_UNIXTIME"),
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
),
output_field=models.DateTimeField(),
db_persist=False,
)
class Meta: class Meta:
verbose_name = "event" verbose_name = "event"
ordering = ["-start"]
class EventMeetingTime(models.Model): class EventMeetingTime(models.Model):

View File

@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
from django.db import transaction from django.db import transaction
from django.db.models import QuerySet
from membershipworks.membershipworks_api import FieldType, MembershipWorks from membershipworks.membershipworks_api import FieldType, MembershipWorks
from membershipworks.models import ( from membershipworks.models import (
@ -99,6 +100,17 @@ def scrape_membershipworks(*args, **options):
scrape_transactions(membershipworks) scrape_transactions(membershipworks)
def scrape_event_details(queryset: QuerySet[EventExt]):
membershipworks = MembershipWorks()
membershipworks.login(
settings.MEMBERSHIPWORKS_USERNAME, settings.MEMBERSHIPWORKS_PASSWORD
)
for event in queryset:
event.details = membershipworks.get_event_by_eid(event.eid)
event.save()
def scrape_events(): def scrape_events():
membershipworks = MembershipWorks() membershipworks = MembershipWorks()
membershipworks.login( membershipworks.login(
@ -150,3 +162,10 @@ def scrape_events():
# if there is exactly one meeting time, it should match the event start/end # if there is exactly one meeting time, it should match the event start/end
elif meeting_times_count == 1: elif meeting_times_count == 1:
event_ext.meeting_times.update(start=event_ext.start, end=event_ext.end) event_ext.meeting_times.update(start=event_ext.start, end=event_ext.end)
# event has no details, or last retrieval was before the event happened
if event_ext.details is None or event_ext.details_timestamp < (
event_ext.end or event_ext.start
):
event_ext.details = membershipworks.get_event_by_eid(event.eid)
event_ext.save()

View File

@ -14,7 +14,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include "cmsmanage/components/download_table.dj.html" %} {% include "cmsmanage/components/download_table.dj.html" %}
{% render_table table %} {% render_table table %}
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if previous_month %} {% if previous_month %}

View File

@ -11,7 +11,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% include "cmsmanage/components/download_table.dj.html" %} {% include "cmsmanage/components/download_table.dj.html" %}
{% render_table table %} {% render_table table %}
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">
{% if previous_year %} {% if previous_year %}

View File

@ -15,6 +15,7 @@
class="wp-image-2319" /> class="wp-image-2319" />
</figure> </figure>
<!-- /wp:image --> <!-- /wp:image -->
<!-- wp:paragraph --> <!-- wp:paragraph -->
<p>Greetings Upper Valley Makers:</p> <p>Greetings Upper Valley Makers:</p>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->
@ -41,6 +42,7 @@
<strong>Tours:</strong> Want to see what the Claremont MakerSpace is all about? Tours are by appointment only. <strong>Tours:</strong> Want to see what the Claremont MakerSpace is all about? Tours are by appointment only.
</p> </p>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->
<!-- wp:paragraph --> <!-- wp:paragraph -->
<p> <p>
<a data-wpel-link="external" <a data-wpel-link="external"
@ -49,6 +51,7 @@
rel="noreferrer noopener external">Contact Us</a> to schedule your tour where you can learn about all the awesome tools that the CMS offers access to, as well as how membership, classes, and studio spaces work. rel="noreferrer noopener external">Contact Us</a> to schedule your tour where you can learn about all the awesome tools that the CMS offers access to, as well as how membership, classes, and studio spaces work.
</p> </p>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->
<!-- wp:separator {"className":"is-style-wide"} --> <!-- wp:separator {"className":"is-style-wide"} -->
<hr class="wp-block-separator has-alpha-channel-opacity is-style-wide" /> <hr class="wp-block-separator has-alpha-channel-opacity is-style-wide" />
<!-- /wp:separator --> <!-- /wp:separator -->
@ -62,6 +65,7 @@
<i>{{ section.blurb }}</i> <i>{{ section.blurb }}</i>
</h4> </h4>
<!-- /wp:heading --> <!-- /wp:heading -->
{% for event in section.events %} {% for event in section.events %}
{% with url="https://claremontmakerspace.org/events/#!event/register/"|add:event.url %} {% with url="https://claremontmakerspace.org/events/#!event/register/"|add:event.url %}
<!-- wp:group {"tagName":"section","layout":{"type":"constrained"}} --> <!-- wp:group {"tagName":"section","layout":{"type":"constrained"}} -->
@ -109,6 +113,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<!-- wp:paragraph --> <!-- wp:paragraph -->
<p style="clear: both;">Happy Makin!</p> <p style="clear: both;">Happy Makin!</p>
<!-- /wp:paragraph --> <!-- /wp:paragraph -->

View File

@ -133,7 +133,7 @@ class EventTable(tables.Table):
start = tables.DateColumn("N d, Y") start = tables.DateColumn("N d, Y")
duration = DurationColumn() duration = DurationColumn()
person_hours = DurationColumn() person_hours = DurationColumn()
meeting_times__count = tables.Column("Meetings") meetings = tables.Column()
class Meta: class Meta:
model = EventExt model = EventExt
@ -145,7 +145,7 @@ class EventTable(tables.Table):
"category", "category",
"count", "count",
"cap", "cap",
"meeting_times__count", "meetings",
"duration", "duration",
"person_hours", "person_hours",
) )
@ -161,7 +161,7 @@ class EventSummaryTable(tables.Table):
canceled_event_count = tables.Column("Canceled Events") canceled_event_count = tables.Column("Canceled Events")
count__sum = tables.Column("Tickets") count__sum = tables.Column("Tickets")
instructor__count = tables.Column("Unique Instructors") instructor__count = tables.Column("Unique Instructors")
meeting_times__count__sum = tables.Column("Meetings") meetings__sum = tables.Column("Meetings")
duration__sum = DurationColumn("Class Hours") duration__sum = DurationColumn("Class Hours")
person_hours__sum = DurationColumn("Person Hours") person_hours__sum = DurationColumn("Person Hours")
@ -178,6 +178,15 @@ class EventIndexReport(
export_formats = ("csv", "xlsx", "ods") export_formats = ("csv", "xlsx", "ods")
export_name = "mw_events_index" export_name = "mw_events_index"
def get_table_data(self):
return (
super()
.get_table_data()
.values(year=TruncYear("start"))
.summarize()
.order_by("year")
)
def get_table_kwargs(self): def get_table_kwargs(self):
year_column = tables.DateColumn( year_column = tables.DateColumn(
"Y", "Y",
@ -191,15 +200,6 @@ class EventIndexReport(
"extra_columns": (("year", year_column),), "extra_columns": (("year", year_column),),
} }
def get_dated_queryset(self, **lookup):
return (
super()
.get_dated_queryset(**lookup)
.values(year=TruncYear("start"))
.summarize()
.order_by("year")
)
class EventYearReport( class EventYearReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, YearArchiveView ExportMixin, SingleTableMixin, PermissionRequiredMixin, YearArchiveView
@ -212,6 +212,15 @@ class EventYearReport(
table_class = EventSummaryTable table_class = EventSummaryTable
export_formats = ("csv", "xlsx", "ods") export_formats = ("csv", "xlsx", "ods")
def get_table_data(self):
return (
super()
.get_table_data()
.values(month=TruncMonth("start"))
.summarize()
.order_by("month")
)
def get_export_filename(self, export_format): def get_export_filename(self, export_format):
return f"mw_events_{self.get_year()}.{export_format}" return f"mw_events_{self.get_year()}.{export_format}"
@ -228,25 +237,19 @@ class EventYearReport(
"extra_columns": (("month", month_column),), "extra_columns": (("month", month_column),),
} }
def get_dated_queryset(self, **lookup):
return (
super()
.get_dated_queryset(**lookup)
.values(month=TruncMonth("start"))
.summarize()
.order_by("month")
)
class EventMonthReport( class EventMonthReport(
ExportMixin, SingleTableMixin, PermissionRequiredMixin, MonthArchiveView ExportMixin, SingleTableMixin, PermissionRequiredMixin, MonthArchiveView
): ):
permission_required = "membershipworks.view_eventext" permission_required = "membershipworks.view_eventext"
queryset = EventExt.objects.select_related("category", "instructor").all() queryset = EventExt.objects.all()
date_field = "start" date_field = "start"
template_name = "membershipworks/event_month_report.dj.html" template_name = "membershipworks/event_month_report.dj.html"
table_class = EventTable table_class = EventTable
export_formats = ("csv", "xlsx", "ods") export_formats = ("csv", "xlsx", "ods")
def get_table_data(self):
return super().get_table_data().select_related("category", "instructor")
def get_export_filename(self, export_format): def get_export_filename(self, export_format):
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}"

View File

@ -24,7 +24,8 @@ class PaperworkDashboardFragment(dashboard.DashboardFragment):
) )
if self.request.user.is_superuser or ( if self.request.user.is_superuser or (
member is not None and Department.filter_by_shop_lead(member).exists() member is not None
and Department.objects.filter_by_shop_lead(member).exists()
): ):
links["Department Certifications"] = reverse( links["Department Certifications"] = reverse(
"paperwork:department_certifications" "paperwork:department_certifications"

159
pdm.lock generated
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:c0e5c80b47118152c5b0167a588d3d1d078b0f2a710b8fc97869052ca04e7874" content_hash = "sha256:16af371b3e9b9b16889ab6dc4e3bd5d84e0119a6804a6081ad64f86f45a2259f"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@ -459,6 +459,19 @@ files = [
{file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"}, {file = "django_extensions-3.2.3-py3-none-any.whl", hash = "sha256:9600b7562f79a92cbf1fde6403c04fee314608fefbb595502e34383ae8203401"},
] ]
[[package]]
name = "django-filter"
version = "23.5"
requires_python = ">=3.7"
summary = "Django-filter is a reusable Django application for allowing users to filter querysets dynamically."
dependencies = [
"Django>=3.2",
]
files = [
{file = "django-filter-23.5.tar.gz", hash = "sha256:67583aa43b91fe8c49f74a832d95f4d8442be628fd4c6d65e9f811f5153a4e5c"},
{file = "django_filter-23.5-py3-none-any.whl", hash = "sha256:99122a201d83860aef4fe77758b69dda913e874cc5e0eaa50a86b0b18d708400"},
]
[[package]] [[package]]
name = "django-markdownx" name = "django-markdownx"
version = "4.0.7" version = "4.0.7"
@ -881,7 +894,7 @@ files = [
[[package]] [[package]]
name = "ipython" name = "ipython"
version = "8.19.0" version = "8.20.0"
requires_python = ">=3.10" requires_python = ">=3.10"
summary = "IPython: Productive Interactive Computing" summary = "IPython: Productive Interactive Computing"
dependencies = [ dependencies = [
@ -896,8 +909,8 @@ dependencies = [
"traitlets>=5", "traitlets>=5",
] ]
files = [ files = [
{file = "ipython-8.19.0-py3-none-any.whl", hash = "sha256:2f55d59370f59d0d2b2212109fe0e6035cfea436b1c0e6150ad2244746272ec5"}, {file = "ipython-8.20.0-py3-none-any.whl", hash = "sha256:bc9716aad6f29f36c449e30821c9dd0c1c1a7b59ddcc26931685b87b4c569619"},
{file = "ipython-8.19.0.tar.gz", hash = "sha256:ac4da4ecf0042fb4e0ce57c60430c2db3c719fa8bdf92f8631d6bd8a5785d1f0"}, {file = "ipython-8.20.0.tar.gz", hash = "sha256:2f21bd3fc1d51550c89ee3944ae04bbc7bc79e129ea0937da6e6c68bfdbf117a"},
] ]
[[package]] [[package]]
@ -936,50 +949,52 @@ files = [
[[package]] [[package]]
name = "lxml" name = "lxml"
version = "5.0.0" version = "5.1.0"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" 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.0.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5382612ba2424cea5d2c89e2c29077023d8de88f8d60d5ceff5f76334516df9e"}, {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:14deca1460b4b0f6b01f1ddc9557704e8b365f55c63070463f6c18619ebf964f"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:07a900735bad9af7be3085480bf384f68ed5580ba465b39a098e6a882c060d6b"}, {file = "lxml-5.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ed8c3d2cd329bf779b7ed38db176738f3f8be637bb395ce9629fc76f78afe3d4"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:980ba47c8db4b9d870014c7040edb230825b79017a6a27aa54cdb6fcc02d8cc0"}, {file = "lxml-5.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:436a943c2900bb98123b06437cdd30580a61340fbdb7b28aaf345a459c19046a"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6507c58431dbd95b50654b3313c5ad54f90e54e5f2cdacf733de61eae478eec5"}, {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.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:4a45a278518e4308865c1e9dbb2c42ce84fb154efb03adeb16fdae3c1687c7c9"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:af8920ce4a55ff41167ddbc20077f5698c2e710ad3353d32a07d3264f3a2021e"},
{file = "lxml-5.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:59cea9ba1c675fbd6867ca1078fc717a113e7f5b7644943b74137b7cc55abebf"}, {file = "lxml-5.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7cfced4a069003d8913408e10ca8ed092c49a7f6cefee9bb74b6b3e860683b45"},
{file = "lxml-5.0.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:dd39ef87fd1f7bb5c4aa53454936e6135cbfe03fe3744e8218be193f9e4fef16"}, {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9e5ac3437746189a9b4121db2a7b86056ac8786b12e88838696899328fc44bb2"},
{file = "lxml-5.0.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e6bb39d91bf932e7520cb5718ae3c2f498052aca53294d5d59fdd9068fe1a7f2"}, {file = "lxml-5.1.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f4c9bda132ad108b387c33fabfea47866af87f4ea6ffb79418004f0521e63204"},
{file = "lxml-5.0.0-cp311-cp311-win32.whl", hash = "sha256:21af2c3862db6f4f486cddf73ec1157b40d5828876c47cd880edcbad8240ea1b"}, {file = "lxml-5.1.0-cp311-cp311-win32.whl", hash = "sha256:bc64d1b1dab08f679fb89c368f4c05693f58a9faf744c4d390d7ed1d8223869b"},
{file = "lxml-5.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:c1249aa4eaced30b59ecf8b8cae0b1ccede04583c74ca7d10b6f8bbead908b2c"}, {file = "lxml-5.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5ab722ae5a873d8dcee1f5f45ddd93c34210aed44ff2dc643b5025981908cda"},
{file = "lxml-5.0.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f30e697b6215e759d0824768b2c5b0618d2dc19abe6c67eeed2b0460f52470d1"}, {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9aa543980ab1fbf1720969af1d99095a548ea42e00361e727c58a40832439114"},
{file = "lxml-5.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d1bb64646480c36a4aa1b6a44a5b6e33d0fcbeab9f53f1b39072cd3bb2c6243a"}, {file = "lxml-5.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6f11b77ec0979f7e4dc5ae081325a2946f1fe424148d3945f943ceaede98adb8"},
{file = "lxml-5.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4e69c36c8618707a90ed3fb6f48a6cc9254ffcdbf7b259e439a5ae5fbf9c5206"}, {file = "lxml-5.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a36c506e5f8aeb40680491d39ed94670487ce6614b9d27cabe45d94cd5d63e1e"},
{file = "lxml-5.0.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9ca498f8554a09fbc3a2f8fc4b23261e07bc27bef99b3df98e2570688033f6fc"}, {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.0.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0326e9b8176ea77269fb39e7af4010906e73e9496a9f8eaf06d253b1b1231ceb"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16dd953fb719f0ffc5bc067428fc9e88f599e15723a85618c45847c96f11f431"},
{file = "lxml-5.0.0-cp312-cp312-win32.whl", hash = "sha256:5fb988e15378d6e905ca8f60813950a0c56da9469d0e8e5d8fe785b282684ec5"}, {file = "lxml-5.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:16018f7099245157564d7148165132c70adb272fb5a17c048ba70d9cc542a1a1"},
{file = "lxml-5.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:bb58e8f4b2cfe012cd312239b8d5139995fe8f5945c7c26d5fbbbb1ddb9acd47"}, {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:82cd34f1081ae4ea2ede3d52f71b7be313756e99b4b5f829f89b12da552d3aa3"},
{file = "lxml-5.0.0-pp310-pypy310_pp73-macosx_11_0_x86_64.whl", hash = "sha256:96095bfc0c02072fc89afa67626013a253596ea5118b8a7f4daaae049dafa096"}, {file = "lxml-5.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:19a1bc898ae9f06bccb7c3e1dfd73897ecbbd2c96afe9095a6026016e5ca97b8"},
{file = "lxml-5.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:992029258ed719f130d5a9c443d142c32843046f1263f2c492862b2a853be570"}, {file = "lxml-5.1.0-cp312-cp312-win32.whl", hash = "sha256:13521a321a25c641b9ea127ef478b580b5ec82aa2e9fc076c86169d161798b01"},
{file = "lxml-5.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:db40e85cffd22f7d65dcce30e85af565a66401a6ed22fc0c56ed342cfa4ffc43"}, {file = "lxml-5.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:1ad17c20e3666c035db502c78b86e58ff6b5991906e55bdbef94977700c72623"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-macosx_11_0_x86_64.whl", hash = "sha256:cfa8a4cdc3765574b7fd0c7cfa5fbd1e2108014c9dfd299c679e5152bea9a55e"}, {file = "lxml-5.1.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9bd0ae7cc2b85320abd5e0abad5ccee5564ed5f0cc90245d2f9a8ef330a8deae"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:049fef98d02513c34f5babd07569fc1cf1ed14c0f2fbff18fe72597f977ef3c2"}, {file = "lxml-5.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c1d679df4361408b628f42b26a5d62bd3e9ba7f0c0e7969f925021554755aa"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:a85136d0ee18a41c91cc3e2844c683be0e72e6dda4cb58da9e15fcaef3726af7"}, {file = "lxml-5.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2ad3a8ce9e8a767131061a22cd28fdffa3cd2dc193f399ff7b81777f3520e372"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:766868f729f3ab84125350f1a0ea2594d8b1628a608a574542a5aff7355b9941"}, {file = "lxml-5.1.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:304128394c9c22b6569eba2a6d98392b56fbdfbad58f83ea702530be80d0f9df"},
{file = "lxml-5.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:99cad5c912f359e59e921689c04e54662cdd80835d80eeaa931e22612f515df7"}, {file = "lxml-5.1.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d74fcaf87132ffc0447b3c685a9f862ffb5b43e70ea6beec2fb8057d5d2a1fea"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-macosx_11_0_x86_64.whl", hash = "sha256:c90c593aa8dd57d5dab0ef6d7d64af894008971d98e6a41b320fdd75258fbc6e"}, {file = "lxml-5.1.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:8cf5877f7ed384dabfdcc37922c3191bf27e55b498fecece9fd5c2c7aaa34c33"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:8134d5441d1ed6a682e3de3d7a98717a328dce619ee9c4c8b3b91f0cb0eb3e28"}, {file = "lxml-5.1.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:877efb968c3d7eb2dad540b6cabf2f1d3c0fbf4b2d309a3c141f79c7e0061324"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f298ac9149037d6a3d5c74991bded39ac46292520b9c7c182cb102486cc87677"}, {file = "lxml-5.1.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f14a4fb1c1c402a22e6a341a24c1341b4a3def81b41cd354386dcb795f83897"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:894c5f71186b410679aaab5774543fcb9cbabe8893f0b31d11cf28a0740e80be"}, {file = "lxml-5.1.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:25663d6e99659544ee8fe1b89b1a8c0aaa5e34b103fab124b17fa958c4a324a6"},
{file = "lxml-5.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:9cd3d6c2c67d4fdcd795e4945e2ba5434909c96640b4cc09453bd0dc7e8e1bac"}, {file = "lxml-5.1.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:8b9f19df998761babaa7f09e6bc169294eefafd6149aaa272081cbddc7ba4ca3"},
{file = "lxml-5.0.0.zip", hash = "sha256:2219cbf790e701acf9a21a31ead75f983e73daf0eceb9da6990212e4d20ebefe"}, {file = "lxml-5.1.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e53d7e6a98b64fe54775d23a7c669763451340c3d44ad5e3a3b48a1efbdc96f"},
{file = "lxml-5.1.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c3cd1fc1dc7c376c54440aeaaa0dcc803d2126732ff5c6b68ccd619f2e64be4f"},
{file = "lxml-5.1.0.tar.gz", hash = "sha256:3eea6ed6e6c918e468e693c41ef07f3c3acc310b70ddd9cc72d9ef84bc9564ca"},
] ]
[[package]] [[package]]
name = "lxml-stubs" name = "lxml-stubs"
version = "0.4.0" version = "0.5.1"
summary = "Type annotations for the lxml package" summary = "Type annotations for the lxml package"
files = [ files = [
{file = "lxml-stubs-0.4.0.tar.gz", hash = "sha256:184877b42127256abc2b932ba8bd0ab5ea80bd0b0fee618d16daa40e0b71abee"}, {file = "lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d"},
{file = "lxml_stubs-0.4.0-py3-none-any.whl", hash = "sha256:3b381e9e82397c64ea3cc4d6f79d1255d015f7b114806d4826218805c10ec003"}, {file = "lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272"},
] ]
[[package]] [[package]]
@ -1577,27 +1592,27 @@ files = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.1.13" version = "0.1.14"
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.1.13-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:e3fd36e0d48aeac672aa850045e784673449ce619afc12823ea7868fcc41d8ba"}, {file = "ruff-0.1.14-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:96f76536df9b26622755c12ed8680f159817be2f725c17ed9305b472a757cdbb"},
{file = "ruff-0.1.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9fb6b3b86450d4ec6a6732f9f60c4406061b6851c4b29f944f8c9d91c3611c7a"}, {file = "ruff-0.1.14-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ab3f71f64498c7241123bb5a768544cf42821d2a537f894b22457a543d3ca7a9"},
{file = "ruff-0.1.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b13ba5d7156daaf3fd08b6b993360a96060500aca7e307d95ecbc5bb47a69296"}, {file = "ruff-0.1.14-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7060156ecc572b8f984fd20fd8b0fcb692dd5d837b7606e968334ab7ff0090ab"},
{file = "ruff-0.1.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9ebb40442f7b531e136d334ef0851412410061e65d61ca8ce90d894a094feb22"}, {file = "ruff-0.1.14-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a53d8e35313d7b67eb3db15a66c08434809107659226a90dcd7acb2afa55faea"},
{file = "ruff-0.1.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:226b517f42d59a543d6383cfe03cccf0091e3e0ed1b856c6824be03d2a75d3b6"}, {file = "ruff-0.1.14-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bea9be712b8f5b4ebed40e1949379cfb2a7d907f42921cf9ab3aae07e6fba9eb"},
{file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5f0312ba1061e9b8c724e9a702d3c8621e3c6e6c2c9bd862550ab2951ac75c16"}, {file = "ruff-0.1.14-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2270504d629a0b064247983cbc495bed277f372fb9eaba41e5cf51f7ba705a6a"},
{file = "ruff-0.1.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2f59bcf5217c661254bd6bc42d65a6fd1a8b80c48763cb5c2293295babd945dd"}, {file = "ruff-0.1.14-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80258bb3b8909b1700610dfabef7876423eed1bc930fe177c71c414921898efa"},
{file = "ruff-0.1.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e6894b00495e00c27b6ba61af1fc666f17de6140345e5ef27dd6e08fb987259d"}, {file = "ruff-0.1.14-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:653230dd00aaf449eb5ff25d10a6e03bc3006813e2cb99799e568f55482e5cae"},
{file = "ruff-0.1.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1600942485c6e66119da294c6294856b5c86fd6df591ce293e4a4cc8e72989"}, {file = "ruff-0.1.14-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b3acc6c4e6928459ba9eb7459dd4f0c4bf266a053c863d72a44c33246bfdbf"},
{file = "ruff-0.1.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ee3febce7863e231a467f90e681d3d89210b900d49ce88723ce052c8761be8c7"}, {file = "ruff-0.1.14-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6b3dadc9522d0eccc060699a9816e8127b27addbb4697fc0c08611e4e6aeb8b5"},
{file = "ruff-0.1.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:dcaab50e278ff497ee4d1fe69b29ca0a9a47cd954bb17963628fa417933c6eb1"}, {file = "ruff-0.1.14-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1c8eca1a47b4150dc0fbec7fe68fc91c695aed798532a18dbb1424e61e9b721f"},
{file = "ruff-0.1.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f57de973de4edef3ad3044d6a50c02ad9fc2dff0d88587f25f1a48e3f72edf5e"}, {file = "ruff-0.1.14-py3-none-musllinux_1_2_i686.whl", hash = "sha256:62ce2ae46303ee896fc6811f63d6dabf8d9c389da0f3e3f2bce8bc7f15ef5488"},
{file = "ruff-0.1.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7a36fa90eb12208272a858475ec43ac811ac37e91ef868759770b71bdabe27b6"}, {file = "ruff-0.1.14-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b2027dde79d217b211d725fc833e8965dc90a16d0d3213f1298f97465956661b"},
{file = "ruff-0.1.13-py3-none-win32.whl", hash = "sha256:a623349a505ff768dad6bd57087e2461be8db58305ebd5577bd0e98631f9ae69"}, {file = "ruff-0.1.14-py3-none-win32.whl", hash = "sha256:722bafc299145575a63bbd6b5069cb643eaa62546a5b6398f82b3e4403329cab"},
{file = "ruff-0.1.13-py3-none-win_amd64.whl", hash = "sha256:f988746e3c3982bea7f824c8fa318ce7f538c4dfefec99cd09c8770bd33e6539"}, {file = "ruff-0.1.14-py3-none-win_amd64.whl", hash = "sha256:e3d241aa61f92b0805a7082bd89a9990826448e4d0398f0e2bc8f05c75c63d99"},
{file = "ruff-0.1.13-py3-none-win_arm64.whl", hash = "sha256:6bbbc3042075871ec17f28864808540a26f0f79a4478c357d3e3d2284e832998"}, {file = "ruff-0.1.14-py3-none-win_arm64.whl", hash = "sha256:269302b31ade4cde6cf6f9dd58ea593773a37ed3f7b97e793c8594b262466b67"},
{file = "ruff-0.1.13.tar.gz", hash = "sha256:e261f1baed6291f434ffb1d5c6bd8051d1c2a26958072d38dfbec39b3dda7352"}, {file = "ruff-0.1.14.tar.gz", hash = "sha256:ad3f8088b2dfd884820289a06ab718cde7d38b94972212cc4ba90d5fbc9955f3"},
] ]
[[package]] [[package]]
@ -1766,15 +1781,15 @@ files = [
[[package]] [[package]]
name = "types-requests" name = "types-requests"
version = "2.31.0.20231231" version = "2.31.0.20240125"
requires_python = ">=3.7" 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.20231231.tar.gz", hash = "sha256:0f8c0c9764773384122813548d9eea92a5c4e1f33ed54556b508968ec5065cee"}, {file = "types-requests-2.31.0.20240125.tar.gz", hash = "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5"},
{file = "types_requests-2.31.0.20231231-py3-none-any.whl", hash = "sha256:2e2230c7bc8dd63fa3153c1c0ae335f8a368447f0582fc332f17d54f88e69027"}, {file = "types_requests-2.31.0.20240125-py3-none-any.whl", hash = "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1"},
] ]
[[package]] [[package]]
@ -1808,7 +1823,7 @@ files = [
[[package]] [[package]]
name = "udm-rest-client" name = "udm-rest-client"
version = "1.2.2" version = "1.2.3"
requires_python = ">=3.6" requires_python = ">=3.6"
summary = "Python library to interact with the Univention UDM REST API. Implements the simple Python UDM API." summary = "Python library to interact with the Univention UDM REST API. Implements the simple Python UDM API."
dependencies = [ dependencies = [
@ -1819,8 +1834,8 @@ dependencies = [
"requests<3,>=2.26", "requests<3,>=2.26",
] ]
files = [ files = [
{file = "udm-rest-client-1.2.2.tar.gz", hash = "sha256:219fc5646cd33f158cc595579f58fdcb7de085a8bae44a0f83481a607202d1c0"}, {file = "udm-rest-client-1.2.3.tar.gz", hash = "sha256:80724cafa3498d23849124f27f965bc56f10f1dd09b8c2e61e89670c40931a5e"},
{file = "udm_rest_client-1.2.2-py2.py3-none-any.whl", hash = "sha256:add1d551b89161a8d06663b8fe92385747e1d780f67d2c882b7a023c1bfc7966"}, {file = "udm_rest_client-1.2.3-py2.py3-none-any.whl", hash = "sha256:ee29e94e3ba5fba63a694e33d119b1af7450afcdce3a44301d4cd5ddfa1f980b"},
] ]
[[package]] [[package]]
@ -1835,7 +1850,7 @@ files = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.25.0" version = "0.27.0"
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "The lightning-fast ASGI server." summary = "The lightning-fast ASGI server."
dependencies = [ dependencies = [
@ -1843,13 +1858,13 @@ dependencies = [
"h11>=0.8", "h11>=0.8",
] ]
files = [ files = [
{file = "uvicorn-0.25.0-py3-none-any.whl", hash = "sha256:ce107f5d9bd02b4636001a77a4e74aab5e1e2b146868ebbad565237145af444c"}, {file = "uvicorn-0.27.0-py3-none-any.whl", hash = "sha256:890b00f6c537d58695d3bb1f28e23db9d9e7a17cbcc76d7457c499935f933e24"},
{file = "uvicorn-0.25.0.tar.gz", hash = "sha256:6dddbad1d7ee0f5140aba5ec138ddc9612c5109399903828b4874c9937f009c2"}, {file = "uvicorn-0.27.0.tar.gz", hash = "sha256:c855578045d45625fd027367f7653d249f7c49f9361ba15cf9624186b26b8eb6"},
] ]
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.25.0" version = "0.27.0"
extras = ["standard"] extras = ["standard"]
requires_python = ">=3.8" requires_python = ">=3.8"
summary = "The lightning-fast ASGI server." summary = "The lightning-fast ASGI server."
@ -1858,14 +1873,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.25.0", "uvicorn==0.27.0",
"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.25.0-py3-none-any.whl", hash = "sha256:ce107f5d9bd02b4636001a77a4e74aab5e1e2b146868ebbad565237145af444c"}, {file = "uvicorn-0.27.0-py3-none-any.whl", hash = "sha256:890b00f6c537d58695d3bb1f28e23db9d9e7a17cbcc76d7457c499935f933e24"},
{file = "uvicorn-0.25.0.tar.gz", hash = "sha256:6dddbad1d7ee0f5140aba5ec138ddc9612c5109399903828b4874c9937f009c2"}, {file = "uvicorn-0.27.0.tar.gz", hash = "sha256:c855578045d45625fd027367f7653d249f7c49f9361ba15cf9624186b26b8eb6"},
] ]
[[package]] [[package]]

View File

@ -23,7 +23,7 @@ dependencies = [
"semver~=3.0", "semver~=3.0",
"djangorestframework~=3.14", "djangorestframework~=3.14",
"django-q2~=1.6", "django-q2~=1.6",
"lxml~=5.0", "lxml~=5.1",
"django-object-actions~=4.2", "django-object-actions~=4.2",
"udm-rest-client~=1.2", "udm-rest-client~=1.2",
"openapi-client-udm~=1.0", "openapi-client-udm~=1.0",
@ -31,12 +31,13 @@ dependencies = [
"nh3~=0.2", "nh3~=0.2",
"django-tables2~=2.7", "django-tables2~=2.7",
"tablib[ods,xlsx]~=3.5", "tablib[ods,xlsx]~=3.5",
"django-filter~=23.5",
] ]
requires-python = ">=3.11" requires-python = ">=3.11"
[project.optional-dependencies] [project.optional-dependencies]
server = [ server = [
"uvicorn[standard]~=0.25", "uvicorn[standard]~=0.27",
"setuptools~=69.0", "setuptools~=69.0",
] ]
@ -67,6 +68,7 @@ profile="django"
extension = ".dj.html" extension = ".dj.html"
indent = 2 indent = 2
blank_line_after_tag = "load,extends" blank_line_after_tag = "load,extends"
max_blank_lines = 1
ignore = "T003,H017,H021,H030,H031" ignore = "T003,H017,H021,H030,H031"
format_css = true format_css = true
format_js = true format_js = true
@ -111,14 +113,14 @@ typing = [
"types-requests~=2.31", "types-requests~=2.31",
"types-urllib3~=1.26", "types-urllib3~=1.26",
"djangorestframework-stubs[compatible-mypy]~=3.14", "djangorestframework-stubs[compatible-mypy]~=3.14",
"lxml-stubs~=0.4", "lxml-stubs~=0.5",
] ]
debug = [ debug = [
"django-debug-toolbar~=4.2", "django-debug-toolbar~=4.2",
] ]
dev = [ dev = [
"django-extensions~=3.2", "django-extensions~=3.2",
"ipython~=8.19", "ipython~=8.20",
] ]
[tool.pdm.scripts] [tool.pdm.scripts]