Files
TimeTracker/app/models/activity.py
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

151 lines
5.6 KiB
Python

from datetime import datetime
from app import db
class Activity(db.Model):
"""Activity log for tracking user actions across the system
Provides a comprehensive audit trail and activity feed showing
what users are doing in the application.
"""
__tablename__ = 'activities'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
# Action details
action = db.Column(db.String(50), nullable=False, index=True) # 'created', 'updated', 'deleted', 'started', 'stopped', etc.
entity_type = db.Column(db.String(50), nullable=False, index=True) # 'project', 'task', 'time_entry', 'invoice', 'client'
entity_id = db.Column(db.Integer, nullable=False, index=True)
entity_name = db.Column(db.String(500), nullable=True) # Cached name for display
# Description and extra data
description = db.Column(db.Text, nullable=True) # Human-readable description
extra_data = db.Column(db.JSON, nullable=True) # Additional context (changes, values, etc.)
# IP and user agent for security audit
ip_address = db.Column(db.String(45), nullable=True)
user_agent = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False, index=True)
# Relationships
user = db.relationship('User', backref='activities')
# Indexes for common queries
__table_args__ = (
db.Index('ix_activities_user_created', 'user_id', 'created_at'),
db.Index('ix_activities_entity', 'entity_type', 'entity_id'),
)
def __repr__(self):
return f'<Activity {self.user.username if self.user else "Unknown"} {self.action} {self.entity_type}#{self.entity_id}>'
@classmethod
def log(cls, user_id, action, entity_type, entity_id, entity_name=None, description=None, extra_data=None, metadata=None, ip_address=None, user_agent=None):
"""Convenience method to log an activity
Usage:
Activity.log(
user_id=current_user.id,
action='created',
entity_type='project',
entity_id=project.id,
entity_name=project.name,
description=f'Created project "{project.name}"'
)
Note: 'metadata' parameter is deprecated, use 'extra_data' instead.
"""
# Support both parameter names for backward compatibility
data = extra_data if extra_data is not None else metadata
activity = cls(
user_id=user_id,
action=action,
entity_type=entity_type,
entity_id=entity_id,
entity_name=entity_name,
description=description,
extra_data=data,
ip_address=ip_address,
user_agent=user_agent
)
db.session.add(activity)
try:
db.session.commit()
except Exception as e:
db.session.rollback()
# Don't let activity logging break the main flow
print(f"Failed to log activity: {e}")
@classmethod
def get_recent(cls, user_id=None, limit=50, entity_type=None):
"""Get recent activities
Args:
user_id: Filter by user (None for all users)
limit: Maximum number of activities to return
entity_type: Filter by entity type
"""
query = cls.query
if user_id:
query = query.filter_by(user_id=user_id)
if entity_type:
query = query.filter_by(entity_type=entity_type)
return query.order_by(cls.created_at.desc()).limit(limit).all()
def to_dict(self):
"""Convert to dictionary for API responses"""
return {
'id': self.id,
'user_id': self.user_id,
'username': self.user.username if self.user else None,
'display_name': self.user.display_name if self.user else None,
'action': self.action,
'entity_type': self.entity_type,
'entity_id': self.entity_id,
'entity_name': self.entity_name,
'description': self.description,
'extra_data': self.extra_data,
'metadata': self.extra_data, # For backward compatibility
'created_at': self.created_at.isoformat() if self.created_at else None,
}
def get_icon(self):
"""Get icon class for this activity type"""
icons = {
'created': 'fas fa-plus-circle text-green-500',
'updated': 'fas fa-edit text-blue-500',
'deleted': 'fas fa-trash text-red-500',
'started': 'fas fa-play text-green-500',
'stopped': 'fas fa-stop text-red-500',
'completed': 'fas fa-check-circle text-green-500',
'assigned': 'fas fa-user-plus text-blue-500',
'commented': 'fas fa-comment text-gray-500',
'sent': 'fas fa-paper-plane text-blue-500',
'paid': 'fas fa-dollar-sign text-green-500',
}
return icons.get(self.action, 'fas fa-circle text-gray-500')
def get_color(self):
"""Get color class for this activity type"""
colors = {
'created': 'green',
'updated': 'blue',
'deleted': 'red',
'started': 'green',
'stopped': 'red',
'completed': 'green',
'assigned': 'blue',
'commented': 'gray',
'sent': 'blue',
'paid': 'green',
}
return colors.get(self.action, 'gray')