ferrero-opentext/Python-Version/scripts/a5_to_a6_download.py
DJP 80dfbe7834 Fix A5→A6 Final Assets search and unify email template styling
Critical fix and UX improvements for all workflow email notifications.

CRITICAL FIX:
- A5→A6 now correctly searches Final Assets folder (is_global=True)
- Previously searched Master Assets folder (wrong location)
- Now finds NOT APPROVED rework assets correctly

TESTED SUCCESSFULLY:
✓ Found 6 total assets in Final Assets folder
✓ Filtered 4 NOT APPROVED assets correctly
✓ Skipped 2 folders without ECOMMERCE STATUS field
✓ Downloaded and uploaded 4 assets to Box Revisions folder
✓ Email sent with rejection details
✓ Status updated A5→A6

EMAIL TEMPLATE STYLING UNIFICATION:
All templates now use consistent modern styling matching a5_to_a6_rejections:
- Colored header bars with centered titles
- Bordered info boxes with left accent bars
- Card-based asset display with colored headers
- Consistent spacing and typography
- Professional color scheme

Templates Updated:
1. a1_to_a2_complete - Green theme (#28a745)
2. a1_to_a2_partial - Orange theme (#ff9800)
3. a2_to_a3_complete - Green theme (#28a745)
4. a2_to_a3_file_uploaded - Green/Blue theme
5. b1_to_b2_complete - Blue theme (#1976d2)
6. b1_to_b2_partial - Orange theme (#ff9800)
7. upload_failed - Red theme (#d32f2f)

All templates keep existing data/functionality, only styling improved.

Color Scheme:
- Success: Green (#28a745)
- Warning/Partial: Orange (#ff9800)
- Error: Red (#d32f2f)
- Info: Blue (#1976d2)
- Highlights: Yellow (#ffc107)

Changes:
- Python-Version/scripts/a5_to_a6_download.py (is_global=True fix)
- Python-Version/scripts/shared/notifier.py (7 templates restyled)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-04 15:25:30 -05:00

367 lines
14 KiB
Python
Executable file

#!/usr/bin/env python3
"""
A5→A6 Rework Asset Downloader
Polls DAM for campaigns with status A5, downloads rework assets, uploads to Box
Updates status to A6 only when ALL assets successfully processed
Compatible with Python 3.6+
"""
import sys
import os
import time
import logging
# Add shared library to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from shared.config_loader import load_config, load_field_mappings
from shared.dam_client import DAMClient
from shared.box_client import BoxClient
from shared.database import Database
from shared.notifier import Notifier
# Setup logging with rotation
from logging.handlers import RotatingFileHandler
# Create logs directory if it doesn't exist
os.makedirs('logs', exist_ok=True)
os.makedirs('logs/backup', exist_ok=True)
# Configure logging with rotation
# Keep 1 week of active logs (7 days * 10MB = 70MB)
# Backup rotates keep 4 weeks (28 backups * 10MB = 280MB total)
log_handler = RotatingFileHandler(
'logs/a5_to_a6.log',
maxBytes=10*1024*1024, # 10MB per file
backupCount=28 # Keep 28 rotated files (approximately 1 month)
)
log_handler.setLevel(logging.INFO)
log_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO)
console_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logging.basicConfig(
level=logging.INFO,
handlers=[log_handler, console_handler]
)
logger = logging.getLogger('A5toA6')
def process_campaign(campaign, dam, box, db, notifier, config):
"""
Process single campaign - download all rework assets
Returns:
dict with success, processed_count, failed_count
"""
campaign_id = campaign['asset_id']
campaign_name = campaign['campaign_name']
campaign_number = campaign.get('campaign_id', 'N/A')
logger.info("=" * 60)
logger.info("Processing campaign: {} ({})".format(campaign_name, campaign_number))
logger.info("=" * 60)
try:
# Get ALL assets from Final Assets folder (recursive search)
# A5 campaigns are Local Adaptation but use Final Assets folder (like B1)
# So we pass is_global=True to search Final Assets folder
all_assets = dam.get_master_assets(campaign_id, is_global=True)
logger.info("Found {} total assets in Final Assets folder".format(len(all_assets)))
# Filter for NOT APPROVED assets only
not_approved_assets = []
skipped_assets = []
for asset in all_assets:
if dam.is_asset_not_approved(asset):
not_approved_assets.append(asset)
else:
skipped_assets.append(asset)
logger.info("NOT APPROVED (rejected) assets: {}".format(len(not_approved_assets)))
logger.info("Approved/other status (skipped): {}".format(len(skipped_assets)))
# If NO rejected assets found, send email and exit
if len(not_approved_assets) == 0:
logger.info("No NOT APPROVED assets found - all assets are approved")
# Send "no rejections" email
notifier.send_email(
template_name='a5_to_a6_no_rejections',
recipients=config['notifications']['recipients']['success'],
data={
'campaign_name': campaign_name,
'campaign_id': campaign_id,
'campaign_number': campaign_number,
'total_assets': len(all_assets),
'skipped_count': len(skipped_assets)
}
)
logger.info("✓ Email sent: No rework required")
return {'success': True, 'processed': 0, 'failed': 0}
# Track results
processed_assets = []
failed_assets = []
# Get Final Assets folder for upload directory
final_folder_id = dam.find_final_assets_folder(campaign_id)
if not final_folder_id:
logger.error("Final Assets folder not found")
return {'success': False, 'processed': 0, 'failed': len(not_approved_assets)}
# Process ONLY NOT APPROVED assets
for asset in not_approved_assets:
asset_id = asset['asset_id']
asset_name = asset.get('name', 'unknown')
folder_path = asset.get('folder_path', '') # Get subfolder path from recursive search
try:
if folder_path:
logger.info("Processing NOT APPROVED: {} (from subfolder: {})".format(asset_name, folder_path))
else:
logger.info("Processing NOT APPROVED: {}".format(asset_name))
# 1. Extract rejection details for email
rejection_details = dam.extract_rejection_details(asset)
# 2. Download from DAM
file_path = dam.download_asset(
asset_id,
output_dir='temp/downloads/{}'.format(campaign_id)
)
# 3. Extract Global Campaign Reference and Local Campaign ID from asset metadata
global_ref = db.extract_global_campaign_reference(asset)
# 4. Check if asset already exists in database (from A1→A2)
# This will either find existing tracking_id or generate new one
tracking_result = db.find_or_create_tracking_id(
opentext_id=asset_id,
local_campaign_id=global_ref['local_campaign_id']
)
tracking_id = tracking_result['tracking_id']
is_existing = tracking_result['is_existing']
if is_existing:
logger.info("Found existing tracking ID: {} (updating record)".format(tracking_id))
else:
logger.info("Generated new tracking ID: {}".format(tracking_id))
# 5. Upload to Box (Revisions folder with -Revisions suffix, preserve folder structure)
box_result = box.upload_with_tracking_id(
file_path=file_path,
campaign_id=campaign_number, # Use C000000078, not hex asset_id
campaign_name=campaign_name + "-Revisions", # Add -Revisions suffix
tracking_id=tracking_id,
subfolder_path=folder_path # Preserve DAM folder structure
)
# 5. Store in database with FULL metadata and campaign references
db_result = db.store_master_asset(
tracking_id=tracking_id,
opentext_id=asset_id,
asset_data=asset,
box_file_id=box_result['file_id'],
box_url=box_result['url'],
upload_folder_id=final_folder_id,
global_master_campaign_id=global_ref['global_master_campaign_id'],
global_master_folder_id=global_ref['global_master_folder_id'],
local_campaign_id=global_ref['local_campaign_id']
)
if db_result['success']:
processed_assets.append({
'asset_id': asset_id,
'asset_name': asset_name,
'tracking_id': tracking_id,
'box_file_id': box_result['file_id'],
'box_url': box_result['url'],
'is_existing': is_existing,
'folder_path': folder_path,
'rejection_details': rejection_details # Include rejection comments for email
})
logger.info("✓ Success: {}{}{}".format(
asset_name,
tracking_id,
" (updated)" if is_existing else " (new)"
))
else:
raise Exception("Database storage failed")
# Clean up temp file
os.remove(file_path)
except Exception as e:
logger.error("✗ Failed: {} - {}".format(asset_name, str(e)))
failed_assets.append({
'asset_id': asset_id,
'asset_name': asset_name,
'error': str(e)
})
# CHECK: All rejected assets processed successfully?
all_done = len(processed_assets) == len(not_approved_assets)
logger.info("")
logger.info("Campaign {} Results:".format(campaign_id))
logger.info(" NOT APPROVED (rejected): {}".format(len(not_approved_assets)))
logger.info(" Successfully processed: {}".format(len(processed_assets)))
logger.info(" Failed: {}".format(len(failed_assets)))
logger.info(" Approved/skipped: {}".format(len(skipped_assets)))
logger.info(" All Done: {}".format("YES" if all_done else "NO"))
logger.info("")
if all_done:
# ALL rejected assets processed - update status
logger.info("All NOT APPROVED assets processed - Updating status A5 → A6")
status_result = dam.update_campaign_status(campaign_id, 'A6')
if status_result['success']:
logger.info("✓ Status updated successfully")
# NOTE: No webhook for A5→A6 (rework workflow)
# Send success email with rejection details
notifier.send_email(
template_name='a5_to_a6_rejections',
recipients=config['notifications']['recipients']['success'],
data={
'campaign_name': campaign_name,
'campaign_id': campaign_id,
'campaign_number': campaign_number,
'rejected_count': len(processed_assets),
'skipped_count': len(skipped_assets),
'rejected_assets': processed_assets # Includes rejection_details
}
)
return {'success': True, 'processed': len(processed_assets), 'failed': 0}
else:
logger.error("✗ Status update failed: {}".format(status_result.get('error')))
# Don't send success notification if status update failed
return {'success': False, 'processed': len(processed_assets), 'failed': 0}
else:
# NOT all done - some failed
logger.warning("Campaign incomplete - NOT updating status (remains A5)")
# Send partial completion email with asset details
notifier.send_email(
template_name='a5_to_a6_partial',
recipients=config['notifications']['recipients']['errors'],
data={
'campaign_name': campaign_name,
'campaign_id': campaign_id,
'campaign_number': campaign_number,
'total_assets': len(not_approved_assets),
'successful': len(processed_assets),
'failed': len(failed_assets),
'rejected_assets': processed_assets, # Includes rejection_details
'failed_assets': failed_assets
}
)
return {'success': False, 'processed': len(processed_assets), 'failed': len(failed_assets)}
except Exception as e:
logger.error("Campaign processing failed: {}".format(str(e)))
return {'success': False, 'processed': 0, 'failed': 0}
def main():
"""Main polling loop"""
logger.info("=" * 60)
logger.info("Ferrero A5→A6 Rework Asset Downloader Starting")
logger.info("=" * 60)
# Load configuration
config = load_config('config/config.yaml')
# Initialize clients (use Revisions Box folder)
dam = DAMClient(config)
box = BoxClient(config, root_folder_id='349441822875') # Revisions folder
db = Database(config)
notifier = Notifier(config)
# Test connections
logger.info("Testing connections...")
if not dam.test_connection():
logger.error("DAM connection failed - exiting")
sys.exit(1)
if not box.test_connection():
logger.error("Box connection failed - exiting")
sys.exit(1)
if not db.test_connection():
logger.error("Database connection failed - exiting")
sys.exit(1)
logger.info("All connections OK")
logger.info("")
# SINGLE RUN MODE - Process ONE campaign and exit
# Cron will run this script every 5 minutes, processing one campaign at a time
try:
logger.info("Searching for A5 campaigns...")
# Search for campaigns with status A5
campaigns = dam.search_campaigns(status='A5')
if not campaigns:
logger.info("No A5 campaigns found - exiting")
db.close()
sys.exit(0)
# Process ONLY THE FIRST campaign
campaign = campaigns[0]
logger.info("Found {} A5 campaigns - processing first one only".format(len(campaigns)))
logger.info("")
result = process_campaign(campaign, dam, box, db, notifier, config)
if result['success']:
logger.info("")
logger.info("=" * 60)
logger.info("✓ Campaign completed successfully")
logger.info(" Processed: {} assets".format(result['processed']))
logger.info(" Status updated: A5 → A6")
logger.info("=" * 60)
db.close()
sys.exit(0)
else:
logger.warning("")
logger.warning("=" * 60)
logger.warning("✗ Campaign incomplete or failed")
logger.warning(" Processed: {} assets".format(result['processed']))
logger.warning(" Failed: {} assets".format(result['failed']))
logger.warning(" Status NOT updated (remains A5)")
logger.warning("=" * 60)
db.close()
sys.exit(1)
except Exception as e:
logger.critical("Script error: {}".format(str(e)))
# Send critical error notification
notifier.send_email(
template_name='upload_failed',
recipients=config['notifications']['recipients']['critical'],
data={
'filename': 'A5→A6 Script',
'tracking_id': 'N/A',
'error': str(e)
}
)
db.close()
sys.exit(1)
if __name__ == '__main__':
main()