feat: Enhance ExportManager with landscape PDF support and improved graph handling
This commit is contained in:
@@ -18,11 +18,12 @@ from xml.etree.ElementTree import Element, SubElement, tostring
|
||||
|
||||
import pandas as pd
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.pagesizes import A4
|
||||
from reportlab.lib.pagesizes import A4, landscape
|
||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
|
||||
from reportlab.lib.units import inch
|
||||
from reportlab.platypus import (
|
||||
Image,
|
||||
PageBreak,
|
||||
Paragraph,
|
||||
SimpleDocTemplate,
|
||||
Spacer,
|
||||
@@ -178,13 +179,23 @@ class ExportManager:
|
||||
edgecolor="none",
|
||||
)
|
||||
|
||||
# Verify the file was actually created
|
||||
# Ensure the figure data is properly flushed to disk
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
plt.draw()
|
||||
plt.pause(0.01) # Small pause to ensure file is written
|
||||
|
||||
# Verify the file was actually created and has content
|
||||
if not temp_image_path.exists():
|
||||
self.logger.error(
|
||||
f"Graph image file was not created: {temp_image_path}"
|
||||
)
|
||||
return None
|
||||
|
||||
if temp_image_path.stat().st_size == 0:
|
||||
self.logger.error(f"Graph image file is empty: {temp_image_path}")
|
||||
return None
|
||||
|
||||
self.logger.info(f"Graph image saved successfully: {temp_image_path}")
|
||||
return str(temp_image_path)
|
||||
|
||||
@@ -197,10 +208,10 @@ class ExportManager:
|
||||
try:
|
||||
df = self.data_manager.load_data()
|
||||
|
||||
# Create PDF document
|
||||
# Create PDF document in landscape format for better table/graph display
|
||||
doc = SimpleDocTemplate(
|
||||
export_path,
|
||||
pagesize=A4,
|
||||
pagesize=landscape(A4),
|
||||
rightMargin=72,
|
||||
leftMargin=72,
|
||||
topMargin=72,
|
||||
@@ -252,24 +263,26 @@ class ExportManager:
|
||||
# Include graph if requested and available
|
||||
if include_graph:
|
||||
temp_dir = Path(export_path).parent / "temp_export"
|
||||
graph_path = None
|
||||
|
||||
try:
|
||||
graph_path = self._save_graph_as_image(temp_dir)
|
||||
if graph_path and os.path.exists(graph_path):
|
||||
# Add page break before graph for full page display
|
||||
story.append(PageBreak())
|
||||
|
||||
story.append(
|
||||
Paragraph("Data Visualization", styles["Heading2"])
|
||||
)
|
||||
story.append(Spacer(1, 10))
|
||||
|
||||
# Add graph image
|
||||
img = Image(graph_path, width=6 * inch, height=3.6 * inch)
|
||||
story.append(img)
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
# Clean up temp image
|
||||
os.remove(graph_path)
|
||||
# Full page graph - maintain proportions while maximizing size
|
||||
# Let ReportLab scale proportionally to fit landscape page
|
||||
img = Image(graph_path, width=9 * inch, height=5.4 * inch)
|
||||
story.append(img)
|
||||
else:
|
||||
# Graph not available, add a note instead
|
||||
story.append(PageBreak())
|
||||
story.append(
|
||||
Paragraph("Data Visualization", styles["Heading2"])
|
||||
)
|
||||
@@ -281,11 +294,11 @@ class ExportManager:
|
||||
styles["Normal"],
|
||||
)
|
||||
)
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error including graph in PDF: {str(e)}")
|
||||
# Add error note instead of failing completely
|
||||
story.append(PageBreak())
|
||||
story.append(Paragraph("Data Visualization", styles["Heading2"]))
|
||||
story.append(Spacer(1, 10))
|
||||
story.append(
|
||||
@@ -293,20 +306,15 @@ class ExportManager:
|
||||
f"Graph could not be included: {str(e)}", styles["Normal"]
|
||||
)
|
||||
)
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
finally:
|
||||
# Clean up temp directory
|
||||
if temp_dir.exists():
|
||||
with contextlib.suppress(OSError):
|
||||
temp_dir.rmdir()
|
||||
|
||||
# Add data table if we have data
|
||||
if not df.empty:
|
||||
# Start table on new page
|
||||
story.append(PageBreak())
|
||||
story.append(Paragraph("Data Table", styles["Heading2"]))
|
||||
story.append(Spacer(1, 10))
|
||||
story.append(Spacer(1, 20))
|
||||
|
||||
# Prepare table data - limit columns for better PDF formatting
|
||||
# Prepare table data - include all columns for full display
|
||||
display_columns = ["date"]
|
||||
for pathology_key in self.pathology_manager.get_pathology_keys():
|
||||
display_columns.append(pathology_key)
|
||||
@@ -320,11 +328,8 @@ class ExportManager:
|
||||
]
|
||||
display_df = df[available_columns].copy()
|
||||
|
||||
# Truncate long notes for better table formatting
|
||||
if "note" in display_df.columns:
|
||||
display_df["note"] = display_df["note"].apply(
|
||||
lambda x: (str(x)[:50] + "...") if len(str(x)) > 50 else str(x)
|
||||
)
|
||||
# Don't truncate notes - landscape format has full width
|
||||
# Keep notes as-is for complete data visibility
|
||||
|
||||
# Convert to table data
|
||||
table_data = [available_columns] # Headers
|
||||
@@ -333,28 +338,57 @@ class ExportManager:
|
||||
[str(val) if pd.notna(val) else "" for val in row]
|
||||
)
|
||||
|
||||
# Create table with styling
|
||||
table = Table(table_data, repeatRows=1)
|
||||
# Calculate optimal column widths for landscape format
|
||||
col_widths = []
|
||||
for col in available_columns:
|
||||
if col == "date":
|
||||
col_widths.append(1.0 * inch) # Fixed width for dates
|
||||
elif col == "note":
|
||||
col_widths.append(3.5 * inch) # Wider for notes
|
||||
elif col in self.pathology_manager.get_pathology_keys():
|
||||
col_widths.append(0.8 * inch) # Narrow for pathology scores
|
||||
elif col in self.medicine_manager.get_medicine_keys():
|
||||
col_widths.append(0.8 * inch) # Narrow for medicine status
|
||||
else:
|
||||
col_widths.append(1.0 * inch) # Default width
|
||||
|
||||
# Create table with specified column widths and better styling
|
||||
table = Table(table_data, colWidths=col_widths, repeatRows=1)
|
||||
table.setStyle(
|
||||
TableStyle(
|
||||
[
|
||||
("BACKGROUND", (0, 0), (-1, 0), colors.grey),
|
||||
("TEXTCOLOR", (0, 0), (-1, 0), colors.whitesmoke),
|
||||
("ALIGN", (0, 0), (-1, -1), "CENTER"),
|
||||
# Left align for better readability
|
||||
("ALIGN", (0, 0), (-1, -1), "LEFT"),
|
||||
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||
("FONTSIZE", (0, 0), (-1, 0), 10),
|
||||
("BOTTOMPADDING", (0, 0), (-1, 0), 12),
|
||||
# Add more padding for better readability
|
||||
("LEFTPADDING", (0, 0), (-1, -1), 8),
|
||||
("RIGHTPADDING", (0, 0), (-1, -1), 8),
|
||||
("TOPPADDING", (0, 0), (-1, -1), 6),
|
||||
("BOTTOMPADDING", (0, 0), (-1, -1), 6),
|
||||
("BACKGROUND", (0, 1), (-1, -1), colors.beige),
|
||||
("FONTNAME", (0, 1), (-1, -1), "Helvetica"),
|
||||
("FONTSIZE", (0, 1), (-1, -1), 8),
|
||||
# Slightly larger font for better readability
|
||||
("FONTSIZE", (0, 1), (-1, -1), 9),
|
||||
("GRID", (0, 0), (-1, -1), 1, colors.black),
|
||||
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||
("WORDWRAP", (0, 0), (-1, -1), True),
|
||||
# Alternating row colors for better visual separation
|
||||
(
|
||||
"ROWBACKGROUNDS",
|
||||
(0, 1),
|
||||
(-1, -1),
|
||||
[colors.beige, colors.lightgrey],
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
story.append(table)
|
||||
else:
|
||||
story.append(PageBreak())
|
||||
story.append(
|
||||
Paragraph("No data available to export.", styles["Normal"])
|
||||
)
|
||||
@@ -362,6 +396,21 @@ class ExportManager:
|
||||
# Build PDF
|
||||
doc.build(story)
|
||||
|
||||
# Clean up temporary image file after PDF is built
|
||||
if include_graph:
|
||||
temp_dir = Path(export_path).parent / "temp_export"
|
||||
if graph_path and os.path.exists(graph_path):
|
||||
try:
|
||||
os.remove(graph_path)
|
||||
self.logger.debug(f"Cleaned up temporary image: {graph_path}")
|
||||
except OSError as e:
|
||||
self.logger.warning(f"Could not remove temp image: {e}")
|
||||
|
||||
# Clean up temp directory if empty
|
||||
if temp_dir.exists():
|
||||
with contextlib.suppress(OSError):
|
||||
temp_dir.rmdir()
|
||||
|
||||
self.logger.info(f"Data exported to PDF: {export_path}")
|
||||
return True
|
||||
|
||||
|
||||
531
tests/test_export_manager.py
Normal file
531
tests/test_export_manager.py
Normal file
@@ -0,0 +1,531 @@
|
||||
"""
|
||||
Tests for the ExportManager class.
|
||||
"""
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import pandas as pd
|
||||
|
||||
import sys
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
|
||||
|
||||
from src.export_manager import ExportManager
|
||||
|
||||
|
||||
class TestExportManager:
|
||||
"""Test cases for the ExportManager class."""
|
||||
|
||||
@pytest.fixture
|
||||
def mock_data_manager(self):
|
||||
"""Create a mock data manager with sample data."""
|
||||
mock_dm = Mock()
|
||||
sample_data = pd.DataFrame({
|
||||
'date': ['2025-01-01', '2025-01-02'],
|
||||
'depression': [5, 6],
|
||||
'anxiety': [3, 4],
|
||||
'bupropion': [1, 0],
|
||||
'bupropion_doses': ['09:00:150mg', ''],
|
||||
'note': ['feeling better', 'neutral day']
|
||||
})
|
||||
mock_dm.load_data.return_value = sample_data
|
||||
return mock_dm
|
||||
|
||||
@pytest.fixture
|
||||
def mock_graph_manager(self):
|
||||
"""Create a mock graph manager."""
|
||||
mock_gm = Mock()
|
||||
mock_fig = Mock()
|
||||
mock_gm.fig = mock_fig
|
||||
mock_gm.update_graph = Mock()
|
||||
return mock_gm
|
||||
|
||||
@pytest.fixture
|
||||
def mock_medicine_manager(self):
|
||||
"""Create a mock medicine manager."""
|
||||
mock_mm = Mock()
|
||||
mock_mm.get_medicine_keys.return_value = ['bupropion', 'hydroxyzine']
|
||||
return mock_mm
|
||||
|
||||
@pytest.fixture
|
||||
def mock_pathology_manager(self):
|
||||
"""Create a mock pathology manager."""
|
||||
mock_pm = Mock()
|
||||
mock_pm.get_pathology_keys.return_value = ['depression', 'anxiety']
|
||||
return mock_pm
|
||||
|
||||
@pytest.fixture
|
||||
def export_manager(self, mock_data_manager, mock_graph_manager,
|
||||
mock_medicine_manager, mock_pathology_manager, mock_logger):
|
||||
"""Create an ExportManager instance with mocked dependencies."""
|
||||
return ExportManager(
|
||||
mock_data_manager,
|
||||
mock_graph_manager,
|
||||
mock_medicine_manager,
|
||||
mock_pathology_manager,
|
||||
mock_logger
|
||||
)
|
||||
|
||||
def test_init(self, export_manager, mock_data_manager, mock_graph_manager,
|
||||
mock_medicine_manager, mock_pathology_manager, mock_logger):
|
||||
"""Test ExportManager initialization."""
|
||||
assert export_manager.data_manager == mock_data_manager
|
||||
assert export_manager.graph_manager == mock_graph_manager
|
||||
assert export_manager.medicine_manager == mock_medicine_manager
|
||||
assert export_manager.pathology_manager == mock_pathology_manager
|
||||
assert export_manager.logger == mock_logger
|
||||
|
||||
def test_export_data_to_json_success(self, export_manager):
|
||||
"""Test successful JSON export."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = export_manager.export_data_to_json(temp_path)
|
||||
assert result is True
|
||||
assert os.path.exists(temp_path)
|
||||
|
||||
# Verify file content
|
||||
import json
|
||||
with open(temp_path, 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
assert 'metadata' in data
|
||||
assert 'entries' in data
|
||||
assert data['metadata']['total_entries'] == 2
|
||||
assert len(data['entries']) == 2
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_export_data_to_json_empty_data(self, export_manager):
|
||||
"""Test JSON export with empty data."""
|
||||
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = export_manager.export_data_to_json(temp_path)
|
||||
assert result is False
|
||||
export_manager.logger.warning.assert_called_with("No data to export")
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_export_data_to_xml_success(self, export_manager):
|
||||
"""Test successful XML export."""
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = export_manager.export_data_to_xml(temp_path)
|
||||
assert result is True
|
||||
assert os.path.exists(temp_path)
|
||||
|
||||
# Verify file content
|
||||
with open(temp_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
assert 'thechart_data' in content
|
||||
assert 'metadata' in content
|
||||
assert 'entries' in content
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_export_data_to_xml_empty_data(self, export_manager):
|
||||
"""Test XML export with empty data."""
|
||||
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode='w', suffix='.xml', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = export_manager.export_data_to_xml(temp_path)
|
||||
assert result is False
|
||||
export_manager.logger.warning.assert_called_with("No data to export")
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
@patch('matplotlib.pyplot.draw')
|
||||
@patch('matplotlib.pyplot.pause')
|
||||
def test_save_graph_as_image_success(self, mock_pause, mock_draw, export_manager):
|
||||
"""Test successful graph image saving."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
temp_path = Path(temp_dir)
|
||||
|
||||
# Mock the savefig method
|
||||
export_manager.graph_manager.fig.savefig = Mock()
|
||||
|
||||
# Create a dummy image file to simulate successful save
|
||||
image_path = temp_path / "graph.png"
|
||||
image_path.write_bytes(b"fake image data")
|
||||
|
||||
# Mock the savefig to create the file
|
||||
def mock_savefig(path, **kwargs):
|
||||
Path(path).write_bytes(b"fake image data")
|
||||
|
||||
export_manager.graph_manager.fig.savefig.side_effect = mock_savefig
|
||||
|
||||
result = export_manager._save_graph_as_image(temp_path)
|
||||
|
||||
assert result is not None
|
||||
assert str(temp_path / "graph.png") in result
|
||||
export_manager.graph_manager.update_graph.assert_called_once()
|
||||
|
||||
def test_save_graph_as_image_no_graph_manager(self, export_manager):
|
||||
"""Test graph image saving with no graph manager."""
|
||||
export_manager.graph_manager = None
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = export_manager._save_graph_as_image(Path(temp_dir))
|
||||
|
||||
assert result is None
|
||||
export_manager.logger.warning.assert_called_with(
|
||||
"No graph manager available for export"
|
||||
)
|
||||
|
||||
def test_save_graph_as_image_no_figure(self, export_manager):
|
||||
"""Test graph image saving with no figure."""
|
||||
export_manager.graph_manager.fig = None
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = export_manager._save_graph_as_image(Path(temp_dir))
|
||||
|
||||
assert result is None
|
||||
export_manager.logger.warning.assert_called_with(
|
||||
"No graph figure available for export"
|
||||
)
|
||||
|
||||
def test_save_graph_as_image_empty_data(self, export_manager):
|
||||
"""Test graph image saving with empty data."""
|
||||
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
result = export_manager._save_graph_as_image(Path(temp_dir))
|
||||
|
||||
assert result is None
|
||||
export_manager.logger.warning.assert_called_with(
|
||||
"No data available to update graph for export"
|
||||
)
|
||||
|
||||
@patch('src.export_manager.ExportManager._save_graph_as_image')
|
||||
@patch('src.export_manager.SimpleDocTemplate')
|
||||
def test_export_to_pdf_success(self, mock_doc, mock_save_graph, export_manager):
|
||||
"""Test successful PDF export."""
|
||||
# Mock graph image saving
|
||||
mock_save_graph.return_value = "/tmp/test_graph.png"
|
||||
|
||||
# Mock document building
|
||||
mock_doc_instance = Mock()
|
||||
mock_doc.return_value = mock_doc_instance
|
||||
|
||||
# Mock os.path.exists to return True for the image
|
||||
with patch('os.path.exists', return_value=True):
|
||||
with patch('os.remove'): # Mock cleanup
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = export_manager.export_to_pdf(temp_path, include_graph=True)
|
||||
|
||||
assert result is True
|
||||
mock_doc_instance.build.assert_called_once()
|
||||
export_manager.logger.info.assert_called_with(
|
||||
f"Data exported to PDF: {temp_path}"
|
||||
)
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
@patch('src.export_manager.ExportManager._save_graph_as_image')
|
||||
@patch('src.export_manager.SimpleDocTemplate')
|
||||
def test_export_to_pdf_no_graph(self, mock_doc, mock_save_graph, export_manager):
|
||||
"""Test PDF export without graph."""
|
||||
# Mock document building
|
||||
mock_doc_instance = Mock()
|
||||
mock_doc.return_value = mock_doc_instance
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = export_manager.export_to_pdf(temp_path, include_graph=False)
|
||||
|
||||
assert result is True
|
||||
mock_doc_instance.build.assert_called_once()
|
||||
mock_save_graph.assert_not_called()
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
@patch('src.export_manager.SimpleDocTemplate')
|
||||
def test_export_to_pdf_empty_data(self, mock_doc, export_manager):
|
||||
"""Test PDF export with empty data."""
|
||||
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||
|
||||
# Mock document building
|
||||
mock_doc_instance = Mock()
|
||||
mock_doc.return_value = mock_doc_instance
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = export_manager.export_to_pdf(temp_path, include_graph=False)
|
||||
|
||||
assert result is True
|
||||
mock_doc_instance.build.assert_called_once()
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
@patch('src.export_manager.SimpleDocTemplate')
|
||||
def test_export_to_pdf_exception(self, mock_doc, export_manager):
|
||||
"""Test PDF export with exception."""
|
||||
# Mock document building to raise exception
|
||||
mock_doc.side_effect = Exception("PDF generation failed")
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||
temp_path = f.name
|
||||
|
||||
try:
|
||||
result = export_manager.export_to_pdf(temp_path)
|
||||
|
||||
assert result is False
|
||||
export_manager.logger.error.assert_called()
|
||||
finally:
|
||||
if os.path.exists(temp_path):
|
||||
os.unlink(temp_path)
|
||||
|
||||
def test_get_export_info_with_data(self, export_manager):
|
||||
"""Test getting export info with data."""
|
||||
info = export_manager.get_export_info()
|
||||
|
||||
assert info['total_entries'] == 2
|
||||
assert info['has_data'] is True
|
||||
assert info['date_range']['start'] == '2025-01-01'
|
||||
assert info['date_range']['end'] == '2025-01-02'
|
||||
assert info['pathologies'] == ['depression', 'anxiety']
|
||||
assert info['medicines'] == ['bupropion', 'hydroxyzine']
|
||||
|
||||
def test_get_export_info_empty_data(self, export_manager):
|
||||
"""Test getting export info with empty data."""
|
||||
export_manager.data_manager.load_data.return_value = pd.DataFrame()
|
||||
|
||||
info = export_manager.get_export_info()
|
||||
|
||||
assert info['total_entries'] == 0
|
||||
assert info['has_data'] is False
|
||||
assert info['date_range']['start'] is None
|
||||
assert info['date_range']['end'] is None
|
||||
|
||||
|
||||
class TestExportManagerIntegration:
|
||||
"""Integration tests for ExportManager with real-like scenarios."""
|
||||
|
||||
@pytest.fixture
|
||||
def real_data_manager(self, temp_csv_file, mock_logger):
|
||||
"""Create a data manager with real test data."""
|
||||
from src.medicine_manager import MedicineManager
|
||||
from src.pathology_manager import PathologyManager
|
||||
from src.data_manager import DataManager
|
||||
|
||||
# Create managers with real data
|
||||
medicine_manager = MedicineManager(logger=mock_logger)
|
||||
pathology_manager = PathologyManager(logger=mock_logger)
|
||||
|
||||
# Initialize data manager
|
||||
data_manager = DataManager(temp_csv_file, mock_logger, medicine_manager, pathology_manager)
|
||||
|
||||
# Add some test data
|
||||
test_entries = [
|
||||
['2025-01-01', 5, 3, 6, 7, 1, '09:00:150mg', 0, '', 0, '', 0, '', 0, '', 'feeling better today'],
|
||||
['2025-01-02', 6, 4, 5, 6, 0, '', 1, '22:00:25mg', 0, '', 0, '', 0, '', 'neutral day'],
|
||||
['2025-01-03', 4, 2, 7, 8, 1, '09:00:150mg|21:00:150mg', 0, '', 0, '', 0, '', 0, '', 'good sleep, multiple doses'],
|
||||
]
|
||||
|
||||
for entry in test_entries:
|
||||
data_manager.add_entry(entry)
|
||||
|
||||
return data_manager, medicine_manager, pathology_manager
|
||||
|
||||
@pytest.fixture
|
||||
def real_graph_manager(self, mock_logger):
|
||||
"""Create a real graph manager for testing."""
|
||||
import tkinter as tk
|
||||
import tkinter.ttk as ttk
|
||||
from src.graph_manager import GraphManager
|
||||
from src.medicine_manager import MedicineManager
|
||||
from src.pathology_manager import PathologyManager
|
||||
|
||||
# Create minimal tkinter setup
|
||||
root = tk.Tk()
|
||||
root.withdraw() # Hide window
|
||||
frame = ttk.Frame(root)
|
||||
|
||||
medicine_manager = MedicineManager(logger=mock_logger)
|
||||
pathology_manager = PathologyManager(logger=mock_logger)
|
||||
|
||||
graph_manager = GraphManager(frame, medicine_manager, pathology_manager)
|
||||
|
||||
# Store root for cleanup
|
||||
graph_manager._test_root = root
|
||||
|
||||
return graph_manager
|
||||
|
||||
def test_full_pdf_export_integration(self, real_data_manager, real_graph_manager, mock_logger):
|
||||
"""Test complete PDF export with real managers and improved formatting."""
|
||||
data_manager, medicine_manager, pathology_manager = real_data_manager
|
||||
|
||||
# Create export manager
|
||||
export_manager = ExportManager(
|
||||
data_manager, real_graph_manager, medicine_manager, pathology_manager, mock_logger
|
||||
)
|
||||
|
||||
# Update graph with data
|
||||
df = data_manager.load_data()
|
||||
assert not df.empty, "Test data should be loaded"
|
||||
real_graph_manager.update_graph(df)
|
||||
|
||||
# Test PDF export
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||
temp_pdf_path = f.name
|
||||
|
||||
try:
|
||||
success = export_manager.export_to_pdf(temp_pdf_path, include_graph=True)
|
||||
|
||||
# Verify export success
|
||||
assert success is True, "PDF export should succeed"
|
||||
assert os.path.exists(temp_pdf_path), "PDF file should be created"
|
||||
assert os.path.getsize(temp_pdf_path) > 1000, "PDF should have reasonable size"
|
||||
|
||||
# Check that info log was called for successful export
|
||||
mock_logger.info.assert_any_call(f"Data exported to PDF: {temp_pdf_path}")
|
||||
|
||||
finally:
|
||||
# Cleanup
|
||||
if hasattr(real_graph_manager, '_test_root'):
|
||||
real_graph_manager._test_root.destroy()
|
||||
if os.path.exists(temp_pdf_path):
|
||||
os.unlink(temp_pdf_path)
|
||||
|
||||
def test_pdf_export_with_landscape_format(self, real_data_manager, real_graph_manager, mock_logger):
|
||||
"""Test PDF export uses landscape format and proper dimensions."""
|
||||
data_manager, medicine_manager, pathology_manager = real_data_manager
|
||||
|
||||
export_manager = ExportManager(
|
||||
data_manager, real_graph_manager, medicine_manager, pathology_manager, mock_logger
|
||||
)
|
||||
|
||||
# Update graph with data
|
||||
df = data_manager.load_data()
|
||||
real_graph_manager.update_graph(df)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||
temp_pdf_path = f.name
|
||||
|
||||
try:
|
||||
# Mock the SimpleDocTemplate to verify landscape format
|
||||
with patch('src.export_manager.SimpleDocTemplate') as mock_doc:
|
||||
mock_doc_instance = Mock()
|
||||
mock_doc.return_value = mock_doc_instance
|
||||
|
||||
success = export_manager.export_to_pdf(temp_pdf_path, include_graph=True)
|
||||
|
||||
# Verify SimpleDocTemplate was called with landscape pagesize
|
||||
mock_doc.assert_called_once()
|
||||
call_args = mock_doc.call_args
|
||||
|
||||
# Check that landscape format is used
|
||||
from reportlab.lib.pagesizes import landscape, A4
|
||||
expected_pagesize = landscape(A4)
|
||||
assert call_args[1]['pagesize'] == expected_pagesize
|
||||
|
||||
finally:
|
||||
if hasattr(real_graph_manager, '_test_root'):
|
||||
real_graph_manager._test_root.destroy()
|
||||
if os.path.exists(temp_pdf_path):
|
||||
os.unlink(temp_pdf_path)
|
||||
|
||||
def test_pdf_export_table_formatting(self, real_data_manager, real_graph_manager, mock_logger):
|
||||
"""Test PDF export uses improved table formatting."""
|
||||
data_manager, medicine_manager, pathology_manager = real_data_manager
|
||||
|
||||
export_manager = ExportManager(
|
||||
data_manager, real_graph_manager, medicine_manager, pathology_manager, mock_logger
|
||||
)
|
||||
|
||||
df = data_manager.load_data()
|
||||
real_graph_manager.update_graph(df)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||
temp_pdf_path = f.name
|
||||
|
||||
try:
|
||||
# Mock Table to verify column widths and styling
|
||||
with patch('src.export_manager.Table') as mock_table:
|
||||
mock_table_instance = Mock()
|
||||
mock_table.return_value = mock_table_instance
|
||||
|
||||
with patch('src.export_manager.SimpleDocTemplate') as mock_doc:
|
||||
mock_doc_instance = Mock()
|
||||
mock_doc.return_value = mock_doc_instance
|
||||
|
||||
success = export_manager.export_to_pdf(temp_pdf_path, include_graph=True)
|
||||
|
||||
# Verify Table was called with column widths
|
||||
mock_table.assert_called()
|
||||
call_args = mock_table.call_args
|
||||
|
||||
# Check that colWidths parameter is provided
|
||||
assert 'colWidths' in call_args[1]
|
||||
col_widths = call_args[1]['colWidths']
|
||||
|
||||
# Verify column widths are reasonable
|
||||
assert len(col_widths) > 0
|
||||
from reportlab.lib.units import inch
|
||||
assert all(width > 0.5 * inch for width in col_widths) # All columns at least 0.5"
|
||||
|
||||
finally:
|
||||
if hasattr(real_graph_manager, '_test_root'):
|
||||
real_graph_manager._test_root.destroy()
|
||||
if os.path.exists(temp_pdf_path):
|
||||
os.unlink(temp_pdf_path)
|
||||
|
||||
def test_pdf_export_with_long_notes(self, real_data_manager, real_graph_manager, mock_logger):
|
||||
"""Test PDF export handles long notes without truncation."""
|
||||
data_manager, medicine_manager, pathology_manager = real_data_manager
|
||||
|
||||
# Add entry with very long note
|
||||
long_note = "This is a very long note that would have been truncated in the old system but should now be displayed in full with proper word wrapping and formatting in the improved PDF export system."
|
||||
data_manager.add_entry(['2025-01-04', 3, 2, 5, 6, 0, '', 0, '', 0, '', 0, '', 0, '', long_note])
|
||||
|
||||
export_manager = ExportManager(
|
||||
data_manager, real_graph_manager, medicine_manager, pathology_manager, mock_logger
|
||||
)
|
||||
|
||||
df = data_manager.load_data()
|
||||
real_graph_manager.update_graph(df)
|
||||
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as f:
|
||||
temp_pdf_path = f.name
|
||||
|
||||
try:
|
||||
success = export_manager.export_to_pdf(temp_pdf_path, include_graph=True)
|
||||
|
||||
assert success is True
|
||||
|
||||
# Verify that the long note was not truncated by checking data processing
|
||||
df_processed = data_manager.load_data()
|
||||
note_entry = df_processed[df_processed['date'] == '2025-01-04']['note'].iloc[0]
|
||||
assert long_note in note_entry # Full note should be preserved
|
||||
|
||||
finally:
|
||||
if hasattr(real_graph_manager, '_test_root'):
|
||||
real_graph_manager._test_root.destroy()
|
||||
if os.path.exists(temp_pdf_path):
|
||||
os.unlink(temp_pdf_path)
|
||||
Reference in New Issue
Block a user