feat: more complete metadata diff, scp publishing and rendering context
This commit is contained in:
@@ -9,8 +9,7 @@ config :bds, BDS.Repo,
|
|||||||
stacktrace: true,
|
stacktrace: true,
|
||||||
show_sensitive_data_on_connection_error: true
|
show_sensitive_data_on_connection_error: true
|
||||||
|
|
||||||
config :bds, BDS.Application,
|
config :bds, BDS.Application, desktop_adapter: :pending_selection
|
||||||
desktop_adapter: :pending_selection
|
|
||||||
|
|
||||||
config :bds, :scripting,
|
config :bds, :scripting,
|
||||||
runtime: BDS.Scripting.Lua,
|
runtime: BDS.Scripting.Lua,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import Config
|
import Config
|
||||||
|
|
||||||
config :bds, BDS.Repo,
|
config :bds, BDS.Repo, pool_size: 5
|
||||||
pool_size: 5
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
@core_sections [:core, :single, :category, :tag, :date]
|
@core_sections [:core, :single, :category, :tag, :date]
|
||||||
|
|
||||||
def plan_generation(project_id, sections \\ [:core]) when is_binary(project_id) and is_list(sections) do
|
def plan_generation(project_id, sections \\ [:core])
|
||||||
|
when is_binary(project_id) and is_list(sections) do
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
{:ok, metadata} = Metadata.get_project_metadata(project_id)
|
||||||
{:ok, generated_files} = list_generated_files(project_id)
|
{:ok, generated_files} = list_generated_files(project_id)
|
||||||
@@ -34,7 +35,8 @@ defmodule BDS.Generation do
|
|||||||
}}
|
}}
|
||||||
end
|
end
|
||||||
|
|
||||||
def generate_site(project_id, sections \\ [:core]) when is_binary(project_id) and is_list(sections) do
|
def generate_site(project_id, sections \\ [:core])
|
||||||
|
when is_binary(project_id) and is_list(sections) do
|
||||||
with {:ok, plan} <- plan_generation(project_id, sections) do
|
with {:ok, plan} <- plan_generation(project_id, sections) do
|
||||||
outputs = build_outputs(plan)
|
outputs = build_outputs(plan)
|
||||||
|
|
||||||
@@ -106,7 +108,8 @@ defmodule BDS.Generation do
|
|||||||
)}
|
)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete_generated_file(project_id, relative_path) when is_binary(project_id) and is_binary(relative_path) do
|
def delete_generated_file(project_id, relative_path)
|
||||||
|
when is_binary(project_id) and is_binary(relative_path) do
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
|
|
||||||
case File.rm(output_path(project, relative_path)) do
|
case File.rm(output_path(project, relative_path)) do
|
||||||
@@ -117,7 +120,9 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
Repo.delete_all(
|
Repo.delete_all(
|
||||||
from generated_file in GeneratedFileHash,
|
from generated_file in GeneratedFileHash,
|
||||||
where: generated_file.project_id == ^project_id and generated_file.relative_path == ^relative_path
|
where:
|
||||||
|
generated_file.project_id == ^project_id and
|
||||||
|
generated_file.relative_path == ^relative_path
|
||||||
)
|
)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
@@ -146,8 +151,10 @@ defmodule BDS.Generation do
|
|||||||
build_archive_outputs(plan, published_posts)
|
build_archive_outputs(plan, published_posts)
|
||||||
|
|
||||||
urls =
|
urls =
|
||||||
core_outputs ++ single_outputs ++ archive_outputs
|
(core_outputs ++ single_outputs ++ archive_outputs)
|
||||||
|> Enum.map(fn {relative_path, _content} -> url_for_output(plan.base_url, relative_path) end)
|
|> Enum.map(fn {relative_path, _content} ->
|
||||||
|
url_for_output(plan.base_url, relative_path)
|
||||||
|
end)
|
||||||
|
|
||||||
sitemap =
|
sitemap =
|
||||||
if :core in plan.sections do
|
if :core in plan.sections do
|
||||||
@@ -199,9 +206,42 @@ defmodule BDS.Generation do
|
|||||||
Enum.with_index(paginated_posts, 1)
|
Enum.with_index(paginated_posts, 1)
|
||||||
|> Enum.flat_map(fn {page_posts, page_number} ->
|
|> Enum.flat_map(fn {page_posts, page_number} ->
|
||||||
Enum.map(languages, fn language ->
|
Enum.map(languages, fn language ->
|
||||||
|
pagination = %{
|
||||||
|
current_page: page_number,
|
||||||
|
total_pages: length(paginated_posts),
|
||||||
|
total_items: length(posts),
|
||||||
|
items_per_page: max(plan.max_posts_per_page, 1),
|
||||||
|
has_prev_page: page_number > 1,
|
||||||
|
prev_page_href:
|
||||||
|
if(page_number > 1,
|
||||||
|
do:
|
||||||
|
archive_href(
|
||||||
|
route_language(plan.language, language),
|
||||||
|
["category", category_slug],
|
||||||
|
page_number - 1
|
||||||
|
),
|
||||||
|
else: ""
|
||||||
|
),
|
||||||
|
has_next_page: page_number < length(paginated_posts),
|
||||||
|
next_page_href:
|
||||||
|
if(page_number < length(paginated_posts),
|
||||||
|
do:
|
||||||
|
archive_href(
|
||||||
|
route_language(plan.language, language),
|
||||||
|
["category", category_slug],
|
||||||
|
page_number + 1
|
||||||
|
),
|
||||||
|
else: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
archive_path(route_language(plan.language, language), ["category", category_slug], page_number),
|
archive_path(
|
||||||
render_archive_page(plan, category, page_posts, language, "category")
|
route_language(plan.language, language),
|
||||||
|
["category", category_slug],
|
||||||
|
page_number
|
||||||
|
),
|
||||||
|
render_archive_page(plan, category, page_posts, language, "category", pagination)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -216,11 +256,12 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
Enum.flat_map(tag_posts, fn {tag, posts} ->
|
Enum.flat_map(tag_posts, fn {tag, posts} ->
|
||||||
tag_slug = Slug.slugify(tag)
|
tag_slug = Slug.slugify(tag)
|
||||||
|
pagination = pagination_for_posts(posts)
|
||||||
|
|
||||||
Enum.map(languages, fn language ->
|
Enum.map(languages, fn language ->
|
||||||
{
|
{
|
||||||
archive_path(route_language(plan.language, language), ["tag", tag_slug], 1),
|
archive_path(route_language(plan.language, language), ["tag", tag_slug], 1),
|
||||||
render_archive_page(plan, tag, posts, language, "tag")
|
render_archive_page(plan, tag, posts, language, "tag", pagination)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -232,20 +273,24 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
year_outputs =
|
year_outputs =
|
||||||
Enum.flat_map(years, fn {year, posts} ->
|
Enum.flat_map(years, fn {year, posts} ->
|
||||||
|
pagination = pagination_for_posts(posts)
|
||||||
|
|
||||||
Enum.map(languages, fn language ->
|
Enum.map(languages, fn language ->
|
||||||
{
|
{
|
||||||
archive_path(route_language(plan.language, language), [year], 1),
|
archive_path(route_language(plan.language, language), [year], 1),
|
||||||
render_date_archive_page(plan, year, posts, language)
|
render_date_archive_page(plan, year, posts, language, pagination)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
month_outputs =
|
month_outputs =
|
||||||
Enum.flat_map(months, fn {{year, month}, posts} ->
|
Enum.flat_map(months, fn {{year, month}, posts} ->
|
||||||
|
pagination = pagination_for_posts(posts)
|
||||||
|
|
||||||
Enum.map(languages, fn language ->
|
Enum.map(languages, fn language ->
|
||||||
{
|
{
|
||||||
archive_path(route_language(plan.language, language), [year, month], 1),
|
archive_path(route_language(plan.language, language), [year, month], 1),
|
||||||
render_date_archive_page(plan, "#{year}-#{month}", posts, language)
|
render_date_archive_page(plan, "#{year}-#{month}", posts, language, pagination)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
@@ -259,7 +304,16 @@ defmodule BDS.Generation do
|
|||||||
main_posts = build_list_posts(plan.base_url, published_posts, nil)
|
main_posts = build_list_posts(plan.base_url, published_posts, nil)
|
||||||
|
|
||||||
[
|
[
|
||||||
{"index.html", render_list_output(plan, language, plan.project_name, main_posts, %{kind: "core"}, fn -> render_home(plan, language) end)},
|
{"index.html",
|
||||||
|
render_list_output(
|
||||||
|
plan,
|
||||||
|
language,
|
||||||
|
plan.project_name,
|
||||||
|
main_posts,
|
||||||
|
%{kind: "core"},
|
||||||
|
pagination_for_posts(main_posts),
|
||||||
|
fn -> render_home(plan, language) end
|
||||||
|
)},
|
||||||
{"404.html", render_not_found_output(plan, language)},
|
{"404.html", render_not_found_output(plan, language)},
|
||||||
{"feed.xml", render_feed(plan, language, published_posts)},
|
{"feed.xml", render_feed(plan, language, published_posts)},
|
||||||
{"atom.xml", render_atom(plan, language, published_posts)},
|
{"atom.xml", render_atom(plan, language, published_posts)},
|
||||||
@@ -270,10 +324,22 @@ defmodule BDS.Generation do
|
|||||||
localized_posts = build_list_posts(plan.base_url, published_posts, localized_prefix)
|
localized_posts = build_list_posts(plan.base_url, published_posts, localized_prefix)
|
||||||
|
|
||||||
[
|
[
|
||||||
{Path.join(localized_language, "index.html"), render_list_output(plan, localized_language, plan.project_name, localized_posts, %{kind: "core"}, fn -> render_home(plan, localized_language) end)},
|
{Path.join(localized_language, "index.html"),
|
||||||
{Path.join(localized_language, "404.html"), render_not_found_output(plan, localized_language)},
|
render_list_output(
|
||||||
{Path.join(localized_language, "feed.xml"), render_feed(plan, localized_language, published_posts)},
|
plan,
|
||||||
{Path.join(localized_language, "atom.xml"), render_atom(plan, localized_language, published_posts)}
|
localized_language,
|
||||||
|
plan.project_name,
|
||||||
|
localized_posts,
|
||||||
|
%{kind: "core"},
|
||||||
|
pagination_for_posts(localized_posts),
|
||||||
|
fn -> render_home(plan, localized_language) end
|
||||||
|
)},
|
||||||
|
{Path.join(localized_language, "404.html"),
|
||||||
|
render_not_found_output(plan, localized_language)},
|
||||||
|
{Path.join(localized_language, "feed.xml"),
|
||||||
|
render_feed(plan, localized_language, published_posts)},
|
||||||
|
{Path.join(localized_language, "atom.xml"),
|
||||||
|
render_atom(plan, localized_language, published_posts)}
|
||||||
]
|
]
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -284,9 +350,21 @@ defmodule BDS.Generation do
|
|||||||
body = load_body(project_id, post.file_path, post.content)
|
body = load_body(project_id, post.file_path, post.content)
|
||||||
|
|
||||||
{post_output_path(post),
|
{post_output_path(post),
|
||||||
render_post_output(project_id, post.template_slug, %{id: post.id, title: post.title, content: body, slug: post.slug, language: post.language, excerpt: post.excerpt}, fn ->
|
render_post_output(
|
||||||
|
project_id,
|
||||||
|
post.template_slug,
|
||||||
|
%{
|
||||||
|
id: post.id,
|
||||||
|
title: post.title,
|
||||||
|
content: body,
|
||||||
|
slug: post.slug,
|
||||||
|
language: post.language,
|
||||||
|
excerpt: post.excerpt
|
||||||
|
},
|
||||||
|
fn ->
|
||||||
render_post_page(post.title, body, post.slug, post.language)
|
render_post_page(post.title, body, post.slug, post.language)
|
||||||
end)}
|
end
|
||||||
|
)}
|
||||||
end)
|
end)
|
||||||
|
|
||||||
translation_outputs =
|
translation_outputs =
|
||||||
@@ -300,9 +378,21 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
[
|
[
|
||||||
{post_output_path(post, translation.language),
|
{post_output_path(post, translation.language),
|
||||||
render_post_output(project_id, post.template_slug, %{id: translation.id, title: translation.title, content: body, slug: post.slug, language: translation.language, excerpt: translation.excerpt}, fn ->
|
render_post_output(
|
||||||
|
project_id,
|
||||||
|
post.template_slug,
|
||||||
|
%{
|
||||||
|
id: translation.id,
|
||||||
|
title: translation.title,
|
||||||
|
content: body,
|
||||||
|
slug: post.slug,
|
||||||
|
language: translation.language,
|
||||||
|
excerpt: translation.excerpt
|
||||||
|
},
|
||||||
|
fn ->
|
||||||
render_post_page(translation.title, body, post.slug, translation.language)
|
render_post_page(translation.title, body, post.slug, translation.language)
|
||||||
end)}
|
end
|
||||||
|
)}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -434,7 +524,7 @@ defmodule BDS.Generation do
|
|||||||
|> IO.iodata_to_binary()
|
|> IO.iodata_to_binary()
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_archive_page(plan, title, posts, language, kind) do
|
defp render_archive_page(plan, title, posts, language, kind, pagination) do
|
||||||
fallback = fn ->
|
fallback = fn ->
|
||||||
items =
|
items =
|
||||||
posts
|
posts
|
||||||
@@ -460,14 +550,23 @@ defmodule BDS.Generation do
|
|||||||
language,
|
language,
|
||||||
title,
|
title,
|
||||||
Enum.map(posts, fn post ->
|
Enum.map(posts, fn post ->
|
||||||
%{title: post.title, href: "#", excerpt: post.excerpt, content: nil}
|
%{
|
||||||
|
id: post.id,
|
||||||
|
slug: post.slug,
|
||||||
|
title: post.title,
|
||||||
|
href: "#",
|
||||||
|
excerpt: post.excerpt,
|
||||||
|
content: nil,
|
||||||
|
language: post.language
|
||||||
|
}
|
||||||
end),
|
end),
|
||||||
%{kind: kind, name: title},
|
%{kind: kind, name: title},
|
||||||
|
pagination,
|
||||||
fallback
|
fallback
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_date_archive_page(plan, label, posts, language) do
|
defp render_date_archive_page(plan, label, posts, language, pagination) do
|
||||||
fallback = fn ->
|
fallback = fn ->
|
||||||
items =
|
items =
|
||||||
posts
|
posts
|
||||||
@@ -491,21 +590,37 @@ defmodule BDS.Generation do
|
|||||||
language,
|
language,
|
||||||
label,
|
label,
|
||||||
Enum.map(posts, fn post ->
|
Enum.map(posts, fn post ->
|
||||||
%{title: post.title, href: "#", excerpt: post.excerpt, content: nil}
|
%{
|
||||||
|
id: post.id,
|
||||||
|
slug: post.slug,
|
||||||
|
title: post.title,
|
||||||
|
href: "#",
|
||||||
|
excerpt: post.excerpt,
|
||||||
|
content: nil,
|
||||||
|
language: post.language
|
||||||
|
}
|
||||||
end),
|
end),
|
||||||
%{kind: "date", name: label},
|
%{kind: "date", name: label},
|
||||||
|
pagination,
|
||||||
fallback
|
fallback
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_body(_project_id, _file_path, inline_content) when is_binary(inline_content), do: inline_content
|
defp load_body(_project_id, _file_path, inline_content) when is_binary(inline_content),
|
||||||
|
do: inline_content
|
||||||
|
|
||||||
defp load_body(project_id, file_path, _inline_content) do
|
defp load_body(project_id, file_path, _inline_content) do
|
||||||
case file_path do
|
case file_path do
|
||||||
nil -> ""
|
nil ->
|
||||||
"" -> ""
|
""
|
||||||
|
|
||||||
|
"" ->
|
||||||
|
""
|
||||||
|
|
||||||
value ->
|
value ->
|
||||||
project_path = Path.expand(value, Projects.project_data_dir(Projects.get_project!(project_id)))
|
project_path =
|
||||||
|
Path.expand(value, Projects.project_data_dir(Projects.get_project!(project_id)))
|
||||||
|
|
||||||
case File.read(project_path) do
|
case File.read(project_path) do
|
||||||
{:ok, contents} -> parse_frontmatter_body(contents)
|
{:ok, contents} -> parse_frontmatter_body(contents)
|
||||||
{:error, _reason} -> ""
|
{:error, _reason} -> ""
|
||||||
@@ -529,7 +644,9 @@ defmodule BDS.Generation do
|
|||||||
|
|
||||||
defp month_key(created_at) do
|
defp month_key(created_at) do
|
||||||
datetime = DateTime.from_unix!(created_at)
|
datetime = DateTime.from_unix!(created_at)
|
||||||
{Integer.to_string(datetime.year), Integer.to_string(datetime.month) |> String.pad_leading(2, "0")}
|
|
||||||
|
{Integer.to_string(datetime.year),
|
||||||
|
Integer.to_string(datetime.month) |> String.pad_leading(2, "0")}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp build_list_posts(base_url, posts, language_prefix) do
|
defp build_list_posts(base_url, posts, language_prefix) do
|
||||||
@@ -552,25 +669,46 @@ defmodule BDS.Generation do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_list_output(%{project_id: project_id, language: main_language}, language, page_title, posts, archive_context, fallback)
|
defp render_list_output(
|
||||||
|
%{project_id: project_id, language: main_language},
|
||||||
|
language,
|
||||||
|
page_title,
|
||||||
|
posts,
|
||||||
|
archive_context,
|
||||||
|
pagination,
|
||||||
|
fallback
|
||||||
|
)
|
||||||
when is_binary(project_id) do
|
when is_binary(project_id) do
|
||||||
case Rendering.render_list_page(project_id, %{
|
case Rendering.render_list_page(project_id, %{
|
||||||
language: language,
|
language: language,
|
||||||
language_prefix: language_prefix(language, main_language),
|
language_prefix: language_prefix(language, main_language),
|
||||||
page_title: page_title,
|
page_title: page_title,
|
||||||
posts: posts,
|
posts: posts,
|
||||||
archive_context: archive_context
|
archive_context: archive_context,
|
||||||
|
pagination: pagination
|
||||||
}) do
|
}) do
|
||||||
{:ok, rendered} -> rendered
|
{:ok, rendered} -> rendered
|
||||||
{:error, _reason} -> fallback.()
|
{:error, _reason} -> fallback.()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp render_list_output(_plan, _language, _page_title, _posts, _archive_context, fallback), do: fallback.()
|
defp render_list_output(
|
||||||
|
_plan,
|
||||||
|
_language,
|
||||||
|
_page_title,
|
||||||
|
_posts,
|
||||||
|
_archive_context,
|
||||||
|
_pagination,
|
||||||
|
fallback
|
||||||
|
),
|
||||||
|
do: fallback.()
|
||||||
|
|
||||||
defp render_not_found_output(%{project_id: project_id, language: main_language}, language)
|
defp render_not_found_output(%{project_id: project_id, language: main_language}, language)
|
||||||
when is_binary(project_id) do
|
when is_binary(project_id) do
|
||||||
case Rendering.render_not_found_page(project_id, %{language: language, language_prefix: language_prefix(language, main_language)}) do
|
case Rendering.render_not_found_page(project_id, %{
|
||||||
|
language: language,
|
||||||
|
language_prefix: language_prefix(language, main_language)
|
||||||
|
}) do
|
||||||
{:ok, rendered} -> rendered
|
{:ok, rendered} -> rendered
|
||||||
{:error, _reason} -> render_not_found_page(language)
|
{:error, _reason} -> render_not_found_page(language)
|
||||||
end
|
end
|
||||||
@@ -582,6 +720,25 @@ defmodule BDS.Generation do
|
|||||||
defp language_prefix(nil, _main_language), do: ""
|
defp language_prefix(nil, _main_language), do: ""
|
||||||
defp language_prefix(language, _main_language), do: "/#{language}"
|
defp language_prefix(language, _main_language), do: "/#{language}"
|
||||||
|
|
||||||
|
defp pagination_for_posts(posts) do
|
||||||
|
%{
|
||||||
|
current_page: 1,
|
||||||
|
total_pages: 1,
|
||||||
|
total_items: length(posts),
|
||||||
|
items_per_page: length(posts),
|
||||||
|
has_prev_page: false,
|
||||||
|
prev_page_href: "",
|
||||||
|
has_next_page: false,
|
||||||
|
next_page_href: ""
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp archive_href(language, segments, page_number) do
|
||||||
|
archive_path(language, segments, page_number)
|
||||||
|
|> String.trim_trailing("index.html")
|
||||||
|
|> then(&("/" <> String.trim_leading(&1, "/")))
|
||||||
|
end
|
||||||
|
|
||||||
defp url_for_output(nil, relative_path), do: "/" <> String.trim_leading(relative_path, "/")
|
defp url_for_output(nil, relative_path), do: "/" <> String.trim_leading(relative_path, "/")
|
||||||
|
|
||||||
defp url_for_output(base_url, relative_path) do
|
defp url_for_output(base_url, relative_path) do
|
||||||
|
|||||||
@@ -53,7 +53,8 @@ defmodule BDS.Maintenance do
|
|||||||
defp post_diff_reports(project_id, project) do
|
defp post_diff_reports(project_id, project) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from post in Post,
|
from post in Post,
|
||||||
where: post.project_id == ^project_id and not is_nil(post.file_path) and post.file_path != ""
|
where:
|
||||||
|
post.project_id == ^project_id and not is_nil(post.file_path) and post.file_path != ""
|
||||||
)
|
)
|
||||||
|> Enum.flat_map(fn post ->
|
|> Enum.flat_map(fn post ->
|
||||||
case read_frontmatter_document(project, post.file_path) do
|
case read_frontmatter_document(project, post.file_path) do
|
||||||
@@ -66,6 +67,9 @@ defmodule BDS.Maintenance do
|
|||||||
diff_field("language", post.language, Map.get(fields, "language")),
|
diff_field("language", post.language, Map.get(fields, "language")),
|
||||||
diff_field("status", post.status, Map.get(fields, "status")),
|
diff_field("status", post.status, Map.get(fields, "status")),
|
||||||
diff_field("template_slug", post.template_slug, Map.get(fields, "template_slug")),
|
diff_field("template_slug", post.template_slug, Map.get(fields, "template_slug")),
|
||||||
|
diff_field("created_at", post.created_at, Map.get(fields, "created_at")),
|
||||||
|
diff_field("updated_at", post.updated_at, Map.get(fields, "updated_at")),
|
||||||
|
diff_field("published_at", post.published_at, Map.get(fields, "published_at")),
|
||||||
diff_field("tags", post.tags, Map.get(fields, "tags", [])),
|
diff_field("tags", post.tags, Map.get(fields, "tags", [])),
|
||||||
diff_field("categories", post.categories, Map.get(fields, "categories", []))
|
diff_field("categories", post.categories, Map.get(fields, "categories", []))
|
||||||
]
|
]
|
||||||
@@ -86,7 +90,9 @@ defmodule BDS.Maintenance do
|
|||||||
defp media_diff_reports(project_id, project) do
|
defp media_diff_reports(project_id, project) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from media in Media,
|
from media in Media,
|
||||||
where: media.project_id == ^project_id and not is_nil(media.sidecar_path) and media.sidecar_path != ""
|
where:
|
||||||
|
media.project_id == ^project_id and not is_nil(media.sidecar_path) and
|
||||||
|
media.sidecar_path != ""
|
||||||
)
|
)
|
||||||
|> Enum.flat_map(fn media ->
|
|> Enum.flat_map(fn media ->
|
||||||
case read_sidecar_document(project, media.sidecar_path) do
|
case read_sidecar_document(project, media.sidecar_path) do
|
||||||
@@ -98,6 +104,8 @@ defmodule BDS.Maintenance do
|
|||||||
diff_field("caption", media.caption, Map.get(fields, "caption")),
|
diff_field("caption", media.caption, Map.get(fields, "caption")),
|
||||||
diff_field("author", media.author, Map.get(fields, "author")),
|
diff_field("author", media.author, Map.get(fields, "author")),
|
||||||
diff_field("language", media.language, Map.get(fields, "language")),
|
diff_field("language", media.language, Map.get(fields, "language")),
|
||||||
|
diff_field("created_at", media.created_at, Map.get(fields, "created_at")),
|
||||||
|
diff_field("updated_at", media.updated_at, Map.get(fields, "updated_at")),
|
||||||
diff_field("tags", media.tags, Map.get(fields, "tags", []))
|
diff_field("tags", media.tags, Map.get(fields, "tags", []))
|
||||||
]
|
]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
@@ -117,7 +125,9 @@ defmodule BDS.Maintenance do
|
|||||||
defp post_translation_diff_reports(project_id, project) do
|
defp post_translation_diff_reports(project_id, project) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from translation in PostTranslation,
|
from translation in PostTranslation,
|
||||||
where: translation.project_id == ^project_id and not is_nil(translation.file_path) and translation.file_path != ""
|
where:
|
||||||
|
translation.project_id == ^project_id and not is_nil(translation.file_path) and
|
||||||
|
translation.file_path != ""
|
||||||
)
|
)
|
||||||
|> Enum.flat_map(fn translation ->
|
|> Enum.flat_map(fn translation ->
|
||||||
case read_frontmatter_document(project, translation.file_path) do
|
case read_frontmatter_document(project, translation.file_path) do
|
||||||
@@ -128,14 +138,31 @@ defmodule BDS.Maintenance do
|
|||||||
diff_field("excerpt", translation.excerpt, Map.get(fields, "excerpt")),
|
diff_field("excerpt", translation.excerpt, Map.get(fields, "excerpt")),
|
||||||
diff_field("language", translation.language, Map.get(fields, "language")),
|
diff_field("language", translation.language, Map.get(fields, "language")),
|
||||||
diff_field("status", translation.status, Map.get(fields, "status")),
|
diff_field("status", translation.status, Map.get(fields, "status")),
|
||||||
diff_field("translation_for", translation.translation_for, Map.get(fields, "translation_for"))
|
diff_field(
|
||||||
|
"translation_for",
|
||||||
|
translation.translation_for,
|
||||||
|
Map.get(fields, "translation_for")
|
||||||
|
),
|
||||||
|
diff_field("created_at", translation.created_at, Map.get(fields, "created_at")),
|
||||||
|
diff_field("updated_at", translation.updated_at, Map.get(fields, "updated_at")),
|
||||||
|
diff_field(
|
||||||
|
"published_at",
|
||||||
|
translation.published_at,
|
||||||
|
Map.get(fields, "published_at")
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
if differences == [] do
|
if differences == [] do
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
[%{entity_type: "post_translation", entity_id: translation.id, differences: differences}]
|
[
|
||||||
|
%{
|
||||||
|
entity_type: "post_translation",
|
||||||
|
entity_id: translation.id,
|
||||||
|
differences: differences
|
||||||
|
}
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
{:error, _reason} ->
|
{:error, _reason} ->
|
||||||
@@ -157,14 +184,24 @@ defmodule BDS.Maintenance do
|
|||||||
diff_field("alt", translation.alt, Map.get(fields, "alt")),
|
diff_field("alt", translation.alt, Map.get(fields, "alt")),
|
||||||
diff_field("caption", translation.caption, Map.get(fields, "caption")),
|
diff_field("caption", translation.caption, Map.get(fields, "caption")),
|
||||||
diff_field("language", translation.language, Map.get(fields, "language")),
|
diff_field("language", translation.language, Map.get(fields, "language")),
|
||||||
diff_field("translation_for", translation.translation_for, Map.get(fields, "translation_for"))
|
diff_field(
|
||||||
|
"translation_for",
|
||||||
|
translation.translation_for,
|
||||||
|
Map.get(fields, "translation_for")
|
||||||
|
)
|
||||||
]
|
]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
if differences == [] do
|
if differences == [] do
|
||||||
[]
|
[]
|
||||||
else
|
else
|
||||||
[%{entity_type: "media_translation", entity_id: translation.id, differences: differences}]
|
[
|
||||||
|
%{
|
||||||
|
entity_type: "media_translation",
|
||||||
|
entity_id: translation.id,
|
||||||
|
differences: differences
|
||||||
|
}
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
_ ->
|
_ ->
|
||||||
@@ -176,7 +213,9 @@ defmodule BDS.Maintenance do
|
|||||||
defp script_diff_reports(project_id, project) do
|
defp script_diff_reports(project_id, project) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from script in Script,
|
from script in Script,
|
||||||
where: script.project_id == ^project_id and not is_nil(script.file_path) and script.file_path != ""
|
where:
|
||||||
|
script.project_id == ^project_id and not is_nil(script.file_path) and
|
||||||
|
script.file_path != ""
|
||||||
)
|
)
|
||||||
|> Enum.flat_map(fn script ->
|
|> Enum.flat_map(fn script ->
|
||||||
case read_frontmatter_document(project, script.file_path) do
|
case read_frontmatter_document(project, script.file_path) do
|
||||||
@@ -185,7 +224,9 @@ defmodule BDS.Maintenance do
|
|||||||
[
|
[
|
||||||
diff_field("title", script.title, Map.get(fields, "title")),
|
diff_field("title", script.title, Map.get(fields, "title")),
|
||||||
diff_field("entrypoint", script.entrypoint, Map.get(fields, "entrypoint")),
|
diff_field("entrypoint", script.entrypoint, Map.get(fields, "entrypoint")),
|
||||||
diff_field("enabled", script.enabled, Map.get(fields, "enabled"))
|
diff_field("enabled", script.enabled, Map.get(fields, "enabled")),
|
||||||
|
diff_field("created_at", script.created_at, Map.get(fields, "created_at")),
|
||||||
|
diff_field("updated_at", script.updated_at, Map.get(fields, "updated_at"))
|
||||||
]
|
]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
@@ -204,7 +245,9 @@ defmodule BDS.Maintenance do
|
|||||||
defp template_diff_reports(project_id, project) do
|
defp template_diff_reports(project_id, project) do
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from template in Template,
|
from template in Template,
|
||||||
where: template.project_id == ^project_id and not is_nil(template.file_path) and template.file_path != ""
|
where:
|
||||||
|
template.project_id == ^project_id and not is_nil(template.file_path) and
|
||||||
|
template.file_path != ""
|
||||||
)
|
)
|
||||||
|> Enum.flat_map(fn template ->
|
|> Enum.flat_map(fn template ->
|
||||||
case read_frontmatter_document(project, template.file_path) do
|
case read_frontmatter_document(project, template.file_path) do
|
||||||
@@ -212,7 +255,9 @@ defmodule BDS.Maintenance do
|
|||||||
differences =
|
differences =
|
||||||
[
|
[
|
||||||
diff_field("title", template.title, Map.get(fields, "title")),
|
diff_field("title", template.title, Map.get(fields, "title")),
|
||||||
diff_field("enabled", template.enabled, Map.get(fields, "enabled"))
|
diff_field("enabled", template.enabled, Map.get(fields, "enabled")),
|
||||||
|
diff_field("created_at", template.created_at, Map.get(fields, "created_at")),
|
||||||
|
diff_field("updated_at", template.updated_at, Map.get(fields, "updated_at"))
|
||||||
]
|
]
|
||||||
|> Enum.reject(&is_nil/1)
|
|> Enum.reject(&is_nil/1)
|
||||||
|
|
||||||
@@ -229,12 +274,44 @@ defmodule BDS.Maintenance do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp orphan_reports(project_id, project) do
|
defp orphan_reports(project_id, project) do
|
||||||
post_paths = MapSet.new(Repo.all(from post in Post, where: post.project_id == ^project_id, select: post.file_path))
|
post_paths =
|
||||||
media_paths = MapSet.new(Repo.all(from media in Media, where: media.project_id == ^project_id, select: media.sidecar_path))
|
MapSet.new(
|
||||||
post_translation_paths = MapSet.new(Repo.all(from translation in PostTranslation, where: translation.project_id == ^project_id, select: translation.file_path))
|
Repo.all(from post in Post, where: post.project_id == ^project_id, select: post.file_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
media_paths =
|
||||||
|
MapSet.new(
|
||||||
|
Repo.all(
|
||||||
|
from media in Media, where: media.project_id == ^project_id, select: media.sidecar_path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
post_translation_paths =
|
||||||
|
MapSet.new(
|
||||||
|
Repo.all(
|
||||||
|
from translation in PostTranslation,
|
||||||
|
where: translation.project_id == ^project_id,
|
||||||
|
select: translation.file_path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
media_translation_paths = MapSet.new(media_translation_sidecar_paths(project_id))
|
media_translation_paths = MapSet.new(media_translation_sidecar_paths(project_id))
|
||||||
script_paths = MapSet.new(Repo.all(from script in Script, where: script.project_id == ^project_id, select: script.file_path))
|
|
||||||
template_paths = MapSet.new(Repo.all(from template in Template, where: template.project_id == ^project_id, select: template.file_path))
|
script_paths =
|
||||||
|
MapSet.new(
|
||||||
|
Repo.all(
|
||||||
|
from script in Script, where: script.project_id == ^project_id, select: script.file_path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
template_paths =
|
||||||
|
MapSet.new(
|
||||||
|
Repo.all(
|
||||||
|
from template in Template,
|
||||||
|
where: template.project_id == ^project_id,
|
||||||
|
select: template.file_path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
post_orphans =
|
post_orphans =
|
||||||
project
|
project
|
||||||
@@ -276,7 +353,9 @@ defmodule BDS.Maintenance do
|
|||||||
|> Enum.map(&Path.relative_to(&1, Projects.project_data_dir(project)))
|
|> Enum.map(&Path.relative_to(&1, Projects.project_data_dir(project)))
|
||||||
|> Enum.reject(&MapSet.member?(template_paths, &1))
|
|> Enum.reject(&MapSet.member?(template_paths, &1))
|
||||||
|
|
||||||
(post_orphans ++ post_translation_orphans ++ media_orphans ++ media_translation_orphans ++ script_orphans ++ template_orphans)
|
(post_orphans ++
|
||||||
|
post_translation_orphans ++
|
||||||
|
media_orphans ++ media_translation_orphans ++ script_orphans ++ template_orphans)
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
|> Enum.map(&%{file_path: &1})
|
|> Enum.map(&%{file_path: &1})
|
||||||
end
|
end
|
||||||
@@ -297,7 +376,10 @@ defmodule BDS.Maintenance do
|
|||||||
defp stringify_value(value) when is_boolean(value), do: to_string(value)
|
defp stringify_value(value) when is_boolean(value), do: to_string(value)
|
||||||
defp stringify_value(value) when is_integer(value), do: Integer.to_string(value)
|
defp stringify_value(value) when is_integer(value), do: Integer.to_string(value)
|
||||||
defp stringify_value(value) when is_binary(value), do: value
|
defp stringify_value(value) when is_binary(value), do: value
|
||||||
defp stringify_value(value) when is_list(value), do: Enum.map_join(value, ",", &stringify_value/1)
|
|
||||||
|
defp stringify_value(value) when is_list(value),
|
||||||
|
do: Enum.map_join(value, ",", &stringify_value/1)
|
||||||
|
|
||||||
defp stringify_value(value), do: to_string(value)
|
defp stringify_value(value), do: to_string(value)
|
||||||
|
|
||||||
defp read_frontmatter_document(project, relative_path) do
|
defp read_frontmatter_document(project, relative_path) do
|
||||||
@@ -345,7 +427,11 @@ defmodule BDS.Maintenance do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp media_translation_sidecar_path(project_id, translation) do
|
defp media_translation_sidecar_path(project_id, translation) do
|
||||||
case Repo.one(from media in Media, where: media.project_id == ^project_id and media.id == ^translation.translation_for, select: media.file_path) do
|
case Repo.one(
|
||||||
|
from media in Media,
|
||||||
|
where: media.project_id == ^project_id and media.id == ^translation.translation_for,
|
||||||
|
select: media.file_path
|
||||||
|
) do
|
||||||
nil -> nil
|
nil -> nil
|
||||||
file_path -> "#{file_path}.#{translation.language}.meta"
|
file_path -> "#{file_path}.#{translation.language}.meta"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ defmodule BDS.Media do
|
|||||||
{:error, :not_found}
|
{:error, :not_found}
|
||||||
|
|
||||||
media ->
|
media ->
|
||||||
updates = %{}
|
updates =
|
||||||
|
%{}
|
||||||
|> maybe_put(:title, attr(attrs, :title))
|
|> maybe_put(:title, attr(attrs, :title))
|
||||||
|> maybe_put(:alt, attr(attrs, :alt))
|
|> maybe_put(:alt, attr(attrs, :alt))
|
||||||
|> maybe_put(:caption, attr(attrs, :caption))
|
|> maybe_put(:caption, attr(attrs, :caption))
|
||||||
@@ -104,14 +105,21 @@ defmodule BDS.Media do
|
|||||||
{:error, :not_found}
|
{:error, :not_found}
|
||||||
|
|
||||||
media ->
|
media ->
|
||||||
translations = Repo.all(from translation in Translation, where: translation.translation_for == ^media.id)
|
translations =
|
||||||
|
Repo.all(
|
||||||
|
from translation in Translation, where: translation.translation_for == ^media.id
|
||||||
|
)
|
||||||
|
|
||||||
delete_file_if_present(media.project_id, media.file_path)
|
delete_file_if_present(media.project_id, media.file_path)
|
||||||
delete_file_if_present(media.project_id, media.sidecar_path)
|
delete_file_if_present(media.project_id, media.sidecar_path)
|
||||||
delete_thumbnail_files(media.project_id, media)
|
delete_thumbnail_files(media.project_id, media)
|
||||||
|
|
||||||
Enum.each(translations, fn translation ->
|
Enum.each(translations, fn translation ->
|
||||||
delete_file_if_present(media.project_id, translation_sidecar_path(media, translation.language))
|
delete_file_if_present(
|
||||||
|
media.project_id,
|
||||||
|
translation_sidecar_path(media, translation.language)
|
||||||
|
)
|
||||||
|
|
||||||
Repo.delete!(translation)
|
Repo.delete!(translation)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
@@ -243,7 +251,9 @@ defmodule BDS.Media do
|
|||||||
updated_at: Map.get(fields, "updated_at", now)
|
updated_at: Map.get(fields, "updated_at", now)
|
||||||
}
|
}
|
||||||
|
|
||||||
media = Repo.get(Media, attrs.id) || Repo.get_by(Media, project_id: project.id, file_path: relative_file_path) || %Media{}
|
media =
|
||||||
|
Repo.get(Media, attrs.id) ||
|
||||||
|
Repo.get_by(Media, project_id: project.id, file_path: relative_file_path) || %Media{}
|
||||||
|
|
||||||
media
|
media
|
||||||
|> Media.changeset(attrs)
|
|> Media.changeset(attrs)
|
||||||
@@ -278,7 +288,12 @@ defmodule BDS.Media do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp write_translation_sidecar(project, media, translation) do
|
defp write_translation_sidecar(project, media, translation) do
|
||||||
path = Path.join(Projects.project_data_dir(project), translation_sidecar_path(media, translation.language))
|
path =
|
||||||
|
Path.join(
|
||||||
|
Projects.project_data_dir(project),
|
||||||
|
translation_sidecar_path(media, translation.language)
|
||||||
|
)
|
||||||
|
|
||||||
:ok = File.mkdir_p(Path.dirname(path))
|
:ok = File.mkdir_p(Path.dirname(path))
|
||||||
|
|
||||||
atomic_write(
|
atomic_write(
|
||||||
|
|||||||
@@ -58,7 +58,18 @@ defmodule BDS.Media.Media do
|
|||||||
],
|
],
|
||||||
empty_values: [nil]
|
empty_values: [nil]
|
||||||
)
|
)
|
||||||
|> validate_required([:id, :project_id, :filename, :original_name, :mime_type, :size, :file_path, :sidecar_path, :created_at, :updated_at])
|
|> validate_required([
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:filename,
|
||||||
|
:original_name,
|
||||||
|
:mime_type,
|
||||||
|
:size,
|
||||||
|
:file_path,
|
||||||
|
:sidecar_path,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
])
|
||||||
|> assoc_constraint(:project)
|
|> assoc_constraint(:project)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ defmodule BDS.Media.Translation do
|
|||||||
@foreign_key_type :string
|
@foreign_key_type :string
|
||||||
|
|
||||||
schema "media_translations" do
|
schema "media_translations" do
|
||||||
belongs_to :media, BDS.Media.Media, foreign_key: :translation_for, references: :id, type: :string
|
belongs_to :media, BDS.Media.Media,
|
||||||
|
foreign_key: :translation_for,
|
||||||
|
references: :id,
|
||||||
|
type: :string
|
||||||
|
|
||||||
field :project_id, :string
|
field :project_id, :string
|
||||||
field :language, :string
|
field :language, :string
|
||||||
field :title, :string
|
field :title, :string
|
||||||
@@ -20,10 +24,29 @@ defmodule BDS.Media.Translation do
|
|||||||
|
|
||||||
def changeset(translation, attrs) do
|
def changeset(translation, attrs) do
|
||||||
translation
|
translation
|
||||||
|> cast(attrs, [:id, :project_id, :translation_for, :language, :title, :alt, :caption, :created_at, :updated_at],
|
|> cast(
|
||||||
|
attrs,
|
||||||
|
[
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:translation_for,
|
||||||
|
:language,
|
||||||
|
:title,
|
||||||
|
:alt,
|
||||||
|
:caption,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
],
|
||||||
empty_values: [nil]
|
empty_values: [nil]
|
||||||
)
|
)
|
||||||
|> validate_required([:id, :project_id, :translation_for, :language, :created_at, :updated_at])
|
|> validate_required([
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:translation_for,
|
||||||
|
:language,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
])
|
||||||
|> foreign_key_constraint(:translation_for)
|
|> foreign_key_constraint(:translation_for)
|
||||||
|> unique_constraint(:language, name: :media_translations_translation_language_idx)
|
|> unique_constraint(:language, name: :media_translations_translation_language_idx)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ defmodule BDS.Menu do
|
|||||||
alias BDS.Projects
|
alias BDS.Projects
|
||||||
|
|
||||||
Record.defrecord(:xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl"))
|
Record.defrecord(:xmlElement, Record.extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl"))
|
||||||
Record.defrecord(:xmlAttribute, Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl"))
|
|
||||||
|
Record.defrecord(
|
||||||
|
:xmlAttribute,
|
||||||
|
Record.extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl")
|
||||||
|
)
|
||||||
|
|
||||||
@valid_kinds [:page, :submenu, :category_archive, :home]
|
@valid_kinds [:page, :submenu, :category_archive, :home]
|
||||||
|
|
||||||
@@ -187,7 +191,7 @@ defmodule BDS.Menu do
|
|||||||
|> String.replace("&", "&")
|
|> String.replace("&", "&")
|
||||||
|> String.replace("<", "<")
|
|> String.replace("<", "<")
|
||||||
|> String.replace(">", ">")
|
|> String.replace(">", ">")
|
||||||
|> String.replace(~s(") , """)
|
|> String.replace(~s("), """)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp attr(attrs, key) do
|
defp attr(attrs, key) do
|
||||||
|
|||||||
@@ -21,13 +21,28 @@ defmodule BDS.Metadata do
|
|||||||
|
|
||||||
project_metadata =
|
project_metadata =
|
||||||
state
|
state
|
||||||
|> Map.take([:name, :description, :public_url, :main_language, :default_author, :max_posts_per_page, :blogmark_category, :pico_theme, :semantic_similarity_enabled, :blog_languages])
|
|> Map.take([
|
||||||
|
:name,
|
||||||
|
:description,
|
||||||
|
:public_url,
|
||||||
|
:main_language,
|
||||||
|
:default_author,
|
||||||
|
:max_posts_per_page,
|
||||||
|
:blogmark_category,
|
||||||
|
:pico_theme,
|
||||||
|
:semantic_similarity_enabled,
|
||||||
|
:blog_languages
|
||||||
|
])
|
||||||
|> Map.merge(normalize_project_metadata_attrs(attrs, project))
|
|> Map.merge(normalize_project_metadata_attrs(attrs, project))
|
||||||
|
|
||||||
Repo.transaction(fn ->
|
Repo.transaction(fn ->
|
||||||
updated_project =
|
updated_project =
|
||||||
project
|
project
|
||||||
|> Project.changeset(%{name: project_metadata.name, description: project_metadata.description, updated_at: now})
|
|> Project.changeset(%{
|
||||||
|
name: project_metadata.name,
|
||||||
|
description: project_metadata.description,
|
||||||
|
updated_at: now
|
||||||
|
})
|
||||||
|> Repo.update!()
|
|> Repo.update!()
|
||||||
|
|
||||||
persist_setting(project_id, "project", stringify_project_metadata(project_metadata), now)
|
persist_setting(project_id, "project", stringify_project_metadata(project_metadata), now)
|
||||||
@@ -89,8 +104,13 @@ defmodule BDS.Metadata do
|
|||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
now = System.system_time(:second)
|
now = System.system_time(:second)
|
||||||
|
|
||||||
project_metadata_from_files = read_json(project, "project.json") || stringify_project_metadata(default_project_metadata(project))
|
project_metadata_from_files =
|
||||||
categories_from_files = read_json(project, "categories.json") || %{"categories" => @default_categories}
|
read_json(project, "project.json") ||
|
||||||
|
stringify_project_metadata(default_project_metadata(project))
|
||||||
|
|
||||||
|
categories_from_files =
|
||||||
|
read_json(project, "categories.json") || %{"categories" => @default_categories}
|
||||||
|
|
||||||
category_meta_from_files = read_json(project, "category-meta.json") || %{"categories" => %{}}
|
category_meta_from_files = read_json(project, "category-meta.json") || %{"categories" => %{}}
|
||||||
publishing_from_files = read_json(project, "publishing.json") || %{"ssh_mode" => "scp"}
|
publishing_from_files = read_json(project, "publishing.json") || %{"ssh_mode" => "scp"}
|
||||||
|
|
||||||
@@ -125,8 +145,14 @@ defmodule BDS.Metadata do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp load_state(project) do
|
defp load_state(project) do
|
||||||
project_metadata = load_setting(project.id, "project") || stringify_project_metadata(default_project_metadata(project))
|
project_metadata =
|
||||||
categories = (load_setting(project.id, "categories") || %{"categories" => @default_categories})["categories"]
|
load_setting(project.id, "project") ||
|
||||||
|
stringify_project_metadata(default_project_metadata(project))
|
||||||
|
|
||||||
|
categories =
|
||||||
|
(load_setting(project.id, "categories") || %{"categories" => @default_categories})[
|
||||||
|
"categories"
|
||||||
|
]
|
||||||
|
|
||||||
category_settings =
|
category_settings =
|
||||||
(load_setting(project.id, "category_meta") || %{"categories" => %{}})["categories"]
|
(load_setting(project.id, "category_meta") || %{"categories" => %{}})["categories"]
|
||||||
@@ -139,10 +165,12 @@ defmodule BDS.Metadata do
|
|||||||
public_url: Map.get(project_metadata, "public_url"),
|
public_url: Map.get(project_metadata, "public_url"),
|
||||||
main_language: Map.get(project_metadata, "main_language"),
|
main_language: Map.get(project_metadata, "main_language"),
|
||||||
default_author: Map.get(project_metadata, "default_author"),
|
default_author: Map.get(project_metadata, "default_author"),
|
||||||
max_posts_per_page: Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
max_posts_per_page:
|
||||||
|
Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
||||||
blogmark_category: Map.get(project_metadata, "blogmark_category"),
|
blogmark_category: Map.get(project_metadata, "blogmark_category"),
|
||||||
pico_theme: Map.get(project_metadata, "pico_theme"),
|
pico_theme: Map.get(project_metadata, "pico_theme"),
|
||||||
semantic_similarity_enabled: Map.get(project_metadata, "semantic_similarity_enabled", false),
|
semantic_similarity_enabled:
|
||||||
|
Map.get(project_metadata, "semantic_similarity_enabled", false),
|
||||||
blog_languages: Map.get(project_metadata, "blog_languages", []),
|
blog_languages: Map.get(project_metadata, "blog_languages", []),
|
||||||
categories: categories,
|
categories: categories,
|
||||||
category_settings: category_settings,
|
category_settings: category_settings,
|
||||||
@@ -182,10 +210,13 @@ defmodule BDS.Metadata do
|
|||||||
|
|
||||||
defp normalize_category_settings(settings) do
|
defp normalize_category_settings(settings) do
|
||||||
%{
|
%{
|
||||||
"render_in_lists" => Map.get(settings, :render_in_lists, Map.get(settings, "render_in_lists", true)),
|
"render_in_lists" =>
|
||||||
|
Map.get(settings, :render_in_lists, Map.get(settings, "render_in_lists", true)),
|
||||||
"show_title" => Map.get(settings, :show_title, Map.get(settings, "show_title", true)),
|
"show_title" => Map.get(settings, :show_title, Map.get(settings, "show_title", true)),
|
||||||
"post_template_slug" => Map.get(settings, :post_template_slug, Map.get(settings, "post_template_slug")),
|
"post_template_slug" =>
|
||||||
"list_template_slug" => Map.get(settings, :list_template_slug, Map.get(settings, "list_template_slug"))
|
Map.get(settings, :post_template_slug, Map.get(settings, "post_template_slug")),
|
||||||
|
"list_template_slug" =>
|
||||||
|
Map.get(settings, :list_template_slug, Map.get(settings, "list_template_slug"))
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -220,7 +251,8 @@ defmodule BDS.Metadata do
|
|||||||
write_publishing_json(project, state.publishing_preferences)
|
write_publishing_json(project, state.publishing_preferences)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp write_project_json(project, project_json), do: write_json(project, "project.json", project_json)
|
defp write_project_json(project, project_json),
|
||||||
|
do: write_json(project, "project.json", project_json)
|
||||||
|
|
||||||
defp write_categories_json(project, categories) do
|
defp write_categories_json(project, categories) do
|
||||||
write_json(project, "categories.json", %{"categories" => Enum.sort(categories)})
|
write_json(project, "categories.json", %{"categories" => Enum.sort(categories)})
|
||||||
|
|||||||
101
lib/bds/posts.ex
101
lib/bds/posts.ex
@@ -37,7 +37,7 @@ defmodule BDS.Posts do
|
|||||||
categories: attr(attrs, :categories) || [],
|
categories: attr(attrs, :categories) || [],
|
||||||
template_slug: attr(attrs, :template_slug),
|
template_slug: attr(attrs, :template_slug),
|
||||||
language: attr(attrs, :language),
|
language: attr(attrs, :language),
|
||||||
do_not_translate: false,
|
do_not_translate: attr(attrs, :do_not_translate) || false,
|
||||||
published_title: nil,
|
published_title: nil,
|
||||||
published_content: nil,
|
published_content: nil,
|
||||||
published_tags: nil,
|
published_tags: nil,
|
||||||
@@ -63,6 +63,7 @@ defmodule BDS.Posts do
|
|||||||
post ->
|
post ->
|
||||||
with :ok <- validate_slug_change(post, attrs) do
|
with :ok <- validate_slug_change(post, attrs) do
|
||||||
now = System.system_time(:second)
|
now = System.system_time(:second)
|
||||||
|
|
||||||
updates =
|
updates =
|
||||||
attrs
|
attrs
|
||||||
|> normalize_updates(post)
|
|> normalize_updates(post)
|
||||||
@@ -100,7 +101,12 @@ defmodule BDS.Posts do
|
|||||||
body = publishable_post_body(post, full_path, project)
|
body = publishable_post_body(post, full_path, project)
|
||||||
|
|
||||||
:ok = File.mkdir_p(Path.dirname(full_path))
|
:ok = File.mkdir_p(Path.dirname(full_path))
|
||||||
:ok = File.write(full_path, serialize_post_file(%{post | updated_at: updated_at, content: body}, published_at))
|
|
||||||
|
:ok =
|
||||||
|
File.write(
|
||||||
|
full_path,
|
||||||
|
serialize_post_file(%{post | updated_at: updated_at, content: body}, published_at)
|
||||||
|
)
|
||||||
|
|
||||||
post
|
post
|
||||||
|> Post.changeset(%{
|
|> Post.changeset(%{
|
||||||
@@ -197,16 +203,21 @@ defmodule BDS.Posts do
|
|||||||
{:error,
|
{:error,
|
||||||
post
|
post
|
||||||
|> Post.changeset(%{})
|
|> Post.changeset(%{})
|
||||||
|> Ecto.Changeset.add_error(:do_not_translate, "cannot add translations when do_not_translate is true")}
|
|> Ecto.Changeset.add_error(
|
||||||
|
:do_not_translate,
|
||||||
|
"cannot add translations when do_not_translate is true"
|
||||||
|
)}
|
||||||
|
|
||||||
%Post{} = post ->
|
%Post{} = post ->
|
||||||
now = System.system_time(:second)
|
now = System.system_time(:second)
|
||||||
normalized_language = normalize_language(language)
|
normalized_language = normalize_language(language)
|
||||||
|
|
||||||
translation =
|
translation =
|
||||||
Repo.get_by(Translation, translation_for: post.id, language: normalized_language) || %Translation{}
|
Repo.get_by(Translation, translation_for: post.id, language: normalized_language) ||
|
||||||
|
%Translation{}
|
||||||
|
|
||||||
updates = normalize_translation_updates(post, translation, normalized_language, attrs, now)
|
updates =
|
||||||
|
normalize_translation_updates(post, translation, normalized_language, attrs, now)
|
||||||
|
|
||||||
translation
|
translation
|
||||||
|> Translation.changeset(updates)
|
|> Translation.changeset(updates)
|
||||||
@@ -253,7 +264,9 @@ defmodule BDS.Posts do
|
|||||||
where: post.project_id == ^project_id,
|
where: post.project_id == ^project_id,
|
||||||
select: {translation.translation_for, translation.language}
|
select: {translation.translation_for, translation.language}
|
||||||
)
|
)
|
||||||
|> Enum.group_by(fn {post_id, _language} -> post_id end, fn {_post_id, language} -> language end)
|
|> Enum.group_by(fn {post_id, _language} -> post_id end, fn {_post_id, language} ->
|
||||||
|
language
|
||||||
|
end)
|
||||||
|
|
||||||
required_languages =
|
required_languages =
|
||||||
metadata.blog_languages
|
metadata.blog_languages
|
||||||
@@ -268,7 +281,9 @@ defmodule BDS.Posts do
|
|||||||
available = Map.get(translation_languages, post.id, [])
|
available = Map.get(translation_languages, post.id, [])
|
||||||
|
|
||||||
cond do
|
cond do
|
||||||
post.do_not_translate -> []
|
post.do_not_translate ->
|
||||||
|
[]
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
required_languages
|
required_languages
|
||||||
|> Enum.reject(&(&1 in available))
|
|> Enum.reject(&(&1 in available))
|
||||||
@@ -299,7 +314,15 @@ defmodule BDS.Posts do
|
|||||||
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
|
full_path = Path.join(Projects.project_data_dir(project), post.file_path)
|
||||||
body = published_post_body(post, full_path)
|
body = published_post_body(post, full_path)
|
||||||
:ok = File.mkdir_p(Path.dirname(full_path))
|
:ok = File.mkdir_p(Path.dirname(full_path))
|
||||||
:ok = File.write(full_path, serialize_post_file(%{post | content: body}, post.published_at || System.system_time(:second)))
|
|
||||||
|
:ok =
|
||||||
|
File.write(
|
||||||
|
full_path,
|
||||||
|
serialize_post_file(
|
||||||
|
%{post | content: body},
|
||||||
|
post.published_at || System.system_time(:second)
|
||||||
|
)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
@@ -328,7 +351,8 @@ defmodule BDS.Posts do
|
|||||||
|> maybe_put(:published_excerpt, attr(attrs, :published_excerpt))
|
|> maybe_put(:published_excerpt, attr(attrs, :published_excerpt))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp validate_slug_change(%Post{published_at: published_at} = post, attrs) when not is_nil(published_at) do
|
defp validate_slug_change(%Post{published_at: published_at} = post, attrs)
|
||||||
|
when not is_nil(published_at) do
|
||||||
case attr(attrs, :slug) do
|
case attr(attrs, :slug) do
|
||||||
nil ->
|
nil ->
|
||||||
:ok
|
:ok
|
||||||
@@ -357,12 +381,25 @@ defmodule BDS.Posts do
|
|||||||
defp maybe_reopen_published_post(updates, _post), do: updates
|
defp maybe_reopen_published_post(updates, _post), do: updates
|
||||||
|
|
||||||
defp published_content_change?(updates, post) do
|
defp published_content_change?(updates, post) do
|
||||||
Enum.any?([:title, :excerpt, :content, :author, :language, :template_slug, :tags, :categories, :do_not_translate], fn field ->
|
Enum.any?(
|
||||||
|
[
|
||||||
|
:title,
|
||||||
|
:excerpt,
|
||||||
|
:content,
|
||||||
|
:author,
|
||||||
|
:language,
|
||||||
|
:template_slug,
|
||||||
|
:tags,
|
||||||
|
:categories,
|
||||||
|
:do_not_translate
|
||||||
|
],
|
||||||
|
fn field ->
|
||||||
case Map.fetch(updates, field) do
|
case Map.fetch(updates, field) do
|
||||||
{:ok, value} -> value != Map.get(post, field)
|
{:ok, value} -> value != Map.get(post, field)
|
||||||
:error -> false
|
:error -> false
|
||||||
end
|
end
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp unique_slug(project_id, base_slug) do
|
defp unique_slug(project_id, base_slug) do
|
||||||
@@ -386,7 +423,9 @@ defmodule BDS.Posts do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp slug_available?(project_id, slug) do
|
defp slug_available?(project_id, slug) do
|
||||||
not Repo.exists?(from post in Post, where: post.project_id == ^project_id and post.slug == ^slug)
|
not Repo.exists?(
|
||||||
|
from post in Post, where: post.project_id == ^project_id and post.slug == ^slug
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_put(map, _key, nil), do: map
|
defp maybe_put(map, _key, nil), do: map
|
||||||
@@ -409,7 +448,8 @@ defmodule BDS.Posts do
|
|||||||
Path.join(["posts", year, month, "#{slug}.md"])
|
Path.join(["posts", year, month, "#{slug}.md"])
|
||||||
end
|
end
|
||||||
|
|
||||||
defp publishable_post_body(%Post{content: content}, _full_path, _project) when is_binary(content), do: content
|
defp publishable_post_body(%Post{content: content}, _full_path, _project)
|
||||||
|
when is_binary(content), do: content
|
||||||
|
|
||||||
defp publishable_post_body(%Post{file_path: file_path} = post, full_path, project) do
|
defp publishable_post_body(%Post{file_path: file_path} = post, full_path, project) do
|
||||||
source_path =
|
source_path =
|
||||||
@@ -444,7 +484,8 @@ defmodule BDS.Posts do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp published_post_body(%Post{content: content}, _full_path) when is_binary(content), do: content
|
defp published_post_body(%Post{content: content}, _full_path) when is_binary(content),
|
||||||
|
do: content
|
||||||
|
|
||||||
defp published_post_body(_post, full_path) do
|
defp published_post_body(_post, full_path) do
|
||||||
case File.read(full_path) do
|
case File.read(full_path) do
|
||||||
@@ -512,7 +553,8 @@ defmodule BDS.Posts do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_post_file(%Post{project_id: _project_id, file_path: file_path}) when file_path in [nil, ""], do: :ok
|
defp delete_post_file(%Post{project_id: _project_id, file_path: file_path})
|
||||||
|
when file_path in [nil, ""], do: :ok
|
||||||
|
|
||||||
defp delete_post_file(%Post{} = post) do
|
defp delete_post_file(%Post{} = post) do
|
||||||
project = Projects.get_project!(post.project_id)
|
project = Projects.get_project!(post.project_id)
|
||||||
@@ -532,7 +574,8 @@ defmodule BDS.Posts do
|
|||||||
|> maybe_put(:excerpt, attr(attrs, :excerpt))
|
|> maybe_put(:excerpt, attr(attrs, :excerpt))
|
||||||
|> maybe_put(:content, attr(attrs, :content))
|
|> maybe_put(:content, attr(attrs, :content))
|
||||||
|
|
||||||
reopened? = translation.status == :published and translation_content_change?(translation, updates)
|
reopened? =
|
||||||
|
translation.status == :published and translation_content_change?(translation, updates)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: translation.id || Ecto.UUID.generate(),
|
id: translation.id || Ecto.UUID.generate(),
|
||||||
@@ -580,7 +623,15 @@ defmodule BDS.Posts do
|
|||||||
body = publishable_translation_body(translation, full_path)
|
body = publishable_translation_body(translation, full_path)
|
||||||
|
|
||||||
:ok = File.mkdir_p(Path.dirname(full_path))
|
:ok = File.mkdir_p(Path.dirname(full_path))
|
||||||
:ok = File.write(full_path, serialize_translation_file(%{translation | updated_at: updated_at, content: body}, published_at))
|
|
||||||
|
:ok =
|
||||||
|
File.write(
|
||||||
|
full_path,
|
||||||
|
serialize_translation_file(
|
||||||
|
%{translation | updated_at: updated_at, content: body},
|
||||||
|
published_at
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
translation
|
translation
|
||||||
|> Translation.changeset(%{
|
|> Translation.changeset(%{
|
||||||
@@ -619,7 +670,8 @@ defmodule BDS.Posts do
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp publishable_translation_body(%Translation{content: content}, _full_path) when is_binary(content), do: content
|
defp publishable_translation_body(%Translation{content: content}, _full_path)
|
||||||
|
when is_binary(content), do: content
|
||||||
|
|
||||||
defp publishable_translation_body(_translation, full_path) do
|
defp publishable_translation_body(_translation, full_path) do
|
||||||
case File.read(full_path) do
|
case File.read(full_path) do
|
||||||
@@ -634,7 +686,8 @@ defmodule BDS.Posts do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp delete_translation_file(%Translation{project_id: _project_id, file_path: file_path}) when file_path in [nil, ""], do: :ok
|
defp delete_translation_file(%Translation{project_id: _project_id, file_path: file_path})
|
||||||
|
when file_path in [nil, ""], do: :ok
|
||||||
|
|
||||||
defp delete_translation_file(%Translation{} = translation) do
|
defp delete_translation_file(%Translation{} = translation) do
|
||||||
project = Projects.get_project!(translation.project_id)
|
project = Projects.get_project!(translation.project_id)
|
||||||
@@ -649,7 +702,15 @@ defmodule BDS.Posts do
|
|||||||
|
|
||||||
defp orphan_translation_files(project_id) do
|
defp orphan_translation_files(project_id) do
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
translation_paths = MapSet.new(Repo.all(from translation in Translation, where: translation.project_id == ^project_id, select: translation.file_path))
|
|
||||||
|
translation_paths =
|
||||||
|
MapSet.new(
|
||||||
|
Repo.all(
|
||||||
|
from translation in Translation,
|
||||||
|
where: translation.project_id == ^project_id,
|
||||||
|
select: translation.file_path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
project
|
project
|
||||||
|> Projects.project_data_dir()
|
|> Projects.project_data_dir()
|
||||||
|
|||||||
@@ -67,7 +67,15 @@ defmodule BDS.Posts.Post do
|
|||||||
],
|
],
|
||||||
empty_values: [nil]
|
empty_values: [nil]
|
||||||
)
|
)
|
||||||
|> validate_required([:id, :project_id, :slug, :status, :created_at, :updated_at, :do_not_translate])
|
|> validate_required([
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:slug,
|
||||||
|
:status,
|
||||||
|
:created_at,
|
||||||
|
:updated_at,
|
||||||
|
:do_not_translate
|
||||||
|
])
|
||||||
|> assoc_constraint(:project)
|
|> assoc_constraint(:project)
|
||||||
|> unique_constraint(:slug, name: :posts_project_slug_idx)
|
|> unique_constraint(:slug, name: :posts_project_slug_idx)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ defmodule BDS.Posts.Translation do
|
|||||||
@statuses [:draft, :published]
|
@statuses [:draft, :published]
|
||||||
|
|
||||||
schema "post_translations" do
|
schema "post_translations" do
|
||||||
belongs_to :post, BDS.Posts.Post, foreign_key: :translation_for, references: :id, type: :string
|
belongs_to :post, BDS.Posts.Post,
|
||||||
|
foreign_key: :translation_for,
|
||||||
|
references: :id,
|
||||||
|
type: :string
|
||||||
|
|
||||||
field :project_id, :string
|
field :project_id, :string
|
||||||
field :language, :string
|
field :language, :string
|
||||||
@@ -26,7 +29,9 @@ defmodule BDS.Posts.Translation do
|
|||||||
|
|
||||||
def changeset(translation, attrs) do
|
def changeset(translation, attrs) do
|
||||||
translation
|
translation
|
||||||
|> cast(attrs, [
|
|> cast(
|
||||||
|
attrs,
|
||||||
|
[
|
||||||
:id,
|
:id,
|
||||||
:project_id,
|
:project_id,
|
||||||
:translation_for,
|
:translation_for,
|
||||||
@@ -40,8 +45,19 @@ defmodule BDS.Posts.Translation do
|
|||||||
:published_at,
|
:published_at,
|
||||||
:file_path,
|
:file_path,
|
||||||
:checksum
|
:checksum
|
||||||
], empty_values: [nil])
|
],
|
||||||
|> validate_required([:id, :project_id, :translation_for, :language, :title, :status, :created_at, :updated_at])
|
empty_values: [nil]
|
||||||
|
)
|
||||||
|
|> validate_required([
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:translation_for,
|
||||||
|
:language,
|
||||||
|
:title,
|
||||||
|
:status,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
])
|
||||||
|> foreign_key_constraint(:translation_for)
|
|> foreign_key_constraint(:translation_for)
|
||||||
|> unique_constraint(:language, name: :post_translations_translation_language_idx)
|
|> unique_constraint(:language, name: :post_translations_translation_language_idx)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -16,7 +16,11 @@ defmodule BDS.Preview do
|
|||||||
|
|
||||||
def start_preview(project_id) when is_binary(project_id) do
|
def start_preview(project_id) when is_binary(project_id) do
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
GenServer.call(__MODULE__, {:start_preview, project_id, Projects.project_data_dir(project), self()})
|
|
||||||
|
GenServer.call(
|
||||||
|
__MODULE__,
|
||||||
|
{:start_preview, project_id, Projects.project_data_dir(project), self()}
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop_preview(project_id) when is_binary(project_id) do
|
def stop_preview(project_id) when is_binary(project_id) do
|
||||||
@@ -58,7 +62,15 @@ defmodule BDS.Preview do
|
|||||||
state = stop_current_server(state)
|
state = stop_current_server(state)
|
||||||
maybe_allow_repo(owner_pid)
|
maybe_allow_repo(owner_pid)
|
||||||
|
|
||||||
{:ok, listener} = :gen_tcp.listen(@port, [:binary, packet: :raw, active: false, reuseaddr: true, ip: {127, 0, 0, 1}])
|
{:ok, listener} =
|
||||||
|
:gen_tcp.listen(@port, [
|
||||||
|
:binary,
|
||||||
|
packet: :raw,
|
||||||
|
active: false,
|
||||||
|
reuseaddr: true,
|
||||||
|
ip: {127, 0, 0, 1}
|
||||||
|
])
|
||||||
|
|
||||||
acceptor_pid = spawn_link(fn -> accept_loop(listener, project_id) end)
|
acceptor_pid = spawn_link(fn -> accept_loop(listener, project_id) end)
|
||||||
|
|
||||||
server = %{
|
server = %{
|
||||||
@@ -145,7 +157,9 @@ defmodule BDS.Preview do
|
|||||||
end
|
end
|
||||||
|
|
||||||
case full_path do
|
case full_path do
|
||||||
{:error, :not_found} -> {:error, :not_found}
|
{:error, :not_found} ->
|
||||||
|
{:error, :not_found}
|
||||||
|
|
||||||
resolved_path ->
|
resolved_path ->
|
||||||
case read_response(resolved_path) do
|
case read_response(resolved_path) do
|
||||||
{:error, :not_found} -> render_not_found_response(server.project_id)
|
{:error, :not_found} -> render_not_found_response(server.project_id)
|
||||||
@@ -258,7 +272,11 @@ defmodule BDS.Preview do
|
|||||||
path = uri.path || "/"
|
path = uri.path || "/"
|
||||||
query_params = URI.decode_query(uri.query || "")
|
query_params = URI.decode_query(uri.query || "")
|
||||||
|
|
||||||
case GenServer.call(__MODULE__, {:http_request, project_id, method, path, query_params}, 5_000) do
|
case GenServer.call(
|
||||||
|
__MODULE__,
|
||||||
|
{:http_request, project_id, method, path, query_params},
|
||||||
|
5_000
|
||||||
|
) do
|
||||||
{:ok, response} -> http_ok_response(response)
|
{:ok, response} -> http_ok_response(response)
|
||||||
{:error, :not_found} -> http_error_response(404)
|
{:error, :not_found} -> http_error_response(404)
|
||||||
{:error, :not_running} -> http_error_response(503)
|
{:error, :not_running} -> http_error_response(503)
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ defmodule BDS.Projects do
|
|||||||
|> Multi.update_all(:clear_previous, from(p in Project, where: p.is_active == true),
|
|> Multi.update_all(:clear_previous, from(p in Project, where: p.is_active == true),
|
||||||
set: [is_active: false, updated_at: now]
|
set: [is_active: false, updated_at: now]
|
||||||
)
|
)
|
||||||
|> Multi.update(:activate, Project.changeset(project, %{is_active: true, updated_at: now}))
|
|> Multi.update(
|
||||||
|
:activate,
|
||||||
|
Project.changeset(project, %{is_active: true, updated_at: now})
|
||||||
|
)
|
||||||
|> Repo.transaction()
|
|> Repo.transaction()
|
||||||
|> case do
|
|> case do
|
||||||
{:ok, %{activate: active_project}} -> {:ok, active_project}
|
{:ok, %{activate: active_project}} -> {:ok, active_project}
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ defmodule BDS.Projects.Project do
|
|||||||
|
|
||||||
def changeset(project, attrs) do
|
def changeset(project, attrs) do
|
||||||
project
|
project
|
||||||
|> cast(attrs, [:id, :name, :slug, :description, :data_path, :created_at, :updated_at, :is_active],
|
|> cast(
|
||||||
|
attrs,
|
||||||
|
[:id, :name, :slug, :description, :data_path, :created_at, :updated_at, :is_active],
|
||||||
empty_values: [nil]
|
empty_values: [nil]
|
||||||
)
|
)
|
||||||
|> validate_required([:id, :name, :slug, :created_at, :updated_at, :is_active])
|
|> validate_required([:id, :name, :slug, :created_at, :updated_at, :is_active])
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ defmodule BDS.Publishing do
|
|||||||
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
||||||
end
|
end
|
||||||
|
|
||||||
def upload_site(project_id, credentials, opts \\ []) when is_binary(project_id) and is_map(credentials) and is_list(opts) do
|
def upload_site(project_id, credentials, opts \\ [])
|
||||||
|
when is_binary(project_id) and is_map(credentials) and is_list(opts) do
|
||||||
project = Projects.get_project!(project_id)
|
project = Projects.get_project!(project_id)
|
||||||
normalized_credentials = normalize_credentials(credentials)
|
normalized_credentials = normalize_credentials(credentials)
|
||||||
targets = build_upload_targets(Projects.project_data_dir(project), normalized_credentials)
|
targets = build_upload_targets(Projects.project_data_dir(project), normalized_credentials)
|
||||||
@@ -23,7 +24,7 @@ defmodule BDS.Publishing do
|
|||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def init(_state) do
|
def init(_state) do
|
||||||
{:ok, %{jobs: %{}}}
|
{:ok, %{jobs: %{}, scp_uploads: %{}}}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
@@ -41,15 +42,32 @@ defmodule BDS.Publishing do
|
|||||||
{:reply, :ok, next_state}
|
{:reply, :ok, next_state}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def handle_call({:should_upload_scp_file, upload_key, local_mtime}, _from, state) do
|
||||||
|
should_upload? =
|
||||||
|
case state.scp_uploads[upload_key] do
|
||||||
|
nil -> true
|
||||||
|
recorded_mtime -> local_mtime > recorded_mtime
|
||||||
|
end
|
||||||
|
|
||||||
|
{:reply, should_upload?, state}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_call({:mark_uploaded_scp_file, upload_key, local_mtime}, _from, state) do
|
||||||
|
{:reply, :ok, put_in(state, [:scp_uploads, upload_key], local_mtime)}
|
||||||
|
end
|
||||||
|
|
||||||
def handle_call({:upload_site, project_id, credentials, targets, opts}, _from, state) do
|
def handle_call({:upload_site, project_id, credentials, targets, opts}, _from, state) do
|
||||||
job_id = "publish-" <> Integer.to_string(System.unique_integer([:positive, :monotonic]))
|
job_id = "publish-" <> Integer.to_string(System.unique_integer([:positive, :monotonic]))
|
||||||
uploader = build_uploader(opts)
|
uploader = build_uploader(Keyword.put_new(opts, :project_id, project_id))
|
||||||
|
|
||||||
job = %{
|
job = %{
|
||||||
id: job_id,
|
id: job_id,
|
||||||
project_id: project_id,
|
project_id: project_id,
|
||||||
status: :pending,
|
status: :pending,
|
||||||
task_id: nil,
|
task_id: nil,
|
||||||
|
ssh_host: credentials.ssh_host,
|
||||||
|
ssh_user: credentials.ssh_user,
|
||||||
|
ssh_remote_path: credentials.ssh_remote_path,
|
||||||
ssh_mode: credentials.ssh_mode,
|
ssh_mode: credentials.ssh_mode,
|
||||||
targets: Enum.map(targets, & &1.kind),
|
targets: Enum.map(targets, & &1.kind),
|
||||||
error: nil,
|
error: nil,
|
||||||
@@ -58,12 +76,16 @@ defmodule BDS.Publishing do
|
|||||||
}
|
}
|
||||||
|
|
||||||
{:ok, task} =
|
{:ok, task} =
|
||||||
Tasks.submit_task("publish #{project_id}", fn report ->
|
Tasks.submit_task(
|
||||||
|
"publish #{project_id}",
|
||||||
|
fn report ->
|
||||||
run_upload(job_id, credentials, targets, uploader, report)
|
run_upload(job_id, credentials, targets, uploader, report)
|
||||||
end, %{
|
end,
|
||||||
|
%{
|
||||||
group_id: project_id,
|
group_id: project_id,
|
||||||
group_name: "Publishing"
|
group_name: "Publishing"
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
next_job = %{job | task_id: task.id}
|
next_job = %{job | task_id: task.id}
|
||||||
{:reply, {:ok, next_job}, put_in(state, [:jobs, job_id], next_job)}
|
{:reply, {:ok, next_job}, put_in(state, [:jobs, job_id], next_job)}
|
||||||
@@ -104,9 +126,10 @@ defmodule BDS.Publishing do
|
|||||||
nil ->
|
nil ->
|
||||||
runner = Keyword.get(opts, :command_runner, &System.cmd/3)
|
runner = Keyword.get(opts, :command_runner, &System.cmd/3)
|
||||||
ssh_auth_sock = Keyword.get(opts, :ssh_auth_sock, System.get_env("SSH_AUTH_SOCK"))
|
ssh_auth_sock = Keyword.get(opts, :ssh_auth_sock, System.get_env("SSH_AUTH_SOCK"))
|
||||||
|
project_id = Keyword.fetch!(opts, :project_id)
|
||||||
|
|
||||||
fn target, files, credentials ->
|
fn target, files, credentials ->
|
||||||
run_command_upload(target, files, credentials, runner, ssh_auth_sock)
|
run_command_upload(project_id, target, files, credentials, runner, ssh_auth_sock)
|
||||||
end
|
end
|
||||||
|
|
||||||
uploader ->
|
uploader ->
|
||||||
@@ -114,22 +137,60 @@ defmodule BDS.Publishing do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp run_command_upload(target, _files, %{ssh_mode: :rsync} = credentials, runner, ssh_auth_sock) do
|
defp run_command_upload(
|
||||||
|
_project_id,
|
||||||
|
target,
|
||||||
|
_files,
|
||||||
|
%{ssh_mode: :rsync} = credentials,
|
||||||
|
runner,
|
||||||
|
ssh_auth_sock
|
||||||
|
) do
|
||||||
args =
|
args =
|
||||||
["--update", "--compress", "--verbose"] ++
|
["--update", "--compress", "--verbose"] ++
|
||||||
rsync_excludes(target) ++
|
rsync_excludes(target) ++
|
||||||
["-e", "ssh", ensure_trailing_slash(target.local_dir), remote_dir_spec(credentials, target.remote_dir)]
|
[
|
||||||
|
"-e",
|
||||||
|
"ssh",
|
||||||
|
ensure_trailing_slash(target.local_dir),
|
||||||
|
remote_dir_spec(credentials, target.remote_dir)
|
||||||
|
]
|
||||||
|
|
||||||
run_command(runner, "rsync", args, ssh_auth_sock)
|
run_command(runner, "rsync", args, ssh_auth_sock)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp run_command_upload(target, files, credentials, runner, ssh_auth_sock) do
|
defp run_command_upload(project_id, target, files, credentials, runner, ssh_auth_sock) do
|
||||||
Enum.reduce_while(files, :ok, fn relative_path, :ok ->
|
Enum.reduce_while(files, :ok, fn relative_path, :ok ->
|
||||||
local_path = Path.join(target.local_dir, relative_path)
|
local_path = Path.join(target.local_dir, relative_path)
|
||||||
|
|
||||||
|
with {:ok, local_mtime} <- file_mtime(local_path),
|
||||||
|
true <-
|
||||||
|
should_upload_scp_file?(
|
||||||
|
project_id,
|
||||||
|
credentials,
|
||||||
|
target.kind,
|
||||||
|
relative_path,
|
||||||
|
local_mtime
|
||||||
|
) do
|
||||||
remote_path = remote_file_spec(credentials, target.remote_dir, relative_path)
|
remote_path = remote_file_spec(credentials, target.remote_dir, relative_path)
|
||||||
|
|
||||||
case run_command(runner, "scp", ["-q", local_path, remote_path], ssh_auth_sock) do
|
case run_command(runner, "scp", ["-q", local_path, remote_path], ssh_auth_sock) do
|
||||||
:ok -> {:cont, :ok}
|
:ok ->
|
||||||
|
:ok =
|
||||||
|
mark_uploaded_scp_file(
|
||||||
|
project_id,
|
||||||
|
credentials,
|
||||||
|
target.kind,
|
||||||
|
relative_path,
|
||||||
|
local_mtime
|
||||||
|
)
|
||||||
|
|
||||||
|
{:cont, :ok}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
{:halt, {:error, reason}}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
false -> {:cont, :ok}
|
||||||
{:error, reason} -> {:halt, {:error, reason}}
|
{:error, reason} -> {:halt, {:error, reason}}
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
@@ -147,10 +208,49 @@ defmodule BDS.Publishing do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp command_opts(nil), do: [stderr_to_stdout: true]
|
defp command_opts(nil), do: [stderr_to_stdout: true]
|
||||||
defp command_opts(ssh_auth_sock), do: [stderr_to_stdout: true, env: [{"SSH_AUTH_SOCK", ssh_auth_sock}]]
|
|
||||||
|
|
||||||
defp normalize_command_error(_command, output, _status) when is_binary(output) and output != "", do: output
|
defp command_opts(ssh_auth_sock),
|
||||||
defp normalize_command_error(command, _output, status), do: "#{command} exited with status #{status}"
|
do: [stderr_to_stdout: true, env: [{"SSH_AUTH_SOCK", ssh_auth_sock}]]
|
||||||
|
|
||||||
|
defp normalize_command_error(_command, output, _status) when is_binary(output) and output != "",
|
||||||
|
do: output
|
||||||
|
|
||||||
|
defp normalize_command_error(command, _output, status),
|
||||||
|
do: "#{command} exited with status #{status}"
|
||||||
|
|
||||||
|
defp file_mtime(path) do
|
||||||
|
case File.stat(path, time: :posix) do
|
||||||
|
{:ok, stat} -> {:ok, stat.mtime}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp should_upload_scp_file?(project_id, credentials, target_kind, relative_path, local_mtime) do
|
||||||
|
GenServer.call(
|
||||||
|
__MODULE__,
|
||||||
|
{:should_upload_scp_file,
|
||||||
|
scp_upload_key(project_id, credentials, target_kind, relative_path), local_mtime}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp mark_uploaded_scp_file(project_id, credentials, target_kind, relative_path, local_mtime) do
|
||||||
|
GenServer.call(
|
||||||
|
__MODULE__,
|
||||||
|
{:mark_uploaded_scp_file,
|
||||||
|
scp_upload_key(project_id, credentials, target_kind, relative_path), local_mtime}
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp scp_upload_key(project_id, credentials, target_kind, relative_path) do
|
||||||
|
{
|
||||||
|
project_id,
|
||||||
|
credentials.ssh_host,
|
||||||
|
credentials.ssh_user,
|
||||||
|
credentials.ssh_remote_path,
|
||||||
|
target_kind,
|
||||||
|
relative_path
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
defp rsync_excludes(%{kind: :media}), do: ["--exclude=*.meta"]
|
defp rsync_excludes(%{kind: :media}), do: ["--exclude=*.meta"]
|
||||||
defp rsync_excludes(_target), do: []
|
defp rsync_excludes(_target), do: []
|
||||||
@@ -172,8 +272,16 @@ defmodule BDS.Publishing do
|
|||||||
|
|
||||||
[
|
[
|
||||||
%{kind: :html, local_dir: Path.join(base_dir, "html"), remote_dir: remote_root},
|
%{kind: :html, local_dir: Path.join(base_dir, "html"), remote_dir: remote_root},
|
||||||
%{kind: :thumbnails, local_dir: Path.join(base_dir, "thumbnails"), remote_dir: Path.join(remote_root, "thumbnails")},
|
%{
|
||||||
%{kind: :media, local_dir: Path.join(base_dir, "media"), remote_dir: Path.join(remote_root, "media")}
|
kind: :thumbnails,
|
||||||
|
local_dir: Path.join(base_dir, "thumbnails"),
|
||||||
|
remote_dir: Path.join(remote_root, "thumbnails")
|
||||||
|
},
|
||||||
|
%{
|
||||||
|
kind: :media,
|
||||||
|
local_dir: Path.join(base_dir, "media"),
|
||||||
|
remote_dir: Path.join(remote_root, "media")
|
||||||
|
}
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -184,7 +292,9 @@ defmodule BDS.Publishing do
|
|||||||
|> Path.wildcard(match_dot: true)
|
|> Path.wildcard(match_dot: true)
|
||||||
|> Enum.filter(&File.regular?/1)
|
|> Enum.filter(&File.regular?/1)
|
||||||
|> Enum.map(&Path.relative_to(&1, target.local_dir))
|
|> Enum.map(&Path.relative_to(&1, target.local_dir))
|
||||||
|> Enum.reject(fn relative_path -> target.kind == :media and String.ends_with?(relative_path, ".meta") end)
|
|> Enum.reject(fn relative_path ->
|
||||||
|
target.kind == :media and String.ends_with?(relative_path, ".meta")
|
||||||
|
end)
|
||||||
|> Enum.sort()
|
|> Enum.sort()
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -17,23 +17,28 @@ defmodule BDS.Rendering do
|
|||||||
alias BDS.Posts.Translation
|
alias BDS.Posts.Translation
|
||||||
alias BDS.Templates.Template
|
alias BDS.Templates.Template
|
||||||
|
|
||||||
def render_post_page(project_id, template_slug, assigns) when is_binary(project_id) and is_map(assigns) do
|
def render_post_page(project_id, template_slug, assigns)
|
||||||
|
when is_binary(project_id) and is_map(assigns) do
|
||||||
with {:ok, template_source} <- load_template_source(project_id, :post, template_slug),
|
with {:ok, template_source} <- load_template_source(project_id, :post, template_slug),
|
||||||
{:ok, rendered} <- render_template(project_id, template_source, post_assigns(project_id, assigns)) do
|
{:ok, rendered} <-
|
||||||
|
render_template(project_id, template_source, post_assigns(project_id, assigns)) do
|
||||||
{:ok, rendered}
|
{:ok, rendered}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_list_page(project_id, assigns) when is_binary(project_id) and is_map(assigns) do
|
def render_list_page(project_id, assigns) when is_binary(project_id) and is_map(assigns) do
|
||||||
with {:ok, template_source} <- load_template_source(project_id, :list, nil),
|
with {:ok, template_source} <- load_template_source(project_id, :list, nil),
|
||||||
{:ok, rendered} <- render_template(project_id, template_source, list_assigns(project_id, assigns)) do
|
{:ok, rendered} <-
|
||||||
|
render_template(project_id, template_source, list_assigns(project_id, assigns)) do
|
||||||
{:ok, rendered}
|
{:ok, rendered}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_not_found_page(project_id, assigns \\ %{}) when is_binary(project_id) and is_map(assigns) do
|
def render_not_found_page(project_id, assigns \\ %{})
|
||||||
|
when is_binary(project_id) and is_map(assigns) do
|
||||||
with {:ok, template_source} <- load_template_source(project_id, :not_found, nil),
|
with {:ok, template_source} <- load_template_source(project_id, :not_found, nil),
|
||||||
{:ok, rendered} <- render_template(project_id, template_source, not_found_assigns(project_id, assigns)) do
|
{:ok, rendered} <-
|
||||||
|
render_template(project_id, template_source, not_found_assigns(project_id, assigns)) do
|
||||||
{:ok, rendered}
|
{:ok, rendered}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -49,7 +54,8 @@ defmodule BDS.Rendering do
|
|||||||
Repo.one(
|
Repo.one(
|
||||||
from template in Template,
|
from template in Template,
|
||||||
where:
|
where:
|
||||||
template.project_id == ^project_id and template.kind == ^kind and template.status == :published and
|
template.project_id == ^project_id and template.kind == ^kind and
|
||||||
|
template.status == :published and
|
||||||
template.enabled == true and template.slug == ^slug,
|
template.enabled == true and template.slug == ^slug,
|
||||||
limit: 1
|
limit: 1
|
||||||
) || select_template(project_id, kind, nil)
|
) || select_template(project_id, kind, nil)
|
||||||
@@ -59,14 +65,16 @@ defmodule BDS.Rendering do
|
|||||||
Repo.one(
|
Repo.one(
|
||||||
from template in Template,
|
from template in Template,
|
||||||
where:
|
where:
|
||||||
template.project_id == ^project_id and template.kind == ^kind and template.status == :published and
|
template.project_id == ^project_id and template.kind == ^kind and
|
||||||
|
template.status == :published and
|
||||||
template.enabled == true,
|
template.enabled == true,
|
||||||
order_by: [asc: template.created_at, asc: template.slug],
|
order_by: [asc: template.created_at, asc: template.slug],
|
||||||
limit: 1
|
limit: 1
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp published_template_body(%Template{content: content}) when is_binary(content), do: {:ok, content}
|
defp published_template_body(%Template{content: content}) when is_binary(content),
|
||||||
|
do: {:ok, content}
|
||||||
|
|
||||||
defp published_template_body(%Template{} = template) do
|
defp published_template_body(%Template{} = template) do
|
||||||
project = Projects.get_project!(template.project_id)
|
project = Projects.get_project!(template.project_id)
|
||||||
@@ -105,17 +113,32 @@ defmodule BDS.Rendering do
|
|||||||
|
|
||||||
defp post_assigns(project_id, assigns) do
|
defp post_assigns(project_id, assigns) do
|
||||||
metadata = project_metadata(project_id)
|
metadata = project_metadata(project_id)
|
||||||
language = Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
|
||||||
|
language =
|
||||||
|
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
||||||
|
|
||||||
main_language = metadata.main_language || language
|
main_language = metadata.main_language || language
|
||||||
post_record = load_post_record(assigns)
|
post_record = load_post_record(assigns)
|
||||||
post_categories = Map.get(post_record || %{}, :categories, []) || []
|
post_categories = Map.get(post_record || %{}, :categories, []) || []
|
||||||
post_tags = Map.get(post_record || %{}, :tags, []) || []
|
post_tags = Map.get(post_record || %{}, :tags, []) || []
|
||||||
|
canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
|
||||||
|
canonical_media_paths = canonical_media_path_by_source_path(project_id)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
language: language,
|
language: language,
|
||||||
language_prefix: Map.get(assigns, :language_prefix, Map.get(assigns, "language_prefix", language_prefix(language, main_language))),
|
language_prefix:
|
||||||
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title", Map.get(assigns, :title, Map.get(assigns, "title")))),
|
Map.get(
|
||||||
pico_stylesheet_href: nil,
|
assigns,
|
||||||
|
:language_prefix,
|
||||||
|
Map.get(assigns, "language_prefix", language_prefix(language, main_language))
|
||||||
|
),
|
||||||
|
page_title:
|
||||||
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
:page_title,
|
||||||
|
Map.get(assigns, "page_title", Map.get(assigns, :title, Map.get(assigns, "title")))
|
||||||
|
),
|
||||||
|
pico_stylesheet_href: default_pico_stylesheet_href(),
|
||||||
html_theme_attribute: html_theme_attribute(metadata.pico_theme),
|
html_theme_attribute: html_theme_attribute(metadata.pico_theme),
|
||||||
blog_languages: blog_languages(metadata, language),
|
blog_languages: blog_languages(metadata, language),
|
||||||
alternate_links: [],
|
alternate_links: [],
|
||||||
@@ -126,34 +149,40 @@ defmodule BDS.Rendering do
|
|||||||
post_tags: post_tags,
|
post_tags: post_tags,
|
||||||
tag_color_by_name: tag_color_by_name(project_id),
|
tag_color_by_name: tag_color_by_name(project_id),
|
||||||
backlinks: [],
|
backlinks: [],
|
||||||
canonical_post_path_by_slug: canonical_post_path_by_slug(project_id, main_language),
|
canonical_post_path_by_slug: canonical_post_paths,
|
||||||
canonical_media_path_by_source_path: canonical_media_path_by_source_path(project_id),
|
canonical_media_path_by_source_path: canonical_media_paths,
|
||||||
post_data_json_by_id: post_data_json(assigns),
|
post_data_json_by_id: post_data_json(assigns, post_record),
|
||||||
post: %{
|
post: build_post_context(assigns, post_record)
|
||||||
id: Map.get(assigns, :id, Map.get(assigns, "id")),
|
|
||||||
slug: Map.get(assigns, :slug, Map.get(assigns, "slug")),
|
|
||||||
title: Map.get(assigns, :title, Map.get(assigns, "title")),
|
|
||||||
content: Map.get(assigns, :content, Map.get(assigns, "content")),
|
|
||||||
excerpt: Map.get(assigns, :excerpt, Map.get(assigns, "excerpt")),
|
|
||||||
language: Map.get(assigns, :language, Map.get(assigns, "language")),
|
|
||||||
show_title: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp list_assigns(project_id, assigns) do
|
defp list_assigns(project_id, assigns) do
|
||||||
metadata = project_metadata(project_id)
|
metadata = project_metadata(project_id)
|
||||||
language = Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
|
||||||
|
language =
|
||||||
|
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
||||||
|
|
||||||
main_language = metadata.main_language || language
|
main_language = metadata.main_language || language
|
||||||
posts = normalize_list_posts(Map.get(assigns, :posts, Map.get(assigns, "posts", [])))
|
posts = normalize_list_posts(Map.get(assigns, :posts, Map.get(assigns, "posts", [])))
|
||||||
archive_context = Map.get(assigns, :archive_context, Map.get(assigns, "archive_context", %{}))
|
archive_context = Map.get(assigns, :archive_context, Map.get(assigns, "archive_context", %{}))
|
||||||
|
|
||||||
|
pagination =
|
||||||
|
normalize_pagination(Map.get(assigns, :pagination, Map.get(assigns, "pagination")), posts)
|
||||||
|
|
||||||
|
canonical_post_paths = canonical_post_path_by_slug(project_id, main_language)
|
||||||
|
canonical_media_paths = canonical_media_path_by_source_path(project_id)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
language: language,
|
language: language,
|
||||||
language_prefix: Map.get(assigns, :language_prefix, Map.get(assigns, "language_prefix", language_prefix(language, main_language))),
|
language_prefix:
|
||||||
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
:language_prefix,
|
||||||
|
Map.get(assigns, "language_prefix", language_prefix(language, main_language))
|
||||||
|
),
|
||||||
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title")),
|
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title")),
|
||||||
posts: posts,
|
posts: posts,
|
||||||
pico_stylesheet_href: nil,
|
pico_stylesheet_href: default_pico_stylesheet_href(),
|
||||||
html_theme_attribute: html_theme_attribute(metadata.pico_theme),
|
html_theme_attribute: html_theme_attribute(metadata.pico_theme),
|
||||||
blog_languages: blog_languages(metadata, language),
|
blog_languages: blog_languages(metadata, language),
|
||||||
alternate_links: [],
|
alternate_links: [],
|
||||||
@@ -165,15 +194,20 @@ defmodule BDS.Rendering do
|
|||||||
min_date: nil,
|
min_date: nil,
|
||||||
max_date: nil,
|
max_date: nil,
|
||||||
is_list_page: true,
|
is_list_page: true,
|
||||||
is_first_page: true,
|
is_first_page: pagination.current_page <= 1,
|
||||||
is_last_page: true,
|
is_last_page: pagination.current_page >= pagination.total_pages,
|
||||||
has_prev_page: false,
|
has_prev_page: pagination.has_prev_page,
|
||||||
has_next_page: false,
|
has_next_page: pagination.has_next_page,
|
||||||
prev_page_href: "",
|
prev_page_href: pagination.prev_page_href,
|
||||||
next_page_href: "",
|
next_page_href: pagination.next_page_href,
|
||||||
canonical_post_path_by_slug: canonical_post_path_by_slug(project_id, main_language),
|
current_page: pagination.current_page,
|
||||||
canonical_media_path_by_source_path: canonical_media_path_by_source_path(project_id),
|
total_pages: pagination.total_pages,
|
||||||
post_data_json_by_id: Enum.into(posts, %{}, fn post -> {post.id, Jason.encode!(Map.take(post, [:id, :slug, :title, :content]))} end),
|
total_items: pagination.total_items,
|
||||||
|
items_per_page: pagination.items_per_page,
|
||||||
|
canonical_post_path_by_slug: canonical_post_paths,
|
||||||
|
canonical_media_path_by_source_path: canonical_media_paths,
|
||||||
|
post_data_json_by_id:
|
||||||
|
Enum.into(posts, %{}, fn post -> {post.id, post_data_json_value(post)} end),
|
||||||
day_blocks: [
|
day_blocks: [
|
||||||
%{
|
%{
|
||||||
date_label: "",
|
date_label: "",
|
||||||
@@ -187,20 +221,46 @@ defmodule BDS.Rendering do
|
|||||||
|
|
||||||
defp not_found_assigns(project_id, assigns) do
|
defp not_found_assigns(project_id, assigns) do
|
||||||
metadata = project_metadata(project_id)
|
metadata = project_metadata(project_id)
|
||||||
language = Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
|
||||||
|
language =
|
||||||
|
Map.get(assigns, :language, Map.get(assigns, "language", metadata.main_language || "en"))
|
||||||
|
|
||||||
main_language = metadata.main_language || language
|
main_language = metadata.main_language || language
|
||||||
|
|
||||||
%{
|
%{
|
||||||
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title", "404 Not Found")),
|
page_title: Map.get(assigns, :page_title, Map.get(assigns, "page_title", "404")),
|
||||||
language: language,
|
language: language,
|
||||||
language_prefix: Map.get(assigns, :language_prefix, Map.get(assigns, "language_prefix", language_prefix(language, main_language))),
|
language_prefix:
|
||||||
pico_stylesheet_href: nil,
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
:language_prefix,
|
||||||
|
Map.get(assigns, "language_prefix", language_prefix(language, main_language))
|
||||||
|
),
|
||||||
|
pico_stylesheet_href: default_pico_stylesheet_href(),
|
||||||
html_theme_attribute: html_theme_attribute(metadata.pico_theme),
|
html_theme_attribute: html_theme_attribute(metadata.pico_theme),
|
||||||
blog_languages: blog_languages(metadata, language),
|
blog_languages: blog_languages(metadata, language),
|
||||||
menu_items: menu_items(project_id),
|
menu_items: menu_items(project_id),
|
||||||
alternate_links: [],
|
alternate_links: [],
|
||||||
not_found_message: Map.get(assigns, :not_found_message, Map.get(assigns, "not_found_message")),
|
not_found_message:
|
||||||
not_found_back_label: Map.get(assigns, :not_found_back_label, Map.get(assigns, "not_found_back_label"))
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
:not_found_message,
|
||||||
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
"not_found_message",
|
||||||
|
I18n.translate(language, "render.notFound.message")
|
||||||
|
)
|
||||||
|
),
|
||||||
|
not_found_back_label:
|
||||||
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
:not_found_back_label,
|
||||||
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
"not_found_back_label",
|
||||||
|
I18n.translate(language, "render.notFound.back")
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -232,8 +292,13 @@ defmodule BDS.Rendering do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp menu_item_href(%{kind: :home}), do: "/"
|
defp menu_item_href(%{kind: :home}), do: "/"
|
||||||
defp menu_item_href(%{kind: :page, slug: slug}) when is_binary(slug) and slug != "", do: "/#{slug}/"
|
|
||||||
defp menu_item_href(%{kind: :category_archive, slug: slug}) when is_binary(slug) and slug != "", do: "/category/#{URI.encode(slug)}/"
|
defp menu_item_href(%{kind: :page, slug: slug}) when is_binary(slug) and slug != "",
|
||||||
|
do: "/#{slug}/"
|
||||||
|
|
||||||
|
defp menu_item_href(%{kind: :category_archive, slug: slug}) when is_binary(slug) and slug != "",
|
||||||
|
do: "/category/#{URI.encode(slug)}/"
|
||||||
|
|
||||||
defp menu_item_href(%{kind: :submenu}), do: "#"
|
defp menu_item_href(%{kind: :submenu}), do: "#"
|
||||||
defp menu_item_href(_item), do: "#"
|
defp menu_item_href(_item), do: "#"
|
||||||
|
|
||||||
@@ -243,11 +308,13 @@ defmodule BDS.Rendering do
|
|||||||
|> Enum.uniq()
|
|> Enum.uniq()
|
||||||
|> Enum.map(fn language ->
|
|> Enum.map(fn language ->
|
||||||
normalized = I18n.normalize_language(language)
|
normalized = I18n.normalize_language(language)
|
||||||
|
href_prefix = language_prefix(normalized, metadata.main_language || current_language)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
code: normalized,
|
code: normalized,
|
||||||
flag: I18n.flag(normalized),
|
flag: I18n.flag(normalized),
|
||||||
href_prefix: language_prefix(normalized, metadata.main_language || current_language),
|
href: href_for_language(href_prefix),
|
||||||
|
href_prefix: href_prefix,
|
||||||
is_current: normalized == I18n.normalize_language(current_language)
|
is_current: normalized == I18n.normalize_language(current_language)
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
@@ -265,26 +332,39 @@ defmodule BDS.Rendering do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp post_data_json(assigns) do
|
defp post_data_json(assigns, post_record) do
|
||||||
id = Map.get(assigns, :id, Map.get(assigns, "id"))
|
id = Map.get(assigns, :id, Map.get(assigns, "id"))
|
||||||
|
|
||||||
if is_binary(id) do
|
if is_binary(id) do
|
||||||
%{
|
%{
|
||||||
id =>
|
id => post_data_json_value(build_post_context(assigns, post_record))
|
||||||
Jason.encode!(%{
|
|
||||||
id: id,
|
|
||||||
slug: Map.get(assigns, :slug, Map.get(assigns, "slug")),
|
|
||||||
title: Map.get(assigns, :title, Map.get(assigns, "title")),
|
|
||||||
content: Map.get(assigns, :content, Map.get(assigns, "content"))
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
%{}
|
%{}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp post_data_json_value(post_context) do
|
||||||
|
Jason.encode!(%{
|
||||||
|
id: Map.get(post_context, :id),
|
||||||
|
title: Map.get(post_context, :title),
|
||||||
|
slug: Map.get(post_context, :slug),
|
||||||
|
excerpt: Map.get(post_context, :excerpt),
|
||||||
|
author: Map.get(post_context, :author),
|
||||||
|
language: Map.get(post_context, :language),
|
||||||
|
published_at: Map.get(post_context, :published_at),
|
||||||
|
created_at: Map.get(post_context, :created_at),
|
||||||
|
updated_at: Map.get(post_context, :updated_at),
|
||||||
|
tags: Map.get(post_context, :tags, []),
|
||||||
|
categories: Map.get(post_context, :categories, [])
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
defp canonical_post_path_by_slug(project_id, main_language) do
|
defp canonical_post_path_by_slug(project_id, main_language) do
|
||||||
posts = Repo.all(from post in Post, where: post.project_id == ^project_id and post.status == :published)
|
posts =
|
||||||
|
Repo.all(
|
||||||
|
from post in Post, where: post.project_id == ^project_id and post.status == :published
|
||||||
|
)
|
||||||
|
|
||||||
translations =
|
translations =
|
||||||
Repo.all(
|
Repo.all(
|
||||||
@@ -311,6 +391,7 @@ defmodule BDS.Rendering do
|
|||||||
Repo.all(from media in MediaAsset, where: media.project_id == ^project_id)
|
Repo.all(from media in MediaAsset, where: media.project_id == ^project_id)
|
||||||
|> Enum.reduce(%{}, fn media, acc ->
|
|> Enum.reduce(%{}, fn media, acc ->
|
||||||
datetime = DateTime.from_unix!(media.created_at)
|
datetime = DateTime.from_unix!(media.created_at)
|
||||||
|
|
||||||
source_key =
|
source_key =
|
||||||
Path.join([
|
Path.join([
|
||||||
"media",
|
"media",
|
||||||
@@ -324,7 +405,8 @@ defmodule BDS.Rendering do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp post_path(post, language_prefix) when is_binary(language_prefix) and language_prefix != "" do
|
defp post_path(post, language_prefix)
|
||||||
|
when is_binary(language_prefix) and language_prefix != "" do
|
||||||
Path.join([String.trim_leading(language_prefix, "/"), post_path(post, nil)])
|
Path.join([String.trim_leading(language_prefix, "/"), post_path(post, nil)])
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -338,7 +420,7 @@ defmodule BDS.Rendering do
|
|||||||
post.slug,
|
post.slug,
|
||||||
"index.html"
|
"index.html"
|
||||||
])
|
])
|
||||||
|> then(&"/" <> String.trim_trailing(&1, "index.html"))
|
|> then(&("/" <> String.trim_trailing(&1, "index.html")))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp post_path(post, language, main_language) do
|
defp post_path(post, language, main_language) do
|
||||||
@@ -348,16 +430,119 @@ defmodule BDS.Rendering do
|
|||||||
|
|
||||||
defp normalize_list_posts(posts) do
|
defp normalize_list_posts(posts) do
|
||||||
Enum.map(posts, fn post ->
|
Enum.map(posts, fn post ->
|
||||||
|
post_record = load_post_record(post)
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: Map.get(post, :id, Map.get(post, "id")),
|
id: Map.get(post, :id, Map.get(post, "id")),
|
||||||
slug: Map.get(post, :slug, Map.get(post, "slug")),
|
slug: Map.get(post, :slug, Map.get(post, "slug")),
|
||||||
title: Map.get(post, :title, Map.get(post, "title")),
|
title: Map.get(post, :title, Map.get(post, "title")),
|
||||||
content: Map.get(post, :content, Map.get(post, "content", Map.get(post, :excerpt, Map.get(post, "excerpt", "")))),
|
content:
|
||||||
show_title: true
|
Map.get(
|
||||||
|
post,
|
||||||
|
:content,
|
||||||
|
Map.get(post, "content", Map.get(post, :excerpt, Map.get(post, "excerpt", "")))
|
||||||
|
),
|
||||||
|
excerpt:
|
||||||
|
Map.get(post, :excerpt, Map.get(post, "excerpt", Map.get(post_record || %{}, :excerpt))),
|
||||||
|
author: Map.get(post_record || %{}, :author),
|
||||||
|
language:
|
||||||
|
Map.get(
|
||||||
|
post,
|
||||||
|
:language,
|
||||||
|
Map.get(post, "language", Map.get(post_record || %{}, :language))
|
||||||
|
),
|
||||||
|
published_at: Map.get(post_record || %{}, :published_at),
|
||||||
|
created_at: Map.get(post_record || %{}, :created_at),
|
||||||
|
updated_at: Map.get(post_record || %{}, :updated_at),
|
||||||
|
tags: Map.get(post_record || %{}, :tags, []) || [],
|
||||||
|
categories: Map.get(post_record || %{}, :categories, []) || [],
|
||||||
|
template_slug: Map.get(post_record || %{}, :template_slug),
|
||||||
|
do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
|
||||||
|
href: Map.get(post, :href, Map.get(post, "href")),
|
||||||
|
show_title: true,
|
||||||
|
linked_media: [],
|
||||||
|
outgoing_links: [],
|
||||||
|
incoming_links: []
|
||||||
}
|
}
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp build_post_context(assigns, post_record) do
|
||||||
|
%{
|
||||||
|
id: Map.get(assigns, :id, Map.get(assigns, "id")),
|
||||||
|
slug: Map.get(assigns, :slug, Map.get(assigns, "slug")),
|
||||||
|
title: Map.get(assigns, :title, Map.get(assigns, "title")),
|
||||||
|
content: Map.get(assigns, :content, Map.get(assigns, "content")),
|
||||||
|
excerpt:
|
||||||
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
:excerpt,
|
||||||
|
Map.get(assigns, "excerpt", Map.get(post_record || %{}, :excerpt))
|
||||||
|
),
|
||||||
|
author: Map.get(post_record || %{}, :author),
|
||||||
|
language:
|
||||||
|
Map.get(
|
||||||
|
assigns,
|
||||||
|
:language,
|
||||||
|
Map.get(assigns, "language", Map.get(post_record || %{}, :language))
|
||||||
|
),
|
||||||
|
show_title: true,
|
||||||
|
published_at: Map.get(post_record || %{}, :published_at),
|
||||||
|
created_at: Map.get(post_record || %{}, :created_at),
|
||||||
|
updated_at: Map.get(post_record || %{}, :updated_at),
|
||||||
|
tags: Map.get(post_record || %{}, :tags, []) || [],
|
||||||
|
categories: Map.get(post_record || %{}, :categories, []) || [],
|
||||||
|
template_slug:
|
||||||
|
Map.get(
|
||||||
|
post_record || %{},
|
||||||
|
:template_slug,
|
||||||
|
Map.get(assigns, :template_slug, Map.get(assigns, "template_slug"))
|
||||||
|
),
|
||||||
|
do_not_translate: Map.get(post_record || %{}, :do_not_translate, false),
|
||||||
|
linked_media: [],
|
||||||
|
outgoing_links: [],
|
||||||
|
incoming_links: []
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_pagination(nil, posts) do
|
||||||
|
total_items = length(posts)
|
||||||
|
|
||||||
|
%{
|
||||||
|
current_page: 1,
|
||||||
|
total_pages: 1,
|
||||||
|
total_items: total_items,
|
||||||
|
items_per_page: total_items,
|
||||||
|
has_prev_page: false,
|
||||||
|
prev_page_href: "",
|
||||||
|
has_next_page: false,
|
||||||
|
next_page_href: ""
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalize_pagination(%{} = pagination, posts) do
|
||||||
|
total_items =
|
||||||
|
Map.get(pagination, :total_items, Map.get(pagination, "total_items", length(posts)))
|
||||||
|
|
||||||
|
items_per_page =
|
||||||
|
Map.get(pagination, :items_per_page, Map.get(pagination, "items_per_page", total_items))
|
||||||
|
|
||||||
|
%{
|
||||||
|
current_page: Map.get(pagination, :current_page, Map.get(pagination, "current_page", 1)),
|
||||||
|
total_pages: Map.get(pagination, :total_pages, Map.get(pagination, "total_pages", 1)),
|
||||||
|
total_items: total_items,
|
||||||
|
items_per_page: items_per_page,
|
||||||
|
has_prev_page:
|
||||||
|
Map.get(pagination, :has_prev_page, Map.get(pagination, "has_prev_page", false)),
|
||||||
|
prev_page_href:
|
||||||
|
Map.get(pagination, :prev_page_href, Map.get(pagination, "prev_page_href", "")),
|
||||||
|
has_next_page:
|
||||||
|
Map.get(pagination, :has_next_page, Map.get(pagination, "has_next_page", false)),
|
||||||
|
next_page_href:
|
||||||
|
Map.get(pagination, :next_page_href, Map.get(pagination, "next_page_href", ""))
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
defp normalize_archive_context(nil), do: nil
|
defp normalize_archive_context(nil), do: nil
|
||||||
defp normalize_archive_context(%{} = archive_context), do: archive_context
|
defp normalize_archive_context(%{} = archive_context), do: archive_context
|
||||||
|
|
||||||
@@ -365,10 +550,19 @@ defmodule BDS.Rendering do
|
|||||||
defp html_theme_attribute(""), do: nil
|
defp html_theme_attribute(""), do: nil
|
||||||
defp html_theme_attribute(theme), do: ~s(data-theme="#{theme}")
|
defp html_theme_attribute(theme), do: ~s(data-theme="#{theme}")
|
||||||
|
|
||||||
defp calendar_initial_year(%{created_at: created_at}) when is_integer(created_at), do: DateTime.from_unix!(created_at).year
|
defp default_pico_stylesheet_href, do: "/assets/pico.min.css"
|
||||||
|
|
||||||
|
defp href_for_language(""), do: "/"
|
||||||
|
defp href_for_language(prefix), do: prefix <> "/"
|
||||||
|
|
||||||
|
defp calendar_initial_year(%{created_at: created_at}) when is_integer(created_at),
|
||||||
|
do: DateTime.from_unix!(created_at).year
|
||||||
|
|
||||||
defp calendar_initial_year(_post), do: nil
|
defp calendar_initial_year(_post), do: nil
|
||||||
|
|
||||||
defp calendar_initial_month(%{created_at: created_at}) when is_integer(created_at), do: DateTime.from_unix!(created_at).month
|
defp calendar_initial_month(%{created_at: created_at}) when is_integer(created_at),
|
||||||
|
do: DateTime.from_unix!(created_at).month
|
||||||
|
|
||||||
defp calendar_initial_month(_post), do: nil
|
defp calendar_initial_month(_post), do: nil
|
||||||
|
|
||||||
defp calendar_initial_year_from_posts([post | _rest]), do: calendar_initial_year(post)
|
defp calendar_initial_year_from_posts([post | _rest]), do: calendar_initial_year(post)
|
||||||
|
|||||||
@@ -15,7 +15,16 @@ defmodule BDS.Rendering.Filters do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def markdown(value, _post_id, _post_data_json_by_id, canonical_post_paths, canonical_media_paths, language, _language_prefix, context) do
|
def markdown(
|
||||||
|
value,
|
||||||
|
_post_id,
|
||||||
|
_post_data_json_by_id,
|
||||||
|
canonical_post_paths,
|
||||||
|
canonical_media_paths,
|
||||||
|
language,
|
||||||
|
_language_prefix,
|
||||||
|
context
|
||||||
|
) do
|
||||||
value
|
value
|
||||||
|> to_string()
|
|> to_string()
|
||||||
|> replace_built_in_macros(language, context)
|
|> replace_built_in_macros(language, context)
|
||||||
@@ -24,21 +33,37 @@ defmodule BDS.Rendering.Filters do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp replace_built_in_macros(content, language, context) do
|
defp replace_built_in_macros(content, language, context) do
|
||||||
Regex.replace(~r/\[\[(\w+)(?:\s+([^\]]+))?\]\]/, content, fn full_match, macro_name, raw_params ->
|
Regex.replace(~r/\[\[(\w+)(?:\s+([^\]]+))?\]\]/, content, fn full_match,
|
||||||
|
macro_name,
|
||||||
|
raw_params ->
|
||||||
params = parse_macro_params(raw_params)
|
params = parse_macro_params(raw_params)
|
||||||
|
|
||||||
case String.downcase(macro_name) do
|
case String.downcase(macro_name) do
|
||||||
"youtube" ->
|
"youtube" ->
|
||||||
render_macro_template("macros/youtube", %{
|
render_macro_template(
|
||||||
|
"macros/youtube",
|
||||||
|
%{
|
||||||
"id" => Map.get(params, "id", ""),
|
"id" => Map.get(params, "id", ""),
|
||||||
"title" => default_macro_title(Map.get(params, "title"), language, "render.video.youtubeTitle")
|
"title" =>
|
||||||
}, context)
|
default_macro_title(
|
||||||
|
Map.get(params, "title"),
|
||||||
|
language,
|
||||||
|
"render.video.youtubeTitle"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
"vimeo" ->
|
"vimeo" ->
|
||||||
render_macro_template("macros/vimeo", %{
|
render_macro_template(
|
||||||
|
"macros/vimeo",
|
||||||
|
%{
|
||||||
"id" => Map.get(params, "id", ""),
|
"id" => Map.get(params, "id", ""),
|
||||||
"title" => default_macro_title(Map.get(params, "title"), language, "render.video.vimeoTitle")
|
"title" =>
|
||||||
}, context)
|
default_macro_title(Map.get(params, "title"), language, "render.video.vimeoTitle")
|
||||||
|
},
|
||||||
|
context
|
||||||
|
)
|
||||||
|
|
||||||
_other ->
|
_other ->
|
||||||
full_match
|
full_match
|
||||||
@@ -46,8 +71,12 @@ defmodule BDS.Rendering.Filters do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp default_macro_title(nil, language, translation_key), do: I18n.translate(language, translation_key)
|
defp default_macro_title(nil, language, translation_key),
|
||||||
defp default_macro_title("", language, translation_key), do: I18n.translate(language, translation_key)
|
do: I18n.translate(language, translation_key)
|
||||||
|
|
||||||
|
defp default_macro_title("", language, translation_key),
|
||||||
|
do: I18n.translate(language, translation_key)
|
||||||
|
|
||||||
defp default_macro_title(title, _language, _translation_key), do: title
|
defp default_macro_title(title, _language, _translation_key), do: title
|
||||||
|
|
||||||
defp parse_macro_params(nil), do: %{}
|
defp parse_macro_params(nil), do: %{}
|
||||||
@@ -63,8 +92,12 @@ defmodule BDS.Rendering.Filters do
|
|||||||
|
|
||||||
defp render_macro_template(template_path, assigns, context) do
|
defp render_macro_template(template_path, assigns, context) do
|
||||||
case Map.get(assigns, "id") do
|
case Map.get(assigns, "id") do
|
||||||
"" -> ""
|
"" ->
|
||||||
nil -> ""
|
""
|
||||||
|
|
||||||
|
nil ->
|
||||||
|
""
|
||||||
|
|
||||||
_id ->
|
_id ->
|
||||||
template_source = Liquex.FileSystem.read_template_file(context.file_system, template_path)
|
template_source = Liquex.FileSystem.read_template_file(context.file_system, template_path)
|
||||||
template_ast = Liquex.parse!(template_source)
|
template_ast = Liquex.parse!(template_source)
|
||||||
@@ -78,7 +111,10 @@ defmodule BDS.Rendering.Filters do
|
|||||||
|
|
||||||
defp rewrite_rendered_html_urls(html, canonical_post_paths, canonical_media_paths) do
|
defp rewrite_rendered_html_urls(html, canonical_post_paths, canonical_media_paths) do
|
||||||
html
|
html
|
||||||
|> rewrite_attribute("href", &normalize_post_href(&1, canonical_post_paths, canonical_media_paths))
|
|> rewrite_attribute(
|
||||||
|
"href",
|
||||||
|
&normalize_post_href(&1, canonical_post_paths, canonical_media_paths)
|
||||||
|
)
|
||||||
|> rewrite_attribute("src", &normalize_media_src(&1, canonical_media_paths))
|
|> rewrite_attribute("src", &normalize_media_src(&1, canonical_media_paths))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -91,8 +127,12 @@ defmodule BDS.Rendering.Filters do
|
|||||||
|
|
||||||
defp normalize_post_href(raw_href, canonical_post_paths, canonical_media_paths) do
|
defp normalize_post_href(raw_href, canonical_post_paths, canonical_media_paths) do
|
||||||
cond do
|
cond do
|
||||||
raw_href == "" -> raw_href
|
raw_href == "" ->
|
||||||
external_or_special_url?(raw_href) -> raw_href
|
raw_href
|
||||||
|
|
||||||
|
external_or_special_url?(raw_href) ->
|
||||||
|
raw_href
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{path_part, suffix} = split_path_suffix(raw_href)
|
{path_part, suffix} = split_path_suffix(raw_href)
|
||||||
|
|
||||||
@@ -103,7 +143,8 @@ defmodule BDS.Rendering.Filters do
|
|||||||
canonical -> canonical <> suffix
|
canonical -> canonical <> suffix
|
||||||
end
|
end
|
||||||
|
|
||||||
_other -> raw_href
|
_other ->
|
||||||
|
raw_href
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -136,8 +177,12 @@ defmodule BDS.Rendering.Filters do
|
|||||||
|
|
||||||
defp normalize_media_src(raw_src, canonical_media_paths) do
|
defp normalize_media_src(raw_src, canonical_media_paths) do
|
||||||
cond do
|
cond do
|
||||||
raw_src == "" -> raw_src
|
raw_src == "" ->
|
||||||
external_or_special_url?(raw_src) -> raw_src
|
raw_src
|
||||||
|
|
||||||
|
external_or_special_url?(raw_src) ->
|
||||||
|
raw_src
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
{path_part, suffix} = split_path_suffix(raw_src)
|
{path_part, suffix} = split_path_suffix(raw_src)
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,9 @@ defmodule BDS.Scripting do
|
|||||||
opts: batch_job_defaults(opts)}
|
opts: batch_job_defaults(opts)}
|
||||||
|
|
||||||
case DynamicSupervisor.start_child(BDS.Scripting.JobSupervisor, child_spec) do
|
case DynamicSupervisor.start_child(BDS.Scripting.JobSupervisor, child_spec) do
|
||||||
{:ok, _pid} -> {:ok, BDS.Scripting.JobStore.fetch_job!(job_id)}
|
{:ok, _pid} ->
|
||||||
|
{:ok, BDS.Scripting.JobStore.fetch_job!(job_id)}
|
||||||
|
|
||||||
{:error, reason} ->
|
{:error, reason} ->
|
||||||
:ok =
|
:ok =
|
||||||
BDS.Scripting.JobStore.update_job(job_id, %{
|
BDS.Scripting.JobStore.update_job(job_id, %{
|
||||||
@@ -91,7 +93,8 @@ defmodule BDS.Scripting do
|
|||||||
_job -> {:error, :not_running}
|
_job -> {:error, :not_running}
|
||||||
end
|
end
|
||||||
|
|
||||||
pid -> BDS.Scripting.JobRunner.cancel(pid)
|
pid ->
|
||||||
|
BDS.Scripting.JobRunner.cancel(pid)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ defmodule BDS.Scripting.JobStore do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def handle_call({:update_job, job_id, attrs}, _from, state) do
|
def handle_call({:update_job, job_id, attrs}, _from, state) do
|
||||||
next_state = update_in(state, [:jobs, job_id], fn
|
next_state =
|
||||||
|
update_in(state, [:jobs, job_id], fn
|
||||||
nil -> nil
|
nil -> nil
|
||||||
job -> Map.merge(job, attrs)
|
job -> Map.merge(job, attrs)
|
||||||
end)
|
end)
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ defmodule BDS.Scripting.Lua do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp install_progress_callback(_state, callback), do: {:error, {:invalid_progress_callback, callback}}
|
defp install_progress_callback(_state, callback),
|
||||||
|
do: {:error, {:invalid_progress_callback, callback}}
|
||||||
|
|
||||||
defp install_capabilities(state, capabilities) when capabilities in [%{}, []], do: {:ok, state}
|
defp install_capabilities(state, capabilities) when capabilities in [%{}, []], do: {:ok, state}
|
||||||
|
|
||||||
@@ -81,7 +82,8 @@ defmodule BDS.Scripting.Lua do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp install_capabilities(_state, capabilities), do: {:error, {:invalid_capabilities, capabilities}}
|
defp install_capabilities(_state, capabilities),
|
||||||
|
do: {:error, {:invalid_capabilities, capabilities}}
|
||||||
|
|
||||||
defp normalize_progress_payload(payload) when is_list(payload) do
|
defp normalize_progress_payload(payload) when is_list(payload) do
|
||||||
if Enum.all?(payload, &match?({key, _value} when is_binary(key) or is_atom(key), &1)) do
|
if Enum.all?(payload, &match?({key, _value} when is_binary(key) or is_atom(key), &1)) do
|
||||||
|
|||||||
@@ -50,11 +50,19 @@ defmodule BDS.Scripts do
|
|||||||
:ok =
|
:ok =
|
||||||
File.write(
|
File.write(
|
||||||
full_path,
|
full_path,
|
||||||
serialize_script_file(%{script | status: :published, file_path: file_path, updated_at: updated_at}, content)
|
serialize_script_file(
|
||||||
|
%{script | status: :published, file_path: file_path, updated_at: updated_at},
|
||||||
|
content
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
script
|
script
|
||||||
|> Script.changeset(%{status: :published, file_path: file_path, content: nil, updated_at: updated_at})
|
|> Script.changeset(%{
|
||||||
|
status: :published,
|
||||||
|
file_path: file_path,
|
||||||
|
content: nil,
|
||||||
|
updated_at: updated_at
|
||||||
|
})
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -75,7 +83,8 @@ defmodule BDS.Scripts do
|
|||||||
content_changed? = has_attr?(attrs, :content) and attr(attrs, :content) != script.content
|
content_changed? = has_attr?(attrs, :content) and attr(attrs, :content) != script.content
|
||||||
now = System.system_time(:second)
|
now = System.system_time(:second)
|
||||||
|
|
||||||
updates = %{}
|
updates =
|
||||||
|
%{}
|
||||||
|> maybe_put(:title, attr(attrs, :title))
|
|> maybe_put(:title, attr(attrs, :title))
|
||||||
|> maybe_put(:kind, attr(attrs, :kind))
|
|> maybe_put(:kind, attr(attrs, :kind))
|
||||||
|> maybe_put(:entrypoint, attr(attrs, :entrypoint))
|
|> maybe_put(:entrypoint, attr(attrs, :entrypoint))
|
||||||
@@ -84,7 +93,10 @@ defmodule BDS.Scripts do
|
|||||||
|> Map.put(:slug, next_slug)
|
|> Map.put(:slug, next_slug)
|
||||||
|> Map.put(:version, script.version + 1)
|
|> Map.put(:version, script.version + 1)
|
||||||
|> Map.put(:updated_at, now)
|
|> Map.put(:updated_at, now)
|
||||||
|> maybe_put(:status, if(script.status == :published and content_changed?, do: :draft, else: nil))
|
|> maybe_put(
|
||||||
|
:status,
|
||||||
|
if(script.status == :published and content_changed?, do: :draft, else: nil)
|
||||||
|
)
|
||||||
|
|
||||||
script
|
script
|
||||||
|> Script.changeset(updates)
|
|> Script.changeset(updates)
|
||||||
@@ -141,7 +153,8 @@ defmodule BDS.Scripts do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp slug_available?(project_id, slug, exclude_id) do
|
defp slug_available?(project_id, slug, exclude_id) do
|
||||||
query = from script in Script, where: script.project_id == ^project_id and script.slug == ^slug
|
query =
|
||||||
|
from script in Script, where: script.project_id == ^project_id and script.slug == ^slug
|
||||||
|
|
||||||
scoped_query =
|
scoped_query =
|
||||||
case exclude_id do
|
case exclude_id do
|
||||||
|
|||||||
@@ -25,10 +25,38 @@ defmodule BDS.Scripts.Script do
|
|||||||
|
|
||||||
def changeset(script, attrs) do
|
def changeset(script, attrs) do
|
||||||
script
|
script
|
||||||
|> cast(attrs, [:id, :project_id, :slug, :title, :kind, :entrypoint, :enabled, :version, :file_path, :status, :content, :created_at, :updated_at],
|
|> cast(
|
||||||
|
attrs,
|
||||||
|
[
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:slug,
|
||||||
|
:title,
|
||||||
|
:kind,
|
||||||
|
:entrypoint,
|
||||||
|
:enabled,
|
||||||
|
:version,
|
||||||
|
:file_path,
|
||||||
|
:status,
|
||||||
|
:content,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
],
|
||||||
empty_values: [nil]
|
empty_values: [nil]
|
||||||
)
|
)
|
||||||
|> validate_required([:id, :project_id, :slug, :title, :kind, :entrypoint, :enabled, :version, :status, :created_at, :updated_at])
|
|> validate_required([
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:slug,
|
||||||
|
:title,
|
||||||
|
:kind,
|
||||||
|
:entrypoint,
|
||||||
|
:enabled,
|
||||||
|
:version,
|
||||||
|
:status,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
])
|
||||||
|> assoc_constraint(:project)
|
|> assoc_constraint(:project)
|
||||||
|> unique_constraint(:slug, name: :scripts_project_slug_idx)
|
|> unique_constraint(:slug, name: :scripts_project_slug_idx)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -96,8 +96,15 @@ defmodule BDS.Search do
|
|||||||
end
|
end
|
||||||
|
|
||||||
def reindex_project(project_id) do
|
def reindex_project(project_id) do
|
||||||
Repo.query!("DELETE FROM posts_fts WHERE post_id IN (SELECT id FROM posts WHERE project_id = ?)", [project_id])
|
Repo.query!(
|
||||||
Repo.query!("DELETE FROM media_fts WHERE media_id IN (SELECT id FROM media WHERE project_id = ?)", [project_id])
|
"DELETE FROM posts_fts WHERE post_id IN (SELECT id FROM posts WHERE project_id = ?)",
|
||||||
|
[project_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
Repo.query!(
|
||||||
|
"DELETE FROM media_fts WHERE media_id IN (SELECT id FROM media WHERE project_id = ?)",
|
||||||
|
[project_id]
|
||||||
|
)
|
||||||
|
|
||||||
Repo.all(from post in Post, where: post.project_id == ^project_id)
|
Repo.all(from post in Post, where: post.project_id == ^project_id)
|
||||||
|> Enum.each(&sync_post/1)
|
|> Enum.each(&sync_post/1)
|
||||||
@@ -241,7 +248,11 @@ defmodule BDS.Search do
|
|||||||
matches_month?(post, filters.month) and
|
matches_month?(post, filters.month) and
|
||||||
matches_from?(post, filters.from) and
|
matches_from?(post, filters.from) and
|
||||||
matches_to?(post, filters.to) and
|
matches_to?(post, filters.to) and
|
||||||
matches_missing_translation?(post, filters.missing_translation_language, translation_languages)
|
matches_missing_translation?(
|
||||||
|
post,
|
||||||
|
filters.missing_translation_language,
|
||||||
|
translation_languages
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -270,7 +281,13 @@ defmodule BDS.Search do
|
|||||||
defp matches_to?(post, to_unix), do: post.created_at <= to_unix
|
defp matches_to?(post, to_unix), do: post.created_at <= to_unix
|
||||||
|
|
||||||
defp matches_missing_translation?(_post, nil, _translation_languages), do: true
|
defp matches_missing_translation?(_post, nil, _translation_languages), do: true
|
||||||
defp matches_missing_translation?(%Post{do_not_translate: true}, _language, _translation_languages), do: false
|
|
||||||
|
defp matches_missing_translation?(
|
||||||
|
%Post{do_not_translate: true},
|
||||||
|
_language,
|
||||||
|
_translation_languages
|
||||||
|
),
|
||||||
|
do: false
|
||||||
|
|
||||||
defp matches_missing_translation?(post, language, translation_languages) do
|
defp matches_missing_translation?(post, language, translation_languages) do
|
||||||
language not in Map.get(translation_languages, post.id, [])
|
language not in Map.get(translation_languages, post.id, [])
|
||||||
@@ -286,7 +303,9 @@ defmodule BDS.Search do
|
|||||||
"SELECT translation_for, language FROM post_translations WHERE translation_for IN (#{placeholders})",
|
"SELECT translation_for, language FROM post_translations WHERE translation_for IN (#{placeholders})",
|
||||||
post_ids
|
post_ids
|
||||||
).rows
|
).rows
|
||||||
|> Enum.group_by(fn [post_id, _language] -> post_id end, fn [_post_id, language] -> language end)
|
|> Enum.group_by(fn [post_id, _language] -> post_id end, fn [_post_id, language] ->
|
||||||
|
language
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp paginate(items, filters) do
|
defp paginate(items, filters) do
|
||||||
@@ -300,16 +319,27 @@ defmodule BDS.Search do
|
|||||||
post_language = normalize_language(post.language)
|
post_language = normalize_language(post.language)
|
||||||
|
|
||||||
title =
|
title =
|
||||||
[stem(post.title, post_language) | Enum.map(translations, &stem(Map.get(&1, "title"), Map.get(&1, "language")))]
|
[
|
||||||
|
stem(post.title, post_language)
|
||||||
|
| Enum.map(translations, &stem(Map.get(&1, "title"), Map.get(&1, "language")))
|
||||||
|
]
|
||||||
|> join_text()
|
|> join_text()
|
||||||
|
|
||||||
excerpt =
|
excerpt =
|
||||||
[stem(post.excerpt, post_language) | Enum.map(translations, &stem(Map.get(&1, "excerpt"), Map.get(&1, "language")))]
|
[
|
||||||
|
stem(post.excerpt, post_language)
|
||||||
|
| Enum.map(translations, &stem(Map.get(&1, "excerpt"), Map.get(&1, "language")))
|
||||||
|
]
|
||||||
|> join_text()
|
|> join_text()
|
||||||
|
|
||||||
content =
|
content =
|
||||||
[stem(post_content(post), post_language) |
|
[
|
||||||
Enum.map(translations, &stem(translation_content(post.project_id, &1), Map.get(&1, "language")))]
|
stem(post_content(post), post_language)
|
||||||
|
| Enum.map(
|
||||||
|
translations,
|
||||||
|
&stem(translation_content(post.project_id, &1), Map.get(&1, "language"))
|
||||||
|
)
|
||||||
|
]
|
||||||
|> join_text()
|
|> join_text()
|
||||||
|
|
||||||
tags = stem(Enum.join(post.tags || [], " "), post_language)
|
tags = stem(Enum.join(post.tags || [], " "), post_language)
|
||||||
@@ -320,15 +350,25 @@ defmodule BDS.Search do
|
|||||||
|
|
||||||
defp media_index_fields(media) do
|
defp media_index_fields(media) do
|
||||||
translations =
|
translations =
|
||||||
Repo.all(from translation in MediaTranslation, where: translation.translation_for == ^media.id)
|
Repo.all(
|
||||||
|
from translation in MediaTranslation, where: translation.translation_for == ^media.id
|
||||||
|
)
|
||||||
|
|
||||||
media_language = normalize_language(media.language)
|
media_language = normalize_language(media.language)
|
||||||
|
|
||||||
title = [stem(media.title, media_language) | Enum.map(translations, &stem(&1.title, &1.language))] |> join_text()
|
title =
|
||||||
alt = [stem(media.alt, media_language) | Enum.map(translations, &stem(&1.alt, &1.language))] |> join_text()
|
[stem(media.title, media_language) | Enum.map(translations, &stem(&1.title, &1.language))]
|
||||||
|
|> join_text()
|
||||||
|
|
||||||
|
alt =
|
||||||
|
[stem(media.alt, media_language) | Enum.map(translations, &stem(&1.alt, &1.language))]
|
||||||
|
|> join_text()
|
||||||
|
|
||||||
caption =
|
caption =
|
||||||
[stem(media.caption, media_language) | Enum.map(translations, &stem(&1.caption, &1.language))]
|
[
|
||||||
|
stem(media.caption, media_language)
|
||||||
|
| Enum.map(translations, &stem(&1.caption, &1.language))
|
||||||
|
]
|
||||||
|> join_text()
|
|> join_text()
|
||||||
|
|
||||||
original_name = stem(media.original_name || "", media_language)
|
original_name = stem(media.original_name || "", media_language)
|
||||||
@@ -356,7 +396,8 @@ defmodule BDS.Search do
|
|||||||
|
|
||||||
defp post_content(%Post{content: content}) when is_binary(content), do: content
|
defp post_content(%Post{content: content}) when is_binary(content), do: content
|
||||||
|
|
||||||
defp post_content(%Post{project_id: project_id, file_path: file_path}) when is_binary(file_path) and file_path != "" do
|
defp post_content(%Post{project_id: project_id, file_path: file_path})
|
||||||
|
when is_binary(file_path) and file_path != "" do
|
||||||
project_id
|
project_id
|
||||||
|> Projects.get_project!()
|
|> Projects.get_project!()
|
||||||
|> Projects.project_data_dir()
|
|> Projects.project_data_dir()
|
||||||
@@ -366,9 +407,11 @@ defmodule BDS.Search do
|
|||||||
|
|
||||||
defp post_content(_post), do: ""
|
defp post_content(_post), do: ""
|
||||||
|
|
||||||
defp translation_content(_project_id, %{"content" => content}) when is_binary(content), do: content
|
defp translation_content(_project_id, %{"content" => content}) when is_binary(content),
|
||||||
|
do: content
|
||||||
|
|
||||||
defp translation_content(project_id, %{"status" => "published", "file_path" => file_path}) when is_binary(file_path) and file_path != "" do
|
defp translation_content(project_id, %{"status" => "published", "file_path" => file_path})
|
||||||
|
when is_binary(file_path) and file_path != "" do
|
||||||
project_id
|
project_id
|
||||||
|> Projects.get_project!()
|
|> Projects.get_project!()
|
||||||
|> Projects.project_data_dir()
|
|> Projects.project_data_dir()
|
||||||
@@ -403,7 +446,7 @@ defmodule BDS.Search do
|
|||||||
|> Enum.map_join(" OR ", fn tokens ->
|
|> Enum.map_join(" OR ", fn tokens ->
|
||||||
tokens
|
tokens
|
||||||
|> Enum.map_join(" AND ", "ed_term/1)
|
|> Enum.map_join(" AND ", "ed_term/1)
|
||||||
|> then(&"(" <> &1 <> ")")
|
|> then(&("(" <> &1 <> ")"))
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -495,7 +538,10 @@ defmodule BDS.Search do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp normalize_non_negative_integer(nil, default), do: default
|
defp normalize_non_negative_integer(nil, default), do: default
|
||||||
defp normalize_non_negative_integer(value, _default) when is_integer(value) and value >= 0, do: value
|
|
||||||
|
defp normalize_non_negative_integer(value, _default) when is_integer(value) and value >= 0,
|
||||||
|
do: value
|
||||||
|
|
||||||
defp normalize_non_negative_integer(value, default), do: normalize_integer(value) || default
|
defp normalize_non_negative_integer(value, default), do: normalize_integer(value) || default
|
||||||
|
|
||||||
defp normalize_timestamp(nil, _position), do: nil
|
defp normalize_timestamp(nil, _position), do: nil
|
||||||
@@ -508,7 +554,8 @@ defmodule BDS.Search do
|
|||||||
{:ok, datetime} = DateTime.new(date, time, "Etc/UTC")
|
{:ok, datetime} = DateTime.new(date, time, "Etc/UTC")
|
||||||
DateTime.to_unix(datetime)
|
DateTime.to_unix(datetime)
|
||||||
|
|
||||||
{:error, _reason} -> nil
|
{:error, _reason} ->
|
||||||
|
nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,10 @@ defmodule BDS.Tags do
|
|||||||
|
|
||||||
target_tag ->
|
target_tag ->
|
||||||
source_tags =
|
source_tags =
|
||||||
Repo.all(from tag in Tag, where: tag.id in ^source_tag_ids and tag.project_id == ^target_tag.project_id)
|
Repo.all(
|
||||||
|
from tag in Tag,
|
||||||
|
where: tag.id in ^source_tag_ids and tag.project_id == ^target_tag.project_id
|
||||||
|
)
|
||||||
|
|
||||||
Repo.transaction(fn ->
|
Repo.transaction(fn ->
|
||||||
source_names = Enum.map(source_tags, & &1.name)
|
source_names = Enum.map(source_tags, & &1.name)
|
||||||
@@ -227,10 +230,21 @@ defmodule BDS.Tags do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp validate_unique_name(project_id, name) do
|
defp validate_unique_name(project_id, name) do
|
||||||
if Repo.exists?(from tag in Tag, where: tag.project_id == ^project_id and fragment("lower(?)", tag.name) == ^String.downcase(name)) do
|
if Repo.exists?(
|
||||||
|
from tag in Tag,
|
||||||
|
where:
|
||||||
|
tag.project_id == ^project_id and
|
||||||
|
fragment("lower(?)", tag.name) == ^String.downcase(name)
|
||||||
|
) do
|
||||||
{:error,
|
{:error,
|
||||||
%Tag{}
|
%Tag{}
|
||||||
|> Tag.changeset(%{project_id: project_id, name: name, id: Ecto.UUID.generate(), created_at: 0, updated_at: 0})
|
|> Tag.changeset(%{
|
||||||
|
project_id: project_id,
|
||||||
|
name: name,
|
||||||
|
id: Ecto.UUID.generate(),
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0
|
||||||
|
})
|
||||||
|> Ecto.Changeset.add_error(:name, "has already been taken")}
|
|> Ecto.Changeset.add_error(:name, "has already been taken")}
|
||||||
else
|
else
|
||||||
:ok
|
:ok
|
||||||
@@ -238,10 +252,21 @@ defmodule BDS.Tags do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp validate_rename_target(project_id, tag_id, name) do
|
defp validate_rename_target(project_id, tag_id, name) do
|
||||||
if Repo.exists?(from tag in Tag, where: tag.project_id == ^project_id and tag.id != ^tag_id and fragment("lower(?)", tag.name) == ^String.downcase(name)) do
|
if Repo.exists?(
|
||||||
|
from tag in Tag,
|
||||||
|
where:
|
||||||
|
tag.project_id == ^project_id and tag.id != ^tag_id and
|
||||||
|
fragment("lower(?)", tag.name) == ^String.downcase(name)
|
||||||
|
) do
|
||||||
{:error,
|
{:error,
|
||||||
%Tag{}
|
%Tag{}
|
||||||
|> Tag.changeset(%{project_id: project_id, name: name, id: tag_id, created_at: 0, updated_at: 0})
|
|> Tag.changeset(%{
|
||||||
|
project_id: project_id,
|
||||||
|
name: name,
|
||||||
|
id: tag_id,
|
||||||
|
created_at: 0,
|
||||||
|
updated_at: 0
|
||||||
|
})
|
||||||
|> Ecto.Changeset.add_error(:name, "has already been taken")}
|
|> Ecto.Changeset.add_error(:name, "has already been taken")}
|
||||||
else
|
else
|
||||||
:ok
|
:ok
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ defmodule BDS.Tags.Tag do
|
|||||||
|
|
||||||
def changeset(tag, attrs) do
|
def changeset(tag, attrs) do
|
||||||
tag
|
tag
|
||||||
|> cast(attrs, [:id, :project_id, :name, :color, :post_template_slug, :created_at, :updated_at],
|
|> cast(
|
||||||
|
attrs,
|
||||||
|
[:id, :project_id, :name, :color, :post_template_slug, :created_at, :updated_at],
|
||||||
empty_values: [nil]
|
empty_values: [nil]
|
||||||
)
|
)
|
||||||
|> validate_required([:id, :project_id, :name, :created_at, :updated_at])
|
|> validate_required([:id, :project_id, :name, :created_at, :updated_at])
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ defmodule BDS.Tasks do
|
|||||||
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
|
||||||
end
|
end
|
||||||
|
|
||||||
def submit_task(name, work, attrs \\ %{}) when is_binary(name) and is_function(work, 1) and is_map(attrs) do
|
def submit_task(name, work, attrs \\ %{})
|
||||||
|
when is_binary(name) and is_function(work, 1) and is_map(attrs) do
|
||||||
GenServer.call(__MODULE__, {:submit_task, name, work, attrs})
|
GenServer.call(__MODULE__, {:submit_task, name, work, attrs})
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -57,7 +58,8 @@ defmodule BDS.Tasks do
|
|||||||
if map_size(next_state.running) < max_concurrent() do
|
if map_size(next_state.running) < max_concurrent() do
|
||||||
{:reply, {:ok, public_task(task)}, start_task(next_state, task.id, work)}
|
{:reply, {:ok, public_task(task)}, start_task(next_state, task.id, work)}
|
||||||
else
|
else
|
||||||
{:reply, {:ok, public_task(task)}, %{next_state | queue: next_state.queue ++ [{task.id, work}]}}
|
{:reply, {:ok, public_task(task)},
|
||||||
|
%{next_state | queue: next_state.queue ++ [{task.id, work}]}}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -83,7 +85,9 @@ defmodule BDS.Tasks do
|
|||||||
next_state =
|
next_state =
|
||||||
state
|
state
|
||||||
|> update_task(task_id, %{status: :cancelled, finished_at: DateTime.utc_now()})
|
|> update_task(task_id, %{status: :cancelled, finished_at: DateTime.utc_now()})
|
||||||
|> Map.update!(:queue, fn queue -> Enum.reject(queue, fn {queued_id, _work} -> queued_id == task_id end) end)
|
|> Map.update!(:queue, fn queue ->
|
||||||
|
Enum.reject(queue, fn {queued_id, _work} -> queued_id == task_id end)
|
||||||
|
end)
|
||||||
|> start_queued_tasks()
|
|> start_queued_tasks()
|
||||||
|
|
||||||
{:reply, :ok, next_state}
|
{:reply, :ok, next_state}
|
||||||
@@ -109,7 +113,11 @@ defmodule BDS.Tasks do
|
|||||||
def handle_call({:complete_task, task_id}, _from, state) do
|
def handle_call({:complete_task, task_id}, _from, state) do
|
||||||
next_state =
|
next_state =
|
||||||
state
|
state
|
||||||
|> update_task(task_id, %{status: :completed, progress: 1.0, finished_at: DateTime.utc_now()})
|
|> update_task(task_id, %{
|
||||||
|
status: :completed,
|
||||||
|
progress: 1.0,
|
||||||
|
finished_at: DateTime.utc_now()
|
||||||
|
})
|
||||||
|> start_queued_tasks()
|
|> start_queued_tasks()
|
||||||
|
|
||||||
{:reply, :ok, next_state}
|
{:reply, :ok, next_state}
|
||||||
@@ -118,7 +126,11 @@ defmodule BDS.Tasks do
|
|||||||
def handle_call({:fail_task, task_id, error_message}, _from, state) do
|
def handle_call({:fail_task, task_id, error_message}, _from, state) do
|
||||||
next_state =
|
next_state =
|
||||||
state
|
state
|
||||||
|> update_task(task_id, %{status: :failed, message: error_message, finished_at: DateTime.utc_now()})
|
|> update_task(task_id, %{
|
||||||
|
status: :failed,
|
||||||
|
message: error_message,
|
||||||
|
finished_at: DateTime.utc_now()
|
||||||
|
})
|
||||||
|> start_queued_tasks()
|
|> start_queued_tasks()
|
||||||
|
|
||||||
{:reply, :ok, next_state}
|
{:reply, :ok, next_state}
|
||||||
@@ -146,8 +158,16 @@ defmodule BDS.Tasks do
|
|||||||
_status ->
|
_status ->
|
||||||
attrs =
|
attrs =
|
||||||
case normalize_result(result) do
|
case normalize_result(result) do
|
||||||
{:ok, value} -> %{status: :completed, result: value, progress: 1.0, finished_at: DateTime.utc_now()}
|
{:ok, value} ->
|
||||||
{:error, reason} -> %{status: :failed, error: reason, finished_at: DateTime.utc_now()}
|
%{
|
||||||
|
status: :completed,
|
||||||
|
result: value,
|
||||||
|
progress: 1.0,
|
||||||
|
finished_at: DateTime.utc_now()
|
||||||
|
}
|
||||||
|
|
||||||
|
{:error, reason} ->
|
||||||
|
%{status: :failed, error: reason, finished_at: DateTime.utc_now()}
|
||||||
end
|
end
|
||||||
|
|
||||||
update_task(state, task_id, attrs)
|
update_task(state, task_id, attrs)
|
||||||
@@ -176,7 +196,11 @@ defmodule BDS.Tasks do
|
|||||||
state
|
state
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
update_task(state, task_id, %{status: :failed, error: reason, finished_at: DateTime.utc_now()})
|
update_task(state, task_id, %{
|
||||||
|
status: :failed,
|
||||||
|
error: reason,
|
||||||
|
finished_at: DateTime.utc_now()
|
||||||
|
})
|
||||||
end
|
end
|
||||||
|> remove_running(task_id, ref)
|
|> remove_running(task_id, ref)
|
||||||
|> start_queued_tasks()
|
|> start_queued_tasks()
|
||||||
@@ -186,7 +210,9 @@ defmodule BDS.Tasks do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp start_task(state, task_id, work) do
|
defp start_task(state, task_id, work) do
|
||||||
reporter = fn value, message -> send(__MODULE__, {:task_progress, task_id, value, message}) end
|
reporter = fn value, message ->
|
||||||
|
send(__MODULE__, {:task_progress, task_id, value, message})
|
||||||
|
end
|
||||||
|
|
||||||
task =
|
task =
|
||||||
Task.Supervisor.async_nolink(BDS.Tasks.TaskSupervisor, fn ->
|
Task.Supervisor.async_nolink(BDS.Tasks.TaskSupervisor, fn ->
|
||||||
@@ -209,6 +235,7 @@ defmodule BDS.Tasks do
|
|||||||
|
|
||||||
true ->
|
true ->
|
||||||
[{task_id, work} | remaining] = state.queue
|
[{task_id, work} | remaining] = state.queue
|
||||||
|
|
||||||
state
|
state
|
||||||
|> Map.put(:queue, remaining)
|
|> Map.put(:queue, remaining)
|
||||||
|> start_task(task_id, work)
|
|> start_task(task_id, work)
|
||||||
@@ -231,8 +258,13 @@ defmodule BDS.Tasks do
|
|||||||
now_ms = System.monotonic_time(:millisecond)
|
now_ms = System.monotonic_time(:millisecond)
|
||||||
last_reported_at = Map.get(task, :last_reported_at)
|
last_reported_at = Map.get(task, :last_reported_at)
|
||||||
|
|
||||||
if is_nil(last_reported_at) or now_ms - last_reported_at >= progress_throttle_ms() or value == 1.0 do
|
if is_nil(last_reported_at) or now_ms - last_reported_at >= progress_throttle_ms() or
|
||||||
update_task(state, task_id, %{progress: value, message: message, last_reported_at: now_ms})
|
value == 1.0 do
|
||||||
|
update_task(state, task_id, %{
|
||||||
|
progress: value,
|
||||||
|
message: message,
|
||||||
|
last_reported_at: now_ms
|
||||||
|
})
|
||||||
else
|
else
|
||||||
state
|
state
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -50,11 +50,19 @@ defmodule BDS.Templates do
|
|||||||
:ok =
|
:ok =
|
||||||
File.write(
|
File.write(
|
||||||
full_path,
|
full_path,
|
||||||
serialize_template_file(%{template | status: :published, file_path: file_path, updated_at: updated_at}, content)
|
serialize_template_file(
|
||||||
|
%{template | status: :published, file_path: file_path, updated_at: updated_at},
|
||||||
|
content
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
template
|
template
|
||||||
|> Template.changeset(%{status: :published, file_path: file_path, content: nil, updated_at: updated_at})
|
|> Template.changeset(%{
|
||||||
|
status: :published,
|
||||||
|
file_path: file_path,
|
||||||
|
content: nil,
|
||||||
|
updated_at: updated_at
|
||||||
|
})
|
||||||
|> Repo.update()
|
|> Repo.update()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -67,18 +75,32 @@ defmodule BDS.Templates do
|
|||||||
template ->
|
template ->
|
||||||
next_slug =
|
next_slug =
|
||||||
if has_attr?(attrs, :slug) do
|
if has_attr?(attrs, :slug) do
|
||||||
unique_slug(template.project_id, Slug.slugify(attr(attrs, :slug)), "template", template.id)
|
unique_slug(
|
||||||
|
template.project_id,
|
||||||
|
Slug.slugify(attr(attrs, :slug)),
|
||||||
|
"template",
|
||||||
|
template.id
|
||||||
|
)
|
||||||
else
|
else
|
||||||
template.slug
|
template.slug
|
||||||
end
|
end
|
||||||
|
|
||||||
content_changed? = has_attr?(attrs, :content) and attr(attrs, :content) != template.content
|
content_changed? =
|
||||||
|
has_attr?(attrs, :content) and attr(attrs, :content) != template.content
|
||||||
|
|
||||||
slug_changed? = next_slug != template.slug
|
slug_changed? = next_slug != template.slug
|
||||||
now = System.system_time(:second)
|
now = System.system_time(:second)
|
||||||
next_status = if(template.status == :published and content_changed?, do: :draft, else: template.status)
|
|
||||||
|
next_status =
|
||||||
|
if(template.status == :published and content_changed?,
|
||||||
|
do: :draft,
|
||||||
|
else: template.status
|
||||||
|
)
|
||||||
|
|
||||||
next_file_path = next_template_file_path(template, next_slug)
|
next_file_path = next_template_file_path(template, next_slug)
|
||||||
|
|
||||||
updates = %{}
|
updates =
|
||||||
|
%{}
|
||||||
|> maybe_put(:title, attr(attrs, :title))
|
|> maybe_put(:title, attr(attrs, :title))
|
||||||
|> maybe_put(:kind, attr(attrs, :kind))
|
|> maybe_put(:kind, attr(attrs, :kind))
|
||||||
|> maybe_put(:enabled, attr(attrs, :enabled))
|
|> maybe_put(:enabled, attr(attrs, :enabled))
|
||||||
@@ -172,7 +194,9 @@ defmodule BDS.Templates do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp slug_available?(project_id, slug, exclude_id) do
|
defp slug_available?(project_id, slug, exclude_id) do
|
||||||
query = from template in Template, where: template.project_id == ^project_id and template.slug == ^slug
|
query =
|
||||||
|
from template in Template,
|
||||||
|
where: template.project_id == ^project_id and template.slug == ^slug
|
||||||
|
|
||||||
scoped_query =
|
scoped_query =
|
||||||
case exclude_id do
|
case exclude_id do
|
||||||
@@ -210,11 +234,23 @@ defmodule BDS.Templates do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp count_referencing_posts(template) do
|
defp count_referencing_posts(template) do
|
||||||
Repo.aggregate(from(post in BDS.Posts.Post, where: post.project_id == ^template.project_id and post.template_slug == ^template.slug), :count, :id)
|
Repo.aggregate(
|
||||||
|
from(post in BDS.Posts.Post,
|
||||||
|
where: post.project_id == ^template.project_id and post.template_slug == ^template.slug
|
||||||
|
),
|
||||||
|
:count,
|
||||||
|
:id
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp count_referencing_tags(template) do
|
defp count_referencing_tags(template) do
|
||||||
Repo.aggregate(from(tag in BDS.Tags.Tag, where: tag.project_id == ^template.project_id and tag.post_template_slug == ^template.slug), :count, :id)
|
Repo.aggregate(
|
||||||
|
from(tag in BDS.Tags.Tag,
|
||||||
|
where: tag.project_id == ^template.project_id and tag.post_template_slug == ^template.slug
|
||||||
|
),
|
||||||
|
:count,
|
||||||
|
:id
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp clear_template_references(template) do
|
defp clear_template_references(template) do
|
||||||
@@ -227,10 +263,14 @@ defmodule BDS.Templates do
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
from(post in BDS.Posts.Post, where: post.project_id == ^template.project_id and post.template_slug == ^template.slug)
|
from(post in BDS.Posts.Post,
|
||||||
|
where: post.project_id == ^template.project_id and post.template_slug == ^template.slug
|
||||||
|
)
|
||||||
|> Repo.update_all(set: [template_slug: nil, updated_at: now])
|
|> Repo.update_all(set: [template_slug: nil, updated_at: now])
|
||||||
|
|
||||||
from(tag in BDS.Tags.Tag, where: tag.project_id == ^template.project_id and tag.post_template_slug == ^template.slug)
|
from(tag in BDS.Tags.Tag,
|
||||||
|
where: tag.project_id == ^template.project_id and tag.post_template_slug == ^template.slug
|
||||||
|
)
|
||||||
|> Repo.update_all(set: [post_template_slug: nil, updated_at: now])
|
|> Repo.update_all(set: [post_template_slug: nil, updated_at: now])
|
||||||
|
|
||||||
Enum.each(affected_posts, fn post ->
|
Enum.each(affected_posts, fn post ->
|
||||||
@@ -246,17 +286,23 @@ defmodule BDS.Templates do
|
|||||||
affected_posts =
|
affected_posts =
|
||||||
Repo.all(
|
Repo.all(
|
||||||
from(post in BDS.Posts.Post,
|
from(post in BDS.Posts.Post,
|
||||||
where: post.project_id == ^original_template.project_id and post.template_slug == ^original_template.slug
|
where:
|
||||||
|
post.project_id == ^original_template.project_id and
|
||||||
|
post.template_slug == ^original_template.slug
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
from(post in BDS.Posts.Post,
|
from(post in BDS.Posts.Post,
|
||||||
where: post.project_id == ^original_template.project_id and post.template_slug == ^original_template.slug
|
where:
|
||||||
|
post.project_id == ^original_template.project_id and
|
||||||
|
post.template_slug == ^original_template.slug
|
||||||
)
|
)
|
||||||
|> Repo.update_all(set: [template_slug: updated_template.slug, updated_at: updated_at])
|
|> Repo.update_all(set: [template_slug: updated_template.slug, updated_at: updated_at])
|
||||||
|
|
||||||
from(tag in BDS.Tags.Tag,
|
from(tag in BDS.Tags.Tag,
|
||||||
where: tag.project_id == ^original_template.project_id and tag.post_template_slug == ^original_template.slug
|
where:
|
||||||
|
tag.project_id == ^original_template.project_id and
|
||||||
|
tag.post_template_slug == ^original_template.slug
|
||||||
)
|
)
|
||||||
|> Repo.update_all(set: [post_template_slug: updated_template.slug, updated_at: updated_at])
|
|> Repo.update_all(set: [post_template_slug: updated_template.slug, updated_at: updated_at])
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,36 @@ defmodule BDS.Templates.Template do
|
|||||||
|
|
||||||
def changeset(template, attrs) do
|
def changeset(template, attrs) do
|
||||||
template
|
template
|
||||||
|> cast(attrs, [:id, :project_id, :slug, :title, :kind, :enabled, :version, :file_path, :status, :content, :created_at, :updated_at],
|
|> cast(
|
||||||
|
attrs,
|
||||||
|
[
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:slug,
|
||||||
|
:title,
|
||||||
|
:kind,
|
||||||
|
:enabled,
|
||||||
|
:version,
|
||||||
|
:file_path,
|
||||||
|
:status,
|
||||||
|
:content,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
],
|
||||||
empty_values: [nil]
|
empty_values: [nil]
|
||||||
)
|
)
|
||||||
|> validate_required([:id, :project_id, :slug, :title, :kind, :enabled, :version, :status, :created_at, :updated_at])
|
|> validate_required([
|
||||||
|
:id,
|
||||||
|
:project_id,
|
||||||
|
:slug,
|
||||||
|
:title,
|
||||||
|
:kind,
|
||||||
|
:enabled,
|
||||||
|
:version,
|
||||||
|
:status,
|
||||||
|
:created_at,
|
||||||
|
:updated_at
|
||||||
|
])
|
||||||
|> assoc_constraint(:project)
|
|> assoc_constraint(:project)
|
||||||
|> unique_constraint(:slug, name: :templates_project_slug_idx)
|
|> unique_constraint(:slug, name: :templates_project_slug_idx)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ defmodule BDS.Types.StringList do
|
|||||||
:error
|
:error
|
||||||
end
|
end
|
||||||
|
|
||||||
_ -> :error
|
_ ->
|
||||||
|
:error
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ defmodule BDS.GenerationTest do
|
|||||||
|
|
||||||
setup do
|
setup do
|
||||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-generation-#{System.unique_integer([:positive])}")
|
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-generation-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
@@ -18,8 +21,13 @@ defmodule BDS.GenerationTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "write_generated_file writes under html output and skips unchanged content by hash", %{project: project, temp_dir: temp_dir} do
|
test "write_generated_file writes under html output and skips unchanged content by hash", %{
|
||||||
assert {:ok, first_write} = BDS.Generation.write_generated_file(project.id, "index.html", "<html>hello</html>")
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
assert {:ok, first_write} =
|
||||||
|
BDS.Generation.write_generated_file(project.id, "index.html", "<html>hello</html>")
|
||||||
|
|
||||||
assert first_write.written? == true
|
assert first_write.written? == true
|
||||||
|
|
||||||
output_path = Path.join([temp_dir, "html", "index.html"])
|
output_path = Path.join([temp_dir, "html", "index.html"])
|
||||||
@@ -29,18 +37,30 @@ defmodule BDS.GenerationTest do
|
|||||||
assert tracked_file.relative_path == "index.html"
|
assert tracked_file.relative_path == "index.html"
|
||||||
assert tracked_file.content_hash == first_write.content_hash
|
assert tracked_file.content_hash == first_write.content_hash
|
||||||
|
|
||||||
assert {:ok, second_write} = BDS.Generation.write_generated_file(project.id, "index.html", "<html>hello</html>")
|
assert {:ok, second_write} =
|
||||||
|
BDS.Generation.write_generated_file(project.id, "index.html", "<html>hello</html>")
|
||||||
|
|
||||||
assert second_write.written? == false
|
assert second_write.written? == false
|
||||||
assert second_write.content_hash == first_write.content_hash
|
assert second_write.content_hash == first_write.content_hash
|
||||||
|
|
||||||
assert {:ok, third_write} = BDS.Generation.write_generated_file(project.id, "index.html", "<html>updated</html>")
|
assert {:ok, third_write} =
|
||||||
|
BDS.Generation.write_generated_file(project.id, "index.html", "<html>updated</html>")
|
||||||
|
|
||||||
assert third_write.written? == true
|
assert third_write.written? == true
|
||||||
assert third_write.content_hash != first_write.content_hash
|
assert third_write.content_hash != first_write.content_hash
|
||||||
assert File.read!(output_path) == "<html>updated</html>"
|
assert File.read!(output_path) == "<html>updated</html>"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "delete_generated_file removes tracked output and forgets its hash", %{project: project, temp_dir: temp_dir} do
|
test "delete_generated_file removes tracked output and forgets its hash", %{
|
||||||
assert {:ok, _write} = BDS.Generation.write_generated_file(project.id, "tag/elixir/index.html", "<html>tag</html>")
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
assert {:ok, _write} =
|
||||||
|
BDS.Generation.write_generated_file(
|
||||||
|
project.id,
|
||||||
|
"tag/elixir/index.html",
|
||||||
|
"<html>tag</html>"
|
||||||
|
)
|
||||||
|
|
||||||
output_path = Path.join([temp_dir, "html", "tag", "elixir", "index.html"])
|
output_path = Path.join([temp_dir, "html", "tag", "elixir", "index.html"])
|
||||||
assert File.exists?(output_path)
|
assert File.exists?(output_path)
|
||||||
@@ -52,7 +72,8 @@ defmodule BDS.GenerationTest do
|
|||||||
assert files == []
|
assert files == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "plan_generation derives generation settings from project metadata and core generation writes tracked files", %{project: project, temp_dir: temp_dir} do
|
test "plan_generation derives generation settings from project metadata and core generation writes tracked files",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
public_url: "https://example.com/blog",
|
public_url: "https://example.com/blog",
|
||||||
@@ -88,7 +109,8 @@ defmodule BDS.GenerationTest do
|
|||||||
"de/atom.xml"
|
"de/atom.xml"
|
||||||
]
|
]
|
||||||
|
|
||||||
assert Enum.sort(Enum.map(result.generated_files, & &1.relative_path)) == Enum.sort(expected_paths)
|
assert Enum.sort(Enum.map(result.generated_files, & &1.relative_path)) ==
|
||||||
|
Enum.sort(expected_paths)
|
||||||
|
|
||||||
for relative_path <- expected_paths do
|
for relative_path <- expected_paths do
|
||||||
assert File.exists?(Path.join([temp_dir, "html", relative_path]))
|
assert File.exists?(Path.join([temp_dir, "html", relative_path]))
|
||||||
@@ -97,7 +119,10 @@ defmodule BDS.GenerationTest do
|
|||||||
assert File.read!(Path.join([temp_dir, "html", "sitemap.xml"])) =~ "https://example.com/blog/"
|
assert File.read!(Path.join([temp_dir, "html", "sitemap.xml"])) =~ "https://example.com/blog/"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "generation renders published list and post templates for core and single pages", %{project: project, temp_dir: temp_dir} do
|
test "generation renders published list and post templates for core and single pages", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
public_url: "https://example.com/blog",
|
public_url: "https://example.com/blog",
|
||||||
@@ -110,7 +135,8 @@ defmodule BDS.GenerationTest do
|
|||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
title: "List View",
|
title: "List View",
|
||||||
kind: :list,
|
kind: :list,
|
||||||
content: "<main class=\"list-template\"><h1>{{ page_title }}</h1>{% for post in posts %}<a href=\"{{ post.href }}\">{{ post.title }}</a>{% endfor %}</main>"
|
content:
|
||||||
|
"<main class=\"list-template\"><h1>{{ page_title }}</h1>{% for post in posts %}<a href=\"{{ post.href }}\">{{ post.title }}</a>{% endfor %}</main>"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:ok, _published_list} = BDS.Templates.publish_template(list_template.id)
|
assert {:ok, _published_list} = BDS.Templates.publish_template(list_template.id)
|
||||||
@@ -120,7 +146,8 @@ defmodule BDS.GenerationTest do
|
|||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
title: "Post View",
|
title: "Post View",
|
||||||
kind: :post,
|
kind: :post,
|
||||||
content: "<article class=\"post-template\"><h1>{{ post.title }}</h1><div class=\"body\">{{ post.content }}</div></article>"
|
content:
|
||||||
|
"<article class=\"post-template\"><h1>{{ post.title }}</h1><div class=\"body\">{{ post.content }}</div></article>"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:ok, published_post_template} = BDS.Templates.publish_template(post_template.id)
|
assert {:ok, published_post_template} = BDS.Templates.publish_template(post_template.id)
|
||||||
@@ -153,7 +180,10 @@ defmodule BDS.GenerationTest do
|
|||||||
assert post_html =~ "Rendered body"
|
assert post_html =~ "Rendered body"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "generation renders copied starter templates with partials, i18n, and markdown", %{project: project, temp_dir: temp_dir} do
|
test "generation renders copied starter templates with partials, i18n, and markdown", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, _menu} =
|
assert {:ok, _menu} =
|
||||||
BDS.Menu.update_menu(project.id, [
|
BDS.Menu.update_menu(project.id, [
|
||||||
%{kind: :page, label: "Notes", slug: "notes"}
|
%{kind: :page, label: "Notes", slug: "notes"}
|
||||||
@@ -199,7 +229,8 @@ defmodule BDS.GenerationTest do
|
|||||||
assert post_html =~ "Language"
|
assert post_html =~ "Language"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "generation expands starter-template markdown macros, rewrites canonical post links, media links, and emits not-found page", %{project: project, temp_dir: temp_dir} do
|
test "generation expands starter-template markdown macros, rewrites canonical post links, media links, and emits not-found page",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
public_url: "https://example.com/blog",
|
public_url: "https://example.com/blog",
|
||||||
@@ -228,7 +259,10 @@ defmodule BDS.GenerationTest do
|
|||||||
assert {:ok, published_linked_post} = Posts.publish_post(linked_post.id)
|
assert {:ok, published_linked_post} = Posts.publish_post(linked_post.id)
|
||||||
|
|
||||||
media_source_reference = "/" <> Path.join(Path.dirname(media.file_path), media.original_name)
|
media_source_reference = "/" <> Path.join(Path.dirname(media.file_path), media.original_name)
|
||||||
canonical_post_href = "/" <> String.trim_trailing(BDS.Generation.post_output_path(published_linked_post), "index.html")
|
|
||||||
|
canonical_post_href =
|
||||||
|
"/" <>
|
||||||
|
String.trim_trailing(BDS.Generation.post_output_path(published_linked_post), "index.html")
|
||||||
|
|
||||||
assert {:ok, post} =
|
assert {:ok, post} =
|
||||||
Posts.create_post(%{
|
Posts.create_post(%{
|
||||||
@@ -252,7 +286,9 @@ defmodule BDS.GenerationTest do
|
|||||||
|
|
||||||
assert "404.html" in Enum.map(result.generated_files, & &1.relative_path)
|
assert "404.html" in Enum.map(result.generated_files, & &1.relative_path)
|
||||||
|
|
||||||
post_html = File.read!(Path.join([temp_dir, "html", BDS.Generation.post_output_path(published_post)]))
|
post_html =
|
||||||
|
File.read!(Path.join([temp_dir, "html", BDS.Generation.post_output_path(published_post)]))
|
||||||
|
|
||||||
assert post_html =~ ~s(src="https://www.youtube.com/embed/dQw4w9WgXcQ?rel=0")
|
assert post_html =~ ~s(src="https://www.youtube.com/embed/dQw4w9WgXcQ?rel=0")
|
||||||
assert post_html =~ ~s(href="#{canonical_post_href}")
|
assert post_html =~ ~s(href="#{canonical_post_href}")
|
||||||
assert post_html =~ ~s(src="/#{media.file_path}")
|
assert post_html =~ ~s(src="/#{media.file_path}")
|
||||||
@@ -262,7 +298,10 @@ defmodule BDS.GenerationTest do
|
|||||||
assert not_found_html =~ "Back to preview home"
|
assert not_found_html =~ "Back to preview home"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "single generation writes canonical post pages and language-prefixed translation pages", %{project: project, temp_dir: temp_dir} do
|
test "single generation writes canonical post pages and language-prefixed translation pages", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
public_url: "https://example.com/blog",
|
public_url: "https://example.com/blog",
|
||||||
@@ -291,14 +330,18 @@ defmodule BDS.GenerationTest do
|
|||||||
post_path = BDS.Generation.post_output_path(published_post)
|
post_path = BDS.Generation.post_output_path(published_post)
|
||||||
translation_path = BDS.Generation.post_output_path(published_post, "de")
|
translation_path = BDS.Generation.post_output_path(published_post, "de")
|
||||||
|
|
||||||
assert Enum.map(result.generated_files, & &1.relative_path) |> Enum.sort() == Enum.sort([post_path, translation_path])
|
assert Enum.map(result.generated_files, & &1.relative_path) |> Enum.sort() ==
|
||||||
|
Enum.sort([post_path, translation_path])
|
||||||
|
|
||||||
assert File.read!(Path.join([temp_dir, "html", post_path])) =~ "Hello generated world"
|
assert File.read!(Path.join([temp_dir, "html", post_path])) =~ "Hello generated world"
|
||||||
assert File.read!(Path.join([temp_dir, "html", translation_path])) =~ "Hallo generierte Welt"
|
assert File.read!(Path.join([temp_dir, "html", translation_path])) =~ "Hallo generierte Welt"
|
||||||
assert File.read!(Path.join([temp_dir, "html", post_path])) =~ ~s(data-pagefind-body)
|
assert File.read!(Path.join([temp_dir, "html", post_path])) =~ ~s(data-pagefind-body)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "archive generation writes paginated category, tag, and date pages", %{project: project, temp_dir: temp_dir} do
|
test "archive generation writes paginated category, tag, and date pages", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
public_url: "https://example.com/blog",
|
public_url: "https://example.com/blog",
|
||||||
@@ -319,7 +362,11 @@ defmodule BDS.GenerationTest do
|
|||||||
})
|
})
|
||||||
|
|
||||||
created_at = DateTime.to_unix(~U[2026-04-15 12:00:00Z]) + index
|
created_at = DateTime.to_unix(~U[2026-04-15 12:00:00Z]) + index
|
||||||
Repo.update_all(from(p in BDS.Posts.Post, where: p.id == ^post.id), set: [created_at: created_at, updated_at: created_at])
|
|
||||||
|
Repo.update_all(from(p in BDS.Posts.Post, where: p.id == ^post.id),
|
||||||
|
set: [created_at: created_at, updated_at: created_at]
|
||||||
|
)
|
||||||
|
|
||||||
assert {:ok, _published} = Posts.publish_post(post.id)
|
assert {:ok, _published} = Posts.publish_post(post.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -341,8 +388,13 @@ defmodule BDS.GenerationTest do
|
|||||||
|
|
||||||
assert expected_paths -- Enum.map(result.generated_files, & &1.relative_path) == []
|
assert expected_paths -- Enum.map(result.generated_files, & &1.relative_path) == []
|
||||||
|
|
||||||
assert File.read!(Path.join([temp_dir, "html", "category", "notes", "index.html"])) =~ "Archive 1"
|
assert File.read!(Path.join([temp_dir, "html", "category", "notes", "index.html"])) =~
|
||||||
assert File.read!(Path.join([temp_dir, "html", "category", "notes", "page", "2", "index.html"])) =~ "Archive 3"
|
"Archive 1"
|
||||||
|
|
||||||
|
assert File.read!(
|
||||||
|
Path.join([temp_dir, "html", "category", "notes", "page", "2", "index.html"])
|
||||||
|
) =~ "Archive 3"
|
||||||
|
|
||||||
assert File.read!(Path.join([temp_dir, "html", "tag", "elixir", "index.html"])) =~ "Elixir"
|
assert File.read!(Path.join([temp_dir, "html", "tag", "elixir", "index.html"])) =~ "Elixir"
|
||||||
assert File.read!(Path.join([temp_dir, "html", "2026", "04", "index.html"])) =~ "2026-04"
|
assert File.read!(Path.join([temp_dir, "html", "2026", "04", "index.html"])) =~ "2026-04"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ defmodule BDS.MaintenanceTest do
|
|||||||
|
|
||||||
setup do
|
setup do
|
||||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-maintenance-#{System.unique_integer([:positive])}")
|
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-maintenance-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
@@ -15,7 +18,10 @@ defmodule BDS.MaintenanceTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild_from_filesystem dispatches to posts, media, scripts, and templates rebuilders", %{project: project, temp_dir: temp_dir} do
|
test "rebuild_from_filesystem dispatches to posts, media, scripts, and templates rebuilders", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
posts_dir = Path.join([temp_dir, "posts", "2026", "04"])
|
posts_dir = Path.join([temp_dir, "posts", "2026", "04"])
|
||||||
File.mkdir_p!(posts_dir)
|
File.mkdir_p!(posts_dir)
|
||||||
|
|
||||||
@@ -60,6 +66,7 @@ defmodule BDS.MaintenanceTest do
|
|||||||
|
|
||||||
template_dir = Path.join(temp_dir, "templates")
|
template_dir = Path.join(temp_dir, "templates")
|
||||||
File.mkdir_p!(template_dir)
|
File.mkdir_p!(template_dir)
|
||||||
|
|
||||||
File.write!(
|
File.write!(
|
||||||
Path.join(template_dir, "dispatch-view.liquid"),
|
Path.join(template_dir, "dispatch-view.liquid"),
|
||||||
[
|
[
|
||||||
@@ -81,6 +88,7 @@ defmodule BDS.MaintenanceTest do
|
|||||||
|
|
||||||
script_dir = Path.join(temp_dir, "scripts")
|
script_dir = Path.join(temp_dir, "scripts")
|
||||||
File.mkdir_p!(script_dir)
|
File.mkdir_p!(script_dir)
|
||||||
|
|
||||||
File.write!(
|
File.write!(
|
||||||
Path.join(script_dir, "dispatch.lua"),
|
Path.join(script_dir, "dispatch.lua"),
|
||||||
[
|
[
|
||||||
@@ -120,10 +128,14 @@ defmodule BDS.MaintenanceTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild_from_filesystem rejects unsupported entity types", %{project: project} do
|
test "rebuild_from_filesystem rejects unsupported entity types", %{project: project} do
|
||||||
assert {:error, :unsupported_entity_type} = BDS.Maintenance.rebuild_from_filesystem(project.id, "unknown")
|
assert {:error, :unsupported_entity_type} =
|
||||||
|
BDS.Maintenance.rebuild_from_filesystem(project.id, "unknown")
|
||||||
end
|
end
|
||||||
|
|
||||||
test "metadata_diff reports field differences and orphan files across managed entities", %{project: project, temp_dir: temp_dir} do
|
test "metadata_diff reports field differences and orphan files across managed entities", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
source_path = Path.join(temp_dir, "sample.txt")
|
source_path = Path.join(temp_dir, "sample.txt")
|
||||||
File.write!(source_path, "hello media")
|
File.write!(source_path, "hello media")
|
||||||
|
|
||||||
@@ -192,6 +204,7 @@ defmodule BDS.MaintenanceTest do
|
|||||||
assert {:ok, published_template} = BDS.Templates.publish_template(template.id)
|
assert {:ok, published_template} = BDS.Templates.publish_template(template.id)
|
||||||
|
|
||||||
post_path = Path.join(temp_dir, published_post.file_path)
|
post_path = Path.join(temp_dir, published_post.file_path)
|
||||||
|
|
||||||
File.write!(
|
File.write!(
|
||||||
post_path,
|
post_path,
|
||||||
[
|
[
|
||||||
@@ -205,9 +218,9 @@ defmodule BDS.MaintenanceTest do
|
|||||||
"language: de",
|
"language: de",
|
||||||
"do_not_translate: false",
|
"do_not_translate: false",
|
||||||
"template_slug: ",
|
"template_slug: ",
|
||||||
"created_at: #{published_post.created_at}",
|
"created_at: #{published_post.created_at + 10}",
|
||||||
"updated_at: #{published_post.updated_at}",
|
"updated_at: #{published_post.updated_at + 20}",
|
||||||
"published_at: #{published_post.published_at}",
|
"published_at: #{published_post.published_at + 30}",
|
||||||
"tags:",
|
"tags:",
|
||||||
" - beta",
|
" - beta",
|
||||||
"categories:",
|
"categories:",
|
||||||
@@ -220,6 +233,7 @@ defmodule BDS.MaintenanceTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
post_translation_path = Path.join(temp_dir, published_post_translation.file_path)
|
post_translation_path = Path.join(temp_dir, published_post_translation.file_path)
|
||||||
|
|
||||||
File.write!(
|
File.write!(
|
||||||
post_translation_path,
|
post_translation_path,
|
||||||
[
|
[
|
||||||
@@ -241,6 +255,7 @@ defmodule BDS.MaintenanceTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
media_sidecar_path = Path.join(temp_dir, media.sidecar_path)
|
media_sidecar_path = Path.join(temp_dir, media.sidecar_path)
|
||||||
|
|
||||||
File.write!(
|
File.write!(
|
||||||
media_sidecar_path,
|
media_sidecar_path,
|
||||||
[
|
[
|
||||||
@@ -262,7 +277,9 @@ defmodule BDS.MaintenanceTest do
|
|||||||
|> Enum.join("\n")
|
|> Enum.join("\n")
|
||||||
)
|
)
|
||||||
|
|
||||||
media_translation_sidecar_path = Path.join(temp_dir, "#{media.file_path}.#{media_translation.language}.meta")
|
media_translation_sidecar_path =
|
||||||
|
Path.join(temp_dir, "#{media.file_path}.#{media_translation.language}.meta")
|
||||||
|
|
||||||
File.write!(
|
File.write!(
|
||||||
media_translation_sidecar_path,
|
media_translation_sidecar_path,
|
||||||
[
|
[
|
||||||
@@ -277,6 +294,7 @@ defmodule BDS.MaintenanceTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
script_path = Path.join(temp_dir, published_script.file_path)
|
script_path = Path.join(temp_dir, published_script.file_path)
|
||||||
|
|
||||||
File.write!(
|
File.write!(
|
||||||
script_path,
|
script_path,
|
||||||
[
|
[
|
||||||
@@ -298,6 +316,7 @@ defmodule BDS.MaintenanceTest do
|
|||||||
)
|
)
|
||||||
|
|
||||||
template_path = Path.join(temp_dir, published_template.file_path)
|
template_path = Path.join(temp_dir, published_template.file_path)
|
||||||
|
|
||||||
File.write!(
|
File.write!(
|
||||||
template_path,
|
template_path,
|
||||||
[
|
[
|
||||||
@@ -317,50 +336,128 @@ defmodule BDS.MaintenanceTest do
|
|||||||
|> Enum.join("\n")
|
|> Enum.join("\n")
|
||||||
)
|
)
|
||||||
|
|
||||||
File.write!(Path.join([temp_dir, "posts", "2026", "04", "orphan-post.md"]), "---\nid: orphan\ntitle: Orphan\nslug: orphan\nstatus: published\ncreated_at: 1\nupdated_at: 1\npublished_at: 1\ntags:\ncategories:\n---\nBody\n")
|
File.write!(
|
||||||
File.write!(Path.join([temp_dir, "posts", "2026", "04", "orphan-post.es.md"]), "---\nid: orphan-post-translation\ntranslation_for: orphan\nlanguage: es\ntitle: Huerfano\nstatus: published\ncreated_at: 1\nupdated_at: 1\npublished_at: 1\n---\nCuerpo\n")
|
Path.join([temp_dir, "posts", "2026", "04", "orphan-post.md"]),
|
||||||
File.write!(Path.join([temp_dir, "media", "2026", "04", "orphan.txt"]), "orphan")
|
"---\nid: orphan\ntitle: Orphan\nslug: orphan\nstatus: published\ncreated_at: 1\nupdated_at: 1\npublished_at: 1\ntags:\ncategories:\n---\nBody\n"
|
||||||
File.write!(Path.join([temp_dir, "media", "2026", "04", "orphan.txt.meta"]), "id: orphan-media\noriginal_name: orphan.txt\nmime_type: text/plain\nsize: 6\ncreated_at: 1\nupdated_at: 1\ntags:\n")
|
)
|
||||||
File.write!(Path.join([temp_dir, "media", "2026", "04", "orphan.txt.es.meta"]), "translation_for: orphan-media\nlanguage: es\ntitle: Huerfano\nalt: Texto\ncaption: Leyenda\n")
|
|
||||||
File.write!(Path.join([temp_dir, "scripts", "orphan.lua"]), "---\nid: orphan-script\nslug: orphan-script\ntitle: Orphan Script\nkind: utility\nentrypoint: main\nenabled: true\nversion: 1\ncreated_at: 1\nupdated_at: 1\n---\nfunction main() return true end\n")
|
|
||||||
File.write!(Path.join([temp_dir, "templates", "orphan-view.liquid"]), "---\nid: orphan-template\nslug: orphan-view\ntitle: Orphan View\nkind: list\nenabled: true\nversion: 1\ncreated_at: 1\nupdated_at: 1\n---\n<section>Orphan</section>\n")
|
|
||||||
|
|
||||||
assert {:ok, %{diff_reports: diff_reports, orphan_reports: orphan_reports}} = BDS.Maintenance.metadata_diff(project.id)
|
File.write!(
|
||||||
|
Path.join([temp_dir, "posts", "2026", "04", "orphan-post.es.md"]),
|
||||||
|
"---\nid: orphan-post-translation\ntranslation_for: orphan\nlanguage: es\ntitle: Huerfano\nstatus: published\ncreated_at: 1\nupdated_at: 1\npublished_at: 1\n---\nCuerpo\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(Path.join([temp_dir, "media", "2026", "04", "orphan.txt"]), "orphan")
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join([temp_dir, "media", "2026", "04", "orphan.txt.meta"]),
|
||||||
|
"id: orphan-media\noriginal_name: orphan.txt\nmime_type: text/plain\nsize: 6\ncreated_at: 1\nupdated_at: 1\ntags:\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join([temp_dir, "media", "2026", "04", "orphan.txt.es.meta"]),
|
||||||
|
"translation_for: orphan-media\nlanguage: es\ntitle: Huerfano\nalt: Texto\ncaption: Leyenda\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join([temp_dir, "scripts", "orphan.lua"]),
|
||||||
|
"---\nid: orphan-script\nslug: orphan-script\ntitle: Orphan Script\nkind: utility\nentrypoint: main\nenabled: true\nversion: 1\ncreated_at: 1\nupdated_at: 1\n---\nfunction main() return true end\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join([temp_dir, "templates", "orphan-view.liquid"]),
|
||||||
|
"---\nid: orphan-template\nslug: orphan-view\ntitle: Orphan View\nkind: list\nenabled: true\nversion: 1\ncreated_at: 1\nupdated_at: 1\n---\n<section>Orphan</section>\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, %{diff_reports: diff_reports, orphan_reports: orphan_reports}} =
|
||||||
|
BDS.Maintenance.metadata_diff(project.id)
|
||||||
|
|
||||||
assert Enum.any?(diff_reports, fn report ->
|
assert Enum.any?(diff_reports, fn report ->
|
||||||
report.entity_type == "post" and report.entity_id == published_post.id and
|
report.entity_type == "post" and report.entity_id == published_post.id and
|
||||||
Enum.any?(report.differences, &(&1.name == "title" and &1.db_value == "Original Post" and &1.file_value == "Edited Post")) and
|
Enum.any?(
|
||||||
Enum.any?(report.differences, &(&1.name == "excerpt" and &1.db_value == "Original summary" and &1.file_value == "Edited summary"))
|
report.differences,
|
||||||
|
&(&1.name == "title" and &1.db_value == "Original Post" and
|
||||||
|
&1.file_value == "Edited Post")
|
||||||
|
) and
|
||||||
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "excerpt" and &1.db_value == "Original summary" and
|
||||||
|
&1.file_value == "Edited summary")
|
||||||
|
) and
|
||||||
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "created_at" and
|
||||||
|
&1.file_value == Integer.to_string(published_post.created_at + 10))
|
||||||
|
) and
|
||||||
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "updated_at" and
|
||||||
|
&1.file_value == Integer.to_string(published_post.updated_at + 20))
|
||||||
|
) and
|
||||||
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "published_at" and
|
||||||
|
&1.file_value == Integer.to_string(published_post.published_at + 30))
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert Enum.any?(diff_reports, fn report ->
|
assert Enum.any?(diff_reports, fn report ->
|
||||||
report.entity_type == "media" and report.entity_id == media.id and
|
report.entity_type == "media" and report.entity_id == media.id and
|
||||||
Enum.any?(report.differences, &(&1.name == "title" and &1.file_value == "Edited media title")) and
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "title" and &1.file_value == "Edited media title")
|
||||||
|
) and
|
||||||
Enum.any?(report.differences, &(&1.name == "language" and &1.file_value == "de"))
|
Enum.any?(report.differences, &(&1.name == "language" and &1.file_value == "de"))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert Enum.any?(diff_reports, fn report ->
|
assert Enum.any?(diff_reports, fn report ->
|
||||||
report.entity_type == "script" and report.entity_id == published_script.id and
|
report.entity_type == "script" and report.entity_id == published_script.id and
|
||||||
Enum.any?(report.differences, &(&1.name == "title" and &1.file_value == "Edited Script")) and
|
Enum.any?(
|
||||||
Enum.any?(report.differences, &(&1.name == "entrypoint" and &1.file_value == "run"))
|
report.differences,
|
||||||
|
&(&1.name == "title" and &1.file_value == "Edited Script")
|
||||||
|
) and
|
||||||
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "entrypoint" and &1.file_value == "run")
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert Enum.any?(diff_reports, fn report ->
|
assert Enum.any?(diff_reports, fn report ->
|
||||||
report.entity_type == "template" and report.entity_id == published_template.id and
|
report.entity_type == "template" and report.entity_id == published_template.id and
|
||||||
Enum.any?(report.differences, &(&1.name == "title" and &1.file_value == "Edited Template")) and
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "title" and &1.file_value == "Edited Template")
|
||||||
|
) and
|
||||||
Enum.any?(report.differences, &(&1.name == "enabled" and &1.file_value == "false"))
|
Enum.any?(report.differences, &(&1.name == "enabled" and &1.file_value == "false"))
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert Enum.any?(diff_reports, fn report ->
|
assert Enum.any?(diff_reports, fn report ->
|
||||||
report.entity_type == "post_translation" and report.entity_id == published_post_translation.id and
|
report.entity_type == "post_translation" and
|
||||||
Enum.any?(report.differences, &(&1.name == "title" and &1.db_value == "Ursprunglicher Beitrag" and &1.file_value == "Bearbeiteter Beitrag")) and
|
report.entity_id == published_post_translation.id and
|
||||||
Enum.any?(report.differences, &(&1.name == "excerpt" and &1.db_value == "Zusammenfassung" and &1.file_value == "Bearbeitete Zusammenfassung"))
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "title" and &1.db_value == "Ursprunglicher Beitrag" and
|
||||||
|
&1.file_value == "Bearbeiteter Beitrag")
|
||||||
|
) and
|
||||||
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "excerpt" and &1.db_value == "Zusammenfassung" and
|
||||||
|
&1.file_value == "Bearbeitete Zusammenfassung")
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
assert Enum.any?(diff_reports, fn report ->
|
assert Enum.any?(diff_reports, fn report ->
|
||||||
report.entity_type == "media_translation" and report.entity_id == media_translation.id and
|
report.entity_type == "media_translation" and
|
||||||
Enum.any?(report.differences, &(&1.name == "title" and &1.db_value == "Ubersetzter Medientitel" and &1.file_value == "Bearbeiteter Medientitel")) and
|
report.entity_id == media_translation.id and
|
||||||
Enum.any?(report.differences, &(&1.name == "alt" and &1.db_value == "Ubersetzter Alt-Text" and &1.file_value == "Bearbeiteter Alt-Text"))
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "title" and &1.db_value == "Ubersetzter Medientitel" and
|
||||||
|
&1.file_value == "Bearbeiteter Medientitel")
|
||||||
|
) and
|
||||||
|
Enum.any?(
|
||||||
|
report.differences,
|
||||||
|
&(&1.name == "alt" and &1.db_value == "Ubersetzter Alt-Text" and
|
||||||
|
&1.file_value == "Bearbeiteter Alt-Text")
|
||||||
|
)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
orphan_paths = Enum.map(orphan_reports, & &1.file_path)
|
orphan_paths = Enum.map(orphan_reports, & &1.file_path)
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ defmodule BDS.MediaTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "import_media copies the binary, creates a sidecar, and persists the row", %{project: project, temp_dir: temp_dir} do
|
test "import_media copies the binary, creates a sidecar, and persists the row", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
source_path = Path.join(temp_dir, "sample.txt")
|
source_path = Path.join(temp_dir, "sample.txt")
|
||||||
File.write!(source_path, "hello media")
|
File.write!(source_path, "hello media")
|
||||||
|
|
||||||
@@ -54,7 +57,8 @@ defmodule BDS.MediaTest do
|
|||||||
source_path = Path.join(temp_dir, "sample.txt")
|
source_path = Path.join(temp_dir, "sample.txt")
|
||||||
File.write!(source_path, "hello media")
|
File.write!(source_path, "hello media")
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
assert {:ok, updated} =
|
assert {:ok, updated} =
|
||||||
BDS.Media.update_media(media.id, %{
|
BDS.Media.update_media(media.id, %{
|
||||||
@@ -76,11 +80,15 @@ defmodule BDS.MediaTest do
|
|||||||
assert sidecar =~ "tags:\n - beta\n"
|
assert sidecar =~ "tags:\n - beta\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "delete_media removes the binary, sidecar, and database row", %{project: project, temp_dir: temp_dir} do
|
test "delete_media removes the binary, sidecar, and database row", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
source_path = Path.join(temp_dir, "sample.txt")
|
source_path = Path.join(temp_dir, "sample.txt")
|
||||||
File.write!(source_path, "hello media")
|
File.write!(source_path, "hello media")
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
assert {:ok, _translation} =
|
assert {:ok, _translation} =
|
||||||
BDS.Media.upsert_media_translation(media.id, "de", %{
|
BDS.Media.upsert_media_translation(media.id, "de", %{
|
||||||
@@ -103,7 +111,10 @@ defmodule BDS.MediaTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild_media_from_files recreates media rows from sidecars", %{project: project, temp_dir: temp_dir} do
|
test "rebuild_media_from_files recreates media rows from sidecars", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
media_dir = Path.join([temp_dir, "media", "2026", "04"])
|
media_dir = Path.join([temp_dir, "media", "2026", "04"])
|
||||||
File.mkdir_p!(media_dir)
|
File.mkdir_p!(media_dir)
|
||||||
|
|
||||||
@@ -181,16 +192,27 @@ defmodule BDS.MediaTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "import_media generates the four thumbnail files in bucketed thumbnail paths", %{project: project, temp_dir: temp_dir} do
|
test "import_media generates the four thumbnail files in bucketed thumbnail paths", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
source_path = Path.join(temp_dir, "sample.jpg")
|
source_path = Path.join(temp_dir, "sample.jpg")
|
||||||
File.write!(source_path, tiny_jpeg_binary())
|
File.write!(source_path, tiny_jpeg_binary())
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
thumbnail_paths = BDS.Media.thumbnail_paths(media)
|
thumbnail_paths = BDS.Media.thumbnail_paths(media)
|
||||||
assert thumbnail_paths.small == "thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-small.webp"
|
|
||||||
assert thumbnail_paths.medium == "thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-medium.webp"
|
assert thumbnail_paths.small ==
|
||||||
assert thumbnail_paths.large == "thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-large.webp"
|
"thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-small.webp"
|
||||||
|
|
||||||
|
assert thumbnail_paths.medium ==
|
||||||
|
"thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-medium.webp"
|
||||||
|
|
||||||
|
assert thumbnail_paths.large ==
|
||||||
|
"thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-large.webp"
|
||||||
|
|
||||||
assert thumbnail_paths.ai == "thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-ai.jpg"
|
assert thumbnail_paths.ai == "thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-ai.jpg"
|
||||||
|
|
||||||
Enum.each(Map.values(thumbnail_paths), fn path ->
|
Enum.each(Map.values(thumbnail_paths), fn path ->
|
||||||
@@ -198,11 +220,15 @@ defmodule BDS.MediaTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "import_media extracts image dimensions and writes real encoded thumbnails", %{project: project, temp_dir: temp_dir} do
|
test "import_media extracts image dimensions and writes real encoded thumbnails", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
source_path = Path.join(temp_dir, "sample.jpg")
|
source_path = Path.join(temp_dir, "sample.jpg")
|
||||||
File.write!(source_path, tiny_jpeg_binary())
|
File.write!(source_path, tiny_jpeg_binary())
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
assert media.mime_type == "image/jpeg"
|
assert media.mime_type == "image/jpeg"
|
||||||
assert media.width == 3
|
assert media.width == 3
|
||||||
@@ -230,11 +256,13 @@ defmodule BDS.MediaTest do
|
|||||||
assert Path.extname(thumbnail_paths.ai) == ".jpg"
|
assert Path.extname(thumbnail_paths.ai) == ".jpg"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "import_media keeps raw header dimensions but autorotates thumbnails from EXIF orientation", %{project: project, temp_dir: temp_dir} do
|
test "import_media keeps raw header dimensions but autorotates thumbnails from EXIF orientation",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
source_path = Path.join(temp_dir, "rotated.jpg")
|
source_path = Path.join(temp_dir, "rotated.jpg")
|
||||||
write_oriented_jpeg!(source_path, 6)
|
write_oriented_jpeg!(source_path, 6)
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
assert media.width == 2
|
assert media.width == 2
|
||||||
assert media.height == 3
|
assert media.height == 3
|
||||||
@@ -248,11 +276,15 @@ defmodule BDS.MediaTest do
|
|||||||
assert_images_match!(actual_thumbnail, expected_thumbnail)
|
assert_images_match!(actual_thumbnail, expected_thumbnail)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "regenerate_thumbnails recreates thumbnail files for an existing image media item", %{project: project, temp_dir: temp_dir} do
|
test "regenerate_thumbnails recreates thumbnail files for an existing image media item", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
source_path = Path.join(temp_dir, "sample.jpg")
|
source_path = Path.join(temp_dir, "sample.jpg")
|
||||||
File.write!(source_path, tiny_jpeg_binary())
|
File.write!(source_path, tiny_jpeg_binary())
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
thumbnail_paths = BDS.Media.thumbnail_paths(media)
|
thumbnail_paths = BDS.Media.thumbnail_paths(media)
|
||||||
File.rm!(Path.join(temp_dir, thumbnail_paths.small))
|
File.rm!(Path.join(temp_dir, thumbnail_paths.small))
|
||||||
@@ -266,12 +298,17 @@ defmodule BDS.MediaTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "import_media generates thumbnails for png and webp sources", %{project: project, temp_dir: temp_dir} do
|
test "import_media generates thumbnails for png and webp sources", %{
|
||||||
Enum.each([{ ".png", "image/png"}, {".webp", "image/webp"}], fn {extension, mime_type} ->
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
Enum.each([{".png", "image/png"}, {".webp", "image/webp"}], fn {extension, mime_type} ->
|
||||||
source_path = Path.join(temp_dir, "sample#{extension}")
|
source_path = Path.join(temp_dir, "sample#{extension}")
|
||||||
File.write!(source_path, sample_image_binary(extension))
|
File.write!(source_path, sample_image_binary(extension))
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
assert media.mime_type == mime_type
|
assert media.mime_type == mime_type
|
||||||
assert media.width == 2
|
assert media.width == 2
|
||||||
assert media.height == 3
|
assert media.height == 3
|
||||||
@@ -282,29 +319,39 @@ defmodule BDS.MediaTest do
|
|||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "import_media detects supported TIFF, BMP, HEIC, and HEIF extensions", %{project: project, temp_dir: temp_dir} do
|
test "import_media detects supported TIFF, BMP, HEIC, and HEIF extensions", %{
|
||||||
Enum.each([
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
Enum.each(
|
||||||
|
[
|
||||||
{"asset.tif", "image/tiff"},
|
{"asset.tif", "image/tiff"},
|
||||||
{"asset.tiff", "image/tiff"},
|
{"asset.tiff", "image/tiff"},
|
||||||
{"asset.bmp", "image/bmp"},
|
{"asset.bmp", "image/bmp"},
|
||||||
{"asset.heic", "image/heic"},
|
{"asset.heic", "image/heic"},
|
||||||
{"asset.heif", "image/heif"}
|
{"asset.heif", "image/heif"}
|
||||||
], fn {file_name, mime_type} ->
|
],
|
||||||
|
fn {file_name, mime_type} ->
|
||||||
source_path = Path.join(temp_dir, file_name)
|
source_path = Path.join(temp_dir, file_name)
|
||||||
File.write!(source_path, "placeholder")
|
File.write!(source_path, "placeholder")
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
assert media.mime_type == mime_type
|
assert media.mime_type == mime_type
|
||||||
assert media.width == nil
|
assert media.width == nil
|
||||||
assert media.height == nil
|
assert media.height == nil
|
||||||
end)
|
end
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "upsert_media_translation persists the row and writes a translated sidecar next to the binary", %{project: project, temp_dir: temp_dir} do
|
test "upsert_media_translation persists the row and writes a translated sidecar next to the binary",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
source_path = Path.join(temp_dir, "sample.txt")
|
source_path = Path.join(temp_dir, "sample.txt")
|
||||||
File.write!(source_path, "hello media")
|
File.write!(source_path, "hello media")
|
||||||
|
|
||||||
assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
assert {:ok, media} =
|
||||||
|
BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
|
||||||
|
|
||||||
assert {:ok, translation} =
|
assert {:ok, translation} =
|
||||||
BDS.Media.upsert_media_translation(media.id, "de", %{
|
BDS.Media.upsert_media_translation(media.id, "de", %{
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ defmodule BDS.MenuTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_menu normalizes Home first, writes meta/menu.opml, and load returns nested items", %{project: project, temp_dir: temp_dir} do
|
test "update_menu normalizes Home first, writes meta/menu.opml, and load returns nested items",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, menu} =
|
assert {:ok, menu} =
|
||||||
BDS.Menu.update_menu(project.id, [
|
BDS.Menu.update_menu(project.id, [
|
||||||
%{kind: :page, label: "About", slug: "about"},
|
%{kind: :page, label: "About", slug: "about"},
|
||||||
@@ -52,7 +53,10 @@ defmodule BDS.MenuTest do
|
|||||||
assert loaded == menu
|
assert loaded == menu
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sync_menu_from_filesystem loads canonical OPML and preserves a prepended Home entry", %{project: project, temp_dir: temp_dir} do
|
test "sync_menu_from_filesystem loads canonical OPML and preserves a prepended Home entry", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
meta_dir = Path.join(temp_dir, "meta")
|
meta_dir = Path.join(temp_dir, "meta")
|
||||||
File.mkdir_p!(meta_dir)
|
File.mkdir_p!(meta_dir)
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ defmodule BDS.MetadataTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_project_metadata writes meta/project.json and load returns the saved values", %{project: project, temp_dir: temp_dir} do
|
test "update_project_metadata writes meta/project.json and load returns the saved values", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, metadata} =
|
assert {:ok, metadata} =
|
||||||
BDS.Metadata.update_project_metadata(project.id, %{
|
BDS.Metadata.update_project_metadata(project.id, %{
|
||||||
name: "Renamed Blog",
|
name: "Renamed Blog",
|
||||||
@@ -51,7 +54,8 @@ defmodule BDS.MetadataTest do
|
|||||||
assert loaded.blog_languages == ["de", "fr"]
|
assert loaded.blog_languages == ["de", "fr"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "category and publishing updates write their meta files and sync_project_metadata_from_filesystem loads them", %{project: project, temp_dir: temp_dir} do
|
test "category and publishing updates write their meta files and sync_project_metadata_from_filesystem loads them",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, _metadata} = BDS.Metadata.add_category(project.id, "news")
|
assert {:ok, _metadata} = BDS.Metadata.add_category(project.id, "news")
|
||||||
|
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
|
|
||||||
setup do
|
setup do
|
||||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-post-translations-#{System.unique_integer([:positive])}")
|
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-post-translations-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
@@ -14,7 +17,8 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "upserted post translations publish with the canonical post, reopen on edit, and delete their file", %{project: project, temp_dir: temp_dir} do
|
test "upserted post translations publish with the canonical post, reopen on edit, and delete their file",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, post} =
|
assert {:ok, post} =
|
||||||
Posts.create_post(%{
|
Posts.create_post(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -75,7 +79,8 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
assert {:ok, []} = Posts.list_post_translations(post.id)
|
assert {:ok, []} = Posts.list_post_translations(post.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "validate_translations reports missing languages, orphan translation files, and do-not-translate posts", %{project: project, temp_dir: temp_dir} do
|
test "validate_translations reports missing languages, orphan translation files, and do-not-translate posts",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
main_language: "en",
|
main_language: "en",
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ defmodule BDS.PostsTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_post slugifies titles, stores list fields, and defaults draft fields", %{project: project} do
|
test "create_post slugifies titles, stores list fields, and defaults draft fields", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
assert {:ok, post} =
|
assert {:ok, post} =
|
||||||
BDS.Posts.create_post(%{
|
BDS.Posts.create_post(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -48,7 +50,10 @@ defmodule BDS.PostsTest do
|
|||||||
assert duplicate_slug_post.categories == []
|
assert duplicate_slug_post.categories == []
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_post falls back to untitled and keeps slug uniqueness scoped to a project", %{project: project, temp_dir: temp_dir} do
|
test "create_post falls back to untitled and keeps slug uniqueness scoped to a project", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, first} = BDS.Posts.create_post(%{project_id: project.id, title: nil})
|
assert {:ok, first} = BDS.Posts.create_post(%{project_id: project.id, title: nil})
|
||||||
assert first.title == ""
|
assert first.title == ""
|
||||||
assert first.slug == "untitled"
|
assert first.slug == "untitled"
|
||||||
@@ -59,12 +64,15 @@ defmodule BDS.PostsTest do
|
|||||||
other_temp_dir = Path.join(temp_dir, "elsewhere")
|
other_temp_dir = Path.join(temp_dir, "elsewhere")
|
||||||
File.mkdir_p!(other_temp_dir)
|
File.mkdir_p!(other_temp_dir)
|
||||||
|
|
||||||
assert {:ok, other_project} = BDS.Projects.create_project(%{name: "Elsewhere", data_path: other_temp_dir})
|
assert {:ok, other_project} =
|
||||||
|
BDS.Projects.create_project(%{name: "Elsewhere", data_path: other_temp_dir})
|
||||||
|
|
||||||
assert {:ok, other_post} = BDS.Posts.create_post(%{project_id: other_project.id, title: nil})
|
assert {:ok, other_post} = BDS.Posts.create_post(%{project_id: other_project.id, title: nil})
|
||||||
assert other_post.slug == "untitled"
|
assert other_post.slug == "untitled"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_post rejects slug changes after first publish and reopens published posts when content changes", %{project: project} do
|
test "update_post rejects slug changes after first publish and reopens published posts when content changes",
|
||||||
|
%{project: project} do
|
||||||
assert {:ok, post} =
|
assert {:ok, post} =
|
||||||
BDS.Posts.create_post(%{
|
BDS.Posts.create_post(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -97,11 +105,14 @@ defmodule BDS.PostsTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "publish_post writes frontmatter to the project data directory and clears draft content" do
|
test "publish_post writes frontmatter to the project data directory and clears draft content" do
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-post-publish-#{System.unique_integer([:positive])}")
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-post-publish-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
assert {:ok, project} = BDS.Projects.create_project(%{name: "Filesystem", data_path: temp_dir})
|
assert {:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "Filesystem", data_path: temp_dir})
|
||||||
|
|
||||||
assert {:ok, post} =
|
assert {:ok, post} =
|
||||||
BDS.Posts.create_post(%{
|
BDS.Posts.create_post(%{
|
||||||
@@ -144,7 +155,9 @@ defmodule BDS.PostsTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "delete_post removes the database row and published markdown file when present" do
|
test "delete_post removes the database row and published markdown file when present" do
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-post-delete-#{System.unique_integer([:positive])}")
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-post-delete-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
@@ -178,11 +191,14 @@ defmodule BDS.PostsTest do
|
|||||||
assert {:ok, archived_draft} = BDS.Posts.archive_post(draft_post.id)
|
assert {:ok, archived_draft} = BDS.Posts.archive_post(draft_post.id)
|
||||||
assert archived_draft.status == :archived
|
assert archived_draft.status == :archived
|
||||||
|
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-post-archive-#{System.unique_integer([:positive])}")
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-post-archive-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
assert {:ok, publish_project} = BDS.Projects.create_project(%{name: "Archive Published", data_path: temp_dir})
|
assert {:ok, publish_project} =
|
||||||
|
BDS.Projects.create_project(%{name: "Archive Published", data_path: temp_dir})
|
||||||
|
|
||||||
assert {:ok, published_post} =
|
assert {:ok, published_post} =
|
||||||
BDS.Posts.create_post(%{
|
BDS.Posts.create_post(%{
|
||||||
@@ -200,7 +216,9 @@ defmodule BDS.PostsTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "publish_post republishes archived posts without losing the existing body or original published_at" do
|
test "publish_post republishes archived posts without losing the existing body or original published_at" do
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-post-republish-#{System.unique_integer([:positive])}")
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-post-republish-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
@@ -227,7 +245,9 @@ defmodule BDS.PostsTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild_posts_from_files recreates published posts from disk" do
|
test "rebuild_posts_from_files recreates published posts from disk" do
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-post-rebuild-#{System.unique_integer([:positive])}")
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-post-rebuild-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
@@ -279,9 +299,9 @@ defmodule BDS.PostsTest do
|
|||||||
assert post.language == "en"
|
assert post.language == "en"
|
||||||
assert post.do_not_translate == true
|
assert post.do_not_translate == true
|
||||||
assert post.template_slug == "article"
|
assert post.template_slug == "article"
|
||||||
assert post.created_at == 1711843200
|
assert post.created_at == 1_711_843_200
|
||||||
assert post.updated_at == 1711929600
|
assert post.updated_at == 1_711_929_600
|
||||||
assert post.published_at == 1712016000
|
assert post.published_at == 1_712_016_000
|
||||||
assert post.tags == ["alpha"]
|
assert post.tags == ["alpha"]
|
||||||
assert post.categories == ["notes"]
|
assert post.categories == ["notes"]
|
||||||
assert post.file_path == "posts/2026/04/recovered-post.md"
|
assert post.file_path == "posts/2026/04/recovered-post.md"
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ defmodule BDS.PreviewTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "start_preview binds localhost and request resolves generated routes, assets, media, and draft previews", %{project: project, temp_dir: temp_dir} do
|
test "start_preview binds localhost and request resolves generated routes, assets, media, and draft previews",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
Metadata.update_project_metadata(project.id, %{
|
Metadata.update_project_metadata(project.id, %{
|
||||||
public_url: "https://example.com/blog",
|
public_url: "https://example.com/blog",
|
||||||
@@ -24,10 +25,29 @@ defmodule BDS.PreviewTest do
|
|||||||
blog_languages: ["en", "de"]
|
blog_languages: ["en", "de"]
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:ok, _} = Generation.write_generated_file(project.id, "index.html", "<html>home</html>")
|
assert {:ok, _} =
|
||||||
assert {:ok, _} = Generation.write_generated_file(project.id, "de/index.html", "<html>startseite</html>")
|
Generation.write_generated_file(project.id, "index.html", "<html>home</html>")
|
||||||
assert {:ok, _} = Generation.write_generated_file(project.id, "tag/elixir/index.html", "<html>tag archive</html>")
|
|
||||||
assert {:ok, _} = Generation.write_generated_file(project.id, "pagefind/pagefind-ui.js", "console.log('pagefind')")
|
assert {:ok, _} =
|
||||||
|
Generation.write_generated_file(
|
||||||
|
project.id,
|
||||||
|
"de/index.html",
|
||||||
|
"<html>startseite</html>"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, _} =
|
||||||
|
Generation.write_generated_file(
|
||||||
|
project.id,
|
||||||
|
"tag/elixir/index.html",
|
||||||
|
"<html>tag archive</html>"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, _} =
|
||||||
|
Generation.write_generated_file(
|
||||||
|
project.id,
|
||||||
|
"pagefind/pagefind-ui.js",
|
||||||
|
"console.log('pagefind')"
|
||||||
|
)
|
||||||
|
|
||||||
media_dir = Path.join([temp_dir, "media", "2026", "04"])
|
media_dir = Path.join([temp_dir, "media", "2026", "04"])
|
||||||
File.mkdir_p!(media_dir)
|
File.mkdir_p!(media_dir)
|
||||||
@@ -46,11 +66,20 @@ defmodule BDS.PreviewTest do
|
|||||||
assert server.port == 4123
|
assert server.port == 4123
|
||||||
assert server.is_running == true
|
assert server.is_running == true
|
||||||
|
|
||||||
assert {:ok, %{body: "<html>home</html>", content_type: "text/html"}} = BDS.Preview.request(project.id, "/")
|
assert {:ok, %{body: "<html>home</html>", content_type: "text/html"}} =
|
||||||
assert {:ok, %{body: "<html>startseite</html>", content_type: "text/html"}} = BDS.Preview.request(project.id, "/de/")
|
BDS.Preview.request(project.id, "/")
|
||||||
assert {:ok, %{body: "<html>tag archive</html>", content_type: "text/html"}} = BDS.Preview.request(project.id, "/tag/elixir")
|
|
||||||
assert {:ok, %{body: "console.log('pagefind')", content_type: "application/javascript"}} = BDS.Preview.request(project.id, "/pagefind/pagefind-ui.js")
|
assert {:ok, %{body: "<html>startseite</html>", content_type: "text/html"}} =
|
||||||
assert {:ok, %{body: "media body", content_type: "text/plain"}} = BDS.Preview.request(project.id, "/media/2026/04/image.txt")
|
BDS.Preview.request(project.id, "/de/")
|
||||||
|
|
||||||
|
assert {:ok, %{body: "<html>tag archive</html>", content_type: "text/html"}} =
|
||||||
|
BDS.Preview.request(project.id, "/tag/elixir")
|
||||||
|
|
||||||
|
assert {:ok, %{body: "console.log('pagefind')", content_type: "application/javascript"}} =
|
||||||
|
BDS.Preview.request(project.id, "/pagefind/pagefind-ui.js")
|
||||||
|
|
||||||
|
assert {:ok, %{body: "media body", content_type: "text/plain"}} =
|
||||||
|
BDS.Preview.request(project.id, "/media/2026/04/image.txt")
|
||||||
|
|
||||||
assert {:ok, %{body: draft_html, content_type: "text/html"}} =
|
assert {:ok, %{body: draft_html, content_type: "text/html"}} =
|
||||||
BDS.Preview.preview_draft(project.id, "/draft/draft-post", post.id)
|
BDS.Preview.preview_draft(project.id, "/draft/draft-post", post.id)
|
||||||
@@ -67,7 +96,8 @@ defmodule BDS.PreviewTest do
|
|||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
title: "Preview Post",
|
title: "Preview Post",
|
||||||
kind: :post,
|
kind: :post,
|
||||||
content: "<article class=\"preview-template\"><h1>{{ post.title }}</h1><div>{{ post.content }}</div></article>"
|
content:
|
||||||
|
"<article class=\"preview-template\"><h1>{{ post.title }}</h1><div>{{ post.content }}</div></article>"
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:ok, published_template} = BDS.Templates.publish_template(template.id)
|
assert {:ok, published_template} = BDS.Templates.publish_template(template.id)
|
||||||
@@ -93,7 +123,9 @@ defmodule BDS.PreviewTest do
|
|||||||
assert :ok = BDS.Preview.stop_preview(project.id)
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "draft preview renders through copied starter templates with markdown and i18n", %{project: project} do
|
test "draft preview renders through copied starter templates with markdown and i18n", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
assert {:ok, _menu} =
|
assert {:ok, _menu} =
|
||||||
BDS.Menu.update_menu(project.id, [
|
BDS.Menu.update_menu(project.id, [
|
||||||
%{kind: :page, label: "Notes", slug: "notes"}
|
%{kind: :page, label: "Notes", slug: "notes"}
|
||||||
@@ -126,7 +158,8 @@ defmodule BDS.PreviewTest do
|
|||||||
assert :ok = BDS.Preview.stop_preview(project.id)
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "preview renders not-found template for missing routes and rewrites markdown macros and canonical URLs", %{project: project, temp_dir: temp_dir} do
|
test "preview renders not-found template for missing routes and rewrites markdown macros and canonical URLs",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
:inets.start()
|
:inets.start()
|
||||||
|
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
@@ -157,7 +190,10 @@ defmodule BDS.PreviewTest do
|
|||||||
assert {:ok, published_linked_post} = Posts.publish_post(linked_post.id)
|
assert {:ok, published_linked_post} = Posts.publish_post(linked_post.id)
|
||||||
|
|
||||||
media_source_reference = "/" <> Path.join(Path.dirname(media.file_path), media.original_name)
|
media_source_reference = "/" <> Path.join(Path.dirname(media.file_path), media.original_name)
|
||||||
canonical_post_href = "/" <> String.trim_trailing(BDS.Generation.post_output_path(published_linked_post), "index.html")
|
|
||||||
|
canonical_post_href =
|
||||||
|
"/" <>
|
||||||
|
String.trim_trailing(BDS.Generation.post_output_path(published_linked_post), "index.html")
|
||||||
|
|
||||||
assert {:ok, post} =
|
assert {:ok, post} =
|
||||||
Posts.create_post(%{
|
Posts.create_post(%{
|
||||||
@@ -190,14 +226,21 @@ defmodule BDS.PreviewTest do
|
|||||||
assert missing_body =~ ~s(data-template="not-found")
|
assert missing_body =~ ~s(data-template="not-found")
|
||||||
|
|
||||||
assert {:ok, {{_version, 404, _reason}, _headers, body}} =
|
assert {:ok, {{_version, 404, _reason}, _headers, body}} =
|
||||||
:httpc.request(:get, {to_charlist("http://#{server.host}:#{server.port}/missing-page"), []}, [], body_format: :binary)
|
:httpc.request(
|
||||||
|
:get,
|
||||||
|
{to_charlist("http://#{server.host}:#{server.port}/missing-page"), []},
|
||||||
|
[],
|
||||||
|
body_format: :binary
|
||||||
|
)
|
||||||
|
|
||||||
assert body =~ ~s(data-template="not-found")
|
assert body =~ ~s(data-template="not-found")
|
||||||
|
|
||||||
assert :ok = BDS.Preview.stop_preview(project.id)
|
assert :ok = BDS.Preview.stop_preview(project.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "start_preview serves generated and draft routes over real HTTP on localhost", %{project: project} do
|
test "start_preview serves generated and draft routes over real HTTP on localhost", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
:inets.start()
|
:inets.start()
|
||||||
|
|
||||||
assert {:ok, _metadata} =
|
assert {:ok, _metadata} =
|
||||||
@@ -207,7 +250,8 @@ defmodule BDS.PreviewTest do
|
|||||||
blog_languages: ["en"]
|
blog_languages: ["en"]
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:ok, _} = Generation.write_generated_file(project.id, "index.html", "<html>http home</html>")
|
assert {:ok, _} =
|
||||||
|
Generation.write_generated_file(project.id, "index.html", "<html>http home</html>")
|
||||||
|
|
||||||
assert {:ok, post} =
|
assert {:ok, post} =
|
||||||
Posts.create_post(%{
|
Posts.create_post(%{
|
||||||
@@ -220,13 +264,26 @@ defmodule BDS.PreviewTest do
|
|||||||
assert {:ok, server} = BDS.Preview.start_preview(project.id)
|
assert {:ok, server} = BDS.Preview.start_preview(project.id)
|
||||||
|
|
||||||
assert {:ok, {{_version, 200, _reason}, headers, body}} =
|
assert {:ok, {{_version, 200, _reason}, headers, body}} =
|
||||||
:httpc.request(:get, {to_charlist("http://#{server.host}:#{server.port}/"), []}, [], body_format: :binary)
|
:httpc.request(:get, {to_charlist("http://#{server.host}:#{server.port}/"), []}, [],
|
||||||
|
body_format: :binary
|
||||||
|
)
|
||||||
|
|
||||||
assert body == "<html>http home</html>"
|
assert body == "<html>http home</html>"
|
||||||
assert Enum.any?(headers, fn {name, value} -> String.downcase(to_string(name)) == "content-type" and to_string(value) =~ "text/html" end)
|
|
||||||
|
assert Enum.any?(headers, fn {name, value} ->
|
||||||
|
String.downcase(to_string(name)) == "content-type" and
|
||||||
|
to_string(value) =~ "text/html"
|
||||||
|
end)
|
||||||
|
|
||||||
assert {:ok, {{_version, 200, _reason}, _headers, draft_body}} =
|
assert {:ok, {{_version, 200, _reason}, _headers, draft_body}} =
|
||||||
:httpc.request(:get, {to_charlist("http://#{server.host}:#{server.port}/draft/http-draft?post_id=#{post.id}"), []}, [], body_format: :binary)
|
:httpc.request(
|
||||||
|
:get,
|
||||||
|
{to_charlist(
|
||||||
|
"http://#{server.host}:#{server.port}/draft/http-draft?post_id=#{post.id}"
|
||||||
|
), []},
|
||||||
|
[],
|
||||||
|
body_format: :binary
|
||||||
|
)
|
||||||
|
|
||||||
assert draft_body =~ "Draft over HTTP"
|
assert draft_body =~ "Draft over HTTP"
|
||||||
|
|
||||||
|
|||||||
@@ -18,13 +18,16 @@ defmodule BDS.ProjectsTest do
|
|||||||
%{temp_root: temp_root}
|
%{temp_root: temp_root}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_project slugifies names, keeps new projects inactive, and deduplicates slugs", %{temp_root: temp_root} do
|
test "create_project slugifies names, keeps new projects inactive, and deduplicates slugs", %{
|
||||||
|
temp_root: temp_root
|
||||||
|
} do
|
||||||
first_dir = Path.join(temp_root, "first")
|
first_dir = Path.join(temp_root, "first")
|
||||||
second_dir = Path.join(temp_root, "second")
|
second_dir = Path.join(temp_root, "second")
|
||||||
File.mkdir_p!(first_dir)
|
File.mkdir_p!(first_dir)
|
||||||
File.mkdir_p!(second_dir)
|
File.mkdir_p!(second_dir)
|
||||||
|
|
||||||
assert {:ok, first} = BDS.Projects.create_project(%{name: "Föö Bär Blog", data_path: first_dir})
|
assert {:ok, first} =
|
||||||
|
BDS.Projects.create_project(%{name: "Föö Bär Blog", data_path: first_dir})
|
||||||
|
|
||||||
assert first.name == "Föö Bär Blog"
|
assert first.name == "Föö Bär Blog"
|
||||||
assert first.slug == "foo-bar-blog"
|
assert first.slug == "foo-bar-blog"
|
||||||
@@ -33,16 +36,21 @@ defmodule BDS.ProjectsTest do
|
|||||||
assert is_integer(first.created_at)
|
assert is_integer(first.created_at)
|
||||||
assert is_integer(first.updated_at)
|
assert is_integer(first.updated_at)
|
||||||
|
|
||||||
assert {:ok, second} = BDS.Projects.create_project(%{name: "Föö Bär Blog", data_path: second_dir})
|
assert {:ok, second} =
|
||||||
|
BDS.Projects.create_project(%{name: "Föö Bär Blog", data_path: second_dir})
|
||||||
|
|
||||||
assert second.slug == "foo-bar-blog-2"
|
assert second.slug == "foo-bar-blog-2"
|
||||||
assert second.is_active == false
|
assert second.is_active == false
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_project installs starter templates into the project data directory", %{temp_root: temp_root} do
|
test "create_project installs starter templates into the project data directory", %{
|
||||||
|
temp_root: temp_root
|
||||||
|
} do
|
||||||
temp_dir = Path.join(temp_root, "starter")
|
temp_dir = Path.join(temp_root, "starter")
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
|
|
||||||
assert {:ok, project} = BDS.Projects.create_project(%{name: "Starter Blog", data_path: temp_dir})
|
assert {:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "Starter Blog", data_path: temp_dir})
|
||||||
|
|
||||||
assert File.exists?(Path.join([temp_dir, "templates", "single-post.liquid"]))
|
assert File.exists?(Path.join([temp_dir, "templates", "single-post.liquid"]))
|
||||||
assert File.exists?(Path.join([temp_dir, "templates", "post-list.liquid"]))
|
assert File.exists?(Path.join([temp_dir, "templates", "post-list.liquid"]))
|
||||||
@@ -52,18 +60,25 @@ defmodule BDS.ProjectsTest do
|
|||||||
assert File.exists?(Path.join([temp_dir, "templates", "macros", "gallery.liquid"]))
|
assert File.exists?(Path.join([temp_dir, "templates", "macros", "gallery.liquid"]))
|
||||||
|
|
||||||
starter_slugs =
|
starter_slugs =
|
||||||
Repo.all(from template in Template, where: template.project_id == ^project.id, select: {template.slug, template.kind})
|
Repo.all(
|
||||||
|
from template in Template,
|
||||||
|
where: template.project_id == ^project.id,
|
||||||
|
select: {template.slug, template.kind}
|
||||||
|
)
|
||||||
|
|
||||||
assert {"single-post", :post} in starter_slugs
|
assert {"single-post", :post} in starter_slugs
|
||||||
assert {"post-list", :list} in starter_slugs
|
assert {"post-list", :list} in starter_slugs
|
||||||
assert {"not-found", :not_found} in starter_slugs
|
assert {"not-found", :not_found} in starter_slugs
|
||||||
end
|
end
|
||||||
|
|
||||||
test "starter template installation is idempotent for existing top-level templates", %{temp_root: temp_root} do
|
test "starter template installation is idempotent for existing top-level templates", %{
|
||||||
|
temp_root: temp_root
|
||||||
|
} do
|
||||||
temp_dir = Path.join(temp_root, "idempotent-starter")
|
temp_dir = Path.join(temp_root, "idempotent-starter")
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
|
|
||||||
assert {:ok, project} = BDS.Projects.create_project(%{name: "Starter Blog", data_path: temp_dir})
|
assert {:ok, project} =
|
||||||
|
BDS.Projects.create_project(%{name: "Starter Blog", data_path: temp_dir})
|
||||||
|
|
||||||
template_path = Path.join([temp_dir, "templates", "single-post.liquid"])
|
template_path = Path.join([temp_dir, "templates", "single-post.liquid"])
|
||||||
original_contents = File.read!(template_path)
|
original_contents = File.read!(template_path)
|
||||||
@@ -75,11 +90,15 @@ defmodule BDS.ProjectsTest do
|
|||||||
reinstalled_contents = File.read!(template_path)
|
reinstalled_contents = File.read!(template_path)
|
||||||
assert reinstalled_contents == original_contents
|
assert reinstalled_contents == original_contents
|
||||||
|
|
||||||
assert {:ok, %{fields: reinstalled_fields}} = BDS.Frontmatter.parse_document(reinstalled_contents)
|
assert {:ok, %{fields: reinstalled_fields}} =
|
||||||
|
BDS.Frontmatter.parse_document(reinstalled_contents)
|
||||||
|
|
||||||
assert reinstalled_fields["id"] == original_fields["id"]
|
assert reinstalled_fields["id"] == original_fields["id"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "set_active_project clears the previous active project and activates the target", %{temp_root: temp_root} do
|
test "set_active_project clears the previous active project and activates the target", %{
|
||||||
|
temp_root: temp_root
|
||||||
|
} do
|
||||||
first_dir = Path.join(temp_root, "active-first")
|
first_dir = Path.join(temp_root, "active-first")
|
||||||
second_dir = Path.join(temp_root, "active-second")
|
second_dir = Path.join(temp_root, "active-second")
|
||||||
File.mkdir_p!(first_dir)
|
File.mkdir_p!(first_dir)
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ defmodule BDS.PublishingTest do
|
|||||||
|
|
||||||
setup do
|
setup do
|
||||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
temp_dir = Path.join(System.tmp_dir!(), "bds-publishing-#{System.unique_integer([:positive])}")
|
|
||||||
|
temp_dir =
|
||||||
|
Path.join(System.tmp_dir!(), "bds-publishing-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
File.mkdir_p!(temp_dir)
|
File.mkdir_p!(temp_dir)
|
||||||
on_exit(fn -> File.rm_rf(temp_dir) end)
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
@@ -11,7 +14,10 @@ defmodule BDS.PublishingTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "upload_site creates a publish job, uploads all targets, and excludes media sidecars", %{project: project, temp_dir: temp_dir} do
|
test "upload_site creates a publish job, uploads all targets, and excludes media sidecars", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
test_pid = self()
|
test_pid = self()
|
||||||
|
|
||||||
File.mkdir_p!(Path.join([temp_dir, "html"]))
|
File.mkdir_p!(Path.join([temp_dir, "html"]))
|
||||||
@@ -25,7 +31,11 @@ defmodule BDS.PublishingTest do
|
|||||||
File.write!(Path.join([temp_dir, "media", "asset.jpg.meta"]), "meta")
|
File.write!(Path.join([temp_dir, "media", "asset.jpg.meta"]), "meta")
|
||||||
|
|
||||||
uploader = fn target, files, credentials ->
|
uploader = fn target, files, credentials ->
|
||||||
send(test_pid, {:uploaded, target.kind, target.remote_dir, Enum.sort(files), credentials.ssh_mode})
|
send(
|
||||||
|
test_pid,
|
||||||
|
{:uploaded, target.kind, target.remote_dir, Enum.sort(files), credentials.ssh_mode}
|
||||||
|
)
|
||||||
|
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -46,7 +56,10 @@ defmodule BDS.PublishingTest do
|
|||||||
assert_receive {:uploaded, :media, "/srv/blog/media", ["asset.jpg"], :rsync}
|
assert_receive {:uploaded, :media, "/srv/blog/media", ["asset.jpg"], :rsync}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "upload_site runs rsync commands with SSH agent env and media exclude filters", %{project: project, temp_dir: temp_dir} do
|
test "upload_site runs rsync commands with SSH agent env and media exclude filters", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
test_pid = self()
|
test_pid = self()
|
||||||
|
|
||||||
File.mkdir_p!(Path.join([temp_dir, "html"]))
|
File.mkdir_p!(Path.join([temp_dir, "html"]))
|
||||||
@@ -78,17 +91,47 @@ defmodule BDS.PublishingTest do
|
|||||||
assert wait_for_publish_job(job.id, &(&1.status == :completed)).status == :completed
|
assert wait_for_publish_job(job.id, &(&1.status == :completed)).status == :completed
|
||||||
|
|
||||||
assert_receive {:command_run, "rsync", html_args, html_opts}
|
assert_receive {:command_run, "rsync", html_args, html_opts}
|
||||||
assert html_args == ["--update", "--compress", "--verbose", "-e", "ssh", Path.join([temp_dir, "html"]) <> "/", "deploy@example.com:/srv/blog/"]
|
|
||||||
|
assert html_args == [
|
||||||
|
"--update",
|
||||||
|
"--compress",
|
||||||
|
"--verbose",
|
||||||
|
"-e",
|
||||||
|
"ssh",
|
||||||
|
Path.join([temp_dir, "html"]) <> "/",
|
||||||
|
"deploy@example.com:/srv/blog/"
|
||||||
|
]
|
||||||
|
|
||||||
assert html_opts[:env] == [{"SSH_AUTH_SOCK", "/tmp/test-agent.sock"}]
|
assert html_opts[:env] == [{"SSH_AUTH_SOCK", "/tmp/test-agent.sock"}]
|
||||||
|
|
||||||
assert_receive {:command_run, "rsync", thumb_args, _thumb_opts}
|
assert_receive {:command_run, "rsync", thumb_args, _thumb_opts}
|
||||||
assert thumb_args == ["--update", "--compress", "--verbose", "-e", "ssh", Path.join([temp_dir, "thumbnails"]) <> "/", "deploy@example.com:/srv/blog/thumbnails/"]
|
|
||||||
|
assert thumb_args == [
|
||||||
|
"--update",
|
||||||
|
"--compress",
|
||||||
|
"--verbose",
|
||||||
|
"-e",
|
||||||
|
"ssh",
|
||||||
|
Path.join([temp_dir, "thumbnails"]) <> "/",
|
||||||
|
"deploy@example.com:/srv/blog/thumbnails/"
|
||||||
|
]
|
||||||
|
|
||||||
assert_receive {:command_run, "rsync", media_args, _media_opts}
|
assert_receive {:command_run, "rsync", media_args, _media_opts}
|
||||||
assert media_args == ["--update", "--compress", "--verbose", "--exclude=*.meta", "-e", "ssh", Path.join([temp_dir, "media"]) <> "/", "deploy@example.com:/srv/blog/media/"]
|
|
||||||
|
assert media_args == [
|
||||||
|
"--update",
|
||||||
|
"--compress",
|
||||||
|
"--verbose",
|
||||||
|
"--exclude=*.meta",
|
||||||
|
"-e",
|
||||||
|
"ssh",
|
||||||
|
Path.join([temp_dir, "media"]) <> "/",
|
||||||
|
"deploy@example.com:/srv/blog/media/"
|
||||||
|
]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "upload_site runs scp commands for each eligible file and fails when a command exits non-zero", %{project: project, temp_dir: temp_dir} do
|
test "upload_site runs scp commands for each eligible file and fails when a command exits non-zero",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
test_pid = self()
|
test_pid = self()
|
||||||
html_index = Path.join([temp_dir, "html", "index.html"])
|
html_index = Path.join([temp_dir, "html", "index.html"])
|
||||||
html_entry = Path.join([temp_dir, "html", "posts", "entry.html"])
|
html_entry = Path.join([temp_dir, "html", "posts", "entry.html"])
|
||||||
@@ -129,14 +172,26 @@ defmodule BDS.PublishingTest do
|
|||||||
failed_job = wait_for_publish_job(job.id, &(&1.status == :failed))
|
failed_job = wait_for_publish_job(job.id, &(&1.status == :failed))
|
||||||
assert failed_job.error =~ "thumbnail failure"
|
assert failed_job.error =~ "thumbnail failure"
|
||||||
|
|
||||||
assert_receive {:command_run, "scp", ["-q", ^html_index, "deploy@example.com:/srv/blog/index.html"], opts_a}
|
assert_receive {:command_run, "scp",
|
||||||
|
["-q", ^html_index, "deploy@example.com:/srv/blog/index.html"], opts_a}
|
||||||
|
|
||||||
assert opts_a[:env] == [{"SSH_AUTH_SOCK", "/tmp/test-agent.sock"}]
|
assert opts_a[:env] == [{"SSH_AUTH_SOCK", "/tmp/test-agent.sock"}]
|
||||||
assert_receive {:command_run, "scp", ["-q", ^html_entry, "deploy@example.com:/srv/blog/posts/entry.html"], _opts_b}
|
|
||||||
assert_receive {:command_run, "scp", ["-q", ^thumb_path, "deploy@example.com:/srv/blog/thumbnails/thumb.jpg"], _opts_c}
|
assert_receive {:command_run, "scp",
|
||||||
refute_receive {:command_run, "scp", ["-q", _, "deploy@example.com:/srv/blog/media/asset.jpg"], _opts_d}
|
["-q", ^html_entry, "deploy@example.com:/srv/blog/posts/entry.html"], _opts_b}
|
||||||
|
|
||||||
|
assert_receive {:command_run, "scp",
|
||||||
|
["-q", ^thumb_path, "deploy@example.com:/srv/blog/thumbnails/thumb.jpg"],
|
||||||
|
_opts_c}
|
||||||
|
|
||||||
|
refute_receive {:command_run, "scp",
|
||||||
|
["-q", _, "deploy@example.com:/srv/blog/media/asset.jpg"], _opts_d}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "upload_site marks the publish job failed when a target upload fails", %{project: project, temp_dir: temp_dir} do
|
test "upload_site marks the publish job failed when a target upload fails", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
File.mkdir_p!(Path.join([temp_dir, "html"]))
|
File.mkdir_p!(Path.join([temp_dir, "html"]))
|
||||||
File.write!(Path.join([temp_dir, "html", "index.html"]), "<html />")
|
File.write!(Path.join([temp_dir, "html", "index.html"]), "<html />")
|
||||||
File.mkdir_p!(Path.join([temp_dir, "thumbnails"]))
|
File.mkdir_p!(Path.join([temp_dir, "thumbnails"]))
|
||||||
@@ -161,6 +216,75 @@ defmodule BDS.PublishingTest do
|
|||||||
assert failed_job.error == "thumbnail failure"
|
assert failed_job.error == "thumbnail failure"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "upload_site skips unchanged files for scp and only re-uploads files with newer mtimes", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
test_pid = self()
|
||||||
|
html_index = Path.join([temp_dir, "html", "index.html"])
|
||||||
|
media_asset = Path.join([temp_dir, "media", "asset.jpg"])
|
||||||
|
|
||||||
|
File.mkdir_p!(Path.dirname(html_index))
|
||||||
|
File.write!(html_index, "<html />")
|
||||||
|
File.mkdir_p!(Path.join([temp_dir, "thumbnails"]))
|
||||||
|
File.write!(Path.join([temp_dir, "thumbnails", "thumb.jpg"]), "thumb")
|
||||||
|
File.mkdir_p!(Path.dirname(media_asset))
|
||||||
|
File.write!(media_asset, "asset")
|
||||||
|
|
||||||
|
runner = fn command, args, opts ->
|
||||||
|
send(test_pid, {:command_run, command, args, opts})
|
||||||
|
{"", 0}
|
||||||
|
end
|
||||||
|
|
||||||
|
credentials = %{
|
||||||
|
ssh_host: "example.com",
|
||||||
|
ssh_user: "deploy",
|
||||||
|
ssh_remote_path: "/srv/blog",
|
||||||
|
ssh_mode: :scp
|
||||||
|
}
|
||||||
|
|
||||||
|
assert {:ok, first_job} =
|
||||||
|
BDS.Publishing.upload_site(project.id, credentials,
|
||||||
|
command_runner: runner,
|
||||||
|
ssh_auth_sock: "/tmp/test-agent.sock"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert wait_for_publish_job(first_job.id, &(&1.status == :completed)).status == :completed
|
||||||
|
first_uploads = collect_command_runs()
|
||||||
|
assert length(first_uploads) == 3
|
||||||
|
|
||||||
|
assert {:ok, second_job} =
|
||||||
|
BDS.Publishing.upload_site(project.id, credentials,
|
||||||
|
command_runner: runner,
|
||||||
|
ssh_auth_sock: "/tmp/test-agent.sock"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert wait_for_publish_job(second_job.id, &(&1.status == :completed)).status == :completed
|
||||||
|
assert collect_command_runs() == []
|
||||||
|
|
||||||
|
:ok = File.touch(html_index, {{2099, 1, 1}, {0, 0, 0}})
|
||||||
|
|
||||||
|
assert {:ok, third_job} =
|
||||||
|
BDS.Publishing.upload_site(project.id, credentials,
|
||||||
|
command_runner: runner,
|
||||||
|
ssh_auth_sock: "/tmp/test-agent.sock"
|
||||||
|
)
|
||||||
|
|
||||||
|
assert wait_for_publish_job(third_job.id, &(&1.status == :completed)).status == :completed
|
||||||
|
|
||||||
|
assert [html_upload] = collect_command_runs()
|
||||||
|
assert elem(html_upload, 0) == "scp"
|
||||||
|
assert elem(html_upload, 1) == ["-q", html_index, "deploy@example.com:/srv/blog/index.html"]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp collect_command_runs(acc \\ []) do
|
||||||
|
receive do
|
||||||
|
{:command_run, command, args, _opts} -> collect_command_runs([{command, args} | acc])
|
||||||
|
after
|
||||||
|
50 -> Enum.reverse(acc)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp wait_for_publish_job(job_id, predicate, attempts \\ 100)
|
defp wait_for_publish_job(job_id, predicate, attempts \\ 100)
|
||||||
|
|
||||||
defp wait_for_publish_job(job_id, predicate, attempts) when attempts > 0 do
|
defp wait_for_publish_job(job_id, predicate, attempts) when attempts > 0 do
|
||||||
|
|||||||
152
test/bds/rendering_test.exs
Normal file
152
test/bds/rendering_test.exs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
defmodule BDS.RenderingTest do
|
||||||
|
use ExUnit.Case, async: false
|
||||||
|
|
||||||
|
import Ecto.Query
|
||||||
|
|
||||||
|
alias BDS.Rendering
|
||||||
|
|
||||||
|
setup do
|
||||||
|
:ok = Ecto.Adapters.SQL.Sandbox.checkout(BDS.Repo)
|
||||||
|
temp_dir = Path.join(System.tmp_dir!(), "bds-rendering-#{System.unique_integer([:positive])}")
|
||||||
|
File.mkdir_p!(temp_dir)
|
||||||
|
on_exit(fn -> File.rm_rf(temp_dir) end)
|
||||||
|
|
||||||
|
{:ok, project} = BDS.Projects.create_project(%{name: "Rendering", data_path: temp_dir})
|
||||||
|
%{project: project, temp_dir: temp_dir}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "render_post_page exposes the spec post context and blog language links", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
|
assert {:ok, _metadata} =
|
||||||
|
BDS.Metadata.update_project_metadata(project.id, %{
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en", "de"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, template} =
|
||||||
|
BDS.Templates.create_template(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Render Post Context",
|
||||||
|
kind: :post,
|
||||||
|
content:
|
||||||
|
"{{ pico_stylesheet_href }}|{% for lang in blog_languages %}[{{ lang.code }}={{ lang.href }}:{{ lang.href_prefix }}]{% endfor %}|{{ post.author }}|{{ post.published_at }}|{{ post.created_at }}|{{ post.updated_at }}|{{ post.tags.size }}|{{ post.categories.size }}|{{ post.template_slug }}|{{ post.do_not_translate }}"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, published_template} = BDS.Templates.publish_template(template.id)
|
||||||
|
|
||||||
|
assert {:ok, post} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Render Me",
|
||||||
|
content: "Body",
|
||||||
|
author: "Writer",
|
||||||
|
tags: ["alpha", "beta"],
|
||||||
|
categories: ["notes"],
|
||||||
|
language: "en",
|
||||||
|
template_slug: published_template.slug,
|
||||||
|
do_not_translate: true
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, published_post} = BDS.Posts.publish_post(post.id)
|
||||||
|
|
||||||
|
assert {:ok, rendered} =
|
||||||
|
Rendering.render_post_page(project.id, published_template.slug, %{
|
||||||
|
id: published_post.id,
|
||||||
|
title: published_post.title,
|
||||||
|
content: published_post.content || "",
|
||||||
|
slug: published_post.slug,
|
||||||
|
language: "de",
|
||||||
|
excerpt: published_post.excerpt,
|
||||||
|
template_slug: published_post.template_slug
|
||||||
|
})
|
||||||
|
|
||||||
|
assert rendered =~ "/assets/pico.min.css"
|
||||||
|
assert rendered =~ "[en=/:]"
|
||||||
|
assert rendered =~ "[de=/de/:/de]"
|
||||||
|
assert rendered =~ "|Writer|"
|
||||||
|
|
||||||
|
assert rendered =~
|
||||||
|
"|#{published_post.published_at}|#{published_post.created_at}|#{published_post.updated_at}|"
|
||||||
|
|
||||||
|
assert rendered =~ "|2|1|#{published_template.slug}|true"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "render_list_page exposes pagination and render_not_found_page localizes default copy", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
|
assert {:ok, _metadata} =
|
||||||
|
BDS.Metadata.update_project_metadata(project.id, %{
|
||||||
|
main_language: "en",
|
||||||
|
blog_languages: ["en", "de"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, list_template} =
|
||||||
|
BDS.Templates.create_template(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Render List Context",
|
||||||
|
kind: :list,
|
||||||
|
content:
|
||||||
|
"{{ current_page }}|{{ total_pages }}|{{ total_items }}|{{ items_per_page }}|{{ has_prev_page }}|{{ prev_page_href }}|{{ has_next_page }}|{{ next_page_href }}"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, not_found_template} =
|
||||||
|
BDS.Templates.create_template(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Render Not Found Context",
|
||||||
|
kind: :not_found,
|
||||||
|
content: "{{ not_found_message }}|{{ not_found_back_label }}"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, published_list_template} = BDS.Templates.publish_template(list_template.id)
|
||||||
|
|
||||||
|
assert {:ok, _published_not_found_template} =
|
||||||
|
BDS.Templates.publish_template(not_found_template.id)
|
||||||
|
|
||||||
|
BDS.Repo.update_all(
|
||||||
|
from(template in BDS.Templates.Template,
|
||||||
|
where:
|
||||||
|
template.project_id == ^project.id and template.kind == :list and
|
||||||
|
template.id != ^published_list_template.id
|
||||||
|
),
|
||||||
|
set: [enabled: false]
|
||||||
|
)
|
||||||
|
|
||||||
|
BDS.Repo.update_all(
|
||||||
|
from(template in BDS.Templates.Template,
|
||||||
|
where:
|
||||||
|
template.project_id == ^project.id and template.kind == :not_found and
|
||||||
|
template.slug != ^not_found_template.slug
|
||||||
|
),
|
||||||
|
set: [enabled: false]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, rendered_list} =
|
||||||
|
Rendering.render_list_page(project.id, %{
|
||||||
|
language: "en",
|
||||||
|
page_title: "Archive",
|
||||||
|
posts: [],
|
||||||
|
archive_context: %{kind: "tag", name: "elixir"},
|
||||||
|
pagination: %{
|
||||||
|
current_page: 2,
|
||||||
|
total_pages: 5,
|
||||||
|
total_items: 12,
|
||||||
|
items_per_page: 3,
|
||||||
|
has_prev_page: true,
|
||||||
|
prev_page_href: "/page/1/",
|
||||||
|
has_next_page: true,
|
||||||
|
next_page_href: "/page/3/"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
assert rendered_list == "2|5|12|3|true|/page/1/|true|/page/3/"
|
||||||
|
|
||||||
|
assert {:ok, rendered_not_found} =
|
||||||
|
Rendering.render_not_found_page(project.id, %{language: "de"})
|
||||||
|
|
||||||
|
assert rendered_not_found ==
|
||||||
|
"Die angeforderte Vorschauseite konnte nicht gefunden werden.|Zurück zur Vorschau-Startseite"
|
||||||
|
|
||||||
|
assert published_list_template.kind == :list
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -180,7 +180,13 @@ defmodule BDS.Repo.SchemaMigrationTest do
|
|||||||
"ai_model_modalities" => ["provider", "model_id", "direction", "modality"],
|
"ai_model_modalities" => ["provider", "model_id", "direction", "modality"],
|
||||||
"ai_catalog_meta" => ["key", "value"],
|
"ai_catalog_meta" => ["key", "value"],
|
||||||
"embedding_keys" => ["label", "post_id", "project_id", "content_hash", "vector"],
|
"embedding_keys" => ["label", "post_id", "project_id", "content_hash", "vector"],
|
||||||
"dismissed_duplicate_pairs" => ["id", "project_id", "post_id_a", "post_id_b", "dismissed_at"],
|
"dismissed_duplicate_pairs" => [
|
||||||
|
"id",
|
||||||
|
"project_id",
|
||||||
|
"post_id_a",
|
||||||
|
"post_id_b",
|
||||||
|
"dismissed_at"
|
||||||
|
],
|
||||||
"import_definitions" => [
|
"import_definitions" => [
|
||||||
"id",
|
"id",
|
||||||
"project_id",
|
"project_id",
|
||||||
@@ -229,8 +235,16 @@ defmodule BDS.Repo.SchemaMigrationTest do
|
|||||||
|
|
||||||
assert unique_index_columns("tags", "tags_project_name_idx") == ["project_id", "name"]
|
assert unique_index_columns("tags", "tags_project_name_idx") == ["project_id", "name"]
|
||||||
assert unique_index_columns("scripts", "scripts_project_slug_idx") == ["project_id", "slug"]
|
assert unique_index_columns("scripts", "scripts_project_slug_idx") == ["project_id", "slug"]
|
||||||
assert unique_index_columns("templates", "templates_project_slug_idx") == ["project_id", "slug"]
|
|
||||||
assert unique_index_columns("post_media", "post_media_post_media_idx") == ["post_id", "media_id"]
|
assert unique_index_columns("templates", "templates_project_slug_idx") == [
|
||||||
|
"project_id",
|
||||||
|
"slug"
|
||||||
|
]
|
||||||
|
|
||||||
|
assert unique_index_columns("post_media", "post_media_post_media_idx") == [
|
||||||
|
"post_id",
|
||||||
|
"media_id"
|
||||||
|
]
|
||||||
|
|
||||||
assert unique_index_columns(
|
assert unique_index_columns(
|
||||||
"generated_file_hashes",
|
"generated_file_hashes",
|
||||||
@@ -345,7 +359,9 @@ defmodule BDS.Repo.SchemaMigrationTest do
|
|||||||
defp unique_index_columns(table, index_name) do
|
defp unique_index_columns(table, index_name) do
|
||||||
indexes = query_rows("PRAGMA index_list(#{table})")
|
indexes = query_rows("PRAGMA index_list(#{table})")
|
||||||
|
|
||||||
assert Enum.any?(indexes, fn [_seq, name, unique | _rest] -> name == index_name and unique == 1 end),
|
assert Enum.any?(indexes, fn [_seq, name, unique | _rest] ->
|
||||||
|
name == index_name and unique == 1
|
||||||
|
end),
|
||||||
"expected unique index #{index_name} on #{table}"
|
"expected unique index #{index_name} on #{table}"
|
||||||
|
|
||||||
query_rows("PRAGMA index_info(#{index_name})")
|
query_rows("PRAGMA index_info(#{index_name})")
|
||||||
|
|||||||
@@ -58,7 +58,13 @@ defmodule BDS.Scripting.JobTest do
|
|||||||
assert {:ok, job} = BDS.Scripting.start_job("irrelevant", "main")
|
assert {:ok, job} = BDS.Scripting.start_job("irrelevant", "main")
|
||||||
assert job.status in [:queued, :running]
|
assert job.status in [:queued, :running]
|
||||||
|
|
||||||
running_job = wait_for_job(job.id, &(&1.status == :running and &1.progress == %{"phase" => "started", "current" => 1, "total" => 2}))
|
running_job =
|
||||||
|
wait_for_job(
|
||||||
|
job.id,
|
||||||
|
&(&1.status == :running and
|
||||||
|
&1.progress == %{"phase" => "started", "current" => 1, "total" => 2})
|
||||||
|
)
|
||||||
|
|
||||||
assert running_job.started_at != nil
|
assert running_job.started_at != nil
|
||||||
|
|
||||||
completed_job = wait_for_job(job.id, &(&1.status == :completed))
|
completed_job = wait_for_job(job.id, &(&1.status == :completed))
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ defmodule BDS.ScriptsTest do
|
|||||||
assert macro_script.slug == "render-card"
|
assert macro_script.slug == "render-card"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "publish_script writes a lua file with frontmatter and clears draft content", %{project: project, temp_dir: temp_dir} do
|
test "publish_script writes a lua file with frontmatter and clears draft content", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, script} =
|
assert {:ok, script} =
|
||||||
BDS.Scripts.create_script(%{
|
BDS.Scripts.create_script(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -73,7 +76,9 @@ defmodule BDS.ScriptsTest do
|
|||||||
assert contents =~ "\n---\nfunction main() return 'ok' end\n"
|
assert contents =~ "\n---\nfunction main() return 'ok' end\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_script bumps version and reopens a published script when content changes", %{project: project} do
|
test "update_script bumps version and reopens a published script when content changes", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
assert {:ok, script} =
|
assert {:ok, script} =
|
||||||
BDS.Scripts.create_script(%{
|
BDS.Scripts.create_script(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -99,7 +104,10 @@ defmodule BDS.ScriptsTest do
|
|||||||
assert updated.updated_at >= published.updated_at
|
assert updated.updated_at >= published.updated_at
|
||||||
end
|
end
|
||||||
|
|
||||||
test "delete_script removes the published file and database row", %{project: project, temp_dir: temp_dir} do
|
test "delete_script removes the published file and database row", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, script} =
|
assert {:ok, script} =
|
||||||
BDS.Scripts.create_script(%{
|
BDS.Scripts.create_script(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -116,7 +124,10 @@ defmodule BDS.ScriptsTest do
|
|||||||
refute File.exists?(Path.join(temp_dir, published.file_path))
|
refute File.exists?(Path.join(temp_dir, published.file_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild_scripts_from_files recreates published scripts from disk", %{project: project, temp_dir: temp_dir} do
|
test "rebuild_scripts_from_files recreates published scripts from disk", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
script_dir = Path.join(temp_dir, "scripts")
|
script_dir = Path.join(temp_dir, "scripts")
|
||||||
File.mkdir_p!(script_dir)
|
File.mkdir_p!(script_dir)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ defmodule BDS.SearchTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search_posts indexes writes, supports filters and pagination, and removes deleted posts", %{project: project} do
|
test "search_posts indexes writes, supports filters and pagination, and removes deleted posts",
|
||||||
|
%{project: project} do
|
||||||
assert {:ok, draft_post} =
|
assert {:ok, draft_post} =
|
||||||
BDS.Posts.create_post(%{
|
BDS.Posts.create_post(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -54,14 +55,25 @@ defmodule BDS.SearchTest do
|
|||||||
assert results.limit == 50
|
assert results.limit == 50
|
||||||
assert Enum.map(results.posts, & &1.id) == [draft_post.id]
|
assert Enum.map(results.posts, & &1.id) == [draft_post.id]
|
||||||
|
|
||||||
assert {:ok, tag_results} = BDS.Search.search_posts(project.id, "galaxy", %{tags: ["space"], categories: ["astronomy"]})
|
assert {:ok, tag_results} =
|
||||||
assert tag_results.total == 2
|
BDS.Search.search_posts(project.id, "galaxy", %{
|
||||||
assert Enum.sort(Enum.map(tag_results.posts, & &1.id)) == Enum.sort([draft_post.id, published_post.id])
|
tags: ["space"],
|
||||||
|
categories: ["astronomy"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert tag_results.total == 2
|
||||||
|
|
||||||
|
assert Enum.sort(Enum.map(tag_results.posts, & &1.id)) ==
|
||||||
|
Enum.sort([draft_post.id, published_post.id])
|
||||||
|
|
||||||
|
assert {:ok, language_results} =
|
||||||
|
BDS.Search.search_posts(project.id, "galaxy", %{language: "de"})
|
||||||
|
|
||||||
assert {:ok, language_results} = BDS.Search.search_posts(project.id, "galaxy", %{language: "de"})
|
|
||||||
assert Enum.map(language_results.posts, & &1.id) == [published_post.id]
|
assert Enum.map(language_results.posts, & &1.id) == [published_post.id]
|
||||||
|
|
||||||
assert {:ok, paged_results} = BDS.Search.search_posts(project.id, "galaxy", %{limit: 1, offset: 1})
|
assert {:ok, paged_results} =
|
||||||
|
BDS.Search.search_posts(project.id, "galaxy", %{limit: 1, offset: 1})
|
||||||
|
|
||||||
assert paged_results.total == 3
|
assert paged_results.total == 3
|
||||||
assert paged_results.offset == 1
|
assert paged_results.offset == 1
|
||||||
assert paged_results.limit == 1
|
assert paged_results.limit == 1
|
||||||
@@ -120,12 +132,17 @@ defmodule BDS.SearchTest do
|
|||||||
assert Enum.map(results.posts, & &1.id) == [post.id]
|
assert Enum.map(results.posts, & &1.id) == [post.id]
|
||||||
|
|
||||||
assert {:ok, missing_translation_results} =
|
assert {:ok, missing_translation_results} =
|
||||||
BDS.Search.search_posts(project.id, "Canonical", %{missing_translation_language: "de"})
|
BDS.Search.search_posts(project.id, "Canonical", %{
|
||||||
|
missing_translation_language: "de"
|
||||||
|
})
|
||||||
|
|
||||||
assert Enum.map(missing_translation_results.posts, & &1.id) == [post.id]
|
assert Enum.map(missing_translation_results.posts, & &1.id) == [post.id]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search_media indexes metadata, includes translation text, and removes deleted media", %{project: project, temp_dir: temp_dir} do
|
test "search_media indexes metadata, includes translation text, and removes deleted media", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
source_path = Path.join(temp_dir, "hero.txt")
|
source_path = Path.join(temp_dir, "hero.txt")
|
||||||
File.write!(source_path, "hero")
|
File.write!(source_path, "hero")
|
||||||
|
|
||||||
@@ -164,7 +181,10 @@ defmodule BDS.SearchTest do
|
|||||||
assert deleted_results.total == 0
|
assert deleted_results.total == 0
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild operations repopulate the search index from filesystem truth", %{project: project, temp_dir: temp_dir} do
|
test "rebuild operations repopulate the search index from filesystem truth", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
posts_dir = Path.join([temp_dir, "posts", "2026", "04"])
|
posts_dir = Path.join([temp_dir, "posts", "2026", "04"])
|
||||||
File.mkdir_p!(posts_dir)
|
File.mkdir_p!(posts_dir)
|
||||||
|
|
||||||
@@ -225,7 +245,9 @@ defmodule BDS.SearchTest do
|
|||||||
assert Enum.map(media_results.media, & &1.id) == ["search-media-from-file"]
|
assert Enum.map(media_results.media, & &1.id) == ["search-media-from-file"]
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search_posts applies language-aware stemming to indexed and query text", %{project: project} do
|
test "search_posts applies language-aware stemming to indexed and query text", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
assert {:ok, german_post} =
|
assert {:ok, german_post} =
|
||||||
BDS.Posts.create_post(%{
|
BDS.Posts.create_post(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
|
|||||||
@@ -14,8 +14,13 @@ defmodule BDS.TagsTest do
|
|||||||
%{project: project, temp_dir: temp_dir}
|
%{project: project, temp_dir: temp_dir}
|
||||||
end
|
end
|
||||||
|
|
||||||
test "create_tag persists the row and rewrites meta/tags.json sorted by name", %{project: project, temp_dir: temp_dir} do
|
test "create_tag persists the row and rewrites meta/tags.json sorted by name", %{
|
||||||
assert {:ok, zebra} = BDS.Tags.create_tag(%{project_id: project.id, name: "Zebra", color: "#000000"})
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
assert {:ok, zebra} =
|
||||||
|
BDS.Tags.create_tag(%{project_id: project.id, name: "Zebra", color: "#000000"})
|
||||||
|
|
||||||
assert {:ok, alpha} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
assert {:ok, alpha} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
||||||
|
|
||||||
assert zebra.name == "Zebra"
|
assert zebra.name == "Zebra"
|
||||||
@@ -35,7 +40,10 @@ defmodule BDS.TagsTest do
|
|||||||
assert "has already been taken" in errors_on(changeset).name
|
assert "has already been taken" in errors_on(changeset).name
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_tag rewrites the tag row and meta/tags.json", %{project: project, temp_dir: temp_dir} do
|
test "update_tag rewrites the tag row and meta/tags.json", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, tag} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
assert {:ok, tag} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
||||||
|
|
||||||
assert {:ok, updated} =
|
assert {:ok, updated} =
|
||||||
@@ -51,11 +59,16 @@ defmodule BDS.TagsTest do
|
|||||||
|
|
||||||
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
||||||
|
|
||||||
assert %{"tags" => [%{"name" => "Alpha", "color" => "#112233", "post_template_slug" => "article"}]} =
|
assert %{
|
||||||
|
"tags" => [
|
||||||
|
%{"name" => "Alpha", "color" => "#112233", "post_template_slug" => "article"}
|
||||||
|
]
|
||||||
|
} =
|
||||||
Jason.decode!(File.read!(tags_path))
|
Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rename_tag updates post tag arrays, rewrites published post files, and refreshes tags.json", %{project: project, temp_dir: temp_dir} do
|
test "rename_tag updates post tag arrays, rewrites published post files, and refreshes tags.json",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, tag} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
assert {:ok, tag} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
||||||
|
|
||||||
assert {:ok, post} =
|
assert {:ok, post} =
|
||||||
@@ -83,7 +96,8 @@ defmodule BDS.TagsTest do
|
|||||||
assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
|
assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "merge_tags moves source tags onto the target, deduplicates post tags, deletes sources, and refreshes tags.json", %{project: project, temp_dir: temp_dir} do
|
test "merge_tags moves source tags onto the target, deduplicates post tags, deletes sources, and refreshes tags.json",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, source_a} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
assert {:ok, source_a} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
||||||
assert {:ok, source_b} = BDS.Tags.create_tag(%{project_id: project.id, name: "Beta"})
|
assert {:ok, source_b} = BDS.Tags.create_tag(%{project_id: project.id, name: "Beta"})
|
||||||
assert {:ok, target} = BDS.Tags.create_tag(%{project_id: project.id, name: "Gamma"})
|
assert {:ok, target} = BDS.Tags.create_tag(%{project_id: project.id, name: "Gamma"})
|
||||||
@@ -115,7 +129,8 @@ defmodule BDS.TagsTest do
|
|||||||
assert %{"tags" => [%{"name" => "Gamma"}]} = Jason.decode!(File.read!(tags_path))
|
assert %{"tags" => [%{"name" => "Gamma"}]} = Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "delete_tag removes the tag from posts, rewrites published files, deletes the row, and refreshes tags.json", %{project: project, temp_dir: temp_dir} do
|
test "delete_tag removes the tag from posts, rewrites published files, deletes the row, and refreshes tags.json",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, doomed} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
assert {:ok, doomed} = BDS.Tags.create_tag(%{project_id: project.id, name: "Alpha"})
|
||||||
assert {:ok, _other} = BDS.Tags.create_tag(%{project_id: project.id, name: "Beta"})
|
assert {:ok, _other} = BDS.Tags.create_tag(%{project_id: project.id, name: "Beta"})
|
||||||
|
|
||||||
@@ -145,7 +160,8 @@ defmodule BDS.TagsTest do
|
|||||||
assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
|
assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sync_tags_from_posts creates missing tags from post tag arrays and refreshes tags.json", %{project: project, temp_dir: temp_dir} do
|
test "sync_tags_from_posts creates missing tags from post tag arrays and refreshes tags.json",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, existing} =
|
assert {:ok, existing} =
|
||||||
BDS.Tags.create_tag(%{
|
BDS.Tags.create_tag(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -183,7 +199,11 @@ defmodule BDS.TagsTest do
|
|||||||
assert %{
|
assert %{
|
||||||
"tags" => [
|
"tags" => [
|
||||||
%{"name" => "Another"},
|
%{"name" => "Another"},
|
||||||
%{"name" => "Existing", "color" => "#112233", "post_template_slug" => "feature-view"},
|
%{
|
||||||
|
"name" => "Existing",
|
||||||
|
"color" => "#112233",
|
||||||
|
"post_template_slug" => "feature-view"
|
||||||
|
},
|
||||||
%{"name" => "Missing"}
|
%{"name" => "Missing"}
|
||||||
]
|
]
|
||||||
} = Jason.decode!(File.read!(tags_path))
|
} = Jason.decode!(File.read!(tags_path))
|
||||||
|
|||||||
@@ -94,7 +94,11 @@ defmodule BDS.TasksTest do
|
|||||||
end
|
end
|
||||||
|
|
||||||
test "external tasks are registered as running and can report progress and complete" do
|
test "external tasks are registered as running and can report progress and complete" do
|
||||||
assert {:ok, task} = BDS.Tasks.register_external_task("preview build", %{group_id: "generation", group_name: "Generation"})
|
assert {:ok, task} =
|
||||||
|
BDS.Tasks.register_external_task("preview build", %{
|
||||||
|
group_id: "generation",
|
||||||
|
group_name: "Generation"
|
||||||
|
})
|
||||||
|
|
||||||
assert task.status == :running
|
assert task.status == :running
|
||||||
assert task.group_id == "generation"
|
assert task.group_id == "generation"
|
||||||
@@ -106,7 +110,9 @@ defmodule BDS.TasksTest do
|
|||||||
assert progressed.status == :running
|
assert progressed.status == :running
|
||||||
|
|
||||||
assert :ok = BDS.Tasks.complete_task(task.id)
|
assert :ok = BDS.Tasks.complete_task(task.id)
|
||||||
assert wait_for_task(task.id, &(&1.status == :completed and &1.progress == 1.0)).status == :completed
|
|
||||||
|
assert wait_for_task(task.id, &(&1.status == :completed and &1.progress == 1.0)).status ==
|
||||||
|
:completed
|
||||||
end
|
end
|
||||||
|
|
||||||
defp receive_started do
|
defp receive_started do
|
||||||
|
|||||||
@@ -33,12 +33,20 @@ defmodule BDS.TemplatesTest do
|
|||||||
assert template.content == "<article>{{ content }}</article>"
|
assert template.content == "<article>{{ content }}</article>"
|
||||||
|
|
||||||
assert {:ok, duplicate} =
|
assert {:ok, duplicate} =
|
||||||
BDS.Templates.create_template(%{project_id: project.id, title: "Article View", kind: :post, content: "x"})
|
BDS.Templates.create_template(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Article View",
|
||||||
|
kind: :post,
|
||||||
|
content: "x"
|
||||||
|
})
|
||||||
|
|
||||||
assert duplicate.slug == "article-view-2"
|
assert duplicate.slug == "article-view-2"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "publish_template writes a liquid file with frontmatter and clears draft content", %{project: project, temp_dir: temp_dir} do
|
test "publish_template writes a liquid file with frontmatter and clears draft content", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
assert {:ok, template} =
|
assert {:ok, template} =
|
||||||
BDS.Templates.create_template(%{
|
BDS.Templates.create_template(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -68,7 +76,9 @@ defmodule BDS.TemplatesTest do
|
|||||||
assert contents =~ "\n---\n<section>{{ page_title }}</section>\n"
|
assert contents =~ "\n---\n<section>{{ page_title }}</section>\n"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_template bumps version and reopens a published template when content changes", %{project: project} do
|
test "update_template bumps version and reopens a published template when content changes", %{
|
||||||
|
project: project
|
||||||
|
} do
|
||||||
assert {:ok, template} =
|
assert {:ok, template} =
|
||||||
BDS.Templates.create_template(%{
|
BDS.Templates.create_template(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -94,7 +104,8 @@ defmodule BDS.TemplatesTest do
|
|||||||
assert updated.updated_at >= published.updated_at
|
assert updated.updated_at >= published.updated_at
|
||||||
end
|
end
|
||||||
|
|
||||||
test "delete_template refuses referenced templates unless forced, then clears references and deletes the file", %{project: project, temp_dir: temp_dir} do
|
test "delete_template refuses referenced templates unless forced, then clears references and deletes the file",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, template} =
|
assert {:ok, template} =
|
||||||
BDS.Templates.create_template(%{
|
BDS.Templates.create_template(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -122,7 +133,8 @@ defmodule BDS.TemplatesTest do
|
|||||||
post_template_slug: published.slug
|
post_template_slug: published.slug
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:error, {:has_references, %{posts: 1, tags: 1}}} = BDS.Templates.delete_template(published.id)
|
assert {:error, {:has_references, %{posts: 1, tags: 1}}} =
|
||||||
|
BDS.Templates.delete_template(published.id)
|
||||||
|
|
||||||
assert {:ok, :deleted} = BDS.Templates.delete_template(published.id, force: true)
|
assert {:ok, :deleted} = BDS.Templates.delete_template(published.id, force: true)
|
||||||
|
|
||||||
@@ -143,7 +155,8 @@ defmodule BDS.TemplatesTest do
|
|||||||
assert %{"tags" => [%{"name" => "Feature"}]} = Jason.decode!(File.read!(tags_path))
|
assert %{"tags" => [%{"name" => "Feature"}]} = Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "update_template cascades slug changes to posts and tags and renames the published file", %{project: project, temp_dir: temp_dir} do
|
test "update_template cascades slug changes to posts and tags and renames the published file",
|
||||||
|
%{project: project, temp_dir: temp_dir} do
|
||||||
assert {:ok, template} =
|
assert {:ok, template} =
|
||||||
BDS.Templates.create_template(%{
|
BDS.Templates.create_template(%{
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
@@ -198,11 +211,15 @@ defmodule BDS.TemplatesTest do
|
|||||||
assert post_contents =~ "\n---\nBody\n"
|
assert post_contents =~ "\n---\nBody\n"
|
||||||
|
|
||||||
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
||||||
|
|
||||||
assert %{"tags" => [%{"name" => "Feature", "post_template_slug" => "feature-view"}]} =
|
assert %{"tags" => [%{"name" => "Feature", "post_template_slug" => "feature-view"}]} =
|
||||||
Jason.decode!(File.read!(tags_path))
|
Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild_templates_from_files recreates published templates from disk", %{project: project, temp_dir: temp_dir} do
|
test "rebuild_templates_from_files recreates published templates from disk", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
template_dir = Path.join(temp_dir, "templates")
|
template_dir = Path.join(temp_dir, "templates")
|
||||||
File.mkdir_p!(template_dir)
|
File.mkdir_p!(template_dir)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user