From f03b3bfdafd6b72c358358d0b35e0d0d3cb2ff86 Mon Sep 17 00:00:00 2001 From: jessikitty Date: Fri, 25 Sep 2026 00:43:26 +1000 Subject: [PATCH] Add SiteSetting model for admin-configurable settings --- app/models/site_setting.py | 98 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 app/models/site_setting.py diff --git a/app/models/site_setting.py b/app/models/site_setting.py new file mode 100644 index 0000000..d4cb5ba --- /dev/null +++ b/app/models/site_setting.py @@ -0,0 +1,98 @@ +""" +Key/value store for admin-configurable site settings. + +Values are strings on disk; the typed helpers below do the coercion. Reads +are cached per process and the cache is dropped on write, so the settings +page is authoritative without hitting the database on every request. + +Created by: Jess Rogerson (yelling commands at Claude.AI) +""" + +from datetime import datetime +import threading + +from app import db + +# Defaults are the behaviour LIM had before settings existed, except where +# noted — anything not yet stored falls back to these. +DEFAULTS = { + 'allow_registration': 'true', + # Visibility given to newly created sets. + 'default_visibility': 'private', + # Send logged-in users straight to their dashboard instead of the + # marketing homepage. + 'home_redirect_dashboard': 'true', +} + +VISIBILITIES = ('private', 'shared', 'public') + +_cache = {} +_cache_lock = threading.Lock() + + +class SiteSetting(db.Model): + """One configurable setting.""" + + __tablename__ = 'site_settings' + + id = db.Column(db.Integer, primary_key=True) + key = db.Column(db.String(64), unique=True, nullable=False, index=True) + value = db.Column(db.Text, nullable=True) + updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, + onupdate=datetime.utcnow) + + def __repr__(self): + return f'' + + # -- reads -------------------------------------------------------------- + + @classmethod + def get(cls, key, default=None): + with _cache_lock: + if key in _cache: + return _cache[key] + try: + row = cls.query.filter_by(key=key).first() + except Exception: + # Table may not exist yet (first boot, before create_all). + return DEFAULTS.get(key, default) + value = row.value if row else DEFAULTS.get(key, default) + with _cache_lock: + _cache[key] = value + return value + + @classmethod + def get_bool(cls, key, default=False): + value = cls.get(key) + if value is None: + return default + return str(value).strip().lower() in ('1', 'true', 'yes', 'on') + + @classmethod + def all_settings(cls): + merged = dict(DEFAULTS) + try: + for row in cls.query.all(): + merged[row.key] = row.value + except Exception: + pass + return merged + + # -- writes ------------------------------------------------------------- + + @classmethod + def set(cls, key, value): + if isinstance(value, bool): + value = 'true' if value else 'false' + row = cls.query.filter_by(key=key).first() + if row: + row.value = value + else: + db.session.add(cls(key=key, value=value)) + cls.clear_cache() + return value + + @classmethod + def clear_cache(cls): + with _cache_lock: + _cache.clear()