Extracts CreativeX score and URL from DAM master metadata during the B1→B2 download, persists to creativex_scores with new status 'b1-master-cx-score' (dedup by tracking_id), and surfaces the score in the b1_to_b2_complete and b1_to_b2_partial emails — falling back to "No CreativeX Score" when the master has no score yet. Skipped already-downloaded assets backfill from full_metadata JSONB on next pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1243 lines
90 KiB
Python
1243 lines
90 KiB
Python
"""
|
||
Notifier - Email and Webhook Notifications
|
||
Handles SMTP emails (Mailgun) and outgoing webhooks
|
||
Compatible with Python 3.6+
|
||
"""
|
||
|
||
import requests
|
||
import logging
|
||
import smtplib
|
||
from email.mime.text import MIMEText
|
||
from email.mime.multipart import MIMEMultipart
|
||
from jinja2 import Template
|
||
|
||
logger = logging.getLogger('Notifier')
|
||
|
||
class Notifier:
|
||
def __init__(self, config):
|
||
self.config = config
|
||
self.enabled = config['notifications']['enabled']
|
||
|
||
# SMTP configuration
|
||
smtp_config = config['notifications'].get('smtp', {})
|
||
self.smtp_server = smtp_config.get('server')
|
||
self.smtp_port = smtp_config.get('port', 587)
|
||
self.smtp_user = smtp_config.get('user')
|
||
self.smtp_password = smtp_config.get('password')
|
||
self.sender_email = smtp_config.get('sender_email')
|
||
|
||
# Mailgun API configuration (preferred over SMTP when configured)
|
||
mailgun_config = config['notifications'].get('mailgun', {})
|
||
self.mailgun_api_key = mailgun_config.get('api_key')
|
||
self.mailgun_domain = mailgun_config.get('domain')
|
||
self.mailgun_sender = mailgun_config.get('sender_email') or self.sender_email
|
||
|
||
self.recipients = config['notifications']['recipients']
|
||
self.webhook_config = config.get('webhooks', {})
|
||
|
||
def send_email(self, template_name, recipients, data, attachments=None):
|
||
"""
|
||
Send email via SMTP (Mailgun)
|
||
|
||
Args:
|
||
template_name: Name of email template
|
||
recipients: List of email addresses or single email
|
||
data: Template data dict
|
||
attachments: List of file paths to attach (optional)
|
||
"""
|
||
if not self.enabled:
|
||
logger.info("Notifications disabled, skipping email")
|
||
return
|
||
|
||
if not self.mailgun_api_key and (not self.smtp_server or not self.smtp_user):
|
||
logger.warning("Neither Mailgun API nor SMTP configured, skipping email")
|
||
return
|
||
|
||
try:
|
||
# Simple templates (full template system would load from YAML)
|
||
templates = {
|
||
'a1_to_a2_complete': {
|
||
'subject': "✅ Master Assets Downloaded - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">✅ Master Assets Downloaded Successfully</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Total Assets:</strong> {{ asset_count }}
|
||
{% if existing_asset_count and existing_asset_count > 0 %}
|
||
({{ existing_asset_count }} previously downloaded, <strong>{{ new_asset_count }} new this run</strong>)
|
||
{% endif %}
|
||
</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status Updated:</strong> A1 → A2</p>
|
||
</div>
|
||
|
||
{% if new_assets is defined %}
|
||
{% if new_assets|length > 0 %}
|
||
<h3 style="margin-top: 30px; color: #28a745;">🆕 New This Run ({{ new_assets|length }}):</h3>
|
||
{% for asset in new_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
|
||
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
{% if existing_assets is defined and existing_assets|length > 0 %}
|
||
<h3 style="margin-top: 30px; color: #666;">📁 Previously Downloaded ({{ existing_assets|length }}):</h3>
|
||
<div style="border: 1px solid #ddd; padding: 10px 15px; background-color: #f5f5f5; border-radius: 4px;">
|
||
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">These files were already in Box from an earlier run and were skipped.</p>
|
||
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #555;">
|
||
{% for asset in existing_assets %}
|
||
<li style="margin: 3px 0;">{{ asset.asset_name }} <code style="color: #888; font-size: 11px;">({{ asset.tracking_id }})</code></li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<h3 style="margin-top: 30px; color: #333;">Processed Assets:</h3>
|
||
{% for asset in processed_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
|
||
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>✓ Complete:</strong> All assets downloaded from DAM and uploaded to Box with tracking IDs.</p>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">Campaign status updated from A1 to A2. Box Folder: 348304357505 (Local Adaptation)</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a2_to_a3_complete': {
|
||
'subject': "✅ Localized Assets Uploaded - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">✅ Localized Assets Uploaded Successfully</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Assets Uploaded:</strong> {{ asset_count }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status Updated:</strong> A2 → A3</p>
|
||
</div>
|
||
|
||
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>✓ Complete:</strong> All localized assets have been uploaded to DAM.</p>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">Campaign status updated from A2 to A3.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a2_to_a3_batch_complete': {
|
||
'subject': "A2→A3 Batch Upload Complete - {successful_count}/{total_files} Successful",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: {% if failed_count == 0 %}#28a745{% else %}#ff9800{% endif %}; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">{% if failed_count == 0 %}✅ Batch Upload Complete{% else %}⚠️ Batch Upload Partial{% endif %}</h1>
|
||
</div>
|
||
|
||
<div style="background-color: {% if failed_count == 0 %}#d4edda{% else %}#fff3cd{% endif %}; border-left: 4px solid {% if failed_count == 0 %}#28a745{% else %}#ffc107{% endif %}; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Total Files:</strong> {{ total_files }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>✓ Successful:</strong> {{ successful_count }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>✗ Failed:</strong> {{ failed_count }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Source Folder:</strong> {{ box_folder }}</p>
|
||
</div>
|
||
|
||
{% if successful_files %}
|
||
<h3 style="margin-top: 30px; color: #28a745;">✅ Successfully Uploaded ({{ successful_count }}):</h3>
|
||
{% for asset in successful_files %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.clean_filename }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Original File:</span> {{ asset.filename }}</p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Asset ID:</span> {{ asset.asset_id }}</p>
|
||
{% if asset.subfolder_path %}
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Folder Path:</span> {{ asset.subfolder_path }}</p>
|
||
{% endif %}
|
||
{% if asset.creativex_found %}
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX Score:</span> {{ asset.creativex_score }}</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
{% if failed_files %}
|
||
<h3 style="margin-top: 30px; color: #d32f2f;">❌ Failed Uploads ({{ failed_count }}):</h3>
|
||
{% for asset in failed_files %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #ffebee; border-radius: 4px;">
|
||
<div style="background-color: #d32f2f; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.filename }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0; color: #d32f2f;"><span style="font-weight: bold;">Error:</span> {{ asset.error }}</p>
|
||
{% if asset.tracking_id %}<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> {{ asset.tracking_id }}</p>{% endif %}
|
||
{% if asset.subfolder_path %}
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Folder Path:</span> {{ asset.subfolder_path }}</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Status:</strong> Batch processing complete.</p>
|
||
</div>
|
||
</div>
|
||
"""
|
||
},
|
||
'upload_failed': {
|
||
'subject': "❌ Upload Failed - {filename}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #d32f2f; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">❌ Upload Failed</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Filename:</strong> {{ filename }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Tracking ID:</strong> <code>{{ tracking_id }}</code></p>
|
||
</div>
|
||
|
||
<div style="border: 1px solid #ddd; margin: 20px 0; padding: 15px; background-color: #ffebee; border-radius: 4px;">
|
||
<div style="background-color: #d32f2f; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>Error Details</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0; color: #d32f2f;"><strong>Error:</strong> {{ error }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>📌 Action Required:</strong> Please investigate the error and retry the upload.</p>
|
||
</div>
|
||
</div>
|
||
"""
|
||
},
|
||
'a1_to_a2_partial': {
|
||
'subject': "⚠️ Partial Download - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">⚠️ Campaign Partially Processed</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Total Assets:</strong> {{ total_assets }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Successful:</strong> {{ successful }} | <strong>Failed:</strong> {{ failed }}</p>
|
||
</div>
|
||
|
||
{% if successful > 0 %}
|
||
<h3 style="margin-top: 30px; color: #28a745;">✅ Successfully Processed ({{ successful }}):</h3>
|
||
{% for asset in processed_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
|
||
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
{% if failed > 0 %}
|
||
<h3 style="margin-top: 30px; color: #d32f2f;">❌ Failed Assets ({{ failed }}):</h3>
|
||
{% for asset in failed_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #ffebee; border-radius: 4px;">
|
||
<div style="background-color: #d32f2f; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0; color: #d32f2f;"><span style="font-weight: bold;">Error:</span> {{ asset.error }}</p>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0; color: #d32f2f;"><strong>⚠️ Status NOT updated.</strong> Campaign remains at A1.</p>
|
||
<p style="margin: 5px 0 0 0;">The script will retry failed assets on the next run (every 5 minutes).</p>
|
||
</div>
|
||
</div>
|
||
"""
|
||
},
|
||
'a2_to_a3_file_uploaded': {
|
||
'subject': "✅ Asset Uploaded to DAM - {clean_filename}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">✅ Asset Uploaded Successfully (A2→A3)</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Original Filename (from Box):</strong> <code>{{ filename }}</code></p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Clean Filename (in DAM):</strong> <code>{{ clean_filename }}</code></p>
|
||
<p style="margin: 5px 0 0 0;"><strong>DAM Asset ID:</strong> <code>{{ asset_id }}</code></p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Tracking ID:</strong> <code>{{ tracking_id }}</code></p>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 30px; color: #333;">Processing Details:</h3>
|
||
<div style="padding: 15px; background-color: #f8f9fa; border-radius: 4px; margin: 10px 0;">
|
||
<p style="margin: 5px 0;"><strong>Master Asset ID:</strong> {{ master_asset_name }}</p>
|
||
<p style="margin: 5px 0;"><strong>Uploaded to DAM Folder:</strong> {{ upload_folder }}</p>
|
||
<p style="margin: 5px 0;"><strong>Downloaded from Box Folder:</strong> {{ box_folder }}</p>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 30px; color: #333;">What Was Done:</h3>
|
||
<div style="padding: 15px; background-color: #f8f9fa; border-radius: 4px; margin: 10px 0;">
|
||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||
<li>✅ Downloaded from Box processing folder (348526703108)</li>
|
||
<li>✅ Loaded master metadata from database ({{ tracking_id }})</li>
|
||
<li>✅ Built asset representation with 27 MVP fields</li>
|
||
<li>✅ Updated Description from filename</li>
|
||
<li>✅ Updated Language from filename</li>
|
||
<li>✅ Set State to "Local"</li>
|
||
{% if creativex_found %}
|
||
<li>✅ <strong>CreativeX Score Added:</strong> {{ creativex_score }} (from database)</li>
|
||
{% else %}
|
||
<li>⚠️ <strong>CreativeX Score:</strong> Not found - used default (0)</li>
|
||
{% endif %}
|
||
<li>✅ Stripped OMG Job Number and Tracking ID from filename</li>
|
||
<li>✅ Uploaded to DAM Final Assets folder</li>
|
||
<li>✅ <strong>Deleted file from Box</strong></li>
|
||
</ul>
|
||
</div>
|
||
|
||
{% if not creativex_found %}
|
||
<div style="background-color: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>⚠️ CreativeX Score Missing</strong></p>
|
||
<p style="margin: 5px 0 0 0;">No CreativeX score found in database for: <code>{{ clean_filename }}</code></p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Default Values Used:</strong></p>
|
||
<ul style="margin: 5px 0 0 20px; padding: 0;">
|
||
<li>Score: 0</li>
|
||
<li>URL: None (no CreativeX URL sent)</li>
|
||
</ul>
|
||
<p style="margin: 10px 0 0 0; font-size: 12px; color: #666;">
|
||
<em>To add CreativeX score: Upload PDF report to Box folder 350605024645 and run creativex_scoring_storing.py</em>
|
||
</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Status:</strong> Asset processing complete.</p>
|
||
<p style="margin: 5px 0 0 0;"><em>Note: Campaign status will be updated to A3 once all assets are uploaded.</em></p>
|
||
</div>
|
||
</div>
|
||
"""
|
||
},
|
||
'b1_to_b2_complete': {
|
||
'subject': "✅ Global Master Assets Downloaded - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #1976d2; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">✅ Global Master Assets Downloaded Successfully</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign Type:</strong> Global Masters</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Total Assets:</strong> {{ asset_count }}
|
||
{% if existing_asset_count and existing_asset_count > 0 %}
|
||
({{ existing_asset_count }} previously downloaded, <strong>{{ new_asset_count }} new this run</strong>)
|
||
{% endif %}
|
||
</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status Updated:</strong> B1 → B2</p>
|
||
</div>
|
||
|
||
{% if new_assets is defined %}
|
||
{% if new_assets|length > 0 %}
|
||
<h3 style="margin-top: 30px; color: #1976d2;">🆕 New This Run ({{ new_assets|length }}):</h3>
|
||
{% for asset in new_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #1976d2; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX Score:</span> {% if asset.creativex_score %}{{ asset.creativex_score }}{% if asset.creativex_url %} (<a href="{{ asset.creativex_url }}">View on CreativeX</a>){% endif %}{% else %}<span style="color: #999;">No CreativeX Score</span>{% endif %}</p>
|
||
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
{% if existing_assets is defined and existing_assets|length > 0 %}
|
||
<h3 style="margin-top: 30px; color: #666;">📁 Previously Downloaded ({{ existing_assets|length }}):</h3>
|
||
<div style="border: 1px solid #ddd; padding: 10px 15px; background-color: #f5f5f5; border-radius: 4px;">
|
||
<p style="margin: 0 0 8px 0; color: #666; font-size: 13px;">These files were already in Box from an earlier run and were skipped.</p>
|
||
<ul style="margin: 5px 0 0 0; padding-left: 20px; color: #555;">
|
||
{% for asset in existing_assets %}
|
||
<li style="margin: 3px 0;">{{ asset.asset_name }} <code style="color: #888; font-size: 11px;">({{ asset.tracking_id }})</code> — <span style="font-size: 12px;">CreativeX: {% if asset.creativex_score %}{{ asset.creativex_score }}{% else %}<span style="color: #999;">none</span>{% endif %}</span></li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<h3 style="margin-top: 30px; color: #333;">Processed Assets:</h3>
|
||
{% for asset in processed_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #1976d2; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ asset.box_file_id }}</p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX Score:</span> {% if asset.creativex_score %}{{ asset.creativex_score }}{% if asset.creativex_url %} (<a href="{{ asset.creativex_url }}">View on CreativeX</a>){% endif %}{% else %}<span style="color: #999;">No CreativeX Score</span>{% endif %}</p>
|
||
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>✓ Complete:</strong> All Global Master assets downloaded from DAM and uploaded to Box with tracking IDs.</p>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">Campaign status updated from B1 to B2. Box Folder: 349261192115 (Global Masters)</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'b1_to_b2_partial': {
|
||
'subject': "⚠️ Partial Download - Global Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">⚠️ Global Campaign Partially Processed</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign Type:</strong> Global Masters (B1→B2)</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Total Assets:</strong> {{ total_assets }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Successful:</strong> {{ successful }} | <strong>Failed:</strong> {{ failed }}</p>
|
||
</div>
|
||
|
||
{% if successful > 0 %}
|
||
<h3 style="margin-top: 30px; color: #28a745;">✅ Successfully Processed ({{ successful }}):</h3>
|
||
{% for asset in processed_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #28a745; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX Score:</span> {% if asset.creativex_score %}{{ asset.creativex_score }}{% if asset.creativex_url %} (<a href="{{ asset.creativex_url }}">View on CreativeX</a>){% endif %}{% else %}<span style="color: #999;">No CreativeX Score</span>{% endif %}</p>
|
||
{% if asset.folder_path %}<p style="margin: 5px 0;"><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
{% if failed > 0 %}
|
||
<h3 style="margin-top: 30px; color: #d32f2f;">❌ Failed Assets ({{ failed }}):</h3>
|
||
{% for asset in failed_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #ffebee; border-radius: 4px;">
|
||
<div style="background-color: #d32f2f; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0; color: #d32f2f;"><span style="font-weight: bold;">Error:</span> {{ asset.error }}</p>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0; color: #d32f2f;"><strong>⚠️ Status NOT updated.</strong> Campaign remains at B1.</p>
|
||
<p style="margin: 5px 0 0 0;">The script will retry failed assets on the next run (every 5 minutes).</p>
|
||
</div>
|
||
</div>
|
||
"""
|
||
},
|
||
'a5_to_a6_rejections': {
|
||
'subject': "⚠️ NOT APPROVED Assets - Rework Required - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #d32f2f; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">⚠️ NOT APPROVED ASSETS - REWORK REQUIRED</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>⚠️ Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>NOT APPROVED assets:</strong> {{ rejected_count }} | <strong>Approved/skipped:</strong> {{ skipped_count }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status Updated:</strong> A5 → A6</p>
|
||
</div>
|
||
|
||
{% for asset in rejected_assets %}
|
||
<div style="border: 1px solid #ddd; margin: 20px 0; padding: 20px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #424242; color: white; padding: 12px 15px; margin: -20px -20px 20px -20px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ asset.asset_name }}</strong>
|
||
</div>
|
||
|
||
<div style="margin: 10px 0; padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p><span style="font-weight: bold;">Status:</span> <span style="color: #d32f2f; font-weight: bold;">NOT APPROVED</span></p>
|
||
<p><span style="font-weight: bold;">Tracking ID:</span> <code>{{ asset.tracking_id }}</code>{% if asset.is_existing %} <span style="color: #007bff;">(Updated existing)</span>{% endif %}</p>
|
||
<p><span style="font-weight: bold;">Box URL:</span> <a href="{{ asset.box_url }}">{{ asset.box_url }}</a></p>
|
||
{% if asset.folder_path %}<p><span style="font-weight: bold;">DAM Path:</span> {{ asset.folder_path }}</p>{% endif %}
|
||
</div>
|
||
|
||
{# Rejection details commented out
|
||
{% if asset.rejection_details %}
|
||
{% set approver = asset.rejection_details.approver %}
|
||
{% set legal = asset.rejection_details.legal %}
|
||
{% set ia_cc = asset.rejection_details.ia_cc %}
|
||
|
||
{% if approver.comment %}
|
||
<div style="margin: 15px 0; padding: 15px; border-left: 4px solid #d32f2f; background-color: #ffebee; border-radius: 0 4px 4px 0;">
|
||
<div style="font-weight: bold; color: #d32f2f; margin-bottom: 10px;">🔴 APPROVER REJECTION</div>
|
||
<p style="margin: 8px 0;"><strong>Reason:</strong> {{ approver.comment }}</p>
|
||
{% if approver.certifier_name %}<p style="margin: 8px 0;"><strong>Rejected By:</strong> {{ approver.certifier_name }}</p>{% endif %}
|
||
{% if approver.date %}<p style="margin: 8px 0;"><strong>Date:</strong> {{ approver.date }}</p>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if legal.comment %}
|
||
<div style="margin: 15px 0; padding: 15px; border-left: 4px solid #d32f2f; background-color: #ffebee; border-radius: 0 4px 4px 0;">
|
||
<div style="font-weight: bold; color: #d32f2f; margin-bottom: 10px;">⚖️ LEGAL REJECTION</div>
|
||
<p style="margin: 8px 0;"><strong>Reason:</strong> {{ legal.comment }}</p>
|
||
{% if legal.certifier_name %}<p style="margin: 8px 0;"><strong>Rejected By:</strong> {{ legal.certifier_name }}</p>{% endif %}
|
||
{% if legal.date %}<p style="margin: 8px 0;"><strong>Date:</strong> {{ legal.date }}</p>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if ia_cc.comment %}
|
||
<div style="margin: 15px 0; padding: 15px; border-left: 4px solid #d32f2f; background-color: #ffebee; border-radius: 0 4px 4px 0;">
|
||
<div style="font-weight: bold; color: #d32f2f; margin-bottom: 10px;">🔍 IA&CC REJECTION</div>
|
||
<p style="margin: 8px 0;"><strong>Reason:</strong> {{ ia_cc.comment }}</p>
|
||
{% if ia_cc.certifier_name %}<p style="margin: 8px 0;"><strong>Rejected By:</strong> {{ ia_cc.certifier_name }}</p>{% endif %}
|
||
{% if ia_cc.date %}<p style="margin: 8px 0;"><strong>Date:</strong> {{ ia_cc.date }}</p>{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if not approver.comment and not legal.comment and not ia_cc.comment %}
|
||
<div style="color: #999; font-style: italic; padding: 10px;">(No specific rejection comments available)</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<div style="color: #999; font-style: italic; padding: 10px;">(No rejection details available)</div>
|
||
{% endif %}
|
||
#}
|
||
</div>
|
||
{% endfor %}
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<strong>📌 Next Steps:</strong> Please review and rework the above assets according to the rejection comments.
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">All rejected assets have been downloaded to Box Revisions folder (349441822875). Campaign status updated A5 → A6.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a5_to_a6_partial': {
|
||
'subject': "⚠️ Partial Rework Download - Campaign {campaign_name}",
|
||
'html': """
|
||
<h2 style="color: orange;">Rework Campaign Partially Processed (A5→A6)</h2>
|
||
<p><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_id }})</p>
|
||
<p><strong>Campaign Number:</strong> {{ campaign_number }}</p>
|
||
<p><strong>Total NOT APPROVED:</strong> {{ total_assets }}</p>
|
||
<p><strong>Successful:</strong> {{ successful }}</p>
|
||
<p><strong>Failed:</strong> {{ failed }}</p>
|
||
<hr>
|
||
|
||
{% if successful > 0 %}
|
||
<h3 style="color: green;">✅ Successfully Processed ({{ successful }}):</h3>
|
||
<ul>
|
||
{% for asset in rejected_assets %}
|
||
<li><strong>{{ asset.asset_name }}</strong>
|
||
<br>Tracking ID: <code>{{ asset.tracking_id }}</code>{% if asset.is_existing %} <span style="color: #007bff;">(Updated existing)</span>{% endif %}
|
||
<br>Box URL: <a href="{{ asset.box_url }}">{{ asset.box_url }}</a>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
|
||
{% if failed > 0 %}
|
||
<h3 style="color: red;">❌ Failed Assets ({{ failed }}):</h3>
|
||
<ul>
|
||
{% for asset in failed_assets %}
|
||
<li><strong>{{ asset.asset_name }}</strong>
|
||
<br><span style="color: red;">Error: {{ asset.error }}</span>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
{% endif %}
|
||
|
||
<hr>
|
||
<p style="color: red;"><strong>Status NOT updated.</strong> Campaign remains at A5.</p>
|
||
<p>The script will retry failed assets on the next run (every 5 minutes).</p>
|
||
"""
|
||
},
|
||
'a5_to_a6_no_rejections': {
|
||
'subject': "✅ No Rework Required - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #28a745; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">✅ No Rework Required</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Total assets checked:</strong> {{ total_assets }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Approved/other status:</strong> {{ skipped_count }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>NOT APPROVED assets:</strong> 0</p>
|
||
</div>
|
||
|
||
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
|
||
<h3 style="color: #28a745; margin-top: 0;">Good News!</h3>
|
||
<p>All assets in the Final Assets folder are approved or have other status.</p>
|
||
<p>No assets with "NOT APPROVED" status were found, so no rework is required.</p>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>📌 Note:</strong></p>
|
||
<ul style="margin: 10px 0;">
|
||
<li>No downloads were performed</li>
|
||
<li>Status NOT updated (remains A5)</li>
|
||
<li>Script will check again on next run</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">A5→A6 script completed successfully with no rejected assets found.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a1_to_a2_no_assets': {
|
||
'subject': "⚠️ No Assets Found - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">⚠️ No Master Assets Found</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A1</p>
|
||
</div>
|
||
|
||
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
|
||
<h3 style="color: #ff9800; margin-top: 0;">Campaign Set to A1 but No Assets Found</h3>
|
||
<p>The Master Assets folder was searched (including subfolders) but no assets were found.</p>
|
||
<p>This campaign is set to status A1 but appears to have no master assets ready for download.</p>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>📌 What Happens Next:</strong></p>
|
||
<ul style="margin: 10px 0;">
|
||
<li>No downloads were performed</li>
|
||
<li>Status NOT updated (remains A1)</li>
|
||
<li>Script will check again on next run (every 5 minutes)</li>
|
||
<li>Please verify assets exist in Master Assets folder</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">A1→A2 script completed with no assets to process.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a1_to_a2_no_assets_retry': {
|
||
'subject': "⚠️ No Assets Found (Attempt {retry_count}/3) - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">⚠️ No Master Assets Found (Retry {{ retry_count }}/{{ max_retries }})</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A1</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Retry Attempt:</strong> {{ retry_count }} of {{ max_retries }}</p>
|
||
</div>
|
||
|
||
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
|
||
<h3 style="color: #ff9800; margin-top: 0;">Campaign Set to A1 but No Assets Found</h3>
|
||
<p>The Master Assets folder was searched (including subfolders) but no assets were found.</p>
|
||
<p>This campaign is set to status A1 but appears to have no master assets ready for download.</p>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>📌 What Happens Next:</strong></p>
|
||
<ul style="margin: 10px 0;">
|
||
<li>This is attempt <strong>{{ retry_count }}</strong> of <strong>{{ max_retries }}</strong></li>
|
||
<li>System will retry automatically on next run (every 3 minutes)</li>
|
||
{% if retry_count < max_retries %}
|
||
<li><strong>{{ max_retries - retry_count }} attempt(s) remaining</strong> before marking as permanently failed</li>
|
||
{% else %}
|
||
<li style="color: #d32f2f;"><strong>WARNING: This is the final attempt!</strong> Next failure will mark campaign as permanently failed.</li>
|
||
{% endif %}
|
||
<li>Please verify assets exist in Master Assets folder</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">A1→A2 script will retry automatically. No action needed unless this persists.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a1_to_a2_no_assets_warning': {
|
||
'subject': "⚠️ Campaign in A1 with no assets yet - {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">⚠️ Campaign in A1 with No Assets Yet</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A1</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Polls with empty folder:</strong> {{ poll_count }}</p>
|
||
</div>
|
||
|
||
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
|
||
<h3 style="color: #ff9800; margin-top: 0;">Master Assets Folder Has Been Empty for ~1 Hour</h3>
|
||
<p>This campaign has been at status A1 for roughly an hour with no master assets in the folder.</p>
|
||
<p>This is often expected — the folder may have been created before assets were uploaded — and the system will keep checking automatically.</p>
|
||
<p>This is a <strong>one-time warning</strong>; no further emails will be sent for this campaign.</p>
|
||
</div>
|
||
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>📌 Action only needed if:</strong></p>
|
||
<ul style="margin: 10px 0;">
|
||
<li>You expected assets to be uploaded already</li>
|
||
<li>The campaign was set to A1 by mistake (change the status in DAM)</li>
|
||
</ul>
|
||
<p style="margin: 10px 0 0 0;">Otherwise no action needed — processing will start automatically as soon as assets appear in the Master Assets folder.</p>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">A1→A2 script will continue to check silently every 3 minutes.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a1_to_a2_permanently_failed': {
|
||
'subject': "❌ PERMANENTLY FAILED - Campaign {campaign_name} (No Assets After 3 Attempts)",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #d32f2f; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">❌ CAMPAIGN PERMANENTLY FAILED</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A1</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Failed Attempts:</strong> {{ retry_count }} / {{ max_retries }}</p>
|
||
</div>
|
||
|
||
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
|
||
<h3 style="color: #d32f2f; margin-top: 0;">Campaign Marked as Permanently Failed</h3>
|
||
<p>After {{ max_retries }} consecutive attempts, the system was unable to find any master assets in the Master Assets folder.</p>
|
||
<p><strong>This campaign will no longer be processed automatically.</strong></p>
|
||
</div>
|
||
|
||
<div style="background-color: #ffebee; border-left: 4px solid #d32f2f; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>🔧 Required Actions:</strong></p>
|
||
<ol style="margin: 10px 0;">
|
||
<li>Verify the campaign should actually be in A1 status</li>
|
||
<li>Check if Master Assets folder exists and contains files</li>
|
||
<li>If this is a mistake, change campaign status to something else</li>
|
||
<li>If assets need to be added, add them to Master Assets folder</li>
|
||
<li><strong>Once fixed, manually reset the retry counter</strong></li>
|
||
</ol>
|
||
</div>
|
||
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>💡 How to Reset This Campaign:</strong></p>
|
||
<p style="margin: 10px 0; padding: 15px; background-color: white; border-radius: 4px;">
|
||
To reset the status and retry this campaign, please contact support at: <br>
|
||
<strong><a href="mailto:optical@oliver.agency" style="color: #1976d2;">optical@oliver.agency</a></strong>
|
||
</p>
|
||
<p style="margin: 5px 0 0 0; font-size: 12px; color: #666;">Support will reset the retry counter and investigate the issue.</p>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">Automated processing stopped. Manual intervention required.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'b1_to_b2_no_assets': {
|
||
'subject': "⚠️ No Assets Found - Global Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">⚠️ No Global Master Assets Found</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign ID:</strong> {{ campaign_id }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> B1</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Campaign Type:</strong> Global Masters</p>
|
||
</div>
|
||
|
||
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
|
||
<h3 style="color: #ff9800; margin-top: 0;">Campaign Set to B1 but No Assets Found</h3>
|
||
<p>The Final Assets folder was searched (including subfolders) but no assets were found.</p>
|
||
<p>This Global Masters campaign is set to status B1 but appears to have no assets ready for download.</p>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>📌 What Happens Next:</strong></p>
|
||
<ul style="margin: 10px 0;">
|
||
<li>No downloads were performed</li>
|
||
<li>Status NOT updated (remains B1)</li>
|
||
<li>Script will check again on next run (every 5 minutes)</li>
|
||
<li>Please verify assets exist in Final Assets folder</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">B1→B2 script completed with no assets to process.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a4_webhook_sent': {
|
||
'subject': "📢 A4 Campaign Webhook Sent - {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #607d8b; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">📢 A4 Campaign Webhook Sent</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #eceff1; border-left: 4px solid #607d8b; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Campaign:</strong> {{ campaign_name }} ({{ campaign_number }})</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Status:</strong> A4 (Not Going Live)</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Live Campaign:</strong> <span style="color: #d32f2f; font-weight: bold;">NO</span></p>
|
||
</div>
|
||
|
||
<div style="padding: 20px; background-color: #f8f9fa; border-radius: 4px; margin: 20px 0;">
|
||
<h3 style="color: #607d8b; margin-top: 0;">Webhook Notification Sent</h3>
|
||
<p>A webhook notification has been sent indicating this campaign is marked as A4 and will not go live.</p>
|
||
<p><strong>Webhook URL:</strong> <code style="font-size: 11px;">{{ webhook_url }}</code></p>
|
||
</div>
|
||
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Payload Sent:</strong></p>
|
||
<ul style="margin: 10px 0;">
|
||
<li>campaign_id: {{ campaign_id }}</li>
|
||
<li>campaign_number: {{ campaign_number }}</li>
|
||
<li>campaign_name: {{ campaign_name }}</li>
|
||
<li>status: A4</li>
|
||
<li><strong>live_campaign: NO</strong></li>
|
||
</ul>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">A4 webhook monitor completed successfully.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'daily_report': {
|
||
'subject': "📊 Ferrero Automation Daily Report - {report_date}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 1000px; margin: 0 auto;">
|
||
<div style="background-color: #1976d2; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">📊 Ferrero Automation Daily Report</h1>
|
||
<p style="margin: 10px 0 0 0; opacity: 0.9;">{{ report_date }} - Generated at {{ report_time }}</p>
|
||
</div>
|
||
|
||
<!-- Overall Summary -->
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #1976d2; padding: 15px; margin: 20px 0;">
|
||
<h2 style="margin: 0 0 10px 0; color: #1976d2;">📈 Overall Summary (Last 24 Hours)</h2>
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-top: 10px;">
|
||
<div><strong>Campaigns Found:</strong> {{ total_stats.campaigns_found }}</div>
|
||
<div><strong>Campaigns Processed:</strong> {{ total_stats.campaigns_processed }}</div>
|
||
<div><strong>✅ Completed:</strong> <span style="color: #28a745;">{{ total_stats.campaigns_completed }}</span></div>
|
||
<div><strong>⚠️ Partial:</strong> <span style="color: #ff9800;">{{ total_stats.campaigns_partial }}</span></div>
|
||
<div><strong>No Assets:</strong> {{ total_stats.campaigns_no_assets }}</div>
|
||
<div><strong>Total Assets:</strong> {{ total_stats.total_assets }}</div>
|
||
<div><strong>✓ Successful:</strong> <span style="color: #28a745;">{{ total_stats.assets_successful }}</span></div>
|
||
<div><strong>✗ Failed:</strong> <span style="color: #d32f2f;">{{ total_stats.assets_failed }}</span></div>
|
||
{% if total_stats.assets_skipped > 0 %}<div><strong>Skipped (Approved):</strong> {{ total_stats.assets_skipped }}</div>{% endif %}
|
||
{% if total_stats.not_approved_count > 0 %}<div><strong>🔴 NOT APPROVED:</strong> {{ total_stats.not_approved_count }}</div>{% endif %}
|
||
</div>
|
||
{% if total_stats.total_assets > 0 %}
|
||
<div style="margin-top: 15px; padding: 10px; background-color: white; border-radius: 4px;">
|
||
<strong>Success Rate:</strong>
|
||
<span style="font-size: 24px; font-weight: bold; color: {% if total_stats.success_rate >= 95 %}#28a745{% elif total_stats.success_rate >= 80 %}#ff9800{% else %}#d32f2f{% endif %};">
|
||
{{ "%.1f"|format(total_stats.success_rate) }}%
|
||
</span>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<!-- Workflow Breakdown -->
|
||
<h2 style="margin: 30px 0 20px 0; color: #333;">🔄 Workflow Breakdown</h2>
|
||
|
||
{% for workflow_name, stats in workflow_stats.items() %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 0; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #424242; color: white; padding: 12px 15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ workflow_name }}</strong>
|
||
</div>
|
||
<div style="padding: 15px;">
|
||
{% if stats.campaigns_processed > 0 %}
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px;">
|
||
<div><strong>Campaigns:</strong> {{ stats.campaigns_processed }}</div>
|
||
<div><strong>Assets:</strong> {{ stats.total_assets }}</div>
|
||
<div style="color: #28a745;"><strong>✓ Successful:</strong> {{ stats.assets_successful }}</div>
|
||
<div style="color: #d32f2f;"><strong>✗ Failed:</strong> {{ stats.assets_failed }}</div>
|
||
{% if stats.assets_skipped > 0 %}<div><strong>Skipped:</strong> {{ stats.assets_skipped }}</div>{% endif %}
|
||
{% if stats.not_approved_count > 0 %}<div style="color: #d32f2f;"><strong>NOT APPROVED:</strong> {{ stats.not_approved_count }}</div>{% endif %}
|
||
</div>
|
||
|
||
{% if stats.campaign_details %}
|
||
<details style="margin-top: 10px;">
|
||
<summary style="cursor: pointer; font-weight: bold; color: #1976d2;">View Campaign Details ({{ stats.campaign_details|length }})</summary>
|
||
<div style="margin-top: 10px;">
|
||
{% for campaign in stats.campaign_details %}
|
||
<div style="padding: 8px; margin: 5px 0; background-color: white; border-left: 3px solid {% if campaign.status == 'completed' %}#28a745{% elif campaign.status == 'partial' %}#ff9800{% else %}#999{% endif %}; border-radius: 2px;">
|
||
<strong>{{ campaign.name }}</strong> ({{ campaign.number }})
|
||
<br><small>Total: {{ campaign.total_assets }}, Success: {{ campaign.successful }}, Failed: {{ campaign.failed }}{% if campaign.get('skipped') %}, Skipped: {{ campaign.skipped }}{% endif %}</small>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</details>
|
||
{% endif %}
|
||
|
||
{% if stats.errors %}
|
||
<details style="margin-top: 10px;">
|
||
<summary style="cursor: pointer; font-weight: bold; color: #d32f2f;">⚠️ Errors ({{ stats.errors|length }})</summary>
|
||
<div style="margin-top: 10px;">
|
||
{% for error in stats.errors[:10] %}
|
||
<div style="padding: 5px; margin: 3px 0; background-color: #ffebee; border-radius: 2px; font-size: 12px;">
|
||
{{ error }}
|
||
</div>
|
||
{% endfor %}
|
||
{% if stats.errors|length > 10 %}
|
||
<div style="padding: 5px; margin: 3px 0; font-style: italic; color: #666;">
|
||
... and {{ stats.errors|length - 10 }} more errors
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
</details>
|
||
{% endif %}
|
||
{% else %}
|
||
<p style="color: #999; font-style: italic; margin: 0;">No activity in the last 24 hours</p>
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
<!-- Footer -->
|
||
<div style="background-color: #f8f9fa; padding: 15px; margin: 20px 0; border-radius: 4px; text-align: center;">
|
||
<p style="margin: 0; color: #666;">
|
||
<strong>📌 Automation Scripts:</strong> A1→A2, A2→A3, A5→A6, B1→B2
|
||
</p>
|
||
<p style="margin: 5px 0 0 0; color: #666; font-size: 12px;">
|
||
Scripts run every 5 minutes | Report generated daily at 7:00 PM
|
||
</p>
|
||
</div>
|
||
</div>
|
||
"""
|
||
},
|
||
'creativex_complete': {
|
||
'subject': "✅ CreativeX Scores Extracted - {file_count} files processed",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #9c27b0; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">✅ CreativeX Score Extraction Complete</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #f3e5f5; border-left: 4px solid #9c27b0; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Files Processed:</strong> {{ file_count }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Scores Extracted:</strong> {{ success_count }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Source:</strong> Box Folder 350605024645</p>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 30px; color: #333;">Extracted Scores:</h3>
|
||
{% for score in processed_files %}
|
||
<div style="border: 1px solid #ddd; margin: 15px 0; padding: 15px; background-color: #fafafa; border-radius: 4px;">
|
||
<div style="background-color: #9c27b0; color: white; padding: 10px 15px; margin: -15px -15px 15px -15px; border-radius: 4px 4px 0 0;">
|
||
<strong>{{ score.filename }}</strong>
|
||
{% if score.version_number > 1 %}<span style="float: right; background-color: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 3px; font-size: 12px;">Version {{ score.version_number }}{% if score.is_update %} (Updated){% endif %}</span>{% endif %}
|
||
</div>
|
||
<div style="padding: 10px; background-color: white; border-radius: 4px;">
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Quality Score:</span> <span style="font-size: 20px; color: #9c27b0;">{{ score.quality_score }}</span></p>
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX ID:</span> {{ score.creativex_id }}</p>
|
||
{% if score.creativex_url %}<p style="margin: 5px 0;"><span style="font-weight: bold;">CreativeX URL:</span> <a href="{{ score.creativex_url }}">{{ score.creativex_url }}</a></p>{% endif %}
|
||
<p style="margin: 5px 0;"><span style="font-weight: bold;">Box File ID:</span> {{ score.box_file_id }}</p>
|
||
{% if score.version_number > 1 %}<p style="margin: 5px 0; color: #ff9800;"><span style="font-weight: bold;">📝 Note:</span> This is version {{ score.version_number }} of this file (previous versions preserved in database)</p>{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
|
||
<div style="background-color: #f3e5f5; border-left: 4px solid #9c27b0; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>✓ Complete:</strong> All scores extracted and stored in database.</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Files Removed:</strong> Processed PDFs deleted from Box folder.</p>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">CreativeX scores stored with full JSON for future lookups.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'creativex_partial': {
|
||
'subject': "⚠️ CreativeX Extraction Partial - {success_count}/{file_count} successful",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #ff9800; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">⚠️ CreativeX Extraction Partially Complete</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Total Files:</strong> {{ file_count }}</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>✓ Successful:</strong> <span style="color: #28a745;">{{ success_count }}</span></p>
|
||
<p style="margin: 5px 0 0 0;"><strong>✗ Failed:</strong> <span style="color: #d32f2f;">{{ failed_count }}</span></p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Source:</strong> Box Folder 350605024645</p>
|
||
</div>
|
||
|
||
{% if processed_files %}
|
||
<h3 style="margin-top: 30px; color: #28a745;">✅ Successful Extractions ({{ success_count }}):</h3>
|
||
{% for score in processed_files %}
|
||
<div style="border: 1px solid #c8e6c9; margin: 10px 0; padding: 12px; background-color: #f1f8e9; border-radius: 4px;">
|
||
<strong>{{ score.filename }}</strong> - Score: {{ score.quality_score }}
|
||
{% if score.version_number > 1 %}<span style="color: #ff9800; font-size: 12px;"> (Version {{ score.version_number }})</span>{% endif %}
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
{% if failed_files %}
|
||
<h3 style="margin-top: 30px; color: #d32f2f;">❌ Failed Extractions ({{ failed_count }}):</h3>
|
||
{% for file in failed_files %}
|
||
<div style="border: 1px solid #ffcdd2; margin: 10px 0; padding: 12px; background-color: #ffebee; border-radius: 4px;">
|
||
<strong>{{ file.filename }}</strong>
|
||
<br><small style="color: #666;">Error: {{ file.error }}</small>
|
||
</div>
|
||
{% endfor %}
|
||
{% endif %}
|
||
|
||
<div style="background-color: #fff3e0; border-left: 4px solid #ff9800; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>⚠️ Action Required:</strong> Review failed extractions above.</p>
|
||
<p style="margin: 5px 0 0 0;">Failed files remain in Box folder for retry.</p>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">Successful scores stored in database. Failed files not deleted from Box.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'creativex_no_files': {
|
||
'subject': "ℹ️ CreativeX Extraction - No files found",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
<div style="background-color: #607d8b; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0;">
|
||
<h1 style="margin: 0;">ℹ️ CreativeX Extraction - No Files</h1>
|
||
</div>
|
||
|
||
<div style="background-color: #eceff1; border-left: 4px solid #607d8b; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>Status:</strong> No PDF files found</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Source:</strong> Box Folder 350605024645</p>
|
||
<p style="margin: 5px 0 0 0;"><strong>Run Time:</strong> {{ timestamp }}</p>
|
||
</div>
|
||
|
||
<div style="background-color: #e3f2fd; border-left: 4px solid #2196f3; padding: 15px; margin: 20px 0;">
|
||
<p style="margin: 0;"><strong>ℹ️ Note:</strong> This is expected behavior when no new PDFs are ready for processing.</p>
|
||
<p style="margin: 5px 0 0 0;">Upload PDFs to Box folder 350605024645 to process CreativeX scores.</p>
|
||
</div>
|
||
|
||
<p style="color: #666; font-size: 12px; margin-top: 20px;">Script completed successfully with no errors.</p>
|
||
</div>
|
||
"""
|
||
},
|
||
'a5_to_a6_no_rejections': {
|
||
'subject': "✅ No Rework Required - Campaign {campaign_name}",
|
||
'html': """
|
||
<div style="font-family: Arial, sans-serif; max-width: 900px; margin: 0 auto;">
|
||
# ... (template content skipped for brevity)
|
||
</div>
|
||
"""
|
||
}
|
||
}
|
||
except Exception as e:
|
||
logger.error("Error creating templates: {}".format(str(e)))
|
||
return
|
||
|
||
try:
|
||
# 1. Prepare message content
|
||
template = templates.get(template_name)
|
||
if not template:
|
||
logger.error("Email template not found: {}".format(template_name))
|
||
return
|
||
|
||
jinja_template = Template(template['html'])
|
||
html_content = jinja_template.render(data)
|
||
subject = template['subject'].format(**data)
|
||
|
||
# 2. Send via Mailgun API or SMTP
|
||
recipient_list = recipients if isinstance(recipients, list) else [recipients]
|
||
|
||
if self.mailgun_api_key and self.mailgun_domain:
|
||
self._send_via_mailgun_api(recipient_list, subject, html_content, attachments)
|
||
else:
|
||
self._send_via_smtp(recipient_list, subject, html_content, attachments)
|
||
|
||
logger.info("Email sent to {} (Template: {})".format(recipients, template_name))
|
||
|
||
except Exception as e:
|
||
logger.error("Failed to send email: {}".format(str(e)))
|
||
|
||
def _send_via_mailgun_api(self, recipient_list, subject, html_content, attachments=None):
|
||
"""Send email via Mailgun REST API - sends one request per recipient for reliable delivery"""
|
||
import os
|
||
url = "https://api.mailgun.net/v3/{}/messages".format(self.mailgun_domain)
|
||
|
||
# Normalize: split any comma-separated strings into individual addresses
|
||
normalized = []
|
||
for r in recipient_list:
|
||
for addr in r.split(','):
|
||
addr = addr.strip()
|
||
if addr:
|
||
normalized.append(addr)
|
||
|
||
for recipient in normalized:
|
||
files = []
|
||
try:
|
||
if attachments:
|
||
for file_path in attachments:
|
||
if os.path.exists(file_path):
|
||
files.append(("attachment", (os.path.basename(file_path), open(file_path, "rb"))))
|
||
else:
|
||
logger.warning("Attachment not found: {}".format(file_path))
|
||
|
||
data = {
|
||
"from": self.mailgun_sender,
|
||
"to": [recipient],
|
||
"subject": subject,
|
||
"html": html_content,
|
||
}
|
||
|
||
response = requests.post(
|
||
url,
|
||
auth=("api", self.mailgun_api_key),
|
||
data=data,
|
||
files=files if files else None,
|
||
)
|
||
response.raise_for_status()
|
||
logger.info("Mailgun API sent to {}: {}".format(recipient, response.json()))
|
||
except Exception as e:
|
||
logger.error("Mailgun API failed for {}: {}".format(recipient, str(e)))
|
||
finally:
|
||
for _, file_tuple in files:
|
||
file_tuple[1].close()
|
||
|
||
def _send_via_smtp(self, recipient_list, subject, html_content, attachments=None):
|
||
"""Send email via SMTP"""
|
||
import os
|
||
from email.mime.base import MIMEBase
|
||
from email import encoders
|
||
|
||
if attachments:
|
||
message = MIMEMultipart()
|
||
message['From'] = self.sender_email
|
||
message['To'] = ", ".join(recipient_list)
|
||
message['Subject'] = subject
|
||
message.attach(MIMEText(html_content, "html"))
|
||
|
||
for file_path in attachments:
|
||
try:
|
||
if os.path.exists(file_path):
|
||
with open(file_path, "rb") as attachment:
|
||
part = MIMEBase("application", "octet-stream")
|
||
part.set_payload(attachment.read())
|
||
encoders.encode_base64(part)
|
||
filename = os.path.basename(file_path)
|
||
part.add_header(
|
||
"Content-Disposition",
|
||
"attachment; filename= {}".format(filename),
|
||
)
|
||
message.attach(part)
|
||
logger.info("Attached file: {}".format(filename))
|
||
else:
|
||
logger.warning("Attachment not found: {}".format(file_path))
|
||
except Exception as e:
|
||
logger.error("Failed to attach file {}: {}".format(file_path, str(e)))
|
||
else:
|
||
message = MIMEText(html_content, "html")
|
||
message['From'] = self.sender_email
|
||
message['To'] = ", ".join(recipient_list)
|
||
message['Subject'] = subject
|
||
|
||
with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
|
||
server.starttls()
|
||
server.login(self.smtp_user, self.smtp_password)
|
||
server.send_message(message)
|
||
|
||
def send_webhook(self, url, payload):
|
||
"""
|
||
url: Webhook URL
|
||
payload: dict to send as JSON
|
||
|
||
Returns:
|
||
bool: Success status
|
||
"""
|
||
try:
|
||
# Get webhook config if exists
|
||
webhook_config = None
|
||
for name, config in self.webhook_config.items():
|
||
if config.get('url') == url:
|
||
webhook_config = config
|
||
break
|
||
|
||
if not webhook_config:
|
||
webhook_config = {'timeout_seconds': 10, 'auth': {}}
|
||
|
||
# Prepare headers
|
||
headers = {'Content-Type': 'application/json'}
|
||
|
||
# Add auth if configured
|
||
auth_config = webhook_config.get('auth', {})
|
||
if auth_config.get('type') == 'bearer' and auth_config.get('token'):
|
||
headers['Authorization'] = 'Bearer {}'.format(auth_config['token'])
|
||
elif auth_config.get('type') == 'basic':
|
||
# Could add basic auth here if needed
|
||
pass
|
||
|
||
# Send webhook
|
||
response = requests.post(
|
||
url,
|
||
json=payload,
|
||
headers=headers,
|
||
timeout=webhook_config.get('timeout_seconds', 10)
|
||
)
|
||
|
||
if response.status_code in [200, 201, 202]:
|
||
logger.info("Webhook sent successfully: {}".format(url))
|
||
return True
|
||
else:
|
||
logger.warning("Webhook failed: HTTP {} - {}".format(
|
||
response.status_code, response.text[:200]
|
||
))
|
||
return False
|
||
|
||
except Exception as e:
|
||
logger.error("Webhook error: {}".format(str(e)))
|
||
return False
|