chore: Site to typebase (#35632)

This commit is contained in:
Asuka Minato 2026-09-02 14:44:08 +00:00 committed by GitHub
parent f28d993607
commit 8129555642
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 119 additions and 68 deletions

View File

@ -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:

View File

@ -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,

View File

@ -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",

View File

@ -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"

View File

@ -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",

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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,
)
)