Coverage for config.py: 98%

85 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 11:51 +0000

1""" 

2Configuration overrides for settings.py 

3""" 

4 

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 

14 

15ADMIN_USERS = [] 

16ALLOWED_HOSTS = ["*"] 

17 

18# These are modified here because settings.py does 'from config import *' 

19# AFTER defining them, and these are mutable objects (lists/dicts). 

20 

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) 

32 

33MIDDLEWARE.extend( 

34 [ 

35 "ivatar.middleware.CustomLocaleMiddleware", 

36 ] 

37) 

38 

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) 

45 

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) 

54 

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) 

63 

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 

70 

71SITE_NAME = os.environ.get("SITE_NAME", "libravatar") 

72IVATAR_VERSION = "2.0" 

73 

74SCHEMAROOT = "https://www.libravatar.org/schemas/export/0.2" 

75 

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/") 

80 

81LOGIN_REDIRECT_URL = reverse_lazy("profile") 

82MAX_LENGTH_EMAIL = 254 # http://stackoverflow.com/questions/386294 

83 

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 

90 

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 

96 

97# Robohash Performance Optimization 

98# Enable optimized robohash implementation for 6-22x performance improvement 

99ROBOHASH_OPTIMIZATION_ENABLED = True 

100 

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 

104 

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 

108 

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 

115 

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} 

135 

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" 

150 

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") 

153 

154try: 

155 from ivatar.settings import DATABASES 

156except ImportError: # pragma: no cover 

157 DATABASES = [] # pragma: no cover 

158 

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 } 

164 

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 } 

173 

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 } 

182 

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 } 

196 

197SESSION_SERIALIZER = "django.contrib.sessions.serializers.JSONSerializer" 

198 

199USE_X_FORWARDED_HOST = True 

200ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ 

201 "avatars.linux-kernel.at", 

202 "localhost", 

203] 

204 

205DEFAULT_AVATAR_SIZE = 80 

206 

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 

212 

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) 

230 

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} 

238 

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} 

254 

255CACHE_RESPONSE = True 

256 

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] 

302 

303URL_TIMEOUT = 10 

304 

305 

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} 

313 

314 return trusted_url 

315 

316 

317# Backward compability for legacy behavior 

318TRUSTED_DEFAULT_URLS = list(map(map_legacy_config, TRUSTED_DEFAULT_URLS)) 

319 

320# Bluesky settings 

321BLUESKY_IDENTIFIER = os.environ.get("BLUESKY_IDENTIFIER", None) 

322BLUESKY_APP_PASSWORD = os.environ.get("BLUESKY_APP_PASSWORD", None) 

323 

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 

328 

329# Enhanced file upload security 

330ENABLE_FILE_SECURITY_VALIDATION = True 

331ENABLE_EXIF_SANITIZATION = True 

332ENABLE_MALICIOUS_CONTENT_SCAN = True 

333 

334# Avatar optimization settings 

335PAGAN_CACHE_SIZE = 1000 # Number of pagan avatars to cache 

336 

337# Logging configuration - can be overridden in local config 

338# Example: LOGS_DIR = "/var/log/ivatar" # For production deployments 

339 

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