From 0944dd79924637579cd528375b391fac0a27ee1c Mon Sep 17 00:00:00 2001 From: Adam Goldsmith Date: Sat, 4 May 2024 16:38:51 -0400 Subject: [PATCH] Fix various type issues --- doorcontrol/models.py | 4 +- doorcontrol/tasks/update_doors.py | 20 ++++- doorcontrol/views.py | 10 +-- membershipworks/invoice_email.py | 2 + membershipworks/membershipworks_api.py | 20 ++++- membershipworks/models.py | 111 +++++++++++++++---------- membershipworks/tasks/scrape.py | 3 +- membershipworks/views.py | 4 +- paperwork/api.py | 37 ++++++--- paperwork/forms.py | 4 +- paperwork/tests.py | 20 ++++- paperwork/views.py | 9 +- pdm.lock | 103 ++++++++++++++++++++--- pyproject.toml | 5 +- rentals/forms.py | 4 +- 15 files changed, 264 insertions(+), 92 deletions(-) diff --git a/doorcontrol/models.py b/doorcontrol/models.py index fb7a15f..e15d80f 100644 --- a/doorcontrol/models.py +++ b/doorcontrol/models.py @@ -220,7 +220,7 @@ class HIDEvent(models.Model): ) @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.column: field.attname for field in HIDEvent._meta.get_fields() } @@ -287,7 +287,7 @@ class HIDEvent(models.Model): def __str__(self): 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`""" if self.raw_card_number is None: return None diff --git a/doorcontrol/tasks/update_doors.py b/doorcontrol/tasks/update_doors.py index c7400e3..da59ad4 100644 --- a/doorcontrol/tasks/update_doors.py +++ b/doorcontrol/tasks/update_doors.py @@ -1,5 +1,6 @@ import dataclasses import logging +from typing import TypedDict from django_q.tasks import async_task @@ -11,10 +12,20 @@ from membershipworks.models import Member logger = logging.getLogger(__name__) +class CardholderAttribs(TypedDict): + forename: str + middleName: str + surname: str + email: str + phone: str + custom1: str + custom2: str + + @dataclasses.dataclass class DoorMember: door: Door - attribs: dict[str, str] + attribs: CardholderAttribs credentials: set[Credential] schedules: set[str] cardholderID: str | None = None @@ -33,7 +44,7 @@ class DoorMember: else: credentials = set() - reasons_and_schedules = {} + reasons_and_schedules: dict[str, str] = {} if ( member.is_active or member.flags.filter(name="Misc. Access", type="folder").exists() @@ -112,6 +123,9 @@ class DoorMember: all_members: list["DoorMember"], 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 = { 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 = { member.membershipworks_id: member for member in [ - DoorMember.from_cardholder(ch, door.controller) + DoorMember.from_cardholder(ch, door) for ch in door.controller.get_cardholders() ] } diff --git a/doorcontrol/views.py b/doorcontrol/views.py index e91f283..7b9791b 100644 --- a/doorcontrol/views.py +++ b/doorcontrol/views.py @@ -12,7 +12,7 @@ from django.views.generic.list import ListView import django_filters import django_tables2 as tables 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_tables2 import SingleTableMixin from django_tables2.export.views import ExportMixin @@ -30,7 +30,7 @@ from .tables import ( REPORTS = [] -def register_report(cls: "BaseAccessReport"): +def register_report(cls: "type[BaseAccessReport]"): REPORTS.append(cls) return cls @@ -55,7 +55,7 @@ class BaseAccessReport( filterset_class = AccessReportFilterSet - _report_name = None + _report_name: str @classmethod def _report_types(cls): @@ -76,9 +76,9 @@ class BaseAccessReport( def _selected_report(self): 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: - return int(self.request.GET.get("items_per_page")) + return int(self.request.GET["items_per_page"]) return super().get_paginate_by(queryset) def get_queryset(self): diff --git a/membershipworks/invoice_email.py b/membershipworks/invoice_email.py index 446d430..a01b6ec 100644 --- a/membershipworks/invoice_email.py +++ b/membershipworks/invoice_email.py @@ -28,6 +28,8 @@ def make_multipart_email( def make_instructor_email( invoice: EventInvoice, pdf: bytes, event_url: str ) -> 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( "membershipworks/email/event_invoice_instructor.dj.html" ) diff --git a/membershipworks/membershipworks_api.py b/membershipworks/membershipworks_api.py index b8ab1fa..c70c42d 100644 --- a/membershipworks/membershipworks_api.py +++ b/membershipworks/membershipworks_api.py @@ -2,6 +2,7 @@ import csv import datetime from enum import Enum from io import StringIO +from typing import Any 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): def __init__(self, reason, r): super().__init__( @@ -104,7 +112,7 @@ class MembershipWorks: def _inject_auth(self, kwargs): # TODO: should probably be a decorator or something if self.auth_token is None: - raise RuntimeError("Not Logged in to MembershipWorks") + raise NotAuthenticatedError() # add auth token to params if "params" not in kwargs: kwargs["params"] = {} @@ -126,6 +134,8 @@ class MembershipWorks: Is this terrible? Yes. Also, not dissimilar to how MW does it in all.js. """ + if not self.org_info: + raise NotAuthenticatedError() fields = staticFlags.copy() # 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. """ - 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"]: # TODO: there must be a better way. this is stupid @@ -242,8 +254,8 @@ class MembershipWorks: def get_events_list( self, - start_date: datetime.datetime = None, - end_date: datetime.datetime = None, + start_date: datetime.datetime | None = None, + end_date: datetime.datetime | None = None, categories=False, ): """Retrive a list of events between `start_date` and `end_date`, optionally including category information""" diff --git a/membershipworks/models.py b/membershipworks/models.py index d1ea11e..1b5d74d 100644 --- a/membershipworks/models.py +++ b/membershipworks/models.py @@ -1,6 +1,7 @@ import uuid -from datetime import datetime -from typing import Optional +from datetime import datetime, timedelta +from decimal import Decimal +from typing import TYPE_CHECKING, TypedDict import django.core.mail.message from django.conf import settings @@ -15,6 +16,7 @@ from django.db.models import ( Func, OuterRef, Q, + QuerySet, Subquery, Sum, Value, @@ -26,12 +28,13 @@ from django.utils import timezone import nh3 from django_db_views.db_view import DBView +from django_stubs_ext import WithAnnotations class BaseModel(models.Model): - _api_names_override = {} - _date_fields = {} - _allowed_missing_fields = [] + _api_names_override: dict[str, str] = {} + _date_fields: dict[str, str | None] = {} + _allowed_missing_fields: list[str] = [] class Meta: abstract = True @@ -274,9 +277,10 @@ class Member(BaseModel): ] @classmethod - def from_user(cls, user) -> Optional["Member"]: + def from_user(cls, user) -> "Member | None": if hasattr(user, "ldap_user"): return cls.objects.get(uid=user.ldap_user.attrs["employeeNumber"][0]) + return None def sanitized_mailbox(self, use_volunteer=False) -> str: 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 -class EventExtQuerySet(models.QuerySet["EventExt"]): +class EventExtQuerySet(models.QuerySet["EventExtAnnotated"]): def summarize(self, aggregate: bool = False): method = self.aggregate if aggregate else self.annotate return method( @@ -465,7 +469,7 @@ class EventExtQuerySet(models.QuerySet["EventExt"]): net_revenue__sum=Sum("net_revenue", filter=F("occurred")), ) - def with_financials(self): + def with_financials(self) -> "QuerySet[EventExtAnnotatedWithFinancials]": return self.annotate( **{ field: Subquery( @@ -495,40 +499,35 @@ class EventExtQuerySet(models.QuerySet["EventExt"]): ) -class EventExtManager(models.Manager["EventExt"]): - def get_queryset(self) -> models.QuerySet["EventExt"]: - return ( - super() - .get_queryset() - .annotate( - meetings=Subquery( - EventMeetingTime.objects.filter(event=OuterRef("pk")) - .values("event__pk") - .annotate(d=Count("pk")) - .values("d")[:1], - output_field=models.IntegerField(), - ), - duration=Subquery( - EventMeetingTime.objects.filter(event=OuterRef("pk")) - .values("event__pk") - .annotate(d=Sum("duration")) - .values("d")[:1], - output_field=models.DurationField(), - ), - person_hours=ExpressionWrapper( - ExpressionWrapper(F("duration"), models.IntegerField()) - * F("count"), - models.DurationField(), - ), - # TODO: this could be a GeneratedField, but that - # currently breaks saving when the primary key is - # provided (Django 5.0.1) - details_timestamp=Func( - Func(F("details___ts"), function="FROM_UNIXTIME"), - template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')", - output_field=models.DateTimeField(), - ), - ) +class EventExtManager(models.Manager): + def get_queryset(self) -> EventExtQuerySet: + return EventExtQuerySet(self.model, using=self._db).annotate( + meetings=Subquery( + EventMeetingTime.objects.filter(event=OuterRef("pk")) + .values("event__pk") + .annotate(d=Count("pk")) + .values("d")[:1], + output_field=models.IntegerField(), + ), + duration=Subquery( + EventMeetingTime.objects.filter(event=OuterRef("pk")) + .values("event__pk") + .annotate(d=Sum("duration")) + .values("d")[:1], + output_field=models.DurationField(), + ), + person_hours=ExpressionWrapper( + ExpressionWrapper(F("duration"), models.IntegerField()) * F("count"), + models.DurationField(), + ), + # TODO: this could be a GeneratedField, but that + # currently breaks saving when the primary key is + # provided (Django 5.0.1) + 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 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: @@ -582,6 +581,32 @@ class EventExt(Event): 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): event = models.ForeignKey( EventExt, on_delete=models.CASCADE, related_name="meeting_times" diff --git a/membershipworks/tasks/scrape.py b/membershipworks/tasks/scrape.py index 7bdad2a..9796bdf 100644 --- a/membershipworks/tasks/scrape.py +++ b/membershipworks/tasks/scrape.py @@ -1,4 +1,5 @@ import logging +from collections.abc import Iterable from datetime import datetime, timedelta from django.conf import settings @@ -30,7 +31,7 @@ def flags_for_member(csv_member, all_flags, folders): 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 name, id in flags_of_type.items(): flag = Flag(id=id, name=name, type=typ[:-1]) diff --git a/membershipworks/views.py b/membershipworks/views.py index b0967e0..0601e58 100644 --- a/membershipworks/views.py +++ b/membershipworks/views.py @@ -32,7 +32,7 @@ import django_tables2 as tables import weasyprint from dal import autocomplete from django_filters.views import BaseFilterView -from django_mysql.models import GroupConcat +from django_mysql.models.aggregates import GroupConcat from django_sendfile import sendfile from django_tables2 import A, SingleTableMixin from django_tables2.export.views import ExportMixin @@ -241,7 +241,7 @@ class EventMonthReport( class UserEventView(SingleTableMixin, ListView): - model = EventExt + model: type[EventExt] = EventExt table_class = UserEventTable export_formats = ("csv", "xlsx", "ods") template_name = "membershipworks/user_event_list.dj.html" diff --git a/paperwork/api.py b/paperwork/api.py index 7dcd6f4..96b17f1 100644 --- a/paperwork/api.py +++ b/paperwork/api.py @@ -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.decorators import action @@ -20,8 +22,20 @@ class DepartmentSerializer(serializers.HyperlinkedModelSerializer): 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): - queryset = Department.objects.all() + queryset: QuerySet[Department] = Department.objects.all() serializer_class = DepartmentSerializer @action(detail=False, methods=["get"]) @@ -34,15 +48,20 @@ class DepartmentViewSet(viewsets.ModelViewSet): "children", "shop_lead_flag__members", ) - lists = {} + lists: dict[str, MailingList] = {} + shopleads: dict[Member, list[Department]] = {} for department in departments.filter(has_mailing_list=True): if department.shop_lead_flag is not None: moderator_emails = { 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(): + if member not in shopleads: + shopleads[member] = [] + shopleads[member].append(department) else: - moderator_emails = [] + moderator_emails = set() active_certified_members = { 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] = { "config": { "real_name": department.list_name, @@ -76,13 +98,6 @@ class DepartmentViewSet(viewsets.ModelViewSet): if department.parent_id is None: 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 lists["ShopLeads"] = { "members": { diff --git a/paperwork/forms.py b/paperwork/forms.py index 955f7cb..a7c5981 100644 --- a/paperwork/forms.py +++ b/paperwork/forms.py @@ -1,7 +1,7 @@ from django import forms from django.core.exceptions import ValidationError -from dal import autocomplete +from dal.autocomplete import ModelSelect2 from .models import Certification, CertificationDefinition @@ -49,7 +49,7 @@ class CertificationForm(forms.ModelForm): "notes", ] widgets = { - "certification_version": autocomplete.ModelSelect2( + "certification_version": ModelSelect2( url="paperwork:certification_version_autocomplete", forward=["certification_definition"], ) diff --git a/paperwork/tests.py b/paperwork/tests.py index d4b939b..8d29b71 100644 --- a/paperwork/tests.py +++ b/paperwork/tests.py @@ -1,8 +1,11 @@ +from collections.abc import Callable from itertools import chain +from typing import TypedDict 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.db import models from django.test import Client from hypothesis import given @@ -19,9 +22,20 @@ from paperwork.models import ( ) +class PermissionLookup(TypedDict): + codename: str + model: type[models.Model] + + class PermissionRequiredViewTestCaseMixin: - permissions = [] - path = None + permissions: list[PermissionLookup] = [] + path: str + + client: Client + user_with_permission: AbstractBaseUser + user_without_permission: AbstractBaseUser + + assertEqual: Callable @classmethod def setUpTestData(cls): diff --git a/paperwork/views.py b/paperwork/views.py index 0c880ba..4ef7148 100644 --- a/paperwork/views.py +++ b/paperwork/views.py @@ -1,7 +1,9 @@ +from collections.abc import Iterable + from django.conf import settings -from django.contrib import staticfiles from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import PermissionRequiredMixin +from django.contrib.staticfiles import finders as staticfiles_finders from django.db import models from django.db.models import ( Case, @@ -21,7 +23,7 @@ from django.views.generic import ListView import requests import weasyprint -from django_mysql.models import GroupConcat +from django_mysql.models.aggregates import GroupConcat from django_tables2 import SingleTableMixin from django_tables2.export.views import ExportMixin @@ -63,6 +65,7 @@ class MemberCertificationListView(ListView): @login_required def department_certifications(request): + departments: Iterable[Department] if (member := Member.from_user(request.user)) is not None: departments = Department.objects.filter_by_shop_lead(member) else: @@ -115,7 +118,7 @@ def certification_pdf(request, cert_name): 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]) return HttpResponse( pdf, diff --git a/pdm.lock b/pdm.lock index 8272417..ff07ebb 100644 --- a/pdm.lock +++ b/pdm.lock @@ -5,7 +5,7 @@ groups = ["default", "debug", "lint", "server", "typing", "dev"] strategy = ["cross_platform"] lock_version = "4.4.1" -content_hash = "sha256:4bada05219eeff5a98ea768509968f336ba2a4e86914ba0992cd90bfa91cbb31" +content_hash = "sha256:8e68a7f1608469e70bc3e7502f747bbe5f38ca4bc15f504811377509599bb7a1" [[package]] name = "aiohttp" @@ -415,6 +415,16 @@ files = [ {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]] name = "cssselect2" version = "0.7.0" @@ -1237,15 +1247,6 @@ files = [ {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]] name = "markdown" version = "3.4.1" @@ -2010,6 +2011,19 @@ files = [ {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]] name = "types-bleach" version = "6.1.0.20240331" @@ -2023,6 +2037,16 @@ files = [ {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]] name = "types-html5lib" version = "1.1.11.20240228" @@ -2033,6 +2057,55 @@ files = [ {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]] name = "types-pyyaml" version = "6.0.12.2" @@ -2055,6 +2128,16 @@ files = [ {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]] name = "types-urllib3" version = "1.26.25.14" diff --git a/pyproject.toml b/pyproject.toml index e321e93..cb346fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,10 @@ typing = [ "types-requests~=2.31", "types-urllib3~=1.26", "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 = [ "django-debug-toolbar~=4.3", diff --git a/rentals/forms.py b/rentals/forms.py index d331bcf..60f0f3a 100644 --- a/rentals/forms.py +++ b/rentals/forms.py @@ -1,6 +1,6 @@ from django import forms -from dal import autocomplete +from dal.autocomplete import ModelSelect2 from rentals.models import LockerInfo @@ -16,7 +16,7 @@ class LockerInfoForm(forms.ModelForm): "notes", ] widgets = { - "renter": autocomplete.ModelSelect2( + "renter": ModelSelect2( url="membershipworks:member-autocomplete", attrs={ "data-placeholder": "-- No Renter --",