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.models.tree import Tree
|
||||||
from app.schemas.admin import GlobalCategoryCreate, GlobalCategoryUpdate, GlobalCategoryResponse
|
from app.schemas.admin import GlobalCategoryCreate, GlobalCategoryUpdate, GlobalCategoryResponse
|
||||||
from app.api.deps import require_admin
|
from app.api.deps import require_admin
|
||||||
|
from app.core.service_account import PLATFORM_ACCOUNT_ID
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin/categories", tags=["admin-categories"])
|
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)."""
|
"""List all global categories (account_id IS NULL)."""
|
||||||
result = await db.execute(
|
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()
|
categories = result.scalars().all()
|
||||||
|
|
||||||
@@ -51,18 +52,18 @@ async def create_global_category(
|
|||||||
"""Create a global category."""
|
"""Create a global category."""
|
||||||
# Check slug uniqueness for global categories
|
# Check slug uniqueness for global categories
|
||||||
existing = await db.execute(
|
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():
|
if existing.scalar_one_or_none():
|
||||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Global category with this slug already exists")
|
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)
|
db.add(category)
|
||||||
await log_audit(db, current_user.id, "global_category.create", "category", details={"name": data.name})
|
await log_audit(db, current_user.id, "global_category.create", "category", details={"name": data.name})
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(category)
|
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)
|
@router.put("/global/{category_id}", response_model=GlobalCategoryResponse)
|
||||||
@@ -74,7 +75,7 @@ async def update_global_category(
|
|||||||
):
|
):
|
||||||
"""Update a global category."""
|
"""Update a global category."""
|
||||||
result = await db.execute(
|
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()
|
category = result.scalar_one_or_none()
|
||||||
if not category:
|
if not category:
|
||||||
@@ -86,7 +87,7 @@ async def update_global_category(
|
|||||||
# Check slug uniqueness
|
# Check slug uniqueness
|
||||||
existing = await db.execute(
|
existing = await db.execute(
|
||||||
select(TreeCategory).where(
|
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():
|
if existing.scalar_one_or_none():
|
||||||
@@ -103,7 +104,7 @@ async def update_global_category(
|
|||||||
|
|
||||||
return GlobalCategoryResponse(
|
return GlobalCategoryResponse(
|
||||||
id=category.id, name=category.name, slug=category.slug,
|
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."""
|
"""Delete (archive) a global category."""
|
||||||
result = await db.execute(
|
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()
|
category = result.scalar_one_or_none()
|
||||||
if not category:
|
if not category:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from app.models.user import User
|
|||||||
from app.schemas.category import CategoryCreate, CategoryUpdate, CategoryResponse, CategoryListResponse
|
from app.schemas.category import CategoryCreate, CategoryUpdate, CategoryResponse, CategoryListResponse
|
||||||
from app.api.deps import get_current_active_user
|
from app.api.deps import get_current_active_user
|
||||||
from app.core.permissions import can_manage_category, can_create_category
|
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"])
|
router = APIRouter(prefix="/categories", tags=["categories"])
|
||||||
|
|
||||||
@@ -47,13 +48,13 @@ async def list_categories(
|
|||||||
elif current_user.account_id:
|
elif current_user.account_id:
|
||||||
query = query.where(
|
query = query.where(
|
||||||
or_(
|
or_(
|
||||||
TreeCategory.account_id.is_(None), # Global
|
TreeCategory.account_id == PLATFORM_ACCOUNT_ID, # Global
|
||||||
TreeCategory.account_id == current_user.account_id # User's account
|
TreeCategory.account_id == current_user.account_id # User's account
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# User has no account, only show global categories
|
# 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)
|
query = query.order_by(TreeCategory.display_order, TreeCategory.name)
|
||||||
|
|
||||||
@@ -173,7 +174,7 @@ async def create_category(
|
|||||||
name=category_data.name,
|
name=category_data.name,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
description=category_data.description,
|
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,
|
display_order=max_order + 1,
|
||||||
created_by=current_user.id
|
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.api.deps import get_current_active_user
|
||||||
from app.core.permissions import can_manage_step_category, can_create_step_category
|
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"])
|
router = APIRouter(prefix="/step-categories", tags=["step-categories"])
|
||||||
|
|
||||||
@@ -44,13 +45,13 @@ async def list_step_categories(
|
|||||||
elif current_user.account_id:
|
elif current_user.account_id:
|
||||||
query = query.where(
|
query = query.where(
|
||||||
or_(
|
or_(
|
||||||
StepCategory.account_id.is_(None), # Global
|
StepCategory.account_id == PLATFORM_ACCOUNT_ID, # Global
|
||||||
StepCategory.account_id == current_user.account_id # User's account
|
StepCategory.account_id == current_user.account_id # User's account
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# User has no account, only show global categories
|
# 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)
|
query = query.order_by(StepCategory.display_order, StepCategory.name)
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@ async def create_step_category(
|
|||||||
name=category_data.name,
|
name=category_data.name,
|
||||||
slug=slug,
|
slug=slug,
|
||||||
description=category_data.description,
|
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,
|
display_order=max_order + 1,
|
||||||
created_by=current_user.id
|
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.schemas.tag import TagCreate, TagResponse, TagListResponse, TagAssignment
|
||||||
from app.api.deps import get_current_active_user
|
from app.api.deps import get_current_active_user
|
||||||
from app.core.permissions import can_manage_tree_tags, can_create_tag
|
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"])
|
router = APIRouter(prefix="/tags", tags=["tags"])
|
||||||
|
|
||||||
@@ -33,13 +34,13 @@ async def list_tags(
|
|||||||
if include_account and current_user.account_id:
|
if include_account and current_user.account_id:
|
||||||
query = query.where(
|
query = query.where(
|
||||||
or_(
|
or_(
|
||||||
TreeTag.account_id.is_(None), # Global
|
TreeTag.account_id == PLATFORM_ACCOUNT_ID, # Global
|
||||||
TreeTag.account_id == current_user.account_id # User's account
|
TreeTag.account_id == current_user.account_id # User's account
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Only show global tags
|
# 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)
|
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:
|
if include_account and current_user.account_id:
|
||||||
query = query.where(
|
query = query.where(
|
||||||
or_(
|
or_(
|
||||||
TreeTag.account_id.is_(None),
|
TreeTag.account_id == PLATFORM_ACCOUNT_ID,
|
||||||
TreeTag.account_id == current_user.account_id
|
TreeTag.account_id == current_user.account_id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
else:
|
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)
|
query = query.order_by(TreeTag.usage_count.desc(), TreeTag.name).limit(limit)
|
||||||
|
|
||||||
@@ -147,7 +148,7 @@ async def create_tag(
|
|||||||
new_tag = TreeTag(
|
new_tag = TreeTag(
|
||||||
name=tag_data.name,
|
name=tag_data.name,
|
||||||
slug=slug,
|
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
|
created_by=current_user.id
|
||||||
)
|
)
|
||||||
db.add(new_tag)
|
db.add(new_tag)
|
||||||
@@ -206,7 +207,7 @@ async def add_tags_to_tree(
|
|||||||
tag_query = select(TreeTag).where(
|
tag_query = select(TreeTag).where(
|
||||||
TreeTag.slug == slug,
|
TreeTag.slug == slug,
|
||||||
or_(
|
or_(
|
||||||
TreeTag.account_id.is_(None), # Global tag
|
TreeTag.account_id == PLATFORM_ACCOUNT_ID, # Global tag
|
||||||
TreeTag.account_id == tag_account_id # Account tag
|
TreeTag.account_id == tag_account_id # Account tag
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -340,7 +341,7 @@ async def replace_tree_tags(
|
|||||||
tag_query = select(TreeTag).where(
|
tag_query = select(TreeTag).where(
|
||||||
TreeTag.slug == slug,
|
TreeTag.slug == slug,
|
||||||
or_(
|
or_(
|
||||||
TreeTag.account_id.is_(None),
|
TreeTag.account_id == PLATFORM_ACCOUNT_ID,
|
||||||
TreeTag.account_id == tag_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.audit import log_audit
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.tree_validation import can_publish_tree
|
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.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
|
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,
|
tree_structure=tree_data.tree_structure,
|
||||||
intake_form=intake_form_data,
|
intake_form=intake_form_data,
|
||||||
author_id=service_account_id if is_default else current_user.id,
|
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_public=True if is_default else tree_data.is_public, # Default trees are always public
|
||||||
is_default=is_default,
|
is_default=is_default,
|
||||||
status=tree_data.status
|
status=tree_data.status
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com"
|
SERVICE_ACCOUNT_EMAIL = "noreply@resolutionflow.com"
|
||||||
SERVICE_ACCOUNT_NAME = "ResolutionFlow"
|
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_NAME = "ResolutionFlow System"
|
||||||
SYSTEM_ACCOUNT_DISPLAY_CODE = "RF-SYS-1"
|
SYSTEM_ACCOUNT_DISPLAY_CODE = "RF-SYS-1"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user