fix: replace account_id=None with PLATFORM_ACCOUNT_ID for global content
After migration 174f442795b7 enforces NOT NULL on account_id, all platform/global content must use the sentinel platform account instead of NULL. Three categories of fixes: 1. trees.py: is_default trees now get PLATFORM_ACCOUNT_ID (not None) 2. admin_categories.py: global category CRUD now uses PLATFORM_ACCOUNT_ID 3. categories.py, tags.py, step_categories.py: creation endpoints coerce None → PLATFORM_ACCOUNT_ID; IS NULL filter queries updated to == PLATFORM_ACCOUNT_ID (IS NULL queries returned empty after migration backfilled all global rows to the platform account) Defines PLATFORM_ACCOUNT_ID constant in app/core/service_account.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@ from app.models.category import TreeCategory
|
||||
from app.models.tree import Tree
|
||||
from app.schemas.admin import GlobalCategoryCreate, GlobalCategoryUpdate, GlobalCategoryResponse
|
||||
from app.api.deps import require_admin
|
||||
from app.core.service_account import PLATFORM_ACCOUNT_ID
|
||||
|
||||
router = APIRouter(prefix="/admin/categories", tags=["admin-categories"])
|
||||
|
||||
@@ -22,7 +23,7 @@ async def list_global_categories(
|
||||
):
|
||||
"""List all global categories (account_id IS NULL)."""
|
||||
result = await db.execute(
|
||||
select(TreeCategory).where(TreeCategory.account_id.is_(None)).order_by(TreeCategory.name)
|
||||
select(TreeCategory).where(TreeCategory.account_id == PLATFORM_ACCOUNT_ID).order_by(TreeCategory.name)
|
||||
)
|
||||
categories = result.scalars().all()
|
||||
|
||||
@@ -51,18 +52,18 @@ async def create_global_category(
|
||||
"""Create a global category."""
|
||||
# Check slug uniqueness for global categories
|
||||
existing = await db.execute(
|
||||
select(TreeCategory).where(TreeCategory.slug == data.slug, TreeCategory.account_id.is_(None))
|
||||
select(TreeCategory).where(TreeCategory.slug == data.slug, TreeCategory.account_id == PLATFORM_ACCOUNT_ID)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Global category with this slug already exists")
|
||||
|
||||
category = TreeCategory(name=data.name, slug=data.slug, account_id=None)
|
||||
category = TreeCategory(name=data.name, slug=data.slug, account_id=PLATFORM_ACCOUNT_ID)
|
||||
db.add(category)
|
||||
await log_audit(db, current_user.id, "global_category.create", "category", details={"name": data.name})
|
||||
await db.commit()
|
||||
await db.refresh(category)
|
||||
|
||||
return GlobalCategoryResponse(id=category.id, name=category.name, slug=category.slug, account_id=None, tree_count=0)
|
||||
return GlobalCategoryResponse(id=category.id, name=category.name, slug=category.slug, account_id=PLATFORM_ACCOUNT_ID, tree_count=0)
|
||||
|
||||
|
||||
@router.put("/global/{category_id}", response_model=GlobalCategoryResponse)
|
||||
@@ -74,7 +75,7 @@ async def update_global_category(
|
||||
):
|
||||
"""Update a global category."""
|
||||
result = await db.execute(
|
||||
select(TreeCategory).where(TreeCategory.id == category_id, TreeCategory.account_id.is_(None))
|
||||
select(TreeCategory).where(TreeCategory.id == category_id, TreeCategory.account_id == PLATFORM_ACCOUNT_ID)
|
||||
)
|
||||
category = result.scalar_one_or_none()
|
||||
if not category:
|
||||
@@ -86,7 +87,7 @@ async def update_global_category(
|
||||
# Check slug uniqueness
|
||||
existing = await db.execute(
|
||||
select(TreeCategory).where(
|
||||
TreeCategory.slug == data.slug, TreeCategory.account_id.is_(None), TreeCategory.id != category_id
|
||||
TreeCategory.slug == data.slug, TreeCategory.account_id == PLATFORM_ACCOUNT_ID, TreeCategory.id != category_id
|
||||
)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
@@ -103,7 +104,7 @@ async def update_global_category(
|
||||
|
||||
return GlobalCategoryResponse(
|
||||
id=category.id, name=category.name, slug=category.slug,
|
||||
account_id=None, tree_count=tree_count,
|
||||
account_id=PLATFORM_ACCOUNT_ID, tree_count=tree_count,
|
||||
)
|
||||
|
||||
|
||||
@@ -115,7 +116,7 @@ async def delete_global_category(
|
||||
):
|
||||
"""Delete (archive) a global category."""
|
||||
result = await db.execute(
|
||||
select(TreeCategory).where(TreeCategory.id == category_id, TreeCategory.account_id.is_(None))
|
||||
select(TreeCategory).where(TreeCategory.id == category_id, TreeCategory.account_id == PLATFORM_ACCOUNT_ID)
|
||||
)
|
||||
category = result.scalar_one_or_none()
|
||||
if not category:
|
||||
|
||||
@@ -12,6 +12,7 @@ from app.models.user import User
|
||||
from app.schemas.category import CategoryCreate, CategoryUpdate, CategoryResponse, CategoryListResponse
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.core.permissions import can_manage_category, can_create_category
|
||||
from app.core.service_account import PLATFORM_ACCOUNT_ID
|
||||
|
||||
router = APIRouter(prefix="/categories", tags=["categories"])
|
||||
|
||||
@@ -47,13 +48,13 @@ async def list_categories(
|
||||
elif current_user.account_id:
|
||||
query = query.where(
|
||||
or_(
|
||||
TreeCategory.account_id.is_(None), # Global
|
||||
TreeCategory.account_id == PLATFORM_ACCOUNT_ID, # Global
|
||||
TreeCategory.account_id == current_user.account_id # User's account
|
||||
)
|
||||
)
|
||||
else:
|
||||
# User has no account, only show global categories
|
||||
query = query.where(TreeCategory.account_id.is_(None))
|
||||
query = query.where(TreeCategory.account_id == PLATFORM_ACCOUNT_ID)
|
||||
|
||||
query = query.order_by(TreeCategory.display_order, TreeCategory.name)
|
||||
|
||||
@@ -173,7 +174,7 @@ async def create_category(
|
||||
name=category_data.name,
|
||||
slug=slug,
|
||||
description=category_data.description,
|
||||
account_id=category_data.account_id,
|
||||
account_id=category_data.account_id if category_data.account_id is not None else PLATFORM_ACCOUNT_ID,
|
||||
display_order=max_order + 1,
|
||||
created_by=current_user.id
|
||||
)
|
||||
|
||||
@@ -16,6 +16,7 @@ from app.schemas.step_category import (
|
||||
)
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.core.permissions import can_manage_step_category, can_create_step_category
|
||||
from app.core.service_account import PLATFORM_ACCOUNT_ID
|
||||
|
||||
router = APIRouter(prefix="/step-categories", tags=["step-categories"])
|
||||
|
||||
@@ -44,13 +45,13 @@ async def list_step_categories(
|
||||
elif current_user.account_id:
|
||||
query = query.where(
|
||||
or_(
|
||||
StepCategory.account_id.is_(None), # Global
|
||||
StepCategory.account_id == PLATFORM_ACCOUNT_ID, # Global
|
||||
StepCategory.account_id == current_user.account_id # User's account
|
||||
)
|
||||
)
|
||||
else:
|
||||
# User has no account, only show global categories
|
||||
query = query.where(StepCategory.account_id.is_(None))
|
||||
query = query.where(StepCategory.account_id == PLATFORM_ACCOUNT_ID)
|
||||
|
||||
query = query.order_by(StepCategory.display_order, StepCategory.name)
|
||||
|
||||
@@ -155,7 +156,7 @@ async def create_step_category(
|
||||
name=category_data.name,
|
||||
slug=slug,
|
||||
description=category_data.description,
|
||||
account_id=category_data.account_id,
|
||||
account_id=category_data.account_id if category_data.account_id is not None else PLATFORM_ACCOUNT_ID,
|
||||
display_order=max_order + 1,
|
||||
created_by=current_user.id
|
||||
)
|
||||
|
||||
@@ -12,6 +12,7 @@ from app.models.user import User
|
||||
from app.schemas.tag import TagCreate, TagResponse, TagListResponse, TagAssignment
|
||||
from app.api.deps import get_current_active_user
|
||||
from app.core.permissions import can_manage_tree_tags, can_create_tag
|
||||
from app.core.service_account import PLATFORM_ACCOUNT_ID
|
||||
|
||||
router = APIRouter(prefix="/tags", tags=["tags"])
|
||||
|
||||
@@ -33,13 +34,13 @@ async def list_tags(
|
||||
if include_account and current_user.account_id:
|
||||
query = query.where(
|
||||
or_(
|
||||
TreeTag.account_id.is_(None), # Global
|
||||
TreeTag.account_id == PLATFORM_ACCOUNT_ID, # Global
|
||||
TreeTag.account_id == current_user.account_id # User's account
|
||||
)
|
||||
)
|
||||
else:
|
||||
# Only show global tags
|
||||
query = query.where(TreeTag.account_id.is_(None))
|
||||
query = query.where(TreeTag.account_id == PLATFORM_ACCOUNT_ID)
|
||||
|
||||
query = query.order_by(TreeTag.usage_count.desc(), TreeTag.name)
|
||||
|
||||
@@ -71,12 +72,12 @@ async def search_tags(
|
||||
if include_account and current_user.account_id:
|
||||
query = query.where(
|
||||
or_(
|
||||
TreeTag.account_id.is_(None),
|
||||
TreeTag.account_id == PLATFORM_ACCOUNT_ID,
|
||||
TreeTag.account_id == current_user.account_id
|
||||
)
|
||||
)
|
||||
else:
|
||||
query = query.where(TreeTag.account_id.is_(None))
|
||||
query = query.where(TreeTag.account_id == PLATFORM_ACCOUNT_ID)
|
||||
|
||||
query = query.order_by(TreeTag.usage_count.desc(), TreeTag.name).limit(limit)
|
||||
|
||||
@@ -147,7 +148,7 @@ async def create_tag(
|
||||
new_tag = TreeTag(
|
||||
name=tag_data.name,
|
||||
slug=slug,
|
||||
account_id=tag_data.account_id,
|
||||
account_id=tag_data.account_id if tag_data.account_id is not None else PLATFORM_ACCOUNT_ID,
|
||||
created_by=current_user.id
|
||||
)
|
||||
db.add(new_tag)
|
||||
@@ -206,7 +207,7 @@ async def add_tags_to_tree(
|
||||
tag_query = select(TreeTag).where(
|
||||
TreeTag.slug == slug,
|
||||
or_(
|
||||
TreeTag.account_id.is_(None), # Global tag
|
||||
TreeTag.account_id == PLATFORM_ACCOUNT_ID, # Global tag
|
||||
TreeTag.account_id == tag_account_id # Account tag
|
||||
)
|
||||
)
|
||||
@@ -340,7 +341,7 @@ async def replace_tree_tags(
|
||||
tag_query = select(TreeTag).where(
|
||||
TreeTag.slug == slug,
|
||||
or_(
|
||||
TreeTag.account_id.is_(None),
|
||||
TreeTag.account_id == PLATFORM_ACCOUNT_ID,
|
||||
TreeTag.account_id == tag_account_id
|
||||
)
|
||||
)
|
||||
|
||||
@@ -29,6 +29,7 @@ from app.core.subscriptions import check_tree_limit, get_account_subscription, g
|
||||
from app.core.audit import log_audit
|
||||
from app.core.config import settings
|
||||
from app.core.tree_validation import can_publish_tree
|
||||
from app.core.service_account import PLATFORM_ACCOUNT_ID
|
||||
from app.core.step_sync import sync_steps_from_tree, deactivate_synced_steps_for_tree
|
||||
from app.services.rag_service import index_tree as rag_index_tree
|
||||
|
||||
@@ -470,7 +471,7 @@ async def create_tree(
|
||||
tree_structure=tree_data.tree_structure,
|
||||
intake_form=intake_form_data,
|
||||
author_id=service_account_id if is_default else current_user.id,
|
||||
account_id=None if is_default else current_user.account_id,
|
||||
account_id=PLATFORM_ACCOUNT_ID if is_default else current_user.account_id,
|
||||
is_public=True if is_default else tree_data.is_public, # Default trees are always public
|
||||
is_default=is_default,
|
||||
status=tree_data.status
|
||||
|
||||
@@ -18,6 +18,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com"
|
||||
SERVICE_ACCOUNT_NAME = "ResolutionFlow"
|
||||
|
||||
# Well-known UUID for the platform account — owns all default/global content.
|
||||
# Created by migration 3a40fe11b427_create_global_content_tables.
|
||||
PLATFORM_ACCOUNT_ID = uuid.UUID("00000000-0000-0000-0000-000000000001")
|
||||
SYSTEM_ACCOUNT_NAME = "ResolutionFlow System"
|
||||
SYSTEM_ACCOUNT_DISPLAY_CODE = "RF-SYS-1"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user