fix: issue #2 auto-translate only on manual save now
This commit is contained in:
@@ -503,7 +503,9 @@ defmodule BDS.Desktop.ShellLive.PostEditor do
|
|||||||
active_language = socket.assigns.active_language
|
active_language = socket.assigns.active_language
|
||||||
draft = component_current_draft(socket, post, metadata, active_language)
|
draft = component_current_draft(socket, post, metadata, active_language)
|
||||||
|
|
||||||
case persist(post, draft, active_language, metadata, :save) do
|
action = if keep_draft?, do: :auto_save, else: :save
|
||||||
|
|
||||||
|
case persist(post, draft, active_language, metadata, action) do
|
||||||
{:ok, record} ->
|
{:ok, record} ->
|
||||||
refreshed_post = Posts.get_post!(post.id)
|
refreshed_post = Posts.get_post!(post.id)
|
||||||
_refreshed_form = persisted_form(refreshed_post, metadata, active_language)
|
_refreshed_form = persisted_form(refreshed_post, metadata, active_language)
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
|
|||||||
canonical_language
|
canonical_language
|
||||||
) do
|
) do
|
||||||
post
|
post
|
||||||
|> save_canonical_draft(draft)
|
|> save_canonical_draft(draft, action)
|
||||||
|> maybe_publish_post(post.id, action)
|
|> maybe_publish_post(post.id, action)
|
||||||
else
|
else
|
||||||
post.id
|
post.id
|
||||||
@@ -65,8 +65,10 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
|
|||||||
else: dgettext("ui", "Delete this unpublished draft")
|
else: dgettext("ui", "Delete this unpublished draft")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp save_canonical_draft(%Post{id: post_id}, draft) do
|
defp save_canonical_draft(%Post{id: post_id}, draft, action) do
|
||||||
Posts.update_post(post_id, %{
|
Posts.update_post(
|
||||||
|
post_id,
|
||||||
|
%{
|
||||||
title: blank_to_nil(Map.get(draft, "title")),
|
title: blank_to_nil(Map.get(draft, "title")),
|
||||||
excerpt: blank_to_nil(Map.get(draft, "excerpt")),
|
excerpt: blank_to_nil(Map.get(draft, "excerpt")),
|
||||||
content: blank_to_nil(Map.get(draft, "content")),
|
content: blank_to_nil(Map.get(draft, "content")),
|
||||||
@@ -76,7 +78,9 @@ defmodule BDS.Desktop.ShellLive.PostEditor.Persistence do
|
|||||||
language: blank_to_nil(Map.get(draft, "language")),
|
language: blank_to_nil(Map.get(draft, "language")),
|
||||||
do_not_translate: Map.get(draft, "do_not_translate", false),
|
do_not_translate: Map.get(draft, "do_not_translate", false),
|
||||||
template_slug: blank_to_nil(Map.get(draft, "template_slug"))
|
template_slug: blank_to_nil(Map.get(draft, "template_slug"))
|
||||||
})
|
},
|
||||||
|
auto_translate: action != :auto_save
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp save_translation_draft(post_id, language, draft) do
|
defp save_translation_draft(post_id, language, draft) do
|
||||||
|
|||||||
@@ -106,7 +106,6 @@ defmodule BDS.Posts do
|
|||||||
{:ok, post} ->
|
{:ok, post} ->
|
||||||
:ok = Embeddings.sync_post(post)
|
:ok = Embeddings.sync_post(post)
|
||||||
:ok = Search.sync_post(post)
|
:ok = Search.sync_post(post)
|
||||||
:ok = AutoTranslation.maybe_schedule(post)
|
|
||||||
{:ok, post}
|
{:ok, post}
|
||||||
|
|
||||||
error ->
|
error ->
|
||||||
@@ -114,9 +113,9 @@ defmodule BDS.Posts do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec update_post(String.t(), attrs()) ::
|
@spec update_post(String.t(), attrs(), keyword()) ::
|
||||||
{:ok, Post.t()} | {:error, :not_found | Ecto.Changeset.t()}
|
{:ok, Post.t()} | {:error, :not_found | Ecto.Changeset.t()}
|
||||||
def update_post(post_id, attrs) do
|
def update_post(post_id, attrs, opts \\ []) do
|
||||||
case Repo.get(Post, post_id) do
|
case Repo.get(Post, post_id) do
|
||||||
nil ->
|
nil ->
|
||||||
{:error, :not_found}
|
{:error, :not_found}
|
||||||
@@ -145,7 +144,13 @@ defmodule BDS.Posts do
|
|||||||
:ok = Embeddings.sync_post(updated_post)
|
:ok = Embeddings.sync_post(updated_post)
|
||||||
:ok = PostLinks.sync_post_links(updated_post)
|
:ok = PostLinks.sync_post_links(updated_post)
|
||||||
:ok = Search.sync_post(updated_post)
|
:ok = Search.sync_post(updated_post)
|
||||||
|
|
||||||
|
# Auto-translation only runs on an explicit manual save from the
|
||||||
|
# editor, never on post creation or autosave.
|
||||||
|
if Keyword.get(opts, :auto_translate, false) do
|
||||||
:ok = AutoTranslation.maybe_schedule(updated_post)
|
:ok = AutoTranslation.maybe_schedule(updated_post)
|
||||||
|
end
|
||||||
|
|
||||||
{:ok, updated_post}
|
{:ok, updated_post}
|
||||||
|
|
||||||
error ->
|
error ->
|
||||||
|
|||||||
@@ -231,6 +231,8 @@ rule PostTranslateAction {
|
|||||||
|
|
||||||
rule PostAutoTranslateOnSave {
|
rule PostAutoTranslateOnSave {
|
||||||
when: PostSaved(post_id)
|
when: PostSaved(post_id)
|
||||||
|
-- Only an explicit manual save (or publish) triggers this. Auto-saves and
|
||||||
|
-- post creation (sidebar, deeplink/bookmarklet, import, scripting) never do.
|
||||||
-- Gate: airplane mode check + auto_translate not disabled (doNotTranslate=false)
|
-- Gate: airplane mode check + auto_translate not disabled (doNotTranslate=false)
|
||||||
-- For each configured blog language missing a translation:
|
-- For each configured blog language missing a translation:
|
||||||
-- Enqueue background translation task (title model)
|
-- Enqueue background translation task (title model)
|
||||||
|
|||||||
@@ -192,8 +192,9 @@ invariant FtsIncludesTranslations {
|
|||||||
-- Distilled from: lib/bds/posts/auto_translation.ex
|
-- Distilled from: lib/bds/posts/auto_translation.ex
|
||||||
--
|
--
|
||||||
-- Two entry points share one translation primitive:
|
-- Two entry points share one translation primitive:
|
||||||
-- 1. ScheduleAutoTranslation - reactive, fired after a post is created or
|
-- 1. ScheduleAutoTranslation - reactive, fired only by an explicit manual
|
||||||
-- updated. One background task per missing language produces a DRAFT
|
-- save (or publish) in the post editor — never by post creation or
|
||||||
|
-- auto-save. One background task per missing language produces a DRAFT
|
||||||
-- translation, then cascades to the post's linked media.
|
-- translation, then cascades to the post's linked media.
|
||||||
-- 2. FillMissingTranslations - batch maintenance action. Scans every
|
-- 2. FillMissingTranslations - batch maintenance action. Scans every
|
||||||
-- published post, AUTO-PUBLISHES the generated translations, fills linked
|
-- published post, AUTO-PUBLISHES the generated translations, fills linked
|
||||||
@@ -210,7 +211,9 @@ surface AutoTranslationControlSurface {
|
|||||||
facing _: TranslationOperator
|
facing _: TranslationOperator
|
||||||
|
|
||||||
provides:
|
provides:
|
||||||
-- Reactive trigger: emitted by post create/update side effects.
|
-- Reactive trigger: emitted only when a post is updated with
|
||||||
|
-- auto_translate enabled (the editor's manual save/publish path);
|
||||||
|
-- post creation and auto-save never emit it.
|
||||||
PostSavedForAutoTranslation(post)
|
PostSavedForAutoTranslation(post)
|
||||||
-- Batch trigger: "fill missing translations" maintenance action.
|
-- Batch trigger: "fill missing translations" maintenance action.
|
||||||
FillMissingTranslationsRequested(project)
|
FillMissingTranslationsRequested(project)
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
assert {:ok, []} = Posts.list_post_translations(post.id)
|
assert {:ok, []} = Posts.list_post_translations(post.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_post enqueues and completes auto-translation tasks for missing project languages",
|
test "create_post does not enqueue auto-translation tasks",
|
||||||
%{project: project} do
|
%{project: project} do
|
||||||
configure_auto_translation_test_runtime()
|
configure_auto_translation_test_runtime()
|
||||||
|
|
||||||
@@ -162,21 +162,79 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
language: "en"
|
language: "en"
|
||||||
})
|
})
|
||||||
|
|
||||||
translations = wait_for_post_translations(post.id, ["de", "fr"])
|
assert ai_tasks() == []
|
||||||
|
assert {:ok, []} = Posts.list_post_translations(post.id)
|
||||||
assert Enum.map(translations, & &1.language) == ["de", "fr"]
|
|
||||||
assert Enum.all?(translations, &(&1.title == "Hallo Welt"))
|
|
||||||
assert Enum.all?(translations, &(&1.excerpt == "Kurze Zusammenfassung"))
|
|
||||||
assert Enum.all?(translations, &(&1.content == "# Hallo Welt\n\nUbersetzter Inhalt"))
|
|
||||||
|
|
||||||
tasks = wait_for_ai_tasks(2)
|
|
||||||
|
|
||||||
assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to de" and &1.status == :completed))
|
|
||||||
assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to fr" and &1.status == :completed))
|
|
||||||
assert Enum.all?(tasks, &(&1.group_name == "AI"))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_post auto-translates missing languages and cascades linked media translations",
|
test "update_post without auto_translate does not enqueue auto-translation tasks",
|
||||||
|
%{project: project} do
|
||||||
|
configure_auto_translation_test_runtime()
|
||||||
|
|
||||||
|
assert {:ok, _metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en", "de"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, post} =
|
||||||
|
Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Autosaved",
|
||||||
|
content: "Source body",
|
||||||
|
language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, _updated_post} = Posts.update_post(post.id, %{content: "Autosaved body"})
|
||||||
|
|
||||||
|
assert ai_tasks() == []
|
||||||
|
assert {:ok, []} = Posts.list_post_translations(post.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "editor persist schedules auto-translation on manual save but not on auto-save",
|
||||||
|
%{project: project} do
|
||||||
|
configure_auto_translation_test_runtime()
|
||||||
|
|
||||||
|
assert {:ok, metadata} =
|
||||||
|
Metadata.update_project_metadata(project.id, %{
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en", "de"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, post} =
|
||||||
|
Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Editor Post",
|
||||||
|
content: "Source body",
|
||||||
|
language: "en"
|
||||||
|
})
|
||||||
|
|
||||||
|
draft = %{
|
||||||
|
"title" => "Editor Post",
|
||||||
|
"excerpt" => "",
|
||||||
|
"content" => "Edited body",
|
||||||
|
"tags" => "",
|
||||||
|
"categories" => "",
|
||||||
|
"author" => "",
|
||||||
|
"language" => "en",
|
||||||
|
"do_not_translate" => false,
|
||||||
|
"template_slug" => ""
|
||||||
|
}
|
||||||
|
|
||||||
|
alias BDS.Desktop.ShellLive.PostEditor.Persistence
|
||||||
|
|
||||||
|
assert {:ok, _post} = Persistence.persist(post, draft, "en", metadata, :auto_save)
|
||||||
|
assert ai_tasks() == []
|
||||||
|
|
||||||
|
assert {:ok, _post} = Persistence.persist(post, draft, "en", metadata, :save)
|
||||||
|
|
||||||
|
[translation] = wait_for_post_translations(post.id, ["de"])
|
||||||
|
assert translation.language == "de"
|
||||||
|
|
||||||
|
tasks = wait_for_ai_tasks(1)
|
||||||
|
assert Enum.any?(tasks, &(&1.name == "Auto-translate Post to de" and &1.status == :completed))
|
||||||
|
end
|
||||||
|
|
||||||
|
test "update_post with auto_translate translates missing languages and cascades linked media translations",
|
||||||
%{project: project, temp_dir: temp_dir} do
|
%{project: project, temp_dir: temp_dir} do
|
||||||
configure_auto_translation_test_runtime()
|
configure_auto_translation_test_runtime()
|
||||||
|
|
||||||
@@ -221,10 +279,14 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
])
|
])
|
||||||
|
|
||||||
assert {:ok, _updated_post} =
|
assert {:ok, _updated_post} =
|
||||||
Posts.update_post(post.id, %{
|
Posts.update_post(
|
||||||
|
post.id,
|
||||||
|
%{
|
||||||
do_not_translate: false,
|
do_not_translate: false,
|
||||||
content: "Updated source body"
|
content: "Updated source body"
|
||||||
})
|
},
|
||||||
|
auto_translate: true
|
||||||
|
)
|
||||||
|
|
||||||
[translation] = wait_for_post_translations(post.id, ["de"])
|
[translation] = wait_for_post_translations(post.id, ["de"])
|
||||||
assert translation.language == "de"
|
assert translation.language == "de"
|
||||||
@@ -427,8 +489,6 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp wait_for_ai_tasks(count, attempts \\ 100)
|
|
||||||
|
|
||||||
test "do_not_translate guard prevents translation upsert via the API", %{
|
test "do_not_translate guard prevents translation upsert via the API", %{
|
||||||
project: project
|
project: project
|
||||||
} do
|
} do
|
||||||
@@ -453,14 +513,19 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
{"cannot add translations when do_not_translate is true", []}
|
{"cannot add translations when do_not_translate is true", []}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp ai_tasks do
|
||||||
|
BDS.Tasks.list_tasks()
|
||||||
|
|> Enum.filter(&(&1.group_name == "AI"))
|
||||||
|
end
|
||||||
|
|
||||||
|
defp wait_for_ai_tasks(count, attempts \\ 100)
|
||||||
|
|
||||||
defp wait_for_ai_tasks(_count, 0) do
|
defp wait_for_ai_tasks(_count, 0) do
|
||||||
flunk("AI tasks did not reach expected state")
|
flunk("AI tasks did not reach expected state")
|
||||||
end
|
end
|
||||||
|
|
||||||
defp wait_for_ai_tasks(count, attempts) do
|
defp wait_for_ai_tasks(count, attempts) do
|
||||||
tasks =
|
tasks = ai_tasks()
|
||||||
BDS.Tasks.list_tasks()
|
|
||||||
|> Enum.filter(&(&1.group_name == "AI"))
|
|
||||||
|
|
||||||
if length(tasks) >= count and Enum.all?(tasks, &(&1.status == :completed)) do
|
if length(tasks) >= count and Enum.all?(tasks, &(&1.status == :completed)) do
|
||||||
tasks
|
tasks
|
||||||
|
|||||||
Reference in New Issue
Block a user