# core/context_processors.py from pathlib import Path import os from django.contrib.staticfiles import finders from django.contrib.staticfiles.storage import staticfiles_storage from .models_ann import Announcement, AnnouncementDismissal def app_version(request): version_file = Path(__file__).resolve().parent.parent / "version.txt" try: with open(version_file, "r") as f: version = f.read().strip() except FileNotFoundError: version = "v0.0.0" return {"APP_VERSION": version} def available_themes(request): """ Return a list of theme names by scanning /static/themes/*.css. Works in development (finders) and production (staticfiles_storage). Never returns an empty list. """ names = set() # A) Dev/app static: scan all registered static finders try: for finder in finders.get_finders(): for path, storage in finder.list(['themes']): # e.g. "themes/midnight.css" if path.startswith('themes/') and path.endswith('.css'): names.add(os.path.basename(path)[:-4]) # strip ".css" except Exception: pass # B) Prod/collected: scan STATIC_ROOT via staticfiles_storage try: dirs, files = staticfiles_storage.listdir('themes') for f in files: if f.endswith('.css'): names.add(f[:-4]) except Exception: pass # C) Safety net: default themes so the selector is never empty if not names: names.update({'midnight', 'dawn', 'forest', 'sandstone'}) return {'available_themes': sorted(names)} def pending_announcement(request): """ Expose the latest active, current announcement the user has not dismissed. We'll only use it on the Search page via include. """ user = getattr(request, "user", None) if not (user and user.is_authenticated): return {"pending_announcement": None} current = [a for a in Announcement.objects.all() if a.is_current()] for a in current: if not AnnouncementDismissal.objects.filter(user=user, announcement=a).exists(): return {"pending_announcement": a} return {"pending_announcement": None}