mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
chore: Site to typebase (#35632)
This commit is contained in:
parent
f28d993607
commit
8129555642
@ -15,7 +15,7 @@ import sqlalchemy as sa
|
||||
from flask import request
|
||||
from flask_login import UserMixin # type: ignore[import-untyped]
|
||||
from sqlalchemy import BigInteger, Float, Index, PrimaryKeyConstraint, String, exists, func, select, text
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column
|
||||
from sqlalchemy.orm import Mapped, Session, mapped_column, validates
|
||||
|
||||
from configs import dify_config
|
||||
from constants import DEFAULT_FILE_NUMBER_LIMITS
|
||||
@ -2316,7 +2316,15 @@ class AppMCPServer(TypeBase):
|
||||
return cast(dict[str, str], json.loads(self.parameters))
|
||||
|
||||
|
||||
class Site(Base):
|
||||
class Site(TypeBase):
|
||||
"""Public site configuration backed by the nullable legacy ``sites`` schema.
|
||||
|
||||
Only the app, title, language, and token strategy are required at
|
||||
construction time. Nullable database columns keep ``None`` defaults so
|
||||
converting this model to ``TypeBase`` does not make legacy call sites pass
|
||||
values that the database has never required.
|
||||
"""
|
||||
|
||||
__tablename__ = "sites"
|
||||
__table_args__ = (
|
||||
sa.PrimaryKeyConstraint("id", name="site_pkey"),
|
||||
@ -2324,47 +2332,62 @@ class Site(Base):
|
||||
sa.Index("site_code_idx", "code", "status"),
|
||||
)
|
||||
|
||||
id = mapped_column(StringUUID, default=lambda: str(uuid4()))
|
||||
app_id = mapped_column(StringUUID, nullable=False)
|
||||
app_id: Mapped[str] = mapped_column(StringUUID, nullable=False)
|
||||
title: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
icon_type: Mapped[IconType | None] = mapped_column(EnumText(IconType, length=255), nullable=True)
|
||||
icon: Mapped[str | None] = mapped_column(String(255))
|
||||
icon_background = mapped_column(String(255))
|
||||
description = mapped_column(LongText)
|
||||
default_language: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
chat_color_theme = mapped_column(String(255))
|
||||
chat_color_theme_inverted: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
|
||||
copyright = mapped_column(String(255))
|
||||
privacy_policy = mapped_column(String(255))
|
||||
input_placeholder = mapped_column(String(255))
|
||||
show_workflow_steps: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("true"))
|
||||
use_icon_as_answer_icon: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
|
||||
_custom_disclaimer: Mapped[str] = mapped_column("custom_disclaimer", LongText, default="")
|
||||
customize_domain = mapped_column(String(255))
|
||||
customize_token_strategy: Mapped[CustomizeTokenStrategy] = mapped_column(
|
||||
EnumText(CustomizeTokenStrategy, length=255), nullable=False
|
||||
)
|
||||
prompt_public: Mapped[bool] = mapped_column(sa.Boolean, nullable=False, server_default=sa.text("false"))
|
||||
|
||||
id: Mapped[str] = mapped_column(
|
||||
StringUUID, insert_default=lambda: str(uuid4()), default_factory=lambda: str(uuid4())
|
||||
)
|
||||
icon_type: Mapped[IconType | None] = mapped_column(EnumText(IconType, length=255), nullable=True, default=None)
|
||||
icon: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
||||
icon_background: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
||||
description: Mapped[str | None] = mapped_column(LongText, nullable=True, default=None)
|
||||
copyright: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
||||
privacy_policy: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
||||
input_placeholder: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
||||
created_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None)
|
||||
updated_by: Mapped[str | None] = mapped_column(StringUUID, nullable=True, default=None)
|
||||
code: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
sa.DateTime, nullable=False, server_default=func.current_timestamp(), init=False
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
sa.DateTime,
|
||||
nullable=False,
|
||||
server_default=func.current_timestamp(),
|
||||
onupdate=func.current_timestamp(),
|
||||
init=False,
|
||||
)
|
||||
|
||||
customize_domain: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
||||
chat_color_theme: Mapped[str | None] = mapped_column(String(255), nullable=True, default=None)
|
||||
prompt_public: Mapped[bool] = mapped_column(
|
||||
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
|
||||
)
|
||||
chat_color_theme_inverted: Mapped[bool] = mapped_column(
|
||||
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
|
||||
)
|
||||
show_workflow_steps: Mapped[bool] = mapped_column(
|
||||
sa.Boolean, nullable=False, server_default=sa.text("true"), default=True
|
||||
)
|
||||
use_icon_as_answer_icon: Mapped[bool] = mapped_column(
|
||||
sa.Boolean, nullable=False, server_default=sa.text("false"), default=False
|
||||
)
|
||||
custom_disclaimer: Mapped[str] = mapped_column(LongText, nullable=False, default="")
|
||||
status: Mapped[AppStatus] = mapped_column(
|
||||
EnumText(AppStatus, length=255), nullable=False, server_default=sa.text("'normal'"), default=AppStatus.NORMAL
|
||||
)
|
||||
created_by = mapped_column(StringUUID, nullable=True)
|
||||
created_at = mapped_column(sa.DateTime, nullable=False, server_default=func.current_timestamp())
|
||||
updated_by = mapped_column(StringUUID, nullable=True)
|
||||
updated_at = mapped_column(
|
||||
sa.DateTime, nullable=False, server_default=func.current_timestamp(), onupdate=func.current_timestamp()
|
||||
)
|
||||
code = mapped_column(String(255))
|
||||
|
||||
@property
|
||||
def custom_disclaimer(self):
|
||||
return self._custom_disclaimer
|
||||
|
||||
@custom_disclaimer.setter
|
||||
def custom_disclaimer(self, value: str):
|
||||
@validates("custom_disclaimer")
|
||||
def _validate_custom_disclaimer(self, _key: str, value: str) -> str:
|
||||
"""Reject disclaimers that exceed the public site API's 512-character limit."""
|
||||
if len(value) > 512:
|
||||
raise ValueError("Custom disclaimer cannot exceed 512 characters.")
|
||||
self._custom_disclaimer = value
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def generate_code(n: int, *, session: Session) -> str:
|
||||
|
||||
@ -149,9 +149,9 @@ class DatabaseRecommendedAppCatalogRepository(RecommendedAppCatalogQuery):
|
||||
icon_background=app.icon_background,
|
||||
),
|
||||
app_id=recommended_app.app_id,
|
||||
description=cast(str | None, site.description),
|
||||
copyright=cast(str | None, site.copyright),
|
||||
privacy_policy=cast(str | None, site.privacy_policy),
|
||||
description=site.description,
|
||||
copyright=site.copyright,
|
||||
privacy_policy=site.privacy_policy,
|
||||
custom_disclaimer=cast(str | None, site.custom_disclaimer),
|
||||
categories=app_categories,
|
||||
position=recommended_app.position,
|
||||
|
||||
@ -34,7 +34,7 @@ from models.human_input import (
|
||||
RecipientType,
|
||||
StandaloneWebAppRecipientPayload,
|
||||
)
|
||||
from models.model import App, AppMode, CustomizeTokenStrategy, Site
|
||||
from models.model import App, AppMode, CustomizeTokenStrategy, IconType, Site
|
||||
from models.workflow import WorkflowRun, WorkflowType
|
||||
from repositories.sqlalchemy_api_workflow_run_repository import DifyAPISQLAlchemyWorkflowRunRepository
|
||||
from services.entities.feature_entities import FeatureModel
|
||||
@ -68,7 +68,7 @@ def _create_app_with_site(session: Session) -> tuple[App, Account]:
|
||||
name="Test App",
|
||||
description="",
|
||||
mode=AppMode.WORKFLOW,
|
||||
icon_type="emoji",
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="app",
|
||||
icon_background="#ffffff",
|
||||
enable_site=True,
|
||||
@ -82,7 +82,7 @@ def _create_app_with_site(session: Session) -> tuple[App, Account]:
|
||||
site = Site(
|
||||
app_id=app.id,
|
||||
title="Test Site",
|
||||
icon_type="emoji",
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="robot",
|
||||
icon_background="#ffffff",
|
||||
description="desc",
|
||||
|
||||
@ -12,7 +12,7 @@ import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from graphon.enums import WorkflowExecutionStatus
|
||||
from models.enums import ConversationFromSource, InvokeFrom
|
||||
from models.enums import ConversationFromSource, CustomizeTokenStrategy, InvokeFrom
|
||||
from models.model import App, AppMode, Conversation, Message, Site
|
||||
from models.workflow import Workflow, WorkflowRun, WorkflowRunTriggeredFrom, WorkflowType
|
||||
|
||||
@ -300,7 +300,7 @@ class TestSiteGenerateCode:
|
||||
app_id=app.id,
|
||||
title="Test Site",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="not_allow",
|
||||
customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW,
|
||||
)
|
||||
# Set an explicit code so generate_code must avoid it
|
||||
site.code = "AAAAAAAA"
|
||||
|
||||
@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
from sqlalchemy.orm import Session, object_session, sessionmaker
|
||||
|
||||
from extensions.ext_redis import RedisClientWrapper
|
||||
from models.enums import CustomizeTokenStrategy
|
||||
from models.model import App, RecommendedApp, Site
|
||||
from repositories.recommended_app_catalog_repository import DatabaseRecommendedAppCatalogRepository
|
||||
from services.recommended_app_query_service import RecommendedAppDetailRecord
|
||||
@ -46,7 +47,7 @@ def _add_catalog_app(
|
||||
app_id=app.id,
|
||||
title=f"site-{uuid4()}",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="not_allow",
|
||||
customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW,
|
||||
description="description",
|
||||
copyright="copyright",
|
||||
privacy_policy="privacy",
|
||||
|
||||
@ -1347,13 +1347,14 @@ class TestAppService:
|
||||
app = app_service.create_app(tenant.id, app_args, account, session=db_session_with_containers)
|
||||
|
||||
# Create a site for the app
|
||||
site = Site()
|
||||
site.app_id = app.id
|
||||
site.code = fake.postalcode()
|
||||
site.title = fake.company()
|
||||
site.status = AppStatus.NORMAL
|
||||
site.default_language = "en-US"
|
||||
site.customize_token_strategy = CustomizeTokenStrategy.UUID
|
||||
site = Site(
|
||||
app_id=app.id,
|
||||
code=fake.postalcode(),
|
||||
title=fake.company(),
|
||||
status=AppStatus.NORMAL,
|
||||
default_language="en-US",
|
||||
customize_token_strategy=CustomizeTokenStrategy.UUID,
|
||||
)
|
||||
|
||||
db_session_with_containers.add(site)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
@ -8,7 +8,8 @@ from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import NotFound, Unauthorized
|
||||
|
||||
from libs.password import hash_password
|
||||
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole
|
||||
from models import Account, AccountStatus, Tenant, TenantAccountJoin, TenantAccountRole, TenantStatus
|
||||
from models.enums import AppStatus, CustomizeTokenStrategy
|
||||
from models.model import App, Site
|
||||
from services.errors.account import AccountLoginError, AccountNotFoundError, AccountPasswordError
|
||||
from services.webapp_auth_service import WebAppAuthService
|
||||
@ -67,7 +68,7 @@ class TestWebAppAuthService:
|
||||
email=unique_email,
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
status="active",
|
||||
status=AccountStatus.ACTIVE,
|
||||
)
|
||||
|
||||
db_session_with_containers.add(account)
|
||||
@ -76,7 +77,7 @@ class TestWebAppAuthService:
|
||||
# Create tenant for the account
|
||||
tenant = Tenant(
|
||||
name=fake.company(),
|
||||
status="normal",
|
||||
status=TenantStatus.NORMAL,
|
||||
)
|
||||
db_session_with_containers.add(tenant)
|
||||
db_session_with_containers.commit()
|
||||
@ -120,7 +121,7 @@ class TestWebAppAuthService:
|
||||
email=unique_email,
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
status="active",
|
||||
status=AccountStatus.ACTIVE,
|
||||
)
|
||||
|
||||
# Hash password
|
||||
@ -139,7 +140,7 @@ class TestWebAppAuthService:
|
||||
# Create tenant for the account
|
||||
tenant = Tenant(
|
||||
name=fake.company(),
|
||||
status="normal",
|
||||
status=TenantStatus.NORMAL,
|
||||
)
|
||||
db_session_with_containers.add(tenant)
|
||||
db_session_with_containers.commit()
|
||||
@ -200,8 +201,8 @@ class TestWebAppAuthService:
|
||||
code=fake.unique.lexify(text="??????"),
|
||||
description=fake.text(max_nb_chars=100),
|
||||
default_language="en-US",
|
||||
status="normal",
|
||||
customize_token_strategy="not_allow",
|
||||
status=AppStatus.NORMAL,
|
||||
customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW,
|
||||
)
|
||||
db_session_with_containers.add(site)
|
||||
db_session_with_containers.commit()
|
||||
@ -342,7 +343,7 @@ class TestWebAppAuthService:
|
||||
email=unique_email,
|
||||
name=fake.name(),
|
||||
interface_language="en-US",
|
||||
status="active",
|
||||
status=AccountStatus.ACTIVE,
|
||||
)
|
||||
|
||||
db_session_with_containers.add(account)
|
||||
@ -724,8 +725,8 @@ class TestWebAppAuthService:
|
||||
code=fake.unique.lexify(text="??????"),
|
||||
description=fake.text(max_nb_chars=100),
|
||||
default_language="en-US",
|
||||
status="normal",
|
||||
customize_token_strategy="not_allow",
|
||||
status=AppStatus.NORMAL,
|
||||
customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW,
|
||||
)
|
||||
db_session_with_containers.add(site)
|
||||
db_session_with_containers.commit()
|
||||
|
||||
@ -12,14 +12,15 @@ import json
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
from unittest.mock import PropertyMock, patch
|
||||
from uuid import uuid4
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session, scoped_session
|
||||
|
||||
from models import model as model_module
|
||||
from models.dataset import DatasetCollectionBinding
|
||||
from models.enums import CollectionBindingType, ConversationFromSource
|
||||
from models.enums import CollectionBindingType, ConversationFromSource, CustomizeTokenStrategy
|
||||
from models.model import (
|
||||
App,
|
||||
AppAnnotationHitHistory,
|
||||
@ -1123,6 +1124,28 @@ class TestAppAnnotationHitHistory:
|
||||
class TestSiteModel:
|
||||
"""Test suite for Site model."""
|
||||
|
||||
def test_site_core_bulk_insert_generates_ids(self, sqlite_session: Session):
|
||||
"""Core bulk inserts generate an ID for every site row."""
|
||||
app_ids = [str(uuid4()), str(uuid4())]
|
||||
|
||||
sqlite_session.execute(
|
||||
Site.__table__.insert(),
|
||||
[
|
||||
{
|
||||
"app_id": app_id,
|
||||
"title": f"Site {index}",
|
||||
"default_language": "en-US",
|
||||
"customize_token_strategy": CustomizeTokenStrategy.UUID,
|
||||
}
|
||||
for index, app_id in enumerate(app_ids)
|
||||
],
|
||||
)
|
||||
|
||||
site_ids = sqlite_session.scalars(select(Site.id).where(Site.app_id.in_(app_ids))).all()
|
||||
assert len(site_ids) == len(app_ids)
|
||||
assert len(set(site_ids)) == len(app_ids)
|
||||
assert all(UUID(site_id).version == 4 for site_id in site_ids)
|
||||
|
||||
def test_site_creation_with_required_fields(self):
|
||||
"""Test creating a site with required fields."""
|
||||
# Arrange
|
||||
@ -1133,14 +1156,14 @@ class TestSiteModel:
|
||||
app_id=app_id,
|
||||
title="Test Site",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="uuid",
|
||||
customize_token_strategy=CustomizeTokenStrategy.UUID,
|
||||
)
|
||||
|
||||
# Assert
|
||||
assert site.app_id == app_id
|
||||
assert site.title == "Test Site"
|
||||
assert site.default_language == "en-US"
|
||||
assert site.customize_token_strategy == "uuid"
|
||||
assert site.customize_token_strategy == CustomizeTokenStrategy.UUID
|
||||
|
||||
def test_site_creation_with_optional_fields(self):
|
||||
"""Test creating a site with optional fields."""
|
||||
@ -1149,7 +1172,7 @@ class TestSiteModel:
|
||||
app_id=str(uuid4()),
|
||||
title="Test Site",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="uuid",
|
||||
customize_token_strategy=CustomizeTokenStrategy.UUID,
|
||||
icon_type=IconType.EMOJI,
|
||||
icon="🌐",
|
||||
icon_background="#0066CC",
|
||||
@ -1173,7 +1196,7 @@ class TestSiteModel:
|
||||
app_id=str(uuid4()),
|
||||
title="Test Site",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="uuid",
|
||||
customize_token_strategy=CustomizeTokenStrategy.UUID,
|
||||
)
|
||||
|
||||
# Act
|
||||
@ -1189,7 +1212,7 @@ class TestSiteModel:
|
||||
app_id=str(uuid4()),
|
||||
title="Test Site",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="uuid",
|
||||
customize_token_strategy=CustomizeTokenStrategy.UUID,
|
||||
)
|
||||
long_disclaimer = "x" * 513 # Exceeds 512 character limit
|
||||
|
||||
@ -1380,7 +1403,7 @@ class TestModelIntegration:
|
||||
app_id=app_id,
|
||||
title="Test Site",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="uuid",
|
||||
customize_token_strategy=CustomizeTokenStrategy.UUID,
|
||||
)
|
||||
|
||||
# Assert
|
||||
|
||||
@ -7,6 +7,7 @@ from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from extensions.ext_redis import RedisClientWrapper
|
||||
from models.enums import CustomizeTokenStrategy
|
||||
from models.model import App, AppMode, RecommendedApp, Site
|
||||
from repositories.recommended_app_catalog_repository import DatabaseRecommendedAppCatalogRepository
|
||||
from services.recommended_app_query_service import RecommendedAppDetailRecord
|
||||
@ -58,7 +59,7 @@ def _add_catalog_app(
|
||||
privacy_policy="site privacy",
|
||||
custom_disclaimer="site disclaimer",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="not_allow",
|
||||
customize_token_strategy=CustomizeTokenStrategy.NOT_ALLOW,
|
||||
)
|
||||
)
|
||||
session.commit()
|
||||
|
||||
@ -4,6 +4,7 @@ import pytest
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from models.enums import CustomizeTokenStrategy
|
||||
from models.model import Site
|
||||
from repositories.webapp_access_query_repository import WebAppAccessQueryRepository
|
||||
from services.webapp_access_query_service import WebAppAccessUnavailableError
|
||||
@ -19,7 +20,7 @@ def test_find_app_id_by_code_returns_matching_site_app(sqlite_session_factory: s
|
||||
code="site-code",
|
||||
title="Test Site",
|
||||
default_language="en-US",
|
||||
customize_token_strategy="uuid",
|
||||
customize_token_strategy=CustomizeTokenStrategy.UUID,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user