#!/usr/bin/env python3 """ Client configuration module for managing client-profile relationships """ CLIENT_PROFILES = { 'diageo': { 'name': 'Diageo', 'profiles': ['diageo_key_visual', 'diageo_packaging', 'static_general', 'video_general'], 'display_name': 'Diageo', 'description': 'Diageo brand profiles for key visuals and packaging' }, 'unilever': { 'name': 'Unilever', 'profiles': ['unilever_key_visual', 'unilever_packaging', 'static_general', 'video_general'], 'display_name': 'Unilever', 'description': 'Unilever brand profiles for key visuals and packaging' }, 'loreal': { 'name': "L'Oreal", 'profiles': ['loreal_static', 'static_general', 'video_general'], 'display_name': "L'Oreal", 'description': "L'Oreal brand profiles with focused and comprehensive static QC checks", 'box_folder_id': '381501258415', 'box_reports_folder_id': '382076841334', 'default_profile': 'loreal_static', }, 'amazon': { 'name': 'Amazon', 'profiles': ['amazon_static', 'static_general', 'video_general'], 'display_name': 'Amazon', 'description': 'Amazon brand profiles with design guideline QC checks' }, 'boots': { 'name': 'Boots', 'profiles': ['boots_static', 'boots_ppack', 'static_general', 'video_general'], 'display_name': 'Boots', 'description': 'Boots retail promotional artwork compliance checks' }, 'honda': { 'name': 'Honda', 'profiles': ['static_general', 'video_general'], 'display_name': 'Honda', 'description': 'Honda brand profiles for automotive marketing QC checks' }, 'axa': { 'name': 'AXA', 'profiles': ['axa_policy_document', 'axa_policy_document_diff', 'axa_accessibility', 'static_general', 'video_general'], 'display_name': 'AXA', 'description': 'AXA brand profiles, including multi-page policy document QC for AXA Ireland' }, 'rank': { 'name': 'Rank', 'profiles': ['static_general', 'video_general'], 'display_name': 'Rank', 'description': 'Rank brand profiles for marketing QC checks' }, 'google': { 'name': 'Google', 'profiles': ['static_general', 'video_general'], 'display_name': 'Google', 'description': 'Demo client — scope pending' }, 'hp': { 'name': 'HP', 'profiles': ['hp_copy_review', 'static_general', 'video_general'], 'display_name': 'HP', 'description': 'HP marketing copy QC graded against canonical Source Messaging', 'default_profile': 'hp_copy_review', }, 'ferrero': { 'name': 'Ferrero', 'profiles': ['static_general', 'video_general'], 'display_name': 'Ferrero', 'description': 'Demo client — scope pending' }, 'general': { 'name': 'General', 'profiles': ['static_general', 'video_general', 'inclusive_accessibility'], 'display_name': 'General / Other', 'description': 'General purpose profiles and accessibility checks' } } def get_client_profiles(client_id): """Get profiles available for a specific client""" return CLIENT_PROFILES.get(client_id, {}).get('profiles', []) def get_all_clients(): """Get all available clients""" return CLIENT_PROFILES def get_client_by_box_folder(folder_id): """Reverse-lookup: which client owns this Box folder_id? Used by the Box webhook handler. Returns the client_id (key) or None. Folder IDs are compared as strings since the Box API returns them as such. """ target = str(folder_id) if folder_id is not None else None if not target: return None for cid, cfg in CLIENT_PROFILES.items(): if str(cfg.get('box_folder_id', '') or '') == target: return cid return None def get_clients_with_box_folder(): """Return [(client_id, client_config_dict), ...] for clients with a Box folder configured.""" return [ (cid, cfg) for cid, cfg in CLIENT_PROFILES.items() if cfg.get('box_folder_id') ] # ---------- Runtime override for default_profile ---------- # # The static `default_profile` field on each client in CLIENT_PROFILES is the # baseline value, set in code at deploy time. Admins can override it at runtime # via the Settings UI; overrides persist to backend/client_defaults.json # (gitignored, per-server). This separation means a buggy override write can # never break server boot — worst case the override is ignored and the static # value applies. import os as _os import json as _json _DEFAULTS_OVERRIDE_PATH = _os.path.join( _os.path.dirname(_os.path.abspath(__file__)), 'client_defaults.json' ) def _load_default_overrides(): """Return the override dict {client_id: profile_id}; empty dict if no file or unreadable.""" if not _os.path.exists(_DEFAULTS_OVERRIDE_PATH): return {} try: with open(_DEFAULTS_OVERRIDE_PATH, 'r') as f: data = _json.load(f) return data if isinstance(data, dict) else {} except (OSError, _json.JSONDecodeError): return {} def _save_default_overrides(data): """Persist the override dict. Writes to a temp path then renames for atomicity.""" tmp_path = _DEFAULTS_OVERRIDE_PATH + '.tmp' with open(tmp_path, 'w') as f: _json.dump(data, f, indent=2, sort_keys=True) _os.replace(tmp_path, _DEFAULTS_OVERRIDE_PATH) def get_default_profile(client_id): """Return the effective default profile for a client. Resolution order: runtime override → static `default_profile` field → None. Used by the Box webhook handler (no logged-in user, needs a profile to run). """ overrides = _load_default_overrides() if client_id in overrides: return overrides[client_id] cfg = CLIENT_PROFILES.get(client_id, {}) return cfg.get('default_profile') def set_default_profile(client_id, profile_id): """Persist a runtime override for a client's default profile. Validates that the client exists and the profile is one of the client's allowed profiles. Returns (True, None) on success or (False, reason) on rejection. """ if client_id not in CLIENT_PROFILES: return False, f'unknown client: {client_id}' allowed = get_client_profiles(client_id) if profile_id not in allowed: return False, f"profile '{profile_id}' is not in client {client_id}'s profile list" overrides = _load_default_overrides() overrides[client_id] = profile_id _save_default_overrides(overrides) return True, None def clear_default_profile_override(client_id): """Remove a runtime override so the static default applies again.""" overrides = _load_default_overrides() if client_id in overrides: del overrides[client_id] _save_default_overrides(overrides) def validate_client_profile(client_id, profile_id): """Validate that a profile belongs to a client""" client_profiles = get_client_profiles(client_id) return profile_id in client_profiles def get_profiles_with_visibility(client_id): """ Get all profiles available to a client considering visibility settings Args: client_id: Client ID to get profiles for Returns: List of profile IDs available to this client """ import os import json import glob available_profiles = [] # First, get the profiles assigned to this client in CLIENT_PROFILES # This is our baseline for backward compatibility baseline_profiles = get_client_profiles(client_id) # Get profiles directory profiles_dir = os.path.join(os.path.dirname(__file__), 'profiles') profile_files = glob.glob(os.path.join(profiles_dir, '*.json')) for profile_file in profile_files: try: with open(profile_file, 'r') as f: profile_data = json.load(f) profile_id = os.path.basename(profile_file).replace('.json', '') # Check if profile has visibility settings has_visibility_settings = 'visibility' in profile_data if has_visibility_settings: # Use new visibility system if configured visibility = profile_data.get('visibility', 'all') visible_to_clients = profile_data.get('visible_to_clients', []) if visibility == 'all': available_profiles.append(profile_id) elif visibility == 'client_specific' and client_id in visible_to_clients: available_profiles.append(profile_id) else: # Fall back to CLIENT_PROFILES mapping for backward compatibility if profile_id in baseline_profiles: available_profiles.append(profile_id) except (json.JSONDecodeError, FileNotFoundError): continue return available_profiles # Admin membership now lives in backend/user_access.json. # Kept as a thin shim so any older callers keep working. def is_admin(email): """Deprecated — delegates to user_access.is_admin().""" from user_access import is_admin as _is_admin return _is_admin(email)