Files
TimeTracker/ACTIVITY_LOGGING_INTEGRATION_GUIDE.md
Dries Peeters b1973ca49a feat: Add Quick Wins feature set - activity tracking, templates, and user preferences
This commit introduces several high-impact features to improve user experience
and productivity:

New Features:
- Activity Logging: Comprehensive audit trail tracking user actions across the
  system with Activity model, including IP address and user agent tracking
- Time Entry Templates: Reusable templates for frequently logged activities with
  usage tracking and quick-start functionality
- Saved Filters: Save and reuse common search/filter combinations across
  different views (projects, tasks, reports)
- User Preferences: Enhanced user settings including email notifications,
  timezone, date/time formats, week start day, and theme preferences
- Excel Export: Generate formatted Excel exports for time entries and reports
  with styling and proper formatting
- Email Notifications: Complete email system for task assignments, overdue
  invoices, comments, and weekly summaries with HTML templates
- Scheduled Tasks: Background task scheduler for periodic operations

Models Added:
- Activity: Tracks all user actions with detailed context and metadata
- TimeEntryTemplate: Stores reusable time entry configurations
- SavedFilter: Manages user-saved filter configurations

Routes Added:
- user.py: User profile and settings management
- saved_filters.py: CRUD operations for saved filters
- time_entry_templates.py: Template management endpoints

UI Enhancements:
- Bulk actions widget component
- Keyboard shortcuts help modal with advanced shortcuts
- Save filter widget component
- Email notification templates
- User profile and settings pages
- Saved filters management interface
- Time entry templates interface

Database Changes:
- Migration 022: Creates activities and time_entry_templates tables
- Adds user preference columns (notifications, timezone, date/time formats)
- Proper indexes for query optimization

Backend Updates:
- Enhanced keyboard shortcuts system (commands.js, keyboard-shortcuts-advanced.js)
- Updated projects, reports, and tasks routes with activity logging
- Safe database commit utilities integration
- Event tracking for analytics

Dependencies:
- Added openpyxl for Excel generation
- Added Flask-Mail dependencies
- Updated requirements.txt

All new features include proper error handling, activity logging integration,
and maintain existing functionality while adding new capabilities.
2025-10-23 09:05:07 +02:00

15 KiB

Activity Logging Integration Guide

This guide shows how to integrate Activity logging throughout the TimeTracker application.

Already Integrated

Projects (app/routes/projects.py)

  • Project creation - Line 173

🔧 Integration Pattern

Basic Pattern

from app.models import Activity

Activity.log(
    user_id=current_user.id,
    action='created',  # or 'updated', 'deleted', 'started', 'stopped', etc.
    entity_type='project',  # 'project', 'task', 'time_entry', 'invoice', etc.
    entity_id=entity.id,
    entity_name=entity.name,
    description=f'Human-readable description of what happened',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

📝 Integration Checklist

1. Projects (app/routes/projects.py)

Create Project - DONE (line 173)

Update Project - Add after successful update:

# Find: flash(f'Project "{project.name}" updated successfully', 'success')
# Add before it:
Activity.log(
    user_id=current_user.id,
    action='updated',
    entity_type='project',
    entity_id=project.id,
    entity_name=project.name,
    description=f'Updated project "{project.name}"',
    metadata={'fields_updated': ['name', 'description']},  # optional
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Archive Project - Find the archive route and add:

Activity.log(
    user_id=current_user.id,
    action='archived' if project.status == 'archived' else 'unarchived',
    entity_type='project',
    entity_id=project.id,
    entity_name=project.name,
    description=f'{"Archived" if project.status == "archived" else "Unarchived"} project "{project.name}"',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Delete Project - Add after successful deletion:

Activity.log(
    user_id=current_user.id,
    action='deleted',
    entity_type='project',
    entity_id=project_id,
    entity_name=project_name,  # Store before deletion
    description=f'Deleted project "{project_name}"',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

2. Tasks (app/routes/tasks.py)

Import: Add Activity to imports at the top:

from app.models import Task, Project, User, Activity

Create Task:

# After task creation and commit
Activity.log(
    user_id=current_user.id,
    action='created',
    entity_type='task',
    entity_id=task.id,
    entity_name=task.name,
    description=f'Created task "{task.name}" in project "{task.project.name if task.project else "No project"}"',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Update Task:

Activity.log(
    user_id=current_user.id,
    action='updated',
    entity_type='task',
    entity_id=task.id,
    entity_name=task.name,
    description=f'Updated task "{task.name}"',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Status Change (Important!):

Activity.log(
    user_id=current_user.id,
    action='status_changed',
    entity_type='task',
    entity_id=task.id,
    entity_name=task.name,
    description=f'Changed task "{task.name}" status from {old_status} to {new_status}',
    metadata={'old_status': old_status, 'new_status': new_status},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Task Assignment:

Activity.log(
    user_id=current_user.id,
    action='assigned',
    entity_type='task',
    entity_id=task.id,
    entity_name=task.name,
    description=f'Assigned task "{task.name}" to {assigned_user.display_name}',
    metadata={'assigned_to': assigned_user.id, 'assigned_to_name': assigned_user.display_name},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Delete Task:

Activity.log(
    user_id=current_user.id,
    action='deleted',
    entity_type='task',
    entity_id=task.id,
    entity_name=task.name,
    description=f'Deleted task "{task.name}"',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

3. Time Entries (app/routes/timer.py)

Import: Add Activity to imports

Start Timer:

# After timer starts successfully
Activity.log(
    user_id=current_user.id,
    action='started',
    entity_type='time_entry',
    entity_id=entry.id,
    entity_name=f'{entry.project.name if entry.project else "No project"}',
    description=f'Started timer for {entry.project.name if entry.project else "No project"}',
    metadata={'project_id': entry.project_id},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Stop Timer:

# After timer stops successfully
Activity.log(
    user_id=current_user.id,
    action='stopped',
    entity_type='time_entry',
    entity_id=entry.id,
    entity_name=f'{entry.project.name if entry.project else "No project"}',
    description=f'Stopped timer for {entry.project.name if entry.project else "No project"} - Duration: {entry.duration_formatted}',
    metadata={'duration_hours': entry.duration_hours, 'project_id': entry.project_id},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Manual Time Entry:

Activity.log(
    user_id=current_user.id,
    action='created',
    entity_type='time_entry',
    entity_id=entry.id,
    entity_name=f'{entry.project.name if entry.project else "No project"}',
    description=f'Added manual time entry for {entry.project.name if entry.project else "No project"} - {entry.duration_formatted}',
    metadata={'duration_hours': entry.duration_hours, 'source': 'manual'},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Edit Time Entry:

Activity.log(
    user_id=current_user.id,
    action='updated',
    entity_type='time_entry',
    entity_id=entry.id,
    entity_name=f'{entry.project.name if entry.project else "No project"}',
    description=f'Updated time entry for {entry.project.name if entry.project else "No project"}',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Delete Time Entry:

Activity.log(
    user_id=current_user.id,
    action='deleted',
    entity_type='time_entry',
    entity_id=entry.id,
    entity_name=f'{entry.project.name if entry.project else "No project"}',
    description=f'Deleted time entry for {entry.project.name if entry.project else "No project"} - {entry.duration_formatted}',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

4. Invoices (app/routes/invoices.py)

Import: Add Activity to imports

Create Invoice:

Activity.log(
    user_id=current_user.id,
    action='created',
    entity_type='invoice',
    entity_id=invoice.id,
    entity_name=invoice.invoice_number,
    description=f'Created invoice {invoice.invoice_number} for {invoice.client_name} - {invoice.currency_code} {invoice.total_amount}',
    metadata={'client_id': invoice.client_id, 'amount': float(invoice.total_amount)},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Update Invoice:

Activity.log(
    user_id=current_user.id,
    action='updated',
    entity_type='invoice',
    entity_id=invoice.id,
    entity_name=invoice.invoice_number,
    description=f'Updated invoice {invoice.invoice_number}',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Status Change:

Activity.log(
    user_id=current_user.id,
    action='status_changed',
    entity_type='invoice',
    entity_id=invoice.id,
    entity_name=invoice.invoice_number,
    description=f'Changed invoice {invoice.invoice_number} status from {old_status} to {new_status}',
    metadata={'old_status': old_status, 'new_status': new_status},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Payment Recorded:

Activity.log(
    user_id=current_user.id,
    action='paid',
    entity_type='invoice',
    entity_id=invoice.id,
    entity_name=invoice.invoice_number,
    description=f'Recorded payment for invoice {invoice.invoice_number} - {invoice.currency_code} {amount_paid}',
    metadata={'amount_paid': float(amount_paid), 'payment_method': payment_method},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Send Invoice:

Activity.log(
    user_id=current_user.id,
    action='sent',
    entity_type='invoice',
    entity_id=invoice.id,
    entity_name=invoice.invoice_number,
    description=f'Sent invoice {invoice.invoice_number} to {invoice.client_email}',
    metadata={'sent_to': invoice.client_email},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Delete Invoice:

Activity.log(
    user_id=current_user.id,
    action='deleted',
    entity_type='invoice',
    entity_id=invoice.id,
    entity_name=invoice.invoice_number,
    description=f'Deleted invoice {invoice.invoice_number}',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

5. Clients (app/routes/clients.py)

Import: Add Activity to imports

Create Client:

Activity.log(
    user_id=current_user.id,
    action='created',
    entity_type='client',
    entity_id=client.id,
    entity_name=client.name,
    description=f'Added new client "{client.name}"',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Update Client:

Activity.log(
    user_id=current_user.id,
    action='updated',
    entity_type='client',
    entity_id=client.id,
    entity_name=client.name,
    description=f'Updated client "{client.name}"',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

Delete Client:

Activity.log(
    user_id=current_user.id,
    action='deleted',
    entity_type='client',
    entity_id=client.id,
    entity_name=client.name,
    description=f'Deleted client "{client.name}"',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

6. Comments (app/routes/comments.py)

Create Comment:

Activity.log(
    user_id=current_user.id,
    action='commented',
    entity_type='task',
    entity_id=comment.task_id,
    entity_name=task.name,
    description=f'Commented on task "{task.name}"',
    metadata={'comment_id': comment.id},
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)

🎯 Quick Integration Script

Here's a Python script to help add Activity logging to a route:

def add_activity_logging(route_function_source_code):
    """
    Helper to suggest where to add Activity.log() calls.
    Returns suggested code to insert.
    """
    template = '''
# Log activity
Activity.log(
    user_id=current_user.id,
    action='ACTION_HERE',  # created, updated, deleted, started, stopped, etc.
    entity_type='ENTITY_TYPE',  # project, task, time_entry, invoice, client
    entity_id=entity.id,
    entity_name=entity.name,
    description=f'DESCRIPTION_HERE',
    ip_address=request.remote_addr,
    user_agent=request.headers.get('User-Agent')
)
'''
    return template

📊 Activity Action Types

Use these standardized action types:

Action When to Use
created When creating any entity
updated When modifying entity fields
deleted When removing an entity
started When starting a timer
stopped When stopping a timer
assigned When assigning tasks to users
commented When adding comments
status_changed When changing status fields
sent When sending invoices/emails
paid When recording payments
archived When archiving entities
unarchived When unarchiving entities

🧪 Testing Activity Logging

from app.models import Activity

# Get recent activities
activities = Activity.get_recent(limit=50)
for act in activities:
    print(f"{act.user.username}: {act.action} {act.entity_type} - {act.description}")

# Get activities by entity type
project_activities = Activity.get_recent(entity_type='project', limit=20)

# Get user-specific activities
user_activities = Activity.get_recent(user_id=user.id, limit=30)

📈 Performance Considerations

  1. Don't let activity logging break main flow:

    • Activity.log() includes try/except internally
    • Failures are logged but don't raise exceptions
  2. Batch operations:

    • For bulk operations, consider logging summary activities
  3. Database indexes:

    • Activity table has indexes on user_id, created_at, and composite indexes

🎨 Creating Activity Feed UI

Once Activity logging is integrated, create an activity feed widget:

app/templates/widgets/activity_feed.html:

<div class="activity-feed">
    <h3>Recent Activity</h3>
    {% for activity in activities %}
    <div class="activity-item">
        <i class="{{ activity.get_icon() }}"></i>
        <div>
            <strong>{{ activity.user.display_name }}</strong>
            {{ activity.description }}
        </div>
        <span class="timestamp">{{ activity.created_at|timeago }}</span>
    </div>
    {% endfor %}
</div>

Route for activity feed:

@main_bp.route('/api/activities')
@login_required
def get_activities():
    limit = request.args.get('limit', 20, type=int)
    entity_type = request.args.get('entity_type')
    
    activities = Activity.get_recent(
        user_id=current_user.id if not current_user.is_admin else None,
        limit=limit,
        entity_type=entity_type
    )
    
    return jsonify({
        'activities': [a.to_dict() for a in activities]
    })

Integration Checklist

Use this to track your progress:

  • Projects - Create (DONE)
  • Projects - Update
  • Projects - Delete
  • Projects - Archive/Unarchive
  • Tasks - Create
  • Tasks - Update
  • Tasks - Delete
  • Tasks - Status Change
  • Tasks - Assignment
  • Time Entries - Start Timer
  • Time Entries - Stop Timer
  • Time Entries - Manual Create
  • Time Entries - Update
  • Time Entries - Delete
  • Invoices - Create
  • Invoices - Update
  • Invoices - Status Change
  • Invoices - Payment
  • Invoices - Send
  • Invoices - Delete
  • Clients - Create
  • Clients - Update
  • Clients - Delete
  • Comments - Create
  • User Settings - Update (DONE in user.py)

Estimated Time: 2-3 hours to integrate activity logging throughout the entire application.

Priority Areas: Start with Projects, Tasks, and Time Entries as these are the most frequently used features.