Add is_default flag for system trees
- Add is_default column to trees table - Default trees have no author and are visible to all users - Only admins can create default trees - Update seed script to mark seeded trees as default - Update seed script to use CLI auth instead of creating seed user Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
28
backend/alembic/versions/003_add_tree_is_default.py
Normal file
28
backend/alembic/versions/003_add_tree_is_default.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
"""Add is_default field to trees
|
||||||
|
|
||||||
|
Revision ID: 003
|
||||||
|
Revises: 002
|
||||||
|
Create Date: 2026-02-01
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '003'
|
||||||
|
down_revision: Union[str, None] = '002'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column('trees', sa.Column('is_default', sa.Boolean(), nullable=False, server_default='false'))
|
||||||
|
op.create_index('ix_trees_is_default', 'trees', ['is_default'])
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_index('ix_trees_is_default', table_name='trees')
|
||||||
|
op.drop_column('trees', 'is_default')
|
||||||
@@ -120,13 +120,17 @@ async def create_tree(
|
|||||||
current_user: Annotated[User, Depends(require_engineer_or_admin)]
|
current_user: Annotated[User, Depends(require_engineer_or_admin)]
|
||||||
):
|
):
|
||||||
"""Create a new tree (engineers and admins only)."""
|
"""Create a new tree (engineers and admins only)."""
|
||||||
|
# Only admins can create default/system trees
|
||||||
|
is_default = tree_data.is_default and current_user.role == "admin"
|
||||||
|
|
||||||
new_tree = Tree(
|
new_tree = Tree(
|
||||||
name=tree_data.name,
|
name=tree_data.name,
|
||||||
description=tree_data.description,
|
description=tree_data.description,
|
||||||
category=tree_data.category,
|
category=tree_data.category,
|
||||||
tree_structure=tree_data.tree_structure,
|
tree_structure=tree_data.tree_structure,
|
||||||
author_id=current_user.id,
|
author_id=None if is_default else current_user.id, # Default trees have no author
|
||||||
team_id=current_user.team_id
|
team_id=None if is_default else current_user.team_id,
|
||||||
|
is_default=is_default
|
||||||
)
|
)
|
||||||
db.add(new_tree)
|
db.add(new_tree)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class Tree(Base):
|
|||||||
index=True
|
index=True
|
||||||
)
|
)
|
||||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||||
|
is_default: Mapped[bool] = mapped_column(Boolean, default=False, index=True)
|
||||||
version: Mapped[int] = mapped_column(Integer, default=1)
|
version: Mapped[int] = mapped_column(Integer, default=1)
|
||||||
created_at: Mapped[datetime] = mapped_column(
|
created_at: Mapped[datetime] = mapped_column(
|
||||||
DateTime(timezone=True),
|
DateTime(timezone=True),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class TreeBase(BaseModel):
|
|||||||
|
|
||||||
class TreeCreate(TreeBase):
|
class TreeCreate(TreeBase):
|
||||||
tree_structure: dict[str, Any] = Field(..., description="The decision tree structure in JSON format")
|
tree_structure: dict[str, Any] = Field(..., description="The decision tree structure in JSON format")
|
||||||
|
is_default: bool = Field(False, description="Mark as a default/system tree (admin only)")
|
||||||
|
|
||||||
|
|
||||||
class TreeUpdate(BaseModel):
|
class TreeUpdate(BaseModel):
|
||||||
@@ -28,6 +29,7 @@ class TreeResponse(TreeBase):
|
|||||||
author_id: Optional[UUID] = None
|
author_id: Optional[UUID] = None
|
||||||
team_id: Optional[UUID] = None
|
team_id: Optional[UUID] = None
|
||||||
is_active: bool
|
is_active: bool
|
||||||
|
is_default: bool
|
||||||
version: int
|
version: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
updated_at: datetime
|
updated_at: datetime
|
||||||
@@ -43,6 +45,7 @@ class TreeListResponse(BaseModel):
|
|||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
category: Optional[str] = None
|
category: Optional[str] = None
|
||||||
is_active: bool
|
is_active: bool
|
||||||
|
is_default: bool
|
||||||
version: int
|
version: int
|
||||||
usage_count: int
|
usage_count: int
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|||||||
@@ -20,13 +20,9 @@ from typing import Any
|
|||||||
# API Configuration
|
# API Configuration
|
||||||
API_BASE_URL = "http://localhost:8000/api/v1"
|
API_BASE_URL = "http://localhost:8000/api/v1"
|
||||||
|
|
||||||
# Default admin user for seeding
|
# Admin credentials (set via command line or environment)
|
||||||
SEED_USER = {
|
ADMIN_EMAIL = None
|
||||||
"email": "seed.admin@example.com",
|
ADMIN_PASSWORD = None
|
||||||
"password": "SeedAdmin123!",
|
|
||||||
"name": "Seed Admin",
|
|
||||||
"role": "admin"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
@@ -3308,39 +3304,23 @@ def get_file_share_access_tree() -> dict[str, Any]:
|
|||||||
# SEEDING FUNCTIONS
|
# SEEDING FUNCTIONS
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
async def get_or_create_admin_user(client: httpx.AsyncClient) -> tuple[str, dict]:
|
async def get_admin_token(client: httpx.AsyncClient) -> str:
|
||||||
"""Get authentication token for seeding. Creates admin user if needed."""
|
"""Get authentication token using provided admin credentials."""
|
||||||
|
|
||||||
# Try to login first
|
if not ADMIN_EMAIL or not ADMIN_PASSWORD:
|
||||||
|
raise Exception("Admin email and password are required. Use --email and --password arguments.")
|
||||||
|
|
||||||
|
# Login with provided credentials
|
||||||
login_response = await client.post(
|
login_response = await client.post(
|
||||||
f"{API_BASE_URL}/auth/login/json",
|
f"{API_BASE_URL}/auth/login",
|
||||||
json={"email": SEED_USER["email"], "password": SEED_USER["password"]}
|
data={"username": ADMIN_EMAIL, "password": ADMIN_PASSWORD}
|
||||||
)
|
|
||||||
|
|
||||||
if login_response.status_code == 200:
|
|
||||||
token_data = login_response.json()
|
|
||||||
return token_data["access_token"], {"exists": True}
|
|
||||||
|
|
||||||
# User doesn't exist, create them
|
|
||||||
register_response = await client.post(
|
|
||||||
f"{API_BASE_URL}/auth/register",
|
|
||||||
json=SEED_USER
|
|
||||||
)
|
|
||||||
|
|
||||||
if register_response.status_code not in (200, 201):
|
|
||||||
raise Exception(f"Failed to create seed user: {register_response.text}")
|
|
||||||
|
|
||||||
# Now login
|
|
||||||
login_response = await client.post(
|
|
||||||
f"{API_BASE_URL}/auth/login/json",
|
|
||||||
json={"email": SEED_USER["email"], "password": SEED_USER["password"]}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if login_response.status_code != 200:
|
if login_response.status_code != 200:
|
||||||
raise Exception(f"Failed to login as seed user: {login_response.text}")
|
raise Exception(f"Failed to login: {login_response.text}")
|
||||||
|
|
||||||
token_data = login_response.json()
|
token_data = login_response.json()
|
||||||
return token_data["access_token"], {"exists": False, "user": register_response.json()}
|
return token_data["access_token"]
|
||||||
|
|
||||||
|
|
||||||
async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict) -> dict | None:
|
async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict) -> dict | None:
|
||||||
@@ -3356,6 +3336,9 @@ async def create_tree(client: httpx.AsyncClient, token: str, tree_data: dict) ->
|
|||||||
print(f" [SKIP] Tree '{tree_data['name']}' already exists (ID: {tree['id']})")
|
print(f" [SKIP] Tree '{tree_data['name']}' already exists (ID: {tree['id']})")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# Mark as default/system tree
|
||||||
|
tree_data["is_default"] = True
|
||||||
|
|
||||||
# Create the tree
|
# Create the tree
|
||||||
response = await client.post(
|
response = await client.post(
|
||||||
f"{API_BASE_URL}/trees",
|
f"{API_BASE_URL}/trees",
|
||||||
@@ -3390,16 +3373,13 @@ async def seed_database():
|
|||||||
print(" Run: uvicorn app.main:app --reload")
|
print(" Run: uvicorn app.main:app --reload")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get or create admin user
|
# Authenticate with admin credentials
|
||||||
print("\n[1/3] Setting up seed user...")
|
print("\n[1/3] Authenticating...")
|
||||||
try:
|
try:
|
||||||
token, user_info = await get_or_create_admin_user(client)
|
token = await get_admin_token(client)
|
||||||
if user_info.get("exists"):
|
print(f" Logged in as {ADMIN_EMAIL}")
|
||||||
print(" Using existing seed admin user")
|
|
||||||
else:
|
|
||||||
print(f" Created seed admin user: {SEED_USER['email']}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" [ERROR] Failed to setup seed user: {e}")
|
print(f" [ERROR] Failed to authenticate: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Get all tree definitions
|
# Get all tree definitions
|
||||||
@@ -3460,10 +3440,22 @@ def main():
|
|||||||
default="http://localhost:8000/api/v1",
|
default="http://localhost:8000/api/v1",
|
||||||
help="API base URL (default: http://localhost:8000/api/v1)"
|
help="API base URL (default: http://localhost:8000/api/v1)"
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--email",
|
||||||
|
required=True,
|
||||||
|
help="Admin user email for authentication"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--password",
|
||||||
|
required=True,
|
||||||
|
help="Admin user password for authentication"
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
global API_BASE_URL
|
global API_BASE_URL, ADMIN_EMAIL, ADMIN_PASSWORD
|
||||||
API_BASE_URL = args.api_url
|
API_BASE_URL = args.api_url
|
||||||
|
ADMIN_EMAIL = args.email
|
||||||
|
ADMIN_PASSWORD = args.password
|
||||||
|
|
||||||
success = asyncio.run(seed_database())
|
success = asyncio.run(seed_database())
|
||||||
exit(0 if success else 1)
|
exit(0 if success else 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user