Compare commits
18 Commits
79651731b1
...
8b1722d1f0
Author | SHA1 | Date | |
---|---|---|---|
8b1722d1f0 | |||
df4c5564c4 | |||
1310e72e3f | |||
281c882a82 | |||
7236b55467 | |||
9c2084903f | |||
25fbe3d352 | |||
04ca92b5fe | |||
0944dd7992 | |||
9658366d72 | |||
a2c0707263 | |||
785a445b43 | |||
ee2d63f784 | |||
12eb4038bc | |||
c2b1da743c | |||
99060a8a43 | |||
59cade1cfd | |||
f8a4b425af |
@ -2,7 +2,7 @@ name: Test
|
|||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DJANGO_SETTINGS_MODULE: cmsmanage.settings.ci
|
DJANGO_CONFIGURATION: CI
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
@ -28,6 +28,6 @@ jobs:
|
|||||||
sudo apt-get update &&
|
sudo apt-get update &&
|
||||||
sudo apt-get -y install build-essential python3-dev libldap2-dev libsasl2-dev mariadb-client
|
sudo apt-get -y install build-essential python3-dev libldap2-dev libsasl2-dev mariadb-client
|
||||||
- name: Install python dependencies
|
- name: Install python dependencies
|
||||||
run: pdm sync -d
|
run: pdm sync -d -G dev
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: pdm run -v ./manage.py test
|
run: pdm run -v ./manage.py test
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ __pycache__/
|
|||||||
/__pypackages__/
|
/__pypackages__/
|
||||||
/markdownx/
|
/markdownx/
|
||||||
/media/
|
/media/
|
||||||
|
/settings.*.env
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.5.0
|
rev: v4.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@ -14,13 +14,13 @@ repos:
|
|||||||
- id: djlint-reformat-django
|
- id: djlint-reformat-django
|
||||||
|
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
rev: v0.3.0
|
rev: v0.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: ruff
|
- id: ruff
|
||||||
- id: ruff-format
|
- id: ruff-format
|
||||||
|
|
||||||
- repo: https://github.com/pdm-project/pdm
|
- repo: https://github.com/pdm-project/pdm
|
||||||
rev: 2.12.4
|
rev: 2.15.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pdm-lock-check
|
- id: pdm-lock-check
|
||||||
|
|
||||||
|
@ -9,8 +9,9 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/
|
|||||||
|
|
||||||
import os
|
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_SETTINGS_MODULE", "cmsmanage.settings")
|
||||||
|
os.environ.setdefault("DJANGO_CONFIGURATION", "DEV")
|
||||||
|
|
||||||
application = get_asgi_application()
|
application = get_asgi_application()
|
||||||
|
12
cmsmanage/mypy_django_configurations_plugin.py
Normal file
12
cmsmanage/mypy_django_configurations_plugin.py
Normal 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)
|
327
cmsmanage/settings.py
Normal file
327
cmsmanage/settings.py
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
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"""
|
||||||
|
|
||||||
|
CSRF_TRUSTED_ORIGINS = values.ListValue([])
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
2
cmsmanage/settings/.gitignore
vendored
2
cmsmanage/settings/.gitignore
vendored
@ -1,2 +0,0 @@
|
|||||||
dev.py
|
|
||||||
prod.py
|
|
@ -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"}
|
|
@ -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")
|
|
@ -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",
|
|
||||||
},
|
|
||||||
}
|
|
@ -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"
|
|
@ -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)
|
|
@ -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"
|
|
@ -31,7 +31,7 @@ router.registry.extend(membershipworks_router.registry)
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", include("dashboard.urls")),
|
path("", include("dashboard.urls")),
|
||||||
path("tasks/", include("tasks.urls")),
|
# path("tasks/", include("tasks.urls")),
|
||||||
path("rentals/", include("rentals.urls")),
|
path("rentals/", include("rentals.urls")),
|
||||||
path("membershipworks/", include("membershipworks.urls")),
|
path("membershipworks/", include("membershipworks.urls")),
|
||||||
path("paperwork/", include("paperwork.urls")),
|
path("paperwork/", include("paperwork.urls")),
|
||||||
@ -59,7 +59,7 @@ urlpatterns = [
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
path("api-auth/", include("rest_framework.urls")),
|
path("api-auth/", include("rest_framework.urls")),
|
||||||
path("markdownx/", include("markdownx.urls")),
|
# path("markdownx/", include("markdownx.urls")),
|
||||||
]
|
]
|
||||||
|
|
||||||
if settings.DEBUG:
|
if settings.DEBUG:
|
||||||
|
@ -9,8 +9,9 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
|
|||||||
|
|
||||||
import os
|
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_SETTINGS_MODULE", "cmsmanage.settings")
|
||||||
|
os.environ.setdefault("DJANGO_CONFIGURATION", "Dev")
|
||||||
|
|
||||||
application = get_wsgi_application()
|
application = get_wsgi_application()
|
||||||
|
@ -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):
|
||||||
|
@ -7,9 +7,11 @@ import sys
|
|||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Run administrative tasks."""
|
"""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:
|
try:
|
||||||
from django.core.management import execute_from_command_line
|
from configurations.management import execute_from_command_line
|
||||||
except ImportError as exc:
|
except ImportError as exc:
|
||||||
raise ImportError(
|
raise ImportError(
|
||||||
"Couldn't import Django. Are you sure it's installed and "
|
"Couldn't import Django. Are you sure it's installed and "
|
||||||
|
@ -108,11 +108,6 @@ class EventMeetingTimeInline(admin.TabularInline):
|
|||||||
|
|
||||||
readonly_fields = ["duration"]
|
readonly_fields = ["duration"]
|
||||||
|
|
||||||
# TODO: remove when switched to GeneratedField
|
|
||||||
@admin.display()
|
|
||||||
def duration(self, obj):
|
|
||||||
return obj.duration
|
|
||||||
|
|
||||||
|
|
||||||
@admin.register(EventInstructor)
|
@admin.register(EventInstructor)
|
||||||
class EventInstructorAdmin(admin.ModelAdmin):
|
class EventInstructorAdmin(admin.ModelAdmin):
|
||||||
@ -144,7 +139,7 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|||||||
show_facets = admin.ShowFacets.ALWAYS
|
show_facets = admin.ShowFacets.ALWAYS
|
||||||
search_fields = ["eid", "title", "url"]
|
search_fields = ["eid", "title", "url"]
|
||||||
date_hierarchy = "start"
|
date_hierarchy = "start"
|
||||||
exclude = ["url", "details"]
|
exclude = ["url", "details", "registrations"]
|
||||||
autocomplete_fields = ["instructor"]
|
autocomplete_fields = ["instructor"]
|
||||||
change_actions = ["fetch_details"]
|
change_actions = ["fetch_details"]
|
||||||
actions = ["fetch_details"]
|
actions = ["fetch_details"]
|
||||||
@ -160,7 +155,7 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|||||||
else:
|
else:
|
||||||
fields.append(field.name)
|
fields.append(field.name)
|
||||||
fields.insert(fields.index("end") + 1, "duration")
|
fields.insert(fields.index("end") + 1, "duration")
|
||||||
fields.append("_details_timestamp")
|
fields.append("details_timestamp")
|
||||||
return fields
|
return fields
|
||||||
|
|
||||||
@admin.display(ordering="title")
|
@admin.display(ordering="title")
|
||||||
@ -178,10 +173,6 @@ class EventAdmin(DjangoObjectActions, admin.ModelAdmin):
|
|||||||
obj.url,
|
obj.url,
|
||||||
)
|
)
|
||||||
|
|
||||||
@admin.display(description="Last details fetch")
|
|
||||||
def _details_timestamp(self, obj):
|
|
||||||
return naturaltime(obj.details_timestamp)
|
|
||||||
|
|
||||||
@takes_instance_or_queryset
|
@takes_instance_or_queryset
|
||||||
def fetch_details(self, request, queryset):
|
def fetch_details(self, request, queryset):
|
||||||
scrape_event_details(queryset)
|
scrape_event_details(queryset)
|
||||||
|
@ -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"""
|
||||||
@ -269,6 +281,22 @@ class MembershipWorks:
|
|||||||
)
|
)
|
||||||
return r.json()
|
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):
|
def get_event_by_url(self, url: str):
|
||||||
"""Retrieve a specific event by its url"""
|
"""Retrieve a specific event by its url"""
|
||||||
r = self.sess.get(
|
r = self.sess.get(
|
||||||
|
@ -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/"),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.0.6 on 2024-05-08 16:44
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("membershipworks", "0017_eventext_registrations_alter_eventinvoice_pdf"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="eventext",
|
||||||
|
name="details_timestamp",
|
||||||
|
field=models.GeneratedField(
|
||||||
|
db_persist=False,
|
||||||
|
expression=models.Func(
|
||||||
|
models.Func(models.F("details___ts"), function="FROM_UNIXTIME"),
|
||||||
|
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
|
||||||
|
),
|
||||||
|
output_field=models.DateTimeField(),
|
||||||
|
verbose_name="Last details fetch",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -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,19 +517,9 @@ 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
|
|
||||||
# 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 +542,17 @@ class EventExt(Event):
|
|||||||
max_digits=13, decimal_places=4, default=0
|
max_digits=13, decimal_places=4, default=0
|
||||||
)
|
)
|
||||||
details = models.JSONField(null=True, blank=True)
|
details = models.JSONField(null=True, blank=True)
|
||||||
|
details_timestamp = models.GeneratedField(
|
||||||
|
expression=Func(
|
||||||
|
Func(F("details___ts"), function="FROM_UNIXTIME"),
|
||||||
|
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
|
||||||
|
),
|
||||||
|
output_field=models.DateTimeField(),
|
||||||
|
db_persist=False,
|
||||||
|
verbose_name="Last details fetch",
|
||||||
|
)
|
||||||
|
|
||||||
|
registrations = models.JSONField(null=True, blank=True)
|
||||||
|
|
||||||
def get_absolute_url(self) -> str:
|
def get_absolute_url(self) -> str:
|
||||||
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})
|
return reverse("membershipworks:event-detail", kwargs={"eid": self.eid})
|
||||||
@ -573,7 +575,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:
|
||||||
@ -581,6 +583,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"
|
||||||
|
@ -65,6 +65,30 @@ class EventTable(tables.Table):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class EventRegistrationsTable(tables.Table):
|
||||||
|
ticket_count = tables.Column(empty_values=())
|
||||||
|
name = tables.Column(accessor="Full name")
|
||||||
|
email = tables.EmailColumn(accessor="Email")
|
||||||
|
phone = tables.Column(accessor="Phone")
|
||||||
|
emergency_contact_name = tables.Column(accessor="Emergency Contact Name:")
|
||||||
|
emergency_contact_phone_number = tables.Column(
|
||||||
|
accessor="Emergency Contact Phone Number:"
|
||||||
|
)
|
||||||
|
emergency_contact_relation = tables.Column(accessor="Emergency Contact Relation:")
|
||||||
|
|
||||||
|
def render_ticket_count(self, record):
|
||||||
|
return sum(int(v) for k, v in record.items() if k.startswith("Ticket: "))
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
row_attrs = {
|
||||||
|
"class": lambda table, record: (
|
||||||
|
""
|
||||||
|
if table.render_ticket_count(record) > 0
|
||||||
|
else "text-decoration-line-through table-danger"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class EventSummaryTable(tables.Table):
|
class EventSummaryTable(tables.Table):
|
||||||
event_count = tables.Column("Events")
|
event_count = tables.Column("Events")
|
||||||
canceled_event_count = tables.Column("Canceled Events")
|
canceled_event_count = tables.Column("Canceled Events")
|
||||||
|
@ -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])
|
||||||
@ -110,6 +111,7 @@ def scrape_event_details(queryset: QuerySet[EventExt]):
|
|||||||
|
|
||||||
for event in queryset:
|
for event in queryset:
|
||||||
event.details = membershipworks.get_event_by_eid(event.eid)
|
event.details = membershipworks.get_event_by_eid(event.eid)
|
||||||
|
event.registrations = membershipworks.get_event_registrations(event.eid)
|
||||||
event.save()
|
event.save()
|
||||||
|
|
||||||
|
|
||||||
@ -171,4 +173,5 @@ def scrape_events():
|
|||||||
event_ext.end or event_ext.start
|
event_ext.end or event_ext.start
|
||||||
):
|
):
|
||||||
event_ext.details = membershipworks.get_event_by_eid(event.eid)
|
event_ext.details = membershipworks.get_event_by_eid(event.eid)
|
||||||
|
event_ext.registrations = membershipworks.get_event_registrations(event.eid)
|
||||||
event_ext.save()
|
event_ext.save()
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
{% if perms.membershipworks.view_eventext %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'membershipworks:event-index-report' %}">MW Event Reports</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'membershipworks:event-year-report' event.start|date:"Y" %}">{{ event.start|date:"Y" }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'membershipworks:event-month-report' event.start|date:"Y" event.start|date:"m" %}">{{ event.start|date:"F" }}</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'membershipworks:user-events' %}">My Events</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
@ -7,8 +7,16 @@
|
|||||||
{% block admin_link %}
|
{% block admin_link %}
|
||||||
{% url 'admin:membershipworks_eventext_change' event.pk %}
|
{% url 'admin:membershipworks_eventext_change' event.pk %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{% include "./components/event_breadcrumbs.dj.html" %}
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">{{ event.details.ttl|nh3 }}</li>
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
{% if event.registrations is not None %}
|
||||||
|
{% url 'membershipworks:event-registrations' event.pk as registrations_url %}
|
||||||
|
{% bootstrap_button href=registrations_url content="Show Registrations" %}
|
||||||
|
{% endif %}
|
||||||
{% include "membershipworks/event_invoice.dj.html" %}
|
{% include "membershipworks/event_invoice.dj.html" %}
|
||||||
|
|
||||||
<div class="card w-auto mt-5">
|
<div class="card w-auto mt-5">
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
{% extends "base.dj.html" %}
|
||||||
|
|
||||||
|
{% load nh3_tags %}
|
||||||
|
{% load render_table from django_tables2 %}
|
||||||
|
{% load django_bootstrap5 %}
|
||||||
|
|
||||||
|
{% block title %}Registrations for {{ event.details.ttl|nh3 }}{% endblock %}
|
||||||
|
{% block admin_link %}
|
||||||
|
{% url 'admin:membershipworks_eventext_change' event.pk %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block breadcrumbs %}
|
||||||
|
{% include "./components/event_breadcrumbs.dj.html" %}
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="{% url 'membershipworks:event-detail' event.pk %}">{{ event.details.ttl|nh3|truncatechars_html:40 }}</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item active" aria-current="page">Registrations</li>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<div class="row justify-content-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
{% bootstrap_button extra_classes="btn-sm" href=email_link target="_blank" content="Email all attendees" %}
|
||||||
|
</div>
|
||||||
|
<div class="col-auto">{% include "cmsmanage/components/download_table.dj.html" %}</div>
|
||||||
|
</div>
|
||||||
|
{% render_table table %}
|
||||||
|
<p class="text-center">Data last updated {{ event.details_timestamp }}</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
@ -3,6 +3,7 @@
|
|||||||
{% load render_table from django_tables2 %}
|
{% load render_table from django_tables2 %}
|
||||||
|
|
||||||
{% block title %}My Events{% endblock %}
|
{% block title %}My Events{% endblock %}
|
||||||
|
{% block breadcrumbs %}<li class="breadcrumb-item active" aria-current="page">My Events</li>{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% include "cmsmanage/components/download_table.dj.html" %}
|
{% include "cmsmanage/components/download_table.dj.html" %}
|
||||||
|
|
||||||
|
@ -45,6 +45,11 @@ urlpatterns = [
|
|||||||
views.EventDetailView.as_view(),
|
views.EventDetailView.as_view(),
|
||||||
name="event-detail",
|
name="event-detail",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"event/<eid>/registrations",
|
||||||
|
views.EventRegistrationsView.as_view(),
|
||||||
|
name="event-registrations",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"event/invoice/<uuid:uuid>.pdf",
|
"event/invoice/<uuid:uuid>.pdf",
|
||||||
views.EventInvoicePDFView.as_view(),
|
views.EventInvoicePDFView.as_view(),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import quote, urlencode
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -32,7 +33,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
|
||||||
@ -46,6 +47,7 @@ from .invoice_email import make_invoice_emails
|
|||||||
from .models import EventAttendee, EventExt, EventInvoice, Member
|
from .models import EventAttendee, EventExt, EventInvoice, Member
|
||||||
from .tables import (
|
from .tables import (
|
||||||
EventAttendeeTable,
|
EventAttendeeTable,
|
||||||
|
EventRegistrationsTable,
|
||||||
EventSummaryTable,
|
EventSummaryTable,
|
||||||
EventTable,
|
EventTable,
|
||||||
InvoiceTable,
|
InvoiceTable,
|
||||||
@ -241,7 +243,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"
|
||||||
@ -412,13 +414,56 @@ class EventInvoicePDFView(AccessMixin, BaseDetailView):
|
|||||||
if request.user.has_perm(
|
if request.user.has_perm(
|
||||||
"membershipworks.view_eventinvoice"
|
"membershipworks.view_eventinvoice"
|
||||||
) or invoice.event.user_is_instructor(request.user):
|
) or invoice.event.user_is_instructor(request.user):
|
||||||
# return HttpResponse(invoice.pdf.path)
|
|
||||||
return sendfile(request, invoice.pdf.path, mimetype="application/pdf")
|
return sendfile(request, invoice.pdf.path, mimetype="application/pdf")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return self.handle_no_permission()
|
return self.handle_no_permission()
|
||||||
|
|
||||||
|
|
||||||
|
class EventRegistrationsView(ExportMixin, SingleTableMixin, AccessMixin, DetailView):
|
||||||
|
permission_required = "membershipworks.view_eventext"
|
||||||
|
model = EventExt
|
||||||
|
pk_url_kwarg = "eid"
|
||||||
|
context_object_name = "event"
|
||||||
|
template_name = "membershipworks/event_registrations.dj.html"
|
||||||
|
table_class = EventRegistrationsTable
|
||||||
|
export_formats = ("csv", "xlsx", "ods")
|
||||||
|
|
||||||
|
def render_to_response(
|
||||||
|
self, context: dict[str, Any], **response_kwargs: Any
|
||||||
|
) -> HttpResponse:
|
||||||
|
if self.request.user.has_perm(
|
||||||
|
self.permission_required
|
||||||
|
) or self.object.user_is_instructor(self.request.user):
|
||||||
|
return super().render_to_response(context, **response_kwargs)
|
||||||
|
else:
|
||||||
|
return self.handle_no_permission()
|
||||||
|
|
||||||
|
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
|
||||||
|
context_data = super().get_context_data(**kwargs)
|
||||||
|
|
||||||
|
context_data["email_link"] = "mailto:?" + urlencode(
|
||||||
|
{
|
||||||
|
"subject": f"[CMS Event] {self.object.title}",
|
||||||
|
"bcc": ",".join(
|
||||||
|
mail.message.sanitize_address(
|
||||||
|
(reg["Full name"], reg["Email"]), settings.DEFAULT_CHARSET
|
||||||
|
)
|
||||||
|
for reg in self.object.registrations
|
||||||
|
if any(
|
||||||
|
int(v) > 0 for k, v in reg.items() if k.startswith("Ticket: ")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
quote_via=quote,
|
||||||
|
)
|
||||||
|
|
||||||
|
return context_data
|
||||||
|
|
||||||
|
def get_table_data(self):
|
||||||
|
return self.object.registrations
|
||||||
|
|
||||||
|
|
||||||
class EventAttendeeFilters(django_filters.FilterSet):
|
class EventAttendeeFilters(django_filters.FilterSet):
|
||||||
new_since = django_filters.DateFilter(
|
new_since = django_filters.DateFilter(
|
||||||
field_name="event__start", method="filter_new_since"
|
field_name="event__start", method="filter_new_since"
|
||||||
|
@ -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,
|
||||||
|
357
pdm.lock
357
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:d2a8240d2754416c9c2c277832f3b8a2401eae3b56bda6e412aaf285c1c3b955"
|
content_hash = "sha256:651200bd58f4159fe99a599564e0f83a89fd149e5e7300abd151d6a3bb6477a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@ -224,15 +224,15 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitstring"
|
name = "bitstring"
|
||||||
version = "4.1.4"
|
version = "4.2.1"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.8"
|
||||||
summary = "Simple construction, analysis and modification of binary data."
|
summary = "Simple construction, analysis and modification of binary data."
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitarray<3.0.0,>=2.8.0",
|
"bitarray<3.0.0,>=2.9.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "bitstring-4.1.4-py3-none-any.whl", hash = "sha256:da46c4d6f8f3fb75a85566fdd33d5083ba8b8f268ed76f34eefe5a00da426192"},
|
{file = "bitstring-4.2.1-py3-none-any.whl", hash = "sha256:9ae5d89072b065d640d645d37c0efcd27284b2f79f1c48cc1cd38b54e1932b4f"},
|
||||||
{file = "bitstring-4.1.4.tar.gz", hash = "sha256:94f3f1c45383ebe8fd4a359424ffeb75c2f290760ae8fcac421b44f89ac85213"},
|
{file = "bitstring-4.2.1.tar.gz", hash = "sha256:8abb5a661588c764bacf1a23d64c7bb57517d2841e3e6f54fb8c057119e0540d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -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"
|
||||||
@ -449,9 +459,31 @@ files = [
|
|||||||
{file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"},
|
{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]]
|
[[package]]
|
||||||
name = "django"
|
name = "django"
|
||||||
version = "5.0.4"
|
version = "5.0.6"
|
||||||
requires_python = ">=3.10"
|
requires_python = ">=3.10"
|
||||||
summary = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
summary = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -460,8 +492,8 @@ dependencies = [
|
|||||||
"tzdata; sys_platform == \"win32\"",
|
"tzdata; sys_platform == \"win32\"",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "Django-5.0.4-py3-none-any.whl", hash = "sha256:916423499d75d62da7aa038d19aef23d23498d8df229775eb0a6309ee1013775"},
|
{file = "Django-5.0.6-py3-none-any.whl", hash = "sha256:8363ac062bb4ef7c3f12d078f6fa5d154031d129a15170a1066412af49d30905"},
|
||||||
{file = "Django-5.0.4.tar.gz", hash = "sha256:4bd01a8c830bb77a8a3b0e7d8b25b887e536ad17a81ba2dce5476135c73312bd"},
|
{file = "Django-5.0.6.tar.gz", hash = "sha256:ff1b61005004e476e0aeea47c7f79b85864c70124030e95146315396f1e7951f"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -504,28 +536,57 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-bootstrap5"
|
name = "django-bootstrap5"
|
||||||
version = "24.1"
|
version = "24.2"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Bootstrap 5 for Django"
|
summary = "Bootstrap 5 for Django"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Django>=3.2",
|
"Django>=4.2",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "django-bootstrap5-24.1.tar.gz", hash = "sha256:fc272b5bb218690fe6f32d52b23c155ebb46fbc5a2856c84eb353c1bf5fc49ea"},
|
{file = "django_bootstrap5-24.2-py3-none-any.whl", hash = "sha256:6a5d83e9ff1952f7c07c54cebcb76c85f09787b8b57eeb4ec07554cd583acc64"},
|
||||||
{file = "django_bootstrap5-24.1-py3-none-any.whl", hash = "sha256:c42b4f6e673d35af847486733da77104e1e98e7fd5caecc6d56f32a96cc77479"},
|
{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]]
|
[[package]]
|
||||||
name = "django-db-views"
|
name = "django-db-views"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
summary = "Handle database views. Allow to create migrations for database views. View migrations using django code. They can be reversed. Changes in model view definition are detected automatically. Support almost all options as regular makemigrations command"
|
summary = "Handle database views. Allow to create migrations for database views. View migrations using django code. They can be reversed. Changes in model view definition are detected automatically. Support almost all options as regular makemigrations command"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Django",
|
"Django",
|
||||||
"six",
|
"six",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "django-db-views-0.1.6.tar.gz", hash = "sha256:05718bb87c819323d577b294ee75f25807e5bb767793aa27f1ecc4c7ae073172"},
|
{file = "django-db-views-0.1.7.tar.gz", hash = "sha256:7c0dc78aa5f53cc4eefc4d880450a8cb61bb8376b5494356e20383f71a3e1657"},
|
||||||
{file = "django_db_views-0.1.6-py3-none-any.whl", hash = "sha256:1ae8a6b389a2e8a7a2e246050ce7688780343bf4fd4f9622263b607ae27e5524"},
|
{file = "django_db_views-0.1.7-py3-none-any.whl", hash = "sha256:ffa399af1678e60f532f8c2b531927e94e8249e1012a83fc865234384419bff8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -584,15 +645,15 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-mysql"
|
name = "django-mysql"
|
||||||
version = "4.12.0"
|
version = "4.13.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Django-MySQL extends Django's built-in MySQL and MariaDB support their specific features not available on other databases."
|
summary = "Django-MySQL extends Django's built-in MySQL and MariaDB support their specific features not available on other databases."
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Django>=3.2",
|
"Django>=3.2",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "django_mysql-4.12.0-py3-none-any.whl", hash = "sha256:1c188ee3a92590da21ce23351c309bb02df3b6611926521d312a9cdf6373c3d0"},
|
{file = "django_mysql-4.13.0-py3-none-any.whl", hash = "sha256:be7297fcaa65df109b270553881897adbdb41f67ceae308f6704544dd0a7d222"},
|
||||||
{file = "django_mysql-4.12.0.tar.gz", hash = "sha256:9a29b69ad30c85362984903379783b53731ee7b0cef4dfb4eb078f80e24f26d4"},
|
{file = "django_mysql-4.13.0.tar.gz", hash = "sha256:bb1e544ec1ff4b792a69785fb22bcd0e7c87646bd134843d3f0aad92d98e8eb7"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -673,24 +734,24 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-stubs"
|
name = "django-stubs"
|
||||||
version = "4.2.7"
|
version = "5.0.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Mypy stubs for Django"
|
summary = "Mypy stubs for Django"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"asgiref",
|
||||||
"django",
|
"django",
|
||||||
"django-stubs-ext>=4.2.7",
|
"django-stubs-ext>=5.0.0",
|
||||||
"types-PyYAML",
|
"types-PyYAML",
|
||||||
"types-pytz",
|
|
||||||
"typing-extensions",
|
"typing-extensions",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"},
|
{file = "django_stubs-5.0.0-py3-none-any.whl", hash = "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d"},
|
||||||
{file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"},
|
{file = "django_stubs-5.0.0.tar.gz", hash = "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-stubs-ext"
|
name = "django-stubs-ext"
|
||||||
version = "4.2.7"
|
version = "5.0.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Monkey-patching and extensions for django-stubs"
|
summary = "Monkey-patching and extensions for django-stubs"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -698,23 +759,23 @@ dependencies = [
|
|||||||
"typing-extensions",
|
"typing-extensions",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "django-stubs-ext-4.2.7.tar.gz", hash = "sha256:519342ac0849cda1559746c9a563f03ff99f636b0ebe7c14b75e816a00dfddc3"},
|
{file = "django_stubs_ext-5.0.0-py3-none-any.whl", hash = "sha256:8e1334fdf0c8bff87e25d593b33d4247487338aaed943037826244ff788b56a8"},
|
||||||
{file = "django_stubs_ext-4.2.7-py3-none-any.whl", hash = "sha256:45a5d102417a412e3606e3c358adb4744988a92b7b58ccf3fd64bddd5d04d14c"},
|
{file = "django_stubs_ext-5.0.0.tar.gz", hash = "sha256:5bacfbb498a206d5938454222b843d81da79ea8b6fcd1a59003f529e775bc115"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "django-stubs"
|
name = "django-stubs"
|
||||||
version = "4.2.7"
|
version = "5.0.0"
|
||||||
extras = ["compatible-mypy"]
|
extras = ["compatible-mypy"]
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Mypy stubs for Django"
|
summary = "Mypy stubs for Django"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django-stubs==4.2.7",
|
"django-stubs==5.0.0",
|
||||||
"mypy~=1.7.0",
|
"mypy~=1.10.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "django-stubs-4.2.7.tar.gz", hash = "sha256:8ccd2ff4ee5adf22b9e3b7b1a516d2e1c2191e9d94e672c35cc2bc3dd61e0f6b"},
|
{file = "django_stubs-5.0.0-py3-none-any.whl", hash = "sha256:084484cbe16a6d388e80ec687e46f529d67a232f3befaf55c936b3b476be289d"},
|
||||||
{file = "django_stubs-4.2.7-py3-none-any.whl", hash = "sha256:4cf4de258fa71adc6f2799e983091b9d46cfc67c6eebc68fe111218c9a62b3b8"},
|
{file = "django_stubs-5.0.0.tar.gz", hash = "sha256:b8a792bee526d6cab31e197cb414ee7fa218abd931a50948c66a80b3a2548621"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -768,35 +829,35 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djangorestframework-stubs"
|
name = "djangorestframework-stubs"
|
||||||
version = "3.14.5"
|
version = "3.15.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "PEP-484 stubs for django-rest-framework"
|
summary = "PEP-484 stubs for django-rest-framework"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django-stubs>=4.2.7",
|
"django-stubs>=5.0.0",
|
||||||
"requests>=2.0.0",
|
"requests>=2.0.0",
|
||||||
"types-PyYAML>=5.4.3",
|
"types-PyYAML>=5.4.3",
|
||||||
"types-requests>=0.1.12",
|
"types-requests>=0.1.12",
|
||||||
"typing-extensions>=3.10.0",
|
"typing-extensions>=3.10.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"},
|
{file = "djangorestframework_stubs-3.15.0-py3-none-any.whl", hash = "sha256:6c634f16fe1f9b1654cfd921eca64cd4188ce8534ab5e3ec7e44aaa0ca969d93"},
|
||||||
{file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"},
|
{file = "djangorestframework_stubs-3.15.0.tar.gz", hash = "sha256:f60ee1c80abb01a77acc0169969e07c45c2739ae64667b9a0dd4a2e32697dcab"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "djangorestframework-stubs"
|
name = "djangorestframework-stubs"
|
||||||
version = "3.14.5"
|
version = "3.15.0"
|
||||||
extras = ["compatible-mypy"]
|
extras = ["compatible-mypy"]
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "PEP-484 stubs for django-rest-framework"
|
summary = "PEP-484 stubs for django-rest-framework"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django-stubs[compatible-mypy]",
|
"django-stubs[compatible-mypy]",
|
||||||
"djangorestframework-stubs==3.14.5",
|
"djangorestframework-stubs==3.15.0",
|
||||||
"mypy~=1.7.0",
|
"mypy~=1.10.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "djangorestframework-stubs-3.14.5.tar.gz", hash = "sha256:5dd6f638aa5291fb7863e6166128a6ed20bf4986e2fc5cf334e6afc841797a09"},
|
{file = "djangorestframework_stubs-3.15.0-py3-none-any.whl", hash = "sha256:6c634f16fe1f9b1654cfd921eca64cd4188ce8534ab5e3ec7e44aaa0ca969d93"},
|
||||||
{file = "djangorestframework_stubs-3.14.5-py3-none-any.whl", hash = "sha256:43d788fd50cda49b922cd411e59c5b8cdc3f3de49c02febae12ce42139f0269b"},
|
{file = "djangorestframework_stubs-3.15.0.tar.gz", hash = "sha256:f60ee1c80abb01a77acc0169969e07c45c2739ae64667b9a0dd4a2e32697dcab"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1018,7 +1079,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hypothesis"
|
name = "hypothesis"
|
||||||
version = "6.100.1"
|
version = "6.100.5"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "A library for property-based testing"
|
summary = "A library for property-based testing"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -1026,23 +1087,23 @@ dependencies = [
|
|||||||
"sortedcontainers<3.0.0,>=2.1.0",
|
"sortedcontainers<3.0.0,>=2.1.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "hypothesis-6.100.1-py3-none-any.whl", hash = "sha256:3dacf6ec90e8d14aaee02cde081ac9a17d5b70105e45e6ac822db72052c0195b"},
|
{file = "hypothesis-6.100.5-py3-none-any.whl", hash = "sha256:d2f875a8791abdf68599e85cc9238f7239a73b72362d34be95e532e811766723"},
|
||||||
{file = "hypothesis-6.100.1.tar.gz", hash = "sha256:ebff09d7fa4f1fb6a855a812baf17e578b4481b7b70ec6d96496210d1a4c6c35"},
|
{file = "hypothesis-6.100.5.tar.gz", hash = "sha256:14e06081459ee96ca8f1ed996b6fc19f71910281e01f6a9fa3d9d6e68bbe4a25"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hypothesis"
|
name = "hypothesis"
|
||||||
version = "6.100.1"
|
version = "6.100.5"
|
||||||
extras = ["django"]
|
extras = ["django"]
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "A library for property-based testing"
|
summary = "A library for property-based testing"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"django>=3.2",
|
"django>=3.2",
|
||||||
"hypothesis==6.100.1",
|
"hypothesis==6.100.5",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "hypothesis-6.100.1-py3-none-any.whl", hash = "sha256:3dacf6ec90e8d14aaee02cde081ac9a17d5b70105e45e6ac822db72052c0195b"},
|
{file = "hypothesis-6.100.5-py3-none-any.whl", hash = "sha256:d2f875a8791abdf68599e85cc9238f7239a73b72362d34be95e532e811766723"},
|
||||||
{file = "hypothesis-6.100.1.tar.gz", hash = "sha256:ebff09d7fa4f1fb6a855a812baf17e578b4481b7b70ec6d96496210d1a4c6c35"},
|
{file = "hypothesis-6.100.5.tar.gz", hash = "sha256:14e06081459ee96ca8f1ed996b6fc19f71910281e01f6a9fa3d9d6e68bbe4a25"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1057,7 +1118,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipython"
|
name = "ipython"
|
||||||
version = "8.23.0"
|
version = "8.24.0"
|
||||||
requires_python = ">=3.10"
|
requires_python = ">=3.10"
|
||||||
summary = "IPython: Productive Interactive Computing"
|
summary = "IPython: Productive Interactive Computing"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -1070,11 +1131,11 @@ dependencies = [
|
|||||||
"pygments>=2.4.0",
|
"pygments>=2.4.0",
|
||||||
"stack-data",
|
"stack-data",
|
||||||
"traitlets>=5.13.0",
|
"traitlets>=5.13.0",
|
||||||
"typing-extensions; python_version < \"3.12\"",
|
"typing-extensions>=4.6; python_version < \"3.12\"",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "ipython-8.23.0-py3-none-any.whl", hash = "sha256:07232af52a5ba146dc3372c7bf52a0f890a23edf38d77caef8d53f9cdc2584c1"},
|
{file = "ipython-8.24.0-py3-none-any.whl", hash = "sha256:d7bf2f6c4314984e3e02393213bab8703cf163ede39672ce5918c51fe253a2a3"},
|
||||||
{file = "ipython-8.23.0.tar.gz", hash = "sha256:7468edaf4f6de3e1b912e57f66c241e6fd3c7099f2ec2136e239e142e800274d"},
|
{file = "ipython-8.24.0.tar.gz", hash = "sha256:010db3f8a728a578bb641fdd06c063b9fb8e96a9464c63aec6310fbcb5e80501"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1186,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"
|
||||||
@ -1336,7 +1388,7 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy"
|
name = "mypy"
|
||||||
version = "1.7.1"
|
version = "1.10.0"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.8"
|
||||||
summary = "Optional static typing for Python"
|
summary = "Optional static typing for Python"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
@ -1344,18 +1396,18 @@ dependencies = [
|
|||||||
"typing-extensions>=4.1.0",
|
"typing-extensions>=4.1.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "mypy-1.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b901927f16224d0d143b925ce9a4e6b3a758010673eeded9b748f250cf4e8f7"},
|
{file = "mypy-1.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1"},
|
||||||
{file = "mypy-1.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2f7f6985d05a4e3ce8255396df363046c28bea790e40617654e91ed580ca7c51"},
|
{file = "mypy-1.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee"},
|
||||||
{file = "mypy-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:944bdc21ebd620eafefc090cdf83158393ec2b1391578359776c00de00e8907a"},
|
{file = "mypy-1.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de"},
|
||||||
{file = "mypy-1.7.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9c7ac372232c928fff0645d85f273a726970c014749b924ce5710d7d89763a28"},
|
{file = "mypy-1.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7"},
|
||||||
{file = "mypy-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:f6efc9bd72258f89a3816e3a98c09d36f079c223aa345c659622f056b760ab42"},
|
{file = "mypy-1.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53"},
|
||||||
{file = "mypy-1.7.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6dbdec441c60699288adf051f51a5d512b0d818526d1dcfff5a41f8cd8b4aaf1"},
|
{file = "mypy-1.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b"},
|
||||||
{file = "mypy-1.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4fc3d14ee80cd22367caaaf6e014494415bf440980a3045bf5045b525680ac33"},
|
{file = "mypy-1.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30"},
|
||||||
{file = "mypy-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c6e4464ed5f01dc44dc9821caf67b60a4e5c3b04278286a85c067010653a0eb"},
|
{file = "mypy-1.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e"},
|
||||||
{file = "mypy-1.7.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:d9b338c19fa2412f76e17525c1b4f2c687a55b156320acb588df79f2e6fa9fea"},
|
{file = "mypy-1.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5"},
|
||||||
{file = "mypy-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:204e0d6de5fd2317394a4eff62065614c4892d5a4d1a7ee55b765d7a3d9e3f82"},
|
{file = "mypy-1.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda"},
|
||||||
{file = "mypy-1.7.1-py3-none-any.whl", hash = "sha256:f7c5d642db47376a0cc130f0de6d055056e010debdaf0707cd2b0fc7e7ef30ea"},
|
{file = "mypy-1.10.0-py3-none-any.whl", hash = "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee"},
|
||||||
{file = "mypy-1.7.1.tar.gz", hash = "sha256:fcb6d9afb1b6208b4c712af0dafdc650f518836065df0d4fb1d800f5d6773db2"},
|
{file = "mypy-1.10.0.tar.gz", hash = "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1599,12 +1651,12 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydyf"
|
name = "pydyf"
|
||||||
version = "0.8.0"
|
version = "0.10.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.8"
|
||||||
summary = "A low-level PDF generator."
|
summary = "A low-level PDF generator."
|
||||||
files = [
|
files = [
|
||||||
{file = "pydyf-0.8.0-py3-none-any.whl", hash = "sha256:901186a2e9f897108139426a6486f5225bdcc9b70be2ec965f25111e42f8ac5d"},
|
{file = "pydyf-0.10.0-py3-none-any.whl", hash = "sha256:ef76b6c0976a091a9e15827fb5800e5e37e7cd1a3ca4d4bd19d10a14ea8c0ae3"},
|
||||||
{file = "pydyf-0.8.0.tar.gz", hash = "sha256:b22b1ef016141b54941ad66ed4e036a7bdff39c0b360993b283875c3f854dd9a"},
|
{file = "pydyf-0.10.0.tar.gz", hash = "sha256:357194593efaf61d7b48ab97c3d59722114934967c3df3d7878ca6dd25b04c30"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1781,27 +1833,27 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.3.7"
|
version = "0.4.3"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.7"
|
||||||
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
summary = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"},
|
{file = "ruff-0.4.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b70800c290f14ae6fcbb41bbe201cf62dfca024d124a1f373e76371a007454ce"},
|
||||||
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"},
|
{file = "ruff-0.4.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08a0d6a22918ab2552ace96adeaca308833873a4d7d1d587bb1d37bae8728eb3"},
|
||||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba1f14df3c758dd7de5b55fbae7e1c8af238597961e5fb628f3de446c3c40c5"},
|
||||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:819fb06d535cc76dfddbfe8d3068ff602ddeb40e3eacbc90e0d1272bb8d97113"},
|
||||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0bfc9e955e6dc6359eb6f82ea150c4f4e82b660e5b58d9a20a0e42ec3bb6342b"},
|
||||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:510a67d232d2ebe983fddea324dbf9d69b71c4d2dfeb8a862f4a127536dd4cfb"},
|
||||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc9ff11cd9a092ee7680a56d21f302bdda14327772cd870d806610a3503d001f"},
|
||||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:29efff25bf9ee685c2c8390563a5b5c006a3fee5230d28ea39f4f75f9d0b6f2f"},
|
||||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"},
|
{file = "ruff-0.4.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18b00e0bcccf0fc8d7186ed21e311dffd19761cb632241a6e4fe4477cc80ef6e"},
|
||||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"},
|
{file = "ruff-0.4.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262f5635e2c74d80b7507fbc2fac28fe0d4fef26373bbc62039526f7722bca1b"},
|
||||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"},
|
{file = "ruff-0.4.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7363691198719c26459e08cc17c6a3dac6f592e9ea3d2fa772f4e561b5fe82a3"},
|
||||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"},
|
{file = "ruff-0.4.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eeb039f8428fcb6725bb63cbae92ad67b0559e68b5d80f840f11914afd8ddf7f"},
|
||||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"},
|
{file = "ruff-0.4.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:927b11c1e4d0727ce1a729eace61cee88a334623ec424c0b1c8fe3e5f9d3c865"},
|
||||||
{file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"},
|
{file = "ruff-0.4.3-py3-none-win32.whl", hash = "sha256:25cacda2155778beb0d064e0ec5a3944dcca9c12715f7c4634fd9d93ac33fd30"},
|
||||||
{file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"},
|
{file = "ruff-0.4.3-py3-none-win_amd64.whl", hash = "sha256:7a1c3a450bc6539ef00da6c819fb1b76b6b065dec585f91456e7c0d6a0bbc725"},
|
||||||
{file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"},
|
{file = "ruff-0.4.3-py3-none-win_arm64.whl", hash = "sha256:71ca5f8ccf1121b95a59649482470c5601c60a416bf189d553955b0338e34614"},
|
||||||
{file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"},
|
{file = "ruff-0.4.3.tar.gz", hash = "sha256:ff0a3ef2e3c4b6d133fbedcf9586abfbe38d076041f2dc18ffb2c7e0485d5a07"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1925,15 +1977,15 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinycss2"
|
name = "tinycss2"
|
||||||
version = "1.1.1"
|
version = "1.3.0"
|
||||||
requires_python = ">=3.6"
|
requires_python = ">=3.8"
|
||||||
summary = "A tiny CSS parser"
|
summary = "A tiny CSS parser"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"webencodings>=0.4",
|
"webencodings>=0.4",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "tinycss2-1.1.1-py3-none-any.whl", hash = "sha256:fe794ceaadfe3cf3e686b22155d0da5780dd0e273471a51846d0a02bc204fec8"},
|
{file = "tinycss2-1.3.0-py3-none-any.whl", hash = "sha256:54a8dbdffb334d536851be0226030e9505965bb2f30f21a4a82c55fb2a80fae7"},
|
||||||
{file = "tinycss2-1.1.1.tar.gz", hash = "sha256:b2e44dd8883c360c35dd0d1b5aad0b610e5156c2cb3b33434634e539ead9d8bf"},
|
{file = "tinycss2-1.3.0.tar.gz", hash = "sha256:152f9acabd296a8375fbca5b84c961ff95971fcfc32e79550c8df8e29118c54d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1959,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"
|
||||||
@ -1972,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"
|
||||||
@ -1983,12 +2058,52 @@ files = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "types-pytz"
|
name = "types-lxml"
|
||||||
version = "2022.6.0.1"
|
version = "2024.4.14"
|
||||||
summary = "Typing stubs for pytz"
|
requires_python = ">=3.8"
|
||||||
|
summary = "Complete lxml external type annotation"
|
||||||
|
dependencies = [
|
||||||
|
"cssselect~=1.2",
|
||||||
|
"types-beautifulsoup4~=4.12",
|
||||||
|
"typing-extensions~=4.5",
|
||||||
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "types-pytz-2022.6.0.1.tar.gz", hash = "sha256:d078196374d1277e9f9984d49373ea043cf2c64d5d5c491fbc86c258557bd46f"},
|
{file = "types_lxml-2024.4.14-py3-none-any.whl", hash = "sha256:7e5f836067cde4fddce3cdbf2bac7192c764bf5ee6d3eb86c732ad1b84f265c5"},
|
||||||
{file = "types_pytz-2022.6.0.1-py3-none-any.whl", hash = "sha256:bea605ce5d5a5d52a8e1afd7656c9b42476e18a0f888de6be91587355313ddf4"},
|
{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.18.0.20240506"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Typing stubs for Pygments"
|
||||||
|
dependencies = [
|
||||||
|
"types-docutils",
|
||||||
|
"types-setuptools",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "types-Pygments-2.18.0.20240506.tar.gz", hash = "sha256:4b4c37812c87bbde687dbf27adf5bac593745a321e57f678dbc311571ba2ac9d"},
|
||||||
|
{file = "types_Pygments-2.18.0.20240506-py3-none-any.whl", hash = "sha256:11c90bc1737c9af55e5569558b88df7c2233e12325cb516215f722271444e91d"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2013,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"
|
||||||
@ -2024,12 +2149,12 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.4.0"
|
version = "4.11.0"
|
||||||
requires_python = ">=3.7"
|
requires_python = ">=3.8"
|
||||||
summary = "Backported and Experimental Type Hints for Python 3.7+"
|
summary = "Backported and Experimental Type Hints for Python 3.8+"
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
{file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
|
||||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
{file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2186,8 +2311,8 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weasyprint"
|
name = "weasyprint"
|
||||||
version = "61.2"
|
version = "62.1"
|
||||||
requires_python = ">=3.8"
|
requires_python = ">=3.9"
|
||||||
summary = "The Awesome Document Factory"
|
summary = "The Awesome Document Factory"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"Pillow>=9.1.0",
|
"Pillow>=9.1.0",
|
||||||
@ -2196,12 +2321,12 @@ dependencies = [
|
|||||||
"cssselect2>=0.1",
|
"cssselect2>=0.1",
|
||||||
"fonttools[woff]>=4.0.0",
|
"fonttools[woff]>=4.0.0",
|
||||||
"html5lib>=1.1",
|
"html5lib>=1.1",
|
||||||
"pydyf>=0.8.0",
|
"pydyf>=0.10.0",
|
||||||
"tinycss2>=1.0.0",
|
"tinycss2>=1.3.0",
|
||||||
]
|
]
|
||||||
files = [
|
files = [
|
||||||
{file = "weasyprint-61.2-py3-none-any.whl", hash = "sha256:76c6dc0e75e09182d5645d92c66ddf86b1b992c9420235b723fb374b584e5bf4"},
|
{file = "weasyprint-62.1-py3-none-any.whl", hash = "sha256:654d4c266336cbf9acc4da118c7778ef5839717e6055d5b8f995cf50be200c46"},
|
||||||
{file = "weasyprint-61.2.tar.gz", hash = "sha256:47df6cfeeff8c6c28cf2e4caf837cde17715efe462708ada74baa2eb391b6059"},
|
{file = "weasyprint-62.1.tar.gz", hash = "sha256:bf3c1a9ac4194271a7cf117229c093744105b50ac2fa64c0a6e44e68ae742992"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -12,20 +12,20 @@ dependencies = [
|
|||||||
"django-markdownx~=4.0",
|
"django-markdownx~=4.0",
|
||||||
"django-recurrence~=1.11",
|
"django-recurrence~=1.11",
|
||||||
"django-widget-tweaks~=1.5",
|
"django-widget-tweaks~=1.5",
|
||||||
"django-stubs-ext~=4.2",
|
"django-stubs-ext~=5.0",
|
||||||
"markdownify~=0.12",
|
"markdownify~=0.12",
|
||||||
"mdformat~=0.7",
|
"mdformat~=0.7",
|
||||||
"mdformat-tables~=0.4",
|
"mdformat-tables~=0.4",
|
||||||
"mysqlclient~=2.2",
|
"mysqlclient~=2.2",
|
||||||
"django-autocomplete-light~=3.11",
|
"django-autocomplete-light~=3.11",
|
||||||
"weasyprint~=61.2",
|
"weasyprint~=62.1",
|
||||||
"requests~=2.31",
|
"requests~=2.31",
|
||||||
"semver~=3.0",
|
"semver~=3.0",
|
||||||
"djangorestframework~=3.15",
|
"djangorestframework~=3.15",
|
||||||
"django-q2~=1.6",
|
"django-q2~=1.6",
|
||||||
"lxml~=5.2",
|
"lxml~=5.2",
|
||||||
"django-object-actions~=4.2",
|
"django-object-actions~=4.2",
|
||||||
"bitstring~=4.1",
|
"bitstring~=4.2",
|
||||||
"udm-rest-client~=1.2",
|
"udm-rest-client~=1.2",
|
||||||
"openapi-client-udm~=1.0",
|
"openapi-client-udm~=1.0",
|
||||||
"django-nh3~=0.1",
|
"django-nh3~=0.1",
|
||||||
@ -34,10 +34,11 @@ dependencies = [
|
|||||||
"tablib[ods,xlsx]~=3.6",
|
"tablib[ods,xlsx]~=3.6",
|
||||||
"django-filter~=24.2",
|
"django-filter~=24.2",
|
||||||
"django-db-views~=0.1",
|
"django-db-views~=0.1",
|
||||||
"django-mysql~=4.12",
|
"django-mysql~=4.13",
|
||||||
"django-weasyprint~=2.3",
|
"django-weasyprint~=2.3",
|
||||||
"django-sendfile2~=0.7",
|
"django-sendfile2~=0.7",
|
||||||
"django-bootstrap5~=24.1",
|
"django-bootstrap5~=24.2",
|
||||||
|
"django-configurations[database,email]~=2.5",
|
||||||
]
|
]
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
@ -89,13 +90,14 @@ indent_size = 2
|
|||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
plugins = [
|
plugins = [
|
||||||
|
"./cmsmanage/mypy_django_configurations_plugin.py",
|
||||||
"mypy_django_plugin.main",
|
"mypy_django_plugin.main",
|
||||||
"mypy_drf_plugin.main",
|
"mypy_drf_plugin.main",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.django-stubs]
|
[tool.django-stubs]
|
||||||
django_settings_module = "cmsmanage.settings.dev"
|
django_settings_module = "cmsmanage.settings"
|
||||||
|
strict_settings = false
|
||||||
|
|
||||||
[[tool.pdm.source]]
|
[[tool.pdm.source]]
|
||||||
url = "https://pypi.org/simple"
|
url = "https://pypi.org/simple"
|
||||||
@ -111,24 +113,27 @@ include_packages = ["openapi-client-udm"]
|
|||||||
[tool.pdm.dev-dependencies]
|
[tool.pdm.dev-dependencies]
|
||||||
lint = [
|
lint = [
|
||||||
"djlint~=1.34",
|
"djlint~=1.34",
|
||||||
"ruff~=0.3",
|
"ruff~=0.4",
|
||||||
]
|
]
|
||||||
typing = [
|
typing = [
|
||||||
"mypy~=1.7",
|
"mypy~=1.10",
|
||||||
"django-stubs~=4.2",
|
"django-stubs~=5.0",
|
||||||
"setuptools~=69.5",
|
"setuptools~=69.5",
|
||||||
"types-bleach~=6.1",
|
"types-bleach~=6.1",
|
||||||
"types-requests~=2.31",
|
"types-requests~=2.31",
|
||||||
"types-urllib3~=1.26",
|
"types-urllib3~=1.26",
|
||||||
"djangorestframework-stubs[compatible-mypy]~=3.14",
|
"djangorestframework-stubs[compatible-mypy]~=3.15",
|
||||||
"lxml-stubs~=0.5",
|
"types-Markdown~=3.6",
|
||||||
|
"types-Pygments~=2.18",
|
||||||
|
"types-psycopg2~=2.9",
|
||||||
|
"types-lxml~=2024.4",
|
||||||
]
|
]
|
||||||
debug = [
|
debug = [
|
||||||
"django-debug-toolbar~=4.3",
|
"django-debug-toolbar~=4.3",
|
||||||
]
|
]
|
||||||
dev = [
|
dev = [
|
||||||
"django-extensions~=3.2",
|
"django-extensions~=3.2",
|
||||||
"ipython~=8.23",
|
"ipython~=8.24",
|
||||||
"hypothesis[django]~=6.100",
|
"hypothesis[django]~=6.100",
|
||||||
"tblib~=3.0",
|
"tblib~=3.0",
|
||||||
]
|
]
|
||||||
|
@ -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