mirror of
https://github.com/dartdavros/chatballs.git
synced 2026-10-05 01:14:58 +03:00
🐛 fix(demo): покрыть новые модели веб-чата
This commit is contained in:
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)
|
||||
@@ -25,6 +25,9 @@
|
||||
- Dev-окружение задачи использует постоянный каталог PostgreSQL, Redis и media
|
||||
вне worktree. Скрипт проверяет исходные секреты и не инициализирует
|
||||
отсутствующую базу без явного `-InitializeDatabase`.
|
||||
- Демо-набор покрывает новые модели полей контакта и изображений виджета.
|
||||
Их записи учитываются в реестре; удаление изображения освобождает место
|
||||
в хранилище. Текущее оформление демо-виджета сохраняется.
|
||||
|
||||
## Обновление
|
||||
|
||||
|
||||
Reference in new issue
Block a user