diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index d760bb1..0bd7241 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -2,7 +2,7 @@ name: Test on: [ push, pull_request ] env: - DJANGO_SETTINGS_MODULE: cmsmanage.settings.ci + DJANGO_CONFIGURATION: CI jobs: test: diff --git a/.gitignore b/.gitignore index d07ba1a..5505d2e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__/ /__pypackages__/ /markdownx/ /media/ +/settings.*.env diff --git a/cmsmanage/asgi.py b/cmsmanage/asgi.py index 5d2042c..fad05a3 100644 --- a/cmsmanage/asgi.py +++ b/cmsmanage/asgi.py @@ -9,8 +9,9 @@ https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ import os -from django.core.asgi import get_asgi_application +from configurations.asgi import get_asgi_application os.environ.setdefault("DJANGO_SETTINGS_MODULE", "cmsmanage.settings") +os.environ.setdefault("DJANGO_CONFIGURATION", "DEV") application = get_asgi_application() diff --git a/cmsmanage/settings.py b/cmsmanage/settings.py new file mode 100644 index 0000000..6b5e038 --- /dev/null +++ b/cmsmanage/settings.py @@ -0,0 +1,312 @@ +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 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 = { + "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 " + 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", + }, + } + } diff --git a/cmsmanage/settings/.gitignore b/cmsmanage/settings/.gitignore deleted file mode 100644 index a2ac02f..0000000 --- a/cmsmanage/settings/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dev.py -prod.py diff --git a/cmsmanage/settings/__init__.py b/cmsmanage/settings/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cmsmanage/settings/base.py b/cmsmanage/settings/base.py deleted file mode 100644 index 40502b1..0000000 --- a/cmsmanage/settings/base.py +++ /dev/null @@ -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"} diff --git a/cmsmanage/settings/ci.py b/cmsmanage/settings/ci.py deleted file mode 100644 index 4462fc8..0000000 --- a/cmsmanage/settings/ci.py +++ /dev/null @@ -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") diff --git a/cmsmanage/settings/dev.sample.py b/cmsmanage/settings/dev.sample.py deleted file mode 100644 index f5222aa..0000000 --- a/cmsmanage/settings/dev.sample.py +++ /dev/null @@ -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+=3.2", + "typing-extensions>=3.10.0.0", +] +files = [ + {file = "dj-database-url-2.1.0.tar.gz", hash = "sha256:f2042cefe1086e539c9da39fad5ad7f61173bf79665e69bf7e4de55fa88b135f"}, + {file = "dj_database_url-2.1.0-py3-none-any.whl", hash = "sha256:04bc34b248d4c21aaa13e4ab419ae6575ef5f10f3df735ce7da97722caa356e0"}, +] + +[[package]] +name = "dj-email-url" +version = "1.0.6" +summary = "Use an URL to configure email backend settings in your Django Application." +files = [ + {file = "dj-email-url-1.0.6.tar.gz", hash = "sha256:55ffe3329e48f54f8a75aa36ece08f365e09d61f8a209773ef09a1d4760e699a"}, + {file = "dj_email_url-1.0.6-py2.py3-none-any.whl", hash = "sha256:cbd08327fbb08b104eac160fb4703f375532e4c0243eb230f5b960daee7a96db"}, +] + [[package]] name = "django" version = "5.0.4" @@ -515,6 +537,35 @@ files = [ {file = "django_bootstrap5-24.2.tar.gz", hash = "sha256:a3cee2b3d45745210c5b898af2917f310f44df746269fe09a93be28a0adc2a4b"}, ] +[[package]] +name = "django-configurations" +version = "2.5.1" +requires_python = "<4.0,>=3.8" +summary = "A helper for organizing Django settings." +dependencies = [ + "django>=3.2", +] +files = [ + {file = "django-configurations-2.5.1.tar.gz", hash = "sha256:6e5083757e2bbdf9bb7850567536b96a93515f6b17503d74928ff628db2e0e94"}, + {file = "django_configurations-2.5.1-py3-none-any.whl", hash = "sha256:ceb84858da2dac846b15e715c2fd936cfc4c7917c074aff8d31700564093955e"}, +] + +[[package]] +name = "django-configurations" +version = "2.5.1" +extras = ["database", "email"] +requires_python = "<4.0,>=3.8" +summary = "A helper for organizing Django settings." +dependencies = [ + "dj-database-url", + "dj-email-url", + "django-configurations==2.5.1", +] +files = [ + {file = "django-configurations-2.5.1.tar.gz", hash = "sha256:6e5083757e2bbdf9bb7850567536b96a93515f6b17503d74928ff628db2e0e94"}, + {file = "django_configurations-2.5.1-py3-none-any.whl", hash = "sha256:ceb84858da2dac846b15e715c2fd936cfc4c7917c074aff8d31700564093955e"}, +] + [[package]] name = "django-db-views" version = "0.1.6" diff --git a/pyproject.toml b/pyproject.toml index d3090e3..15a71c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ dependencies = [ "django-weasyprint~=2.3", "django-sendfile2~=0.7", "django-bootstrap5~=24.2", + "django-configurations[database,email]~=2.5", ] requires-python = ">=3.11"