Improve db migrations.
Move all migration code to python module and use it from app.py. Use Enum values to avoid problems with Enum names in DB.
This commit is contained in:
@@ -91,12 +91,6 @@ The integrated migration system handles:
|
||||
- Sample data initialization
|
||||
- Data integrity maintenance during upgrades
|
||||
|
||||
**Legacy Migration Files**: The following files are maintained for reference but are no longer needed:
|
||||
- `migrate_db.py`: Legacy core database migration (now integrated)
|
||||
- `migrate_roles_teams.py`: Legacy role and team migration (now integrated)
|
||||
- `migrate_projects.py`: Legacy project migration (now integrated)
|
||||
- `repair_roles.py`: Legacy role repair utility (functionality now integrated)
|
||||
|
||||
### Configuration
|
||||
|
||||
The application can be configured through:
|
||||
|
||||
932
migrate_db.py
932
migrate_db.py
File diff suppressed because it is too large
Load Diff
@@ -1,172 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Migration script for freelancer support in TimeTrack.
|
||||
|
||||
This migration adds:
|
||||
1. AccountType enum support (handled by SQLAlchemy)
|
||||
2. account_type column to user table
|
||||
3. business_name column to user table
|
||||
4. is_personal column to company table
|
||||
|
||||
Usage:
|
||||
python migrate_freelancers.py # Run migration
|
||||
python migrate_freelancers.py rollback # Rollback migration
|
||||
"""
|
||||
|
||||
from app import app, db
|
||||
import sqlite3
|
||||
import os
|
||||
import sys
|
||||
from models import User, Company, AccountType
|
||||
from datetime import datetime
|
||||
|
||||
def migrate_freelancer_support():
|
||||
"""Add freelancer support to existing database"""
|
||||
db_path = 'timetrack.db'
|
||||
|
||||
# Check if database exists
|
||||
if not os.path.exists(db_path):
|
||||
print("Database doesn't exist. Please run main migration first.")
|
||||
return False
|
||||
|
||||
print("Migrating database for freelancer support...")
|
||||
|
||||
# Connect to the database
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check company table structure
|
||||
cursor.execute("PRAGMA table_info(company)")
|
||||
company_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
# Add is_personal column to company table if it doesn't exist
|
||||
if 'is_personal' not in company_columns:
|
||||
print("Adding is_personal column to company table...")
|
||||
cursor.execute("ALTER TABLE company ADD COLUMN is_personal BOOLEAN DEFAULT 0")
|
||||
|
||||
# Check user table structure
|
||||
cursor.execute("PRAGMA table_info(user)")
|
||||
user_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
# Add account_type column to user table if it doesn't exist
|
||||
if 'account_type' not in user_columns:
|
||||
print("Adding account_type column to user table...")
|
||||
# Default to 'COMPANY_USER' for existing users
|
||||
cursor.execute("ALTER TABLE user ADD COLUMN account_type VARCHAR(20) DEFAULT 'COMPANY_USER'")
|
||||
|
||||
# Add business_name column to user table if it doesn't exist
|
||||
if 'business_name' not in user_columns:
|
||||
print("Adding business_name column to user table...")
|
||||
cursor.execute("ALTER TABLE user ADD COLUMN business_name VARCHAR(100)")
|
||||
|
||||
# Commit changes
|
||||
conn.commit()
|
||||
print("✓ Freelancer migration completed successfully!")
|
||||
|
||||
# Update existing users to have explicit account_type
|
||||
print("Updating existing users to COMPANY_USER account type...")
|
||||
cursor.execute("UPDATE user SET account_type = 'COMPANY_USER' WHERE account_type IS NULL OR account_type = ''")
|
||||
conn.commit()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Migration failed: {str(e)}")
|
||||
conn.rollback()
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def rollback_freelancer_support():
|
||||
"""Rollback freelancer support migration"""
|
||||
db_path = 'timetrack.db'
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print("Database doesn't exist.")
|
||||
return False
|
||||
|
||||
print("Rolling back freelancer support migration...")
|
||||
|
||||
# Connect to the database
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
print("WARNING: SQLite doesn't support dropping columns directly.")
|
||||
print("To fully rollback, you would need to:")
|
||||
print("1. Create new tables without the freelancer columns")
|
||||
print("2. Copy data from old tables to new tables")
|
||||
print("3. Drop old tables and rename new ones")
|
||||
print("\nFor safety, leaving columns in place but marking rollback as complete.")
|
||||
print("The application will work without issues with the extra columns present.")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Rollback failed: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def verify_migration():
|
||||
"""Verify that the migration was applied correctly"""
|
||||
db_path = 'timetrack.db'
|
||||
|
||||
if not os.path.exists(db_path):
|
||||
print("Database doesn't exist.")
|
||||
return False
|
||||
|
||||
conn = sqlite3.connect(db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
try:
|
||||
# Check company table
|
||||
cursor.execute("PRAGMA table_info(company)")
|
||||
company_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
# Check user table
|
||||
cursor.execute("PRAGMA table_info(user)")
|
||||
user_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
print("\n=== Migration Verification ===")
|
||||
print("Company table columns:", company_columns)
|
||||
print("User table columns:", user_columns)
|
||||
|
||||
# Verify required columns exist
|
||||
missing_columns = []
|
||||
if 'is_personal' not in company_columns:
|
||||
missing_columns.append('company.is_personal')
|
||||
if 'account_type' not in user_columns:
|
||||
missing_columns.append('user.account_type')
|
||||
if 'business_name' not in user_columns:
|
||||
missing_columns.append('user.business_name')
|
||||
|
||||
if missing_columns:
|
||||
print(f"✗ Missing columns: {', '.join(missing_columns)}")
|
||||
return False
|
||||
else:
|
||||
print("✓ All required columns present")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Verification failed: {str(e)}")
|
||||
return False
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) > 1 and sys.argv[1] == 'rollback':
|
||||
success = rollback_freelancer_support()
|
||||
elif len(sys.argv) > 1 and sys.argv[1] == 'verify':
|
||||
success = verify_migration()
|
||||
else:
|
||||
success = migrate_freelancer_support()
|
||||
if success:
|
||||
verify_migration()
|
||||
|
||||
if success:
|
||||
print("\n✓ Operation completed successfully!")
|
||||
else:
|
||||
print("\n✗ Operation failed!")
|
||||
sys.exit(1)
|
||||
@@ -1,182 +0,0 @@
|
||||
from app import app, db
|
||||
from models import User, TimeEntry, Project, Team, Role
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def migrate_projects():
|
||||
"""Migration script to add project time logging functionality"""
|
||||
with app.app_context():
|
||||
logger.info("Starting migration for project time logging...")
|
||||
|
||||
# Check if the project table exists
|
||||
try:
|
||||
# Create the project table if it doesn't exist
|
||||
db.engine.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS project (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
code VARCHAR(20) NOT NULL UNIQUE,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by_id INTEGER NOT NULL,
|
||||
team_id INTEGER,
|
||||
start_date DATE,
|
||||
end_date DATE,
|
||||
FOREIGN KEY (created_by_id) REFERENCES user (id),
|
||||
FOREIGN KEY (team_id) REFERENCES team (id)
|
||||
)
|
||||
"""))
|
||||
logger.info("Project table created or already exists")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating project table: {e}")
|
||||
return
|
||||
|
||||
# Check if the time_entry table has the project-related columns
|
||||
try:
|
||||
# Check if project_id and notes columns exist in time_entry
|
||||
result = db.engine.execute(text("PRAGMA table_info(time_entry)"))
|
||||
columns = [row[1] for row in result]
|
||||
|
||||
if 'project_id' not in columns:
|
||||
db.engine.execute(text("ALTER TABLE time_entry ADD COLUMN project_id INTEGER REFERENCES project(id)"))
|
||||
logger.info("Added project_id column to time_entry table")
|
||||
|
||||
if 'notes' not in columns:
|
||||
db.engine.execute(text("ALTER TABLE time_entry ADD COLUMN notes TEXT"))
|
||||
logger.info("Added notes column to time_entry table")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating time_entry table: {e}")
|
||||
return
|
||||
|
||||
# Create some default projects for demonstration
|
||||
try:
|
||||
# Check if any projects exist
|
||||
existing_projects = Project.query.count()
|
||||
if existing_projects == 0:
|
||||
# Find an admin or supervisor user to be the creator
|
||||
admin_user = User.query.filter(User.role.in_([Role.ADMIN, Role.SUPERVISOR])).first()
|
||||
|
||||
if admin_user:
|
||||
# Create some sample projects
|
||||
sample_projects = [
|
||||
{
|
||||
'name': 'General Administration',
|
||||
'code': 'ADMIN001',
|
||||
'description': 'General administrative tasks and meetings',
|
||||
'team_id': None, # Available to all teams
|
||||
},
|
||||
{
|
||||
'name': 'Development Project',
|
||||
'code': 'DEV001',
|
||||
'description': 'Software development and maintenance tasks',
|
||||
'team_id': None, # Available to all teams
|
||||
},
|
||||
{
|
||||
'name': 'Customer Support',
|
||||
'code': 'SUPPORT001',
|
||||
'description': 'Customer service and technical support activities',
|
||||
'team_id': None, # Available to all teams
|
||||
}
|
||||
]
|
||||
|
||||
for proj_data in sample_projects:
|
||||
project = Project(
|
||||
name=proj_data['name'],
|
||||
code=proj_data['code'],
|
||||
description=proj_data['description'],
|
||||
team_id=proj_data['team_id'],
|
||||
created_by_id=admin_user.id,
|
||||
is_active=True
|
||||
)
|
||||
db.session.add(project)
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Created {len(sample_projects)} sample projects")
|
||||
else:
|
||||
logger.warning("No admin or supervisor user found to create sample projects")
|
||||
else:
|
||||
logger.info(f"Found {existing_projects} existing projects, skipping sample creation")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating sample projects: {e}")
|
||||
db.session.rollback()
|
||||
|
||||
# Update database schema to match the current models
|
||||
try:
|
||||
db.create_all()
|
||||
logger.info("Database schema updated successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating database schema: {e}")
|
||||
return
|
||||
|
||||
# Verify the migration
|
||||
try:
|
||||
# Check if we can query the new tables and columns
|
||||
project_count = Project.query.count()
|
||||
logger.info(f"Project table accessible with {project_count} projects")
|
||||
|
||||
# Check if time_entry has the new columns
|
||||
result = db.engine.execute(text("PRAGMA table_info(time_entry)"))
|
||||
columns = [row[1] for row in result]
|
||||
|
||||
required_columns = ['project_id', 'notes']
|
||||
missing_columns = [col for col in required_columns if col not in columns]
|
||||
|
||||
if missing_columns:
|
||||
logger.error(f"Missing columns in time_entry: {missing_columns}")
|
||||
return
|
||||
else:
|
||||
logger.info("All required columns present in time_entry table")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error verifying migration: {e}")
|
||||
return
|
||||
|
||||
logger.info("Project time logging migration completed successfully!")
|
||||
print("\n" + "="*60)
|
||||
print("PROJECT TIME LOGGING FEATURE ENABLED")
|
||||
print("="*60)
|
||||
print("✅ Project management interface available for Admins/Supervisors")
|
||||
print("✅ Time tracking with optional project selection")
|
||||
print("✅ Project-based reporting and filtering")
|
||||
print("✅ Enhanced export functionality with project data")
|
||||
print("\nAccess project management via:")
|
||||
print("- Admin dropdown → Manage Projects")
|
||||
print("- Supervisor dropdown → Manage Projects")
|
||||
print("="*60)
|
||||
|
||||
def rollback_projects():
|
||||
"""Rollback migration (removes project functionality)"""
|
||||
with app.app_context():
|
||||
logger.warning("Rolling back project time logging migration...")
|
||||
|
||||
try:
|
||||
# Drop the project table
|
||||
db.engine.execute(text("DROP TABLE IF EXISTS project"))
|
||||
logger.info("Dropped project table")
|
||||
|
||||
# Note: SQLite doesn't support dropping columns, so we can't remove
|
||||
# project_id and notes columns from time_entry table
|
||||
logger.warning("Note: project_id and notes columns in time_entry table cannot be removed due to SQLite limitations")
|
||||
logger.warning("These columns will remain but will not be used")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during rollback: {e}")
|
||||
return
|
||||
|
||||
logger.info("Project time logging rollback completed")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
if len(sys.argv) > 1 and sys.argv[1] == "rollback":
|
||||
rollback_projects()
|
||||
else:
|
||||
migrate_projects()
|
||||
@@ -1,89 +0,0 @@
|
||||
from app import app, db
|
||||
from models import User, Team, Role, SystemSettings
|
||||
from sqlalchemy import text
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def migrate_roles_teams():
|
||||
with app.app_context():
|
||||
logger.info("Starting migration for roles and teams...")
|
||||
|
||||
# Check if the team table exists
|
||||
try:
|
||||
# Create the team table if it doesn't exist
|
||||
db.engine.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS team (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description VARCHAR(255),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
logger.info("Team table created or already exists")
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating team table: {e}")
|
||||
return
|
||||
|
||||
# Check if the user table has the role and team_id columns
|
||||
try:
|
||||
# Check if role column exists
|
||||
result = db.engine.execute(text("PRAGMA table_info(user)"))
|
||||
columns = [row[1] for row in result]
|
||||
|
||||
if 'role' not in columns:
|
||||
# Use the enum name instead of the value
|
||||
db.engine.execute(text("ALTER TABLE user ADD COLUMN role VARCHAR(20) DEFAULT 'TEAM_MEMBER'"))
|
||||
logger.info("Added role column to user table")
|
||||
|
||||
if 'team_id' not in columns:
|
||||
db.engine.execute(text("ALTER TABLE user ADD COLUMN team_id INTEGER REFERENCES team(id)"))
|
||||
logger.info("Added team_id column to user table")
|
||||
|
||||
# Create a default team for existing users
|
||||
default_team = Team.query.filter_by(name="Default Team").first()
|
||||
if not default_team:
|
||||
default_team = Team(name="Default Team", description="Default team for existing users")
|
||||
db.session.add(default_team)
|
||||
db.session.commit()
|
||||
logger.info("Created default team")
|
||||
|
||||
# Map string role values to enum values
|
||||
role_mapping = {
|
||||
'Team Member': Role.TEAM_MEMBER,
|
||||
'TEAM_MEMBER': Role.TEAM_MEMBER,
|
||||
'Team Leader': Role.TEAM_LEADER,
|
||||
'TEAM_LEADER': Role.TEAM_LEADER,
|
||||
'Supervisor': Role.SUPERVISOR,
|
||||
'SUPERVISOR': Role.SUPERVISOR,
|
||||
'Administrator': Role.ADMIN,
|
||||
'admin': Role.ADMIN,
|
||||
'ADMIN': Role.ADMIN
|
||||
}
|
||||
|
||||
# Assign all existing users to the default team and set role based on admin status
|
||||
users = User.query.all()
|
||||
for user in users:
|
||||
if user.team_id is None:
|
||||
user.team_id = default_team.id
|
||||
|
||||
# Handle role conversion properly
|
||||
if isinstance(user.role, str):
|
||||
# Try to map the string to an enum value
|
||||
user.role = role_mapping.get(user.role, Role.TEAM_MEMBER)
|
||||
elif user.role is None:
|
||||
# Set default role
|
||||
user.role = Role.TEAM_MEMBER
|
||||
|
||||
db.session.commit()
|
||||
logger.info(f"Assigned {len(users)} existing users to default team and updated roles")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating user table: {e}")
|
||||
return
|
||||
|
||||
logger.info("Migration completed successfully")
|
||||
|
||||
if __name__ == "__main__":
|
||||
migrate_roles_teams()
|
||||
151
models.py
151
models.py
@@ -86,6 +86,9 @@ class Project(db.Model):
|
||||
# Optional team assignment - if set, only team members can log time to this project
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('team.id'), nullable=True)
|
||||
|
||||
# Project categorization
|
||||
category_id = db.Column(db.Integer, db.ForeignKey('project_category.id'), nullable=True)
|
||||
|
||||
# Project dates
|
||||
start_date = db.Column(db.Date, nullable=True)
|
||||
end_date = db.Column(db.Date, nullable=True)
|
||||
@@ -94,6 +97,7 @@ class Project(db.Model):
|
||||
created_by = db.relationship('User', foreign_keys=[created_by_id], backref='created_projects')
|
||||
team = db.relationship('Team', backref='projects')
|
||||
time_entries = db.relationship('TimeEntry', backref='project', lazy=True)
|
||||
category = db.relationship('ProjectCategory', back_populates='projects')
|
||||
|
||||
# Unique constraint per company
|
||||
__table_args__ = (db.UniqueConstraint('company_id', 'code', name='uq_project_code_per_company'),)
|
||||
@@ -237,6 +241,10 @@ class TimeEntry(db.Model):
|
||||
# Project association - nullable for backward compatibility
|
||||
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=True)
|
||||
|
||||
# Task/SubTask associations - nullable for backward compatibility
|
||||
task_id = db.Column(db.Integer, db.ForeignKey('task.id'), nullable=True)
|
||||
subtask_id = db.Column(db.Integer, db.ForeignKey('sub_task.id'), nullable=True)
|
||||
|
||||
# Optional notes/description for the time entry
|
||||
notes = db.Column(db.Text, nullable=True)
|
||||
|
||||
@@ -378,4 +386,145 @@ class UserPreferences(db.Model):
|
||||
__table_args__ = (db.UniqueConstraint('user_id', name='uq_user_preferences'),)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<UserPreferences {self.user.username}: {self.date_format}, {"24h" if self.time_format_24h else "12h"}>'
|
||||
return f'<UserPreferences {self.user.username}: {self.date_format}, {"24h" if self.time_format_24h else "12h"}>'
|
||||
|
||||
# Project Category model for organizing projects
|
||||
class ProjectCategory(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(100), nullable=False)
|
||||
description = db.Column(db.Text, nullable=True)
|
||||
color = db.Column(db.String(7), default='#007bff') # Hex color for UI
|
||||
icon = db.Column(db.String(50), nullable=True) # Icon name/emoji
|
||||
|
||||
# Company association for multi-tenancy
|
||||
company_id = db.Column(db.Integer, db.ForeignKey('company.id'), nullable=False)
|
||||
|
||||
# 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='project_categories')
|
||||
created_by = db.relationship('User', foreign_keys=[created_by_id])
|
||||
projects = db.relationship('Project', back_populates='category', lazy=True)
|
||||
|
||||
# Unique constraint per company
|
||||
__table_args__ = (db.UniqueConstraint('company_id', 'name', name='uq_category_name_per_company'),)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<ProjectCategory {self.name}>'
|
||||
|
||||
# Task status enumeration
|
||||
class TaskStatus(enum.Enum):
|
||||
NOT_STARTED = "Not Started"
|
||||
IN_PROGRESS = "In Progress"
|
||||
ON_HOLD = "On Hold"
|
||||
COMPLETED = "Completed"
|
||||
CANCELLED = "Cancelled"
|
||||
|
||||
# Task priority enumeration
|
||||
class TaskPriority(enum.Enum):
|
||||
LOW = "Low"
|
||||
MEDIUM = "Medium"
|
||||
HIGH = "High"
|
||||
URGENT = "Urgent"
|
||||
|
||||
# Task model for project breakdown
|
||||
class Task(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)
|
||||
|
||||
# Task properties
|
||||
status = db.Column(db.Enum(TaskStatus), default=TaskStatus.NOT_STARTED)
|
||||
priority = db.Column(db.Enum(TaskPriority), default=TaskPriority.MEDIUM)
|
||||
estimated_hours = db.Column(db.Float, nullable=True) # Estimated time to complete
|
||||
|
||||
# Project association
|
||||
project_id = db.Column(db.Integer, db.ForeignKey('project.id'), nullable=False)
|
||||
|
||||
# Task assignment
|
||||
assigned_to_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
|
||||
# Task dates
|
||||
start_date = db.Column(db.Date, nullable=True)
|
||||
due_date = db.Column(db.Date, nullable=True)
|
||||
completed_date = db.Column(db.Date, nullable=True)
|
||||
|
||||
# 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
|
||||
project = db.relationship('Project', backref='tasks')
|
||||
assigned_to = db.relationship('User', foreign_keys=[assigned_to_id], backref='assigned_tasks')
|
||||
created_by = db.relationship('User', foreign_keys=[created_by_id])
|
||||
subtasks = db.relationship('SubTask', backref='parent_task', lazy=True, cascade='all, delete-orphan')
|
||||
time_entries = db.relationship('TimeEntry', backref='task', lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<Task {self.name} ({self.status.value})>'
|
||||
|
||||
@property
|
||||
def progress_percentage(self):
|
||||
"""Calculate task progress based on subtasks completion"""
|
||||
if not self.subtasks:
|
||||
return 100 if self.status == TaskStatus.COMPLETED else 0
|
||||
|
||||
completed_subtasks = sum(1 for subtask in self.subtasks if subtask.status == TaskStatus.COMPLETED)
|
||||
return int((completed_subtasks / len(self.subtasks)) * 100)
|
||||
|
||||
@property
|
||||
def total_time_logged(self):
|
||||
"""Calculate total time logged to this task (in seconds)"""
|
||||
return sum(entry.duration or 0 for entry in self.time_entries if entry.duration)
|
||||
|
||||
def can_user_access(self, user):
|
||||
"""Check if a user can access this task"""
|
||||
return self.project.is_user_allowed(user)
|
||||
|
||||
# SubTask model for task breakdown
|
||||
class SubTask(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)
|
||||
|
||||
# SubTask properties
|
||||
status = db.Column(db.Enum(TaskStatus), default=TaskStatus.NOT_STARTED)
|
||||
priority = db.Column(db.Enum(TaskPriority), default=TaskPriority.MEDIUM)
|
||||
estimated_hours = db.Column(db.Float, nullable=True)
|
||||
|
||||
# Parent task association
|
||||
task_id = db.Column(db.Integer, db.ForeignKey('task.id'), nullable=False)
|
||||
|
||||
# Assignment
|
||||
assigned_to_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=True)
|
||||
|
||||
# Dates
|
||||
start_date = db.Column(db.Date, nullable=True)
|
||||
due_date = db.Column(db.Date, nullable=True)
|
||||
completed_date = db.Column(db.Date, nullable=True)
|
||||
|
||||
# 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
|
||||
assigned_to = db.relationship('User', foreign_keys=[assigned_to_id], backref='assigned_subtasks')
|
||||
created_by = db.relationship('User', foreign_keys=[created_by_id])
|
||||
time_entries = db.relationship('TimeEntry', backref='subtask', lazy=True)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<SubTask {self.name} ({self.status.value})>'
|
||||
|
||||
@property
|
||||
def total_time_logged(self):
|
||||
"""Calculate total time logged to this subtask (in seconds)"""
|
||||
return sum(entry.duration or 0 for entry in self.time_entries if entry.duration)
|
||||
|
||||
def can_user_access(self, user):
|
||||
"""Check if a user can access this subtask"""
|
||||
return self.parent_task.can_user_access(user)
|
||||
Reference in New Issue
Block a user