Run DB migrations on app start.

This commit is contained in:
2025-07-01 14:47:50 +02:00
committed by Jens Luedicke
parent 66273561d8
commit 6c27bdeea7
2 changed files with 276 additions and 40 deletions

View File

@@ -65,31 +65,37 @@ pipenv install
# Activate the virtual environment
pipenv shell
# Initialize the database and run migrations
python migrate_db.py
python migrate_roles_teams.py # Add role and team support
python migrate_projects.py # Add project management
# Run the application
# Run the application (migrations run automatically on first startup)
python app.py
```
### First-Time Setup
1. **Admin Account**: Create the first admin user through the registration page
2. **System Configuration**: Access Admin Dashboard to configure system settings
3. **Team Setup**: Create teams and assign team leaders
4. **Project Creation**: Set up projects with codes and team assignments
5. **User Management**: Add users and assign appropriate roles
1. **Start the Application**: The database is automatically created and initialized on first startup
2. **Admin Account**: An initial admin user is created automatically with username `admin` and password `admin`
3. **Change Default Password**: **IMPORTANT**: Change the default admin password immediately after first login
4. **System Configuration**: Access Admin Dashboard to configure system settings
5. **Team Setup**: Create teams and assign team leaders
6. **Project Creation**: Set up projects with codes and team assignments
7. **User Management**: Add users and assign appropriate roles
### Database Migrations
The application includes several migration scripts to upgrade existing installations:
**Automatic Migration System**: All database migrations now run automatically when the application starts. No manual migration scripts need to be run.
- `migrate_db.py`: Core database initialization
- `migrate_roles_teams.py`: Add role-based access control and team management
- `migrate_projects.py`: Add project management capabilities
- `repair_roles.py`: Fix role assignments if needed
The integrated migration system handles:
- Database schema creation for new installations
- Automatic schema updates for existing databases
- User table enhancements (verification, roles, teams, 2FA)
- Project and team management table creation
- 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

278
app.py
View File

@@ -45,22 +45,270 @@ mail = Mail(app)
# Initialize the database with the app
db.init_app(app)
# Add this function to initialize system settings
# Integrated migration and initialization function
def run_migrations():
"""Run all database migrations and initialize system settings."""
import sqlite3
# Determine database path based on environment
db_path = '/data/timetrack.db' if os.path.exists('/data') else 'timetrack.db'
# Check if database exists
if not os.path.exists(db_path):
print("Database doesn't exist. Creating new database.")
db.create_all()
init_system_settings()
return
print("Running database migrations...")
# Connect to the database for raw SQL operations
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
try:
# Migrate time_entry table
cursor.execute("PRAGMA table_info(time_entry)")
time_entry_columns = [column[1] for column in cursor.fetchall()]
migrations = [
('is_paused', "ALTER TABLE time_entry ADD COLUMN is_paused BOOLEAN DEFAULT 0"),
('pause_start_time', "ALTER TABLE time_entry ADD COLUMN pause_start_time TIMESTAMP"),
('total_break_duration', "ALTER TABLE time_entry ADD COLUMN total_break_duration INTEGER DEFAULT 0"),
('user_id', "ALTER TABLE time_entry ADD COLUMN user_id INTEGER"),
('project_id', "ALTER TABLE time_entry ADD COLUMN project_id INTEGER"),
('notes', "ALTER TABLE time_entry ADD COLUMN notes TEXT")
]
for column_name, sql_command in migrations:
if column_name not in time_entry_columns:
print(f"Adding {column_name} column to time_entry...")
cursor.execute(sql_command)
# Create work_config table if it doesn't exist
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='work_config'")
if not cursor.fetchone():
print("Creating work_config table...")
cursor.execute("""
CREATE TABLE work_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
work_hours_per_day FLOAT DEFAULT 8.0,
mandatory_break_minutes INTEGER DEFAULT 30,
break_threshold_hours FLOAT DEFAULT 6.0,
additional_break_minutes INTEGER DEFAULT 15,
additional_break_threshold_hours FLOAT DEFAULT 9.0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
user_id INTEGER
)
""")
cursor.execute("""
INSERT INTO work_config (work_hours_per_day, mandatory_break_minutes, break_threshold_hours)
VALUES (8.0, 30, 6.0)
""")
else:
# Check and add missing columns to work_config
cursor.execute("PRAGMA table_info(work_config)")
work_config_columns = [column[1] for column in cursor.fetchall()]
work_config_migrations = [
('additional_break_minutes', "ALTER TABLE work_config ADD COLUMN additional_break_minutes INTEGER DEFAULT 15"),
('additional_break_threshold_hours', "ALTER TABLE work_config ADD COLUMN additional_break_threshold_hours FLOAT DEFAULT 9.0"),
('user_id', "ALTER TABLE work_config ADD COLUMN user_id INTEGER")
]
for column_name, sql_command in work_config_migrations:
if column_name not in work_config_columns:
print(f"Adding {column_name} column to work_config...")
cursor.execute(sql_command)
# Migrate user table
cursor.execute("PRAGMA table_info(user)")
user_columns = [column[1] for column in cursor.fetchall()]
user_migrations = [
('is_verified', "ALTER TABLE user ADD COLUMN is_verified BOOLEAN DEFAULT 0"),
('verification_token', "ALTER TABLE user ADD COLUMN verification_token VARCHAR(100)"),
('token_expiry', "ALTER TABLE user ADD COLUMN token_expiry TIMESTAMP"),
('is_blocked', "ALTER TABLE user ADD COLUMN is_blocked BOOLEAN DEFAULT 0"),
('role', "ALTER TABLE user ADD COLUMN role VARCHAR(50) DEFAULT 'Team Member'"),
('team_id', "ALTER TABLE user ADD COLUMN team_id INTEGER"),
('two_factor_enabled', "ALTER TABLE user ADD COLUMN two_factor_enabled BOOLEAN DEFAULT 0"),
('two_factor_secret', "ALTER TABLE user ADD COLUMN two_factor_secret VARCHAR(32)")
]
for column_name, sql_command in user_migrations:
if column_name not in user_columns:
print(f"Adding {column_name} column to user...")
cursor.execute(sql_command)
# Create team table if it doesn't exist
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='team'")
if not cursor.fetchone():
print("Creating team table...")
cursor.execute("""
CREATE TABLE team (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name VARCHAR(100) UNIQUE NOT NULL,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Create system_settings table if it doesn't exist
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='system_settings'")
if not cursor.fetchone():
print("Creating system_settings table...")
cursor.execute("""
CREATE TABLE system_settings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key VARCHAR(50) UNIQUE NOT NULL,
value VARCHAR(255) NOT NULL,
description VARCHAR(255),
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
# Create project table if it doesn't exist
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='project'")
if not cursor.fetchone():
print("Creating project table...")
cursor.execute("""
CREATE TABLE 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 TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP 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)
)
""")
# Commit all schema changes
conn.commit()
except Exception as e:
print(f"Error during database migration: {e}")
conn.rollback()
raise
finally:
conn.close()
# Now use SQLAlchemy for data migrations
db.create_all() # This will create any remaining tables defined in models
# Initialize system settings
init_system_settings()
# Handle admin user and data migrations
migrate_data()
print("Database migrations completed successfully!")
def init_system_settings():
# Check if registration_enabled setting exists, if not create it
"""Initialize system settings with default values if they don't exist"""
if not SystemSettings.query.filter_by(key='registration_enabled').first():
registration_setting = SystemSettings(
print("Adding registration_enabled system setting...")
reg_setting = SystemSettings(
key='registration_enabled',
value='true',
description='Controls whether new user registration is allowed'
)
db.session.add(registration_setting)
db.session.add(reg_setting)
db.session.commit()
# Call this function during app initialization (add it where you initialize the app)
def migrate_data():
"""Handle data migrations and setup"""
# Check if admin user exists
admin = User.query.filter_by(username='admin').first()
if not admin:
# Create admin user
admin = User(
username='admin',
email='admin@timetrack.local',
is_admin=True,
is_verified=True,
role=Role.ADMIN,
two_factor_enabled=False
)
admin.set_password('admin')
db.session.add(admin)
db.session.commit()
print("Created admin user with username 'admin' and password 'admin'")
print("IMPORTANT: Change the admin password after first login!")
else:
# Update existing admin user with new fields
admin.is_verified = True
if not admin.role:
admin.role = Role.ADMIN
if admin.two_factor_enabled is None:
admin.two_factor_enabled = False
db.session.commit()
# Update orphaned records
orphan_entries = TimeEntry.query.filter_by(user_id=None).all()
for entry in orphan_entries:
entry.user_id = admin.id
orphan_configs = WorkConfig.query.filter_by(user_id=None).all()
for config in orphan_configs:
config.user_id = admin.id
# Update existing users
users_to_update = User.query.all()
for user in users_to_update:
if user.is_verified is None:
user.is_verified = True
if not user.role:
user.role = Role.ADMIN if user.is_admin else Role.TEAM_MEMBER
if user.two_factor_enabled is None:
user.two_factor_enabled = False
# Create sample projects if none exist
if Project.query.count() == 0:
sample_projects = [
{
'name': 'General Administration',
'code': 'ADMIN001',
'description': 'General administrative tasks and meetings'
},
{
'name': 'Development Project',
'code': 'DEV001',
'description': 'Software development and maintenance tasks'
},
{
'name': 'Customer Support',
'code': 'SUPPORT001',
'description': 'Customer service and technical support activities'
}
]
for proj_data in sample_projects:
project = Project(
name=proj_data['name'],
code=proj_data['code'],
description=proj_data['description'],
created_by_id=admin.id,
is_active=True
)
db.session.add(project)
print(f"Created {len(sample_projects)} sample projects")
db.session.commit()
# Call this function during app initialization
@app.before_first_request
def initialize_app():
init_system_settings()
run_migrations()
# Add this after initializing the app but before defining routes
@app.context_processor
@@ -829,24 +1077,6 @@ def config():
return render_template('config.html', title='Configuration', config=config)
# Create the database tables before first request
@app.before_first_request
def create_tables():
# This will only create tables that don't exist yet
db.create_all()
# Check if we need to add new columns
from sqlalchemy import inspect
inspector = inspect(db.engine)
# Check if user table exists
if 'user' in inspector.get_table_names():
columns = [column['name'] for column in inspector.get_columns('user')]
# Check for verification columns
if 'is_verified' not in columns or 'verification_token' not in columns or 'token_expiry' not in columns:
logger.warning("Database schema is outdated. Please run migrate_db.py to update it.")
print("WARNING: Database schema is outdated. Please run migrate_db.py to update it.")
@app.route('/api/delete/<int:entry_id>', methods=['DELETE'])
@login_required