Files
TimeTracker/app/models/focus_session.py
Dries Peeters b6c0a79ffc feat: Focus mode, estimates/burndown+budget alerts, recurring blocks, saved filters, and rate overrides
Add Pomodoro focus mode with session summaries
Model: FocusSession; API: /api/focus-sessions/; UI: Focus modal on timer page
Add estimates vs actuals with burndown and budget alerts
Project fields: estimated_hours, budget_amount, budget_threshold_percent
API: /api/projects/<id>/burndown; Charts in project view and project report
Implement recurring time blocks/templates
Model: RecurringBlock; API CRUD: /api/recurring-blocks; CLI: flask generate_recurring
Add tagging and saved filters across views
Model: SavedFilter; /api/entries supports tag and saved_filter_id
Support billable rate overrides per project/member
Model: RateOverride; invoicing uses effective rate resolution
Also:
Migration: 016_add_focus_recurring_rates_filters_and_project_budget.py
Integrations and UI updates in projects view, timer page, and reports
Docs updated (startup, invoice, task mgmt) and README feature list
Added basic tests for new features
2025-10-06 13:34:56 +02:00

59 lines
2.5 KiB
Python

from datetime import datetime
from app import db
class FocusSession(db.Model):
"""Pomodoro-style focus session metadata linked to a time entry.
Tracks configuration and outcomes for a single focus session so we can
provide summaries independent of raw time entries.
"""
__tablename__ = 'focus_sessions'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False, index=True)
project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=True, index=True)
task_id = db.Column(db.Integer, db.ForeignKey('tasks.id'), nullable=True, index=True)
time_entry_id = db.Column(db.Integer, db.ForeignKey('time_entries.id'), nullable=True, index=True)
# Session timing
started_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
ended_at = db.Column(db.DateTime, nullable=True)
# Pomodoro configuration (minutes)
pomodoro_length = db.Column(db.Integer, nullable=False, default=25)
short_break_length = db.Column(db.Integer, nullable=False, default=5)
long_break_length = db.Column(db.Integer, nullable=False, default=15)
long_break_interval = db.Column(db.Integer, nullable=False, default=4)
# Outcomes
cycles_completed = db.Column(db.Integer, nullable=False, default=0)
interruptions = db.Column(db.Integer, nullable=False, default=0)
notes = db.Column(db.Text, nullable=True)
created_at = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
def to_dict(self):
return {
'id': self.id,
'user_id': self.user_id,
'project_id': self.project_id,
'task_id': self.task_id,
'time_entry_id': self.time_entry_id,
'started_at': self.started_at.isoformat() if self.started_at else None,
'ended_at': self.ended_at.isoformat() if self.ended_at else None,
'pomodoro_length': self.pomodoro_length,
'short_break_length': self.short_break_length,
'long_break_length': self.long_break_length,
'long_break_interval': self.long_break_interval,
'cycles_completed': self.cycles_completed,
'interruptions': self.interruptions,
'notes': self.notes,
'created_at': self.created_at.isoformat() if self.created_at else None,
'updated_at': self.updated_at.isoformat() if self.updated_at else None,
}