Coverage for config.py: 97%

78 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-24 23:06 +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 = "1.8.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# Robohash Performance Optimization 

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

91ROBOHASH_OPTIMIZATION_ENABLED = True 

92 

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

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

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

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

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

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

99 

100BOOTSTRAP4 = { 

101 "include_jquery": False, 

102 "javascript_in_head": False, 

103 "css_url": { 

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

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

106 "crossorigin": "anonymous", 

107 }, 

108 "javascript_url": { 

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

110 "integrity": "", 

111 "crossorigin": "anonymous", 

112 }, 

113 "popper_url": { 

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

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

116 "crossorigin": "anonymous", 

117 }, 

118} 

119 

120if "EMAIL_BACKEND" in os.environ: 

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

122else: 

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

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

125 else: 

126 try: 

127 ANYMAIL = { # pragma: no cover 

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

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

130 } 

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

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

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

134 

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

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

137 

138try: 

139 from ivatar.settings import DATABASES 

140except ImportError: # pragma: no cover 

141 DATABASES = [] # pragma: no cover 

142 

143if "default" not in DATABASES: 

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

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

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

147 } 

148 

149if "MYSQL_DATABASE" in os.environ: 

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

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

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

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

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

155 "HOST": "mysql", 

156 } 

157 

158if "POSTGRESQL_DATABASE" in os.environ: 

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

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

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

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

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

164 "HOST": "postgresql", 

165 } 

166 

167# CI/CD config has different naming 

168if "POSTGRES_DB" in os.environ: 

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

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

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

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

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

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

175 # Let Django use its default test database naming 

176 # "TEST": { 

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

178 # }, 

179 } 

180 

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

182 

183USE_X_FORWARDED_HOST = True 

184ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = [ 

185 "avatars.linux-kernel.at", 

186 "localhost", 

187] 

188 

189DEFAULT_AVATAR_SIZE = 80 

190 

191LANGUAGES = ( 

192 ("de", _("Deutsch")), 

193 ("en", _("English")), 

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

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

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

197 ("eu", _("Basque")), 

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

199 ("it", _("Italiano")), 

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

201 ("nl", _("Nederlands")), 

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

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

204 ("sq", _("Shqip")), 

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

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

207) 

208 

209MESSAGE_TAGS = { 

210 message_constants.DEBUG: "debug", 

211 message_constants.INFO: "info", 

212 message_constants.SUCCESS: "success", 

213 message_constants.WARNING: "warning", 

214 message_constants.ERROR: "danger", 

215} 

216 

217CACHES = { 

218 "default": { 

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

220 "LOCATION": [ 

221 "127.0.0.1:11211", 

222 ], 

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

224 }, 

225 "filesystem": { 

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

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

228 "TIMEOUT": 900, # 15 minutes 

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

230 }, 

231} 

232 

233# This is 5 minutes caching for generated/resized images, 

234# so the sites don't hit ivatar so much - it's what's set in the HTTP header 

235CACHE_IMAGES_MAX_AGE = 5 * 60 

236 

237CACHE_RESPONSE = True 

238 

239# Trusted URLs for default redirection 

240TRUSTED_DEFAULT_URLS = [ 

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

242 { 

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

244 "host_equals": "gravatar.com", 

245 "path_prefix": "/avatar/", 

246 }, 

247 { 

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

249 "host_suffix": ".gravatar.com", 

250 "path_prefix": "/avatar/", 

251 }, 

252 { 

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

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

255 "path_prefix": "/avatar/", 

256 }, 

257 { 

258 "schemes": ["https"], 

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

260 "path_prefix": "/api/", 

261 }, 

262 { 

263 "schemes": ["https"], 

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

265 "path_prefix": "/", 

266 }, 

267 { 

268 "schemes": ["https"], 

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

270 "path_prefix": "/static/img/", 

271 }, 

272 { 

273 "schemes": ["http"], 

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

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

276 }, 

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

278 { 

279 "schemes": ["https"], 

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

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

282 }, 

283] 

284 

285URL_TIMEOUT = 10 

286 

287 

288def map_legacy_config(trusted_url): 

289 """ 

290 For backward compability with the legacy configuration 

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

292 """ 

293 if isinstance(trusted_url, str): 

294 return {"url_prefix": trusted_url} 

295 

296 return trusted_url 

297 

298 

299# Backward compability for legacy behavior 

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

301 

302# Bluesky settings 

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

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

305 

306# File upload security settings 

307FILE_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB 

308DATA_UPLOAD_MAX_MEMORY_SIZE = 5 * 1024 * 1024 # 5MB 

309FILE_UPLOAD_PERMISSIONS = 0o644 

310 

311# Enhanced file upload security 

312ENABLE_FILE_SECURITY_VALIDATION = True 

313ENABLE_EXIF_SANITIZATION = True 

314ENABLE_MALICIOUS_CONTENT_SCAN = True 

315 

316# Logging configuration - can be overridden in local config 

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

318 

319# This MUST BE THE LAST! 

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

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