Coverage for config.py: 98%
85 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 11:51 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-21 11:51 +0000
1"""
2Configuration overrides for settings.py
3"""
5import os
6import sys
7from django.urls import reverse_lazy
8from django.utils.translation import gettext_lazy as _
9from django.contrib.messages import constants as message_constants
10from ivatar.settings import BASE_DIR
11from ivatar.settings import MIDDLEWARE
12from ivatar.settings import INSTALLED_APPS
13from ivatar.settings import TEMPLATES
15ADMIN_USERS = []
16ALLOWED_HOSTS = ["*"]
18# These are modified here because settings.py does 'from config import *'
19# AFTER defining them, and these are mutable objects (lists/dicts).
21INSTALLED_APPS.extend(
22 [
23 "django_extensions",
24 "django_openid_auth",
25 "bootstrap4",
26 "anymail",
27 "ivatar",
28 "ivatar.ivataraccount",
29 "ivatar.tools",
30 ]
31)
33MIDDLEWARE.extend(
34 [
35 "ivatar.middleware.CustomLocaleMiddleware",
36 ]
37)
39# Add OpenTelemetry middleware only if feature flag is enabled
40# Note: This will be checked at runtime, not at import time
41MIDDLEWARE.insert(
42 0,
43 "ivatar.middleware.MultipleProxyMiddleware",
44)
46AUTHENTICATION_BACKENDS = (
47 # Enable this to allow LDAP authentication.
48 # See INSTALL for more information.
49 # 'django_auth_ldap.backend.LDAPBackend',
50 "django_openid_auth.auth.OpenIDBackend",
51 "ivatar.ivataraccount.auth.FedoraOpenIdConnect",
52 "django.contrib.auth.backends.ModelBackend",
53)
55TEMPLATES[0]["DIRS"].extend(
56 [
57 os.path.join(BASE_DIR, "templates"),
58 ]
59)
60TEMPLATES[0]["OPTIONS"]["context_processors"].append(
61 "ivatar.context_processors.basepage",
62)
64OPENID_CREATE_USERS = True
65OPENID_UPDATE_DETAILS_FROM_SREG = True
66SOCIAL_AUTH_JSONFIELD_ENABLED = True
67# Fedora authentication (OIDC). You need to set these two values to use it.
68SOCIAL_AUTH_FEDORA_KEY = None # Also known as client_id
69SOCIAL_AUTH_FEDORA_SECRET = None # Also known as client_secret
71SITE_NAME = os.environ.get("SITE_NAME", "libravatar")
72IVATAR_VERSION = "2.0"
74SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2"
76SECURE_BASE_URL = os.environ.get(
77 "SECURE_BASE_URL", "https://avatars.linux-kernel.at/avatar/"
78)
79BASE_URL = os.environ.get("BASE_URL", "http://avatars.linux-kernel.at/avatar/")
81LOGIN_REDIRECT_URL = reverse_lazy("profile")
82MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
84MAX_NUM_PHOTOS = 5
85MAX_NUM_UNCONFIRMED_EMAILS = 5
86MAX_PHOTO_SIZE = 10485760 # in bytes
87MAX_PIXELS = 7000
88AVATAR_MAX_SIZE = 512
89JPEG_QUALITY = 85
91# Stats Optimization
92# Batch size for access count updates to the database
93STATS_BATCH_SIZE = 100
94# Enable async access count updates
95ASYNC_ACCESS_COUNT = True
97# Robohash Performance Optimization
98# Enable optimized robohash implementation for 6-22x performance improvement
99ROBOHASH_OPTIMIZATION_ENABLED = True
101# Robohash Configuration
102# Maximum number of robot parts to cache in memory (each ~50-200KB)
103ROBOHASH_CACHE_SIZE = 150 # ~10-30MB total cache size
105# Pagan Avatar Optimization
106# Maximum number of pagan Avatar objects to cache in memory (each ~100-500KB)
107PAGAN_CACHE_SIZE = 100 # ~10-50MB total cache size
109# I'm not 100% sure if single character domains are possible
110# under any tld... so MIN_LENGTH_EMAIL/_URL, might be +1
111MIN_LENGTH_URL = 11 # eg. http://a.io
112MAX_LENGTH_URL = 255 # MySQL can't handle more than that (LP: 1018682)
113MIN_LENGTH_EMAIL = 6 # eg. x@x.xx
114MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294
116BOOTSTRAP4 = {
117 "include_jquery": False,
118 "javascript_in_head": False,
119 "css_url": {
120 "href": "/static/css/bootstrap.min.css",
121 "integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB",
122 "crossorigin": "anonymous",
123 },
124 "javascript_url": {
125 "url": "/static/js/bootstrap.min.js",
126 "integrity": "",
127 "crossorigin": "anonymous",
128 },
129 "popper_url": {
130 "url": "/static/js/popper.min.js",
131 "integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49",
132 "crossorigin": "anonymous",
133 },
134}
136if "EMAIL_BACKEND" in os.environ:
137 EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover
138else:
139 if "test" in sys.argv or "collectstatic" in sys.argv:
140 EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend"
141 else:
142 try:
143 ANYMAIL = { # pragma: no cover
144 "MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"],
145 "MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"],
146 }
147 EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover
148 except Exception: # pragma: nocover # pylint: disable=broad-except
149 EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
151SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "ivatar@mg.linux-kernel.at")
152DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at")
154try:
155 from ivatar.settings import DATABASES
156except ImportError: # pragma: no cover
157 DATABASES = [] # pragma: no cover
159if "default" not in DATABASES:
160 DATABASES["default"] = { # pragma: no cover
161 "ENGINE": "django.db.backends.sqlite3",
162 "NAME": os.path.join(BASE_DIR, "db.sqlite3"),
163 }
165if "MYSQL_DATABASE" in os.environ:
166 DATABASES["default"] = { # pragma: no cover
167 "ENGINE": "django.db.backends.mysql",
168 "NAME": os.environ["MYSQL_DATABASE"],
169 "USER": os.environ["MYSQL_USER"],
170 "PASSWORD": os.environ["MYSQL_PASSWORD"],
171 "HOST": "mysql",
172 }
174if "POSTGRESQL_DATABASE" in os.environ:
175 DATABASES["default"] = { # pragma: no cover
176 "ENGINE": "django.db.backends.postgresql",
177 "NAME": os.environ["POSTGRESQL_DATABASE"],
178 "USER": os.environ["POSTGRESQL_USER"],
179 "PASSWORD": os.environ["POSTGRESQL_PASSWORD"],
180 "HOST": "postgresql",
181 }
183# CI/CD config has different naming
184if "POSTGRES_DB" in os.environ:
185 DATABASES["default"] = { # pragma: no cover
186 "ENGINE": "django.db.backends.postgresql",
187 "NAME": os.environ["POSTGRES_DB"],
188 "USER": os.environ["POSTGRES_USER"],
189 "PASSWORD": os.environ["POSTGRES_PASSWORD"],
190 "HOST": os.environ["POSTGRES_HOST"],
191 # Let Django use its default test database naming
192 # "TEST": {
193 # "NAME": os.environ["POSTGRES_DB"],
194 # },
195 }
197SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer"
199USE_X_FORWARDED_HOST = True
200ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [
201 "avatars.linux-kernel.at",
202 "localhost",
203]
205DEFAULT_AVATAR_SIZE = 80
207# Default settings for Gravatar proxy and redirect behavior
208# These can be overridden by URL parameters
209DEFAULT_GRAVATARPROXY = True
210DEFAULT_GRAVATARREDIRECT = False
211FORCEDEFAULT = False
213LANGUAGES = (
214 ("de", _("Deutsch")),
215 ("en", _("English")),
216 ("ca", _("Català")),
217 ("cs", _("Česky")),
218 ("es", _("Español")),
219 ("eu", _("Basque")),
220 ("fr", _("Français")),
221 ("it", _("Italiano")),
222 ("ja", _("日本語")),
223 ("nl", _("Nederlands")),
224 ("pt", _("Português")),
225 ("ru", _("Русский")),
226 ("sq", _("Shqip")),
227 ("tr", _("Türkçe")),
228 ("uk", _("Українська")),
229)
231MESSAGE_TAGS = {
232 message_constants.DEBUG: "debug",
233 message_constants.INFO: "info",
234 message_constants.SUCCESS: "success",
235 message_constants.WARNING: "warning",
236 message_constants.ERROR: "danger",
237}
239CACHES = {
240 "default": {
241 "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache",
242 "LOCATION": [
243 "127.0.0.1:11211",
244 ],
245 # "OPTIONS": {"MAX_ENTRIES": 1000000},
246 },
247 "filesystem": {
248 "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",
249 "LOCATION": "/var/tmp/ivatar_cache",
250 "TIMEOUT": 86400, # 24 hours
251 "OPTIONS": {"MAX_ENTRIES": 1000000},
252 },
253}
255CACHE_RESPONSE = True
257# Trusted URLs for default redirection
258TRUSTED_DEFAULT_URLS = [
259 {"schemes": ["https"], "host_equals": "ui-avatars.com", "path_prefix": "/api/"},
260 {
261 "schemes": ["http", "https"],
262 "host_equals": "gravatar.com",
263 "path_prefix": "/avatar/",
264 },
265 {
266 "schemes": ["http", "https"],
267 "host_suffix": ".gravatar.com",
268 "path_prefix": "/avatar/",
269 },
270 {
271 "schemes": ["http", "https"],
272 "host_equals": "www.gravatar.org",
273 "path_prefix": "/avatar/",
274 },
275 {
276 "schemes": ["https"],
277 "host_equals": "avatars.dicebear.com",
278 "path_prefix": "/api/",
279 },
280 {
281 "schemes": ["https"],
282 "host_equals": "api.dicebear.com",
283 "path_prefix": "/",
284 },
285 {
286 "schemes": ["https"],
287 "host_equals": "badges.fedoraproject.org",
288 "path_prefix": "/static/img/",
289 },
290 {
291 "schemes": ["http"],
292 "host_equals": "www.planet-libre.org",
293 "path_prefix": "/themes/planetlibre/images/",
294 },
295 {"schemes": ["https"], "host_equals": "www.azuracast.com", "path_prefix": "/img/"},
296 {
297 "schemes": ["https"],
298 "host_equals": "reps.mozilla.org",
299 "path_prefix": "/static/base/img/remo/",
300 },
301]
303URL_TIMEOUT = 10
306def map_legacy_config(trusted_url):
307 """
308 For backward compability with the legacy configuration
309 for trusting URLs. Adapts them to fit the new config.
310 """
311 if isinstance(trusted_url, str):
312 return {"url_prefix": trusted_url}
314 return trusted_url
317# Backward compability for legacy behavior
318TRUSTED_DEFAULT_URLS = list(map(map_legacy_config, TRUSTED_DEFAULT_URLS))
320# Bluesky settings
321BLUESKY_IDENTIFIER = os.environ.get("BLUESKY_IDENTIFIER", None)
322BLUESKY_APP_PASSWORD = os.environ.get("BLUESKY_APP_PASSWORD", None)
324# File upload security settings
325FILE_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB
326DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB
327FILE_UPLOAD_PERMISSIONS = 0o644
329# Enhanced file upload security
330ENABLE_FILE_SECURITY_VALIDATION = True
331ENABLE_EXIF_SANITIZATION = True
332ENABLE_MALICIOUS_CONTENT_SCAN = True
334# Avatar optimization settings
335PAGAN_CACHE_SIZE = 1000 # Number of pagan avatars to cache
337# Logging configuration - can be overridden in local config
338# Example: LOGS_DIR = "/var/log/ivatar" # For production deployments
340# This MUST BE THE LAST!
341if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")):
342 from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover