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