🐛 fix(demo): покрыть новые модели веб-чата

This commit is contained in:
Andrey committed 2026-10-01 19:23:13 +03:00
1 parent 0a8514fe55
commit 34afd9569f
6 files changed
+135 -1

No files matched your search

@@ -0,0 +1,38 @@
"""Поля сайта и загруженные изображения из существующих данных веб-гостей."""
from io import BytesIO
from django.core.files.uploadedfile import UploadedFile
from chatballs.conversations.models import ContactFieldValue
from chatballs.identity.demo_seed import manifest
from chatballs.identity.demo_seed.refs import DemoRefs
from chatballs.tenancy.context import TenantContext
from chatballs.webchat.assets import upload_widget_asset
def load(context: TenantContext, refs: DemoRefs) -> None:
data = manifest.load("conversations", refs.language)
for item in data["conversations"]:
if not item.get("webGuest") or not item.get("guestName"):
continue
conversation = refs.conversations[item["key"]]
ContactFieldValue.objects.get_or_create(
organization=refs.organization,
contact=conversation.contact,
integration=conversation.connection,
key="name",
defaults={"value": item["guestName"]},
)
avatar = item.get("guestAvatar")
if avatar and not conversation.connection.widget_assets.exists():
payload = manifest.media_bytes(f"avatars/{avatar}")
upload_widget_asset(
context=context,
integration=conversation.connection,
upload=UploadedFile(
file=BytesIO(payload), name=avatar, size=len(payload),
),
)
# Изображение доступно в хранилище, но не назначается кнопке или
# шапке: оформление демо-виджета остаётся по дизайн-базлайну.
@@ -24,6 +24,9 @@ from chatballs.identity.demo_seed.loaders import (
from chatballs.identity.demo_seed.loaders import (
support as support_loader,
)
from chatballs.identity.demo_seed.loaders import (
webchat as webchat_loader,
)
from chatballs.identity.demo_seed.refs import DemoRefs
from chatballs.tenancy.context import TenantContext
@@ -52,5 +55,6 @@ def run_demo_seed(
# Поддержка раньше диалогов: статьи портала привязываются к агентам.
support_loader.load(context, refs)
conversations_loader.load(context, refs)
webchat_loader.load(context, refs)
operations_loader.load(context, refs)
return refs
@@ -24,6 +24,9 @@ from django.db.models.deletion import ProtectedError
from django.db.models.signals import post_save
from chatballs.identity.demo_models import DemoDataset, DemoRecord
from chatballs.tenancy.context import TenantContext
from chatballs.webchat.assets import discard_widget_asset_files
from chatballs.webchat.models import WidgetAsset
logger = logging.getLogger(__name__)
@@ -81,6 +84,12 @@ def recording(dataset: DemoDataset) -> Iterator[DemoRecorder]:
def _delete_files(instance: models.Model) -> None:
if isinstance(instance, WidgetAsset):
discard_widget_asset_files(
context=TenantContext.for_resource(instance.organization),
files=[(instance.file.name, instance.size)] if instance.file.name else [],
)
return
for field in instance._meta.get_fields():
if isinstance(field, models.FileField):
file = getattr(instance, field.name, None)
@@ -13,6 +13,7 @@ from django.test import TestCase, override_settings
from chatballs.ai.models import AIAgent, AIAgentStatus, Knowledge, KnowledgeAttachment
from chatballs.conversations.models import (
Contact,
ContactFieldValue,
Conversation,
LifecycleState,
Message,
@@ -72,12 +73,12 @@ def _counts() -> dict[str, int]:
return {m._meta.label: m._base_manager.count() for m in _covered_models()}
@override_settings(MEDIA_ROOT=_MEDIA_ROOT)
def maria_contact_fields(organization) -> tuple[str, str, str]:
contact = Contact.objects.get(organization=organization, name="Мария Соколова")
return contact.description, contact.company, contact.city
@override_settings(MEDIA_ROOT=_MEDIA_ROOT)
class DemoDatasetTests(TestCase):
def setUp(self) -> None:
result = complete_setup(SetupInput(**OWNER))
@@ -279,6 +280,10 @@ class EnglishDemoDatasetTests(TestCase):
self.assertTrue(
AIAgent.objects.filter(organization=organization, name="Consultant").exists()
)
self.assertEqual(
ContactFieldValue.objects.get(organization=organization, key="name").value,
"Dmitry Orlov",
)
# Ни одной кириллической буквы во всём, что увидит человек на экране.
texts = [
*Message.objects.filter(organization=organization).values_list("text", flat=True),
@@ -0,0 +1,75 @@
"""Новые модели веб-чата в демонаборе: реестр, повторный запуск и удаление."""
from tempfile import TemporaryDirectory
from django.test import TestCase, override_settings
from chatballs.conversations.models import ContactFieldValue
from chatballs.identity.demo_models import DemoDataset, DemoDatasetStatus
from chatballs.identity.demo_seed import manifest, service
from chatballs.identity.demo_seed.loaders import webchat
from chatballs.identity.demo_seed.refs import DemoRefs
from chatballs.identity.demo_seed.registry import _delete_record
from chatballs.identity.setup import SetupInput, complete_setup
from chatballs.identity.test_seed_demo import OWNER
from chatballs.tenancy.context import TenantContext
from chatballs.tenancy.database import tenant_atomic
from chatballs.tenancy.models import OrganizationStorageUsage
from chatballs.webchat.appearance import DEFAULTS
from chatballs.webchat.models import WidgetAsset
class DemoWebChatTests(TestCase):
def setUp(self) -> None:
media = TemporaryDirectory()
self.addCleanup(media.cleanup)
override = override_settings(MEDIA_ROOT=media.name)
override.enable()
self.addCleanup(override.disable)
result = complete_setup(SetupInput(**OWNER))
self.context = TenantContext.for_resource(result.organization)
with tenant_atomic(self.context):
self.dataset = DemoDataset.objects.create(
organization=result.organization, status=DemoDatasetStatus.INSTALLING,
)
self.dataset = service.install(context=self.context, dataset=self.dataset)
self.assertEqual(self.dataset.status, DemoDatasetStatus.INSTALLED, self.dataset.error)
def test_web_guest_data_is_registered_without_changing_widget_appearance(self) -> None:
field = ContactFieldValue.objects.get(key="name")
self.assertEqual(field.value, field.contact.name)
asset = WidgetAsset.objects.get(integration=field.integration)
with tenant_atomic(self.context):
self.assertTrue(asset.file.storage.exists(asset.file.name))
for instance in (field, asset):
self.assertTrue(self.dataset.records.filter(
content_type__model=instance._meta.model_name,
object_id=str(instance.pk),
).exists())
self.assertEqual(field.integration.config["appearance"], DEFAULTS)
# Ключ диалога берётся из манифеста, как при штатной установке.
item = next(
item for item in manifest.load("conversations")["conversations"]
if item.get("guestName") == field.value
)
refs = DemoRefs(
organization=self.context.organization,
conversations={item["key"]: field.contact.conversations.get()},
)
with tenant_atomic(self.context):
webchat.load(self.context, refs)
self.assertEqual(ContactFieldValue.objects.count(), 1)
self.assertEqual(WidgetAsset.objects.count(), 1)
def test_asset_removal_deletes_file_and_releases_its_storage_usage(self) -> None:
asset = WidgetAsset.objects.get()
path, storage, size = asset.file.name, asset.file.storage, asset.size
before = OrganizationStorageUsage.objects.get(organization=self.context.organization).bytes_used
record = self.dataset.records.get(content_type__model="widgetasset", object_id=str(asset.pk))
with self.captureOnCommitCallbacks(execute=True), tenant_atomic(self.context):
self.assertTrue(_delete_record(record, set()))
with tenant_atomic(self.context):
self.assertFalse(storage.exists(path))
self.assertFalse(WidgetAsset.objects.exists())
after = OrganizationStorageUsage.objects.get(organization=self.context.organization).bytes_used
self.assertEqual(after, before - size)
+3
View File
@@ -25,6 +25,9 @@
- Dev-окружение задачи использует постоянный каталог PostgreSQL, Redis и media
вне worktree. Скрипт проверяет исходные секреты и не инициализирует
отсутствующую базу без явного `-InitializeDatabase`.
- Демо-набор покрывает новые модели полей контакта и изображений виджета.
Их записи учитываются в реестре; удаление изображения освобождает место
в хранилище. Текущее оформление демо-виджета сохраняется.
## Обновление