data seed feature for demo and prod
This commit is contained in:
@@ -1,167 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Create default admin user for the platform.
|
||||
|
||||
Usage:
|
||||
python scripts/create_admin.py
|
||||
|
||||
This script:
|
||||
- Creates a default admin user if one doesn't exist
|
||||
- Can be run multiple times safely (idempotent)
|
||||
- Should be run AFTER database migrations
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.database import SessionLocal, engine
|
||||
from models.database.user import User
|
||||
from middleware.auth import AuthManager
|
||||
|
||||
# Default admin credentials
|
||||
DEFAULT_ADMIN_EMAIL = "admin@platform.com"
|
||||
DEFAULT_ADMIN_USERNAME = "admin"
|
||||
DEFAULT_ADMIN_PASSWORD = "admin123" # Change this in production!
|
||||
|
||||
|
||||
def create_admin_user(db: Session) -> bool:
|
||||
"""
|
||||
Create default admin user if it doesn't exist.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
|
||||
Returns:
|
||||
bool: True if user was created, False if already exists
|
||||
"""
|
||||
auth_manager = AuthManager()
|
||||
|
||||
# Check if admin user already exists
|
||||
existing_admin = db.execute(
|
||||
select(User).where(User.username == DEFAULT_ADMIN_USERNAME)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if existing_admin:
|
||||
print(f"ℹ️ Admin user '{DEFAULT_ADMIN_USERNAME}' already exists")
|
||||
print(f" Email: {existing_admin.email}")
|
||||
print(f" Role: {existing_admin.role}")
|
||||
print(f" Active: {existing_admin.is_active}")
|
||||
return False
|
||||
|
||||
# Create new admin user
|
||||
print(f"📝 Creating admin user...")
|
||||
|
||||
admin_user = User(
|
||||
email=DEFAULT_ADMIN_EMAIL,
|
||||
username=DEFAULT_ADMIN_USERNAME,
|
||||
hashed_password=auth_manager.hash_password(DEFAULT_ADMIN_PASSWORD),
|
||||
role="admin",
|
||||
is_active=True
|
||||
)
|
||||
|
||||
db.add(admin_user)
|
||||
db.commit()
|
||||
db.refresh(admin_user)
|
||||
|
||||
print("\n✅ Admin user created successfully!")
|
||||
print("\n" + "=" * 50)
|
||||
print(" Admin Credentials:")
|
||||
print("=" * 50)
|
||||
print(f" Email: {DEFAULT_ADMIN_EMAIL}")
|
||||
print(f" Username: {DEFAULT_ADMIN_USERNAME}")
|
||||
print(f" Password: {DEFAULT_ADMIN_PASSWORD}")
|
||||
print("=" * 50)
|
||||
print("\n⚠️ IMPORTANT: Change the password after first login!")
|
||||
print(f" Login at: http://localhost:8000/static/admin/login.html")
|
||||
print()
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def verify_database_ready() -> bool:
|
||||
"""
|
||||
Verify that database tables exist.
|
||||
|
||||
Returns:
|
||||
bool: True if database is ready, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Try to query the users table
|
||||
with engine.connect() as conn:
|
||||
from sqlalchemy import text
|
||||
result = conn.execute(
|
||||
text("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
|
||||
)
|
||||
tables = result.fetchall()
|
||||
return len(tables) > 0
|
||||
except Exception as e:
|
||||
print(f"❌ Error checking database: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to create admin user."""
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(" Admin User Creation Script")
|
||||
print("=" * 50 + "\n")
|
||||
|
||||
# Step 1: Verify database is ready
|
||||
print("1️⃣ Checking database...")
|
||||
|
||||
if not verify_database_ready():
|
||||
print("\n❌ ERROR: Database not ready!")
|
||||
print("\n The 'users' table doesn't exist yet.")
|
||||
print(" Please run database migrations first:")
|
||||
print()
|
||||
print(" alembic upgrade head")
|
||||
print()
|
||||
print(" Or if using make:")
|
||||
print(" make migrate-up")
|
||||
print()
|
||||
sys.exit(1)
|
||||
|
||||
print(" ✓ Database is ready")
|
||||
|
||||
# Step 2: Create admin user
|
||||
print("\n2️⃣ Creating admin user...")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
created = create_admin_user(db)
|
||||
|
||||
if created:
|
||||
print("\n🎉 Setup complete! You can now:")
|
||||
print(" 1. Start the server: uvicorn main:app --reload")
|
||||
print(" 2. Login at: http://localhost:8000/static/admin/login.html")
|
||||
print()
|
||||
else:
|
||||
print("\n✓ No changes needed - admin user already exists")
|
||||
print()
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ ERROR: Failed to create admin user")
|
||||
print(f" {type(e).__name__}: {e}")
|
||||
print()
|
||||
print(" Common issues:")
|
||||
print(" - Database migrations not run")
|
||||
print(" - Database connection issues")
|
||||
print(" - Permissions problems")
|
||||
print()
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
416
scripts/init_production.py
Normal file
416
scripts/init_production.py
Normal file
@@ -0,0 +1,416 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Production Database Initialization for Wizamart Platform
|
||||
|
||||
This script initializes ESSENTIAL data required for production:
|
||||
- Platform admin user
|
||||
- Default vendor roles and permissions
|
||||
- Admin settings
|
||||
- Platform configuration
|
||||
|
||||
This is SAFE to run in production and should be run after migrations.
|
||||
|
||||
Usage:
|
||||
make init-prod
|
||||
|
||||
This script is idempotent - safe to run multiple times.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.core.config import settings, print_environment_info, validate_production_settings
|
||||
from app.core.environment import is_production, get_environment
|
||||
from models.database.user import User
|
||||
from models.database.admin import AdminSetting
|
||||
from middleware.auth import AuthManager
|
||||
from app.core.permissions import PermissionGroups
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def print_header(text: str):
|
||||
"""Print formatted header."""
|
||||
print("\n" + "=" * 70)
|
||||
print(f" {text}")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
def print_step(step: int, text: str):
|
||||
"""Print step indicator."""
|
||||
print(f"\n[{step}] {text}")
|
||||
|
||||
|
||||
def print_success(text: str):
|
||||
"""Print success message."""
|
||||
print(f" ✓ {text}")
|
||||
|
||||
|
||||
def print_warning(text: str):
|
||||
"""Print warning message."""
|
||||
print(f" ⚠ {text}")
|
||||
|
||||
|
||||
def print_error(text: str):
|
||||
"""Print error message."""
|
||||
print(f" ✗ {text}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# INITIALIZATION FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def create_admin_user(db: Session, auth_manager: AuthManager) -> User:
|
||||
"""Create or get the platform admin user."""
|
||||
|
||||
# Check if admin already exists
|
||||
admin = db.execute(
|
||||
select(User).where(User.email == settings.admin_email)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if admin:
|
||||
print_warning(f"Admin user already exists: {admin.email}")
|
||||
return admin
|
||||
|
||||
# Create new admin
|
||||
hashed_password = auth_manager.hash_password(settings.admin_password)
|
||||
|
||||
admin = User(
|
||||
username=settings.admin_username,
|
||||
email=settings.admin_email,
|
||||
hashed_password=hashed_password,
|
||||
role="admin",
|
||||
first_name=settings.admin_first_name,
|
||||
last_name=settings.admin_last_name,
|
||||
is_active=True,
|
||||
is_email_verified=True,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
db.add(admin)
|
||||
db.flush()
|
||||
|
||||
print_success(f"Created admin user: {admin.email}")
|
||||
return admin
|
||||
|
||||
|
||||
def create_default_role_templates(db: Session) -> dict:
|
||||
"""Create default role templates (not tied to any vendor).
|
||||
|
||||
These serve as templates that can be copied when creating vendor-specific roles.
|
||||
Note: Actual roles are vendor-specific and created when vendors are onboarded.
|
||||
"""
|
||||
|
||||
print(" Available role presets:")
|
||||
print(" - Manager: Nearly full access (except team management)")
|
||||
print(" - Staff: Day-to-day operations")
|
||||
print(" - Support: Customer service focused")
|
||||
print(" - Viewer: Read-only access")
|
||||
print(" - Marketing: Marketing and customer communication")
|
||||
|
||||
print_success("Role templates ready for vendor onboarding")
|
||||
|
||||
return {
|
||||
"manager": list(PermissionGroups.MANAGER),
|
||||
"staff": list(PermissionGroups.STAFF),
|
||||
"support": list(PermissionGroups.SUPPORT),
|
||||
"viewer": list(PermissionGroups.VIEWER),
|
||||
"marketing": list(PermissionGroups.MARKETING),
|
||||
}
|
||||
|
||||
|
||||
def create_admin_settings(db: Session) -> int:
|
||||
"""Create essential admin settings."""
|
||||
|
||||
settings_created = 0
|
||||
|
||||
# Essential platform settings
|
||||
default_settings = [
|
||||
{
|
||||
"key": "platform_name",
|
||||
"value": settings.project_name,
|
||||
"value_type": "string",
|
||||
"description": "Platform name displayed in admin panel",
|
||||
"is_public": True,
|
||||
},
|
||||
{
|
||||
"key": "platform_url",
|
||||
"value": f"https://{settings.platform_domain}",
|
||||
"value_type": "string",
|
||||
"description": "Main platform URL",
|
||||
"is_public": True,
|
||||
},
|
||||
{
|
||||
"key": "support_email",
|
||||
"value": f"support@{settings.platform_domain}",
|
||||
"value_type": "string",
|
||||
"description": "Platform support email",
|
||||
"is_public": True,
|
||||
},
|
||||
{
|
||||
"key": "max_vendors_per_user",
|
||||
"value": str(settings.max_vendors_per_user),
|
||||
"value_type": "integer",
|
||||
"description": "Maximum vendors a user can own",
|
||||
"is_public": False,
|
||||
},
|
||||
{
|
||||
"key": "max_team_members_per_vendor",
|
||||
"value": str(settings.max_team_members_per_vendor),
|
||||
"value_type": "integer",
|
||||
"description": "Maximum team members per vendor",
|
||||
"is_public": False,
|
||||
},
|
||||
{
|
||||
"key": "invitation_expiry_days",
|
||||
"value": str(settings.invitation_expiry_days),
|
||||
"value_type": "integer",
|
||||
"description": "Days until team invitation expires",
|
||||
"is_public": False,
|
||||
},
|
||||
{
|
||||
"key": "require_vendor_verification",
|
||||
"value": "true",
|
||||
"value_type": "boolean",
|
||||
"description": "Require admin verification for new vendors",
|
||||
"is_public": False,
|
||||
},
|
||||
{
|
||||
"key": "allow_custom_domains",
|
||||
"value": str(settings.allow_custom_domains).lower(),
|
||||
"value_type": "boolean",
|
||||
"description": "Allow vendors to use custom domains",
|
||||
"is_public": False,
|
||||
},
|
||||
{
|
||||
"key": "maintenance_mode",
|
||||
"value": "false",
|
||||
"value_type": "boolean",
|
||||
"description": "Enable maintenance mode",
|
||||
"is_public": True,
|
||||
},
|
||||
]
|
||||
|
||||
for setting_data in default_settings:
|
||||
# Check if setting already exists
|
||||
existing = db.execute(
|
||||
select(AdminSetting).where(
|
||||
AdminSetting.key == setting_data["key"]
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
|
||||
if not existing:
|
||||
setting = AdminSetting(
|
||||
key=setting_data["key"],
|
||||
value=setting_data["value"],
|
||||
value_type=setting_data["value_type"],
|
||||
description=setting_data.get("description"),
|
||||
is_public=setting_data.get("is_public", False),
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(setting)
|
||||
settings_created += 1
|
||||
|
||||
db.flush()
|
||||
|
||||
if settings_created > 0:
|
||||
print_success(f"Created {settings_created} admin settings")
|
||||
else:
|
||||
print_warning("Admin settings already exist")
|
||||
|
||||
return settings_created
|
||||
|
||||
|
||||
def verify_rbac_schema(db: Session) -> bool:
|
||||
"""Verify that RBAC schema is in place."""
|
||||
|
||||
try:
|
||||
from sqlalchemy import inspect
|
||||
|
||||
inspector = inspect(db.bind)
|
||||
tables = inspector.get_table_names()
|
||||
|
||||
# Check users table has is_email_verified
|
||||
if "users" in tables:
|
||||
user_cols = {col["name"] for col in inspector.get_columns("users")}
|
||||
if "is_email_verified" not in user_cols:
|
||||
print_error("Missing 'is_email_verified' column in users table")
|
||||
return False
|
||||
|
||||
# Check vendor_users has RBAC columns
|
||||
if "vendor_users" in tables:
|
||||
vu_cols = {col["name"] for col in inspector.get_columns("vendor_users")}
|
||||
required_cols = {
|
||||
"user_type",
|
||||
"invitation_token",
|
||||
"invitation_sent_at",
|
||||
"invitation_accepted_at",
|
||||
}
|
||||
missing = required_cols - vu_cols
|
||||
if missing:
|
||||
print_error(f"Missing columns in vendor_users: {missing}")
|
||||
return False
|
||||
|
||||
# Check roles table exists
|
||||
if "roles" not in tables:
|
||||
print_error("Missing 'roles' table")
|
||||
return False
|
||||
|
||||
print_success("RBAC schema verified")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
print_error(f"Schema verification failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN INITIALIZATION
|
||||
# =============================================================================
|
||||
|
||||
def initialize_production(db: Session, auth_manager: AuthManager):
|
||||
"""Initialize production database with essential data."""
|
||||
|
||||
print_header("PRODUCTION INITIALIZATION")
|
||||
|
||||
# Step 1: Verify RBAC schema
|
||||
print_step(1, "Verifying RBAC schema...")
|
||||
if not verify_rbac_schema(db):
|
||||
print_error("RBAC schema not ready. Run migrations first:")
|
||||
print(" make migrate-up")
|
||||
sys.exit(1)
|
||||
|
||||
# Step 2: Create admin user
|
||||
print_step(2, "Creating platform admin...")
|
||||
admin = create_admin_user(db, auth_manager)
|
||||
|
||||
# Step 3: Set up default role templates
|
||||
print_step(3, "Setting up role templates...")
|
||||
role_templates = create_default_role_templates(db)
|
||||
|
||||
# Step 4: Create admin settings
|
||||
print_step(4, "Creating admin settings...")
|
||||
create_admin_settings(db)
|
||||
|
||||
# Commit all changes
|
||||
db.commit()
|
||||
print_success("All changes committed")
|
||||
|
||||
|
||||
def print_summary(db: Session):
|
||||
"""Print initialization summary."""
|
||||
|
||||
print_header("INITIALIZATION SUMMARY")
|
||||
|
||||
# Count records
|
||||
user_count = db.query(User).filter(User.role == "admin").count()
|
||||
setting_count = db.query(AdminSetting).count()
|
||||
|
||||
print(f"\n📊 Database Status:")
|
||||
print(f" Admin users: {user_count}")
|
||||
print(f" Admin settings: {setting_count}")
|
||||
|
||||
print("\n" + "─" * 70)
|
||||
print("🔐 ADMIN CREDENTIALS")
|
||||
print("─" * 70)
|
||||
print(f" URL: /admin/login")
|
||||
print(f" Username: {settings.admin_username}")
|
||||
print(f" Password: {settings.admin_password}")
|
||||
print("─" * 70)
|
||||
|
||||
# Show security warnings if in production
|
||||
if is_production():
|
||||
warnings = validate_production_settings()
|
||||
if warnings:
|
||||
print("\n⚠️ SECURITY WARNINGS:")
|
||||
for warning in warnings:
|
||||
print(f" {warning}")
|
||||
print("\n Please update your .env file with secure values!")
|
||||
else:
|
||||
print("\n⚠️ DEVELOPMENT MODE:")
|
||||
print(" Default credentials are OK for development")
|
||||
print(" Change them in production via .env file")
|
||||
|
||||
print("\n🚀 NEXT STEPS:")
|
||||
print(" 1. Login to admin panel")
|
||||
if is_production():
|
||||
print(" 2. CHANGE DEFAULT PASSWORD IMMEDIATELY!")
|
||||
print(" 3. Configure admin settings")
|
||||
print(" 4. Create first vendor")
|
||||
else:
|
||||
print(" 2. Create demo data: make seed-demo")
|
||||
print(" 3. Start development: make dev")
|
||||
|
||||
print("\n📝 FOR DEMO DATA (Development only):")
|
||||
print(" make seed-demo")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN ENTRY POINT
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
|
||||
print("\n" + "╔" + "═" * 68 + "╗")
|
||||
print("║" + " " * 16 + "PRODUCTION INITIALIZATION" + " " * 27 + "║")
|
||||
print("╚" + "═" * 68 + "╝")
|
||||
|
||||
# Show environment info
|
||||
print_environment_info()
|
||||
|
||||
# Production safety check
|
||||
if is_production():
|
||||
warnings = validate_production_settings()
|
||||
if warnings:
|
||||
print("\n⚠️ PRODUCTION WARNINGS DETECTED:")
|
||||
for warning in warnings:
|
||||
print(f" {warning}")
|
||||
print("\nUpdate your .env file before continuing!")
|
||||
|
||||
response = input("\nContinue anyway? (yes/no): ")
|
||||
if response.lower() != "yes":
|
||||
print("Initialization cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
db = SessionLocal()
|
||||
auth_manager = AuthManager()
|
||||
|
||||
try:
|
||||
initialize_production(db, auth_manager)
|
||||
print_summary(db)
|
||||
|
||||
print_header("✅ INITIALIZATION COMPLETED")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
db.rollback()
|
||||
print("\n\n⚠️ Initialization interrupted")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print_header("❌ INITIALIZATION FAILED")
|
||||
print(f"\nError: {e}\n")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
658
scripts/seed_demo.py
Normal file
658
scripts/seed_demo.py
Normal file
@@ -0,0 +1,658 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo Database Seeder for Wizamart Platform
|
||||
|
||||
Creates DEMO/TEST data for development and testing:
|
||||
- Demo vendors with realistic data
|
||||
- Test customers and addresses
|
||||
- Sample products
|
||||
- Demo orders
|
||||
- Vendor themes and custom domains
|
||||
- Test import jobs
|
||||
|
||||
⚠️ WARNING: This script creates FAKE DATA for development only!
|
||||
⚠️ NEVER run this in production!
|
||||
|
||||
Prerequisites:
|
||||
- Database migrations must be applied (make migrate-up)
|
||||
- Production initialization must be run (make init-prod)
|
||||
|
||||
Usage:
|
||||
make seed-demo # Normal demo seeding
|
||||
make seed-demo-minimal # Minimal seeding (1 vendor only)
|
||||
make seed-demo-reset # Delete all data and reseed (DANGEROUS!)
|
||||
|
||||
This script is idempotent when run normally.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from decimal import Decimal
|
||||
|
||||
# Add project root to path
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import select, delete
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.core.config import settings
|
||||
from app.core.environment import is_production, get_environment
|
||||
from models.database.user import User
|
||||
from models.database.vendor import Vendor, VendorUser, Role
|
||||
from models.database.vendor_domain import VendorDomain
|
||||
from models.database.vendor_theme import VendorTheme
|
||||
from models.database.customer import Customer, CustomerAddress
|
||||
from models.database.product import Product
|
||||
from models.database.marketplace_product import MarketplaceProduct
|
||||
from models.database.marketplace_import_job import MarketplaceImportJob
|
||||
from models.database.order import Order, OrderItem
|
||||
from models.database.admin import PlatformAlert
|
||||
from middleware.auth import AuthManager
|
||||
|
||||
# =============================================================================
|
||||
# MODE DETECTION (from environment variable set by Makefile)
|
||||
# =============================================================================
|
||||
import os
|
||||
|
||||
SEED_MODE = os.getenv('SEED_MODE', 'normal') # normal, minimal, reset
|
||||
|
||||
# =============================================================================
|
||||
# DEMO DATA CONFIGURATION
|
||||
# =============================================================================
|
||||
|
||||
# Demo vendor configurations
|
||||
DEMO_VENDORS = [
|
||||
{
|
||||
"vendor_code": "WIZAMART",
|
||||
"name": "WizaMart",
|
||||
"subdomain": "wizamart",
|
||||
"description": "Premium electronics and gadgets marketplace",
|
||||
"theme_preset": "modern",
|
||||
"custom_domain": "wizamart.shop",
|
||||
},
|
||||
{
|
||||
"vendor_code": "FASHIONHUB",
|
||||
"name": "Fashion Hub",
|
||||
"subdomain": "fashionhub",
|
||||
"description": "Trendy clothing and accessories",
|
||||
"theme_preset": "vibrant",
|
||||
"custom_domain": "fashionhub.store",
|
||||
},
|
||||
{
|
||||
"vendor_code": "BOOKSTORE",
|
||||
"name": "The Book Store",
|
||||
"subdomain": "bookstore",
|
||||
"description": "Books, magazines, and educational materials",
|
||||
"theme_preset": "classic",
|
||||
"custom_domain": None,
|
||||
},
|
||||
]
|
||||
|
||||
# Demo users (vendor owners)
|
||||
DEMO_VENDOR_USERS = [
|
||||
{
|
||||
"username": "vendor1",
|
||||
"email": "vendor1@example.com",
|
||||
"password": "password123",
|
||||
"first_name": "John",
|
||||
"last_name": "Vendor",
|
||||
},
|
||||
{
|
||||
"username": "vendor2",
|
||||
"email": "vendor2@example.com",
|
||||
"password": "password123",
|
||||
"first_name": "Jane",
|
||||
"last_name": "Merchant",
|
||||
},
|
||||
{
|
||||
"username": "vendor3",
|
||||
"email": "vendor3@example.com",
|
||||
"password": "password123",
|
||||
"first_name": "Bob",
|
||||
"last_name": "Seller",
|
||||
},
|
||||
]
|
||||
|
||||
# Theme presets
|
||||
THEME_PRESETS = {
|
||||
"modern": {
|
||||
"primary": "#3b82f6",
|
||||
"secondary": "#06b6d4",
|
||||
"accent": "#f59e0b",
|
||||
"background": "#f9fafb",
|
||||
"text": "#111827",
|
||||
},
|
||||
"classic": {
|
||||
"primary": "#1e40af",
|
||||
"secondary": "#7c3aed",
|
||||
"accent": "#dc2626",
|
||||
"background": "#ffffff",
|
||||
"text": "#374151",
|
||||
},
|
||||
"vibrant": {
|
||||
"primary": "#ec4899",
|
||||
"secondary": "#f59e0b",
|
||||
"accent": "#8b5cf6",
|
||||
"background": "#fef3c7",
|
||||
"text": "#78350f",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def print_header(text: str):
|
||||
"""Print formatted header."""
|
||||
print("\n" + "=" * 70)
|
||||
print(f" {text}")
|
||||
print("=" * 70)
|
||||
|
||||
|
||||
def print_step(step: int, text: str):
|
||||
"""Print step indicator."""
|
||||
print(f"\n[{step}] {text}")
|
||||
|
||||
|
||||
def print_success(text: str):
|
||||
"""Print success message."""
|
||||
print(f" ✓ {text}")
|
||||
|
||||
|
||||
def print_warning(text: str):
|
||||
"""Print warning message."""
|
||||
print(f" ⚠ {text}")
|
||||
|
||||
|
||||
def print_error(text: str):
|
||||
"""Print error message."""
|
||||
print(f" ✗ {text}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SAFETY CHECKS
|
||||
# =============================================================================
|
||||
|
||||
def check_environment():
|
||||
"""Prevent running demo seed in production."""
|
||||
|
||||
if is_production():
|
||||
print_error("Cannot run demo seeding in production!")
|
||||
print(" This script creates FAKE DATA for development only.")
|
||||
print(f" Current environment: {get_environment()}")
|
||||
print("\n To seed in production:")
|
||||
print(" 1. Set ENVIRONMENT=development in .env (or ENV=development)")
|
||||
print(" 2. Run: make seed-demo")
|
||||
sys.exit(1)
|
||||
|
||||
print_success(f"Environment check passed: {get_environment()}")
|
||||
|
||||
|
||||
def check_admin_exists(db: Session) -> bool:
|
||||
"""Check if admin user exists."""
|
||||
|
||||
admin = db.execute(
|
||||
select(User).where(User.role == "admin")
|
||||
).scalar_one_or_none()
|
||||
|
||||
if not admin:
|
||||
print_error("No admin user found!")
|
||||
print(" Run production initialization first:")
|
||||
print(" make init-prod")
|
||||
return False
|
||||
|
||||
print_success(f"Admin user exists: {admin.email}")
|
||||
return True
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# DATA DELETION (for reset mode)
|
||||
# =============================================================================
|
||||
|
||||
def reset_all_data(db: Session):
|
||||
"""Delete ALL data from database (except admin user)."""
|
||||
|
||||
print_warning("RESETTING ALL DATA...")
|
||||
print(" This will delete all vendors, customers, orders, etc.")
|
||||
print(" Admin user will be preserved.")
|
||||
|
||||
# Get confirmation
|
||||
response = input("\n Type 'DELETE ALL DATA' to confirm: ")
|
||||
if response != "DELETE ALL DATA":
|
||||
print(" Reset cancelled.")
|
||||
sys.exit(0)
|
||||
|
||||
# Delete in correct order (respecting foreign keys)
|
||||
tables_to_clear = [
|
||||
OrderItem,
|
||||
Order,
|
||||
CustomerAddress,
|
||||
Customer,
|
||||
MarketplaceImportJob,
|
||||
MarketplaceProduct,
|
||||
Product,
|
||||
VendorDomain,
|
||||
VendorTheme,
|
||||
Role,
|
||||
VendorUser,
|
||||
Vendor,
|
||||
PlatformAlert,
|
||||
]
|
||||
|
||||
for table in tables_to_clear:
|
||||
db.execute(delete(table))
|
||||
|
||||
# Delete non-admin users
|
||||
db.execute(delete(User).where(User.role != "admin"))
|
||||
|
||||
db.commit()
|
||||
print_success("All data deleted (admin preserved)")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# SEEDING FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
def create_demo_vendors(db: Session, auth_manager: AuthManager) -> List[Vendor]:
|
||||
"""Create demo vendors with users."""
|
||||
|
||||
vendors = []
|
||||
|
||||
# Determine how many vendors to create based on mode
|
||||
vendor_count = 1 if SEED_MODE == 'minimal' else settings.seed_demo_vendors
|
||||
vendors_to_create = DEMO_VENDORS[:vendor_count]
|
||||
users_to_create = DEMO_VENDOR_USERS[:vendor_count]
|
||||
|
||||
for vendor_data, user_data in zip(vendors_to_create, users_to_create):
|
||||
# Check if vendor already exists
|
||||
existing = db.execute(
|
||||
select(Vendor).where(Vendor.vendor_code == vendor_data["vendor_code"])
|
||||
).scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
print_warning(f"Vendor already exists: {vendor_data['name']}")
|
||||
vendors.append(existing)
|
||||
continue
|
||||
|
||||
# Create vendor user
|
||||
vendor_user = User(
|
||||
username=user_data["username"],
|
||||
email=user_data["email"],
|
||||
hashed_password=auth_manager.hash_password(user_data["password"]),
|
||||
role="vendor",
|
||||
first_name=user_data["first_name"],
|
||||
last_name=user_data["last_name"],
|
||||
is_active=True,
|
||||
is_email_verified=True,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(vendor_user)
|
||||
db.flush()
|
||||
|
||||
# Create vendor
|
||||
vendor = Vendor(
|
||||
vendor_code=vendor_data["vendor_code"],
|
||||
name=vendor_data["name"],
|
||||
subdomain=vendor_data["subdomain"],
|
||||
description=vendor_data["description"],
|
||||
is_active=True,
|
||||
is_verified=True,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(vendor)
|
||||
db.flush()
|
||||
|
||||
# Link user to vendor as owner
|
||||
vendor_user_link = VendorUser(
|
||||
vendor_id=vendor.id,
|
||||
user_id=vendor_user.id,
|
||||
user_type="owner",
|
||||
is_active=True,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(vendor_user_link)
|
||||
|
||||
# Create vendor theme
|
||||
theme_colors = THEME_PRESETS.get(vendor_data["theme_preset"], THEME_PRESETS["modern"])
|
||||
theme = VendorTheme(
|
||||
vendor_id=vendor.id,
|
||||
theme_name=vendor_data["theme_preset"],
|
||||
primary_color=theme_colors["primary"],
|
||||
secondary_color=theme_colors["secondary"],
|
||||
accent_color=theme_colors["accent"],
|
||||
background_color=theme_colors["background"],
|
||||
text_color=theme_colors["text"],
|
||||
is_active=True,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(theme)
|
||||
|
||||
# Create custom domain if specified
|
||||
if vendor_data.get("custom_domain"):
|
||||
domain = VendorDomain(
|
||||
vendor_id=vendor.id,
|
||||
domain_name=vendor_data["custom_domain"],
|
||||
is_verified=True, # Auto-verified for demo
|
||||
is_active=True,
|
||||
verification_token=None,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(domain)
|
||||
|
||||
vendors.append(vendor)
|
||||
print_success(f"Created vendor: {vendor.name} ({vendor.vendor_code})")
|
||||
|
||||
db.flush()
|
||||
return vendors
|
||||
|
||||
|
||||
def create_demo_customers(db: Session, vendor: Vendor, auth_manager: AuthManager, count: int) -> List[Customer]:
|
||||
"""Create demo customers for a vendor."""
|
||||
|
||||
customers = []
|
||||
# Use a simple demo password for all customers
|
||||
demo_password = "customer123"
|
||||
|
||||
for i in range(1, count + 1):
|
||||
email = f"customer{i}@{vendor.subdomain}.example.com"
|
||||
customer_number = f"CUST-{vendor.vendor_code}-{i:04d}"
|
||||
|
||||
# Check if customer already exists
|
||||
existing_customer = db.query(Customer).filter(
|
||||
Customer.vendor_id == vendor.id,
|
||||
Customer.email == email
|
||||
).first()
|
||||
|
||||
if existing_customer:
|
||||
customers.append(existing_customer)
|
||||
continue # Skip creation, customer already exists
|
||||
|
||||
customer = Customer(
|
||||
vendor_id=vendor.id,
|
||||
email=email,
|
||||
hashed_password=auth_manager.hash_password(demo_password),
|
||||
first_name=f"Customer{i}",
|
||||
last_name=f"Test",
|
||||
phone=f"+352123456{i:03d}",
|
||||
customer_number=customer_number,
|
||||
is_active=True,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(customer)
|
||||
customers.append(customer)
|
||||
|
||||
db.flush()
|
||||
|
||||
new_count = len([c for c in customers if c.id is None or db.is_modified(c)])
|
||||
if new_count > 0:
|
||||
print_success(f"Created {new_count} customers for {vendor.name}")
|
||||
else:
|
||||
print_warning(f"Customers already exist for {vendor.name}")
|
||||
|
||||
return customers
|
||||
|
||||
|
||||
def create_demo_products(db: Session, vendor: Vendor, count: int) -> List[Product]:
|
||||
"""Create demo products for a vendor."""
|
||||
|
||||
products = []
|
||||
|
||||
for i in range(1, count + 1):
|
||||
marketplace_product_id = f"{vendor.vendor_code}-MP-{i:04d}"
|
||||
product_id = f"{vendor.vendor_code}-PROD-{i:03d}"
|
||||
|
||||
# Check if this product already exists
|
||||
existing_product = db.query(Product).filter(
|
||||
Product.vendor_id == vendor.id,
|
||||
Product.product_id == product_id
|
||||
).first()
|
||||
|
||||
if existing_product:
|
||||
products.append(existing_product)
|
||||
continue # Skip creation, product already exists
|
||||
|
||||
# Check if marketplace product already exists
|
||||
existing_mp = db.query(MarketplaceProduct).filter(
|
||||
MarketplaceProduct.marketplace_product_id == marketplace_product_id
|
||||
).first()
|
||||
|
||||
if existing_mp:
|
||||
marketplace_product = existing_mp
|
||||
else:
|
||||
# Create the MarketplaceProduct (base product data)
|
||||
marketplace_product = MarketplaceProduct(
|
||||
marketplace_product_id=marketplace_product_id,
|
||||
title=f"Sample Product {i} - {vendor.name}",
|
||||
description=f"This is a demo product for testing purposes in {vendor.name}. High quality and affordable.",
|
||||
link=f"https://{vendor.subdomain}.example.com/products/sample-{i}",
|
||||
image_link=f"https://{vendor.subdomain}.example.com/images/product-{i}.jpg",
|
||||
price=str(Decimal(f"{(i * 10) % 500 + 9.99}")), # Store as string
|
||||
brand=vendor.name,
|
||||
gtin=f"TEST{vendor.id:02d}{i:010d}",
|
||||
availability="in stock",
|
||||
condition="new",
|
||||
google_product_category="Electronics > Computers > Laptops",
|
||||
marketplace="Wizamart",
|
||||
vendor_name=vendor.name,
|
||||
currency="EUR",
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(marketplace_product)
|
||||
db.flush() # Flush to get the marketplace_product.id
|
||||
|
||||
# Create the Product (vendor-specific entry)
|
||||
product = Product(
|
||||
vendor_id=vendor.id,
|
||||
marketplace_product_id=marketplace_product.id,
|
||||
product_id=product_id,
|
||||
price=float(Decimal(f"{(i * 10) % 500 + 9.99}")), # Store as float
|
||||
availability="in stock",
|
||||
condition="new",
|
||||
currency="EUR",
|
||||
is_active=True,
|
||||
is_featured=(i % 5 == 0), # Every 5th product is featured
|
||||
display_order=i,
|
||||
min_quantity=1,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
db.add(product)
|
||||
products.append(product)
|
||||
|
||||
db.flush()
|
||||
|
||||
new_count = len([p for p in products if p.id is None or db.is_modified(p)])
|
||||
if new_count > 0:
|
||||
print_success(f"Created {new_count} products for {vendor.name}")
|
||||
else:
|
||||
print_warning(f"Products already exist for {vendor.name}")
|
||||
|
||||
return products
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN SEEDING
|
||||
# =============================================================================
|
||||
|
||||
def seed_demo_data(db: Session, auth_manager: AuthManager):
|
||||
"""Seed demo data for development."""
|
||||
|
||||
print_header("DEMO DATA SEEDING")
|
||||
print(f" Mode: {SEED_MODE.upper()}")
|
||||
|
||||
# Step 1: Check environment
|
||||
print_step(1, "Checking environment...")
|
||||
check_environment()
|
||||
|
||||
# Step 2: Check admin exists
|
||||
print_step(2, "Verifying admin user...")
|
||||
if not check_admin_exists(db):
|
||||
sys.exit(1)
|
||||
|
||||
# Step 3: Reset data if in reset mode
|
||||
if SEED_MODE == 'reset':
|
||||
print_step(3, "Resetting data...")
|
||||
reset_all_data(db)
|
||||
|
||||
# Step 4: Create vendors
|
||||
print_step(4, "Creating demo vendors...")
|
||||
vendors = create_demo_vendors(db, auth_manager)
|
||||
|
||||
# Step 5: Create customers
|
||||
print_step(5, "Creating demo customers...")
|
||||
for vendor in vendors:
|
||||
create_demo_customers(
|
||||
db,
|
||||
vendor,
|
||||
auth_manager,
|
||||
count=settings.seed_customers_per_vendor
|
||||
)
|
||||
|
||||
# Step 6: Create products
|
||||
print_step(6, "Creating demo products...")
|
||||
for vendor in vendors:
|
||||
create_demo_products(
|
||||
db,
|
||||
vendor,
|
||||
count=settings.seed_products_per_vendor
|
||||
)
|
||||
|
||||
# Commit all changes
|
||||
db.commit()
|
||||
print_success("All demo data committed")
|
||||
|
||||
|
||||
def print_summary(db: Session):
|
||||
"""Print seeding summary."""
|
||||
|
||||
print_header("SEEDING SUMMARY")
|
||||
|
||||
# Count records
|
||||
vendor_count = db.query(Vendor).count()
|
||||
user_count = db.query(User).count()
|
||||
customer_count = db.query(Customer).count()
|
||||
product_count = db.query(Product).count()
|
||||
|
||||
print(f"\n📊 Database Status:")
|
||||
print(f" Vendors: {vendor_count}")
|
||||
print(f" Users: {user_count}")
|
||||
print(f" Customers: {customer_count}")
|
||||
print(f" Products: {product_count}")
|
||||
|
||||
# Show vendor details
|
||||
vendors = db.query(Vendor).all()
|
||||
print(f"\n🏪 Demo Vendors:")
|
||||
for vendor in vendors:
|
||||
print(f"\n {vendor.name} ({vendor.vendor_code})")
|
||||
print(f" Subdomain: {vendor.subdomain}.{settings.platform_domain}")
|
||||
|
||||
# Query custom domains separately
|
||||
custom_domain = db.query(VendorDomain).filter(
|
||||
VendorDomain.vendor_id == vendor.id,
|
||||
VendorDomain.is_active == True
|
||||
).first()
|
||||
|
||||
if custom_domain:
|
||||
# Try different possible field names (model field might vary)
|
||||
domain_value = (
|
||||
getattr(custom_domain, 'domain', None) or
|
||||
getattr(custom_domain, 'domain_name', None) or
|
||||
getattr(custom_domain, 'name', None)
|
||||
)
|
||||
if domain_value:
|
||||
print(f" Custom: {domain_value}")
|
||||
|
||||
print(f" Status: {'✓ Active' if vendor.is_active else '✗ Inactive'}")
|
||||
|
||||
print(f"\n🔐 Demo Vendor Credentials:")
|
||||
print("─" * 70)
|
||||
for i, vendor_data in enumerate(DEMO_VENDOR_USERS[:vendor_count], 1):
|
||||
vendor = vendors[i - 1] if i <= len(vendors) else None
|
||||
print(f" Vendor {i}:")
|
||||
print(f" Username: {vendor_data['username']}")
|
||||
print(f" Email: {vendor_data['email']}")
|
||||
print(f" Password: {vendor_data['password']}")
|
||||
if vendor:
|
||||
print(f" Login: http://localhost:8000/vendor/{vendor.vendor_code}/login")
|
||||
print(f" or http://{vendor.subdomain}.localhost:8000/vendor/login")
|
||||
print()
|
||||
|
||||
print(f"\n🛒 Demo Customer Credentials:")
|
||||
print("─" * 70)
|
||||
print(f" All customers:")
|
||||
print(f" Email: customer1@{{subdomain}}.example.com")
|
||||
print(f" Password: customer123")
|
||||
print(f" (Replace {{subdomain}} with vendor subdomain, e.g., wizamart)")
|
||||
print()
|
||||
|
||||
print(f"\n🏪 Shop Access (Development):")
|
||||
print("─" * 70)
|
||||
for vendor in vendors:
|
||||
print(f" {vendor.name}:")
|
||||
print(f" Path-based: http://localhost:8000/vendors/{vendor.vendor_code}/shop/")
|
||||
print(f" Subdomain: http://{vendor.subdomain}.localhost:8000/")
|
||||
print()
|
||||
|
||||
print("⚠️ ALL DEMO CREDENTIALS ARE INSECURE - For development only!")
|
||||
|
||||
print("\n🚀 NEXT STEPS:")
|
||||
print(" 1. Start development: make dev")
|
||||
print(" 2. Login as vendor:")
|
||||
print(" • Path-based: http://localhost:8000/vendor/WIZAMART/login")
|
||||
print(" • Subdomain: http://wizamart.localhost:8000/vendor/login")
|
||||
print(f" 3. Visit vendor shop: http://localhost:8000/vendors/WIZAMART/shop/")
|
||||
print(f" 4. Admin panel: http://localhost:8000/admin/login")
|
||||
print(f" Username: {settings.admin_username}")
|
||||
print(f" Password: {settings.admin_password}")
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MAIN ENTRY POINT
|
||||
# =============================================================================
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
|
||||
print("\n" + "╔" + "═" * 68 + "╗")
|
||||
print("║" + " " * 20 + "DEMO DATA SEEDING" + " " * 31 + "║")
|
||||
print("╚" + "═" * 68 + "╝")
|
||||
|
||||
db = SessionLocal()
|
||||
auth_manager = AuthManager()
|
||||
|
||||
try:
|
||||
seed_demo_data(db, auth_manager)
|
||||
print_summary(db)
|
||||
|
||||
print_header("✅ DEMO SEEDING COMPLETED")
|
||||
|
||||
except KeyboardInterrupt:
|
||||
db.rollback()
|
||||
print("\n\n⚠️ Seeding interrupted")
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
print_header("❌ SEEDING FAILED")
|
||||
print(f"\nError: {e}\n")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,66 +0,0 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
echo Generating frontend structure with statistics...
|
||||
echo.
|
||||
|
||||
set OUTPUT=frontend-structure.txt
|
||||
|
||||
echo Frontend Folder Structure > %OUTPUT%
|
||||
echo Generated: %date% %time% >> %OUTPUT%
|
||||
echo ============================================================================== >> %OUTPUT%
|
||||
echo. >> %OUTPUT%
|
||||
|
||||
echo. >> %OUTPUT%
|
||||
echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT%
|
||||
echo ║ JINJA2 TEMPLATES ║ >> %OUTPUT%
|
||||
echo ║ Location: app/templates ║ >> %OUTPUT%
|
||||
echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT%
|
||||
echo. >> %OUTPUT%
|
||||
|
||||
tree /F /A app\templates >> %OUTPUT%
|
||||
|
||||
echo. >> %OUTPUT%
|
||||
echo. >> %OUTPUT%
|
||||
echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT%
|
||||
echo ║ STATIC ASSETS ║ >> %OUTPUT%
|
||||
echo ║ Location: static ║ >> %OUTPUT%
|
||||
echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT%
|
||||
echo. >> %OUTPUT%
|
||||
|
||||
tree /F /A static >> %OUTPUT%
|
||||
|
||||
echo. >> %OUTPUT%
|
||||
echo. >> %OUTPUT%
|
||||
echo ╔══════════════════════════════════════════════════════════════════╗ >> %OUTPUT%
|
||||
echo ║ STATISTICS ║ >> %OUTPUT%
|
||||
echo ╚══════════════════════════════════════════════════════════════════╝ >> %OUTPUT%
|
||||
echo. >> %OUTPUT%
|
||||
|
||||
echo Templates: >> %OUTPUT%
|
||||
echo - Total HTML files: >> %OUTPUT%
|
||||
dir /S /B app\templates\*.html 2>nul | find /C ".html" >> %OUTPUT%
|
||||
echo - Total Jinja2 files: >> %OUTPUT%
|
||||
dir /S /B app\templates\*.j2 2>nul | find /C ".j2" >> %OUTPUT%
|
||||
|
||||
echo. >> %OUTPUT%
|
||||
echo Static Assets: >> %OUTPUT%
|
||||
echo - JavaScript files: >> %OUTPUT%
|
||||
dir /S /B static\*.js 2>nul | find /C ".js" >> %OUTPUT%
|
||||
echo - CSS files: >> %OUTPUT%
|
||||
dir /S /B static\*.css 2>nul | find /C ".css" >> %OUTPUT%
|
||||
echo - Image files: >> %OUTPUT%
|
||||
for %%e in (png jpg jpeg gif svg webp ico) do (
|
||||
dir /S /B static\*.%%e 2>nul | find /C ".%%e" >> %OUTPUT%
|
||||
)
|
||||
|
||||
echo. >> %OUTPUT%
|
||||
echo ============================================================================== >> %OUTPUT%
|
||||
echo End of structure >> %OUTPUT%
|
||||
|
||||
echo.
|
||||
echo ✅ Structure saved to %OUTPUT%
|
||||
echo.
|
||||
echo Opening file...
|
||||
notepad %OUTPUT%
|
||||
|
||||
endlocal
|
||||
528
scripts/show_structure.py
Normal file
528
scripts/show_structure.py
Normal file
@@ -0,0 +1,528 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Cross-platform structure generator for Wizamart project.
|
||||
Works on Windows, Linux, and macOS.
|
||||
|
||||
Usage:
|
||||
python show_structure.py frontend
|
||||
python show_structure.py backend
|
||||
python show_structure.py tests
|
||||
python show_structure.py all
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
def count_files(directory: str, pattern: str) -> int:
|
||||
"""Count files matching pattern in directory."""
|
||||
if not os.path.exists(directory):
|
||||
return 0
|
||||
|
||||
count = 0
|
||||
for root, dirs, files in os.walk(directory):
|
||||
# Skip __pycache__ and other cache directories
|
||||
dirs[:] = [d for d in dirs if d not in ['__pycache__', '.pytest_cache', '.git', 'node_modules']]
|
||||
|
||||
for file in files:
|
||||
if pattern == '*' or file.endswith(pattern):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def get_tree_structure(directory: str, exclude_patterns: List[str] = None) -> str:
|
||||
"""Generate tree structure for directory."""
|
||||
if not os.path.exists(directory):
|
||||
return f"Directory {directory} not found"
|
||||
|
||||
# Try to use system tree command first
|
||||
try:
|
||||
if sys.platform == 'win32':
|
||||
# Windows tree command
|
||||
result = subprocess.run(
|
||||
['tree', '/F', '/A', directory],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding='utf-8',
|
||||
errors='replace'
|
||||
)
|
||||
return result.stdout
|
||||
else:
|
||||
# Linux/Mac tree command with exclusions
|
||||
exclude_args = []
|
||||
if exclude_patterns:
|
||||
exclude_args = ['-I', '|'.join(exclude_patterns)]
|
||||
|
||||
result = subprocess.run(
|
||||
['tree', '-F', '-a'] + exclude_args + [directory],
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return result.stdout
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
pass
|
||||
|
||||
# Fallback: generate tree structure manually
|
||||
return generate_manual_tree(directory, exclude_patterns)
|
||||
|
||||
|
||||
def generate_manual_tree(directory: str, exclude_patterns: List[str] = None, prefix: str = "") -> str:
|
||||
"""Generate tree structure manually when tree command is not available."""
|
||||
if exclude_patterns is None:
|
||||
exclude_patterns = ['__pycache__', '.pytest_cache', '.git', 'node_modules', '*.pyc', '*.pyo']
|
||||
|
||||
output = []
|
||||
path = Path(directory)
|
||||
|
||||
try:
|
||||
items = sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name))
|
||||
|
||||
for i, item in enumerate(items):
|
||||
# Skip excluded patterns
|
||||
skip = False
|
||||
for pattern in exclude_patterns:
|
||||
if pattern.startswith('*'):
|
||||
# File extension pattern
|
||||
if item.name.endswith(pattern[1:]):
|
||||
skip = True
|
||||
break
|
||||
else:
|
||||
# Directory or exact name pattern
|
||||
if item.name == pattern or pattern in str(item):
|
||||
skip = True
|
||||
break
|
||||
|
||||
if skip:
|
||||
continue
|
||||
|
||||
is_last = i == len(items) - 1
|
||||
current_prefix = "└── " if is_last else "├── "
|
||||
|
||||
if item.is_dir():
|
||||
output.append(f"{prefix}{current_prefix}{item.name}/")
|
||||
extension = " " if is_last else "│ "
|
||||
subtree = generate_manual_tree(str(item), exclude_patterns, prefix + extension)
|
||||
if subtree:
|
||||
output.append(subtree)
|
||||
else:
|
||||
output.append(f"{prefix}{current_prefix}{item.name}")
|
||||
except PermissionError:
|
||||
output.append(f"{prefix}[Permission Denied]")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def generate_frontend_structure() -> str:
|
||||
"""Generate frontend structure report."""
|
||||
output = []
|
||||
output.append("Frontend Folder Structure")
|
||||
output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
output.append("=" * 78)
|
||||
output.append("")
|
||||
|
||||
# Templates section
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ JINJA2 TEMPLATES ║")
|
||||
output.append("║ Location: app/templates ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
output.append(get_tree_structure("app/templates"))
|
||||
|
||||
# Static assets section
|
||||
output.append("")
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ STATIC ASSETS ║")
|
||||
output.append("║ Location: static ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
output.append(get_tree_structure("static"))
|
||||
|
||||
# Documentation section (if exists)
|
||||
if os.path.exists("docs"):
|
||||
output.append("")
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ DOCUMENTATION ║")
|
||||
output.append("║ Location: docs ║")
|
||||
output.append("║ (also listed in tools structure) ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
output.append("Note: Documentation is also included in tools structure")
|
||||
output.append(" for infrastructure/DevOps context.")
|
||||
|
||||
# Statistics section
|
||||
output.append("")
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ STATISTICS ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
|
||||
output.append("Templates:")
|
||||
output.append(f" - Total HTML files: {count_files('app/templates', '.html')}")
|
||||
output.append(f" - Total Jinja2 files: {count_files('app/templates', '.j2')}")
|
||||
|
||||
output.append("")
|
||||
output.append("Static Assets:")
|
||||
output.append(f" - JavaScript files: {count_files('static', '.js')}")
|
||||
output.append(f" - CSS files: {count_files('static', '.css')}")
|
||||
output.append(" - Image files:")
|
||||
for ext in ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'ico']:
|
||||
count = count_files('static', f'.{ext}')
|
||||
if count > 0:
|
||||
output.append(f" - .{ext}: {count}")
|
||||
|
||||
if os.path.exists("docs"):
|
||||
output.append("")
|
||||
output.append("Documentation:")
|
||||
output.append(f" - Markdown files: {count_files('docs', '.md')}")
|
||||
output.append(f" - reStructuredText files: {count_files('docs', '.rst')}")
|
||||
|
||||
output.append("")
|
||||
output.append("=" * 78)
|
||||
output.append("End of structure")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def generate_backend_structure() -> str:
|
||||
"""Generate backend structure report."""
|
||||
output = []
|
||||
output.append("Backend Folder Structure")
|
||||
output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
output.append("=" * 78)
|
||||
output.append("")
|
||||
|
||||
exclude = ['__pycache__', '*.pyc', '*.pyo', '.pytest_cache', '*.egg-info', 'templates']
|
||||
|
||||
# Backend directories to include
|
||||
backend_dirs = [
|
||||
('app', 'Application Code'),
|
||||
('middleware', 'Middleware Components'),
|
||||
('models', 'Database Models'),
|
||||
('storage', 'File Storage'),
|
||||
('tasks', 'Background Tasks'),
|
||||
('logs', 'Application Logs'),
|
||||
]
|
||||
|
||||
for directory, title in backend_dirs:
|
||||
if os.path.exists(directory):
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append(f"║ {title.upper().center(62)} ║")
|
||||
output.append(f"║ Location: {directory + '/'.ljust(51)} ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
output.append(get_tree_structure(directory, exclude))
|
||||
|
||||
# Statistics section
|
||||
output.append("")
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ STATISTICS ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
|
||||
output.append("Python Files by Directory:")
|
||||
total_py_files = 0
|
||||
for directory, title in backend_dirs:
|
||||
if os.path.exists(directory):
|
||||
count = count_files(directory, '.py')
|
||||
total_py_files += count
|
||||
output.append(f" - {directory}/: {count} files")
|
||||
|
||||
output.append(f" - Total Python files: {total_py_files}")
|
||||
|
||||
output.append("")
|
||||
output.append("Application Components (if in app/):")
|
||||
components = ['routes', 'services', 'schemas', 'exceptions', 'utils']
|
||||
for component in components:
|
||||
component_path = f"app/{component}"
|
||||
if os.path.exists(component_path):
|
||||
count = count_files(component_path, '.py')
|
||||
output.append(f" - app/{component}: {count} files")
|
||||
|
||||
output.append("")
|
||||
output.append("=" * 78)
|
||||
output.append("End of structure")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def generate_tools_structure() -> str:
|
||||
"""Generate tools/infrastructure structure report."""
|
||||
output = []
|
||||
output.append("Tools & Infrastructure Structure")
|
||||
output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
output.append("=" * 78)
|
||||
output.append("")
|
||||
|
||||
exclude = ['__pycache__', '*.pyc', '*.pyo', '.pytest_cache', '*.egg-info']
|
||||
|
||||
# Tools directories to include
|
||||
tools_dirs = [
|
||||
('alembic', 'Database Migrations'),
|
||||
('scripts', 'Utility Scripts'),
|
||||
('docker', 'Docker Configuration'),
|
||||
('docs', 'Documentation'),
|
||||
]
|
||||
|
||||
for directory, title in tools_dirs:
|
||||
if os.path.exists(directory):
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append(f"║ {title.upper().center(62)} ║")
|
||||
output.append(f"║ Location: {directory + '/'.ljust(51)} ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
output.append(get_tree_structure(directory, exclude))
|
||||
|
||||
# Configuration files section
|
||||
output.append("")
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ CONFIGURATION FILES ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
|
||||
output.append("Root configuration files:")
|
||||
config_files = [
|
||||
('Makefile', 'Build automation'),
|
||||
('requirements.txt', 'Python dependencies'),
|
||||
('pyproject.toml', 'Python project config'),
|
||||
('setup.py', 'Python setup script'),
|
||||
('setup.cfg', 'Setup configuration'),
|
||||
('alembic.ini', 'Alembic migrations config'),
|
||||
('mkdocs.yml', 'MkDocs documentation config'),
|
||||
('Dockerfile', 'Docker image definition'),
|
||||
('docker-compose.yml', 'Docker services'),
|
||||
('.dockerignore', 'Docker ignore patterns'),
|
||||
('.gitignore', 'Git ignore patterns'),
|
||||
('.env.example', 'Environment variables template'),
|
||||
]
|
||||
|
||||
for file, description in config_files:
|
||||
if os.path.exists(file):
|
||||
output.append(f" ✓ {file.ljust(25)} - {description}")
|
||||
|
||||
# Statistics section
|
||||
output.append("")
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ STATISTICS ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
|
||||
output.append("Database Migrations:")
|
||||
if os.path.exists("alembic/versions"):
|
||||
migration_count = count_files("alembic/versions", ".py")
|
||||
output.append(f" - Total migrations: {migration_count}")
|
||||
if migration_count > 0:
|
||||
# Get first and last migration
|
||||
try:
|
||||
migrations = sorted([f for f in os.listdir("alembic/versions") if f.endswith('.py')])
|
||||
if migrations:
|
||||
output.append(f" - First: {migrations[0][:40]}...")
|
||||
if len(migrations) > 1:
|
||||
output.append(f" - Latest: {migrations[-1][:40]}...")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
output.append(" - No alembic/versions directory found")
|
||||
|
||||
output.append("")
|
||||
output.append("Scripts:")
|
||||
if os.path.exists("scripts"):
|
||||
script_types = {
|
||||
'.py': 'Python scripts',
|
||||
'.sh': 'Shell scripts',
|
||||
'.bat': 'Batch scripts',
|
||||
}
|
||||
for ext, desc in script_types.items():
|
||||
count = count_files("scripts", ext)
|
||||
if count > 0:
|
||||
output.append(f" - {desc}: {count}")
|
||||
else:
|
||||
output.append(" - No scripts directory found")
|
||||
|
||||
output.append("")
|
||||
output.append("Documentation:")
|
||||
if os.path.exists("docs"):
|
||||
doc_types = {
|
||||
'.md': 'Markdown files',
|
||||
'.rst': 'reStructuredText files',
|
||||
}
|
||||
for ext, desc in doc_types.items():
|
||||
count = count_files("docs", ext)
|
||||
if count > 0:
|
||||
output.append(f" - {desc}: {count}")
|
||||
else:
|
||||
output.append(" - No docs directory found")
|
||||
|
||||
output.append("")
|
||||
output.append("Docker:")
|
||||
docker_files = ['Dockerfile', 'docker-compose.yml', '.dockerignore']
|
||||
docker_exists = any(os.path.exists(f) for f in docker_files)
|
||||
if docker_exists:
|
||||
output.append(" ✓ Docker configuration present")
|
||||
if os.path.exists("docker"):
|
||||
output.append(f" - Docker directory files: {count_files('docker', '*')}")
|
||||
else:
|
||||
output.append(" - No Docker configuration found")
|
||||
|
||||
output.append("")
|
||||
output.append("=" * 78)
|
||||
output.append("End of structure")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def generate_test_structure() -> str:
|
||||
"""Generate test structure report."""
|
||||
output = []
|
||||
output.append("Test Folder Structure")
|
||||
output.append(f"Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
output.append("=" * 78)
|
||||
output.append("")
|
||||
|
||||
# Test files section
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ TEST FILES ║")
|
||||
output.append("║ Location: tests/ ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
|
||||
exclude = ['__pycache__', '*.pyc', '*.pyo', '.pytest_cache', '*.egg-info']
|
||||
output.append(get_tree_structure("tests", exclude))
|
||||
|
||||
# Configuration section
|
||||
output.append("")
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ TEST CONFIGURATION ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
|
||||
output.append("Test configuration files:")
|
||||
test_config_files = ['pytest.ini', 'conftest.py', 'tests/conftest.py', '.coveragerc']
|
||||
for file in test_config_files:
|
||||
if os.path.exists(file):
|
||||
output.append(f" ✓ {file}")
|
||||
|
||||
# Statistics section
|
||||
output.append("")
|
||||
output.append("")
|
||||
output.append("╔══════════════════════════════════════════════════════════════════╗")
|
||||
output.append("║ STATISTICS ║")
|
||||
output.append("╚══════════════════════════════════════════════════════════════════╝")
|
||||
output.append("")
|
||||
|
||||
# Count test files
|
||||
test_file_count = 0
|
||||
if os.path.exists("tests"):
|
||||
for root, dirs, files in os.walk("tests"):
|
||||
dirs[:] = [d for d in dirs if d != '__pycache__']
|
||||
for file in files:
|
||||
if file.startswith("test_") and file.endswith(".py"):
|
||||
test_file_count += 1
|
||||
|
||||
output.append("Test Files:")
|
||||
output.append(f" - Total test files: {test_file_count}")
|
||||
|
||||
output.append("")
|
||||
output.append("By Category:")
|
||||
categories = ['unit', 'integration', 'system', 'e2e', 'performance']
|
||||
for category in categories:
|
||||
category_path = f"tests/{category}"
|
||||
if os.path.exists(category_path):
|
||||
count = 0
|
||||
for root, dirs, files in os.walk(category_path):
|
||||
dirs[:] = [d for d in dirs if d != '__pycache__']
|
||||
for file in files:
|
||||
if file.startswith("test_") and file.endswith(".py"):
|
||||
count += 1
|
||||
output.append(f" - tests/{category}: {count} files")
|
||||
|
||||
# Count test functions
|
||||
test_function_count = 0
|
||||
if os.path.exists("tests"):
|
||||
for root, dirs, files in os.walk("tests"):
|
||||
dirs[:] = [d for d in dirs if d != '__pycache__']
|
||||
for file in files:
|
||||
if file.startswith("test_") and file.endswith(".py"):
|
||||
filepath = os.path.join(root, file)
|
||||
try:
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
if line.strip().startswith("def test_"):
|
||||
test_function_count += 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
output.append("")
|
||||
output.append("Test Functions:")
|
||||
output.append(f" - Total test functions: {test_function_count}")
|
||||
|
||||
output.append("")
|
||||
output.append("Coverage Files:")
|
||||
if os.path.exists(".coverage"):
|
||||
output.append(" ✓ .coverage (coverage data file exists)")
|
||||
if os.path.exists("htmlcov"):
|
||||
output.append(" ✓ htmlcov/ (HTML coverage report exists)")
|
||||
|
||||
output.append("")
|
||||
output.append("=" * 78)
|
||||
output.append("End of structure")
|
||||
|
||||
return "\n".join(output)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python show_structure.py [frontend|backend|tests|tools|all]")
|
||||
sys.exit(1)
|
||||
|
||||
structure_type = sys.argv[1].lower()
|
||||
|
||||
generators = {
|
||||
'frontend': ('frontend-structure.txt', generate_frontend_structure),
|
||||
'backend': ('backend-structure.txt', generate_backend_structure),
|
||||
'tests': ('test-structure.txt', generate_test_structure),
|
||||
'tools': ('tools-structure.txt', generate_tools_structure),
|
||||
}
|
||||
|
||||
if structure_type == 'all':
|
||||
for name, (filename, generator) in generators.items():
|
||||
print(f"\n{'=' * 60}")
|
||||
print(f"Generating {name} structure...")
|
||||
print('=' * 60)
|
||||
content = generator()
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f"✅ {name.capitalize()} structure saved to {filename}")
|
||||
print(f"\n{content}\n")
|
||||
elif structure_type in generators:
|
||||
filename, generator = generators[structure_type]
|
||||
print(f"Generating {structure_type} structure...")
|
||||
content = generator()
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
print(f"\n✅ Structure saved to {filename}\n")
|
||||
print(content)
|
||||
else:
|
||||
print(f"Error: Unknown structure type '{structure_type}'")
|
||||
print("Valid options: frontend, backend, tests, tools, all")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user