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

140 lines
6.0 KiB
Python

from datetime import datetime
from flask_login import UserMixin
from werkzeug.security import generate_password_hash, check_password_hash
from app import db
import os
class User(UserMixin, db.Model):
"""User model for username-based authentication"""
__tablename__ = 'users'
__table_args__ = (
db.UniqueConstraint('oidc_issuer', 'oidc_sub', name='uq_users_oidc_issuer_sub'),
)
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False, index=True)
email = db.Column(db.String(200), nullable=True, index=True)
full_name = db.Column(db.String(200), nullable=True)
role = db.Column(db.String(20), default='user', nullable=False) # 'user' or 'admin'
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
last_login = db.Column(db.DateTime, nullable=True)
is_active = db.Column(db.Boolean, default=True, nullable=False)
theme_preference = db.Column(db.String(10), default=None, nullable=True) # 'light' | 'dark' | None=system
preferred_language = db.Column(db.String(8), default=None, nullable=True) # e.g., 'en', 'de'
oidc_sub = db.Column(db.String(255), nullable=True)
oidc_issuer = db.Column(db.String(255), nullable=True)
avatar_filename = db.Column(db.String(255), nullable=True)
# User preferences and settings
email_notifications = db.Column(db.Boolean, default=True, nullable=False) # Enable/disable email notifications
notification_overdue_invoices = db.Column(db.Boolean, default=True, nullable=False) # Notify about overdue invoices
notification_task_assigned = db.Column(db.Boolean, default=True, nullable=False) # Notify when assigned to task
notification_task_comments = db.Column(db.Boolean, default=True, nullable=False) # Notify about task comments
notification_weekly_summary = db.Column(db.Boolean, default=False, nullable=False) # Send weekly time summary
timezone = db.Column(db.String(50), nullable=True) # User-specific timezone override
date_format = db.Column(db.String(20), default='YYYY-MM-DD', nullable=False) # Date format preference
time_format = db.Column(db.String(10), default='24h', nullable=False) # '12h' or '24h'
week_start_day = db.Column(db.Integer, default=1, nullable=False) # 0=Sunday, 1=Monday, etc.
# Relationships
time_entries = db.relationship('TimeEntry', backref='user', lazy='dynamic', cascade='all, delete-orphan')
project_costs = db.relationship('ProjectCost', backref='user', lazy='dynamic', cascade='all, delete-orphan')
def __init__(self, username, role='user', email=None, full_name=None):
self.username = username.lower().strip()
self.role = role
self.email = (email or None)
self.full_name = (full_name or None)
def __repr__(self):
return f'<User {self.username}>'
@property
def is_admin(self):
"""Check if user is an admin"""
return self.role == 'admin'
@property
def active_timer(self):
"""Get the user's currently active timer"""
from .time_entry import TimeEntry
return TimeEntry.query.filter_by(
user_id=self.id,
end_time=None
).first()
@property
def total_hours(self):
"""Calculate total hours worked by this user"""
from .time_entry import TimeEntry
total_seconds = db.session.query(
db.func.sum(TimeEntry.duration_seconds)
).filter(
TimeEntry.user_id == self.id,
TimeEntry.end_time.isnot(None)
).scalar() or 0
return round(total_seconds / 3600, 2)
@property
def display_name(self):
"""Preferred display name: full name if available, else username"""
if self.full_name and self.full_name.strip():
return self.full_name.strip()
return self.username
def get_recent_entries(self, limit=10):
"""Get recent time entries for this user"""
from .time_entry import TimeEntry
return self.time_entries.filter(
TimeEntry.end_time.isnot(None)
).order_by(
TimeEntry.start_time.desc()
).limit(limit).all()
def update_last_login(self):
"""Update the last login timestamp"""
self.last_login = datetime.utcnow()
db.session.commit()
def to_dict(self):
"""Convert user to dictionary for API responses"""
return {
'id': self.id,
'username': self.username,
'email': self.email,
'full_name': self.full_name,
'display_name': self.display_name,
'role': self.role,
'created_at': self.created_at.isoformat() if self.created_at else None,
'last_login': self.last_login.isoformat() if self.last_login else None,
'is_active': self.is_active,
'total_hours': self.total_hours,
'avatar_url': self.get_avatar_url(),
}
# Avatar helpers
def get_avatar_url(self):
"""Return the public URL for the user's avatar, or None if not set"""
if self.avatar_filename:
return f"/uploads/avatars/{self.avatar_filename}"
return None
def get_avatar_path(self):
"""Return absolute filesystem path to the user's avatar, or None if not set"""
if not self.avatar_filename:
return None
try:
from flask import current_app
# Avatars are now stored in /data volume to persist between container updates
upload_folder = os.path.join(current_app.config.get('UPLOAD_FOLDER', '/data/uploads'), 'avatars')
return os.path.join(upload_folder, self.avatar_filename)
except Exception:
# Fallback for development/non-docker environments
return os.path.join('/data/uploads', 'avatars', self.avatar_filename)
def has_avatar(self):
"""Check whether the user's avatar file exists on disk"""
path = self.get_avatar_path()
return bool(path and os.path.exists(path))