Fix various type issues
This commit is contained in:
parent
9658366d72
commit
0944dd7992
@ -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
|
||||
|
@ -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()
|
||||
]
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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"
|
||||
)
|
||||
|
@ -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"""
|
||||
|
@ -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,12 +499,9 @@ class EventExtQuerySet(models.QuerySet["EventExt"]):
|
||||
)
|
||||
|
||||
|
||||
class EventExtManager(models.Manager["EventExt"]):
|
||||
def get_queryset(self) -> models.QuerySet["EventExt"]:
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.annotate(
|
||||
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")
|
||||
@ -516,8 +517,7 @@ class EventExtManager(models.Manager["EventExt"]):
|
||||
output_field=models.DurationField(),
|
||||
),
|
||||
person_hours=ExpressionWrapper(
|
||||
ExpressionWrapper(F("duration"), models.IntegerField())
|
||||
* F("count"),
|
||||
ExpressionWrapper(F("duration"), models.IntegerField()) * F("count"),
|
||||
models.DurationField(),
|
||||
),
|
||||
# TODO: this could be a GeneratedField, but that
|
||||
@ -529,7 +529,6 @@ class EventExtManager(models.Manager["EventExt"]):
|
||||
output_field=models.DateTimeField(),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class EventExt(Event):
|
||||
@ -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"
|
||||
|
@ -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])
|
||||
|
@ -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"
|
||||
|
@ -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": {
|
||||
|
@ -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"],
|
||||
)
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
103
pdm.lock
103
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"
|
||||
|
@ -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",
|
||||
|
@ -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 --",
|
||||
|
Loading…
Reference in New Issue
Block a user