Coverage for config/settings/base.py: 99%

162 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-10-06 13:07 +0000

1"""Base settings shared by every environment. 

2 

3Everything a deployment is expected to change is read from the environment; 

4see ``.env.example`` and the configuration reference in the docs. 

5""" 

6 

7from pathlib import Path 

8 

9import environ 

10from celery.schedules import crontab 

11 

12from baibu.core.branding import build_languages 

13from baibu.core.branding import register_extra_languages 

14from baibu.core.storage_config import S3Options 

15from baibu.core.storage_config import build_storages 

16from baibu.users.sign_in_config import login_methods 

17from baibu.users.sign_in_config import signup_fields 

18 

19BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent 

20APPS_DIR = BASE_DIR / "baibu" 

21env = environ.Env() 

22 

23if env.bool("DJANGO_READ_DOT_ENV_FILE", default=False): 

24 # OS environment variables take precedence over variables from .env 

25 env.read_env(str(BASE_DIR / ".env")) 

26 

27# GENERAL 

28# ------------------------------------------------------------------------------ 

29DEBUG = env.bool("DJANGO_DEBUG", default=False) 

30TIME_ZONE = env("DJANGO_TIME_ZONE", default="UTC") 

31USE_I18N = True 

32USE_TZ = True 

33SITE_ID = 1 

34 

35# WHITE-LABEL 

36# ------------------------------------------------------------------------------ 

37# A deployment sets its own name, contact address and languages, and may 

38# override any template, translation or static file by placing it under 

39# DEPLOYMENT_DIR (templates/, locale/, static/). See deployment/README.md. 

40PLATFORM_NAME = env("PLATFORM_NAME", default="Community Language Platform") 

41PLATFORM_TAGLINE = env( 

42 "PLATFORM_TAGLINE", 

43 default="Share your language, with consent, and help build tools that speak it.", 

44) 

45PLATFORM_CONTACT_EMAIL = env("PLATFORM_CONTACT_EMAIL", default="") 

46# Public host name, used in emails (for example links to confirm an address). 

47PLATFORM_DOMAIN = env("PLATFORM_DOMAIN", default="localhost:8000") 

48PLATFORM_LOGO_PATH = env("PLATFORM_LOGO_PATH", default="images/logo.svg") 

49PLATFORM_BRAND_COLOR = env("PLATFORM_BRAND_COLOR", default="") 

50DEPLOYMENT_DIR = Path(env("DEPLOYMENT_DIR", default=str(BASE_DIR / "deployment"))) 

51 

52# Language codes Django already knows (for example "en,fr,sw") need no more 

53# than PLATFORM_LANGUAGES. Languages Django does not ship, such as Javanese, 

54# are declared in PLATFORM_EXTRA_LANGUAGES as "code:English name:Local name" 

55# entries separated by semicolons. 

56PLATFORM_EXTRA_LANGUAGES = env("PLATFORM_EXTRA_LANGUAGES", default="") 

57register_extra_languages(PLATFORM_EXTRA_LANGUAGES) 

58LANGUAGE_CODE = env("PLATFORM_DEFAULT_LANGUAGE", default="en") 

59LANGUAGES = build_languages(env.list("PLATFORM_LANGUAGES", default=["en"]), default=LANGUAGE_CODE) 

60# English is the source language, so the project ships no catalogues. A 

61# deployment's translations come from two places: catalogues its translators 

62# publish from the /translations/ pages (copied by each process into 

63# LOCALIZATION_PUBLISHED_DIR), and .po/.mo files in DEPLOYMENT_DIR/locale. 

64# The first entry wins where both translate the same message. 

65LOCALIZATION_PUBLISHED_DIR = Path(env("LOCALIZATION_PUBLISHED_DIR", default="") or BASE_DIR / "var" / "locale") 

66LOCALE_PATHS = [str(LOCALIZATION_PUBLISHED_DIR), str(DEPLOYMENT_DIR / "locale")] 

67 

68# DATABASES 

69# ------------------------------------------------------------------------------ 

70DATABASES = {"default": env.db("DATABASE_URL")} 

71DATABASES["default"]["ATOMIC_REQUESTS"] = True 

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

73 

74# URLS 

75# ------------------------------------------------------------------------------ 

76ROOT_URLCONF = "config.urls" 

77WSGI_APPLICATION = "config.wsgi.application" 

78 

79# APPS 

80# ------------------------------------------------------------------------------ 

81DJANGO_APPS = [ 

82 "django.contrib.auth", 

83 "django.contrib.contenttypes", 

84 "django.contrib.sessions", 

85 "django.contrib.sites", 

86 "django.contrib.messages", 

87 "django.contrib.staticfiles", 

88 "django.contrib.admin", 

89 "django.forms", 

90] 

91THIRD_PARTY_APPS = [ 

92 "allauth", 

93 "allauth.account", 

94 "allauth.mfa", 

95] 

96LOCAL_APPS = [ 

97 "baibu.core", 

98 "baibu.users", 

99 "baibu.submissions", 

100 "baibu.chat", 

101 "baibu.notifications", 

102 "baibu.staff", 

103 "baibu.localization", 

104 "baibu.metrics", 

105 "baibu.theme", 

106] 

107INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS 

108 

109# AUTHENTICATION 

110# ------------------------------------------------------------------------------ 

111AUTHENTICATION_BACKENDS = [ 

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

113 "allauth.account.auth_backends.AuthenticationBackend", 

114] 

115AUTH_USER_MODEL = "users.User" 

116LOGIN_REDIRECT_URL = "users:account" 

117LOGIN_URL = "account_login" 

118 

119PASSWORD_HASHERS = [ 

120 "django.contrib.auth.hashers.Argon2PasswordHasher", 

121 "django.contrib.auth.hashers.PBKDF2PasswordHasher", 

122 "django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher", 

123 "django.contrib.auth.hashers.BCryptSHA256PasswordHasher", 

124] 

125AUTH_PASSWORD_VALIDATORS = [ 

126 {"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"}, 

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

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

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

130] 

131 

132# MIDDLEWARE 

133# ------------------------------------------------------------------------------ 

134MIDDLEWARE = [ 

135 "django.middleware.security.SecurityMiddleware", 

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

137 # Loads newly published translations; must come before LocaleMiddleware. 

138 "baibu.localization.middleware.TranslationSyncMiddleware", 

139 "django.middleware.locale.LocaleMiddleware", 

140 "django.middleware.common.CommonMiddleware", 

141 "django.middleware.csrf.CsrfViewMiddleware", 

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

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

144 "django.middleware.clickjacking.XFrameOptionsMiddleware", 

145 "allauth.account.middleware.AccountMiddleware", 

146 "baibu.users.middleware.ProfileCompletionMiddleware", 

147] 

148 

149# STATIC AND MEDIA 

150# ------------------------------------------------------------------------------ 

151STATIC_ROOT = str(BASE_DIR / "staticfiles") 

152STATIC_URL = "/static/" 

153STATICFILES_DIRS = [str(DEPLOYMENT_DIR / "static"), str(APPS_DIR / "static")] 

154# The development image keeps a prebuilt stylesheet here so a fresh checkout is 

155# styled before the tailwind service has run; a local build takes precedence. 

156if BUILT_STATIC_DIR := env("BUILT_STATIC_DIR", default=""): 

157 STATICFILES_DIRS.append(BUILT_STATIC_DIR) 

158STATICFILES_FINDERS = [ 

159 "django.contrib.staticfiles.finders.FileSystemFinder", 

160 "django.contrib.staticfiles.finders.AppDirectoriesFinder", 

161] 

162MEDIA_ROOT = env("DJANGO_MEDIA_ROOT", default=str(BASE_DIR / "var" / "media")) 

163MEDIA_URL = "/media/" 

164 

165# FILE STORAGE 

166# ------------------------------------------------------------------------------ 

167# "local" keeps files on disk; "s3" uses any S3-compatible object store. Code 

168# stores contributor files through baibu.core.storage, never a vendor SDK. 

169STORAGE_BACKEND = env("STORAGE_BACKEND", default="local") 

170# Contributor files (submitted text, audio). Never served over HTTP. 

171PRIVATE_MEDIA_ROOT = env("DJANGO_PRIVATE_MEDIA_ROOT", default=str(BASE_DIR / "var" / "private")) 

172STORAGES = build_storages( 

173 STORAGE_BACKEND, 

174 media_root=MEDIA_ROOT, 

175 private_root=PRIVATE_MEDIA_ROOT, 

176 s3=S3Options( 

177 bucket=env("S3_BUCKET", default=""), 

178 endpoint_url=env("S3_ENDPOINT_URL", default=""), 

179 region=env("S3_REGION", default=""), 

180 access_key_id=env("S3_ACCESS_KEY_ID", default=""), 

181 secret_access_key=env("S3_SECRET_ACCESS_KEY", default=""), 

182 addressing_style=env("S3_ADDRESSING_STYLE", default="path"), 

183 signed_url_seconds=env.int("S3_SIGNED_URL_SECONDS", default=3600), 

184 ), 

185) 

186 

187# TEMPLATES 

188# ------------------------------------------------------------------------------ 

189TEMPLATES = [ 

190 { 

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

192 # Deployment templates come first so they override the defaults. 

193 "DIRS": [str(DEPLOYMENT_DIR / "templates"), str(APPS_DIR / "templates")], 

194 "APP_DIRS": True, 

195 "OPTIONS": { 

196 "context_processors": [ 

197 "django.template.context_processors.debug", 

198 "django.template.context_processors.request", 

199 "django.contrib.auth.context_processors.auth", 

200 "django.template.context_processors.i18n", 

201 "django.template.context_processors.media", 

202 "django.template.context_processors.static", 

203 "django.template.context_processors.tz", 

204 "django.contrib.messages.context_processors.messages", 

205 "baibu.core.context_processors.platform", 

206 "baibu.notifications.context_processors.notifications", 

207 ], 

208 }, 

209 }, 

210] 

211FORM_RENDERER = "django.forms.renderers.TemplatesSetting" 

212 

213# SECURITY 

214# ------------------------------------------------------------------------------ 

215SESSION_COOKIE_HTTPONLY = True 

216CSRF_COOKIE_HTTPONLY = True 

217X_FRAME_OPTIONS = "DENY" 

218 

219# EMAIL 

220# ------------------------------------------------------------------------------ 

221EMAIL_BACKEND = env("DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.smtp.EmailBackend") 

222EMAIL_TIMEOUT = 5 

223DEFAULT_FROM_EMAIL = env("DJANGO_DEFAULT_FROM_EMAIL", default=PLATFORM_CONTACT_EMAIL or "webmaster@localhost") 

224SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) 

225EMAIL_SUBJECT_PREFIX = f"[{PLATFORM_NAME}] " 

226 

227# ADMIN 

228# ------------------------------------------------------------------------------ 

229ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/") 

230# Operational emails fall back to these addresses when the operations-email 

231# group is empty. Format: "Name <email>, Name <email>". 

232ADMINS = [ 

233 (name.strip(), email.strip(" >")) 

234 for name, _, email in (entry.partition("<") for entry in env.list("DJANGO_ADMINS", default=[])) 

235 if email 

236] 

237MANAGERS = ADMINS 

238# Send the admin sign-in through allauth (rate limits, MFA). 

239DJANGO_ADMIN_FORCE_ALLAUTH = env.bool("DJANGO_ADMIN_FORCE_ALLAUTH", default=False) 

240 

241# LOGGING 

242# ------------------------------------------------------------------------------ 

243LOGGING = { 

244 "version": 1, 

245 "disable_existing_loggers": False, 

246 "formatters": { 

247 "verbose": {"format": "%(levelname)s %(asctime)s %(module)s %(process)d %(message)s"}, 

248 }, 

249 "handlers": { 

250 "console": {"level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose"}, 

251 }, 

252 "root": {"level": "INFO", "handlers": ["console"]}, 

253} 

254 

255# CACHE AND BROKER 

256# ------------------------------------------------------------------------------ 

257# Any Redis-protocol server works. The Compose stack uses Valkey (BSD licence). 

258REDIS_URL = env("REDIS_URL", default="redis://valkey:6379/0") 

259CACHES = { 

260 "default": { 

261 "BACKEND": "django_redis.cache.RedisCache", 

262 "LOCATION": REDIS_URL, 

263 "OPTIONS": { 

264 "CLIENT_CLASS": "django_redis.client.DefaultClient", 

265 # Cache outages must not take the site down. 

266 "IGNORE_EXCEPTIONS": True, 

267 }, 

268 }, 

269} 

270 

271# CELERY 

272# ------------------------------------------------------------------------------ 

273CELERY_BROKER_URL = env("CELERY_BROKER_URL", default=REDIS_URL) 

274CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND", default=None) 

275CELERY_TASK_IGNORE_RESULT = True 

276CELERY_ACCEPT_CONTENT = ["json"] 

277CELERY_TASK_SERIALIZER = "json" 

278CELERY_RESULT_SERIALIZER = "json" 

279CELERY_TIMEZONE = TIME_ZONE 

280CELERY_BROKER_CONNECTION_RETRY_ON_STARTUP = True 

281CELERY_WORKER_PREFETCH_MULTIPLIER = 1 

282# Periodic tasks run by Celery beat. Feature apps add their own entries. 

283CELERY_BEAT_SCHEDULE = { 

284 "worker-heartbeat": { 

285 "task": "baibu.core.tasks.heartbeat", 

286 "schedule": env.int("WORKER_HEARTBEAT_SECONDS", default=60), 

287 }, 

288} 

289CELERY_BEAT_SCHEDULE["chat-sweep-runs"] = { 

290 "task": "baibu.chat.tasks.sweep_runs", 

291 "schedule": env.int("CHAT_SWEEP_SECONDS", default=60), 

292} 

293CELERY_BEAT_SCHEDULE["delete-old-notifications"] = { 

294 "task": "baibu.notifications.tasks.delete_old_notifications", 

295 "schedule": 24 * 60 * 60, 

296} 

297CELERY_BEAT_SCHEDULE["chat-sweep-transcriptions"] = { 

298 "task": "baibu.chat.tasks.sweep_transcriptions", 

299 "schedule": env.int("CHAT_SWEEP_SECONDS", default=60), 

300} 

301CELERY_BEAT_SCHEDULE["chat-tag-conversations"] = { 

302 "task": "baibu.chat.tasks.tag_conversations", 

303 "schedule": env.int("CHAT_TAGGING_INTERVAL_SECONDS", default=600), 

304} 

305# The health endpoint reports the worker as stale after this many seconds 

306# without a heartbeat. 

307WORKER_HEARTBEAT_STALE_SECONDS = env.int("WORKER_HEARTBEAT_STALE_SECONDS", default=300) 

308 

309# django-allauth 

310# ------------------------------------------------------------------------------ 

311# Additional sign-in providers (any provider allauth supports) can be enabled 

312# by a deployment; none is enabled here. 

313ACCOUNT_ALLOW_REGISTRATION = env.bool("DJANGO_ACCOUNT_ALLOW_REGISTRATION", default=True) 

314# Phone sign-in is off by default. When on, people can add a phone number 

315# (verified by a code sent to it) and sign in with a one-time code sent to 

316# that number, alongside email. Codes go out through MESSAGING_PROVIDER. 

317PHONE_SIGN_IN_ENABLED = env.bool("PHONE_SIGN_IN_ENABLED", default=False) 

318# Ask for a phone number at sign-up (verified before the first sign-in). 

319PHONE_SIGN_UP_REQUIRED = env.bool("PHONE_SIGN_UP_REQUIRED", default=False) 

320ACCOUNT_LOGIN_METHODS = login_methods(phone=PHONE_SIGN_IN_ENABLED) 

321ACCOUNT_UNIQUE_EMAIL = True 

322ACCOUNT_USER_MODEL_USERNAME_FIELD = None 

323ACCOUNT_SIGNUP_FIELDS = signup_fields(phone=PHONE_SIGN_IN_ENABLED, phone_required=PHONE_SIGN_UP_REQUIRED) 

324ACCOUNT_EMAIL_VERIFICATION = env("DJANGO_ACCOUNT_EMAIL_VERIFICATION", default="mandatory") 

325ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True 

326ACCOUNT_LOGIN_BY_CODE_ENABLED = True 

327ACCOUNT_ADAPTER = "baibu.users.adapters.AccountAdapter" 

328ACCOUNT_FORMS = {"signup": "baibu.users.forms.UserSignupForm"} 

329# Phone sign-in: dotted path to a baibu.users.messaging.MessagingProvider. 

330# The console provider only writes messages to the log. 

331MESSAGING_PROVIDER = env("MESSAGING_PROVIDER", default="baibu.users.messaging.ConsoleMessagingProvider") 

332# Six digits: easy to type on any phone and picked up by SMS autofill. 

333ACCOUNT_PHONE_VERIFICATION_CODE_FORMAT = {"numeric": True, "dashed": False, "length": 6} 

334# Text messages get lost; allow two more codes per phone verification. 

335ACCOUNT_PHONE_VERIFICATION_SUPPORTS_RESEND = True 

336 

337# PROFILE AND CONSENT 

338# ------------------------------------------------------------------------------ 

339# Signed-in users missing any of these profile fields are asked to complete 

340# their profile before continuing. Allowed values: name, city, country. 

341PROFILE_REQUIRED_FIELDS = env.list("PROFILE_REQUIRED_FIELDS", default=["name"]) 

342# The consent scope recorded by the account settings page. Feature apps that 

343# need their own consent (for example chat) record under their own scope. 

344CONSENT_DEFAULT_SCOPE = env("CONSENT_DEFAULT_SCOPE", default="platform") 

345# Stored with every consent decision. Change it whenever the wording people 

346# agree to (consent page or privacy notice) changes. 

347CONSENT_TEXT_VERSION = env("CONSENT_TEXT_VERSION", default="1") 

348 

349# ACCOUNT DELETION 

350# ------------------------------------------------------------------------------ 

351ACCOUNT_DELETION_REQUEST_RATE_LIMIT = env.int("ACCOUNT_DELETION_REQUEST_RATE_LIMIT", default=3) 

352ACCOUNT_DELETION_REQUEST_RATE_WINDOW_SECONDS = env.int("ACCOUNT_DELETION_REQUEST_RATE_WINDOW_SECONDS", default=3600) 

353 

354# SUBMISSIONS 

355# ------------------------------------------------------------------------------ 

356# Languages people may contribute in. Defaults to the interface languages; any 

357# code Django knows or PLATFORM_EXTRA_LANGUAGES declares is allowed. 

358SUBMISSION_LANGUAGES = env.list("SUBMISSION_LANGUAGES", default=[code for code, _name in LANGUAGES]) 

359# Contributors need at least eval_only consent under this scope. 

360SUBMISSION_CONSENT_SCOPE = env("SUBMISSION_CONSENT_SCOPE", default=CONSENT_DEFAULT_SCOPE) 

361SUBMISSION_MIN_WORDS = env.int("SUBMISSION_MIN_WORDS", default=10) 

362SUBMISSION_MAX_CHARACTERS = env.int("SUBMISSION_MAX_CHARACTERS", default=20000) 

363# Characters of cleaned text kept in the database and shown to the contributor. 

364SUBMISSION_EXCERPT_LENGTH = env.int("SUBMISSION_EXCERPT_LENGTH", default=280) 

365# The cleaning step after the built-in rules: RuleCleaner (rules only, the 

366# default), LiteLLMCleaner (a model the deployment chooses) or MockCleaner. 

367SUBMISSION_CLEANER = env("SUBMISSION_CLEANER", default="baibu.submissions.cleaners.RuleCleaner") 

368# For LiteLLMCleaner: any model name LiteLLM accepts, and optionally the 

369# endpoint and key (for example a self-hosted OpenAI-compatible server). 

370SUBMISSION_CLEANER_MODEL = env("SUBMISSION_CLEANER_MODEL", default="") 

371SUBMISSION_CLEANER_API_BASE = env("SUBMISSION_CLEANER_API_BASE", default="") 

372SUBMISSION_CLEANER_API_KEY = env("SUBMISSION_CLEANER_API_KEY", default="") 

373SUBMISSION_CLEANER_TIMEOUT = env.int("SUBMISSION_CLEANER_TIMEOUT", default=60) 

374# Texts a cleaner scores below this (0-100) go to staff review. 

375SUBMISSION_MIN_QUALITY = env.int("SUBMISSION_MIN_QUALITY", default=50) 

376 

377# CHAT 

378# ------------------------------------------------------------------------------ 

379CHAT_ENABLED = env.bool("CHAT_ENABLED", default=True) 

380# Consent scope for conversations. Users choose a tier once before chatting. 

381CHAT_CONSENT_SCOPE = env("CHAT_CONSENT_SCOPE", default="chat") 

382# Model used when no model variant is marked as default in the admin. "mock" 

383# answers locally without any model; any LiteLLM model name works. 

384CHAT_MODEL = env("CHAT_MODEL", default="mock") 

385CHAT_MODEL_TIMEOUT = env.int("CHAT_MODEL_TIMEOUT", default=60) 

386# Earlier messages sent to the model with each new one. 

387CHAT_CONTEXT_MESSAGES = env.int("CHAT_CONTEXT_MESSAGES", default=20) 

388CHAT_MAX_MESSAGE_CHARACTERS = env.int("CHAT_MAX_MESSAGE_CHARACTERS", default=4000) 

389# Attempts at a reply to one message, including retries. 

390CHAT_MAX_ATTEMPTS = env.int("CHAT_MAX_ATTEMPTS", default=3) 

391# A reply still running after this long is failed so the user can retry. 

392CHAT_RUN_TIMEOUT_SECONDS = env.int("CHAT_RUN_TIMEOUT_SECONDS", default=180) 

393# Web search for the assistant's internet_search tool: a SearchProvider 

394# subclass (see baibu.chat.search). Empty disables the tool. No provider is 

395# bundled; baibu.chat.search.MockSearchProvider returns made-up results. 

396CHAT_SEARCH_PROVIDER = env("CHAT_SEARCH_PROVIDER", default="") 

397CHAT_SEARCH_MAX_RESULTS = env.int("CHAT_SEARCH_MAX_RESULTS", default=5) 

398# Rounds of tool calls the model may make before it must answer. 

399CHAT_MAX_TOOL_ROUNDS = env.int("CHAT_MAX_TOOL_ROUNDS", default=2) 

400# Voice messages: a SpeechToText subclass (see baibu.chat.speech). Empty 

401# turns voice input off. LiteLLMSpeechToText reaches any transcription model 

402# LiteLLM supports; MockSpeechToText makes up transcripts without a model. 

403CHAT_STT_PROVIDER = env("CHAT_STT_PROVIDER", default="") 

404# For LiteLLMSpeechToText: model name, optional endpoint, the *name* of the 

405# environment variable holding the API key (never the key itself), timeout 

406# and extra arguments for every call as JSON (e.g. {"language": "sw"}). 

407CHAT_STT_MODEL = env("CHAT_STT_MODEL", default="") 

408CHAT_STT_API_BASE = env("CHAT_STT_API_BASE", default="") 

409CHAT_STT_API_KEY_ENV = env("CHAT_STT_API_KEY_ENV", default="") 

410CHAT_STT_TIMEOUT = env.int("CHAT_STT_TIMEOUT", default=60) 

411CHAT_STT_PARAMETERS = env.json("CHAT_STT_PARAMETERS", default={}) 

412# Largest recording accepted, in bytes, and the longest the browser records. 

413CHAT_VOICE_MAX_BYTES = env.int("CHAT_VOICE_MAX_BYTES", default=10 * 1024 * 1024) 

414CHAT_VOICE_MAX_SECONDS = env.int("CHAT_VOICE_MAX_SECONDS", default=120) 

415# Audio formats accepted for upload. 

416CHAT_VOICE_CONTENT_TYPES = env.list( 

417 "CHAT_VOICE_CONTENT_TYPES", 

418 default=[ 

419 "audio/webm", 

420 "audio/ogg", 

421 "audio/mp4", 

422 "audio/x-m4a", 

423 "audio/aac", 

424 "audio/mpeg", 

425 "audio/wav", 

426 "audio/x-wav", 

427 ], 

428) 

429# Topic tagging of conversations (off by default). Topics are defined in the 

430# admin. Only conversations whose chat consent is at least 

431# CHAT_TAGGING_MIN_TIER are tagged. 

432CHAT_TAGGING_ENABLED = env.bool("CHAT_TAGGING_ENABLED", default=False) 

433CHAT_TAGGING_MIN_TIER = env("CHAT_TAGGING_MIN_TIER", default="eval_only") 

434# Empty: the chat's default model. "mock" matches topic names as keywords. 

435CHAT_TAGGING_MODEL = env("CHAT_TAGGING_MODEL", default="") 

436CHAT_TAGGING_INTENTS = env.list( 

437 "CHAT_TAGGING_INTENTS", default=["question", "advice", "information", "conversation", "other"] 

438) 

439CHAT_TAGGING_IDLE_MINUTES = env.int("CHAT_TAGGING_IDLE_MINUTES", default=30) 

440CHAT_TAGGING_MAX_TOPICS = env.int("CHAT_TAGGING_MAX_TOPICS", default=3) 

441CHAT_TAGGING_MAX_MESSAGES = env.int("CHAT_TAGGING_MAX_MESSAGES", default=40) 

442CHAT_TAGGING_MAX_CHARACTERS = env.int("CHAT_TAGGING_MAX_CHARACTERS", default=12000) 

443CHAT_TAGGING_BATCH_SIZE = env.int("CHAT_TAGGING_BATCH_SIZE", default=100) 

444 

445# NOTIFICATIONS 

446# ------------------------------------------------------------------------------ 

447NOTIFICATIONS_ENABLED = env.bool("NOTIFICATIONS_ENABLED", default=True) 

448# Read notifications older than this are deleted daily. 

449NOTIFICATIONS_RETENTION_DAYS = env.int("NOTIFICATIONS_RETENTION_DAYS", default=180) 

450 

451# TRANSLATIONS 

452# ------------------------------------------------------------------------------ 

453# Load translations published from the /translations/ pages into every web and 

454# worker process. Turn off only if you manage catalogues as files. 

455LOCALIZATION_SYNC = env.bool("LOCALIZATION_SYNC", default=True) 

456# How often each process checks (through the cache) for a new publication. 

457LOCALIZATION_SYNC_SECONDS = env.int("LOCALIZATION_SYNC_SECONDS", default=10) 

458 

459# USAGE METRICS 

460# ------------------------------------------------------------------------------ 

461# Count-only daily rollups for the staff usage page (/staff/usage/). Days are 

462# calendar days in TIME_ZONE. Beat computes yesterday at this local time. 

463METRICS_COMPUTE_HOUR = env.int("METRICS_COMPUTE_HOUR", default=2) 

464METRICS_COMPUTE_MINUTE = env.int("METRICS_COMPUTE_MINUTE", default=15) 

465CELERY_BEAT_SCHEDULE["metrics-daily"] = { 

466 "task": "baibu.metrics.tasks.compute_daily_metrics", 

467 "schedule": crontab(hour=METRICS_COMPUTE_HOUR, minute=METRICS_COMPUTE_MINUTE), 

468} 

469# If the worker was down, the nightly task fills in up to this many missed days. 

470METRICS_CATCH_UP_DAYS = env.int("METRICS_CATCH_UP_DAYS", default=7) 

471# Weekly cohorts: how many weeks after joining retention is counted. 

472METRICS_RETENTION_WEEKS = env.int("METRICS_RETENTION_WEEKS", default=8) 

473# Breakdowns by language or consent tier show counts from 1 up to this size 

474# minus one as "fewer than N", so small groups are not singled out. 0 or 1: off. 

475METRICS_MIN_GROUP_SIZE = env.int("METRICS_MIN_GROUP_SIZE", default=5) 

476# Longest date range the usage page and its CSV export accept. 

477METRICS_MAX_RANGE_DAYS = env.int("METRICS_MAX_RANGE_DAYS", default=366)