diff --git a/app/utils/template_filters.py b/app/utils/template_filters.py index 24ebc90..b9acbf4 100644 --- a/app/utils/template_filters.py +++ b/app/utils/template_filters.py @@ -104,48 +104,60 @@ def register_template_filters(app): return f"{float(value):,.2f}" except Exception: return str(value) - - def get_logo_base64(logo_path): - """Convert logo file to base64 data URI for PDF embedding""" - import os - import base64 - import mimetypes + + @app.template_filter('timeago') + def timeago_filter(dt): + """Convert a datetime to a 'time ago' string (e.g., '2 hours ago')""" + if dt is None: + return "" - if not logo_path: - print("DEBUG: logo_path is None or empty") - return None - - if not os.path.exists(logo_path): - print(f"DEBUG: Logo file does not exist: {logo_path}") - return None + # Import here to avoid circular imports + from datetime import datetime, timezone - try: - print(f"DEBUG: Reading logo from: {logo_path}") - with open(logo_path, 'rb') as logo_file: - logo_data = base64.b64encode(logo_file.read()).decode('utf-8') - - # Detect MIME type - mime_type, _ = mimetypes.guess_type(logo_path) - if not mime_type: - # Try to detect from file extension - ext = os.path.splitext(logo_path)[1].lower() - mime_map = { - '.png': 'image/png', - '.jpg': 'image/jpeg', - '.jpeg': 'image/jpeg', - '.gif': 'image/gif', - '.svg': 'image/svg+xml', - '.webp': 'image/webp' - } - mime_type = mime_map.get(ext, 'image/png') - - print(f"DEBUG: Logo encoded successfully, MIME type: {mime_type}, size: {len(logo_data)} bytes") - return f'data:{mime_type};base64,{logo_data}' - except Exception as e: - print(f"DEBUG: Error encoding logo: {e}") - import traceback - print(traceback.format_exc()) - return None - - # Make get_logo_base64 available in templates as a global function - app.jinja_env.globals.update(get_logo_base64=get_logo_base64) \ No newline at end of file + # Ensure we're working with a timezone-aware datetime + if dt.tzinfo is None: + # Assume UTC if no timezone info + dt = dt.replace(tzinfo=timezone.utc) + + # Get current time in UTC + now = datetime.now(timezone.utc) + + # Calculate difference + diff = now - dt + + # Convert to seconds + seconds = diff.total_seconds() + + # Handle future dates + if seconds < 0: + return "just now" + + # Calculate time units + minutes = seconds / 60 + hours = minutes / 60 + days = hours / 24 + weeks = days / 7 + months = days / 30 + years = days / 365 + + # Return appropriate string + if seconds < 60: + return "just now" + elif minutes < 60: + m = int(minutes) + return f"{m} minute{'s' if m != 1 else ''} ago" + elif hours < 24: + h = int(hours) + return f"{h} hour{'s' if h != 1 else ''} ago" + elif days < 7: + d = int(days) + return f"{d} day{'s' if d != 1 else ''} ago" + elif weeks < 4: + w = int(weeks) + return f"{w} week{'s' if w != 1 else ''} ago" + elif months < 12: + mo = int(months) + return f"{mo} month{'s' if mo != 1 else ''} ago" + else: + y = int(years) + return f"{y} year{'s' if y != 1 else ''} ago" diff --git a/tests/test_time_entry_templates.py b/tests/test_time_entry_templates.py index c68162d..1ca6970 100644 --- a/tests/test_time_entry_templates.py +++ b/tests/test_time_entry_templates.py @@ -239,6 +239,32 @@ class TestTimeEntryTemplateRoutes: response = client.get('/templates', follow_redirects=False) assert response.status_code == 302 # Redirect to login + @pytest.mark.smoke + def test_list_templates_with_usage_data(self, authenticated_client, user, project): + """Test templates list page renders correctly with templates that have usage data""" + # Create a template with usage data (last_used_at set) + from datetime import datetime, timezone + from app.models import TimeEntryTemplate + from app import db + + template = TimeEntryTemplate( + user_id=user.id, + name='Used Template', + project_id=project.id, + default_duration_minutes=60, + usage_count=5, + last_used_at=datetime.now(timezone.utc) + ) + db.session.add(template) + db.session.commit() + + # Access the list page + response = authenticated_client.get('/templates') + assert response.status_code == 200 + assert b'Used Template' in response.data + # Verify that timeago filter is working (should show "just now" or similar) + assert b'ago' in response.data or b'just now' in response.data + def test_create_template_page_get(self, authenticated_client): """Test accessing create template page""" response = authenticated_client.get('/templates/create') diff --git a/tests/test_utils.py b/tests/test_utils.py index 6d23a98..09b5693 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -266,6 +266,169 @@ def test_format_money_filter_invalid(app): assert result == "not a number" +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_none(app): + """Test timeago filter with None.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + result = filter_func(None) + assert result == "" + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_just_now(app): + """Test timeago filter with very recent datetime.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create datetime for 30 seconds ago + now = datetime.datetime.now(datetime.timezone.utc) + dt = now - datetime.timedelta(seconds=30) + result = filter_func(dt) + assert result == "just now" + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_minutes(app): + """Test timeago filter with minutes ago.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create datetime for 5 minutes ago + now = datetime.datetime.now(datetime.timezone.utc) + dt = now - datetime.timedelta(minutes=5) + result = filter_func(dt) + assert "minute" in result + assert "ago" in result + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_hours(app): + """Test timeago filter with hours ago.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create datetime for 3 hours ago + now = datetime.datetime.now(datetime.timezone.utc) + dt = now - datetime.timedelta(hours=3) + result = filter_func(dt) + assert "hour" in result + assert "ago" in result + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_days(app): + """Test timeago filter with days ago.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create datetime for 2 days ago + now = datetime.datetime.now(datetime.timezone.utc) + dt = now - datetime.timedelta(days=2) + result = filter_func(dt) + assert "day" in result + assert "ago" in result + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_weeks(app): + """Test timeago filter with weeks ago.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create datetime for 2 weeks ago + now = datetime.datetime.now(datetime.timezone.utc) + dt = now - datetime.timedelta(weeks=2) + result = filter_func(dt) + assert "week" in result + assert "ago" in result + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_months(app): + """Test timeago filter with months ago.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create datetime for 60 days ago (2 months) + now = datetime.datetime.now(datetime.timezone.utc) + dt = now - datetime.timedelta(days=60) + result = filter_func(dt) + assert "month" in result + assert "ago" in result + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_years(app): + """Test timeago filter with years ago.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create datetime for 400 days ago (over a year) + now = datetime.datetime.now(datetime.timezone.utc) + dt = now - datetime.timedelta(days=400) + result = filter_func(dt) + assert "year" in result + assert "ago" in result + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_future(app): + """Test timeago filter with future datetime.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create datetime in the future + now = datetime.datetime.now(datetime.timezone.utc) + dt = now + datetime.timedelta(hours=2) + result = filter_func(dt) + assert result == "just now" + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_naive_datetime(app): + """Test timeago filter with naive datetime (no timezone).""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + # Create naive datetime for 1 hour ago + dt = datetime.datetime.now() - datetime.timedelta(hours=1) + result = filter_func(dt) + # Should still work and convert to UTC + assert "ago" in result or result == "just now" + + +@pytest.mark.unit +@pytest.mark.utils +def test_timeago_filter_singular_plural(app): + """Test timeago filter uses correct singular/plural forms.""" + register_template_filters(app) + with app.app_context(): + filter_func = app.jinja_env.filters.get('timeago') + now = datetime.datetime.now(datetime.timezone.utc) + + # Test singular (1 minute) + dt = now - datetime.timedelta(minutes=1) + result = filter_func(dt) + assert "1 minute ago" in result + + # Test plural (2 minutes) + dt = now - datetime.timedelta(minutes=2) + result = filter_func(dt) + assert "2 minutes ago" in result + + # ============================================================================ # Context Processor Tests # ============================================================================