Files
TimeTracker/app/models/recurring_task.py
T
Dries Peeters b4486a627f fix: CI tests, code quality, and duplicate DB indexes
- Webhook models: remove duplicate index definitions so db.create_all()
  no longer raises 'index already exists' (columns already have index=True)
- ImportService: fix circular import by late-importing ClientService,
  ProjectService, TimeTrackingService in __init__
- reports: fix F823 by renaming unpack variable _ to _entry_count to avoid
  shadowing gettext _ in export_task_excel()
- Code quality: add .flake8 with extend-ignore so flake8 CI passes;
  simplify pyproject.toml isort config (drop unsupported options)
- Format: run black and isort on app/
- tests: restore minimal app fixture in test_import_export_models
2026-03-15 10:51:52 +01:00

146 lines
6.2 KiB
Python

"""
Recurring Task model for automated task creation
Similar to recurring invoices but for tasks
"""
from datetime import date, datetime, timedelta
from dateutil.relativedelta import relativedelta
from app import db
class RecurringTask(db.Model):
"""Recurring task template for automated task creation"""
__tablename__ = "recurring_tasks"
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(200), nullable=False) # Template name/description
project_id = db.Column(db.Integer, db.ForeignKey("projects.id"), nullable=False, index=True)
# Recurrence settings
frequency = db.Column(db.String(20), nullable=False) # 'daily', 'weekly', 'monthly', 'yearly'
interval = db.Column(db.Integer, nullable=False, default=1) # Every N periods
next_run_date = db.Column(db.Date, nullable=False) # Next date to create task
end_date = db.Column(db.Date, nullable=True) # Optional end date
# Task template settings (copied to generated tasks)
task_name_template = db.Column(db.String(500), nullable=False) # Can include {{date}} etc.
description = db.Column(db.Text, nullable=True)
priority = db.Column(db.String(20), default="medium", nullable=False) # 'low', 'medium', 'high'
estimated_hours = db.Column(db.Numeric(10, 2), nullable=True)
assigned_to = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True, index=True)
# Auto-creation settings
is_active = db.Column(db.Boolean, nullable=False, default=True)
auto_assign = db.Column(db.Boolean, nullable=False, default=False) # Auto-assign to template creator
# Tracking
created_by = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
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)
last_created_at = db.Column(db.DateTime, nullable=True) # Last time a task was created from this template
tasks_created_count = db.Column(db.Integer, default=0, nullable=False)
# Relationships
project = db.relationship("Project", backref=db.backref("recurring_tasks", lazy="dynamic"))
creator = db.relationship(
"User", foreign_keys=[created_by], backref=db.backref("created_recurring_tasks", lazy="dynamic")
)
assignee = db.relationship("User", foreign_keys=[assigned_to])
def __init__(self, name, project_id, frequency, next_run_date, created_by, **kwargs):
self.name = name
self.project_id = project_id
self.frequency = frequency
self.next_run_date = next_run_date
self.created_by = created_by
# Set optional fields
self.interval = kwargs.get("interval", 1)
self.end_date = kwargs.get("end_date")
self.task_name_template = kwargs.get("task_name_template", name)
self.description = kwargs.get("description")
self.priority = kwargs.get("priority", "medium")
self.estimated_hours = kwargs.get("estimated_hours")
self.assigned_to = kwargs.get("assigned_to")
self.is_active = kwargs.get("is_active", True)
self.auto_assign = kwargs.get("auto_assign", False)
def __repr__(self):
return f"<RecurringTask {self.name} ({self.frequency})>"
def calculate_next_run_date(self, from_date=None):
"""Calculate the next run date based on frequency and interval"""
if from_date is None:
from_date = datetime.utcnow().date()
if self.frequency == "daily":
return from_date + timedelta(days=self.interval)
elif self.frequency == "weekly":
return from_date + timedelta(weeks=self.interval)
elif self.frequency == "monthly":
return from_date + relativedelta(months=self.interval)
elif self.frequency == "yearly":
return from_date + relativedelta(years=self.interval)
else:
raise ValueError(f"Invalid frequency: {self.frequency}")
def create_task(self):
"""Create a task from this template"""
from app.models import Task
# Resolve task name template variables
task_name = self.task_name_template
task_name = task_name.replace("{{date}}", self.next_run_date.strftime("%Y-%m-%d"))
task_name = task_name.replace("{{week}}", f"Week {self.next_run_date.isocalendar()[1]}")
task_name = task_name.replace("{{month}}", self.next_run_date.strftime("%B"))
task = Task(
project_id=self.project_id,
name=task_name,
description=self.description,
priority=self.priority,
estimated_hours=float(self.estimated_hours) if self.estimated_hours else None,
assigned_to=self.assigned_to if not self.auto_assign else self.created_by,
status="todo",
)
db.session.add(task)
# Update template
self.last_created_at = datetime.utcnow()
self.tasks_created_count += 1
self.next_run_date = self.calculate_next_run_date(self.next_run_date)
# Check if we've reached the end date
if self.end_date and self.next_run_date > self.end_date:
self.is_active = False
db.session.commit()
return task
def to_dict(self):
"""Convert to dictionary"""
return {
"id": self.id,
"name": self.name,
"project_id": self.project_id,
"frequency": self.frequency,
"interval": self.interval,
"next_run_date": self.next_run_date.isoformat() if self.next_run_date else None,
"end_date": self.end_date.isoformat() if self.end_date else None,
"task_name_template": self.task_name_template,
"description": self.description,
"priority": self.priority,
"estimated_hours": float(self.estimated_hours) if self.estimated_hours else None,
"assigned_to": self.assigned_to,
"is_active": self.is_active,
"auto_assign": self.auto_assign,
"created_by": self.created_by,
"created_at": self.created_at.isoformat() if self.created_at else None,
"last_created_at": self.last_created_at.isoformat() if self.last_created_at else None,
"tasks_created_count": self.tasks_created_count,
}