First client to use the Phase 4 unattended-QC pipeline. Adds three optional fields to the loreal entry in client_config.py: - box_folder_id=381501258415 (AI-QC > INCOMING > AI QC LOREAL IN) - box_reports_folder_id=382076841334 (AI-QC > REPORTS > AI QC LOREAL REPORTS) - default_profile=loreal_static When a file lands in the INCOMING folder, /api/box/webhook will pick it up, run loreal_static (strict-grade), and upload the HTML report to the REPORTS folder. Other clients remain unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
179 lines
6.3 KiB
Python
179 lines
6.3 KiB
Python
#!/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': ['static_general', 'video_general'],
|
|
'display_name': 'HP',
|
|
'description': 'Demo client — scope pending'
|
|
},
|
|
'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')
|
|
]
|
|
|
|
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)
|