Fix various type issues

This commit is contained in:
Adam Goldsmith 2024-05-04 16:38:51 -04:00
parent 9658366d72
commit 0944dd7992
15 changed files with 264 additions and 92 deletions

View File

@ -220,7 +220,7 @@ class HIDEvent(models.Model):
) )
@classmethod @classmethod
def from_xml_attributes(cls, door: Door, attrib: dict[str:str]): def from_xml_attributes(cls, door: Door, attrib: dict[str, str]):
field_lookup = { field_lookup = {
field.column: field.attname for field in HIDEvent._meta.get_fields() field.column: field.attname for field in HIDEvent._meta.get_fields()
} }
@ -287,7 +287,7 @@ class HIDEvent(models.Model):
def __str__(self): def __str__(self):
return f"{self.door.name} {self.timestamp} - {self.description}" return f"{self.door.name} {self.timestamp} - {self.description}"
def decoded_card_number(self) -> str: def decoded_card_number(self) -> str | None:
"""Requires annotations from `with_decoded_card_number`""" """Requires annotations from `with_decoded_card_number`"""
if self.raw_card_number is None: if self.raw_card_number is None:
return None return None

View File

@ -1,5 +1,6 @@
import dataclasses import dataclasses
import logging import logging
from typing import TypedDict
from django_q.tasks import async_task from django_q.tasks import async_task
@ -11,10 +12,20 @@ from membershipworks.models import Member
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CardholderAttribs(TypedDict):
forename: str
middleName: str
surname: str
email: str
phone: str
custom1: str
custom2: str
@dataclasses.dataclass @dataclasses.dataclass
class DoorMember: class DoorMember:
door: Door door: Door
attribs: dict[str, str] attribs: CardholderAttribs
credentials: set[Credential] credentials: set[Credential]
schedules: set[str] schedules: set[str]
cardholderID: str | None = None cardholderID: str | None = None
@ -33,7 +44,7 @@ class DoorMember:
else: else:
credentials = set() credentials = set()
reasons_and_schedules = {} reasons_and_schedules: dict[str, str] = {}
if ( if (
member.is_active member.is_active
or member.flags.filter(name="Misc. Access", type="folder").exists() or member.flags.filter(name="Misc. Access", type="folder").exists()
@ -112,6 +123,9 @@ class DoorMember:
all_members: list["DoorMember"], all_members: list["DoorMember"],
old_credentials: set[Credential] = set(), old_credentials: set[Credential] = set(),
): ):
# cardholderID should be set on a member before this is called
assert self.cardholderID is not None
other_assigned_cards = { other_assigned_cards = {
card for m in all_members if m != self for card in m.credentials card for m in all_members if m != self for card in m.credentials
} }
@ -187,7 +201,7 @@ def update_door(door: Door, dry_run: bool = False):
cardholders = { cardholders = {
member.membershipworks_id: member member.membershipworks_id: member
for member in [ for member in [
DoorMember.from_cardholder(ch, door.controller) DoorMember.from_cardholder(ch, door)
for ch in door.controller.get_cardholders() for ch in door.controller.get_cardholders()
] ]
} }

View File

@ -12,7 +12,7 @@ from django.views.generic.list import ListView
import django_filters import django_filters
import django_tables2 as tables import django_tables2 as tables
from django_filters.views import BaseFilterView from django_filters.views import BaseFilterView
from django_mysql.models import GroupConcat from django_mysql.models.aggregates import GroupConcat
from django_mysql.models.functions import ConcatWS from django_mysql.models.functions import ConcatWS
from django_tables2 import SingleTableMixin from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
@ -30,7 +30,7 @@ from .tables import (
REPORTS = [] REPORTS = []
def register_report(cls: "BaseAccessReport"): def register_report(cls: "type[BaseAccessReport]"):
REPORTS.append(cls) REPORTS.append(cls)
return cls return cls
@ -55,7 +55,7 @@ class BaseAccessReport(
filterset_class = AccessReportFilterSet filterset_class = AccessReportFilterSet
_report_name = None _report_name: str
@classmethod @classmethod
def _report_types(cls): def _report_types(cls):
@ -76,9 +76,9 @@ class BaseAccessReport(
def _selected_report(self): def _selected_report(self):
return self._report_name return self._report_name
def get_paginate_by(self, queryset) -> int: def get_paginate_by(self, queryset) -> int | None:
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["items_per_page"])
return super().get_paginate_by(queryset) return super().get_paginate_by(queryset)
def get_queryset(self): def get_queryset(self):

View File

@ -28,6 +28,8 @@ def make_multipart_email(
def make_instructor_email( def make_instructor_email(
invoice: EventInvoice, pdf: bytes, event_url: str invoice: EventInvoice, pdf: bytes, event_url: str
) -> EmailMessage: ) -> EmailMessage:
if invoice.event.instructor is None or invoice.event.instructor.member is None:
raise ValueError("Event Instructor not defined or is not member")
template = loader.get_template( template = loader.get_template(
"membershipworks/email/event_invoice_instructor.dj.html" "membershipworks/email/event_invoice_instructor.dj.html"
) )

View File

@ -2,6 +2,7 @@ import csv
import datetime import datetime
from enum import Enum from enum import Enum
from io import StringIO from io import StringIO
from typing import Any
import requests import requests
@ -67,6 +68,13 @@ staticFlags = {
} }
class NotAuthenticatedError(Exception):
def __init__(self) -> None:
super().__init__(
"Not authenticated to membershipworks, please call .login() first"
)
class MembershipWorksRemoteError(Exception): class MembershipWorksRemoteError(Exception):
def __init__(self, reason, r): def __init__(self, reason, r):
super().__init__( super().__init__(
@ -104,7 +112,7 @@ class MembershipWorks:
def _inject_auth(self, kwargs): def _inject_auth(self, kwargs):
# TODO: should probably be a decorator or something # TODO: should probably be a decorator or something
if self.auth_token is None: if self.auth_token is None:
raise RuntimeError("Not Logged in to MembershipWorks") raise NotAuthenticatedError()
# add auth token to params # add auth token to params
if "params" not in kwargs: if "params" not in kwargs:
kwargs["params"] = {} kwargs["params"] = {}
@ -126,6 +134,8 @@ class MembershipWorks:
Is this terrible? Yes. Also, not dissimilar to how MW does it Is this terrible? Yes. Also, not dissimilar to how MW does it
in all.js. in all.js.
""" """
if not self.org_info:
raise NotAuthenticatedError()
fields = staticFlags.copy() fields = staticFlags.copy()
# TODO: this will take the later option, if the same field # TODO: this will take the later option, if the same field
@ -148,7 +158,9 @@ class MembershipWorks:
This is terrible, and there might be a better way to do this. This is terrible, and there might be a better way to do this.
""" """
ret = {"folders": {}, "levels": {}, "addons": {}, "labels": {}} if not self.org_info:
raise NotAuthenticatedError()
ret: dict[str, Any] = {"folders": {}, "levels": {}, "addons": {}, "labels": {}}
for dek in self.org_info["dek"]: for dek in self.org_info["dek"]:
# TODO: there must be a better way. this is stupid # TODO: there must be a better way. this is stupid
@ -242,8 +254,8 @@ class MembershipWorks:
def get_events_list( def get_events_list(
self, self,
start_date: datetime.datetime = None, start_date: datetime.datetime | None = None,
end_date: datetime.datetime = None, end_date: datetime.datetime | None = None,
categories=False, categories=False,
): ):
"""Retrive a list of events between `start_date` and `end_date`, optionally including category information""" """Retrive a list of events between `start_date` and `end_date`, optionally including category information"""

View File

@ -1,6 +1,7 @@
import uuid import uuid
from datetime import datetime from datetime import datetime, timedelta
from typing import Optional from decimal import Decimal
from typing import TYPE_CHECKING, TypedDict
import django.core.mail.message import django.core.mail.message
from django.conf import settings from django.conf import settings
@ -15,6 +16,7 @@ from django.db.models import (
Func, Func,
OuterRef, OuterRef,
Q, Q,
QuerySet,
Subquery, Subquery,
Sum, Sum,
Value, Value,
@ -26,12 +28,13 @@ from django.utils import timezone
import nh3 import nh3
from django_db_views.db_view import DBView from django_db_views.db_view import DBView
from django_stubs_ext import WithAnnotations
class BaseModel(models.Model): class BaseModel(models.Model):
_api_names_override = {} _api_names_override: dict[str, str] = {}
_date_fields = {} _date_fields: dict[str, str | None] = {}
_allowed_missing_fields = [] _allowed_missing_fields: list[str] = []
class Meta: class Meta:
abstract = True abstract = True
@ -274,9 +277,10 @@ class Member(BaseModel):
] ]
@classmethod @classmethod
def from_user(cls, user) -> Optional["Member"]: def from_user(cls, user) -> "Member | None":
if hasattr(user, "ldap_user"): if hasattr(user, "ldap_user"):
return cls.objects.get(uid=user.ldap_user.attrs["employeeNumber"][0]) return cls.objects.get(uid=user.ldap_user.attrs["employeeNumber"][0])
return None
def sanitized_mailbox(self, use_volunteer=False) -> str: def sanitized_mailbox(self, use_volunteer=False) -> str:
if use_volunteer and self.volunteer_email: if use_volunteer and self.volunteer_email:
@ -447,7 +451,7 @@ class EventInstructor(models.Model):
return str(self.member) if self.member else self.name return str(self.member) if self.member else self.name
class EventExtQuerySet(models.QuerySet["EventExt"]): class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]):
def summarize(self, aggregate: bool = False): def summarize(self, aggregate: bool = False):
method = self.aggregate if aggregate else self.annotate method = self.aggregate if aggregate else self.annotate
return method( return method(
@ -465,7 +469,7 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
net_revenue__sum=Sum("net_revenue", filter=F("occurred")), net_revenue__sum=Sum("net_revenue", filter=F("occurred")),
) )
def with_financials(self): def with_financials(self) -> "QuerySet[EventExtAnnotatedWithFinancials]":
return self.annotate( return self.annotate(
**{ **{
field: Subquery( field: Subquery(
@ -495,40 +499,35 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
) )
class EventExtManager(models.Manager["EventExt"]): class EventExtManager(models.Manager):
def get_queryset(self) -> models.QuerySet["EventExt"]: def get_queryset(self) -> EventExtQuerySet:
return ( return EventExtQuerySet(self.model, using=self._db).annotate(
super() meetings=Subquery(
.get_queryset() EventMeetingTime.objects.filter(event=OuterRef("pk"))
.annotate( .values("event__pk")
meetings=Subquery( .annotate(d=Count("pk"))
EventMeetingTime.objects.filter(event=OuterRef("pk")) .values("d")[:1],
.values("event__pk") output_field=models.IntegerField(),
.annotate(d=Count("pk")) ),
.values("d")[:1], duration=Subquery(
output_field=models.IntegerField(), EventMeetingTime.objects.filter(event=OuterRef("pk"))
), .values("event__pk")
duration=Subquery( .annotate(d=Sum("duration"))
EventMeetingTime.objects.filter(event=OuterRef("pk")) .values("d")[:1],
.values("event__pk") output_field=models.DurationField(),
.annotate(d=Sum("duration")) ),
.values("d")[:1], person_hours=ExpressionWrapper(
output_field=models.DurationField(), ExpressionWrapper(F("duration"), models.IntegerField()) * F("count"),
), models.DurationField(),
person_hours=ExpressionWrapper( ),
ExpressionWrapper(F("duration"), models.IntegerField()) # TODO: this could be a GeneratedField, but that
* F("count"), # currently breaks saving when the primary key is
models.DurationField(), # provided (Django 5.0.1)
), details_timestamp=Func(
# TODO: this could be a GeneratedField, but that Func(F("details___ts"), function="FROM_UNIXTIME"),
# currently breaks saving when the primary key is template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
# provided (Django 5.0.1) output_field=models.DateTimeField(),
details_timestamp=Func( ),
Func(F("details___ts"), function="FROM_UNIXTIME"),
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
output_field=models.DateTimeField(),
),
)
) )
@ -574,7 +573,7 @@ class EventExt(Event):
self.materials_fee_included_in_price is not None self.materials_fee_included_in_price is not None
or self.materials_fee == 0 or self.materials_fee == 0
) )
and self.total_due_to_instructor is not None and getattr(self, "total_due_to_instructor") is not None
) )
class Meta: class Meta:
@ -582,6 +581,32 @@ class EventExt(Event):
ordering = ["-start"] ordering = ["-start"]
if TYPE_CHECKING:
class EventExtAnnotations(TypedDict):
meetings: int
duration: timedelta
person_hours: timedelta
details_timestamp: datetime
class EventExtFinancialAnnotations(TypedDict):
quantity: Decimal
amount: Decimal
materials: Decimal
amount_without_materials: Decimal
instructor_revenue: Decimal
instructor_amount: Decimal
total_due_to_instructor: Decimal
gross_revenue: Decimal
net_revenue: Decimal
EventExtAnnotated = WithAnnotations[EventExt, EventExtAnnotations]
EventExtAnnotatedWithFinancials = WithAnnotations[EventExt, EventExtAnnotations]
else:
EventExtAnnotated = WithAnnotations[EventExt]
EventExtAnnotatedWithFinancials = WithAnnotations[EventExt]
class EventMeetingTime(models.Model): class EventMeetingTime(models.Model):
event = models.ForeignKey( event = models.ForeignKey(
EventExt, on_delete=models.CASCADE, related_name="meeting_times" EventExt, on_delete=models.CASCADE, related_name="meeting_times"

View File

@ -1,4 +1,5 @@
import logging import logging
from collections.abc import Iterable
from datetime import datetime, timedelta from datetime import datetime, timedelta
from django.conf import settings from django.conf import settings
@ -30,7 +31,7 @@ def flags_for_member(csv_member, all_flags, folders):
yield flag yield flag
def update_flags(mw_flags) -> list[Flag]: def update_flags(mw_flags) -> Iterable[Flag]:
for typ, flags_of_type in mw_flags.items(): for typ, flags_of_type in mw_flags.items():
for name, id in flags_of_type.items(): for name, id in flags_of_type.items():
flag = Flag(id=id, name=name, type=typ[:-1]) flag = Flag(id=id, name=name, type=typ[:-1])

View File

@ -32,7 +32,7 @@ import django_tables2 as tables
import weasyprint 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.aggregates import GroupConcat
from django_sendfile import sendfile 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
@ -241,7 +241,7 @@ class EventMonthReport(
class UserEventView(SingleTableMixin, ListView): class UserEventView(SingleTableMixin, ListView):
model = EventExt model: type[EventExt] = EventExt
table_class = UserEventTable table_class = UserEventTable
export_formats = ("csv", "xlsx", "ods") export_formats = ("csv", "xlsx", "ods")
template_name = "membershipworks/user_event_list.dj.html" template_name = "membershipworks/user_event_list.dj.html"

View File

@ -1,4 +1,6 @@
from django.db.models import Q from typing import Required, TypedDict
from django.db.models import Q, QuerySet
from rest_framework import routers, serializers, viewsets from rest_framework import routers, serializers, viewsets
from rest_framework.decorators import action from rest_framework.decorators import action
@ -20,8 +22,20 @@ class DepartmentSerializer(serializers.HyperlinkedModelSerializer):
fields = ["name", "parent", "shop_lead_flag", "list_reply_to_address"] fields = ["name", "parent", "shop_lead_flag", "list_reply_to_address"]
class ListConfig(TypedDict, total=False):
real_name: str
subject_prefix: str
reply_to_address: str
class MailingList(TypedDict, total=False):
config: ListConfig
moderators: set[str]
members: Required[set[str]]
class DepartmentViewSet(viewsets.ModelViewSet): class DepartmentViewSet(viewsets.ModelViewSet):
queryset = Department.objects.all() queryset: QuerySet[Department] = Department.objects.all()
serializer_class = DepartmentSerializer serializer_class = DepartmentSerializer
@action(detail=False, methods=["get"]) @action(detail=False, methods=["get"])
@ -34,15 +48,20 @@ class DepartmentViewSet(viewsets.ModelViewSet):
"children", "children",
"shop_lead_flag__members", "shop_lead_flag__members",
) )
lists = {} lists: dict[str, MailingList] = {}
shopleads: dict[Member, list[Department]] = {}
for department in departments.filter(has_mailing_list=True): for department in departments.filter(has_mailing_list=True):
if department.shop_lead_flag is not None: if department.shop_lead_flag is not None:
moderator_emails = { moderator_emails = {
member.volunteer_email if member.volunteer_email else member.email member.volunteer_email if member.volunteer_email else member.email
for member in department.shop_lead_flag.members.all() for member in department.shop_lead_flag.members.all()
} }
for member in department.shop_lead_flag.members.all():
if member not in shopleads:
shopleads[member] = []
shopleads[member].append(department)
else: else:
moderator_emails = [] moderator_emails = set()
active_certified_members = { active_certified_members = {
member.sanitized_mailbox() member.sanitized_mailbox()
@ -52,6 +71,9 @@ class DepartmentViewSet(viewsets.ModelViewSet):
) )
} }
# list_name can only be None if has_mailing_list is False
assert department.list_name is not None
lists[department.list_name] = { lists[department.list_name] = {
"config": { "config": {
"real_name": department.list_name, "real_name": department.list_name,
@ -76,13 +98,6 @@ class DepartmentViewSet(viewsets.ModelViewSet):
if department.parent_id is None: if department.parent_id is None:
recurse_children(department) recurse_children(department)
shopleads = {}
for department in departments.filter(shop_lead_flag__isnull=False):
for member in department.shop_lead_flag.members.all():
if member not in shopleads:
shopleads[member] = []
shopleads[member].append(department)
# Add members to the Shop Leads mailing list, but don't configure it # Add members to the Shop Leads mailing list, but don't configure it
lists["ShopLeads"] = { lists["ShopLeads"] = {
"members": { "members": {

View File

@ -1,7 +1,7 @@
from django import forms from django import forms
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from dal import autocomplete from dal.autocomplete import ModelSelect2
from .models import Certification, CertificationDefinition from .models import Certification, CertificationDefinition
@ -49,7 +49,7 @@ class CertificationForm(forms.ModelForm):
"notes", "notes",
] ]
widgets = { widgets = {
"certification_version": autocomplete.ModelSelect2( "certification_version": ModelSelect2(
url="paperwork:certification_version_autocomplete", url="paperwork:certification_version_autocomplete",
forward=["certification_definition"], forward=["certification_definition"],
) )

View File

@ -1,8 +1,11 @@
from collections.abc import Callable
from itertools import chain from itertools import chain
from typing import TypedDict
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.auth.models import Permission from django.contrib.auth.models import AbstractBaseUser, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.test import Client from django.test import Client
from hypothesis import given from hypothesis import given
@ -19,9 +22,20 @@ from paperwork.models import (
) )
class PermissionLookup(TypedDict):
codename: str
model: type[models.Model]
class PermissionRequiredViewTestCaseMixin: class PermissionRequiredViewTestCaseMixin:
permissions = [] permissions: list[PermissionLookup] = []
path = None path: str
client: Client
user_with_permission: AbstractBaseUser
user_without_permission: AbstractBaseUser
assertEqual: Callable
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):

View File

@ -1,7 +1,9 @@
from collections.abc import Iterable
from django.conf import settings from django.conf import settings
from django.contrib import staticfiles
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import PermissionRequiredMixin
from django.contrib.staticfiles import finders as staticfiles_finders
from django.db import models from django.db import models
from django.db.models import ( from django.db.models import (
Case, Case,
@ -21,7 +23,7 @@ from django.views.generic import ListView
import requests import requests
import weasyprint import weasyprint
from django_mysql.models import GroupConcat from django_mysql.models.aggregates import GroupConcat
from django_tables2 import SingleTableMixin from django_tables2 import SingleTableMixin
from django_tables2.export.views import ExportMixin from django_tables2.export.views import ExportMixin
@ -63,6 +65,7 @@ class MemberCertificationListView(ListView):
@login_required @login_required
def department_certifications(request): def department_certifications(request):
departments: Iterable[Department]
if (member := Member.from_user(request.user)) is not None: if (member := Member.from_user(request.user)) is not None:
departments = Department.objects.filter_by_shop_lead(member) departments = Department.objects.filter_by_shop_lead(member)
else: else:
@ -115,7 +118,7 @@ def certification_pdf(request, cert_name):
html = weasyprint.HTML(f"{WIKI_URL}/index.php?title={wiki_page}") html = weasyprint.HTML(f"{WIKI_URL}/index.php?title={wiki_page}")
stylesheet = staticfiles.finders.find("paperwork/certification-print.css") stylesheet = staticfiles_finders.find("paperwork/certification-print.css")
pdf = html.write_pdf(stylesheets=[stylesheet]) pdf = html.write_pdf(stylesheets=[stylesheet])
return HttpResponse( return HttpResponse(
pdf, pdf,

103
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:4bada05219eeff5a98ea768509968f336ba2a4e86914ba0992cd90bfa91cbb31" content_hash = "sha256:8e68a7f1608469e70bc3e7502f747bbe5f38ca4bc15f504811377509599bb7a1"
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
@ -415,6 +415,16 @@ files = [
{file = "cssbeautifier-1.14.7.tar.gz", hash = "sha256:be7f1ea7a7b009f0172c2c0d0bebb2d136346e786f7182185ea944affb52135a"}, {file = "cssbeautifier-1.14.7.tar.gz", hash = "sha256:be7f1ea7a7b009f0172c2c0d0bebb2d136346e786f7182185ea944affb52135a"},
] ]
[[package]]
name = "cssselect"
version = "1.2.0"
requires_python = ">=3.7"
summary = "cssselect parses CSS3 Selectors and translates them to XPath 1.0"
files = [
{file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"},
{file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"},
]
[[package]] [[package]]
name = "cssselect2" name = "cssselect2"
version = "0.7.0" version = "0.7.0"
@ -1237,15 +1247,6 @@ files = [
{file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"}, {file = "lxml-5.2.1.tar.gz", hash = "sha256:3f7765e69bbce0906a7c74d5fe46d2c7a7596147318dbc08e4a2431f3060e306"},
] ]
[[package]]
name = "lxml-stubs"
version = "0.5.1"
summary = "Type annotations for the lxml package"
files = [
{file = "lxml-stubs-0.5.1.tar.gz", hash = "sha256:e0ec2aa1ce92d91278b719091ce4515c12adc1d564359dfaf81efa7d4feab79d"},
{file = "lxml_stubs-0.5.1-py3-none-any.whl", hash = "sha256:1f689e5dbc4b9247cb09ae820c7d34daeb1fdbd1db06123814b856dae7787272"},
]
[[package]] [[package]]
name = "markdown" name = "markdown"
version = "3.4.1" version = "3.4.1"
@ -2010,6 +2011,19 @@ files = [
{file = "traitlets-5.13.0.tar.gz", hash = "sha256:9b232b9430c8f57288c1024b34a8f0251ddcc47268927367a0dd3eeaca40deb5"}, {file = "traitlets-5.13.0.tar.gz", hash = "sha256:9b232b9430c8f57288c1024b34a8f0251ddcc47268927367a0dd3eeaca40deb5"},
] ]
[[package]]
name = "types-beautifulsoup4"
version = "4.12.0.20240229"
requires_python = ">=3.8"
summary = "Typing stubs for beautifulsoup4"
dependencies = [
"types-html5lib",
]
files = [
{file = "types-beautifulsoup4-4.12.0.20240229.tar.gz", hash = "sha256:e37e4cfa11b03b01775732e56d2c010cb24ee107786277bae6bc0fa3e305b686"},
{file = "types_beautifulsoup4-4.12.0.20240229-py3-none-any.whl", hash = "sha256:000cdddb8aee4effb45a04be95654de8629fb8594a4f2f1231cff81108977324"},
]
[[package]] [[package]]
name = "types-bleach" name = "types-bleach"
version = "6.1.0.20240331" version = "6.1.0.20240331"
@ -2023,6 +2037,16 @@ files = [
{file = "types_bleach-6.1.0.20240331-py3-none-any.whl", hash = "sha256:399bc59bfd20a36a56595f13f805e56c8a08e5a5c07903e5cf6fafb5a5107dd4"}, {file = "types_bleach-6.1.0.20240331-py3-none-any.whl", hash = "sha256:399bc59bfd20a36a56595f13f805e56c8a08e5a5c07903e5cf6fafb5a5107dd4"},
] ]
[[package]]
name = "types-docutils"
version = "0.21.0.20240423"
requires_python = ">=3.8"
summary = "Typing stubs for docutils"
files = [
{file = "types-docutils-0.21.0.20240423.tar.gz", hash = "sha256:7716ec6c68b5179b7ba1738cace2f1326e64df9f44b7ab08d9904d32c23fc15f"},
{file = "types_docutils-0.21.0.20240423-py3-none-any.whl", hash = "sha256:7f6e84ba8fcd2454c5b8bb8d77384d091a901929cc2b31079316e10eb346580a"},
]
[[package]] [[package]]
name = "types-html5lib" name = "types-html5lib"
version = "1.1.11.20240228" version = "1.1.11.20240228"
@ -2033,6 +2057,55 @@ files = [
{file = "types_html5lib-1.1.11.20240228-py3-none-any.whl", hash = "sha256:af5de0125cb0fe5667543b158db83849b22e25c0e36c9149836b095548bf1020"}, {file = "types_html5lib-1.1.11.20240228-py3-none-any.whl", hash = "sha256:af5de0125cb0fe5667543b158db83849b22e25c0e36c9149836b095548bf1020"},
] ]
[[package]]
name = "types-lxml"
version = "2024.4.14"
requires_python = ">=3.8"
summary = "Complete lxml external type annotation"
dependencies = [
"cssselect~=1.2",
"types-beautifulsoup4~=4.12",
"typing-extensions~=4.5",
]
files = [
{file = "types_lxml-2024.4.14-py3-none-any.whl", hash = "sha256:7e5f836067cde4fddce3cdbf2bac7192c764bf5ee6d3eb86c732ad1b84f265c5"},
{file = "types_lxml-2024.4.14.tar.gz", hash = "sha256:dd8105b579925af1b6ae77469f4fc835be3872b15e86cb46ad4fcc33b20c781d"},
]
[[package]]
name = "types-markdown"
version = "3.6.0.20240316"
requires_python = ">=3.8"
summary = "Typing stubs for Markdown"
files = [
{file = "types-Markdown-3.6.0.20240316.tar.gz", hash = "sha256:de9fb84860b55b647b170ca576895fcca61b934a6ecdc65c31932c6795b440b8"},
{file = "types_Markdown-3.6.0.20240316-py3-none-any.whl", hash = "sha256:d3ecd26a940781787c7b57a0e3c9d77c150db64e12989ef687059edc83dfd78a"},
]
[[package]]
name = "types-psycopg2"
version = "2.9.21.20240417"
requires_python = ">=3.8"
summary = "Typing stubs for psycopg2"
files = [
{file = "types-psycopg2-2.9.21.20240417.tar.gz", hash = "sha256:05db256f4a459fb21a426b8e7fca0656c3539105ff0208eaf6bdaf406a387087"},
{file = "types_psycopg2-2.9.21.20240417-py3-none-any.whl", hash = "sha256:644d6644d64ebbe37203229b00771012fb3b3bddd507a129a2e136485990e4f8"},
]
[[package]]
name = "types-pygments"
version = "2.17.0.20240310"
requires_python = ">=3.8"
summary = "Typing stubs for Pygments"
dependencies = [
"types-docutils",
"types-setuptools",
]
files = [
{file = "types-Pygments-2.17.0.20240310.tar.gz", hash = "sha256:b1d97e905ce36343c7283b0319182ae6d4f967188f361f45502a18ae43e03e1f"},
{file = "types_Pygments-2.17.0.20240310-py3-none-any.whl", hash = "sha256:b101ca9448aaff52af6966506f1fdd73b1e60a79b8a79a8bace3366cbf1f7ed9"},
]
[[package]] [[package]]
name = "types-pyyaml" name = "types-pyyaml"
version = "6.0.12.2" version = "6.0.12.2"
@ -2055,6 +2128,16 @@ files = [
{file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"}, {file = "types_requests-2.31.0.20240406-py3-none-any.whl", hash = "sha256:6216cdac377c6b9a040ac1c0404f7284bd13199c0e1bb235f4324627e8898cf5"},
] ]
[[package]]
name = "types-setuptools"
version = "69.5.0.20240423"
requires_python = ">=3.8"
summary = "Typing stubs for setuptools"
files = [
{file = "types-setuptools-69.5.0.20240423.tar.gz", hash = "sha256:a7ba908f1746c4337d13f027fa0f4a5bcad6d1d92048219ba792b3295c58586d"},
{file = "types_setuptools-69.5.0.20240423-py3-none-any.whl", hash = "sha256:a4381e041510755a6c9210e26ad55b1629bc10237aeb9cb8b6bd24996b73db48"},
]
[[package]] [[package]]
name = "types-urllib3" name = "types-urllib3"
version = "1.26.25.14" version = "1.26.25.14"

View File

@ -123,7 +123,10 @@ typing = [
"types-requests~=2.31", "types-requests~=2.31",
"types-urllib3~=1.26", "types-urllib3~=1.26",
"djangorestframework-stubs[compatible-mypy]~=3.15", "djangorestframework-stubs[compatible-mypy]~=3.15",
"lxml-stubs~=0.5", "types-Markdown~=3.6",
"types-Pygments~=2.17",
"types-psycopg2~=2.9",
"types-lxml~=2024.4",
] ]
debug = [ debug = [
"django-debug-toolbar~=4.3", "django-debug-toolbar~=4.3",

View File

@ -1,6 +1,6 @@
from django import forms from django import forms
from dal import autocomplete from dal.autocomplete import ModelSelect2
from rentals.models import LockerInfo from rentals.models import LockerInfo
@ -16,7 +16,7 @@ class LockerInfoForm(forms.ModelForm):
"notes", "notes",
] ]
widgets = { widgets = {
"renter": autocomplete.ModelSelect2( "renter": ModelSelect2(
url="membershipworks:member-autocomplete", url="membershipworks:member-autocomplete",
attrs={ attrs={
"data-placeholder": "-- No Renter --", "data-placeholder": "-- No Renter --",