Ticket #37022: settings.py

File settings.py, 15.6 KB (added by Chris Rose, 6 hours ago)
Line 
1# ruff: noqa: F401, E402
2"""
3Django settings for the NomNom test project.
4
5Manually maintained, similar to the settings.py.jinja2 template in convention-template.
6
7For more information on this file, see
8https://docs.djangoproject.com/en/5.0/topics/settings/
9
10For the full list of settings and their values, see
11https://docs.djangoproject.com/en/5.0/ref/settings/
12
13To enable this, set DJANGO_SETTINGS_MODULE=nomnom_dev.settings
14"""
15
16import os
17import subprocess
18from pathlib import Path
19
20import bleach.sanitizer
21import djp
22import structlog
23
24# Build paths inside the project like this: BASE_DIR / 'subdir'.
25BASE_DIR = Path(__file__).resolve().parent.parent
26
27
28# the dev server settings perform some additional environment surgery to ensure we have the current
29# ports for redis, mailcatcher, and the DB
30def get_docker_port(service, port):
31 """
32 Get the port for a service in the docker-compose file.
33 """
34 res = subprocess.run(
35 f"docker compose port '{service}' {port}",
36 capture_output=True,
37 check=True,
38 shell=True,
39 )
40 port = res.stdout.decode("utf-8").strip().split(":")[-1]
41 return int(port)
42
43
44os.environ["NOM_DB_PORT"] = str(get_docker_port("db", "5432"))
45os.environ["NOM_REDIS_PORT"] = str(get_docker_port("redis", "6379"))
46os.environ["NOM_EMAIL_PORT"] = str(get_docker_port("mailcatcher", "1025"))
47
48# import the system configuration from the application
49from nomnom.convention import system_configuration as cfg
50
51# Quick-start development settings - unsuitable for production
52# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
53
54# SECURITY WARNING: keep the secret key used in production secret!
55SECRET_KEY = cfg.secret_key
56
57# SECURITY WARNING: don't run with debug turned on in production!
58DEBUG = cfg.debug
59TEMPLATE_DEBUG = cfg.debug
60
61
62class InvalidStringShowWarning(str):
63 def __mod__(self, other):
64 logger = structlog.get_logger(__name__)
65 logger.warning(
66 f"In template, undefined variable or unknown value for: '{other}'"
67 )
68 return ""
69
70 def __bool__(self): # if using Python 2, use __nonzero__ instead
71 # make the template tag `default` use its fallback value
72 return False
73
74
75ALLOWED_HOSTS = cfg.allowed_hosts
76
77CSRF_TRUSTED_ORIGINS = [f"https://{h}" for h in ALLOWED_HOSTS] if ALLOWED_HOSTS else []
78
79# Application definition
80
81INSTALLED_APPS = [
82 "nomnom.apps.NomnomAdminConfig",
83 "django.contrib.auth",
84 "django.contrib.contenttypes",
85 "django.contrib.humanize",
86 "django.contrib.sessions",
87 "django.contrib.messages",
88 "django.contrib.sites",
89 # use whitenoise to serve static files, instead of django's builtin
90 "whitenoise.runserver_nostatic",
91 "django.contrib.staticfiles",
92 # deferred tasks
93 "django_celery_results",
94 "django_celery_beat",
95 # debug helper
96 "django_extensions",
97 "django_browser_reload",
98 # debugging
99 "debug_toolbar",
100 # to render markdown to HTML in templates
101 "markdownify.apps.MarkdownifyConfig",
102 # OAuth login
103 "social_django",
104 # Admin filtering enhancements
105 "admin_auto_filters",
106 # Admin forms
107 "django_admin_action_forms",
108 # Admin audit logging
109 "logentry_admin",
110 # Theming
111 "django_bootstrap5",
112 "bootstrap",
113 "fontawesome",
114 # Fonts
115 "django_google_fonts",
116 # HTMX support
117 "django_htmx",
118 # A healthcheck
119 "watchman",
120 # Markdown field support
121 "markdownfield",
122 # Feature flags
123 "waffle",
124 # Structured logging
125 "django_structlog",
126 # the convention theme; this MUST come before the nominate app, so that its templates can
127 # override the nominate ones.
128 "nomnom_dev",
129 "nomnom.base",
130 # The nominating and voting app
131 "nomnom.nominate",
132 "nomnom.canonicalize",
133 # Advisory Votes
134 "nomnom.advise",
135 # The hugo packet app
136 "nomnom.hugopacket",
137 # Convention admin utilities and seeding commands
138 "nomnom.convention_admin",
139]
140
141SITE_ID = 1
142
143NOMNOM_ALLOW_USERNAME_LOGIN_FOR_MEMBERS = cfg.allow_username_login
144
145AUTHENTICATION_BACKENDS = [
146 # NOTE: the nomnom.nominate.apps.AppConfig.ready() hook will install handlers in this, as the first
147 # set. Any handler in here will be superseded by those.
148 #
149 # Uncomment following if you want to access the admin
150 "django.contrib.auth.backends.ModelBackend",
151]
152
153MIDDLEWARE = [
154 "debug_toolbar.middleware.DebugToolbarMiddleware",
155 "django.middleware.security.SecurityMiddleware",
156 "whitenoise.middleware.WhiteNoiseMiddleware",
157 "django.contrib.sessions.middleware.SessionMiddleware",
158 "django.middleware.common.CommonMiddleware",
159 "django.middleware.csrf.CsrfViewMiddleware",
160 "django.contrib.auth.middleware.AuthenticationMiddleware",
161 "django.contrib.messages.middleware.MessageMiddleware",
162 "django.middleware.clickjacking.XFrameOptionsMiddleware",
163 "django_browser_reload.middleware.BrowserReloadMiddleware",
164 "social_django.middleware.SocialAuthExceptionMiddleware",
165 "django_htmx.middleware.HtmxMiddleware",
166 "nomnom.middleware.HtmxMessageMiddleware",
167 "waffle.middleware.WaffleMiddleware",
168 "django_structlog.middlewares.RequestMiddleware",
169]
170
171DJANGO_STRUCTLOG_CELERY_ENABLED = True
172
173ROOT_URLCONF = "nomnom_dev.urls"
174
175TEMPLATES = [
176 {
177 "BACKEND": "django.template.backends.django.DjangoTemplates",
178 "DIRS": [],
179 "APP_DIRS": True,
180 "OPTIONS": {
181 "context_processors": [
182 "django.template.context_processors.debug",
183 "django.template.context_processors.request",
184 "django.contrib.auth.context_processors.auth",
185 "django.contrib.messages.context_processors.messages",
186 "social_django.context_processors.backends",
187 "social_django.context_processors.login_redirect",
188 "nomnom.nominate.context_processors.site",
189 "nomnom.nominate.context_processors.inject_login_form",
190 ],
191 "string_if_invalid": InvalidStringShowWarning("%s"),
192 },
193 },
194]
195
196WSGI_APPLICATION = "nomnom_dev.wsgi.application"
197
198
199# Database
200# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
201
202DATABASES = {
203 "default": {
204 "ENGINE": "django.db.backends.postgresql",
205 "NAME": cfg.db.name,
206 "USER": cfg.db.user,
207 "PASSWORD": cfg.db.password,
208 "HOST": cfg.db.host,
209 "PORT": str(cfg.db.port),
210 "DISABLE_SERVER_SIDE_CURSORS": True,
211 }
212}
213
214# reuse some of those DB connections
215if not DEBUG:
216 CONN_HEALTH_CHECKS = True
217 CONN_MAX_AGE = 600
218
219CACHES = {
220 "default": {
221 "BACKEND": "django.core.cache.backends.redis.RedisCache",
222 "LOCATION": f"redis://{cfg.redis.host}:{cfg.redis.port}",
223 },
224}
225
226# Password validation
227# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
228
229AUTH_PASSWORD_VALIDATORS = [
230 {
231 "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
232 },
233 {
234 "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
235 },
236 {
237 "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
238 },
239 {
240 "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
241 },
242]
243
244LOGOUT_REDIRECT_URL = "index"
245
246# we are using postgres, so this is recommended in the docs.
247SOCIAL_AUTH_JSONFIELD_ENABLED = True
248
249# If using social authentication, configure it here. See the social_django documentation for
250# details.
251
252# Common social auth settings
253# Can't use the backend-specific one because of https://github.com/python-social-auth/social-core/issues/875
254# SOCIAL_AUTH_CLYDE_LOGIN_ERROR_URL = "nominate:login_error"
255SOCIAL_AUTH_LOGIN_ERROR_URL = "election:login_error"
256
257SOCIAL_AUTH_ADMIN_USER_SEARCH_FIELDS = ["username", "first_name", "email"]
258
259# This is a probably-okay social auth pipeline, but you may need to adjust it for your needs.
260# See the social_django documentation for details.
261SOCIAL_AUTH_PIPELINE = [
262 "social_core.pipeline.social_auth.social_details",
263 "social_core.pipeline.social_auth.social_uid",
264 "social_core.pipeline.social_auth.auth_allowed",
265 "social_core.pipeline.social_auth.social_user",
266 "social_core.pipeline.user.get_username",
267 "social_core.pipeline.user.create_user",
268 "social_core.pipeline.social_auth.associate_user",
269 "social_core.pipeline.social_auth.load_extra_data",
270 "social_core.pipeline.user.user_details",
271 "nomnom.nominate.social_auth.pipeline.get_wsfs_permissions",
272 "nomnom.nominate.social_auth.pipeline.set_user_wsfs_membership",
273 "nomnom.nominate.social_auth.pipeline.normalize_date_fields",
274 "nomnom.nominate.social_auth.pipeline.restrict_wsfs_permissions_by_date",
275 "nomnom.nominate.social_auth.pipeline.add_election_permissions",
276]
277# Internationalization
278# https://docs.djangoproject.com/en/5.0/topics/i18n/
279
280LANGUAGE_CODE = "en-us"
281
282TIME_ZONE = "UTC"
283
284USE_I18N = True
285
286USE_TZ = True
287
288SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
289USE_X_FORWARDED_HOST = True
290USE_X_FORWARDED_PORT = True
291
292# Static files (CSS, JavaScript, Images)
293# https://docs.djangoproject.com/en/5.0/howto/static-files/
294
295STATIC_URL = "static/"
296STATIC_ROOT = cfg.static_file_root
297GOOGLE_FONTS_DIR = BASE_DIR / "nomnom_dev" / "static" / "google_fonts"
298STATICFILES_DIRS = [GOOGLE_FONTS_DIR]
299
300# Default primary key field type
301# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
302
303DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
304
305# Async tasks
306CELERY_RESULT_BACKEND = "django-db"
307CELERY_CACHE_BACKEND = "default"
308CELERY_BROKER_URL = f"redis://{cfg.redis.host}:{cfg.redis.port}"
309CELERY_TIMEZONE = "America/Los_Angeles"
310CELERY_TASK_TRACK_STARTED = True
311CELERY_TASK_TIME_LIMIT = 30 * 60
312CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True
313
314# Presentation
315ADMIN_MANAGED_ATTRIBUTES = bleach.sanitizer.ALLOWED_ATTRIBUTES.copy()
316ADMIN_MANAGED_ATTRIBUTES.update(
317 {
318 "span": bleach.sanitizer.ALLOWED_ATTRIBUTES.get("span", []) + ["lang"],
319 "p": bleach.sanitizer.ALLOWED_ATTRIBUTES.get("p", []) + ["lang"],
320 "div": bleach.sanitizer.ALLOWED_ATTRIBUTES.get("div", []) + ["lang"],
321 }
322)
323ADMIN_ALERT_ATTRIBUTES = ADMIN_MANAGED_ATTRIBUTES.copy()
324ADMIN_ALERT_ATTRIBUTES["div"] += ["class", "role"]
325
326MARKDOWNIFY = {
327 "default": {
328 "WHITELIST_TAGS": bleach.sanitizer.ALLOWED_TAGS | {"p", "h4", "h5"},
329 },
330 "admin-content": {
331 "WHITELIST_TAGS": bleach.sanitizer.ALLOWED_TAGS | {"p", "h4", "h5", "span"},
332 "WHITELIST_ATTRS": ADMIN_MANAGED_ATTRIBUTES,
333 },
334 "admin-alert": {
335 "WHITELIST_TAGS": bleach.sanitizer.ALLOWED_TAGS
336 | {"p", "h4", "h5", "span", "div"},
337 "WHITELIST_ATTRS": ADMIN_ALERT_ATTRIBUTES,
338 },
339 "admin-label": {
340 # no block-level elements
341 "WHITELIST_TAGS": bleach.sanitizer.ALLOWED_TAGS
342 | {"span"} - {"blockquote", "ol", "li", "ul"}
343 | {"s"},
344 "WHITELIST_ATTRS": ADMIN_MANAGED_ATTRIBUTES,
345 },
346}
347
348MARKDOWN_EXTENSIONS = [
349 "pymdownx.tilde",
350]
351
352
353BOOTSTRAP5 = {
354 "field_renderers": {
355 "default": "django_bootstrap5.renderers.FieldRenderer",
356 "blank-safe": "nomnom.nominate.renderers.BlankSafeFieldRenderer",
357 },
358}
359
360GOOGLE_FONTS = [
361 "Roboto",
362 "Roboto Slab",
363 "Gruppo",
364]
365
366# Email
367EMAIL_HOST = cfg.email.host
368EMAIL_PORT = cfg.email.port
369EMAIL_HOST_USER = cfg.email.host_user
370EMAIL_HOST_PASSWORD = cfg.email.host_password
371EMAIL_USE_TLS = cfg.email.use_tls
372
373# Structlog configuration - must come before LOGGING
374structlog.configure(
375 processors=[
376 structlog.contextvars.merge_contextvars,
377 structlog.stdlib.filter_by_level,
378 structlog.processors.TimeStamper(fmt="iso"),
379 structlog.stdlib.add_logger_name,
380 structlog.stdlib.add_log_level,
381 structlog.stdlib.PositionalArgumentsFormatter(),
382 structlog.processors.StackInfoRenderer(),
383 structlog.processors.UnicodeDecoder(),
384 structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
385 ],
386 logger_factory=structlog.stdlib.LoggerFactory(),
387 wrapper_class=structlog.stdlib.BoundLogger,
388 cache_logger_on_first_use=True,
389)
390
391LOGGING = {
392 "version": 1,
393 "disable_existing_loggers": False,
394 "filters": {
395 "require_debug_true": {
396 "()": "django.utils.log.RequireDebugTrue",
397 },
398 },
399 "formatters": {
400 "plain_console": {
401 "()": "structlog.stdlib.ProcessorFormatter",
402 "processors": [
403 structlog.stdlib.ProcessorFormatter.remove_processors_meta,
404 structlog.dev.ConsoleRenderer(colors=True),
405 ],
406 "foreign_pre_chain": [
407 structlog.contextvars.merge_contextvars,
408 structlog.processors.TimeStamper(fmt="iso"),
409 structlog.stdlib.add_log_level,
410 structlog.stdlib.add_logger_name,
411 ],
412 },
413 "json": {
414 "()": "structlog.stdlib.ProcessorFormatter",
415 "processors": [
416 structlog.stdlib.ProcessorFormatter.remove_processors_meta,
417 structlog.processors.JSONRenderer(),
418 ],
419 "foreign_pre_chain": [
420 structlog.contextvars.merge_contextvars,
421 structlog.processors.TimeStamper(fmt="iso"),
422 structlog.stdlib.add_log_level,
423 structlog.stdlib.add_logger_name,
424 ],
425 },
426 },
427 "handlers": {
428 "console": {
429 "level": "INFO",
430 "class": "logging.StreamHandler",
431 "formatter": "plain_console" if DEBUG else "json",
432 },
433 "debug_console": {
434 "level": "DEBUG",
435 "filters": ["require_debug_true"],
436 "class": "logging.StreamHandler",
437 "formatter": "plain_console",
438 },
439 "json_file": {
440 "level": "DEBUG",
441 "class": "logging.handlers.RotatingFileHandler",
442 "filename": BASE_DIR / ".local/logs" / "json.log",
443 "maxBytes": 10485760, # 10MB
444 "backupCount": 5,
445 "formatter": "json",
446 },
447 },
448 "loggers": {
449 "django.db.backends": {
450 "level": os.environ.get("DJANGO_DB_LOG_LEVEL", "INFO"),
451 "handlers": ["debug_console"],
452 },
453 "django_structlog": {
454 "handlers": ["console", "json_file"],
455 "level": "INFO",
456 },
457 "django.server": {
458 "handlers": ["console", "json_file"],
459 "level": "INFO",
460 "propagate": False,
461 },
462 "django.request": {
463 "handlers": ["console", "json_file"],
464 "level": "INFO",
465 "propagate": False,
466 },
467 # Root logger - catches all logs not matched by more specific loggers
468 "": {
469 "handlers": ["console", "json_file"],
470 "level": "DEBUG" if DEBUG else "INFO",
471 },
472 },
473}
474
475
476# Sentry
477if cfg.sentry_sdk.dsn is not None:
478 # settings.py
479 import sentry_sdk
480
481 sentry_sdk.init(
482 dsn=cfg.sentry_sdk.dsn,
483 # Set traces_sample_rate to 1.0 to capture 100%
484 # of transactions for performance monitoring.
485 traces_sample_rate=1.0,
486 # Set profiles_sample_rate to 1.0 to profile 100%
487 # of sampled transactions.
488 # We recommend adjusting this value in production.
489 profiles_sample_rate=1.0,
490 # Our environment
491 environment=cfg.sentry_sdk.environment,
492 # include the user and client IP
493 send_default_pii=True,
494 )
495
496if cfg.debug:
497 import icecream
498
499 icecream.install()
500
501if cfg.debug:
502 INTERNAL_IPS = [
503 "127.0.0.1",
504 ]
505
506# Seed data can come from here:
507FIXTURE_DIRS = [BASE_DIR / "seed"]
508
509DEBUG_TOOLBAR_ENABLED = True
510DEBUG_PROPAGATE_EXCEPTIONS = True
511try:
512 from .settings_override import * # noqa: F403
513except ImportError:
514 ...
515
516djp.settings(globals())
Back to Top