Files
TimeTracker/apply_migration.py
Dries Peeters 48ec29e096 feat: Add per-user time rounding preferences
Implement comprehensive time rounding preferences that allow each user to
configure how their time entries are rounded when stopping timers.

Features:
- Per-user rounding settings (independent from global config)
- Multiple rounding intervals: 1, 5, 10, 15, 30, 60 minutes
- Three rounding methods: nearest, up (ceiling), down (floor)
- Enable/disable toggle for flexible time tracking
- Real-time preview showing rounding examples
- Backward compatible with existing global rounding settings

Database Changes:
- Add migration 027 with three new user columns:
  * time_rounding_enabled (Boolean, default: true)
  * time_rounding_minutes (Integer, default: 1)
  * time_rounding_method (String, default: 'nearest')

Implementation:
- Update User model with rounding preference fields
- Modify TimeEntry.calculate_duration() to use per-user rounding
- Create app/utils/time_rounding.py with core rounding logic
- Update user settings route and template with rounding UI
- Add comprehensive unit, model, and smoke tests (50+ test cases)

UI/UX:
- Add "Time Rounding Preferences" section to user settings page
- Interactive controls with live example visualization
- Descriptive help text and method explanations
- Fix navigation: Settings link now correctly points to user.settings
- Fix CSRF token in settings form

Documentation:
- Add comprehensive user guide (docs/TIME_ROUNDING_PREFERENCES.md)
- Include API documentation and usage examples
- Provide troubleshooting guide and best practices
- Add deployment instructions for migration

Testing:
- Unit tests for rounding logic (tests/test_time_rounding.py)
- Model integration tests (tests/test_time_rounding_models.py)
- End-to-end smoke tests (tests/test_time_rounding_smoke.py)

Fixes:
- Correct settings navigation link in user dropdown menu
- Fix CSRF token format in user settings template

This feature enables flexible billing practices, supports different client
requirements, and maintains exact time tracking when needed.
2025-10-24 09:36:03 +02:00

106 lines
3.8 KiB
Python

#!/usr/bin/env python3
"""
Simple script to apply the time rounding preferences migration
"""
import os
import sys
# Add the project root to the path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import create_app, db
from sqlalchemy import inspect, text
def check_columns_exist():
"""Check if the time rounding columns already exist"""
app = create_app()
with app.app_context():
inspector = inspect(db.engine)
columns = [col['name'] for col in inspector.get_columns('users')]
has_enabled = 'time_rounding_enabled' in columns
has_minutes = 'time_rounding_minutes' in columns
has_method = 'time_rounding_method' in columns
return has_enabled, has_minutes, has_method
def apply_migration():
"""Apply the migration to add time rounding columns"""
app = create_app()
with app.app_context():
print("Applying time rounding preferences migration...")
# Check if columns already exist
has_enabled, has_minutes, has_method = check_columns_exist()
if has_enabled and has_minutes and has_method:
print("✓ Migration already applied! All columns exist.")
return True
# Apply the migration
try:
if not has_enabled:
print("Adding time_rounding_enabled column...")
db.session.execute(text(
"ALTER TABLE users ADD COLUMN time_rounding_enabled BOOLEAN DEFAULT 1 NOT NULL"
))
if not has_minutes:
print("Adding time_rounding_minutes column...")
db.session.execute(text(
"ALTER TABLE users ADD COLUMN time_rounding_minutes INTEGER DEFAULT 1 NOT NULL"
))
if not has_method:
print("Adding time_rounding_method column...")
db.session.execute(text(
"ALTER TABLE users ADD COLUMN time_rounding_method VARCHAR(10) DEFAULT 'nearest' NOT NULL"
))
db.session.commit()
print("✓ Migration applied successfully!")
# Verify
has_enabled, has_minutes, has_method = check_columns_exist()
if has_enabled and has_minutes and has_method:
print("✓ Verification passed! All columns exist.")
return True
else:
print("✗ Verification failed! Some columns are missing.")
return False
except Exception as e:
print(f"✗ Migration failed: {e}")
db.session.rollback()
return False
if __name__ == '__main__':
print("=== Time Rounding Preferences Migration ===")
print()
# Check current state
try:
has_enabled, has_minutes, has_method = check_columns_exist()
print("Current database state:")
print(f" - time_rounding_enabled: {'✓ exists' if has_enabled else '✗ missing'}")
print(f" - time_rounding_minutes: {'✓ exists' if has_minutes else '✗ missing'}")
print(f" - time_rounding_method: {'✓ exists' if has_method else '✗ missing'}")
print()
except Exception as e:
print(f"✗ Could not check database state: {e}")
sys.exit(1)
# Apply migration if needed
if has_enabled and has_minutes and has_method:
print("All columns already exist. No migration needed.")
else:
success = apply_migration()
if success:
print("\n✓ Migration complete! You can now use the time rounding preferences feature.")
print(" Please restart your application to load the changes.")
else:
print("\n✗ Migration failed. Please check the error messages above.")
sys.exit(1)