Add Sprint Management feature.
This commit is contained in:
113
models.py
113
models.py
@@ -443,6 +443,9 @@ class Task(db.Model):
|
||||
|
||||
# Project association
|
||||
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
|
||||
|
||||
# Sprint association (optional)
|
||||
sprint_id = db.Column(db.Integer, db.ForeignKey('sprint.id'), nullable=True)
|
||||
|
||||
# Task assignment
|
||||
assigned_to_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
@@ -854,6 +857,116 @@ class KanbanCard(db.Model):
|
||||
"""Get project name for display purposes"""
|
||||
return self.project.name if self.project else None
|
||||
|
||||
# Sprint Management System
|
||||
class SprintStatus(enum.Enum):
|
||||
PLANNING = "Planning"
|
||||
ACTIVE = "Active"
|
||||
COMPLETED = "Completed"
|
||||
CANCELLED = "Cancelled"
|
||||
|
||||
class Sprint(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(200), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
|
||||
# Sprint status
|
||||
status = db.Column(db.Enum(SprintStatus), nullable=False, default=SprintStatus.PLANNING)
|
||||
|
||||
# Company association - sprints are company-scoped
|
||||
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
|
||||
|
||||
# Optional project association - can be project-specific or company-wide
|
||||
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=True)
|
||||
|
||||
# Sprint timeline
|
||||
start_date = db.Column(db.Date, nullable=False)
|
||||
end_date = db.Column(db.Date, nullable=False)
|
||||
|
||||
# Sprint goals and metrics
|
||||
goal = db.Column(db.Text, nullable=True) # Sprint goal description
|
||||
capacity_hours = db.Column(db.Integer, nullable=True) # Planned capacity in hours
|
||||
|
||||
# Metadata
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
updated_at = db.Column(db.DateTime, default=datetime.now, onupdate=datetime.now)
|
||||
created_by_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
|
||||
|
||||
# Relationships
|
||||
company = db.relationship('Company', backref='sprints')
|
||||
project = db.relationship('Project', backref='sprints')
|
||||
created_by = db.relationship('User', foreign_keys=[created_by_id])
|
||||
tasks = db.relationship('Task', backref='sprint', lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Sprint {self.name}>'
|
||||
|
||||
@property
|
||||
def is_current(self):
|
||||
"""Check if this sprint is currently active"""
|
||||
from datetime import date
|
||||
today = date.today()
|
||||
return (self.status == SprintStatus.ACTIVE and
|
||||
self.start_date <= today <= self.end_date)
|
||||
|
||||
@property
|
||||
def duration_days(self):
|
||||
"""Get sprint duration in days"""
|
||||
return (self.end_date - self.start_date).days + 1
|
||||
|
||||
@property
|
||||
def days_remaining(self):
|
||||
"""Get remaining days in sprint"""
|
||||
from datetime import date
|
||||
today = date.today()
|
||||
if self.end_date < today:
|
||||
return 0
|
||||
elif self.start_date > today:
|
||||
return self.duration_days
|
||||
else:
|
||||
return (self.end_date - today).days + 1
|
||||
|
||||
@property
|
||||
def progress_percentage(self):
|
||||
"""Calculate sprint progress percentage based on dates"""
|
||||
from datetime import date
|
||||
today = date.today()
|
||||
|
||||
if today < self.start_date:
|
||||
return 0
|
||||
elif today > self.end_date:
|
||||
return 100
|
||||
else:
|
||||
total_days = self.duration_days
|
||||
elapsed_days = (today - self.start_date).days + 1
|
||||
return min(100, int((elapsed_days / total_days) * 100))
|
||||
|
||||
def get_task_summary(self):
|
||||
"""Get summary of tasks in this sprint"""
|
||||
total_tasks = len(self.tasks)
|
||||
completed_tasks = len([t for t in self.tasks if t.status == TaskStatus.COMPLETED])
|
||||
in_progress_tasks = len([t for t in self.tasks if t.status == TaskStatus.IN_PROGRESS])
|
||||
|
||||
return {
|
||||
'total': total_tasks,
|
||||
'completed': completed_tasks,
|
||||
'in_progress': in_progress_tasks,
|
||||
'not_started': total_tasks - completed_tasks - in_progress_tasks,
|
||||
'completion_percentage': int((completed_tasks / total_tasks) * 100) if total_tasks > 0 else 0
|
||||
}
|
||||
|
||||
def can_user_access(self, user):
|
||||
"""Check if user can access this sprint"""
|
||||
# Must be in same company
|
||||
if self.company_id != user.company_id:
|
||||
return False
|
||||
|
||||
# If sprint is project-specific, check project access
|
||||
if self.project_id:
|
||||
return self.project.is_user_allowed(user)
|
||||
|
||||
# Company-wide sprints are accessible to all company members
|
||||
return True
|
||||
|
||||
# Dashboard Widget System
|
||||
class WidgetType(enum.Enum):
|
||||
# Time Tracking Widgets
|
||||
|
||||
Reference in New Issue
Block a user