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
« prev ^ index » next coverage.py v7.10.7, created at 2026-10-06 13:07 +0000
1"""Base settings shared by every environment.
3Everything a deployment is expected to change is read from the environment;
4see ``.env.example`` and the configuration reference in the docs.
5"""
7from pathlib import Path
9import environ
10from celery.schedules import crontab
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
19BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
20APPS_DIR = BASE_DIR / "baibu"
21env = environ.Env()
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"))
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
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")))
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")]
68# DATABASES
69# ------------------------------------------------------------------------------
70DATABASES = {"default": env.db("DATABASE_URL")}
71DATABASES["default"]["ATOMIC_REQUESTS"] = True
72DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
74# URLS
75# ------------------------------------------------------------------------------
76ROOT_URLCONF = "config.urls"
77WSGI_APPLICATION = "config.wsgi.application"
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
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"
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]
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]
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/"
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)
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"
213# SECURITY
214# ------------------------------------------------------------------------------
215SESSION_COOKIE_HTTPONLY = True
216CSRF_COOKIE_HTTPONLY = True
217X_FRAME_OPTIONS = "DENY"
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}] "
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)
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}
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}
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)
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
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")
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)
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)
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)
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)
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)
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)