Compare commits
10 Commits
f8a4b425af
...
04ca92b5fe
Author | SHA1 | Date | |
---|---|---|---|
04ca92b5fe | |||
0944dd7992 | |||
9658366d72 | |||
a2c0707263 | |||
785a445b43 | |||
ee2d63f784 | |||
12eb4038bc | |||
c2b1da743c | |||
99060a8a43 | |||
59cade1cfd |
@ -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
|
||||||
|
@ -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)
|
326
cmsmanage/settings.py
Normal file
326
cmsmanage/settings.py
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from django.core import validators
|
||||||
|
|
||||||
|
from configurations import Configuration, values
|
||||||
|
|
||||||
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
class Base(Configuration):
|
||||||
|
@classmethod
|
||||||
|
def pre_setup(cls):
|
||||||
|
super().pre_setup()
|
||||||
|
|
||||||
|
# load systemd credentials, as per https://systemd.io/CREDENTIALS/
|
||||||
|
credentials_directory = os.getenv("CREDENTIALS_DIRECTORY")
|
||||||
|
if credentials_directory is not None:
|
||||||
|
for credential in Path(credentials_directory).iterdir():
|
||||||
|
if credential.name.isupper():
|
||||||
|
os.environ.setdefault(credential.name, credential.read_text())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setup(cls):
|
||||||
|
super().setup()
|
||||||
|
cls.DATABASES["default"]["OPTIONS"] = {"charset": "utf8mb4"}
|
||||||
|
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"dal",
|
||||||
|
"dal_select2",
|
||||||
|
"django.contrib.admin",
|
||||||
|
"django.contrib.auth",
|
||||||
|
"django.contrib.contenttypes",
|
||||||
|
"django.contrib.sessions",
|
||||||
|
"django.contrib.messages",
|
||||||
|
"django.contrib.staticfiles",
|
||||||
|
"django_admin_logs",
|
||||||
|
"django_object_actions",
|
||||||
|
"widget_tweaks",
|
||||||
|
# "markdownx",
|
||||||
|
# "recurrence",
|
||||||
|
"rest_framework",
|
||||||
|
"rest_framework.authtoken",
|
||||||
|
"django_q",
|
||||||
|
"django_nh3",
|
||||||
|
"django_tables2",
|
||||||
|
"django_filters",
|
||||||
|
"django_db_views",
|
||||||
|
"django_mysql",
|
||||||
|
"django_sendfile",
|
||||||
|
"django_bootstrap5",
|
||||||
|
# "tasks.apps.TasksConfig",
|
||||||
|
"rentals.apps.RentalsConfig",
|
||||||
|
"membershipworks.apps.MembershipworksConfig",
|
||||||
|
"paperwork.apps.PaperworkConfig",
|
||||||
|
"doorcontrol.apps.DoorControlConfig",
|
||||||
|
"dashboard.apps.DashboardConfig",
|
||||||
|
]
|
||||||
|
|
||||||
|
MIDDLEWARE = [
|
||||||
|
"django.middleware.security.SecurityMiddleware",
|
||||||
|
"django.contrib.sessions.middleware.SessionMiddleware",
|
||||||
|
"django.middleware.common.CommonMiddleware",
|
||||||
|
"django.middleware.csrf.CsrfViewMiddleware",
|
||||||
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
|
]
|
||||||
|
|
||||||
|
ROOT_URLCONF = "cmsmanage.urls"
|
||||||
|
|
||||||
|
TEMPLATES = [
|
||||||
|
{
|
||||||
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
|
"DIRS": [BASE_DIR / "templates"],
|
||||||
|
"APP_DIRS": True,
|
||||||
|
"OPTIONS": {
|
||||||
|
"context_processors": [
|
||||||
|
"django.template.context_processors.debug",
|
||||||
|
"django.template.context_processors.request",
|
||||||
|
"django.contrib.auth.context_processors.auth",
|
||||||
|
"django.contrib.messages.context_processors.messages",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
|
WSGI_APPLICATION = "cmsmanage.wsgi.application"
|
||||||
|
|
||||||
|
# Default URL to redirect to after authentication
|
||||||
|
LOGIN_REDIRECT_URL = "/"
|
||||||
|
LOGIN_URL = "/auth/login/"
|
||||||
|
|
||||||
|
# Internationalization
|
||||||
|
# https://docs.djangoproject.com/en/3.1/topics/i18n/
|
||||||
|
|
||||||
|
LANGUAGE_CODE = "en-us"
|
||||||
|
TIME_ZONE = "America/New_York"
|
||||||
|
USE_I18N = False
|
||||||
|
USE_L10N = True
|
||||||
|
USE_TZ = True
|
||||||
|
USE_DEPRECATED_PYTZ = False
|
||||||
|
|
||||||
|
# Static files (CSS, JavaScript, Images)
|
||||||
|
# https://docs.djangoproject.com/en/3.1/howto/static-files/
|
||||||
|
|
||||||
|
STATIC_URL = "/static/"
|
||||||
|
STATICFILES_DIRS = [BASE_DIR / "static"]
|
||||||
|
|
||||||
|
LOGGING = values.DictValue(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"disable_existing_loggers": False,
|
||||||
|
"handlers": {
|
||||||
|
"my_console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"loggers": {
|
||||||
|
"": {
|
||||||
|
"handlers": ["my_console"],
|
||||||
|
"level": "WARNING",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
MEDIA_ROOT = "media"
|
||||||
|
MEDIA_URL = "media/"
|
||||||
|
SENDFILE_ROOT = str(BASE_DIR / "media" / "protected")
|
||||||
|
|
||||||
|
SERVER_EMAIL = "cmsmanage <cmsmanage@claremontmakerspace.org>"
|
||||||
|
EMAIL_SUBJECT_PREFIX = "[cmsmanage] "
|
||||||
|
DEFAULT_FROM_EMAIL = SERVER_EMAIL
|
||||||
|
# Django Rest Framework
|
||||||
|
REST_FRAMEWORK = {
|
||||||
|
# Use Django's standard `django.contrib.auth` permissions
|
||||||
|
"DEFAULT_PERMISSION_CLASSES": [
|
||||||
|
"cmsmanage.drf_permissions.DjangoModelPermissionsWithView"
|
||||||
|
],
|
||||||
|
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||||
|
"rest_framework.authentication.SessionAuthentication",
|
||||||
|
"rest_framework.authentication.TokenAuthentication",
|
||||||
|
],
|
||||||
|
"DEFAULT_VERSIONING_CLASS": "rest_framework.versioning.NamespaceVersioning",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Django Q
|
||||||
|
Q_CLUSTER = {
|
||||||
|
"name": "cmsmanage",
|
||||||
|
"orm": "default",
|
||||||
|
"retry": 60 * 6,
|
||||||
|
"timeout": 60 * 5,
|
||||||
|
"catch_up": False,
|
||||||
|
"error_reporter": {"admin_email": {}},
|
||||||
|
"ack_failures": True,
|
||||||
|
"max_attempts": 1,
|
||||||
|
"ALT_CLUSTERS": {
|
||||||
|
"internal": {
|
||||||
|
"retry": 60 * 60,
|
||||||
|
"timeout": 60 * 59,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Django-Tables2
|
||||||
|
DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5-responsive.html"
|
||||||
|
DJANGO_TABLES2_TABLE_ATTRS = {"class": "table mx-auto w-auto"}
|
||||||
|
|
||||||
|
SECRET_KEY = values.SecretValue(environ_required=True)
|
||||||
|
|
||||||
|
# CMSManage specific stuff
|
||||||
|
WIKI_URL = values.URLValue("https://wiki.claremontmakerspace.org")
|
||||||
|
|
||||||
|
|
||||||
|
class NonCIBase(Base):
|
||||||
|
"""required for all but CI"""
|
||||||
|
|
||||||
|
DATABASES = values.DatabaseURLValue(environ_required=True)
|
||||||
|
EMAIL = values.EmailURLValue(environ_required=True)
|
||||||
|
# TODO: should validate emails
|
||||||
|
ADMINS = values.SingleNestedTupleValue(environ_required=True)
|
||||||
|
|
||||||
|
MEMBERSHIPWORKS_USERNAME = values.EmailValue(
|
||||||
|
environ_required=True, environ_prefix=None
|
||||||
|
)
|
||||||
|
MEMBERSHIPWORKS_PASSWORD = values.SecretValue(
|
||||||
|
environ_required=True, environ_prefix=None
|
||||||
|
)
|
||||||
|
|
||||||
|
HID_DOOR_USERNAME = values.Value(environ_required=True, environ_prefix=None)
|
||||||
|
HID_DOOR_PASSWORD = values.SecretValue(environ_prefix=None)
|
||||||
|
|
||||||
|
# TODO: should validate emails (but EmailValidator doesn't handle name parts)
|
||||||
|
INVOICE_HANDLERS = values.ListValue(
|
||||||
|
environ_required=True, environ_prefix="CMSMANAGE"
|
||||||
|
)
|
||||||
|
|
||||||
|
# arguments for https://udm-rest-client.readthedocs.io/en/latest/udm_rest_client.html#udm_rest_client.udm.UDM
|
||||||
|
UCS = values.DictValue(environ_required=True, environ_prefix="CMSMANAGE")
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPURLValue(values.ValidationMixin, values.Value):
|
||||||
|
message = "Cannot interpret LDAP URL value {0!r}"
|
||||||
|
validator = validators.URLValidator(schemes=["ldap", "ldaps"])
|
||||||
|
|
||||||
|
|
||||||
|
class Prod(NonCIBase):
|
||||||
|
import ldap
|
||||||
|
from django_auth_ldap.config import LDAPGroupQuery, LDAPSearch, PosixGroupType
|
||||||
|
|
||||||
|
DOTENV = BASE_DIR / "settings.dev.env"
|
||||||
|
|
||||||
|
DEBUG = False
|
||||||
|
|
||||||
|
# LDAP Authentication
|
||||||
|
# https://django-auth-ldap.readthedocs.io/en/latest/
|
||||||
|
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
"django_auth_ldap.backend.LDAPBackend",
|
||||||
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
|
]
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_SEARCH = LDAPSearch(
|
||||||
|
"cn=users,dc=sawtooth,dc=claremontmakerspace,dc=org",
|
||||||
|
ldap.SCOPE_SUBTREE,
|
||||||
|
"(uid=%(user)s)",
|
||||||
|
)
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_ATTR_MAP = {
|
||||||
|
"first_name": "givenName",
|
||||||
|
"last_name": "sn",
|
||||||
|
"email": "mail",
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
|
||||||
|
"is_staff": (
|
||||||
|
LDAPGroupQuery(
|
||||||
|
"cn=MW_CMS Staff,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
|
)
|
||||||
|
| LDAPGroupQuery(
|
||||||
|
"cn=MW_Database Admin,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
|
)
|
||||||
|
| LDAPGroupQuery(
|
||||||
|
"cn=MW_Database Access,cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AUTH_LDAP_GROUP_SEARCH = LDAPSearch(
|
||||||
|
"cn=groups,dc=sawtooth,dc=claremontmakerspace,dc=org",
|
||||||
|
ldap.SCOPE_SUBTREE,
|
||||||
|
"(objectClass=posixGroup)",
|
||||||
|
)
|
||||||
|
AUTH_LDAP_GROUP_TYPE = PosixGroupType()
|
||||||
|
AUTH_LDAP_MIRROR_GROUPS = True
|
||||||
|
|
||||||
|
SENDFILE_BACKEND = "django_sendfile.backends.nginx"
|
||||||
|
SENDFILE_URL = "/media/protected"
|
||||||
|
|
||||||
|
AUTH_LDAP_SERVER_URI = LDAPURLValue(None, environ_required=True)
|
||||||
|
AUTH_LDAP_BIND_DN = values.Value(environ_required=True)
|
||||||
|
AUTH_LDAP_BIND_PASSWORD = values.SecretValue()
|
||||||
|
|
||||||
|
ALLOWED_HOSTS = values.ListValue([], environ_required=True)
|
||||||
|
STATIC_ROOT = values.PathValue(environ_required=True)
|
||||||
|
|
||||||
|
|
||||||
|
def configure_hypothesis_profiles():
|
||||||
|
from hypothesis import HealthCheck, Verbosity, settings
|
||||||
|
|
||||||
|
settings.register_profile("ci", suppress_health_check=(HealthCheck.too_slow,))
|
||||||
|
settings.register_profile("dev", max_examples=20)
|
||||||
|
settings.register_profile("debug", max_examples=10, verbosity=Verbosity.verbose)
|
||||||
|
|
||||||
|
|
||||||
|
class Dev(NonCIBase):
|
||||||
|
@classmethod
|
||||||
|
def post_setup(cls):
|
||||||
|
import os
|
||||||
|
|
||||||
|
from hypothesis import settings
|
||||||
|
|
||||||
|
super().post_setup()
|
||||||
|
|
||||||
|
configure_hypothesis_profiles()
|
||||||
|
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "dev"))
|
||||||
|
|
||||||
|
DOTENV = BASE_DIR / "settings.dev.env"
|
||||||
|
|
||||||
|
DEBUG = values.BooleanValue(True)
|
||||||
|
INTERNAL_IPS = ["127.0.0.1"]
|
||||||
|
|
||||||
|
INSTALLED_APPS = NonCIBase.INSTALLED_APPS + ["debug_toolbar", "django_extensions"]
|
||||||
|
MIDDLEWARE = ["debug_toolbar.middleware.DebugToolbarMiddleware"] + Base.MIDDLEWARE
|
||||||
|
EMAIL = values.EmailURLValue("smtp://localhost:1025") # for local `mailpit`
|
||||||
|
|
||||||
|
SENDFILE_BACKEND = "django_sendfile.backends.development"
|
||||||
|
|
||||||
|
|
||||||
|
class CI(Base):
|
||||||
|
@classmethod
|
||||||
|
def post_setup(cls):
|
||||||
|
from hypothesis import settings
|
||||||
|
|
||||||
|
super().post_setup()
|
||||||
|
|
||||||
|
configure_hypothesis_profiles()
|
||||||
|
settings.load_profile("ci")
|
||||||
|
|
||||||
|
SECRET_KEY = "aed7jee2kai1we9eithae0gaegh9ohthoh4phahk5bau4Ahxaijo3aicheex3qua"
|
||||||
|
|
||||||
|
DATABASES = {
|
||||||
|
"default": {
|
||||||
|
"ENGINE": "django.db.backends.mysql",
|
||||||
|
"HOST": "mariadb",
|
||||||
|
"NAME": "CMS_Database",
|
||||||
|
"USER": "root",
|
||||||
|
"PASSWORD": "whatever",
|
||||||
|
"OPTIONS": {
|
||||||
|
"charset": "utf8mb4",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
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 "
|
||||||
|
@ -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/"),
|
||||||
|
),
|
||||||
|
]
|
@ -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,40 +499,35 @@ 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()
|
meetings=Subquery(
|
||||||
.get_queryset()
|
EventMeetingTime.objects.filter(event=OuterRef("pk"))
|
||||||
.annotate(
|
.values("event__pk")
|
||||||
meetings=Subquery(
|
.annotate(d=Count("pk"))
|
||||||
EventMeetingTime.objects.filter(event=OuterRef("pk"))
|
.values("d")[:1],
|
||||||
.values("event__pk")
|
output_field=models.IntegerField(),
|
||||||
.annotate(d=Count("pk"))
|
),
|
||||||
.values("d")[:1],
|
duration=Subquery(
|
||||||
output_field=models.IntegerField(),
|
EventMeetingTime.objects.filter(event=OuterRef("pk"))
|
||||||
),
|
.values("event__pk")
|
||||||
duration=Subquery(
|
.annotate(d=Sum("duration"))
|
||||||
EventMeetingTime.objects.filter(event=OuterRef("pk"))
|
.values("d")[:1],
|
||||||
.values("event__pk")
|
output_field=models.DurationField(),
|
||||||
.annotate(d=Sum("duration"))
|
),
|
||||||
.values("d")[:1],
|
person_hours=ExpressionWrapper(
|
||||||
output_field=models.DurationField(),
|
ExpressionWrapper(F("duration"), models.IntegerField()) * F("count"),
|
||||||
),
|
models.DurationField(),
|
||||||
person_hours=ExpressionWrapper(
|
),
|
||||||
ExpressionWrapper(F("duration"), models.IntegerField())
|
# TODO: this could be a GeneratedField, but that
|
||||||
* F("count"),
|
# currently breaks saving when the primary key is
|
||||||
models.DurationField(),
|
# provided (Django 5.0.1)
|
||||||
),
|
details_timestamp=Func(
|
||||||
# TODO: this could be a GeneratedField, but that
|
Func(F("details___ts"), function="FROM_UNIXTIME"),
|
||||||
# currently breaks saving when the primary key is
|
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
|
||||||
# provided (Django 5.0.1)
|
output_field=models.DateTimeField(),
|
||||||
details_timestamp=Func(
|
),
|
||||||
Func(F("details___ts"), function="FROM_UNIXTIME"),
|
|
||||||
template="CONVERT_TZ(%(expressions)s, @@session.time_zone, 'UTC')",
|
|
||||||
output_field=models.DateTimeField(),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -551,6 +550,7 @@ class EventExt(Event):
|
|||||||
max_digits=13, decimal_places=4, default=0
|
max_digits=13, decimal_places=4, default=0
|
||||||
)
|
)
|
||||||
details = models.JSONField(null=True, blank=True)
|
details = models.JSONField(null=True, blank=True)
|
||||||
|
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 +573,7 @@ class EventExt(Event):
|
|||||||
self.materials_fee_included_in_price is not None
|
self.materials_fee_included_in_price is not None
|
||||||
or self.materials_fee == 0
|
or self.materials_fee == 0
|
||||||
)
|
)
|
||||||
and self.total_due_to_instructor is not None
|
and getattr(self, "total_due_to_instructor") is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -581,6 +581,32 @@ class EventExt(Event):
|
|||||||
ordering = ["-start"]
|
ordering = ["-start"]
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
|
||||||
|
class EventExtAnnotations(TypedDict):
|
||||||
|
meetings: int
|
||||||
|
duration: timedelta
|
||||||
|
person_hours: timedelta
|
||||||
|
details_timestamp: datetime
|
||||||
|
|
||||||
|
class EventExtFinancialAnnotations(TypedDict):
|
||||||
|
quantity: Decimal
|
||||||
|
amount: Decimal
|
||||||
|
materials: Decimal
|
||||||
|
amount_without_materials: Decimal
|
||||||
|
instructor_revenue: Decimal
|
||||||
|
instructor_amount: Decimal
|
||||||
|
total_due_to_instructor: Decimal
|
||||||
|
gross_revenue: Decimal
|
||||||
|
net_revenue: Decimal
|
||||||
|
|
||||||
|
EventExtAnnotated = WithAnnotations[EventExt, EventExtAnnotations]
|
||||||
|
EventExtAnnotatedWithFinancials = WithAnnotations[EventExt, EventExtAnnotations]
|
||||||
|
else:
|
||||||
|
EventExtAnnotated = WithAnnotations[EventExt]
|
||||||
|
EventExtAnnotatedWithFinancials = WithAnnotations[EventExt]
|
||||||
|
|
||||||
|
|
||||||
class EventMeetingTime(models.Model):
|
class EventMeetingTime(models.Model):
|
||||||
event = models.ForeignKey(
|
event = models.ForeignKey(
|
||||||
EventExt, on_delete=models.CASCADE, related_name="meeting_times"
|
EventExt, on_delete=models.CASCADE, related_name="meeting_times"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
from collections.abc import Iterable
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@ -30,7 +31,7 @@ def flags_for_member(csv_member, all_flags, folders):
|
|||||||
yield flag
|
yield flag
|
||||||
|
|
||||||
|
|
||||||
def update_flags(mw_flags) -> list[Flag]:
|
def update_flags(mw_flags) -> Iterable[Flag]:
|
||||||
for typ, flags_of_type in mw_flags.items():
|
for typ, flags_of_type in mw_flags.items():
|
||||||
for name, id in flags_of_type.items():
|
for name, id in flags_of_type.items():
|
||||||
flag = Flag(id=id, name=name, type=typ[:-1])
|
flag = Flag(id=id, name=name, type=typ[:-1])
|
||||||
@ -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()
|
||||||
|
@ -32,7 +32,7 @@ import django_tables2 as tables
|
|||||||
import weasyprint
|
import weasyprint
|
||||||
from dal import autocomplete
|
from dal import autocomplete
|
||||||
from django_filters.views import BaseFilterView
|
from django_filters.views import BaseFilterView
|
||||||
from django_mysql.models import GroupConcat
|
from django_mysql.models.aggregates import GroupConcat
|
||||||
from django_sendfile import sendfile
|
from django_sendfile import sendfile
|
||||||
from django_tables2 import A, SingleTableMixin
|
from django_tables2 import A, SingleTableMixin
|
||||||
from django_tables2.export.views import ExportMixin
|
from django_tables2.export.views import ExportMixin
|
||||||
@ -241,7 +241,7 @@ class EventMonthReport(
|
|||||||
|
|
||||||
|
|
||||||
class UserEventView(SingleTableMixin, ListView):
|
class UserEventView(SingleTableMixin, ListView):
|
||||||
model = EventExt
|
model: type[EventExt] = EventExt
|
||||||
table_class = UserEventTable
|
table_class = UserEventTable
|
||||||
export_formats = ("csv", "xlsx", "ods")
|
export_formats = ("csv", "xlsx", "ods")
|
||||||
template_name = "membershipworks/user_event_list.dj.html"
|
template_name = "membershipworks/user_event_list.dj.html"
|
||||||
@ -412,7 +412,6 @@ 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:
|
||||||
|
@ -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,
|
||||||
|
253
pdm.lock
253
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:503ddceb3ef04537dbeea6dc15d2ce2c46bc4ade0a2e394938224f6da4033c81"
|
content_hash = "sha256:8e68a7f1608469e70bc3e7502f747bbe5f38ca4bc15f504811377509599bb7a1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiohttp"
|
name = "aiohttp"
|
||||||
@ -415,6 +415,16 @@ files = [
|
|||||||
{file = "cssbeautifier-1.14.7.tar.gz", hash = "sha256:be7f1ea7a7b009f0172c2c0d0bebb2d136346e786f7182185ea944affb52135a"},
|
{file = "cssbeautifier-1.14.7.tar.gz", hash = "sha256:be7f1ea7a7b009f0172c2c0d0bebb2d136346e786f7182185ea944affb52135a"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cssselect"
|
||||||
|
version = "1.2.0"
|
||||||
|
requires_python = ">=3.7"
|
||||||
|
summary = "cssselect parses CSS3 Selectors and translates them to XPath 1.0"
|
||||||
|
files = [
|
||||||
|
{file = "cssselect-1.2.0-py2.py3-none-any.whl", hash = "sha256:da1885f0c10b60c03ed5eccbb6b68d6eff248d91976fcde348f395d54c9fd35e"},
|
||||||
|
{file = "cssselect-1.2.0.tar.gz", hash = "sha256:666b19839cfaddb9ce9d36bfe4c969132c647b92fc9088c4e23f786b30f1b3dc"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cssselect2"
|
name = "cssselect2"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -449,6 +459,28 @@ 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.4"
|
||||||
@ -515,6 +547,35 @@ files = [
|
|||||||
{file = "django_bootstrap5-24.2.tar.gz", hash = "sha256:a3cee2b3d45745210c5b898af2917f310f44df746269fe09a93be28a0adc2a4b"},
|
{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.6"
|
||||||
@ -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]]
|
||||||
@ -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]]
|
||||||
@ -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.17.0.20240310"
|
||||||
|
requires_python = ">=3.8"
|
||||||
|
summary = "Typing stubs for Pygments"
|
||||||
|
dependencies = [
|
||||||
|
"types-docutils",
|
||||||
|
"types-setuptools",
|
||||||
|
]
|
||||||
|
files = [
|
||||||
|
{file = "types-Pygments-2.17.0.20240310.tar.gz", hash = "sha256:b1d97e905ce36343c7283b0319182ae6d4f967188f361f45502a18ae43e03e1f"},
|
||||||
|
{file = "types_Pygments-2.17.0.20240310-py3-none-any.whl", hash = "sha256:b101ca9448aaff52af6966506f1fdd73b1e60a79b8a79a8bace3366cbf1f7ed9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -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"
|
||||||
@ -2186,8 +2311,8 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weasyprint"
|
name = "weasyprint"
|
||||||
version = "61.2"
|
version = "62.0"
|
||||||
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.0-py3-none-any.whl", hash = "sha256:021b2fcde720756dd496645f251e40240b742e91a85f446f612f4666fdca316d"},
|
||||||
{file = "weasyprint-61.2.tar.gz", hash = "sha256:47df6cfeeff8c6c28cf2e4caf837cde17715efe462708ada74baa2eb391b6059"},
|
{file = "weasyprint-62.0.tar.gz", hash = "sha256:9f56eaefdaafcf35ae568c56eee5dba189af1272a46f4d205bd12936315f9481"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -12,13 +12,13 @@ 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.0",
|
||||||
"requests~=2.31",
|
"requests~=2.31",
|
||||||
"semver~=3.0",
|
"semver~=3.0",
|
||||||
"djangorestframework~=3.15",
|
"djangorestframework~=3.15",
|
||||||
@ -38,6 +38,7 @@ dependencies = [
|
|||||||
"django-weasyprint~=2.3",
|
"django-weasyprint~=2.3",
|
||||||
"django-sendfile2~=0.7",
|
"django-sendfile2~=0.7",
|
||||||
"django-bootstrap5~=24.2",
|
"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"
|
||||||
@ -114,14 +116,17 @@ lint = [
|
|||||||
"ruff~=0.4",
|
"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.17",
|
||||||
|
"types-psycopg2~=2.9",
|
||||||
|
"types-lxml~=2024.4",
|
||||||
]
|
]
|
||||||
debug = [
|
debug = [
|
||||||
"django-debug-toolbar~=4.3",
|
"django-debug-toolbar~=4.3",
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from dal import autocomplete
|
from dal.autocomplete import ModelSelect2
|
||||||
|
|
||||||
from rentals.models import LockerInfo
|
from rentals.models import LockerInfo
|
||||||
|
|
||||||
@ -16,7 +16,7 @@ class LockerInfoForm(forms.ModelForm):
|
|||||||
"notes",
|
"notes",
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
"renter": autocomplete.ModelSelect2(
|
"renter": ModelSelect2(
|
||||||
url="membershipworks:member-autocomplete",
|
url="membershipworks:member-autocomplete",
|
||||||
attrs={
|
attrs={
|
||||||
"data-placeholder": "-- No Renter --",
|
"data-placeholder": "-- No Renter --",
|
||||||
|
Loading…
Reference in New Issue
Block a user