team-3/src/c3nav/settings.py
2024-09-18 01:37:51 +02:00

816 lines
32 KiB
Python

# c3nav settings, mostly taken from the pretix project
import math
import os
import re
import string
import sys
from contextlib import suppress
from pathlib import Path
from typing import Optional
import django.conf.locale
from django.contrib.messages import constants as messages
from django.core.exceptions import ImproperlyConfigured
from django.utils.crypto import get_random_string
from django.utils.dateparse import parse_duration
from django.utils.translation import gettext_lazy as _
from pyproj import Proj, Transformer
from c3nav import __version__ as c3nav_version
from c3nav.utils.config import C3navConfigParser
from c3nav.utils.environ import Env
def get_data_dir(setting: str, fallback: Path, create: bool = True, parents: bool = False,
config_section: str = 'c3nav', config_option: Optional[str] = None):
if not config_option:
config_option = setting.lower()
subdir = config.get(config_section, config_option, fallback=None, env='C3NAV_' + setting)
subdir = Path(subdir).resolve() if subdir else fallback
if not subdir.exists():
if create:
subdir.mkdir(parents=parents)
else:
raise FileNotFoundError('The %s directory [%s] doesn\'t exist.' % (config_option, subdir))
elif not subdir.is_dir():
raise NotADirectoryError('The path set for the %s directory [%s] is not a directory.' % (config_option, subdir))
return subdir
env = Env()
C3NAV_CONFIG = config = C3navConfigParser(env=env)
if 'C3NAV_CONFIG' in env:
# if a config file is explicitly defined, make sure we can read it.
env.path('C3NAV_CONFIG').open('r')
config.read(['/etc/c3nav/c3nav.cfg', os.path.expanduser('~/.c3nav.cfg'), env.str('C3NAV_CONFIG', 'c3nav.cfg')],
encoding='utf-8')
INSTANCE_NAME = config.get('c3nav', 'name', fallback='', env='C3NAV_INSTANCE_NAME')
SENTRY_DSN = config.get('sentry', 'dsn', fallback=None, env='SENTRY_DSN')
with suppress(ImportError):
if SENTRY_DSN:
import sentry_sdk
from sentry_sdk.integrations.celery import CeleryIntegration
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.scrubber import DEFAULT_DENYLIST, EventScrubber
sensitive_env_vars = ['C3NAV_DJANGO_SECRET', 'C3NAV_TILE_SECRET', 'C3NAV_DATABASE', 'C3NAV_DATABASE_PASSWORD',
'C3NAV_MEMCACHED', 'C3NAV_MEMCACHED_USER', 'C3NAV_MEMCACHED_PASSWORD',
'C3NAV_REDIS', 'C3NAV_CELERY_BROKER', 'C3NAV_CELERY_BACKEND',
'C3NAV_EMAIL', 'C3NAV_EMAIL_PASSWORD']
sensitive_vars = ['SECRET_KEY', 'TILE_SECRET_KEY', 'DATABASES', 'CACHES', 'BROKER_URL', 'CELERY_RESULT_BACKEND']
denylist = DEFAULT_DENYLIST + sensitive_env_vars + sensitive_vars
sentry_sdk.init(
dsn=SENTRY_DSN,
release=c3nav_version,
integrations=[CeleryIntegration(), DjangoIntegration()],
event_scrubber=EventScrubber(denylist=denylist),
enable_tracing=bool(config.getfloat('sentry', 'traces_sample_rate', fallback=0.0)),
traces_sample_rate=config.getfloat('sentry', 'traces_sample_rate', fallback=0.0),
)
# Build paths inside the project like this: BASE_DIR / 'something'
PROJECT_DIR = Path(__file__).resolve().parent
BASE_DIR = PROJECT_DIR.parent
DATA_DIR = get_data_dir('DATA_DIR', BASE_DIR / 'data', parents=True, config_option='datadir')
LOG_DIR = get_data_dir('LOG_DIR', DATA_DIR / 'logs', config_option='logdir')
MEDIA_ROOT = get_data_dir('MEDIA_ROOT', DATA_DIR / 'media', config_section='django')
STATIC_ROOT = get_data_dir('STATIC_ROOT', PROJECT_DIR / 'static.dist', config_section='django')
SOURCES_ROOT = get_data_dir('SOURCES_ROOT', DATA_DIR / 'sources')
MAP_ROOT = get_data_dir('MAP_ROOT', DATA_DIR / 'map')
RENDER_ROOT = get_data_dir('RENDER_ROOT', DATA_DIR / 'render')
TILES_ROOT = get_data_dir('TILES_ROOT', DATA_DIR / 'tiles')
CACHE_ROOT = get_data_dir('CACHE_ROOT', DATA_DIR / 'cache')
STATS_ROOT = get_data_dir('STATS_ROOT', DATA_DIR / 'stats')
PREVIEWS_ROOT = get_data_dir('PREVIEWS_ROOT', DATA_DIR / 'previews')
# override the matplotlib default config directory if it's not configured
os.environ.setdefault('MPLCONFIGDIR', str(get_data_dir('MPLCONFIGDIR', CACHE_ROOT / 'matplotlib')))
PUBLIC_EDITOR = config.getboolean('c3nav', 'editor', fallback=True)
PUBLIC_BASE_MAPDATA = config.getboolean('c3nav', 'public_base_mapdata', fallback=False)
AUTO_PROCESS_UPDATES = config.getboolean('c3nav', 'auto_process_updates', fallback=True)
RANDOM_LOCATION_GROUPS = config.getlist('c3nav', 'random_location_groups', fallback=None)
if RANDOM_LOCATION_GROUPS:
RANDOM_LOCATION_GROUPS = tuple(int(i) for i in RANDOM_LOCATION_GROUPS)
SECRET_KEY = config.get('django', 'secret', fallback=None)
if not SECRET_KEY:
SECRET_FILE = config.get('django', 'secret_file', fallback=None)
if SECRET_FILE:
SECRET_FILE = Path(SECRET_FILE)
else:
SECRET_FILE = DATA_DIR / '.secret'
if SECRET_FILE.exists():
with open(SECRET_FILE, 'r') as f:
SECRET_KEY = f.read().strip()
else:
SECRET_KEY = get_random_string(50, string.printable)
with open(SECRET_FILE, 'w') as f:
os.chmod(SECRET_FILE, 0o600)
try:
os.chown(SECRET_FILE, os.getuid(), os.getgid())
except AttributeError:
pass
f.write(SECRET_KEY)
SECRET_TILE_KEY = config.get('c3nav', 'tile_secret', fallback=None)
if not SECRET_TILE_KEY:
SECRET_TILE_FILE = config.get('c3nav', 'tile_secret_file', fallback=None)
if SECRET_TILE_FILE:
SECRET_TILE_FILE = Path(SECRET_TILE_FILE)
else:
SECRET_TILE_FILE = DATA_DIR / '.tile_secret'
if SECRET_TILE_FILE.exists():
with open(SECRET_TILE_FILE, 'r') as f:
SECRET_TILE_KEY = f.read().strip()
else:
SECRET_TILE_KEY = get_random_string(50, string.printable)
with open(SECRET_TILE_FILE, 'w') as f:
os.chmod(SECRET_TILE_FILE, 0o600)
try:
os.chown(SECRET_TILE_FILE, os.getuid(), os.getgid())
except AttributeError:
pass
f.write(SECRET_TILE_KEY)
SECRET_MESH_KEY = config.get('c3nav', 'mesh_secret', fallback=None)
if not SECRET_MESH_KEY:
SECRET_MESH_FILE = config.get('c3nav', 'mesh_secret_file', fallback=None)
if SECRET_MESH_FILE:
SECRET_MESH_FILE = Path(SECRET_MESH_FILE)
else:
SECRET_MESH_FILE = DATA_DIR / '.mesh_secret'
if SECRET_MESH_FILE.exists():
with open(SECRET_MESH_FILE, 'r') as f:
SECRET_MESH_KEY = f.read().strip()
else:
SECRET_MESH_KEY = get_random_string(50, string.printable)
with open(SECRET_MESH_FILE, 'w') as f:
os.chmod(SECRET_MESH_FILE, 0o600)
try:
os.chown(SECRET_MESH_FILE, os.getuid(), os.getgid())
except AttributeError:
pass
f.write(SECRET_MESH_KEY)
# Adjustable settings
debug_fallback = "runserver" in sys.argv
DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback, env='C3NAV_DEBUG')
BRANDING = config.get('c3nav', 'branding', fallback='c3nav')
APP_ENABLED = config.getboolean('c3nav', 'app_enabled', fallback=False)
ENABLE_MESH = config.getboolean('c3nav', 'enable_mesh', fallback=True, env='ENABLE_MESH')
SERVE_ANYTHING = config.getboolean('c3nav', 'serve_anything', fallback=True, env='SERVE_ANYTHING')
SERVE_API = config.getboolean('c3nav', 'serve_api', fallback=SERVE_ANYTHING, env='SERVE_API')
# how many location lookups to cache in each worker's in-memory LRU cache proxy
CACHE_SIZE_LOCATIONS = config.getint('c3nav', 'cache_size_locations', fallback=128)
CACHE_SIZE_API = config.getint('c3nav', 'cache_size_api', fallback=64)
RENDER_SCALE = config.getfloat('c3nav', 'render_scale', fallback=20.0)
IMAGE_RENDERER = config.get('c3nav', 'image_renderer', fallback='svg')
SVG_RENDERER = config.get('c3nav', 'svg_renderer', fallback='rsvg-convert')
CACHE_TILES = config.getboolean('c3nav', 'cache_tiles', fallback=not DEBUG)
CACHE_PREVIEWS = config.getboolean('c3nav', 'cache_previews', fallback=not DEBUG)
CACHE_RESOLUTION = config.getint('c3nav', 'cache_resolution', fallback=4)
IMPRINT_LINK = config.get('c3nav', 'imprint_link', fallback=None)
IMPRINT_PATRONS = config.get('c3nav', 'imprint_patrons', fallback=None)
IMPRINT_TEAM = config.get('c3nav', 'imprint_team', fallback=None)
IMPRINT_HOSTING = config.get('c3nav', 'imprint_hosting', fallback=None)
ABOUT_EXTRA = config.get('c3nav', 'about_extra', fallback=None)
INITIAL_LEVEL = config.get('c3nav', 'initial_level', fallback=None)
INITIAL_BOUNDS = config.get('c3nav', 'initial_bounds', fallback='').split(' ')
GRID_ROWS = config.get('c3nav', 'grid_rows', fallback=None)
GRID_COLS = config.get('c3nav', 'grid_cols', fallback=None)
MAIN_PREVIEW_SLUG = config.get('c3nav', 'main_preview_slug', fallback='level-0')
if len(INITIAL_BOUNDS) == 4:
try:
INITIAL_BOUNDS = tuple(float(i) for i in INITIAL_BOUNDS)
except ValueError:
INITIAL_BOUNDS = None
else:
INITIAL_BOUNDS = None
HUB_API_BASE = config.get('c3nav', 'hub_api_base', fallback='').removesuffix('/')
HUB_API_SECRET = config.get('c3nav', 'hub_api_secret', fallback='')
if not HUB_API_SECRET:
HUB_API_SECRET_FILE = config.get('c3nav', 'hub_api_secret_file', fallback=None)
if HUB_API_SECRET_FILE:
HUB_API_SECRET_FILE = Path(HUB_API_SECRET_FILE)
else:
HUB_API_SECRET_FILE = DATA_DIR / '.hub_api_secret'
if HUB_API_SECRET_FILE.exists():
HUB_API_SECRET = HUB_API_SECRET_FILE.read_text().strip()
_db_backend = config.get('database', 'backend', fallback='sqlite3')
DATABASES: dict[str, dict[str, str | int | Path]] = {
'default': env.db_url('C3NAV_DATABASE') if 'C3NAV_DATABASE' in env else {
'ENGINE': _db_backend if '.' in _db_backend else 'django.db.backends.' + _db_backend,
}
}
for key in ('NAME', 'USER', 'PASSWORD', 'HOST', 'PORT'):
if 'C3NAV_DATABASE' in env:
# if the C3NAV_DATABASE is present all database options in the config files are ignored
value = env.str('C3NAV_DATABASE_' + key, default=None)
else:
value = config.get('database', key.lower(), fallback=None)
if value:
DATABASES['default'][key] = value
elif key == 'NAME':
DATABASES['default'].setdefault(key, DATA_DIR / 'db.sqlite3' if _db_backend.endswith('sqlite3')
else (f'c3nav_{INSTANCE_NAME}' if INSTANCE_NAME else 'c3nav'))
DATABASES['default'].setdefault('CONN_MAX_AGE',
config.getint('database', 'conn_max_age',
fallback=(0 if _db_backend.endswith('sqlite3') else 120)))
DATABASES['default'].setdefault('CONN_HEALTH_CHECKS', not _db_backend.endswith('sqlite3'))
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField'
STATIC_URL = config.get('django', 'static_url', fallback='/static/', env='C3NAV_STATIC_URL')
MEDIA_URL = config.get('django', 'media_url', fallback='/media/', env='C3NAV_MEDIA_URL')
ALLOWED_HOSTS = config.getlist('django', 'allowed_hosts', fallback='*')
if config.getboolean('django', 'reverse_proxy', fallback=False):
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
LANGUAGE_CODE = config.get('locale', 'default', fallback='en', env='C3NAV_DEFAULT_LOCALE')
TIME_ZONE = config.get('locale', 'timezone', fallback='UTC', env='C3NAV_TIMEZONE')
MAIL_FROM = SERVER_EMAIL = DEFAULT_FROM_EMAIL = config.get('email', 'from', fallback='c3nav@localhost')
EMAIL_HOST = config.get('email', 'host', fallback='' if DEBUG else 'localhost')
EMAIL_PORT = config.getint('email', 'port', fallback=25)
EMAIL_HOST_USER = config.get('email', 'user', fallback='')
EMAIL_HOST_PASSWORD = config.get('email', 'password', fallback='')
EMAIL_USE_TLS = config.getboolean('email', 'tls', fallback=False)
EMAIL_USE_SSL = config.getboolean('email', 'ssl', fallback=False)
EMAIL_BACKEND = config.get(
'email', 'ssl',
fallback='django.core.mail.backends.' + ('smtp' if EMAIL_HOST else 'console') + '.EmailBackend',
)
if 'C3NAV_EMAIL' in env:
vars().update(env.email_url('C3NAV_EMAIL'))
EMAIL_SUBJECT_PREFIX = ('[c3nav-%s] ' % INSTANCE_NAME) if INSTANCE_NAME else '[c3nav]'
if config.has_section('mail'):
raise ImproperlyConfigured('mail config section got renamed to email. Please fix your config file.')
ADMINS = [('Admin', n) for n in config.getlist('mail', 'admins', fallback='')]
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
'LOCATION': 'unique-snowflake',
}
}
HAS_REAL_CACHE = False
SESSION_ENGINE = "django.contrib.sessions.backends.db"
HAS_MEMCACHED = bool(config.get('memcached', 'location', fallback=None, env='C3NAV_MEMCACHED'))
if HAS_MEMCACHED:
HAS_REAL_CACHE = True
CACHES['default'] = {
'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
'LOCATION': config.get('memcached', 'location', env='C3NAV_MEMCACHED'),
'OPTIONS': {
'username': config.get('memcached', 'username', fallback=None),
'password': config.get('memcached', 'password', fallback=None),
}
}
SESSION_ENGINE = "django.contrib.sessions.backends.cached_db"
HAS_REDIS = bool(config.get('redis', 'location', fallback=None, env='C3NAV_REDIS'))
REDIS_CONNECTION_POOL = None
if HAS_REDIS:
import redis
HAS_REAL_CACHE = True
REDIS_SERVERS = config.getlist('redis', 'location', env='C3NAV_REDIS')
CACHES['redis'] = {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": REDIS_SERVERS,
}
if not HAS_MEMCACHED:
CACHES['default'] = CACHES['redis']
else:
SESSION_CACHE_ALIAS = "redis"
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
REDIS_CONNECTION_POOL = redis.ConnectionPool.from_url(REDIS_SERVERS[0])
HAS_CELERY = bool(config.get('celery', 'broker', fallback=None))
if HAS_CELERY:
BROKER_URL = config.get('celery', 'broker')
CELERY_RESULT_BACKEND = config.get('celery', 'backend')
CELERY_SEND_TASK_ERROR_EMAILS = bool(ADMINS)
else:
CELERY_ALWAYS_EAGER = True
CELERY_TASK_SERIALIZER = 'json'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_RESULT_SERIALIZER = 'json'
TILE_CACHE_SERVER = config.get('c3nav', 'tile_cache_server', fallback=None)
# Internal settings
SESSION_COOKIE_NAME = 'c3nav_session'
SESSION_COOKIE_DOMAIN = config.get('c3nav', 'session_cookie_domain', fallback=None)
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SECURE = not DEBUG
SESSION_COOKIE_SAMESITE = 'none' if SESSION_COOKIE_SECURE else 'lax'
LANGUAGE_COOKIE_NAME = 'c3nav_language'
CSRF_COOKIE_NAME = 'c3nav_csrftoken'
CSRF_COOKIE_SECURE = not DEBUG
TILE_ACCESS_COOKIE_NAME = 'c3nav_tile_access'
TILE_ACCESS_COOKIE_DOMAIN = config.get('c3nav', 'tile_access_cookie_domain', fallback=None)
TILE_ACCESS_COOKIE_HTTPONLY = True
TILE_ACCESS_COOKIE_SECURE = not DEBUG
TILE_ACCESS_COOKIE_SAMESITE = 'none' if SESSION_COOKIE_SECURE else 'lax'
SSO_ENABLED = config.getboolean('sso', 'enabled', fallback=False)
# Application definition
INSTALLED_APPS = [
*(["daphne"] if DEBUG else []),
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'compressor',
'bootstrap3',
*(['social_django'] if SSO_ENABLED else []),
*(["ninja"] if SERVE_API else []),
'c3nav.api',
'c3nav.mapdata',
'c3nav.routing',
'c3nav.site',
'c3nav.control',
*(["c3nav.mesh"] if ENABLE_MESH else []),
'c3nav.editor',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'c3nav.mapdata.middleware.NoLanguageMiddleware',
'django.middleware.locale.LocaleMiddleware',
'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',
'c3nav.mapdata.middleware.UserDataMiddleware',
'c3nav.site.middleware.MobileclientMiddleware',
'c3nav.control.middleware.UserPermissionsMiddleware',
# 'c3nav.api.middleware.JsonRequestBodyMiddleware', # might still be needed in editor
]
with suppress(ImportError):
import debug_toolbar # noqa
INSTALLED_APPS.append('debug_toolbar')
MIDDLEWARE.append('debug_toolbar.middleware.DebugToolbarMiddleware')
with suppress(ImportError):
import htmlmin # noqa
MIDDLEWARE += [
'htmlmin.middleware.HtmlMinifyMiddleware',
'htmlmin.middleware.MarkRequestMiddleware',
]
with suppress(ImportError):
import django_extensions # noqa
INSTALLED_APPS.append('django_extensions')
METRICS = config.getboolean('c3nav', 'metrics', fallback=False)
if METRICS:
try:
import django_prometheus # noqa
INSTALLED_APPS.append('django_prometheus')
MIDDLEWARE = [
'django_prometheus.middleware.PrometheusBeforeMiddleware',
*MIDDLEWARE,
'django_prometheus.middleware.PrometheusAfterMiddleware',
]
except ImportError:
METRICS = False
# Security settings
X_FRAME_OPTIONS = 'DENY'
# URL settings
ROOT_URLCONF = 'c3nav.urls'
WSGI_APPLICATION = 'c3nav.wsgi.application'
ASGI_APPLICATION = 'c3nav.asgi.application'
if HAS_REDIS:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": config.getlist('redis', 'location', env='C3NAV_REDIS'),
},
},
}
else:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
USE_I18N = True
USE_L10N = True
USE_TZ = True
NINJA_PAGINATION_CLASS = "ninja.pagination.LimitOffsetPagination"
LOCALE_PATHS = (
PROJECT_DIR / 'locale',
)
EXTRA_LANG_INFO = {
'en-UW': {
'bidi': False,
'code': 'en-UW',
'name': 'Engwish UwU',
'name_local': u'Engwish UwU', #unicode codepoints here
},
}
# Add custom languages not provided by Django
LANG_INFO = dict(django.conf.locale.LANG_INFO, **EXTRA_LANG_INFO)
django.conf.locale.LANG_INFO = LANG_INFO
SELECTED_LANGUAGES = frozenset(config.getlist('locale', 'languages', fallback='en,de',
env='C3NAV_LANGUAGES'))
LANGUAGES = [(code, name) for code, name in [
('en', _('English')),
('en-UW', _('Engwish UwU')),
('de', _('German')),
] if code in SELECTED_LANGUAGES]
template_loaders = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
if not DEBUG:
template_loaders = (
('django.template.loaders.cached.Loader', template_loaders),
)
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'OPTIONS': {
'context_processors': [
'django.contrib.auth.context_processors.auth',
'django.template.context_processors.debug',
'django.template.context_processors.i18n',
'django.template.context_processors.request',
'django.contrib.messages.context_processors.messages',
'c3nav.site.context_processors.logos',
'c3nav.site.context_processors.user_data_json',
'c3nav.site.context_processors.theme',
'c3nav.site.context_processors.header_logo_mask',
],
'loaders': template_loaders
},
},
]
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
'compressor.finders.CompressorFinder',
'c3nav.site.finders.LogoFinder',
)
BOOTSTRAP3 = {
'success_css_class': '',
}
STATICFILES_DIRS = [
BASE_DIR / 'c3nav' / 'static',
*config.getlist('c3nav', 'extra_static_dirs', fallback=''),
]
COMPRESS_PRECOMPILERS = (
('text/x-scss', 'django_libsass.SassCompiler'),
)
COMPRESS_ENABLED = COMPRESS_OFFLINE = not debug_fallback
COMPRESS_CSS_FILTERS = (
'compressor.filters.css_default.CssAbsoluteFilter',
'compressor.filters.cssmin.CSSCompressorFilter',
)
COMPRESS_CSS_HASHING_METHOD = 'content'
HEADER_LOGO = config.get('c3nav', 'header_logo', fallback=None)
HEADER_LOGO_MASK_MODE = config.get('c3nav', 'header_logo_mask_mode', fallback=None)
FAVICON = config.get('c3nav', 'favicon', fallback=None)
FAVICON_PACKAGE = config.get('c3nav', 'favicon_package', fallback=None)
PRIMARY_COLOR_RANDOMISATION = {
'mode': config.get('primary_color_randomization', 'mode', fallback='off'),
'duration': parse_duration(config.get('primary_color_randomization', 'duration', fallback='1:00')),
'chroma': float(config.get('primary_color_randomization', 'chroma', fallback='0.5')),
'lightness': float(config.get('primary_color_randomization', 'lightness', fallback='0.3')),
}
def oklch_to_oklab(L, C, h):
from math import cos, sin
a = C * cos(h)
b = C * sin(h)
return L, a, b
def clamp(x, low, high):
return min(max(x, low), high)
def oklab_to_linear_rgb(L, a, b):
"""
see https://bottosson.github.io/posts/oklab/
"""
l_ = L + 0.3963377774 * a + 0.2158037573 * b
m_ = L - 0.1055613458 * a - 0.0638541728 * b
s_ = L - 0.0894841775 * a - 1.2914855480 * b
l = l_ * l_ * l_
m = m_ * m_ * m_
s = s_ * s_ * s_
return (
clamp(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s, 0, 1),
clamp(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s, 0, 1),
clamp(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s, 0, 1),
)
def linear_to_s(linear):
if linear <= 0.0031308:
return linear * 12.92
else:
return 1.055 * pow(linear, 1.0 / 2.4) - 0.055
def linear_rgb_to_srgb(r, g, b):
return linear_to_s(r), linear_to_s(g), linear_to_s(b)
def hex_from_oklch(L, C, h):
oklab = oklch_to_oklab(L, C, h)
linear_rgb = oklab_to_linear_rgb(*oklab)
srgb = linear_rgb_to_srgb(*linear_rgb)
srgb255 = tuple(int(round(x * 255, 0)) for x in srgb)
hex = '#%0.2X%0.2X%0.2X' % srgb255
return hex
RANDOM_PRIMARY_COLOR_LIST = [hex_from_oklch(PRIMARY_COLOR_RANDOMISATION['lightness'],
PRIMARY_COLOR_RANDOMISATION['chroma'],
x) for x in range(0, 360)]
DEFAULT_THEME = config.getint('c3nav', 'default_theme', fallback=0)
BASE_THEME = {
'is_dark': config.getboolean('theme', 'is_dark', fallback=False),
'randomize_primary_color': config.getboolean('theme', 'randomize_primary_color', fallback=False),
'map': {
'background': config.get('theme', 'map_background', fallback='#dcdcdc'),
'wall_fill': config.get('theme', 'map_wall_fill', fallback='#aaaaaa'),
'wall_border': config.get('theme', 'map_wall_border', fallback='#666666'),
'door_fill': config.get('theme', 'map_door_fill', fallback='#ffffff'),
'ground_fill': config.get('theme', 'map_ground_fill', fallback='#eeeeee'),
'obstacles_default_fill': config.get('theme', 'map_obstacles_default_fill', fallback='#b7b7b7'),
'obstacles_default_border': config.get('theme', 'map_obstacles_default_border', fallback='#888888'),
'highlight': config.get('theme', 'css_primary', fallback='#9b4dca'),
},
'css_vars': {
'initial': config.get('theme', 'css_initial', fallback='#ffffff'),
'primary': config.get('theme', 'css_primary', fallback='#9b4dca'),
'logo': config.get('theme', 'css_logo', fallback=None),
'secondary': config.get('theme', 'css_secondary', fallback='#525862'),
'tertiary': config.get('theme', 'css_tertiary', fallback='#f0f0f0'),
'quaternary': config.get('theme', 'css_quaternary', fallback='#767676'),
'quinary': config.get('theme', 'css_quinary', fallback='#cccccc'),
'header-text': config.get('theme', 'css_header_text', fallback='#ffffff'),
'header-text-hover': config.get('theme', 'css_header_text_hover', fallback='#eeeeee'),
'header-background': config.get('theme', 'css_header_background', fallback='#000000'),
'shadow': config.get('theme', 'css_shadow', fallback='#000000'),
'overlay-background': config.get('theme', 'css_overlay_background', fallback='#ffffff'),
'grid': config.get('theme', 'css_grid', fallback='#000000'),
'modal-backdrop': config.get('theme', 'css_modal_backdrop', fallback='#000000'),
'route-dots-shadow': config.get('theme', 'css_route_dots_shadow', fallback='#ffffff'),
'leaflet-background': config.get('theme', 'map_background', fallback='#dcdcdc'),
}
}
WIFI_SSIDS = config.getlist('c3nav', 'wifi_ssids', fallback='')
# Projection
PROJECTION_PROJ4 = config.get('projection', 'proj4', fallback=None)
PROJECTION_ZERO_POINT = config.get('projection', 'zero_point', fallback=None)
PROJECTION_ZERO_POINT_IS_WGS84 = '°' in PROJECTION_ZERO_POINT if PROJECTION_ZERO_POINT else False
PROJECTION_ROTATION = config.getfloat('projection', 'rotation', fallback=0.0)
PROJECTION_ROTATION_MATRIX = config.get('projection', 'rotation_matrix', fallback=None)
PROJECTION_TRANSFORMER: Optional[Transformer] = None
PROJECTION_TRANSFORMER_STRING: Optional[str] = None
if PROJECTION_PROJ4:
if '+units=m' not in PROJECTION_PROJ4:
PROJECTION_PROJ4 += ' +units=m'
PROJECTION_TRANSFORMER_STRING = re.sub(r'\s?\+no_defs', '', PROJECTION_PROJ4)
if (PROJECTION_ZERO_POINT or PROJECTION_ROTATION) and 'pipeline' not in PROJECTION_TRANSFORMER_STRING:
PROJECTION_TRANSFORMER_STRING = f'+proj=pipeline +step {PROJECTION_TRANSFORMER_STRING}'
if PROJECTION_ZERO_POINT:
PROJECTION_ZERO_POINT = tuple((float(i) for i in PROJECTION_ZERO_POINT.split(',')))
if len(PROJECTION_ZERO_POINT) != 2:
raise ImproperlyConfigured(f'invalid projection zero point "{PROJECTION_ZERO_POINT!r}"')
if PROJECTION_ZERO_POINT_IS_WGS84:
PROJECTION_ZERO_POINT = Proj.from_pipeline(PROJECTION_PROJ4).transform(PROJECTION_ZERO_POINT[0],
PROJECTION_ZERO_POINT[1])
PROJECTION_TRANSFORMER_STRING += (f' +step +proj=affine +xoff=-{PROJECTION_ZERO_POINT[0]} '
f'+yoff=-{PROJECTION_ZERO_POINT[1]}')
if PROJECTION_ROTATION != 0:
PROJECTION_ROTATION_MATRIX = (
math.cos(math.radians(PROJECTION_ROTATION)), math.sin(math.radians(PROJECTION_ROTATION)), 0, 0,
-math.sin(math.radians(PROJECTION_ROTATION)), math.cos(math.radians(PROJECTION_ROTATION)), 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
)
elif PROJECTION_ROTATION_MATRIX:
PROJECTION_ROTATION_MATRIX = tuple((float(i) for i in PROJECTION_ROTATION_MATRIX.split(',')))
if len(PROJECTION_ROTATION_MATRIX) != 16:
raise ImproperlyConfigured(f'invalid rotation matrix "{PROJECTION_ROTATION_MATRIX!r}"')
if PROJECTION_ROTATION_MATRIX:
PROJECTION_TRANSFORMER_STRING += (
f' +step +proj=affine '
f'+s11={PROJECTION_ROTATION_MATRIX[0]} +s12={PROJECTION_ROTATION_MATRIX[1]}'
)
if PROJECTION_ROTATION_MATRIX[2] != 0:
PROJECTION_TRANSFORMER_STRING += f' +s13={PROJECTION_ROTATION_MATRIX[2]}'
PROJECTION_TRANSFORMER_STRING += f' +s21={PROJECTION_ROTATION_MATRIX[4]} +s22={PROJECTION_ROTATION_MATRIX[5]}'
if PROJECTION_ROTATION_MATRIX[6] != 0:
PROJECTION_TRANSFORMER_STRING += ' +s23={PROJECTION_ROTATION_MATRIX[6]}'
if PROJECTION_ROTATION_MATRIX[8] != 0:
PROJECTION_TRANSFORMER_STRING += f' +s31={PROJECTION_ROTATION_MATRIX[8]}'
if PROJECTION_ROTATION_MATRIX[9] != 0:
PROJECTION_TRANSFORMER_STRING += f' +s32={PROJECTION_ROTATION_MATRIX[9]}'
if PROJECTION_ROTATION_MATRIX[10] != 1:
PROJECTION_TRANSFORMER_STRING += f' +s33={PROJECTION_ROTATION_MATRIX[10]}'
if PROJECTION_ROTATION_MATRIX[15] != 1:
PROJECTION_TRANSFORMER_STRING += f' +tscale={PROJECTION_ROTATION_MATRIX[15]}'
PROJECTION_TRANSFORMER_STRING += ' +no_defs'
PROJECTION_TRANSFORMER = Proj.from_pipeline(PROJECTION_TRANSFORMER_STRING)
USER_REGISTRATION = config.getboolean('c3nav', 'user_registration', fallback=True)
INTERNAL_IPS = ('127.0.0.1', '::1')
MESSAGE_TAGS = {
messages.INFO: 'alert-info',
messages.ERROR: 'alert-danger',
messages.WARNING: 'alert-warning',
messages.SUCCESS: 'alert-success',
}
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
SILENCED_SYSTEM_CHECKS = ['debug_toolbar.W006']
loglevel = env.str('C3NAV_LOGLEVEL', default='DEBUG' if DEBUG else 'INFO').upper()
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'default': {
'format': '%(levelname)s %(asctime)s %(name)s %(module)s %(message)s'
},
},
'handlers': {
'console': {
'level': loglevel,
'class': 'logging.StreamHandler',
'formatter': 'default'
},
'file': {
'level': loglevel,
'class': 'logging.FileHandler',
'filename': LOG_DIR / 'c3nav.log',
'formatter': 'default'
}
},
'loggers': {
'': {
'handlers': ['file', 'console'],
'level': loglevel,
'propagate': True,
},
'django.request': {
'handlers': ['file', 'console'],
'level': loglevel,
'propagate': True,
},
'django.security': {
'handlers': ['file', 'console'],
'level': loglevel,
'propagate': True,
},
'django.db.backends': {
'handlers': ['file', 'console'],
'level': 'INFO', # Do not output all the queries
'propagate': True,
},
'shapely.geos': {
'handlers': ['file', 'console'],
'level': 'INFO',
'propagate': True,
},
'daphne.ws_protocol': {
'handlers': ['file', 'console'],
'level': 'INFO', # Do not output all communication
'propagate': True,
},
},
}
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# SSO
SOCIAL_AUTH_STRATEGY = 'c3nav.control.sso.C3navStrategy'
SOCIAL_AUTH_JSONFIELD_ENABLED = DATABASES['default']['ENGINE'] == 'django.db.backends.postgresql'
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
if SSO_ENABLED:
# add the enabled authentication backends to AUTHENTICATION_BACKENDS
# we need this despite our own strategy looking it up directly because the backends context processor of
# social_django directly uses the django setting without asking the normal config pipeline
AUTHENTICATION_BACKENDS = (
* config.getlist('sso', 'authentication_backends', fallback=''),
*AUTHENTICATION_BACKENDS,
)
SOCIAL_AUTH_PIPELINE = (
'social_core.pipeline.social_auth.social_details',
'social_core.pipeline.social_auth.social_uid',
'social_core.pipeline.social_auth.auth_allowed',
'social_core.pipeline.social_auth.social_user',
'social_core.pipeline.user.get_username',
'social_core.pipeline.user.create_user',
'social_core.pipeline.social_auth.associate_user',
'social_core.pipeline.social_auth.load_extra_data',
'social_core.pipeline.user.user_details',
'c3nav.control.sso.pipeline.access_permissions',
)