add organization cmd

pull/21891/head
ytqh 1 year ago
parent 0056e6566c
commit 29df704818

@ -726,3 +726,231 @@ def create_admin_with_phone(name: str, phone: str, tenant_id: Optional[str] = No
except Exception as e:
db.session.rollback()
click.echo(click.style(f"Error: {str(e)}", fg="red"))
@click.command("create-organization", help="Create a new organization for multi-school support.")
@click.option("--tenant-id", required=True, help="ID of the tenant that owns this organization")
@click.option("--name", required=True, help="Name of the organization")
@click.option("--code", required=True, help="Unique code for the organization")
@click.option(
"--type",
'org_type',
default="school",
type=click.Choice(["school", "university", "company", "organization"]),
help="Type of organization",
)
@click.option("--description", default="", help="Description of the organization")
@click.option("--email-domains", default="", help="Comma-separated list of allowed email domains")
@click.option("--created-by", required=True, help="Account ID of the creator")
def create_organization_cmd(tenant_id, name, code, org_type, description, email_domains, created_by):
"""Create a new organization under a tenant for multi-school support"""
try:
# Check if code already exists
from models.organization import Organization
existing = db.session.query(Organization).filter(Organization.code == code).first()
if existing:
click.echo(f"Error: Organization with code '{code}' already exists")
return
# Check if creator account exists
creator = db.session.query(Account).filter(Account.id == created_by).first()
if not creator:
click.echo(f"Error: Creator account with ID '{created_by}' not found")
return
# Parse email domains
allowed_domains = [d.strip() for d in email_domains.split(',') if d.strip()]
# Create settings
settings = {'allowed_email_domains': allowed_domains}
# Create organization
organization = Organization(
tenant_id=tenant_id,
name=name,
code=code,
type=org_type,
description=description,
settings=json.dumps(settings),
status="active",
created_by=created_by,
)
db.session.add(organization)
db.session.commit()
click.echo(f"Organization '{name}' (ID: {organization.id}) created successfully")
except Exception as e:
db.session.rollback()
click.echo(f"Error creating organization: {str(e)}")
@click.command("update-organization", help="Update an existing organization.")
@click.option("--id", 'org_id', required=True, help="ID of the organization to update")
@click.option("--name", help="New name for the organization")
@click.option("--description", help="New description")
@click.option("--email-domains", help="Comma-separated list of allowed email domains")
@click.option("--status", type=click.Choice(["active", "inactive"]), help="Organization status")
def update_organization_cmd(org_id, name, description, email_domains, status):
"""Update an existing organization's configuration"""
try:
from models.organization import Organization
organization = db.session.query(Organization).filter(Organization.id == org_id).first()
if not organization:
click.echo(f"Error: Organization with ID '{org_id}' not found")
return
if name:
organization.name = name
if description:
organization.description = description
if status:
organization.status = status
if email_domains is not None:
settings = organization.settings_dict
allowed_domains = [d.strip() for d in email_domains.split(',') if d.strip()]
settings['allowed_email_domains'] = allowed_domains
organization.settings_dict = settings
db.session.commit()
click.echo(f"Organization '{organization.name}' updated successfully")
except Exception as e:
db.session.rollback()
click.echo(f"Error updating organization: {str(e)}")
@click.command("list-organizations", help="List all organizations.")
@click.option("--tenant-id", help="Filter by tenant ID")
def list_organizations_cmd(tenant_id):
"""List all organizations with optional tenant filtering"""
try:
from models.organization import Organization
query = db.session.query(Organization)
if tenant_id:
query = query.filter(Organization.tenant_id == tenant_id)
organizations = query.all()
if not organizations:
click.echo("No organizations found")
return
click.echo(f"{'ID':<36} | {'Code':<10} | {'Name':<30} | {'Type':<12} | {'Status':<8} | {'Email Domains'}")
click.echo("-" * 120)
for org in organizations:
email_domains = ', '.join(org.allowed_email_domains)
click.echo(
f"{org.id:<36} | {org.code:<10} | {org.name:<30} | {org.type:<12} | {org.status:<8} | {email_domains}"
)
except Exception as e:
click.echo(f"Error listing organizations: {str(e)}")
@click.command("show-organization", help="Show details of a specific organization.")
@click.option("--id", 'org_id', required=True, help="ID of the organization to show")
def show_organization_cmd(org_id):
"""Show detailed information about a specific organization"""
try:
from models.organization import Organization
organization = db.session.query(Organization).filter(Organization.id == org_id).first()
if not organization:
click.echo(f"Error: Organization with ID '{org_id}' not found")
return
click.echo(f"ID: {organization.id}")
click.echo(f"Tenant ID: {organization.tenant_id}")
click.echo(f"Name: {organization.name}")
click.echo(f"Code: {organization.code}")
click.echo(f"Type: {organization.type}")
click.echo(f"Description: {organization.description or ''}")
click.echo(f"Status: {organization.status}")
click.echo(f"Email Domains: {', '.join(organization.allowed_email_domains)}")
click.echo(f"Created At: {organization.created_at}")
click.echo(f"Updated At: {organization.updated_at}")
except Exception as e:
click.echo(f"Error showing organization: {str(e)}")
@click.command("add-account-to-organization", help="Add an account to an organization with a specific role.")
@click.option("--org-id", required=True, help="ID of the organization")
@click.option("--account-id", required=True, help="ID of the account to add")
@click.option(
"--role",
required=True,
type=click.Choice(["admin", "teacher", "student", "staff", "manager", "employee", "guest"]),
help="Role in the organization",
)
@click.option("--department", help="Department within the organization")
@click.option("--title", help="Job title or position")
@click.option("--is-default", is_flag=True, help="Set as the account's default organization")
def add_account_to_organization_cmd(org_id, account_id, role, department, title, is_default):
"""Add an account to an organization with appropriate role and metadata"""
try:
from models.organization import Organization, OrganizationMember
# Check if organization exists
organization = db.session.query(Organization).filter(Organization.id == org_id).first()
if not organization:
click.echo(f"Error: Organization with ID '{org_id}' not found")
return
# Check if account exists
account = db.session.query(Account).filter(Account.id == account_id).first()
if not account:
click.echo(f"Error: Account with ID '{account_id}' not found")
return
# Check if membership already exists
existing = (
db.session.query(OrganizationMember)
.filter(OrganizationMember.organization_id == org_id, OrganizationMember.account_id == account_id)
.first()
)
if existing:
click.echo(f"Account is already a member of this organization. Updating role and metadata.")
existing.role = role
existing.department = department
existing.title = title
existing.is_default = is_default
else:
# Create new membership with meta_data instead of metadata (reserved word)
member = OrganizationMember(
organization_id=org_id,
account_id=account_id,
role=role,
department=department,
title=title,
is_default=is_default,
created_by=account_id,
# Use meta_data instead of metadata as it's a reserved word in SQLAlchemy
meta_data=json.dumps({}),
)
db.session.add(member)
# If set as default, update the account's current_organization_id
if is_default:
account.current_organization_id = org_id
db.session.commit()
click.echo(
f"Account successfully {'added to' if not existing else 'updated in'} organization with role '{role}'"
)
except Exception as e:
db.session.rollback()
click.echo(f"Error adding account to organization: {str(e)}")

@ -3,14 +3,19 @@ from dify_app import DifyApp
def init_app(app: DifyApp):
from commands import (
add_account_to_organization_cmd,
add_qdrant_doc_id_index,
convert_to_agent_apps,
create_admin_with_phone,
create_organization_cmd,
create_tenant,
fix_app_site_missing,
list_organizations_cmd,
reset_email,
reset_encrypt_key_pair,
reset_password,
show_organization_cmd,
update_organization_cmd,
upgrade_db,
vdb_migrate,
)
@ -26,6 +31,11 @@ def init_app(app: DifyApp):
upgrade_db,
fix_app_site_missing,
create_admin_with_phone,
create_organization_cmd,
add_account_to_organization_cmd,
list_organizations_cmd,
show_organization_cmd,
update_organization_cmd,
]
for cmd in cmds_to_register:
app.cli.add_command(cmd)

@ -0,0 +1,96 @@
"""register organiztion
Revision ID: 4b37d4034604
Revises: 18dd49e03533
Create Date: 2025-03-24 20:33:14.746272
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '4b37d4034604'
down_revision = '18dd49e03533'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('app_organization_access',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('app_id', models.types.StringUUID(), nullable=False),
sa.Column('organization_id', models.types.StringUUID(), nullable=False),
sa.Column('permissions', sa.Text(), nullable=True),
sa.Column('created_by', models.types.StringUUID(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id', name='app_organization_access_pkey'),
sa.UniqueConstraint('app_id', 'organization_id', name='unique_app_organization')
)
with op.batch_alter_table('app_organization_access', schema=None) as batch_op:
batch_op.create_index('app_org_access_app_idx', ['app_id'], unique=False)
batch_op.create_index('app_org_access_org_idx', ['organization_id'], unique=False)
op.create_table('organization_members',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('organization_id', models.types.StringUUID(), nullable=False),
sa.Column('account_id', models.types.StringUUID(), nullable=False),
sa.Column('role', sa.String(length=64), nullable=False),
sa.Column('department', sa.String(length=255), nullable=True),
sa.Column('title', sa.String(length=255), nullable=True),
sa.Column('is_default', sa.Boolean(), server_default=sa.text('false'), nullable=False),
sa.Column('meta_data', sa.Text(), nullable=True),
sa.Column('created_by', models.types.StringUUID(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id', name='organization_member_pkey'),
sa.UniqueConstraint('organization_id', 'account_id', name='unique_org_account')
)
with op.batch_alter_table('organization_members', schema=None) as batch_op:
batch_op.create_index('org_member_account_idx', ['account_id'], unique=False)
batch_op.create_index('org_member_org_idx', ['organization_id'], unique=False)
op.create_table('organizations',
sa.Column('id', models.types.StringUUID(), server_default=sa.text('uuid_generate_v4()'), nullable=False),
sa.Column('tenant_id', models.types.StringUUID(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('code', sa.String(length=64), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('type', sa.String(length=64), nullable=False),
sa.Column('logo', sa.String(length=255), nullable=True),
sa.Column('settings', sa.Text(), nullable=True),
sa.Column('status', sa.String(length=16), server_default=sa.text("'active'::character varying"), nullable=False),
sa.Column('created_by', models.types.StringUUID(), nullable=False),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
sa.PrimaryKeyConstraint('id', name='organization_pkey'),
sa.UniqueConstraint('code', name=op.f('organizations_code_key'))
)
with op.batch_alter_table('organizations', schema=None) as batch_op:
batch_op.create_index('organization_code_idx', ['code'], unique=False)
batch_op.create_index('organization_tenant_id_idx', ['tenant_id'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('organizations', schema=None) as batch_op:
batch_op.drop_index('organization_tenant_id_idx')
batch_op.drop_index('organization_code_idx')
op.drop_table('organizations')
with op.batch_alter_table('organization_members', schema=None) as batch_op:
batch_op.drop_index('org_member_org_idx')
batch_op.drop_index('org_member_account_idx')
op.drop_table('organization_members')
with op.batch_alter_table('app_organization_access', schema=None) as batch_op:
batch_op.drop_index('app_org_access_org_idx')
batch_op.drop_index('app_org_access_app_idx')
op.drop_table('app_organization_access')
# ### end Alembic commands ###

@ -57,6 +57,7 @@ from .model import (
TraceAppConfig,
UploadFile,
)
from .organization import AppOrganizationAccess, Organization, OrganizationMember
from .provider import (
LoadBalancingModelConfig,
Provider,
@ -183,5 +184,8 @@ __all__ = [
"WorkflowRunTriggeredFrom",
"WorkflowToolProvider",
"WorkflowType",
"Organization",
"OrganizationMember",
"AppOrganizationAccess",
"db",
]

@ -140,7 +140,7 @@ class OrganizationMember(db.Model): # type: ignore[name-defined]
department: Mapped[str] = mapped_column(db.String(255), nullable=True)
title: Mapped[str] = mapped_column(db.String(255), nullable=True)
is_default: Mapped[bool] = mapped_column(db.Boolean, nullable=False, server_default=db.text("false"))
metadata: Mapped[str] = mapped_column(db.Text, nullable=True) # Additional metadata as JSON
meta_data: Mapped[str] = mapped_column(db.Text, nullable=True) # Additional metadata as JSON
created_by: Mapped[str] = mapped_column(StringUUID, nullable=False)
created_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
updated_at: Mapped[datetime] = mapped_column(db.DateTime, nullable=False, server_default=func.current_timestamp())
@ -148,12 +148,12 @@ class OrganizationMember(db.Model): # type: ignore[name-defined]
@property
def metadata_dict(self) -> dict:
"""Get member metadata as a dictionary"""
return json.loads(self.metadata) if self.metadata else {}
return json.loads(self.meta_data) if self.meta_data else {}
@metadata_dict.setter
def metadata_dict(self, value: dict):
"""Set member metadata from a dictionary"""
self.metadata = json.dumps(value)
self.meta_data = json.dumps(value)
class AppOrganizationAccess(db.Model): # type: ignore[name-defined]

Loading…
Cancel
Save