Add Freelancer registration model.
This commit is contained in:
180
MIGRATION_FREELANCERS.md
Normal file
180
MIGRATION_FREELANCERS.md
Normal file
@@ -0,0 +1,180 @@
|
||||
# Freelancer Migration Guide
|
||||
|
||||
This document explains the database migration for freelancer support in TimeTrack.
|
||||
|
||||
## Overview
|
||||
|
||||
The freelancer migration adds support for independent users who can register without a company token. It introduces:
|
||||
|
||||
1. **Account Types**: Users can be either "Company User" or "Freelancer"
|
||||
2. **Personal Companies**: Freelancers automatically get their own company workspace
|
||||
3. **Business Names**: Optional field for freelancers to specify their business name
|
||||
|
||||
## Database Changes
|
||||
|
||||
### User Table Changes
|
||||
- `account_type` VARCHAR(20) DEFAULT 'Company User' - Type of account
|
||||
- `business_name` VARCHAR(100) - Optional business name for freelancers
|
||||
- `company_id` INTEGER - Foreign key to company table (for multi-tenancy)
|
||||
|
||||
### Company Table Changes
|
||||
- `is_personal` BOOLEAN DEFAULT 0 - Marks companies auto-created for freelancers
|
||||
|
||||
## Migration Options
|
||||
|
||||
### Option 1: Automatic Migration (Recommended)
|
||||
The main migration script (`migrate_db.py`) now includes freelancer support:
|
||||
|
||||
```bash
|
||||
python migrate_db.py
|
||||
```
|
||||
|
||||
This will:
|
||||
- Add new columns to existing tables
|
||||
- Create company table if it doesn't exist
|
||||
- Set default values for existing users
|
||||
|
||||
### Option 2: Dedicated Freelancer Migration
|
||||
Use the dedicated freelancer migration script:
|
||||
|
||||
```bash
|
||||
python migrate_freelancers.py
|
||||
```
|
||||
|
||||
### Option 3: Manual SQL Migration
|
||||
If you prefer manual control:
|
||||
|
||||
```sql
|
||||
-- Add columns to user table
|
||||
ALTER TABLE user ADD COLUMN account_type VARCHAR(20) DEFAULT 'Company User';
|
||||
ALTER TABLE user ADD COLUMN business_name VARCHAR(100);
|
||||
ALTER TABLE user ADD COLUMN company_id INTEGER;
|
||||
|
||||
-- Create company table (if it doesn't exist)
|
||||
CREATE TABLE company (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
slug VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_personal BOOLEAN DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
max_users INTEGER DEFAULT 100
|
||||
);
|
||||
|
||||
-- Or add column to existing company table
|
||||
ALTER TABLE company ADD COLUMN is_personal BOOLEAN DEFAULT 0;
|
||||
|
||||
-- Update existing users
|
||||
UPDATE user SET account_type = 'Company User' WHERE account_type IS NULL;
|
||||
```
|
||||
|
||||
## Post-Migration Steps
|
||||
|
||||
### For Existing Installations
|
||||
1. **Create Default Company**: If you have existing users without a company, create one:
|
||||
```python
|
||||
# In Python/Flask shell
|
||||
from models import db, Company, User
|
||||
|
||||
# Create default company
|
||||
company = Company(
|
||||
name="Default Company",
|
||||
slug="default-company",
|
||||
description="Default company for existing users"
|
||||
)
|
||||
db.session.add(company)
|
||||
db.session.flush()
|
||||
|
||||
# Assign existing users to default company
|
||||
User.query.filter_by(company_id=None).update({'company_id': company.id})
|
||||
db.session.commit()
|
||||
```
|
||||
|
||||
2. **Verify Migration**: Check that all users have a company_id:
|
||||
```sql
|
||||
SELECT COUNT(*) FROM user WHERE company_id IS NULL;
|
||||
-- Should return 0
|
||||
```
|
||||
|
||||
### Testing Freelancer Registration
|
||||
1. Visit `/register/freelancer`
|
||||
2. Register a new freelancer account
|
||||
3. Verify the personal company was created
|
||||
4. Test login and time tracking functionality
|
||||
|
||||
## New Features Available
|
||||
|
||||
### Freelancer Registration
|
||||
- **URL**: `/register/freelancer`
|
||||
- **Features**:
|
||||
- No company token required
|
||||
- Auto-creates personal workspace
|
||||
- Optional business name field
|
||||
- Immediate account activation
|
||||
|
||||
### Registration Options
|
||||
- **Company Registration**: `/register` (existing)
|
||||
- **Freelancer Registration**: `/register/freelancer` (new)
|
||||
- **Login Page**: Shows both registration options
|
||||
|
||||
### User Experience
|
||||
- Freelancers get admin privileges in their personal company
|
||||
- Can create projects and track time immediately
|
||||
- Personal workspace is limited to 1 user by default
|
||||
- Can optionally expand to hire employees later
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Migration fails with "column already exists"**
|
||||
- This is normal if you've run the migration before
|
||||
- The migration script checks for existing columns
|
||||
|
||||
**Users missing company_id after migration**
|
||||
- Run the post-migration steps above to assign a default company
|
||||
|
||||
**Freelancer registration fails**
|
||||
- Check that the AccountType enum is imported correctly
|
||||
- Verify database migration completed successfully
|
||||
|
||||
### Rollback (Limited)
|
||||
SQLite doesn't support dropping columns, so rollback is limited:
|
||||
|
||||
```bash
|
||||
python migrate_freelancers.py rollback
|
||||
```
|
||||
|
||||
For full rollback, you would need to:
|
||||
1. Export user data
|
||||
2. Recreate tables without freelancer columns
|
||||
3. Re-import data
|
||||
|
||||
## Verification Commands
|
||||
|
||||
```bash
|
||||
# Verify migration applied
|
||||
python migrate_freelancers.py verify
|
||||
|
||||
# Check table structure
|
||||
sqlite3 timetrack.db ".schema user"
|
||||
sqlite3 timetrack.db ".schema company"
|
||||
|
||||
# Check data
|
||||
sqlite3 timetrack.db "SELECT account_type, COUNT(*) FROM user GROUP BY account_type;"
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Freelancers get unique usernames/emails globally (not per-company)
|
||||
- Personal companies are limited to 1 user by default
|
||||
- Freelancers have admin privileges only in their personal workspace
|
||||
- Multi-tenant isolation is maintained
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Allow freelancers to upgrade to team accounts
|
||||
- Billing integration for freelancer vs company accounts
|
||||
- Advanced freelancer-specific features
|
||||
- Integration with invoicing systems
|
||||
126
app.py
126
app.py
@@ -1,5 +1,5 @@
|
||||
from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, session, g, Response, send_file
|
||||
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion
|
||||
from models import db, TimeEntry, WorkConfig, User, SystemSettings, Team, Role, Project, Company, CompanyWorkConfig, UserPreferences, WorkRegion, AccountType
|
||||
from data_formatting import (
|
||||
format_duration, prepare_export_data, prepare_team_hours_export_data,
|
||||
format_table_data, format_graph_data, format_team_data
|
||||
@@ -159,7 +159,9 @@ def run_migrations():
|
||||
('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)")
|
||||
('two_factor_secret', "ALTER TABLE user ADD COLUMN two_factor_secret VARCHAR(32)"),
|
||||
('account_type', "ALTER TABLE user ADD COLUMN account_type VARCHAR(20) DEFAULT 'Company User'"),
|
||||
('business_name', "ALTER TABLE user ADD COLUMN business_name VARCHAR(100)")
|
||||
]
|
||||
|
||||
for column_name, sql_command in user_migrations:
|
||||
@@ -193,7 +195,10 @@ def run_migrations():
|
||||
role VARCHAR(50) DEFAULT 'Team Member',
|
||||
team_id INTEGER,
|
||||
two_factor_enabled BOOLEAN DEFAULT 0,
|
||||
two_factor_secret VARCHAR(32)
|
||||
two_factor_secret VARCHAR(32),
|
||||
account_type VARCHAR(20) DEFAULT 'Company User',
|
||||
business_name VARCHAR(100),
|
||||
company_id INTEGER
|
||||
)
|
||||
""")
|
||||
|
||||
@@ -201,10 +206,13 @@ def run_migrations():
|
||||
cursor.execute("""
|
||||
INSERT INTO user_new (id, username, email, password_hash, created_at, is_verified,
|
||||
verification_token, token_expiry, is_blocked, role, team_id,
|
||||
two_factor_enabled, two_factor_secret)
|
||||
two_factor_enabled, two_factor_secret, account_type, business_name, company_id)
|
||||
SELECT id, username, email, password_hash, created_at, is_verified,
|
||||
verification_token, token_expiry, is_blocked, role, team_id,
|
||||
two_factor_enabled, two_factor_secret
|
||||
two_factor_enabled, two_factor_secret,
|
||||
COALESCE(account_type, 'Company User'),
|
||||
business_name,
|
||||
company_id
|
||||
FROM user
|
||||
""")
|
||||
|
||||
@@ -276,10 +284,24 @@ def run_migrations():
|
||||
slug VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_personal BOOLEAN DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
max_users INTEGER DEFAULT 100
|
||||
)
|
||||
""")
|
||||
else:
|
||||
# Check and add missing columns to existing company table
|
||||
cursor.execute("PRAGMA table_info(company)")
|
||||
company_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
company_migrations = [
|
||||
('is_personal', "ALTER TABLE company ADD COLUMN is_personal BOOLEAN DEFAULT 0")
|
||||
]
|
||||
|
||||
for column_name, sql_command in company_migrations:
|
||||
if column_name not in company_columns:
|
||||
print(f"Adding {column_name} column to company...")
|
||||
cursor.execute(sql_command)
|
||||
|
||||
# Create company_work_config table if it doesn't exist
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='company_work_config'")
|
||||
@@ -963,6 +985,100 @@ The TimeTrack Team
|
||||
|
||||
return render_template('register.html', title='Register')
|
||||
|
||||
@app.route('/register/freelancer', methods=['GET', 'POST'])
|
||||
def register_freelancer():
|
||||
"""Freelancer registration route - creates user without company token"""
|
||||
# Check if registration is enabled
|
||||
registration_enabled = get_system_setting('registration_enabled', 'true') == 'true'
|
||||
|
||||
if not registration_enabled:
|
||||
flash('Registration is currently disabled by the administrator.', 'error')
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
username = request.form.get('username')
|
||||
email = request.form.get('email')
|
||||
password = request.form.get('password')
|
||||
confirm_password = request.form.get('confirm_password')
|
||||
business_name = request.form.get('business_name', '').strip()
|
||||
|
||||
# Validate input
|
||||
error = None
|
||||
if not username:
|
||||
error = 'Username is required'
|
||||
elif not email:
|
||||
error = 'Email is required'
|
||||
elif not password:
|
||||
error = 'Password is required'
|
||||
elif password != confirm_password:
|
||||
error = 'Passwords do not match'
|
||||
|
||||
# Check for existing users globally (freelancers get unique usernames/emails)
|
||||
if not error:
|
||||
if User.query.filter_by(username=username).first():
|
||||
error = 'Username already exists'
|
||||
elif User.query.filter_by(email=email).first():
|
||||
error = 'Email already registered'
|
||||
|
||||
if error is None:
|
||||
try:
|
||||
# Create personal company for freelancer
|
||||
company_name = business_name if business_name else f"{username}'s Workspace"
|
||||
|
||||
# Generate unique company slug
|
||||
import re
|
||||
slug = re.sub(r'[^\w\s-]', '', company_name.lower())
|
||||
slug = re.sub(r'[-\s]+', '-', slug).strip('-')
|
||||
|
||||
# Ensure slug uniqueness
|
||||
base_slug = slug
|
||||
counter = 1
|
||||
while Company.query.filter_by(slug=slug).first():
|
||||
slug = f"{base_slug}-{counter}"
|
||||
counter += 1
|
||||
|
||||
# Create personal company
|
||||
personal_company = Company(
|
||||
name=company_name,
|
||||
slug=slug,
|
||||
description=f"Personal workspace for {username}",
|
||||
is_personal=True,
|
||||
max_users=1 # Limit to single user
|
||||
)
|
||||
|
||||
db.session.add(personal_company)
|
||||
db.session.flush() # Get company ID
|
||||
|
||||
# Create freelancer user
|
||||
new_user = User(
|
||||
username=username,
|
||||
email=email,
|
||||
company_id=personal_company.id,
|
||||
account_type=AccountType.FREELANCER,
|
||||
business_name=business_name if business_name else None,
|
||||
role=Role.ADMIN, # Freelancers are admins of their personal company
|
||||
is_verified=True # Auto-verify freelancers
|
||||
)
|
||||
new_user.set_password(password)
|
||||
|
||||
db.session.add(new_user)
|
||||
db.session.commit()
|
||||
|
||||
logger.info(f"Freelancer account created: {username} with personal company: {company_name}")
|
||||
flash(f'Welcome {username}! Your freelancer account has been created successfully. You can now log in.', 'success')
|
||||
|
||||
return redirect(url_for('login'))
|
||||
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
logger.error(f"Error during freelancer registration: {str(e)}")
|
||||
error = f"An error occurred during registration: {str(e)}"
|
||||
|
||||
if error:
|
||||
flash(error, 'error')
|
||||
|
||||
return render_template('register_freelancer.html', title='Register as Freelancer')
|
||||
|
||||
@app.route('/setup_company', methods=['GET', 'POST'])
|
||||
def setup_company():
|
||||
"""Company setup route for creating new companies with admin users"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from app import app, db
|
||||
import sqlite3
|
||||
import os
|
||||
from models import User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project
|
||||
from models import User, TimeEntry, WorkConfig, SystemSettings, Team, Role, Project, Company, AccountType
|
||||
from werkzeug.security import generate_password_hash
|
||||
from datetime import datetime
|
||||
|
||||
@@ -117,6 +117,21 @@ def migrate_database():
|
||||
print("Adding team_id column to user table...")
|
||||
cursor.execute("ALTER TABLE user ADD COLUMN team_id INTEGER")
|
||||
|
||||
# Add freelancer support columns to user table
|
||||
if 'account_type' not in user_columns:
|
||||
print("Adding account_type column to user table...")
|
||||
cursor.execute("ALTER TABLE user ADD COLUMN account_type VARCHAR(20) DEFAULT 'Company User'")
|
||||
|
||||
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)")
|
||||
|
||||
# Add company_id to user table for multi-tenancy
|
||||
if 'company_id' not in user_columns:
|
||||
print("Adding company_id column to user table...")
|
||||
# Note: We can't add NOT NULL constraint to existing table, so allow NULL initially
|
||||
cursor.execute("ALTER TABLE user ADD COLUMN company_id INTEGER")
|
||||
|
||||
# Add 2FA columns to user table if they don't exist
|
||||
if 'two_factor_enabled' not in user_columns:
|
||||
print("Adding two_factor_enabled column to user table...")
|
||||
@@ -175,6 +190,31 @@ def migrate_database():
|
||||
)
|
||||
""")
|
||||
|
||||
# Check if the company table exists
|
||||
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='company'")
|
||||
if not cursor.fetchone():
|
||||
print("Creating company table...")
|
||||
cursor.execute("""
|
||||
CREATE TABLE company (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
slug VARCHAR(50) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
is_personal BOOLEAN DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
max_users INTEGER DEFAULT 100
|
||||
)
|
||||
""")
|
||||
else:
|
||||
# Check if company table has freelancer columns
|
||||
cursor.execute("PRAGMA table_info(company)")
|
||||
company_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
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")
|
||||
|
||||
# Add project-related columns to time_entry table
|
||||
cursor.execute("PRAGMA table_info(time_entry)")
|
||||
time_entry_columns = [column[1] for column in cursor.fetchall()]
|
||||
|
||||
172
migrate_freelancers.py
Normal file
172
migrate_freelancers.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/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)
|
||||
12
models.py
12
models.py
@@ -13,6 +13,11 @@ class Role(enum.Enum):
|
||||
SUPERVISOR = "Supervisor"
|
||||
ADMIN = "Administrator" # Keep existing admin role
|
||||
|
||||
# Define Account Type for freelancer support
|
||||
class AccountType(enum.Enum):
|
||||
COMPANY_USER = "Company User"
|
||||
FREELANCER = "Freelancer"
|
||||
|
||||
# Company model for multi-tenancy
|
||||
class Company(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
@@ -21,6 +26,9 @@ class Company(db.Model):
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.now)
|
||||
|
||||
# Freelancer support
|
||||
is_personal = db.Column(db.Boolean, default=False) # True for auto-created freelancer companies
|
||||
|
||||
# Company settings
|
||||
is_active = db.Column(db.Boolean, default=True)
|
||||
max_users = db.Column(db.Integer, default=100) # Optional user limit
|
||||
@@ -135,6 +143,10 @@ class User(db.Model):
|
||||
role = db.Column(db.Enum(Role), default=Role.TEAM_MEMBER)
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('team.id'), nullable=True)
|
||||
|
||||
# Freelancer support
|
||||
account_type = db.Column(db.Enum(AccountType), default=AccountType.COMPANY_USER)
|
||||
business_name = db.Column(db.String(100), nullable=True) # Optional business name for freelancers
|
||||
|
||||
# Unique constraints per company
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('company_id', 'username', name='uq_user_username_per_company'),
|
||||
|
||||
@@ -35,7 +35,8 @@
|
||||
</div>
|
||||
|
||||
<div class="auth-links">
|
||||
<p>Don't have an account? <a href="{{ url_for('register') }}">Register here</a></p>
|
||||
<p>Don't have an account?</p>
|
||||
<p><a href="{{ url_for('register') }}">Register with Company Code</a> | <a href="{{ url_for('register_freelancer') }}">Register as Freelancer</a></p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<h1>Register for TimeTrack</h1>
|
||||
<p class="text-muted">Join your company team</p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
@@ -12,6 +13,14 @@
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<div class="registration-options mb-4">
|
||||
<div class="alert alert-info">
|
||||
<h5>Registration Options:</h5>
|
||||
<p><strong>Company Employee:</strong> You're on the right page! Enter your company code below.</p>
|
||||
<p><strong>Freelancer/Independent:</strong> <a href="{{ url_for('register_freelancer') }}" class="btn btn-outline-primary btn-sm">Register as Freelancer</a></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="POST" action="{{ url_for('register') }}" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="company_code">Company Code</label>
|
||||
|
||||
65
templates/register_freelancer.html
Normal file
65
templates/register_freelancer.html
Normal file
@@ -0,0 +1,65 @@
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="auth-container">
|
||||
<h1>Register as Freelancer</h1>
|
||||
<p class="text-muted">Create your independent freelancer account</p>
|
||||
|
||||
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
<div class="alert alert-{{ category }}">{{ message }}</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
<form method="POST" action="{{ url_for('register_freelancer') }}" class="auth-form">
|
||||
<div class="form-group">
|
||||
<label for="username">Username</label>
|
||||
<input type="text" id="username" name="username" class="form-control" required autofocus>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="email">Email</label>
|
||||
<input type="email" id="email" name="email" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="business_name">Business Name (Optional)</label>
|
||||
<input type="text" id="business_name" name="business_name" class="form-control"
|
||||
placeholder="Your business or freelance name">
|
||||
<small class="form-text text-muted">Leave blank to use your username as your workspace name.</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">Password</label>
|
||||
<input type="password" id="password" name="password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="confirm_password">Confirm Password</label>
|
||||
<input type="password" id="confirm_password" name="confirm_password" class="form-control" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn btn-primary">Create Freelancer Account</button>
|
||||
</div>
|
||||
|
||||
<div class="auth-links">
|
||||
<p>Working for a company? <a href="{{ url_for('register') }}">Register with company code</a></p>
|
||||
<p>Already have an account? <a href="{{ url_for('login') }}">Login here</a></p>
|
||||
</div>
|
||||
|
||||
<div class="freelancer-info">
|
||||
<h4>What you get as a freelancer:</h4>
|
||||
<ul>
|
||||
<li>Your own personal workspace</li>
|
||||
<li>Time tracking for your projects</li>
|
||||
<li>Project management tools</li>
|
||||
<li>Export capabilities for invoicing</li>
|
||||
<li>No company code required</li>
|
||||
</ul>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user