Coverage for sm / settings.py: 83%

104 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-17 13:46 +0000

1import os 

2import dj_database_url 

3from pathlib import Path 

4from sys import platform, argv 

5import django.contrib.messages as messages 

6from decouple import config, Csv 

7 

8# Build paths inside the project like this: BASE_DIR / 'subdir'. 

9BASE_DIR = Path(__file__).resolve().parent.parent 

10 

11# Environment-based configuration 

12SECRET_KEY = config("SECRET_KEY", default="django-insecure-default-key-for-dev") 

13DEBUG = config("DEBUG", default=False, cast=bool) 

14ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=Csv()) 

15EMAIL_BACKEND = config( 

16 "EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" 

17) 

18 

19THEME_CONTACT_EMAIL = config("THEME_CONTACT_EMAIL", default="oliver@linux-kernel.at") 

20THEME_GITHUB_URL = config("THEME_GITHUB_URL", default="https://github.com/ofalk/sm") 

21 

22# Version information 

23APP_VERSION = config("APP_VERSION", default="unknown") 

24APP_MODIFICATION_DATE = config("APP_MODIFICATION_DATE", default="unknown") 

25 

26if APP_VERSION == "unknown" or APP_MODIFICATION_DATE == "unknown": 

27 try: 

28 import subprocess 

29 

30 if APP_VERSION == "unknown": 

31 APP_VERSION = ( 

32 subprocess.check_output( 

33 ["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL 

34 ) 

35 .decode("utf-8") 

36 .strip() 

37 ) 

38 if APP_MODIFICATION_DATE == "unknown": 

39 APP_MODIFICATION_DATE = ( 

40 subprocess.check_output( 

41 ["git", "log", "-1", "--format=%cd"], stderr=subprocess.DEVNULL 

42 ) 

43 .decode("utf-8") 

44 .strip() 

45 ) 

46 except Exception: 

47 pass 

48 

49DISABLE_SOCIAL_AUTH = config("DISABLE_SOCIAL_AUTH", default=False, cast=bool) 

50 

51# Analytics settings 

52ANALYTICS_ENABLED = config("ANALYTICS_ENABLED", default=False, cast=bool) 

53ANALYTICS_ID = config("ANALYTICS_ID", default=None) 

54ANALYTICS_BASE_URL = config("ANALYTICS_BASE_URL", default="https://api.swetrix.com") 

55 

56# Application definition 

57 

58INSTALLED_APPS = [ 

59 "whitenoise.runserver_nostatic", 

60 "django.contrib.admin", 

61 "django.contrib.auth", 

62 "django.contrib.contenttypes", 

63 "django.contrib.sessions", 

64 "django.contrib.messages", 

65 "django.contrib.staticfiles", 

66 "django.contrib.sites", 

67 "django.contrib.admindocs", 

68 "rest_framework", 

69 "bootstrap4", 

70 "django_countries", 

71 "taggit", 

72 "simple_history", 

73 "drf_spectacular", 

74 # Allauth 

75 "allauth", 

76 "allauth.account", 

77 "allauth.socialaccount", 

78 "allauth.mfa", 

79 "social_django", 

80] 

81 

82if DEBUG: 

83 INSTALLED_APPS += ["debug_toolbar"] 

84 

85if not DISABLE_SOCIAL_AUTH: 

86 INSTALLED_APPS += [ 

87 "allauth.socialaccount.providers.google", 

88 "allauth.socialaccount.providers.openid_connect", 

89 ] 

90 SOCIALACCOUNT_ENABLED = True 

91else: 

92 SOCIALACCOUNT_ENABLED = False 

93 

94INSTALLED_APPS += [ 

95 # Project Apps 

96 "cluster", 

97 "operatingsystem", 

98 "clusterpackage", 

99 "patchtime", 

100 "location", 

101 "servermodel", 

102 "server", 

103 "status", 

104 "domain", 

105 "clustersoftware", 

106 "clusterpackagetype", 

107 "vendor", 

108 "sm.apps.SmConfig", 

109] 

110 

111MIDDLEWARE = [ 

112 "django.middleware.security.SecurityMiddleware", 

113 "whitenoise.middleware.WhiteNoiseMiddleware", 

114 "django.contrib.sessions.middleware.SessionMiddleware", 

115 "django.middleware.common.CommonMiddleware", 

116 "django.middleware.csrf.CsrfViewMiddleware", 

117 "django.contrib.auth.middleware.AuthenticationMiddleware", 

118 "django.contrib.messages.middleware.MessageMiddleware", 

119 "django.middleware.clickjacking.XFrameOptionsMiddleware", 

120 "htmlmin.middleware.HtmlMinifyMiddleware", 

121 "htmlmin.middleware.MarkRequestMiddleware", 

122 "django.contrib.sites.middleware.CurrentSiteMiddleware", 

123 "simple_history.middleware.HistoryRequestMiddleware", 

124 # Allauth middleware 

125 "allauth.account.middleware.AccountMiddleware", 

126] 

127 

128if DEBUG: 

129 # Insert DebugToolbarMiddleware after SessionMiddleware but before CommonMiddleware 

130 try: 

131 session_idx = MIDDLEWARE.index( 

132 "django.contrib.sessions.middleware.SessionMiddleware" 

133 ) 

134 MIDDLEWARE.insert( 

135 session_idx + 1, "debug_toolbar.middleware.DebugToolbarMiddleware" 

136 ) 

137 except ValueError: 

138 MIDDLEWARE.append("debug_toolbar.middleware.DebugToolbarMiddleware") 

139 

140ROOT_URLCONF = "sm.urls" 

141 

142TEMPLATES = [ 

143 { 

144 "BACKEND": "django.template.backends.django.DjangoTemplates", 

145 "DIRS": [BASE_DIR / "sm" / "templates"], 

146 "OPTIONS": { 

147 "context_processors": [ 

148 "django.template.context_processors.debug", 

149 "django.template.context_processors.request", 

150 "django.contrib.auth.context_processors.auth", 

151 "django.contrib.messages.context_processors.messages", 

152 "django.template.context_processors.i18n", 

153 "django.template.context_processors.media", 

154 "django.template.context_processors.static", 

155 "django.template.context_processors.tz", 

156 "sm.context_processors.theme_settings", 

157 ], 

158 "loaders": [ 

159 "django.template.loaders.filesystem.Loader", 

160 "sm.template.loaders.app_directories_enhanced.Loader", 

161 "django.template.loaders.app_directories.Loader", 

162 ], 

163 }, 

164 }, 

165] 

166 

167WSGI_APPLICATION = "sm.wsgi.application" 

168 

169# Database 

170# https://docs.djangoproject.com/en/stable/ref/settings/#databases 

171 

172DATABASE_URL = config("DATABASE_URL", default=None) 

173 

174if DATABASE_URL: 

175 DATABASES = { 

176 "default": dj_database_url.config(default=DATABASE_URL, conn_max_age=600) 

177 } 

178elif "test" in argv: 

179 DATABASES = { 

180 "default": { 

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

182 "NAME": ":memory:", 

183 } 

184 } 

185elif platform == "darwin": 

186 DATABASES = { 

187 "default": { 

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

189 "NAME": BASE_DIR / "db.sqlite3", 

190 } 

191 } 

192else: 

193 # Use config_local or environment variables for MySQL if needed 

194 DATABASES = { 

195 "default": { 

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

197 "NAME": BASE_DIR / "db.sqlite3", 

198 } 

199 } 

200 

201# Password validation 

202# https://docs.djangoproject.com/en/stable/ref/settings/#auth-password-validators 

203 

204AUTH_PASSWORD_VALIDATORS = [ 

205 { 

206 "NAME": ( 

207 "django.contrib.auth.password_validation." 

208 "UserAttributeSimilarityValidator" 

209 ) 

210 }, 

211 {"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"}, 

212 {"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"}, 

213 {"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"}, 

214] 

215 

216# Internationalization 

217# https://docs.djangoproject.com/en/stable/topics/i18n/ 

218 

219LANGUAGE_CODE = "en-us" 

220TIME_ZONE = "UTC" 

221USE_I18N = True 

222USE_TZ = True 

223 

224# Static files (CSS, JavaScript, Images) 

225# https://docs.djangoproject.com/en/stable/howto/static-files/ 

226 

227STATIC_URL = "/static/" 

228STATIC_ROOT = BASE_DIR / "static" 

229STATICFILES_DIRS = [ 

230 BASE_DIR / "sm" / "static", 

231] 

232 

233STORAGES = { 

234 "default": { 

235 "BACKEND": "django.core.files.storage.FileSystemStorage", 

236 }, 

237 "staticfiles": { 

238 "BACKEND": "whitenoise.storage.CompressedStaticFilesStorage", 

239 }, 

240} 

241 

242# Default primary key field type 

243# https://docs.djangoproject.com/en/stable/ref/settings/#default-auto-field 

244 

245DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" 

246 

247# Extra settings 

248 

249SITE_ID = 1 

250 

251AUTHENTICATION_BACKENDS = [ 

252 # Needed to login by username in Django admin, regardless of `allauth` 

253 "django.contrib.auth.backends.ModelBackend", 

254 # `allauth` specific authentication methods, such as login by e-mail 

255 "allauth.account.auth_backends.AuthenticationBackend", 

256] 

257 

258# Allauth settings 

259LOGIN_REDIRECT_URL = "/" 

260ACCOUNT_LOGIN_METHODS = {"username", "email"} 

261ACCOUNT_SIGNUP_FIELDS = ["email*", "username*", "password1*", "password2*"] 

262ACCOUNT_EMAIL_VERIFICATION = "optional" 

263ACCOUNT_EMAIL_CONFIRMATION_ANONYMOUS_REDIRECT_URL = "/" 

264ACCOUNT_EMAIL_CONFIRMATION_AUTHENTICATED_REDIRECT_URL = "/" 

265ACCOUNT_LOGIN_REDIRECT_URL = "/" 

266ACCOUNT_LOGOUT_REDIRECT_URL = "/" 

267 

268# Social Auth (Legacy/Transition) 

269SOCIAL_AUTH_STRATEGY = "social_django.strategy.DjangoStrategy" 

270SOCIAL_AUTH_STORAGE = "social_django.models.DjangoStorage" 

271 

272# Disable MFA features that might cause issues if allauth.mfa is not fully configured 

273MFA_PASSKEY_LOGIN_ENABLED = False 

274MFA_SUPPORTED_TYPES = ["totp", "recovery_codes"] 

275 

276if not DISABLE_SOCIAL_AUTH: 

277 SOCIALACCOUNT_AUTO_SIGNUP = True 

278 SOCIALACCOUNT_ADAPTER = "sm.adapter.MySocialAccountAdapter" 

279 

280 SOCIALACCOUNT_PROVIDERS = {} 

281 

282 # Google Auth 

283 google_id = config("GOOGLE_CLIENT_ID", default=None) 

284 google_secret = config("GOOGLE_SECRET", default=None) 

285 if google_id and google_secret: 

286 SOCIALACCOUNT_PROVIDERS["google"] = { 

287 "APPS": [ 

288 { 

289 "client_id": google_id, 

290 "secret": google_secret, 

291 "settings": { 

292 "scope": ["profile", "email"], 

293 "auth_params": {"access_type": "online"}, 

294 }, 

295 } 

296 ] 

297 } 

298 

299 # Generic OIDC 

300 oidc_id = config("OIDC_CLIENT_ID", default=None) 

301 oidc_secret = config("OIDC_SECRET", default=None) 

302 oidc_url = config("OIDC_URL", default=None) 

303 oidc_name = config("OIDC_NAME", default="Localghost SSO") 

304 

305 if oidc_id and oidc_secret and oidc_url: 

306 SOCIALACCOUNT_PROVIDERS["openid_connect"] = { 

307 "APPS": [ 

308 { 

309 "provider_id": "oidc", 

310 "name": oidc_name, 

311 "client_id": oidc_id, 

312 "secret": oidc_secret, 

313 "settings": {"server_url": oidc_url}, 

314 } 

315 ] 

316 } 

317 

318INTERNAL_IPS = [ 

319 "127.0.0.1", 

320] 

321 

322MESSAGE_TAGS = {messages.ERROR: "danger"} 

323 

324MESSAGE_STORAGE = "django.contrib.messages.storage.fallback.FallbackStorage" 

325 

326TAGGIT_CASE_INSENSITIVE = True 

327 

328# REST Framework Settings 

329REST_FRAMEWORK = { 

330 "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", 

331} 

332 

333SPECTACULAR_SETTINGS = { 

334 "TITLE": "ServerManager API", 

335 "DESCRIPTION": "API for managing servers, clusters, and infrastructure.", 

336 "VERSION": "1.0.0", 

337 "SERVE_INCLUDE_SCHEMA": False, 

338} 

339 

340# Bootstrap 4 settings 

341BOOTSTRAP4 = { 

342 "error_css_class": "bootstrap4-error", 

343 "required_css_class": "bootstrap4-required", 

344 "javascript_in_head": True, 

345 "success_css_class": "bootstrap4-bound", 

346} 

347 

348HTML_MINIFY = True 

349 

350if not DISABLE_SOCIAL_AUTH and os.path.isfile(BASE_DIR / "config_local.py"): 

351 from config_local import * # noqa 

352 

353# Redefine to ensure no social_core backends survive in quick test mode 

354if DISABLE_SOCIAL_AUTH: 

355 AUTHENTICATION_BACKENDS = [ 

356 "django.contrib.auth.backends.ModelBackend", 

357 "allauth.account.auth_backends.AuthenticationBackend", 

358 ]