mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-01-21 03:50:01 -06:00
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.
151 lines
5.6 KiB
Python
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')
|
|
|