280 lines
8.5 KiB
Elixir
280 lines
8.5 KiB
Elixir
defmodule BDS.Posts.Translations do
|
|
@moduledoc false
|
|
|
|
import Ecto.Query
|
|
|
|
alias BDS.Persistence
|
|
alias BDS.Posts
|
|
alias BDS.Posts.FileSync
|
|
alias BDS.Posts.Post
|
|
alias BDS.Posts.RebuildFromFiles
|
|
alias BDS.Posts.Translation
|
|
alias BDS.Projects
|
|
alias BDS.Repo
|
|
alias BDS.Search
|
|
|
|
@type attrs :: %{optional(atom()) => term(), optional(String.t()) => term()}
|
|
|
|
@spec publish_post_translation(String.t(), String.t() | atom()) ::
|
|
{:ok, Translation.t()} | {:error, :not_found | term()}
|
|
def publish_post_translation(post_id, language) do
|
|
normalized_language = language |> to_string() |> String.trim() |> String.downcase()
|
|
|
|
case Repo.get_by(Translation, translation_for: post_id, language: normalized_language) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
%Translation{} ->
|
|
with {:ok, _post} <- Posts.publish_post(post_id),
|
|
%Translation{} = translation <-
|
|
Repo.get_by(Translation, translation_for: post_id, language: normalized_language) do
|
|
{:ok, translation}
|
|
else
|
|
nil -> {:error, :not_found}
|
|
error -> error
|
|
end
|
|
end
|
|
end
|
|
|
|
@spec list_post_translations(String.t()) :: {:ok, [Translation.t()]}
|
|
def list_post_translations(post_id) do
|
|
{:ok,
|
|
Repo.all(
|
|
from(translation in Translation,
|
|
where: translation.translation_for == ^post_id,
|
|
order_by: [asc: translation.language]
|
|
)
|
|
)}
|
|
end
|
|
|
|
@spec upsert_post_translation(String.t(), String.t() | atom(), attrs()) ::
|
|
{:ok, Translation.t()} | {:error, :not_found | Ecto.Changeset.t()}
|
|
def upsert_post_translation(post_id, language, attrs) do
|
|
case Repo.get(Post, post_id) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
%Post{do_not_translate: true} = post ->
|
|
{:error,
|
|
post
|
|
|> Post.changeset(%{})
|
|
|> Ecto.Changeset.add_error(
|
|
:do_not_translate,
|
|
"cannot add translations when do_not_translate is true"
|
|
)}
|
|
|
|
%Post{} = post ->
|
|
now = Persistence.now_ms()
|
|
normalized_language = normalize_language(language)
|
|
|
|
translation =
|
|
Repo.get_by(Translation, translation_for: post.id, language: normalized_language) ||
|
|
%Translation{}
|
|
|
|
updates =
|
|
normalize_translation_updates(post, translation, normalized_language, attrs, now)
|
|
|
|
translation
|
|
|> Translation.changeset(updates)
|
|
|> Repo.insert_or_update()
|
|
|> case do
|
|
{:ok, saved_translation} ->
|
|
{:ok, _post} = maybe_reopen_source_post_for_manual_translation(post, attrs)
|
|
:ok = Search.sync_post(post.id)
|
|
{:ok, saved_translation}
|
|
|
|
error ->
|
|
error
|
|
end
|
|
end
|
|
end
|
|
|
|
@spec delete_post_translation(String.t()) :: {:ok, :deleted} | {:error, :not_found}
|
|
def delete_post_translation(translation_id) do
|
|
case Repo.get(Translation, translation_id) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
%Translation{} = translation ->
|
|
:ok = FileSync.delete_translation_file(translation)
|
|
Repo.delete!(translation)
|
|
:ok = Search.sync_post(translation.translation_for)
|
|
{:ok, :deleted}
|
|
end
|
|
end
|
|
|
|
@spec sync_post_translation_from_file(String.t()) ::
|
|
{:ok, Translation.t()} | {:error, :not_found}
|
|
def sync_post_translation_from_file(translation_id) do
|
|
case Repo.get(Translation, translation_id) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
%Translation{file_path: file_path} when file_path in [nil, ""] ->
|
|
{:error, :not_found}
|
|
|
|
%Translation{} = translation ->
|
|
project = Projects.get_project!(translation.project_id)
|
|
full_path = Path.join(Projects.project_data_dir(project), translation.file_path)
|
|
|
|
if File.exists?(full_path) do
|
|
rebuild_file = RebuildFromFiles.parse_rebuild_file(project, full_path)
|
|
|
|
{:ok,
|
|
RebuildFromFiles.upsert_post_translation_from_rebuild_file(
|
|
translation.project_id,
|
|
rebuild_file,
|
|
sync_search: true
|
|
)}
|
|
else
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
end
|
|
|
|
@spec rewrite_published_post_translation(String.t()) ::
|
|
{:ok, Translation.t()} | {:error, :not_found}
|
|
def rewrite_published_post_translation(translation_id) do
|
|
case Repo.get(Translation, translation_id) do
|
|
nil ->
|
|
{:error, :not_found}
|
|
|
|
%Translation{file_path: file_path, status: status} = translation
|
|
when file_path not in [nil, ""] and status == :published ->
|
|
post = Repo.get!(Post, translation.translation_for)
|
|
:ok = publish_translation(post, translation)
|
|
{:ok, Repo.get!(Translation, translation_id)}
|
|
|
|
%Translation{} ->
|
|
{:error, :not_found}
|
|
end
|
|
end
|
|
|
|
@doc false
|
|
def publish_post_translations(%Post{} = post) do
|
|
Repo.all(from(translation in Translation, where: translation.translation_for == ^post.id))
|
|
|> Enum.each(fn translation ->
|
|
if translation.status == :draft do
|
|
publish_translation(post, translation)
|
|
end
|
|
end)
|
|
|
|
:ok
|
|
end
|
|
|
|
@doc false
|
|
def publish_translation(%Post{} = post, %Translation{} = translation) do
|
|
project = Projects.get_project!(post.project_id)
|
|
published_at = translation.published_at || Persistence.now_ms()
|
|
relative_path = FileSync.translation_relative_path(post, translation.language)
|
|
full_path = Path.join(Projects.project_data_dir(project), relative_path)
|
|
updated_at = Persistence.now_ms()
|
|
body = FileSync.publishable_translation_body(translation, full_path)
|
|
|
|
:ok =
|
|
Persistence.atomic_write(
|
|
full_path,
|
|
FileSync.serialize_translation_file(
|
|
%{translation | updated_at: updated_at, content: body},
|
|
published_at
|
|
)
|
|
)
|
|
|
|
translation
|
|
|> Translation.changeset(%{
|
|
status: :published,
|
|
published_at: published_at,
|
|
file_path: relative_path,
|
|
content: nil,
|
|
updated_at: updated_at
|
|
})
|
|
|> Repo.update!()
|
|
|
|
:ok
|
|
end
|
|
|
|
defp normalize_translation_updates(post, %Translation{} = translation, language, attrs, now) do
|
|
requested_status =
|
|
case attr(attrs, :status) do
|
|
nil -> nil
|
|
status -> RebuildFromFiles.parse_translation_status(status)
|
|
end
|
|
|
|
updates =
|
|
%{}
|
|
|> maybe_put(:title, attr(attrs, :title))
|
|
|> maybe_put(:excerpt, attr(attrs, :excerpt))
|
|
|> maybe_put(:content, attr(attrs, :content))
|
|
|
|
reopened? =
|
|
translation.status == :published and translation_content_change?(translation, updates)
|
|
|
|
status = if(reopened?, do: :draft, else: requested_status || translation.status || :draft)
|
|
|
|
%{
|
|
id: translation.id || Ecto.UUID.generate(),
|
|
project_id: post.project_id,
|
|
translation_for: post.id,
|
|
language: language,
|
|
title: Map.get(updates, :title, translation.title),
|
|
excerpt: Map.get(updates, :excerpt, translation.excerpt),
|
|
content: Map.get(updates, :content, translation.content),
|
|
status: status,
|
|
created_at: translation.created_at || now,
|
|
updated_at: now,
|
|
published_at: translation.published_at || if(status == :published, do: now, else: nil),
|
|
file_path: translation.file_path || "",
|
|
checksum: translation.checksum
|
|
}
|
|
end
|
|
|
|
defp translation_content_change?(translation, updates) do
|
|
Enum.any?([:title, :excerpt, :content], fn field ->
|
|
case Map.fetch(updates, field) do
|
|
{:ok, value} -> value != Map.get(translation, field)
|
|
:error -> false
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp maybe_reopen_source_post_for_manual_translation(%Post{} = post, attrs) do
|
|
if attr(attrs, :auto_generated) == true or post.status != :published or
|
|
post.file_path in [nil, ""] do
|
|
{:ok, post}
|
|
else
|
|
project = Projects.get_project!(post.project_id)
|
|
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
|
|
restored_content = FileSync.published_post_body(post, full_path)
|
|
|
|
post
|
|
|> Post.changeset(%{
|
|
status: :draft,
|
|
content: restored_content,
|
|
updated_at: Persistence.now_ms()
|
|
})
|
|
|> Repo.update()
|
|
end
|
|
end
|
|
|
|
defp normalize_language(nil), do: ""
|
|
|
|
defp normalize_language(language) do
|
|
language
|
|
|> to_string()
|
|
|> String.downcase()
|
|
|> String.split("-", parts: 2)
|
|
|> hd()
|
|
end
|
|
|
|
defp maybe_put(map, _key, nil), do: map
|
|
defp maybe_put(map, key, value), do: Map.put(map, key, value)
|
|
|
|
defp attr(attrs, key) do
|
|
cond do
|
|
Map.has_key?(attrs, key) -> Map.get(attrs, key)
|
|
Map.has_key?(attrs, Atom.to_string(key)) -> Map.get(attrs, Atom.to_string(key))
|
|
true -> nil
|
|
end
|
|
end
|
|
end
|