- Add OwnedMixin to 15 models (contact_folder, user_preference, workspace, mcp_server_config, agent_definition, automation_definition, report_template, report_instance, entity_link, comm_conversation, proactive_suggestion, ai_agent, ai_chat_session, tag, share_link) - Migration 0102: Add owner_id column to 15 tables with backfill from user_id - Fix EntityPermission Registry: remove notification, add entity_attachment, entity_history, subtask, calendar, folder; fix wrong class names (DmsFile→File, CalendarEvent→CalendarEntry, Mailbox→MailAccount) - Add apply_visibility_filter to list endpoints in tags, tasks, mcp_client, automation, report_generator, ai_assistant routes - Add owner_id to create handlers for all new OwnedMixin models - Patch tasks/services.py and automation/services.py list methods with user_id and is_system_admin parameters
This commit is contained in:
@@ -14,9 +14,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class ContactFolder(Base, TenantMixin):
|
||||
class ContactFolder(Base, TenantMixin, OwnedMixin):
|
||||
"""Hierarchical folder for organizing contacts.
|
||||
|
||||
Folders are tenant-scoped and user-owned. A folder with parent_id=NULL
|
||||
|
||||
@@ -11,9 +11,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class UserPreference(Base, TenantMixin):
|
||||
class UserPreference(Base, TenantMixin, OwnedMixin):
|
||||
"""Per-user preference entry — stores a single UI preference as JSONB value.
|
||||
|
||||
Keys are arbitrary strings (e.g. 'sidebar_collapsed', 'theme', 'active_tab').
|
||||
|
||||
@@ -30,9 +30,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Workspace(Base, TenantMixin):
|
||||
class Workspace(Base, TenantMixin, OwnedMixin):
|
||||
"""A workspace is a UI/navigation context for a user.
|
||||
|
||||
It defines which modules are visible, which dashboard widgets appear,
|
||||
|
||||
@@ -19,6 +19,7 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
# --- Providers ---
|
||||
@@ -101,7 +102,7 @@ class AIPreset(Base, TenantMixin):
|
||||
|
||||
# --- Agents ---
|
||||
|
||||
class AIAgent(Base, TenantMixin):
|
||||
class AIAgent(Base, TenantMixin, OwnedMixin):
|
||||
"""AI agent with system prompt and assigned tools."""
|
||||
|
||||
__tablename__ = "ai_agents"
|
||||
@@ -128,7 +129,7 @@ class AIAgent(Base, TenantMixin):
|
||||
|
||||
# --- Chat Sessions ---
|
||||
|
||||
class AIChatSession(Base, TenantMixin):
|
||||
class AIChatSession(Base, TenantMixin, OwnedMixin):
|
||||
"""Chat session for a user with a specific agent."""
|
||||
|
||||
__tablename__ = "ai_chat_sessions"
|
||||
|
||||
@@ -330,9 +330,13 @@ async def list_agents(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
result = await db.execute(
|
||||
select(AIAgent).where(AIAgent.tenant_id == tenant_id)
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
query = select(AIAgent).where(AIAgent.tenant_id == tenant_id)
|
||||
query = await apply_visibility_filter(
|
||||
db, query, "ai_agent", AIAgent, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
result = await db.execute(query)
|
||||
agents = list(result.scalars().all())
|
||||
return [agent_to_response(a) for a in agents]
|
||||
|
||||
@@ -344,6 +348,7 @@ async def create_agent(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
await set_tenant_context(db, tenant_id)
|
||||
|
||||
agent = AIAgent(
|
||||
@@ -355,6 +360,7 @@ async def create_agent(
|
||||
is_active=data.is_active,
|
||||
config=data.config,
|
||||
tenant_id=tenant_id,
|
||||
owner_id=user_id,
|
||||
)
|
||||
db.add(agent)
|
||||
await db.commit()
|
||||
@@ -422,10 +428,13 @@ async def list_sessions(
|
||||
):
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
stmt = (
|
||||
select(AIChatSession)
|
||||
.where(AIChatSession.tenant_id == tenant_id)
|
||||
.where(AIChatSession.user_id == user_id)
|
||||
)
|
||||
stmt = await apply_visibility_filter(
|
||||
db, stmt, "ai_chat_session", AIChatSession, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
if is_sidebar is not None:
|
||||
stmt = stmt.where(AIChatSession.is_sidebar == is_sidebar)
|
||||
@@ -470,6 +479,7 @@ async def create_session(
|
||||
is_sidebar=data.is_sidebar,
|
||||
folder_id=folder_id,
|
||||
tenant_id=tenant_id,
|
||||
owner_id=user_id,
|
||||
)
|
||||
db.add(session)
|
||||
await db.commit()
|
||||
|
||||
@@ -20,9 +20,10 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class ProactiveSuggestion(Base, TenantMixin):
|
||||
class ProactiveSuggestion(Base, TenantMixin, OwnedMixin):
|
||||
"""A proactive AI suggestion generated from user context."""
|
||||
|
||||
__tablename__ = "ai_proactive_suggestions"
|
||||
|
||||
@@ -16,6 +16,7 @@ from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db, set_tenant_context
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.core.event_bus import get_event_bus
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.ai_proactive.models import (
|
||||
|
||||
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db, set_tenant_context
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.automation.models import (
|
||||
AgentDefinition,
|
||||
@@ -112,8 +113,11 @@ async def list_agents(
|
||||
):
|
||||
"""List agent definitions with optional filters."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
items, total = await AgentService.list(
|
||||
db, tenant_id, is_active=is_active, mode=mode, limit=limit, offset=offset
|
||||
db, tenant_id, is_active=is_active, mode=mode, limit=limit, offset=offset,
|
||||
user_id=user_id, is_system_admin=is_system_admin,
|
||||
)
|
||||
return AgentDefinitionListResponse(
|
||||
items=[_agent_to_response(a) for a in items],
|
||||
|
||||
@@ -21,9 +21,10 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class AgentDefinition(Base, TenantMixin):
|
||||
class AgentDefinition(Base, TenantMixin, OwnedMixin):
|
||||
"""An AI agent definition — configures an LLM-powered agent with tools and behavior."""
|
||||
|
||||
__tablename__ = "automation_agent_definitions"
|
||||
@@ -89,7 +90,7 @@ class AgentVersion(Base, TenantMixin):
|
||||
)
|
||||
|
||||
|
||||
class AutomationDefinition(Base, TenantMixin):
|
||||
class AutomationDefinition(Base, TenantMixin, OwnedMixin):
|
||||
"""An automation workflow definition — event/schedule/manual triggered with conditions and actions."""
|
||||
|
||||
__tablename__ = "automation_definitions"
|
||||
|
||||
@@ -13,6 +13,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db, set_tenant_context
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.automation.models import (
|
||||
AutomationDefinition,
|
||||
@@ -112,9 +113,12 @@ async def list_automations(
|
||||
):
|
||||
"""List automation definitions with optional filters."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
items, total = await AutomationService.list(
|
||||
db, tenant_id, trigger_type=trigger_type, is_active=is_active,
|
||||
limit=limit, offset=offset,
|
||||
user_id=user_id, is_system_admin=is_system_admin,
|
||||
)
|
||||
return AutomationDefinitionListResponse(
|
||||
items=[_automation_to_response(a) for a in items],
|
||||
|
||||
@@ -11,6 +11,7 @@ from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import func, select, text, update
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.plugins.builtins.automation.models import (
|
||||
@@ -40,9 +41,15 @@ class AgentService:
|
||||
mode: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
user_id: uuid.UUID | None = None,
|
||||
is_system_admin: bool = False,
|
||||
) -> tuple[list[AgentDefinition], int]:
|
||||
"""List agent definitions with optional filters."""
|
||||
query = select(AgentDefinition).where(AgentDefinition.tenant_id == tenant_id)
|
||||
if user_id and not is_system_admin:
|
||||
query = await apply_visibility_filter(
|
||||
db, query, "agent_definition", AgentDefinition, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
count_query = select(func.count()).select_from(AgentDefinition).where(
|
||||
AgentDefinition.tenant_id == tenant_id
|
||||
)
|
||||
@@ -108,6 +115,7 @@ class AgentService:
|
||||
max_duration_seconds=data.get("max_duration_seconds", 300),
|
||||
budget_limit_usd=data.get("budget_limit_usd", 1.0),
|
||||
created_by=user_id,
|
||||
owner_id=user_id,
|
||||
)
|
||||
db.add(agent)
|
||||
await db.flush()
|
||||
@@ -291,11 +299,17 @@ class AutomationService:
|
||||
is_active: bool | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
user_id: uuid.UUID | None = None,
|
||||
is_system_admin: bool = False,
|
||||
) -> tuple[list[AutomationDefinition], int]:
|
||||
"""List automation definitions with optional filters."""
|
||||
query = select(AutomationDefinition).where(
|
||||
AutomationDefinition.tenant_id == tenant_id
|
||||
)
|
||||
if user_id and not is_system_admin:
|
||||
query = await apply_visibility_filter(
|
||||
db, query, "automation_definition", AutomationDefinition, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
count_query = (
|
||||
select(func.count())
|
||||
.select_from(AutomationDefinition)
|
||||
@@ -366,6 +380,7 @@ class AutomationService:
|
||||
is_active=data.get("is_active", True),
|
||||
dry_run=data.get("dry_run", False),
|
||||
created_by=user_id,
|
||||
owner_id=user_id,
|
||||
)
|
||||
db.add(automation)
|
||||
await db.flush()
|
||||
|
||||
@@ -9,9 +9,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class EntityLink(Base, TenantMixin):
|
||||
class EntityLink(Base, TenantMixin, OwnedMixin):
|
||||
"""N:M link between files/folders and companies/contacts."""
|
||||
|
||||
__tablename__ = "entity_links"
|
||||
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.entity_links.models import EntityLink
|
||||
from app.plugins.builtins.entity_links.schemas import EntityLinkRequest
|
||||
@@ -92,6 +93,7 @@ async def unlink_file_from_entity(
|
||||
):
|
||||
"""Remove a link between a file and an entity."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
fid = _parse_uuid(file_id, "file_id")
|
||||
entity_id = _parse_uuid(body.entity_id, "entity_id")
|
||||
|
||||
@@ -119,6 +121,7 @@ async def list_file_links(
|
||||
):
|
||||
"""List all entities linked to a file."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
fid = _parse_uuid(file_id, "file_id")
|
||||
|
||||
result = await db.execute(
|
||||
|
||||
@@ -21,9 +21,10 @@ from sqlalchemy.dialects.postgresql import JSONB, UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class CommConversation(Base, TenantMixin):
|
||||
class CommConversation(Base, TenantMixin, OwnedMixin):
|
||||
"""Conversation / Room — tenant-scoped, supports pinning, locking, archiving."""
|
||||
|
||||
__tablename__ = "comm_conversations"
|
||||
|
||||
@@ -11,6 +11,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, UploadFile, File,
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.kommunikation.rbac import CommRBAC
|
||||
from app.plugins.builtins.kommunikation.schemas import (
|
||||
|
||||
@@ -258,6 +258,7 @@ async def create_conversation(
|
||||
conv = CommConversation(
|
||||
tenant_id=tenant_id,
|
||||
title=title,
|
||||
owner_id=user_id,
|
||||
is_direct=is_direct,
|
||||
created_by=user_id,
|
||||
created_by_type="user",
|
||||
@@ -1068,6 +1069,7 @@ async def create_plugin_room(
|
||||
conv = CommConversation(
|
||||
tenant_id=tenant_id,
|
||||
title=title,
|
||||
owner_id=user_id,
|
||||
is_locked=True,
|
||||
locked_by=plugin_name,
|
||||
is_direct=False,
|
||||
|
||||
@@ -10,9 +10,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class McpServerConfig(Base, TenantMixin):
|
||||
class McpServerConfig(Base, TenantMixin, OwnedMixin):
|
||||
"""Configuration for an external MCP server — tenant-scoped."""
|
||||
|
||||
__tablename__ = "mcp_server_configs"
|
||||
|
||||
@@ -12,6 +12,7 @@ from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.mcp_client.client import McpClient
|
||||
from app.plugins.builtins.mcp_client.models import McpServerConfig as McpServerConfigModel
|
||||
@@ -50,7 +51,13 @@ async def list_mcp_servers(
|
||||
current_user: dict[str, Any] = Depends(require_permission("mcp-client:read")),
|
||||
) -> list[McpServerConfigResponse]:
|
||||
"""List all configured MCP servers for the current tenant."""
|
||||
stmt = select(McpServerConfigModel).where(McpServerConfigModel.tenant_id == uuid.UUID(current_user["tenant_id"]))
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
stmt = select(McpServerConfigModel).where(McpServerConfigModel.tenant_id == tenant_id)
|
||||
stmt = await apply_visibility_filter(
|
||||
db, stmt, "mcp_server_config", McpServerConfigModel, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
configs = result.scalars().all()
|
||||
return [_config_to_response(c) for c in configs]
|
||||
@@ -71,6 +78,7 @@ async def create_mcp_server(
|
||||
enabled=body.enabled,
|
||||
description=body.description,
|
||||
created_by=uuid.UUID(current_user["user_id"]),
|
||||
owner_id=uuid.UUID(current_user["user_id"]),
|
||||
)
|
||||
db.add(cfg)
|
||||
await db.commit()
|
||||
|
||||
@@ -10,6 +10,7 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Permission(Base, TenantMixin):
|
||||
@@ -37,7 +38,7 @@ class Permission(Base, TenantMixin):
|
||||
access_level: Mapped[str] = mapped_column(String(10), nullable=False, default="read")
|
||||
|
||||
|
||||
class ShareLink(Base, TenantMixin):
|
||||
class ShareLink(Base, TenantMixin, OwnedMixin):
|
||||
"""Public share link for a file — optional password and expiry."""
|
||||
|
||||
__tablename__ = "share_links"
|
||||
|
||||
@@ -12,6 +12,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.auth import hash_password, verify_password
|
||||
from app.core.db import get_db
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.permissions.models import Permission, ShareLink
|
||||
from app.plugins.builtins.permissions.schemas import (
|
||||
@@ -48,6 +49,7 @@ async def list_permissions(
|
||||
):
|
||||
"""List all permissions for a file."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
fid = _parse_uuid(file_id, "file_id")
|
||||
|
||||
result = await db.execute(
|
||||
@@ -78,6 +80,7 @@ async def grant_permission(
|
||||
):
|
||||
"""Grant a permission on a file to a user."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
fid = _parse_uuid(file_id, "file_id")
|
||||
user_id = _parse_uuid(body.user_id, "user_id")
|
||||
group_id = _parse_uuid(body.group_id, "group_id") if body.group_id else None
|
||||
@@ -123,6 +126,7 @@ async def revoke_permission(
|
||||
):
|
||||
"""Revoke all permissions for a user on a file."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
fid = _parse_uuid(file_id, "file_id")
|
||||
uid = _parse_uuid(user_id, "user_id")
|
||||
|
||||
|
||||
@@ -9,9 +9,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class ReportTemplate(Base, TenantMixin):
|
||||
class ReportTemplate(Base, TenantMixin, OwnedMixin):
|
||||
"""Report template entity — Jinja2 or SQL template, tenant-scoped, soft-deletable."""
|
||||
|
||||
__tablename__ = "report_templates"
|
||||
@@ -35,7 +36,7 @@ class ReportTemplate(Base, TenantMixin):
|
||||
created_by: Mapped[uuid.UUID] = mapped_column(PGUUID(as_uuid=True), nullable=False)
|
||||
|
||||
|
||||
class ReportInstance(Base, TenantMixin):
|
||||
class ReportInstance(Base, TenantMixin, OwnedMixin):
|
||||
"""Report instance entity — a generated report, tenant-scoped."""
|
||||
|
||||
__tablename__ = "report_instances"
|
||||
|
||||
@@ -16,6 +16,7 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.contact import Contact
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.models.audit import AuditLog
|
||||
|
||||
from app.core.db import get_db, set_tenant_context
|
||||
@@ -292,12 +293,16 @@ async def list_templates(
|
||||
):
|
||||
"""List all report templates for the current tenant."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
result = await db.execute(
|
||||
select(ReportTemplate).where(
|
||||
ReportTemplate.tenant_id == tenant_id,
|
||||
ReportTemplate.deleted_at.is_(None),
|
||||
)
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
query = select(ReportTemplate).where(
|
||||
ReportTemplate.tenant_id == tenant_id,
|
||||
ReportTemplate.deleted_at.is_(None),
|
||||
)
|
||||
query = await apply_visibility_filter(
|
||||
db, query, "report_template", ReportTemplate, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
result = await db.execute(query)
|
||||
templates = result.scalars().all()
|
||||
return [_template_to_response(t).model_dump() for t in templates]
|
||||
|
||||
@@ -319,6 +324,7 @@ async def create_template(
|
||||
content=body.content,
|
||||
output_format=body.output_format,
|
||||
created_by=user_id,
|
||||
owner_id=user_id,
|
||||
)
|
||||
db.add(template)
|
||||
await db.flush()
|
||||
|
||||
@@ -9,9 +9,10 @@ from sqlalchemy.dialects.postgresql import UUID as PGUUID
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from app.core.db import Base, TenantMixin
|
||||
from app.models.owned_mixin import OwnedMixin
|
||||
|
||||
|
||||
class Tag(Base, TenantMixin):
|
||||
class Tag(Base, TenantMixin, OwnedMixin):
|
||||
"""Tag entity — globally managed, tenant-scoped."""
|
||||
|
||||
__tablename__ = "tags"
|
||||
|
||||
@@ -9,6 +9,7 @@ from sqlalchemy import delete, func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.tags.models import Tag, TagAssignment
|
||||
from app.plugins.builtins.tags.schemas import (
|
||||
@@ -40,6 +41,8 @@ async def list_tags(
|
||||
):
|
||||
"""List all tags with entity counts."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
|
||||
# Subquery for entity counts per tag
|
||||
count_sq = (
|
||||
@@ -52,12 +55,16 @@ async def list_tags(
|
||||
.subquery()
|
||||
)
|
||||
|
||||
result = await db.execute(
|
||||
query = (
|
||||
select(Tag, func.coalesce(count_sq.c.entity_count, 0))
|
||||
.outerjoin(count_sq, Tag.id == count_sq.c.tag_id)
|
||||
.where(Tag.tenant_id == tenant_id)
|
||||
.order_by(Tag.name)
|
||||
)
|
||||
query = await apply_visibility_filter(
|
||||
db, query, "tag", Tag, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
result = await db.execute(query)
|
||||
rows = result.all()
|
||||
|
||||
return [
|
||||
@@ -79,6 +86,7 @@ async def create_tag(
|
||||
):
|
||||
"""Create a new tag."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
|
||||
# Check name uniqueness within tenant
|
||||
existing = await db.execute(
|
||||
@@ -87,7 +95,7 @@ async def create_tag(
|
||||
if existing.scalar_one_or_none() is not None:
|
||||
raise HTTPException(409, detail={"detail": "Tag name already exists", "code": "duplicate"})
|
||||
|
||||
tag = Tag(tenant_id=tenant_id, name=body.name, color=body.color)
|
||||
tag = Tag(tenant_id=tenant_id, name=body.name, color=body.color, owner_id=user_id)
|
||||
db.add(tag)
|
||||
await db.flush()
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.db import get_db
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.deps import get_current_user, require_permission
|
||||
from app.plugins.builtins.tasks import services
|
||||
from app.plugins.builtins.tasks.schemas import (
|
||||
@@ -43,12 +44,15 @@ async def list_tasks(
|
||||
):
|
||||
"""List tasks with filtering and pagination."""
|
||||
tenant_id = uuid.UUID(current_user["tenant_id"])
|
||||
user_id = uuid.UUID(current_user["user_id"])
|
||||
is_system_admin = current_user.get("is_system_admin", False)
|
||||
return await services.list_tasks(
|
||||
db, tenant_id,
|
||||
page=page, page_size=page_size,
|
||||
status=status, priority=priority,
|
||||
assigned_to=assigned_to, contact_id=contact_id,
|
||||
search=search,
|
||||
user_id=user_id, is_system_admin=is_system_admin,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Any
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.visibility import apply_visibility_filter
|
||||
from app.plugins.builtins.tasks.models import Task
|
||||
|
||||
|
||||
@@ -40,10 +41,17 @@ async def list_tasks(
|
||||
assigned_to: str | None = None,
|
||||
contact_id: str | None = None,
|
||||
search: str | None = None,
|
||||
user_id: uuid.UUID | None = None,
|
||||
is_system_admin: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
"""List tasks with filtering and pagination."""
|
||||
query = select(Task).where(Task.tenant_id == tenant_id, Task.deleted_at.is_(None))
|
||||
|
||||
if user_id and not is_system_admin:
|
||||
query = await apply_visibility_filter(
|
||||
db, query, "task", Task, user_id, tenant_id, is_system_admin
|
||||
)
|
||||
|
||||
if status:
|
||||
query = query.where(Task.status == status)
|
||||
if priority:
|
||||
@@ -103,6 +111,7 @@ async def create_task(
|
||||
assigned_to=uuid.UUID(data["assigned_to"]) if data.get("assigned_to") else None,
|
||||
contact_id=uuid.UUID(data["contact_id"]) if data.get("contact_id") else None,
|
||||
created_by=user_id,
|
||||
owner_id=user_id,
|
||||
)
|
||||
db.add(task)
|
||||
await db.flush()
|
||||
|
||||
@@ -36,7 +36,6 @@ from app.models.sequence import Sequence
|
||||
from app.models.saved_filter import SavedFilter
|
||||
from app.models.saved_view import SavedView
|
||||
from app.models.webhook import Webhook
|
||||
from app.models.notification import Notification
|
||||
from app.models.custom_field_definition import CustomFieldDefinition
|
||||
from app.models.contact_folder import ContactFolder
|
||||
|
||||
@@ -56,20 +55,46 @@ ENTITY_MODELS: dict[str, type] = {
|
||||
"saved_filter": SavedFilter,
|
||||
"saved_view": SavedView,
|
||||
"webhook": Webhook,
|
||||
"notification": Notification,
|
||||
"custom_field_definition": CustomFieldDefinition,
|
||||
"contact_folder": ContactFolder,
|
||||
}
|
||||
|
||||
# Try to add plugin models if available
|
||||
# Core models with OwnedMixin (Phase 2 additions)
|
||||
try:
|
||||
from app.plugins.builtins.dms.models import DmsFile
|
||||
ENTITY_MODELS["dms_file"] = DmsFile
|
||||
from app.models.entity_attachment import EntityAttachment
|
||||
ENTITY_MODELS["entity_attachment"] = EntityAttachment
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from app.plugins.builtins.calendar.models import CalendarEvent
|
||||
ENTITY_MODELS["calendar_event"] = CalendarEvent
|
||||
from app.models.entity_history import EntityHistory
|
||||
ENTITY_MODELS["entity_history"] = EntityHistory
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# Plugin models if available
|
||||
try:
|
||||
from app.plugins.builtins.dms.models import File as DmsFile
|
||||
ENTITY_MODELS["file"] = DmsFile
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from app.plugins.builtins.dms.models import Folder as DmsFolder
|
||||
ENTITY_MODELS["folder"] = DmsFolder
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from app.plugins.builtins.calendar.models import CalendarEntry
|
||||
ENTITY_MODELS["calendar_event"] = CalendarEntry
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from app.plugins.builtins.calendar.models import Calendar
|
||||
ENTITY_MODELS["calendar"] = Calendar
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from app.plugins.builtins.calendar.models import Subtask
|
||||
ENTITY_MODELS["subtask"] = Subtask
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
@@ -78,8 +103,8 @@ try:
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from app.plugins.builtins.mail.models import Mailbox
|
||||
ENTITY_MODELS["mailbox"] = Mailbox
|
||||
from app.plugins.builtins.mail.models import MailAccount
|
||||
ENTITY_MODELS["mailbox"] = MailAccount
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
Reference in New Issue
Block a user