DELTA Jira sync excel operational

This commit is contained in:
ValueOn AG 2025-09-12 19:38:13 +02:00
parent 27107c8a67
commit 8259c198e7
6 changed files with 652 additions and 26 deletions

44
force_reauth.py Normal file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""
Script to force Microsoft re-authentication for SharePoint access.
This will disconnect the existing connection and provide a new login URL.
"""
import requests
import json
# Configuration
BASE_URL = "http://localhost:8000" # Adjust if your server runs on different port
CONNECTION_ID = "cc62583d-3a68-44b6-8283-726725916a7e" # From the logs
def force_reauth():
"""Force Microsoft re-authentication by disconnecting and providing new login URL."""
print("🔄 Forcing Microsoft re-authentication for SharePoint access...")
# Step 1: Disconnect existing connection
print(f"1. Disconnecting connection {CONNECTION_ID}...")
disconnect_url = f"{BASE_URL}/api/connections/{CONNECTION_ID}/disconnect"
try:
response = requests.post(disconnect_url)
if response.status_code == 200:
print("✅ Connection disconnected successfully")
else:
print(f"❌ Failed to disconnect: {response.status_code} - {response.text}")
return
except Exception as e:
print(f"❌ Error disconnecting: {e}")
return
# Step 2: Get new login URL
print("2. Getting new Microsoft login URL...")
login_url = f"{BASE_URL}/api/msft/login?state=connection&connectionId={CONNECTION_ID}"
print(f"\n🔗 Please visit this URL to re-authenticate with SharePoint permissions:")
print(f" {login_url}")
print("\nAfter re-authentication, the JIRA sync should work with SharePoint access.")
print("\nNote: The new token will include Sites.ReadWrite.All and Files.ReadWrite.All scopes.")
if __name__ == "__main__":
force_reauth()

View file

@ -406,8 +406,12 @@ class ConnectorSharepoint:
logger.info(f"File copied: {source_file} -> {dest_file}")
except Exception as e:
logger.error(f"Error copying file: {str(e)}")
raise
# Provide more specific error information
error_msg = str(e)
if "itemNotFound" in error_msg or "404" in error_msg:
raise Exception(f"Source file not found (404): {source_path} - {error_msg}")
else:
raise Exception(f"Error copying file: {error_msg}")
async def download_file_by_path(self, site_id: str, file_path: str) -> Optional[bytes]:
"""Download a file by its path within a site."""

View file

@ -2,6 +2,7 @@ from dataclasses import dataclass
from io import BytesIO, StringIO
from typing import Any
import pandas as pd
import openpyxl
from modules.shared.timezoneUtils import get_utc_now
from modules.connectors.connectorSharepoint import ConnectorSharepoint
@ -48,13 +49,21 @@ class TicketSharepointSyncInterface:
timestamp = get_utc_now().strftime("%Y%m%d_%H%M%S")
backup_filename = f"backup_{timestamp}_{self.sync_file}"
await self.connector_sharepoint.copy_file_async(
site_id=self.site_id,
source_folder=self.sync_folder,
source_file=self.sync_file,
dest_folder=self.backup_folder,
dest_file=backup_filename,
)
try:
await self.connector_sharepoint.copy_file_async(
site_id=self.site_id,
source_folder=self.sync_folder,
source_file=self.sync_file,
dest_folder=self.backup_folder,
dest_file=backup_filename,
)
except Exception as e:
# If the source file doesn't exist (404 error), that's okay for first-time sync
if "itemNotFound" in str(e) or "404" in str(e) or "could not be found" in str(e):
raise Exception(f"Source file does not exist - no backup needed: {self.sync_file}")
else:
# Re-raise other errors
raise
async def sync_from_jira_to_csv(self):
"""Syncs tasks from JIRA to a CSV file in SharePoint."""
@ -369,6 +378,296 @@ class TicketSharepointSyncInterface:
# Write audit log to SharePoint
await self._write_audit_log(audit_log, "csv_to_jira")
async def sync_from_jira_to_excel(self):
"""Syncs tasks from JIRA to an Excel file in SharePoint."""
start_time = get_utc_now()
audit_log = []
audit_log.append("=== JIRA TO EXCEL SYNC STARTED ===")
audit_log.append(f"Start Time: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
audit_log.append(f"Sync File: {self.sync_file}")
audit_log.append(f"Sync Folder: {self.sync_folder}")
audit_log.append("")
try:
# 1. Read JIRA tickets
audit_log.append("Step 1: Reading JIRA tickets...")
tickets = await self.connector_ticket.read_tasks(limit=0)
audit_log.append(f"JIRA issues read: {len(tickets)}")
audit_log.append("")
# 2. Transform tasks according to task_sync_definition
audit_log.append("Step 2: Transforming JIRA data...")
transformed_tasks = self._transform_tasks(tickets, include_put=True)
jira_data = [task.data for task in transformed_tasks]
audit_log.append(f"JIRA issues transformed: {len(jira_data)}")
audit_log.append("")
# 3. Create JIRA export file in audit folder
audit_log.append("Step 3: Creating JIRA export file...")
try:
timestamp = get_utc_now().strftime("%Y%m%d_%H%M%S")
jira_export_filename = f"jira_export_{timestamp}.xlsx"
# Use default headers for JIRA export
jira_export_content = self._create_excel_content(jira_data, {"header1": "JIRA Export", "header2": "Raw Data"})
await self.connector_sharepoint.upload_file(
site_id=self.site_id,
folder_path=self.audit_folder,
file_name=jira_export_filename,
content=jira_export_content,
)
audit_log.append(f"JIRA export file created: {jira_export_filename}")
except Exception as e:
audit_log.append(f"Failed to create JIRA export file: {str(e)}")
audit_log.append("")
# 4. Create backup of existing Excel file (if it exists)
audit_log.append("Step 4: Creating backup...")
backup_created = False
try:
await self.create_backup()
backup_created = True
audit_log.append("Backup created successfully")
except Exception as e:
audit_log.append(
f"Backup creation failed (file might not exist): {str(e)}"
)
audit_log.append("")
# 5. Try to read existing Excel file from SharePoint
audit_log.append("Step 5: Reading existing Excel file...")
existing_data = []
existing_file_found = False
existing_headers = {"header1": "Header 1", "header2": "Header 2"}
try:
file_path = f"{self.sync_folder}/{self.sync_file}"
excel_content = await self.connector_sharepoint.download_file_by_path(
site_id=self.site_id, file_path=file_path
)
# Parse Excel file with 4-row structure
existing_data, existing_headers = self._parse_excel_content(excel_content)
existing_file_found = True
audit_log.append(
f"Existing Excel file found with {len(existing_data)} records"
)
audit_log.append(f"Preserved headers: Header1='{existing_headers['header1']}', Header2='{existing_headers['header2']}'")
except Exception as e:
audit_log.append(f"No existing Excel file found or read error: {str(e)}")
audit_log.append("")
# 6. Merge JIRA data with existing data and track changes
audit_log.append("Step 6: Merging JIRA data with existing data...")
merged_data, change_details = self._merge_jira_with_existing_detailed(
jira_data, existing_data
)
# Log detailed changes
audit_log.append(f"Total records after merge: {len(merged_data)}")
audit_log.append(f"Records updated: {change_details['updated']}")
audit_log.append(f"Records added: {change_details['added']}")
audit_log.append(f"Records unchanged: {change_details['unchanged']}")
audit_log.append("")
# Log individual changes
if change_details["changes"]:
audit_log.append("DETAILED CHANGES:")
for change in change_details["changes"]:
audit_log.append(f"- {change}")
audit_log.append("")
# 7. Create Excel with 4-row structure and write to SharePoint
audit_log.append("Step 7: Writing updated Excel to SharePoint...")
excel_content = self._create_excel_content(merged_data, existing_headers)
await self.connector_sharepoint.upload_file(
site_id=self.site_id,
folder_path=self.sync_folder,
file_name=self.sync_file,
content=excel_content,
)
audit_log.append("Excel file successfully written to SharePoint")
audit_log.append("")
# Success summary
end_time = get_utc_now()
duration = (end_time - start_time).total_seconds()
audit_log.append("=== SYNC COMPLETED SUCCESSFULLY ===")
audit_log.append(f"End Time: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
audit_log.append(f"Duration: {duration:.2f} seconds")
audit_log.append(f"Total JIRA issues processed: {len(jira_data)}")
audit_log.append(f"Total records in final Excel: {len(merged_data)}")
except Exception as e:
# Error handling
end_time = get_utc_now()
duration = (end_time - start_time).total_seconds()
audit_log.append("")
audit_log.append("=== SYNC FAILED ===")
audit_log.append(f"Error Time: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
audit_log.append(f"Duration before failure: {duration:.2f} seconds")
audit_log.append(f"Error: {str(e)}")
raise
finally:
# Write audit log to SharePoint
await self._write_audit_log(audit_log, "jira_to_excel")
async def sync_from_excel_to_jira(self):
"""Syncs tasks from an Excel file in SharePoint to JIRA."""
start_time = get_utc_now()
audit_log = []
audit_log.append("=== EXCEL TO JIRA SYNC STARTED ===")
audit_log.append(f"Start Time: {start_time.strftime('%Y-%m-%d %H:%M:%S')}")
audit_log.append(f"Sync File: {self.sync_file}")
audit_log.append(f"Sync Folder: {self.sync_folder}")
audit_log.append("")
try:
# 1. Read Excel file from SharePoint
audit_log.append("Step 1: Reading Excel file from SharePoint...")
try:
file_path = f"{self.sync_folder}/{self.sync_file}"
excel_content = await self.connector_sharepoint.download_file_by_path(
site_id=self.site_id, file_path=file_path
)
# Parse Excel file with 4-row structure
excel_data, _ = self._parse_excel_content(excel_content)
audit_log.append(
f"Excel file read successfully with {len(excel_data)} records"
)
except Exception as e:
audit_log.append(f"Failed to read Excel file: {str(e)}")
audit_log.append("Excel to JIRA sync aborted - no file to process")
return
audit_log.append("")
# 2. Read current JIRA data for comparison
audit_log.append("Step 2: Reading current JIRA data for comparison...")
try:
current_jira_tasks = await self.connector_ticket.read_tasks(limit=0)
current_jira_data = self._transform_tasks(
current_jira_tasks, include_put=True
)
jira_lookup = {
task.data.get("ID"): task.data for task in current_jira_data
}
audit_log.append(f"Current JIRA data read: {len(jira_lookup)} tasks")
except Exception as e:
audit_log.append(f"Failed to read current JIRA data: {str(e)}")
raise
audit_log.append("")
# 3. Detect actual changes in "put" fields
audit_log.append("Step 3: Detecting changes in 'put' fields...")
actual_changes = {}
records_with_changes = 0
total_changes = 0
for row in excel_data:
task_id = row.get("ID")
if not task_id or task_id not in jira_lookup:
continue
current_jira_task = jira_lookup[task_id]
task_changes = {}
for field_name, field_config in self.task_sync_definition.items():
if field_config[0] == "put": # Only process "put" fields
excel_value = row.get(field_name, "")
jira_value = current_jira_task.get(field_name, "")
# Convert None to empty string for comparison
excel_value = "" if excel_value is None else str(excel_value).strip()
jira_value = (
"" if jira_value is None else str(jira_value).strip()
)
# Include if values are different (allow empty strings to clear fields like the reference does)
if excel_value != jira_value:
task_changes[field_name] = excel_value
if task_changes:
actual_changes[task_id] = task_changes
records_with_changes += 1
total_changes += len(task_changes)
audit_log.append(f"Records with actual changes: {records_with_changes}")
audit_log.append(f"Total field changes detected: {total_changes}")
audit_log.append("")
# Log detailed changes
if actual_changes:
audit_log.append("DETAILED CHANGES TO APPLY TO JIRA:")
for task_id, changes in actual_changes.items():
change_list = [
f"{field}: '{value}'" for field, value in changes.items()
]
audit_log.append(f"- Task ID {task_id}: {', '.join(change_list)}")
audit_log.append("")
# 4. Update JIRA tasks with actual changes
if actual_changes:
audit_log.append("Step 4: Updating JIRA tasks...")
# Convert to Task objects for the connector
tasks_to_update = []
for task_id, changes in actual_changes.items():
# Create task data structure expected by JIRA connector
# Build the nested fields structure that JIRA expects
fields = {}
for field_name, new_value in changes.items():
# Map back to JIRA field structure using task_sync_definition
field_config = self.task_sync_definition[field_name]
field_path = field_config[1]
# Extract the JIRA field ID from the path
# For "put" fields, the path is like ['fields', 'customfield_10067']
if len(field_path) >= 2 and field_path[0] == "fields":
jira_field_id = field_path[1]
fields[jira_field_id] = new_value
if fields:
task_data = {"ID": task_id, "fields": fields}
task = Task(data=task_data)
tasks_to_update.append(task)
# Write tasks back to JIRA
try:
await self.connector_ticket.write_tasks(tasks_to_update)
audit_log.append(
f"Successfully updated {len(tasks_to_update)} JIRA tasks"
)
except Exception as e:
audit_log.append(f"Failed to update JIRA tasks: {str(e)}")
raise
else:
audit_log.append("Step 4: No changes to apply to JIRA")
audit_log.append("")
# Success summary
end_time = get_utc_now()
duration = (end_time - start_time).total_seconds()
audit_log.append("=== SYNC COMPLETED SUCCESSFULLY ===")
audit_log.append(f"End Time: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
audit_log.append(f"Duration: {duration:.2f} seconds")
audit_log.append(f"Total Excel records processed: {len(excel_data)}")
audit_log.append(f"Records with actual changes: {records_with_changes}")
audit_log.append(f"JIRA tasks updated: {len(actual_changes)}")
except Exception as e:
# Error handling
end_time = get_utc_now()
duration = (end_time - start_time).total_seconds()
audit_log.append("")
audit_log.append("=== SYNC FAILED ===")
audit_log.append(f"Error Time: {end_time.strftime('%Y-%m-%d %H:%M:%S')}")
audit_log.append(f"Duration before failure: {duration:.2f} seconds")
audit_log.append(f"Error: {str(e)}")
raise
finally:
# Write audit log to SharePoint
await self._write_audit_log(audit_log, "excel_to_jira")
def _transform_tasks(
self, tasks: list[Task], include_put: bool = False
) -> list[Task]:
@ -656,3 +955,158 @@ class TicketSharepointSyncInterface:
csv_text = StringIO()
final_df.to_csv(csv_text, index=False, header=False, quoting=1, escapechar='\\')
return csv_text.getvalue().encode("utf-8")
def _create_excel_content(self, data: list[dict], existing_headers: dict = None) -> bytes:
"""Create Excel content with 4-row structure matching reference code."""
# Get current timestamp for header
timestamp = get_utc_now().strftime("%Y-%m-%d %H:%M:%S UTC")
# Use existing headers if provided, otherwise use defaults
if existing_headers is None:
existing_headers = {"header1": "Header 1", "header2": "Header 2"}
if not data:
# Build an empty table with the expected columns from schema
cols = list(self.task_sync_definition.keys())
df = pd.DataFrame(columns=cols)
# Parse existing headers to extract individual columns
import csv as csv_module
header1_text = existing_headers.get("header1", "Header 1")
header2_text = existing_headers.get("header2", "Header 2")
# Parse the existing header rows
header1_reader = csv_module.reader([header1_text])
header2_reader = csv_module.reader([header2_text])
header1_row = next(header1_reader, [])
header2_row = next(header2_reader, [])
# Row 1: Use existing header1 or default
if len(header1_row) >= len(cols):
header_row1_data = header1_row[:len(cols)]
else:
header_row1_data = header1_row + [""] * (len(cols) - len(header1_row))
header_row1 = pd.DataFrame([header_row1_data], columns=cols)
# Row 2: Use existing header2 and add timestamp to second column
if len(header2_row) >= len(cols):
header_row2_data = header2_row[:len(cols)]
else:
header_row2_data = header2_row + [""] * (len(cols) - len(header2_row))
if len(header_row2_data) > 1:
header_row2_data[1] = timestamp
header_row2 = pd.DataFrame([header_row2_data], columns=cols)
# Row 3: table headers
table_headers = pd.DataFrame([cols], columns=cols)
final_df = pd.concat(
[header_row1, header_row2, table_headers, df], ignore_index=True
)
# Create Excel file in memory
excel_buffer = BytesIO()
final_df.to_excel(excel_buffer, index=False, header=False, engine='openpyxl')
return excel_buffer.getvalue()
# Create DataFrame from data
df = pd.DataFrame(data)
# Force all columns to be object (string) type to preserve empty cells
for column in df.columns:
df[column] = df[column].astype("object")
df[column] = df[column].fillna("")
# Clean data: replace actual line breaks with \n and escape quotes
for column in df.columns:
df[column] = df[column].astype(str).str.replace('\n', '\\n', regex=False)
df[column] = df[column].str.replace('"', '""', regex=False)
# Create the 4-row structure
# Parse existing headers to extract individual columns
import csv as csv_module
header1_text = existing_headers.get("header1", "Header 1")
header2_text = existing_headers.get("header2", "Header 2")
# Parse the existing header rows
header1_reader = csv_module.reader([header1_text])
header2_reader = csv_module.reader([header2_text])
header1_row = next(header1_reader, [])
header2_row = next(header2_reader, [])
# Row 1: Use existing header1 or default
if len(header1_row) >= len(df.columns):
header_row1_data = header1_row[:len(df.columns)]
else:
header_row1_data = header1_row + [""] * (len(df.columns) - len(header1_row))
header_row1 = pd.DataFrame([header_row1_data], columns=df.columns)
# Row 2: Use existing header2 and add timestamp to second column
if len(header2_row) >= len(df.columns):
header_row2_data = header2_row[:len(df.columns)]
else:
header_row2_data = header2_row + [""] * (len(df.columns) - len(header2_row))
if len(header_row2_data) > 1:
header_row2_data[1] = timestamp
header_row2 = pd.DataFrame([header_row2_data], columns=df.columns)
# Row 3: Table headers (column names)
table_headers = pd.DataFrame([df.columns.tolist()], columns=df.columns)
# Concatenate all rows: header1 + header2 + table_headers + data
final_df = pd.concat(
[header_row1, header_row2, table_headers, df], ignore_index=True
)
# Create Excel file in memory
excel_buffer = BytesIO()
final_df.to_excel(excel_buffer, index=False, header=False, engine='openpyxl')
return excel_buffer.getvalue()
def _parse_excel_content(self, excel_content: bytes) -> tuple[list[dict], dict]:
"""Parse Excel content with 4-row structure and return data and headers."""
try:
# Load Excel file from bytes
df = pd.read_excel(
BytesIO(excel_content),
engine='openpyxl',
header=None
)
# Extract the 4 parts:
# Row 1: Static header row 1
header_row1 = df.iloc[0:1].copy()
# Row 2: Static header row 2
header_row2 = df.iloc[1:2].copy()
# Row 3: Table headers
table_headers = df.iloc[2:3].copy()
# Row 4+: Data rows
df_data = df.iloc[3:].copy()
# Set column names from row 3
df_data.columns = table_headers.iloc[0]
# Reset index to start from 0
df_data = df_data.reset_index(drop=True)
# Force all columns to be object (string) type and handle NaN values
for column in df_data.columns:
df_data[column] = df_data[column].astype('object')
# Fill NaN values with empty string to keep cells empty
df_data[column] = df_data[column].fillna('')
# Convert DataFrame to list of dictionaries
data = df_data.to_dict(orient='records')
# Extract headers as strings (like CSV version)
headers = {
"header1": ",".join([str(x) if pd.notna(x) else "" for x in header_row1.iloc[0].tolist()]),
"header2": ",".join([str(x) if pd.notna(x) else "" for x in header_row2.iloc[0].tolist()])
}
return data, headers
except Exception as e:
raise Exception(f"Failed to parse Excel content: {str(e)}")

View file

@ -44,7 +44,9 @@ SCOPES = [
"Mail.ReadWrite", # Read and write mail
"Mail.Send", # Send mail
"Mail.ReadWrite.Shared", # Access shared mailboxes
"User.Read" # Read user profile
"User.Read", # Read user profile
"Sites.ReadWrite.All", # Read and write all SharePoint sites
"Files.ReadWrite.All" # Read and write all files
]
@router.get("/login")

View file

@ -25,17 +25,29 @@ APP_ENV_TYPE = APP_CONFIG.get("APP_ENV_TYPE", "dev")
class ManagerSyncDelta:
"""Manages JIRA to SharePoint synchronization for Delta Group."""
"""Manages JIRA to SharePoint synchronization for Delta Group.
Supports two sync modes:
- CSV mode: Uses CSV files for synchronization (default)
- Excel mode: Uses Excel (.xlsx) files for synchronization
To change sync mode, use the set_sync_mode() method or modify SYNC_MODE class variable.
"""
SHAREPOINT_SITE_ID = "02830618-4029-4dc8-8d3d-f5168f282249"
SHAREPOINT_SITE_NAME = "SteeringBPM"
SHAREPOINT_SITE_PATH = "SteeringBPM"
SHAREPOINT_MAIN_FOLDER = "/sites/SteeringBPM/Freigegebene Dokumente/General/50 Docs hosted by SELISE"
SHAREPOINT_BACKUP_FOLDER = "/sites/SteeringBPM/Freigegebene Dokumente/General/50 Docs hosted by SELISE/SyncHistory"
SHAREPOINT_AUDIT_FOLDER = "/sites/SteeringBPM/Freigegebene Dokumente/General/50 Docs hosted by SELISE/SyncHistory"
SHAREPOINT_HOSTNAME = "deltasecurityag.sharepoint.com"
SHAREPOINT_MAIN_FOLDER = "/General/50 Docs hosted by SELISE"
SHAREPOINT_BACKUP_FOLDER = "/General/50 Docs hosted by SELISE/SyncHistory"
SHAREPOINT_AUDIT_FOLDER = "/General/50 Docs hosted by SELISE/SyncHistory"
SHAREPOINT_USER_ID = "patrick.motsch@delta.ch"
# Fixed filename for the main CSV file (like original synchronizer)
SYNC_FILE_NAME = "DELTAgroup x SELISE Ticket Exchange List.csv"
# Sync mode: "csv" or "xlsx"
SYNC_MODE = "xlsx" # Can be "csv" or "xlsx"
# File names for different sync modes
SYNC_FILE_CSV = "DELTAgroup x SELISE Ticket Exchange List.csv"
SYNC_FILE_XLSX = "DELTAgroup x SELISE Ticket Exchange List.xlsx"
# JIRA connection parameters (hardcoded for Delta Group)
JIRA_USERNAME = "p.motsch@valueon.ch"
@ -65,6 +77,30 @@ class ManagerSyncDelta:
self.jira_connector = None
self.sharepoint_connector = None
self.target_site = None
def get_sync_file_name(self) -> str:
"""Get the appropriate sync file name based on the sync mode."""
if self.SYNC_MODE == "xlsx":
return self.SYNC_FILE_XLSX
else: # Default to CSV
return self.SYNC_FILE_CSV
def set_sync_mode(self, mode: str) -> bool:
"""Set the sync mode to either 'csv' or 'xlsx'.
Args:
mode: Either 'csv' or 'xlsx'
Returns:
bool: True if mode was set successfully, False if invalid mode
"""
if mode.lower() in ["csv", "xlsx"]:
self.SYNC_MODE = mode.lower()
logger.info(f"Sync mode changed to: {self.SYNC_MODE}")
return True
else:
logger.error(f"Invalid sync mode: {mode}. Must be 'csv' or 'xlsx'")
return False
async def initialize_connectors(self) -> bool:
"""Initialize JIRA and SharePoint connectors."""
@ -159,33 +195,42 @@ class ManagerSyncDelta:
async def sync_jira_to_sharepoint(self) -> bool:
"""Perform the main JIRA to SharePoint synchronization using sophisticated sync logic."""
try:
logger.info("Starting JIRA to SharePoint synchronization")
logger.info(f"Starting JIRA to SharePoint synchronization (Mode: {self.SYNC_MODE})")
# Initialize connectors
if not await self.initialize_connectors():
logger.error("Failed to initialize connectors")
return False
# Get the appropriate sync file name based on mode
sync_file_name = self.get_sync_file_name()
logger.info(f"Using sync file: {sync_file_name}")
# Create the sophisticated sync interface
sync_interface = await TicketSharepointSyncInterface.create(
connector_ticket=self.jira_connector,
connector_sharepoint=self.sharepoint_connector,
task_sync_definition=self.TASK_SYNC_DEFINITION,
sync_folder=self.SHAREPOINT_MAIN_FOLDER,
sync_file=self.SYNC_FILE_NAME,
sync_file=sync_file_name,
backup_folder=self.SHAREPOINT_BACKUP_FOLDER,
audit_folder=self.SHAREPOINT_AUDIT_FOLDER,
site_id=self.target_site['id']
)
# Perform the sophisticated sync
logger.info("Performing JIRA to CSV sync...")
await sync_interface.sync_from_jira_to_csv()
# TODO: Uncomment when CSV to JIRA sync is implemented
#logger.info("Performing CSV to JIRA sync...")
#await sync_interface.sync_from_csv_to_jira()
# Perform the sophisticated sync based on mode
if self.SYNC_MODE == "xlsx":
logger.info("Performing JIRA to Excel sync...")
await sync_interface.sync_from_jira_to_excel()
logger.info("Performing Excel to JIRA sync...")
await sync_interface.sync_from_excel_to_jira()
else: # CSV mode (default)
logger.info("Performing JIRA to CSV sync...")
await sync_interface.sync_from_jira_to_csv()
logger.info("Performing CSV to JIRA sync...")
await sync_interface.sync_from_csv_to_jira()
logger.info("JIRA to SharePoint synchronization completed successfully")
logger.info(f"JIRA to SharePoint synchronization completed successfully (Mode: {self.SYNC_MODE})")
return True
except Exception as e:
@ -205,7 +250,7 @@ async def perform_sync_jira_delta_group() -> bool:
"""
try:
#TODO: ADAPT to prod
if APP_ENV_TYPE != "dev":
if APP_ENV_TYPE != "dev" and APP_ENV_TYPE != "prod":
logger.info("JIRA to SharePoint synchronization: TASK to run only in PROD")
return True

77
test_excel_fix.py Normal file
View file

@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Test script to verify the Excel header parsing fix
"""
import sys
import os
import pandas as pd
from io import BytesIO
# Add the gateway modules to the path
sys.path.append(os.path.join(os.path.dirname(__file__), 'modules'))
from modules.interfaces.interfaceTicketObjects import TicketSharepointSyncInterface
def test_excel_header_parsing():
"""Test the Excel header parsing fix"""
print("=== Testing Excel Header Parsing Fix ===\n")
# Create a mock interface instance
interface = TicketSharepointSyncInterface(
connector_ticket=None,
connector_sharepoint=None,
task_sync_definition={
"ID": ["get", ["id"]],
"Summary": ["get", ["fields", "summary"]],
"Status": ["get", ["fields", "status", "name"]],
"Assignee": ["put", ["fields", "assignee", "displayName"]]
},
sync_folder="test",
sync_file="test.xlsx",
backup_folder="backup",
audit_folder="audit",
site_id="test"
)
# Test data
test_data = [
{"ID": "TEST-1", "Summary": "Test Issue 1", "Status": "Open", "Assignee": "John Doe"},
{"ID": "TEST-2", "Summary": "Test Issue 2", "Status": "Closed", "Assignee": "Jane Smith"},
]
# Create Excel content
print("1. Creating Excel content...")
excel_content = interface._create_excel_content(test_data)
print(f" ✓ Created Excel content: {len(excel_content)} bytes")
# Parse it back
print("2. Parsing Excel content...")
try:
parsed_data, parsed_headers = interface._parse_excel_content(excel_content)
print(f" ✓ Parsed Excel content: {len(parsed_data)} records")
print(f" ✓ Headers type: header1={type(parsed_headers['header1'])}, header2={type(parsed_headers['header2'])}")
print(f" ✓ Headers content: header1='{parsed_headers['header1']}', header2='{parsed_headers['header2']}'")
# Test creating content with the parsed headers
print("3. Testing round-trip with parsed headers...")
new_excel_content = interface._create_excel_content(test_data, parsed_headers)
print(f" ✓ Created new Excel content: {len(new_excel_content)} bytes")
# Parse the new content
final_data, final_headers = interface._parse_excel_content(new_excel_content)
print(f" ✓ Final parse successful: {len(final_data)} records")
print(f" ✓ Final headers: header1='{final_headers['header1']}', header2='{final_headers['header2']}'")
print("\n✅ All tests passed! The header parsing fix works correctly.")
return True
except Exception as e:
print(f" ✗ Error during parsing: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
success = test_excel_header_parsing()
exit(0 if success else 1)