Coverage for config.py: 98%

85 statements  

« prev     ^ index     » next       coverage.py v7.14.2, created at 2026-06-21 23:07 +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 

11 

12from ivatar.settings import MIDDLEWARE 

13from ivatar.settings import INSTALLED_APPS 

14from ivatar.settings import TEMPLATES 

15 

16ADMIN_USERS = [] 

17ALLOWED_HOSTS = ["*"] 

18 

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) 

30 

31MIDDLEWARE.extend( 

32 [ 

33 "ivatar.middleware.CustomLocaleMiddleware", 

34 ] 

35) 

36 

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) 

43 

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) 

52 

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) 

61 

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 

68 

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

70IVATAR_VERSION = "2.0" 

71 

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

73 

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

78 

79LOGIN_REDIRECT_URL = reverse_lazy("profile") 

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

81 

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 

88 

89# Stats Optimization 

90# Batch size for access count updates to the database 

91STATS_BATCH_SIZE = 100 

92# Enable async access count updates 

93ASYNC_ACCESS_COUNT = True 

94 

95# Robohash Performance Optimization 

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

97ROBOHASH_OPTIMIZATION_ENABLED = True 

98 

99# Robohash Configuration 

100# Maximum number of robot parts to cache in memory (each ~50-200KB) 

101ROBOHASH_CACHE_SIZE = 150 # ~10-30MB total cache size 

102 

103# Pagan Avatar Optimization 

104# Maximum number of pagan Avatar objects to cache in memory (each ~100-500KB) 

105PAGAN_CACHE_SIZE = 100 # ~10-50MB total cache size 

106 

107# I'm not 100% sure if single character domains are possible 

108# under any tld... so MIN_LENGTH_EMAIL/_URL, might be +1 

109MIN_LENGTH_URL = 11 # eg. http://a.io 

110MAX_LENGTH_URL = 255 # MySQL can't handle more than that (LP: 1018682) 

111MIN_LENGTH_EMAIL = 6 # eg. x@x.xx 

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

113 

114BOOTSTRAP4 = { 

115 "include_jquery": False, 

116 "javascript_in_head": False, 

117 "css_url": { 

118 "href": "/static/css/bootstrap.min.css", 

119 "integrity": "sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB", 

120 "crossorigin": "anonymous", 

121 }, 

122 "javascript_url": { 

123 "url": "/static/js/bootstrap.min.js", 

124 "integrity": "", 

125 "crossorigin": "anonymous", 

126 }, 

127 "popper_url": { 

128 "url": "/static/js/popper.min.js", 

129 "integrity": "sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49", 

130 "crossorigin": "anonymous", 

131 }, 

132} 

133 

134if "EMAIL_BACKEND" in os.environ: 

135 EMAIL_BACKEND = os.environ["EMAIL_BACKEND"] # pragma: no cover 

136else: 

137 if "test" in sys.argv or "collectstatic" in sys.argv: 

138 EMAIL_BACKEND = "django.core.mail.backends.locmem.EmailBackend" 

139 else: 

140 try: 

141 ANYMAIL = { # pragma: no cover 

142 "MAILGUN_API_KEY": os.environ["IVATAR_MAILGUN_API_KEY"], 

143 "MAILGUN_SENDER_DOMAIN": os.environ["IVATAR_MAILGUN_SENDER_DOMAIN"], 

144 } 

145 EMAIL_BACKEND = "anymail.backends.mailgun.EmailBackend" # pragma: no cover 

146 except Exception: # pragma: nocover # pylint: disable=broad-except 

147 EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" 

148 

149SERVER_EMAIL = os.environ.get("SERVER_EMAIL", "ivatar@mg.linux-kernel.at") 

150DEFAULT_FROM_EMAIL = os.environ.get("DEFAULT_FROM_EMAIL", "ivatar@mg.linux-kernel.at") 

151 

152try: 

153 from ivatar.settings import DATABASES 

154except ImportError: # pragma: no cover 

155 DATABASES = [] # pragma: no cover 

156 

157if "default" not in DATABASES: 

158 DATABASES["default"] = { # pragma: no cover 

159 "ENGINE": "django.db.backends.sqlite3", 

160 "NAME": os.path.join(BASE_DIR, "db.sqlite3"), 

161 } 

162 

163if "MYSQL_DATABASE" in os.environ: 

164 DATABASES["default"] = { # pragma: no cover 

165 "ENGINE": "django.db.backends.mysql", 

166 "NAME": os.environ["MYSQL_DATABASE"], 

167 "USER": os.environ["MYSQL_USER"], 

168 "PASSWORD": os.environ["MYSQL_PASSWORD"], 

169 "HOST": "mysql", 

170 } 

171 

172if "POSTGRESQL_DATABASE" in os.environ: 

173 DATABASES["default"] = { # pragma: no cover 

174 "ENGINE": "django.db.backends.postgresql", 

175 "NAME": os.environ["POSTGRESQL_DATABASE"], 

176 "USER": os.environ["POSTGRESQL_USER"], 

177 "PASSWORD": os.environ["POSTGRESQL_PASSWORD"], 

178 "HOST": "postgresql", 

179 } 

180 

181# CI/CD config has different naming 

182if "POSTGRES_DB" in os.environ: 

183 DATABASES["default"] = { # pragma: no cover 

184 "ENGINE": "django.db.backends.postgresql", 

185 "NAME": os.environ["POSTGRES_DB"], 

186 "USER": os.environ["POSTGRES_USER"], 

187 "PASSWORD": os.environ["POSTGRES_PASSWORD"], 

188 "HOST": os.environ["POSTGRES_HOST"], 

189 # Let Django use its default test database naming 

190 # "TEST": { 

191 # "NAME": os.environ["POSTGRES_DB"], 

192 # }, 

193 } 

194 

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

196 

197USE_X_FORWARDED_HOST = True 

198ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ 

199 "avatars.linux-kernel.at", 

200 "localhost", 

201] 

202 

203DEFAULT_AVATAR_SIZE = 80 

204 

205# Default settings for Gravatar proxy and redirect behavior 

206# These can be overridden by URL parameters 

207DEFAULT_GRAVATARPROXY = True 

208DEFAULT_GRAVATARREDIRECT = False 

209FORCEDEFAULT = False 

210 

211LANGUAGES = ( 

212 ("de", _("Deutsch")), 

213 ("en", _("English")), 

214 ("ca", _("Català")), 

215 ("cs", _("Česky")), 

216 ("es", _("Español")), 

217 ("eu", _("Basque")), 

218 ("fr", _("Français")), 

219 ("it", _("Italiano")), 

220 ("ja", _("日本語")), 

221 ("nl", _("Nederlands")), 

222 ("pt", _("Português")), 

223 ("ru", _("Русский")), 

224 ("sq", _("Shqip")), 

225 ("tr", _("Türkçe")), 

226 ("uk", _("Українська")), 

227) 

228 

229MESSAGE_TAGS = { 

230 message_constants.DEBUG: "debug", 

231 message_constants.INFO: "info", 

232 message_constants.SUCCESS: "success", 

233 message_constants.WARNING: "warning", 

234 message_constants.ERROR: "danger", 

235} 

236 

237CACHES = { 

238 "default": { 

239 "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", 

240 "LOCATION": [ 

241 "127.0.0.1:11211", 

242 ], 

243 # "OPTIONS": {"MAX_ENTRIES": 1000000}, 

244 }, 

245 "filesystem": { 

246 "BACKEND": "django.core.cache.backends.filebased.FileBasedCache", 

247 "LOCATION": "/var/tmp/ivatar_cache", 

248 "TIMEOUT": 900, # 15 minutes 

249 "OPTIONS": {"MAX_ENTRIES": 1000000}, 

250 }, 

251} 

252 

253CACHE_RESPONSE = True 

254 

255# Trusted URLs for default redirection 

256TRUSTED_DEFAULT_URLS = [ 

257 {"schemes": ["https"], "host_equals": "ui-avatars.com", "path_prefix": "/api/"}, 

258 { 

259 "schemes": ["http", "https"], 

260 "host_equals": "gravatar.com", 

261 "path_prefix": "/avatar/", 

262 }, 

263 { 

264 "schemes": ["http", "https"], 

265 "host_suffix": ".gravatar.com", 

266 "path_prefix": "/avatar/", 

267 }, 

268 { 

269 "schemes": ["http", "https"], 

270 "host_equals": "www.gravatar.org", 

271 "path_prefix": "/avatar/", 

272 }, 

273 { 

274 "schemes": ["https"], 

275 "host_equals": "avatars.dicebear.com", 

276 "path_prefix": "/api/", 

277 }, 

278 { 

279 "schemes": ["https"], 

280 "host_equals": "api.dicebear.com", 

281 "path_prefix": "/", 

282 }, 

283 { 

284 "schemes": ["https"], 

285 "host_equals": "badges.fedoraproject.org", 

286 "path_prefix": "/static/img/", 

287 }, 

288 { 

289 "schemes": ["http"], 

290 "host_equals": "www.planet-libre.org", 

291 "path_prefix": "/themes/planetlibre/images/", 

292 }, 

293 {"schemes": ["https"], "host_equals": "www.azuracast.com", "path_prefix": "/img/"}, 

294 { 

295 "schemes": ["https"], 

296 "host_equals": "reps.mozilla.org", 

297 "path_prefix": "/static/base/img/remo/", 

298 }, 

299] 

300 

301URL_TIMEOUT = 10 

302 

303 

304def map_legacy_config(trusted_url): 

305 """ 

306 For backward compability with the legacy configuration 

307 for trusting URLs. Adapts them to fit the new config. 

308 """ 

309 if isinstance(trusted_url, str): 

310 return {"url_prefix": trusted_url} 

311 

312 return trusted_url 

313 

314 

315# Backward compability for legacy behavior 

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

317 

318# Bluesky settings 

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

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

321 

322# File upload security settings 

323FILE_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB 

324DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB 

325FILE_UPLOAD_PERMISSIONS = 0o644 

326 

327# Enhanced file upload security 

328ENABLE_FILE_SECURITY_VALIDATION = True 

329ENABLE_EXIF_SANITIZATION = True 

330ENABLE_MALICIOUS_CONTENT_SCAN = True 

331 

332# Avatar optimization settings 

333PAGAN_CACHE_SIZE = 1000 # Number of pagan avatars to cache 

334 

335# Logging configuration - can be overridden in local config 

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

337 

338# This MUST BE THE LAST! 

339if os.path.isfile(os.path.join(BASE_DIR, "config_local.py")): 

340 from config_local import * # noqa # flake8: noqa # NOQA # pragma: no cover