Merge branch 'develop' into 152-getting-started-doc-first-login

This commit is contained in:
Dries Peeters
2025-10-25 07:24:59 +02:00
committed by GitHub
3 changed files with 244 additions and 43 deletions

View File

@@ -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)
# 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"

View File

@@ -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')

View File

@@ -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
# ============================================================================