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