Compare commits

...

10 Commits

Author SHA1 Message Date
04ca92b5fe Make LOGGING setting configurable via environment variable
All checks were successful
Ruff / ruff (push) Successful in 29s
Test / test (push) Successful in 5m10s
2024-05-04 20:34:36 -04:00
0944dd7992 Fix various type issues 2024-05-04 18:03:22 -04:00
9658366d72 Fix mypy for django-configurations
still have a lot of bad typing, but at least it runs again
2024-05-03 12:37:48 -04:00
a2c0707263 Bump dependencies 2024-05-03 12:37:48 -04:00
785a445b43 Add support for loading configurations values from systemd credentials 2024-05-03 12:37:48 -04:00
ee2d63f784 membershipworks: Add support for scraping event registration data 2024-05-03 12:37:48 -04:00
12eb4038bc Disable the Tasks app
it has not been used in production for some time
2024-05-03 12:37:48 -04:00
c2b1da743c membershipworks: Remove commented out line 2024-05-03 12:37:48 -04:00
99060a8a43 Convert settings to use django-configurations 2024-05-03 12:37:48 -04:00
59cade1cfd ci: Only install the dev dev-dependencies group for testing 2024-05-03 12:37:48 -04:00
32 changed files with 761 additions and 475 deletions

View File

@ -2,7 +2,7 @@ name: Test
on: [ push, pull_request ]
env:
DJANGO_SETTINGS_MODULE: cmsmanage.settings.ci
DJANGO_CONFIGURATION: CI
jobs:
test:
@ -28,6 +28,6 @@ jobs:
sudo apt-get update &&
sudo apt-get -y install build-essential python3-dev libldap2-dev libsasl2-dev mariadb-client
- name: Install python dependencies
run: pdm sync -d
run: pdm sync -d -G dev
- name: Run tests
run: pdm run -v ./manage.py test

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ __pycache__/
/__pypackages__/
/markdownx/
/media/
/settings.*.env

View File

@ -9,8 +9,9 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
import os
from django.core.asgi import get_asgi_application
from configurations.asgi import get_asgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cmsmanage.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "DEV")
application = get_asgi_application()

View File

@ -0,0 +1,12 @@
# https://github.com/typeddjango/django-stubs/pull/180#issuecomment-820062352
import os
from configurations import importer
from mypy_django_plugin import main
def plugin(version):
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cmsmanage.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Dev")
importer.install()
return main.plugin(version)

326
cmsmanage/settings.py Normal file
View File

@ -0,0 +1,326 @@
import os
from pathlib import Path
from django.core import validators
from configurations import Configuration, values
BASE_DIR = Path(__file__).resolve().parent.parent
class Base(Configuration):
@classmethod
def pre_setup(cls):
super().pre_setup()
# load systemd credentials, as per https://systemd.io/CREDENTIALS/
credentials_directory = os.getenv("CREDENTIALS_DIRECTORY")
if credentials_directory is not None:
for credential in Path(credentials_directory).iterdir():
if credential.name.isupper():
os.environ.setdefault(credential.name, credential.read_text())
@classmethod
def setup(cls):
super().setup()
cls.DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"}
INSTALLED_APPS = [
"dal",
"dal_select2",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_admin_logs",
"django_object_actions",
"widget_tweaks",
# "markdownx",
# "recurrence",
"rest_framework",
"rest_framework.authtoken",
"django_q",
"django_nh3",
"django_tables2",
"django_filters",
"django_db_views",
"django_mysql",
"django_sendfile",
"django_bootstrap5",
# "tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig",
"paperwork.apps.PaperworkConfig",
"doorcontrol.apps.DoorControlConfig",
"dashboard.apps.DashboardConfig",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "cmsmanage.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
WSGI_APPLICATION = "cmsmanage.wsgi.application"
# Default URL to redirect to after authentication
LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/auth/login/"
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "America/New_York"
USE_I18N = False
USE_L10N = True
USE_TZ = True
USE_DEPRECATED_PYTZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
LOGGING = values.DictValue(
{
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"my_console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"": {
"handlers": ["my_console"],
"level": "WARNING",
},
},
}
)
MEDIA_ROOT = "media"
MEDIA_URL = "media/"
SENDFILE_ROOT = str(BASE_DIR / "media" / "protected")
SERVER_EMAIL = "cmsmanage <cmsmanage@claremontmakerspace.org>"
EMAIL_SUBJECT_PREFIX = "[cmsmanage] "
DEFAULT_FROM_EMAIL = SERVER_EMAIL
# Django Rest Framework
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions
"DEFAULT_PERMISSION_CLASSES": [
"cmsmanage.drf_permissions.DjangoModelPermissionsWithView"
],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
}
# Django Q
Q_CLUSTER = {
"name": "cmsmanage",
"orm": "default",
"retry": 60 * 6,
"timeout": 60 * 5,
"catch_up": False,
"error_reporter": {"admin_email": {}},
"ack_failures": True,
"max_attempts": 1,
"ALT_CLUSTERS": {
"internal": {
"retry": 60 * 60,
"timeout": 60 * 59,
},
},
}
# Django-Tables2
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5-responsive.html"
DJANGO_TABLES2_TABLE_ATTRS = {"class": "table mx-auto w-auto"}
SECRET_KEY = values.SecretValue(environ_required=True)
# CMSManage specific stuff
WIKI_URL = values.URLValue("https://wiki.claremontmakerspace.org")
class NonCIBase(Base):
"""required for all but CI"""
DATABASES = values.DatabaseURLValue(environ_required=True)
EMAIL = values.EmailURLValue(environ_required=True)
# TODO: should validate emails
ADMINS = values.SingleNestedTupleValue(environ_required=True)
MEMBERSHIPWORKS_USERNAME = values.EmailValue(
environ_required=True, environ_prefix=None
)
MEMBERSHIPWORKS_PASSWORD = values.SecretValue(
environ_required=True, environ_prefix=None
)
HID_DOOR_USERNAME = values.Value(environ_required=True, environ_prefix=None)
HID_DOOR_PASSWORD = values.SecretValue(environ_prefix=None)
# TODO: should validate emails (but EmailValidator doesn't handle name parts)
INVOICE_HANDLERS = values.ListValue(
environ_required=True, environ_prefix="CMSMANAGE"
)
# arguments for https://udm-rest-client.readthedocs.io/en/latest/udm_rest_client.html#udm_rest_client.udm.UDM
UCS = values.DictValue(environ_required=True, environ_prefix="CMSMANAGE")
class LDAPURLValue(values.ValidationMixin, values.Value):
message = "Cannot interpret LDAP URL value {0!r}"
validator = validators.URLValidator(schemes=["ldap", "ldaps"])
class Prod(NonCIBase):
import ldap
from django_auth_ldap.config import LDAPGroupQuery, LDAPSearch, PosixGroupType
DOTENV = BASE_DIR / "settings.dev.env"
DEBUG = False
# LDAP Authentication
# https://django-auth-ldap.readthedocs.io/en/latest/
AUTHENTICATION_BACKENDS = [
"django_auth_ldap.backend.LDAPBackend",
"django.contrib.auth.backends.ModelBackend",
]
AUTH_LDAP_USER_SEARCH = LDAPSearch(
"cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org",
ldap.SCOPE_SUBTREE,
"(uid=%(user)s)",
)
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail",
}
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_staff": (
LDAPGroupQuery(
"cn=MW_CMS Staff,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
)
| LDAPGroupQuery(
"cn=MW_Database Admin,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
)
| LDAPGroupQuery(
"cn=MW_Database Access,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
)
)
}
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
"cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org",
ldap.SCOPE_SUBTREE,
"(objectClass=posixGroup)",
)
AUTH_LDAP_GROUP_TYPE = PosixGroupType()
AUTH_LDAP_MIRROR_GROUPS = True
SENDFILE_BACKEND = "django_sendfile.backends.nginx"
SENDFILE_URL = "/media/protected"
AUTH_LDAP_SERVER_URI = LDAPURLValue(None, environ_required=True)
AUTH_LDAP_BIND_DN = values.Value(environ_required=True)
AUTH_LDAP_BIND_PASSWORD = values.SecretValue()
ALLOWED_HOSTS = values.ListValue([], environ_required=True)
STATIC_ROOT = values.PathValue(environ_required=True)
def configure_hypothesis_profiles():
from hypothesis import HealthCheck, Verbosity, settings
settings.register_profile("ci", suppress_health_check=(HealthCheck.too_slow,))
settings.register_profile("dev", max_examples=20)
settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose)
class Dev(NonCIBase):
@classmethod
def post_setup(cls):
import os
from hypothesis import settings
super().post_setup()
configure_hypothesis_profiles()
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "dev"))
DOTENV = BASE_DIR / "settings.dev.env"
DEBUG = values.BooleanValue(True)
INTERNAL_IPS = ["127.0.0.1"]
INSTALLED_APPS = NonCIBase.INSTALLED_APPS + ["debug_toolbar", "django_extensions"]
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + Base.MIDDLEWARE
EMAIL = values.EmailURLValue("smtp://localhost:1025") # for local `mailpit`
SENDFILE_BACKEND = "django_sendfile.backends.development"
class CI(Base):
@classmethod
def post_setup(cls):
from hypothesis import settings
super().post_setup()
configure_hypothesis_profiles()
settings.load_profile("ci")
SECRET_KEY = "aed7jee2kai1we9eithae0gaegh9ohthoh4phahk5bau4Ahxaijo3aicheex3qua"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"HOST": "mariadb",
"NAME": "CMS_Database",
"USER": "root",
"PASSWORD": "whatever",
"OPTIONS": {
"charset": "utf8mb4",
},
}
}

View File

@ -1,2 +0,0 @@
dev.py
prod.py

View File

@ -1,163 +0,0 @@
"""
Django settings for cmsmanage project.
Generated by 'django-admin startproject' using Django 3.1.3.
For more information on this file, see
https://docs.djangoproject.com/en/3.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent.parent
ALLOWED_HOSTS: list[str] = []
# Application definition
INSTALLED_APPS = [
"dal",
"dal_select2",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_admin_logs",
"django_object_actions",
"widget_tweaks",
"markdownx",
"recurrence",
"rest_framework",
"rest_framework.authtoken",
"django_q",
"django_nh3",
"django_tables2",
"django_filters",
"django_db_views",
"django_mysql",
"django_sendfile",
"django_bootstrap5",
"tasks.apps.TasksConfig",
"rentals.apps.RentalsConfig",
"membershipworks.apps.MembershipworksConfig",
"paperwork.apps.PaperworkConfig",
"doorcontrol.apps.DoorControlConfig",
"dashboard.apps.DashboardConfig",
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
]
ROOT_URLCONF = "cmsmanage.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [Path(BASE_DIR) / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
WSGI_APPLICATION = "cmsmanage.wsgi.application"
# Default URL to redirect to after authentication
LOGIN_REDIRECT_URL = "/"
LOGIN_URL = "/auth/login/"
# Internationalization
# https://docs.djangoproject.com/en/3.1/topics/i18n/
LANGUAGE_CODE = "en-us"
TIME_ZONE = "America/New_York"
USE_I18N = False
USE_L10N = True
USE_TZ = True
USE_DEPRECATED_PYTZ = False
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.1/howto/static-files/
STATIC_URL = "/static/"
STATICFILES_DIRS = [BASE_DIR / "static"]
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"my_console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"": {
"handlers": ["my_console"],
"level": "WARNING",
},
},
}
MEDIA_ROOT = "media"
MEDIA_URL = "media/"
SENDFILE_ROOT = str(Path(__file__).parents[2] / "media" / "protected")
WIKI_URL = "https://wiki.claremontmakerspace.org"
# Django Rest Framework
REST_FRAMEWORK = {
# Use Django's standard `django.contrib.auth` permissions
"DEFAULT_PERMISSION_CLASSES": [
"cmsmanage.drf_permissions.DjangoModelPermissionsWithView"
],
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
}
# Django Q
Q_CLUSTER = {
"name": "cmsmanage",
"orm": "default",
"retry": 60 * 6,
"timeout": 60 * 5,
"catch_up": False,
"error_reporter": {"admin_email": {}},
"ack_failures": True,
"max_attempts": 1,
"ALT_CLUSTERS": {
"internal": {
"retry": 60 * 60,
"timeout": 60 * 59,
},
},
}
# Django-Tables2
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5-responsive.html"
DJANGO_TABLES2_TABLE_ATTRS = {"class": "table mx-auto w-auto"}

View File

@ -1,38 +0,0 @@
from hypothesis import settings
from .base import * # noqa: F403
from .hypothesis import configure_hypothesis_profiles
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "aed7jee2kai1we9eithae0gaegh9ohthoh4phahk5bau4Ahxaijo3aicheex3qua"
# Database
# https://docs.djangoproject.com/en/3.1/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"HOST": "mariadb",
"NAME": "CMS_Database",
"USER": "root",
"PASSWORD": "whatever",
"OPTIONS": {
"charset": "utf8mb4",
},
},
"membershipworks": {
"ENGINE": "django.db.backends.mysql",
"HOST": "mariadb",
"NAME": "membershipworks",
"USER": "root",
"PASSWORD": "whatever",
"OPTIONS": {
"charset": "utf8mb4",
},
},
}
configure_hypothesis_profiles()
settings.load_profile("ci")

View File

@ -1,25 +0,0 @@
from .dev_base import * # noqa: F403
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = "+>+4?MO:*@`KFF?($O}F+<dI/oE'/8V?s%d?fpL5_UF_703}*0&g04BFPqkl&`Tz"
EMAIL_BACKEND = "django.core.mail.backends.filebased.EmailBackend"
EMAIL_FILE_PATH = "./email-messages" # change this to a proper location
# Database
# https://docs.djangoproject.com/en/4.0/ref/settings/#databases
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "default",
},
# Currently a separate database, for historical reasons
"membershipworks": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": "membershipworks",
},
}

View File

@ -1,25 +0,0 @@
import os
from hypothesis import settings
from .base import * # noqa: F403
from .hypothesis import configure_hypothesis_profiles
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
INTERNAL_IPS = ["127.0.0.1"]
INSTALLED_APPS.append("debug_toolbar") # noqa: F405
INSTALLED_APPS.append("django_extensions") # noqa: F405
MIDDLEWARE.insert(0, "debug_toolbar.middleware.DebugToolbarMiddleware") # noqa: F405
configure_hypothesis_profiles()
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "dev"))
SENDFILE_BACKEND = "django_sendfile.backends.development"

View File

@ -1,7 +0,0 @@
from hypothesis import HealthCheck, Verbosity, settings
def configure_hypothesis_profiles():
settings.register_profile("ci", suppress_health_check=(HealthCheck.too_slow,))
settings.register_profile("dev", max_examples=20)
settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose)

View File

@ -1,53 +0,0 @@
import ldap
from django_auth_ldap.config import LDAPGroupQuery, LDAPSearch, PosixGroupType
from .base import * # noqa: F403
DEBUG = False
# LDAP Authentication
# https://django-auth-ldap.readthedocs.io/en/latest/
# "AUTH_LDAP_SERVER_URI", "AUTH_LDAP_BIND_DN", and "AUTH_LDAP_BIND_PASSWORD" set in prod.py
AUTHENTICATION_BACKENDS = [
"django_auth_ldap.backend.LDAPBackend",
"django.contrib.auth.backends.ModelBackend",
]
AUTH_LDAP_USER_SEARCH = LDAPSearch(
"cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org",
ldap.SCOPE_SUBTREE,
"(uid=%(user)s)",
)
AUTH_LDAP_USER_ATTR_MAP = {
"first_name": "givenName",
"last_name": "sn",
"email": "mail",
}
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
"is_staff": (
LDAPGroupQuery(
"cn=MW_CMS Staff,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
)
| LDAPGroupQuery(
"cn=MW_Database Admin,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
)
| LDAPGroupQuery(
"cn=MW_Database Access,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
)
)
}
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
"cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org",
ldap.SCOPE_SUBTREE,
"(objectClass=posixGroup)",
)
AUTH_LDAP_GROUP_TYPE = PosixGroupType()
AUTH_LDAP_MIRROR_GROUPS = True
SENDFILE_BACKEND = "django_sendfile.backends.nginx"
SENDFILE_URL = "/media/protected"

View File

@ -31,7 +31,7 @@ router.registry.extend(membershipworks_router.registry)
urlpatterns = [
path("", include("dashboard.urls")),
path("tasks/", include("tasks.urls")),
# path("tasks/", include("tasks.urls")),
path("rentals/", include("rentals.urls")),
path("membershipworks/", include("membershipworks.urls")),
path("paperwork/", include("paperwork.urls")),
@ -59,7 +59,7 @@ urlpatterns = [
),
),
path("api-auth/", include("rest_framework.urls")),
path("markdownx/", include("markdownx.urls")),
# path("markdownx/", include("markdownx.urls")),
]
if settings.DEBUG:

View File

@ -9,8 +9,9 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
import os
from django.core.wsgi import get_wsgi_application
from configurations.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cmsmanage.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Dev")
application = get_wsgi_application()

View File

@ -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

View File

@ -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()
]
}

View File

@ -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):

View File

@ -7,9 +7,11 @@ import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cmsmanage.settings.dev")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cmsmanage.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Dev")
try:
from django.core.management import execute_from_command_line
from configurations.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "

View File

@ -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"
)

View File

@ -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"""
@ -269,6 +281,22 @@ class MembershipWorks:
)
return r.json()
def get_event_registrations(self, event_id: str):
r = self._get_v1(
BASE_URL + "/v1/csv",
params={
"_rt": "946702800", # unknown
"evt": event_id,
},
)
if r.status_code != requests.codes.ok:
raise MembershipWorksRemoteError("csv generation", r)
if r.text[0] == "\ufeff":
r.encoding = r.encoding + "-sig"
return list(csv.DictReader(StringIO(r.text)))
def get_event_by_url(self, url: str):
"""Retrieve a specific event by its url"""
r = self.sess.get(

View File

@ -0,0 +1,22 @@
# Generated by Django 5.0.4 on 2024-04-30 05:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("membershipworks", "0016_eventinvoice"),
]
operations = [
migrations.AddField(
model_name="eventext",
name="registrations",
field=models.JSONField(blank=True, null=True),
),
migrations.AlterField(
model_name="eventinvoice",
name="pdf",
field=models.FileField(upload_to="protected/invoices/%Y/%m/%d/"),
),
]

View File

@ -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(),
),
)
@ -551,6 +550,7 @@ class EventExt(Event):
max_digits=13, decimal_places=4, default=0
)
details = models.JSONField(null=True, blank=True)
registrations = models.JSONField(null=True, blank=True)
def get_absolute_url(self) -> str:
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})
@ -573,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:
@ -581,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"

View File

@ -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])
@ -110,6 +111,7 @@ def scrape_event_details(queryset: QuerySet[EventExt]):
for event in queryset:
event.details = membershipworks.get_event_by_eid(event.eid)
event.registrations = membershipworks.get_event_registrations(event.eid)
event.save()
@ -171,4 +173,5 @@ def scrape_events():
event_ext.end or event_ext.start
):
event_ext.details = membershipworks.get_event_by_eid(event.eid)
event_ext.registrations = membershipworks.get_event_registrations(event.eid)
event_ext.save()

View File

@ -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"
@ -412,7 +412,6 @@ class EventInvoicePDFView(AccessMixin, BaseDetailView):
if request.user.has_perm(
"membershipworks.view_eventinvoice"
) or invoice.event.user_is_instructor(request.user):
# return HttpResponse(invoice.pdf.path)
return sendfile(request, invoice.pdf.path, mimetype="application/pdf")
else:

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.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": {

View File

@ -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"],
)

View File

@ -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):

View File

@ -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,

253
pdm.lock
View File

@ -5,7 +5,7 @@
groups = ["default", "debug", "lint", "server", "typing", "dev"]
strategy = ["cross_platform"]
lock_version = "4.4.1"
content_hash = "sha256:503ddceb3ef04537dbeea6dc15d2ce2c46bc4ade0a2e394938224f6da4033c81"
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"
@ -449,6 +459,28 @@ files = [
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
]
[[package]]
name = "dj-database-url"
version = "2.1.0"
summary = "Use Database URLs in your Django Application."
dependencies = [
"Django>=3.2",
"typing-extensions>=3.10.0.0",
]
files = [
{file = "dj-database-url-2.1.0.tar.gz", hash = "sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f"},
{file = "dj_database_url-2.1.0-py3-none-any.whl", hash = "sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0"},
]
[[package]]
name = "dj-email-url"
version = "1.0.6"
summary = "Use an URL to configure email backend settings in your Django Application."
files = [
{file = "dj-email-url-1.0.6.tar.gz", hash = "sha256:55ffe3329e48f54f8a75aa36ece08f365e09d61f8a209773ef09a1d4760e699a"},
{file = "dj_email_url-1.0.6-py2.py3-none-any.whl", hash = "sha256:cbd08327fbb08b104eac160fb4703f375532e4c0243eb230f5b960daee7a96db"},
]
[[package]]
name = "django"
version = "5.0.4"
@ -515,6 +547,35 @@ files = [
{file = "django_bootstrap5-24.2.tar.gz", hash = "sha256:a3cee2b3d45745210c5b898af2917f310f44df746269fe09a93be28a0adc2a4b"},
]
[[package]]
name = "django-configurations"
version = "2.5.1"
requires_python = "<4.0,>=3.8"
summary = "A helper for organizing Django settings."
dependencies = [
"django>=3.2",
]
files = [
{file = "django-configurations-2.5.1.tar.gz", hash = "sha256:6e5083757e2bbdf9bb7850567536b96a93515f6b17503d74928ff628db2e0e94"},
{file = "django_configurations-2.5.1-py3-none-any.whl", hash = "sha256:ceb84858da2dac846b15e715c2fd936cfc4c7917c074aff8d31700564093955e"},
]
[[package]]
name = "django-configurations"
version = "2.5.1"
extras = ["database", "email"]
requires_python = "<4.0,>=3.8"
summary = "A helper for organizing Django settings."
dependencies = [
"dj-database-url",
"dj-email-url",
"django-configurations==2.5.1",
]
files = [
{file = "django-configurations-2.5.1.tar.gz", hash = "sha256:6e5083757e2bbdf9bb7850567536b96a93515f6b17503d74928ff628db2e0e94"},
{file = "django_configurations-2.5.1-py3-none-any.whl", hash = "sha256:ceb84858da2dac846b15e715c2fd936cfc4c7917c074aff8d31700564093955e"},
]
[[package]]
name = "django-db-views"
version = "0.1.6"
@ -673,24 +734,24 @@ files = [
[[package]]
name = "django-stubs"
version = "4.2.7"
version = "5.0.0"
requires_python = ">=3.8"
summary = "Mypy stubs for Django"
dependencies = [
"asgiref",
"django",
"django-stubs-ext>=4.2.7",
"django-stubs-ext>=5.0.0",
"types-PyYAML",
"types-pytz",
"typing-extensions",
]
files = [
{file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"},
{file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"},
{file = "django_stubs-5.0.0-py3-none-any.whl", hash = "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d"},
{file = "django_stubs-5.0.0.tar.gz", hash = "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"},
]
[[package]]
name = "django-stubs-ext"
version = "4.2.7"
version = "5.0.0"
requires_python = ">=3.8"
summary = "Monkey-patching and extensions for django-stubs"
dependencies = [
@ -698,23 +759,23 @@ dependencies = [
"typing-extensions",
]
files = [
{file = "django-stubs-ext-4.2.7.tar.gz", hash = "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3"},
{file = "django_stubs_ext-4.2.7-py3-none-any.whl", hash = "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c"},
{file = "django_stubs_ext-5.0.0-py3-none-any.whl", hash = "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8"},
{file = "django_stubs_ext-5.0.0.tar.gz", hash = "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115"},
]
[[package]]
name = "django-stubs"
version = "4.2.7"
version = "5.0.0"
extras = ["compatible-mypy"]
requires_python = ">=3.8"
summary = "Mypy stubs for Django"
dependencies = [
"django-stubs==4.2.7",
"mypy~=1.7.0",
"django-stubs==5.0.0",
"mypy~=1.10.0",
]
files = [
{file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"},
{file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"},
{file = "django_stubs-5.0.0-py3-none-any.whl", hash = "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d"},
{file = "django_stubs-5.0.0.tar.gz", hash = "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"},
]
[[package]]
@ -768,35 +829,35 @@ files = [
[[package]]
name = "djangorestframework-stubs"
version = "3.14.5"
version = "3.15.0"
requires_python = ">=3.8"
summary = "PEP-484 stubs for django-rest-framework"
dependencies = [
"django-stubs>=4.2.7",
"django-stubs>=5.0.0",
"requests>=2.0.0",
"types-PyYAML>=5.4.3",
"types-requests>=0.1.12",
"typing-extensions>=3.10.0",
]
files = [
{file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"},
{file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"},
{file = "djangorestframework_stubs-3.15.0-py3-none-any.whl", hash = "sha256:6c634f16fe1f9b1654cfd921eca64cd4188ce8534ab5e3ec7e44aaa0ca969d93"},
{file = "djangorestframework_stubs-3.15.0.tar.gz", hash = "sha256:f60ee1c80abb01a77acc0169969e07c45c2739ae64667b9a0dd4a2e32697dcab"},
]
[[package]]
name = "djangorestframework-stubs"
version = "3.14.5"
version = "3.15.0"
extras = ["compatible-mypy"]
requires_python = ">=3.8"
summary = "PEP-484 stubs for django-rest-framework"
dependencies = [
"django-stubs[compatible-mypy]",
"djangorestframework-stubs==3.14.5",
"mypy~=1.7.0",
"djangorestframework-stubs==3.15.0",
"mypy~=1.10.0",
]
files = [
{file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"},
{file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"},
{file = "djangorestframework_stubs-3.15.0-py3-none-any.whl", hash = "sha256:6c634f16fe1f9b1654cfd921eca64cd4188ce8534ab5e3ec7e44aaa0ca969d93"},
{file = "djangorestframework_stubs-3.15.0.tar.gz", hash = "sha256:f60ee1c80abb01a77acc0169969e07c45c2739ae64667b9a0dd4a2e32697dcab"},
]
[[package]]
@ -1186,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"
@ -1336,7 +1388,7 @@ files = [
[[package]]
name = "mypy"
version = "1.7.1"
version = "1.10.0"
requires_python = ">=3.8"
summary = "Optional static typing for Python"
dependencies = [
@ -1344,18 +1396,18 @@ dependencies = [
"typing-extensions>=4.1.0",
]
files = [
{file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"},
{file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"},
{file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"},
{file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"},
{file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"},
{file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"},
{file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"},
{file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"},
{file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"},
{file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"},
{file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"},
{file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"},
{file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
{file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
{file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
{file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
{file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
{file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
{file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
{file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
{file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
{file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
{file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
{file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
]
[[package]]
@ -1599,12 +1651,12 @@ files = [
[[package]]
name = "pydyf"
version = "0.8.0"
requires_python = ">=3.7"
version = "0.10.0"
requires_python = ">=3.8"
summary = "A low-level PDF generator."
files = [
{file = "pydyf-0.8.0-py3-none-any.whl", hash = "sha256:901186a2e9f897108139426a6486f5225bdcc9b70be2ec965f25111e42f8ac5d"},
{file = "pydyf-0.8.0.tar.gz", hash = "sha256:b22b1ef016141b54941ad66ed4e036a7bdff39c0b360993b283875c3f854dd9a"},
{file = "pydyf-0.10.0-py3-none-any.whl", hash = "sha256:ef76b6c0976a091a9e15827fb5800e5e37e7cd1a3ca4d4bd19d10a14ea8c0ae3"},
{file = "pydyf-0.10.0.tar.gz", hash = "sha256:357194593efaf61d7b48ab97c3d59722114934967c3df3d7878ca6dd25b04c30"},
]
[[package]]
@ -1925,15 +1977,15 @@ files = [
[[package]]
name = "tinycss2"
version = "1.1.1"
requires_python = ">=3.6"
version = "1.3.0"
requires_python = ">=3.8"
summary = "A tiny CSS parser"
dependencies = [
"webencodings>=0.4",
]
files = [
{file = "tinycss2-1.1.1-py3-none-any.whl", hash = "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8"},
{file = "tinycss2-1.1.1.tar.gz", hash = "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf"},
{file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"},
{file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"},
]
[[package]]
@ -1959,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"
@ -1972,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"
@ -1983,12 +2058,52 @@ files = [
]
[[package]]
name = "types-pytz"
version = "2022.6.0.1"
summary = "Typing stubs for pytz"
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-pytz-2022.6.0.1.tar.gz", hash = "sha256:d078196374d1277e9f9984d49373ea043cf2c64d5d5c491fbc86c258557bd46f"},
{file = "types_pytz-2022.6.0.1-py3-none-any.whl", hash = "sha256:bea605ce5d5a5d52a8e1afd7656c9b42476e18a0f888de6be91587355313ddf4"},
{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]]
@ -2013,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"
@ -2186,8 +2311,8 @@ files = [
[[package]]
name = "weasyprint"
version = "61.2"
requires_python = ">=3.8"
version = "62.0"
requires_python = ">=3.9"
summary = "The Awesome Document Factory"
dependencies = [
"Pillow>=9.1.0",
@ -2196,12 +2321,12 @@ dependencies = [
"cssselect2>=0.1",
"fonttools[woff]>=4.0.0",
"html5lib>=1.1",
"pydyf>=0.8.0",
"tinycss2>=1.0.0",
"pydyf>=0.10.0",
"tinycss2>=1.3.0",
]
files = [
{file = "weasyprint-61.2-py3-none-any.whl", hash = "sha256:76c6dc0e75e09182d5645d92c66ddf86b1b992c9420235b723fb374b584e5bf4"},
{file = "weasyprint-61.2.tar.gz", hash = "sha256:47df6cfeeff8c6c28cf2e4caf837cde17715efe462708ada74baa2eb391b6059"},
{file = "weasyprint-62.0-py3-none-any.whl", hash = "sha256:021b2fcde720756dd496645f251e40240b742e91a85f446f612f4666fdca316d"},
{file = "weasyprint-62.0.tar.gz", hash = "sha256:9f56eaefdaafcf35ae568c56eee5dba189af1272a46f4d205bd12936315f9481"},
]
[[package]]

View File

@ -12,13 +12,13 @@ dependencies = [
"django-markdownx~=4.0",
"django-recurrence~=1.11",
"django-widget-tweaks~=1.5",
"django-stubs-ext~=4.2",
"django-stubs-ext~=5.0",
"markdownify~=0.12",
"mdformat~=0.7",
"mdformat-tables~=0.4",
"mysqlclient~=2.2",
"django-autocomplete-light~=3.11",
"weasyprint~=61.2",
"weasyprint~=62.0",
"requests~=2.31",
"semver~=3.0",
"djangorestframework~=3.15",
@ -38,6 +38,7 @@ dependencies = [
"django-weasyprint~=2.3",
"django-sendfile2~=0.7",
"django-bootstrap5~=24.2",
"django-configurations[database,email]~=2.5",
]
requires-python = ">=3.11"
@ -89,13 +90,14 @@ indent_size = 2
[tool.mypy]
plugins = [
"./cmsmanage/mypy_django_configurations_plugin.py",
"mypy_django_plugin.main",
"mypy_drf_plugin.main",
]
[tool.django-stubs]
django_settings_module = "cmsmanage.settings.dev"
django_settings_module = "cmsmanage.settings"
strict_settings = false
[[tool.pdm.source]]
url = "https://pypi.org/simple"
@ -114,14 +116,17 @@ lint = [
"ruff~=0.4",
]
typing = [
"mypy~=1.7",
"django-stubs~=4.2",
"mypy~=1.10",
"django-stubs~=5.0",
"setuptools~=69.5",
"types-bleach~=6.1",
"types-requests~=2.31",
"types-urllib3~=1.26",
"djangorestframework-stubs[compatible-mypy]~=3.14",
"lxml-stubs~=0.5",
"djangorestframework-stubs[compatible-mypy]~=3.15",
"types-Markdown~=3.6",
"types-Pygments~=2.17",
"types-psycopg2~=2.9",
"types-lxml~=2024.4",
]
debug = [
"django-debug-toolbar~=4.3",

View File

@ -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 --",