fix: more fixes to file formats
This commit is contained in:
@@ -175,5 +175,8 @@ defmodule BDS.Frontmatter do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp timestamp_key?(key), do: String.ends_with?(to_string(key), "_at")
|
defp timestamp_key?(key) do
|
||||||
|
rendered = to_string(key)
|
||||||
|
String.ends_with?(rendered, "_at") or String.ends_with?(rendered, "At")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -341,6 +341,7 @@ defmodule BDS.Media do
|
|||||||
|
|
||||||
defp upsert_media_from_sidecar(project, sidecar_path) do
|
defp upsert_media_from_sidecar(project, sidecar_path) do
|
||||||
{:ok, fields} = sidecar_path |> File.read!() |> Sidecar.parse_document()
|
{:ok, fields} = sidecar_path |> File.read!() |> Sidecar.parse_document()
|
||||||
|
fields = normalize_media_sidecar_fields(fields)
|
||||||
relative_sidecar_path = Path.relative_to(sidecar_path, Projects.project_data_dir(project))
|
relative_sidecar_path = Path.relative_to(sidecar_path, Projects.project_data_dir(project))
|
||||||
relative_file_path = String.trim_trailing(relative_sidecar_path, ".meta")
|
relative_file_path = String.trim_trailing(relative_sidecar_path, ".meta")
|
||||||
filename = Path.basename(relative_file_path)
|
filename = Path.basename(relative_file_path)
|
||||||
@@ -386,20 +387,21 @@ defmodule BDS.Media do
|
|||||||
atomic_write(
|
atomic_write(
|
||||||
path,
|
path,
|
||||||
Sidecar.serialize_document([
|
Sidecar.serialize_document([
|
||||||
{:id, media.id},
|
{"id", media.id},
|
||||||
{:original_name, media.original_name},
|
{"originalName", media.original_name},
|
||||||
{:mime_type, media.mime_type},
|
{"mimeType", media.mime_type},
|
||||||
{:size, media.size},
|
{"size", media.size},
|
||||||
{:width, media.width},
|
{"width", media.width},
|
||||||
{:height, media.height},
|
{"height", media.height},
|
||||||
{:title, media.title},
|
{"title", media.title},
|
||||||
{:alt, media.alt},
|
{"alt", media.alt},
|
||||||
{:caption, media.caption},
|
{"caption", media.caption},
|
||||||
{:author, media.author},
|
{"author", media.author},
|
||||||
{:language, media.language},
|
{"language", media.language},
|
||||||
{:created_at, media.created_at},
|
{"createdAt", media.created_at},
|
||||||
{:updated_at, media.updated_at},
|
{"updatedAt", media.updated_at},
|
||||||
{:tags, media.tags || []}
|
{"linkedPostIds", linked_post_ids(media.id)},
|
||||||
|
{"tags", media.tags || []}
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -416,11 +418,11 @@ defmodule BDS.Media do
|
|||||||
atomic_write(
|
atomic_write(
|
||||||
path,
|
path,
|
||||||
Sidecar.serialize_document([
|
Sidecar.serialize_document([
|
||||||
{:translation_for, media.id},
|
{"translationFor", media.id},
|
||||||
{:language, translation.language},
|
{"language", translation.language},
|
||||||
{:title, translation.title},
|
{"title", translation.title},
|
||||||
{:alt, translation.alt},
|
{"alt", translation.alt},
|
||||||
{:caption, translation.caption}
|
{"caption", translation.caption}
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
@@ -434,6 +436,7 @@ defmodule BDS.Media do
|
|||||||
|
|
||||||
media ->
|
media ->
|
||||||
{:ok, fields} = sidecar_path |> File.read!() |> Sidecar.parse_document()
|
{:ok, fields} = sidecar_path |> File.read!() |> Sidecar.parse_document()
|
||||||
|
fields = normalize_media_sidecar_fields(fields)
|
||||||
now = Persistence.now_ms()
|
now = Persistence.now_ms()
|
||||||
language = Map.fetch!(fields, "language")
|
language = Map.fetch!(fields, "language")
|
||||||
|
|
||||||
@@ -607,6 +610,30 @@ defmodule BDS.Media do
|
|||||||
Persistence.atomic_write(path, contents)
|
Persistence.atomic_write(path, contents)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp normalize_media_sidecar_fields(fields) when is_map(fields) do
|
||||||
|
[
|
||||||
|
{"originalName", "original_name"},
|
||||||
|
{"mimeType", "mime_type"},
|
||||||
|
{"createdAt", "created_at"},
|
||||||
|
{"updatedAt", "updated_at"},
|
||||||
|
{"translationFor", "translation_for"},
|
||||||
|
{"linkedPostIds", "linked_post_ids"}
|
||||||
|
]
|
||||||
|
|> Enum.reduce(fields, fn {file_key, current_key}, acc ->
|
||||||
|
case Map.fetch(acc, file_key) do
|
||||||
|
{:ok, value} -> Map.put_new(acc, current_key, value)
|
||||||
|
:error -> acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp linked_post_ids(media_id) do
|
||||||
|
case Repo.query("SELECT post_id FROM post_media WHERE media_id = ? ORDER BY sort_order ASC, post_id ASC", [media_id]) do
|
||||||
|
{:ok, %{rows: rows}} -> Enum.map(rows, fn [post_id] -> post_id end)
|
||||||
|
{:error, _reason} -> []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp blank_to_nil(nil), do: nil
|
defp blank_to_nil(nil), do: nil
|
||||||
defp blank_to_nil(""), do: nil
|
defp blank_to_nil(""), do: nil
|
||||||
defp blank_to_nil(value), do: value
|
defp blank_to_nil(value), do: value
|
||||||
|
|||||||
@@ -128,11 +128,8 @@ defmodule BDS.Menu do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp render_attributes(item) do
|
defp render_attributes(item) do
|
||||||
[
|
item
|
||||||
{"kind", item.kind},
|
|> render_outline_attributes()
|
||||||
{"text", item.label},
|
|
||||||
{"slug", item.slug}
|
|
||||||
]
|
|
||||||
|> Enum.reject(fn {_key, value} -> value in [nil, ""] end)
|
|> Enum.reject(fn {_key, value} -> value in [nil, ""] end)
|
||||||
|> Enum.map_join("", fn {key, value} -> ~s( #{key}="#{xml_escape(to_string(value))}") end)
|
|> Enum.map_join("", fn {key, value} -> ~s( #{key}="#{xml_escape(to_string(value))}") end)
|
||||||
end
|
end
|
||||||
@@ -145,12 +142,12 @@ defmodule BDS.Menu do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp parse_outline(element) do
|
defp parse_outline(element) do
|
||||||
kind = element |> xml_attr(:kind) |> normalize_kind()
|
kind = element |> outline_kind() |> normalize_kind()
|
||||||
|
|
||||||
base = %{
|
base = %{
|
||||||
kind: kind,
|
kind: kind,
|
||||||
label: xml_attr(element, :text) || "",
|
label: xml_attr(element, :text) || "",
|
||||||
slug: normalize_optional_string(xml_attr(element, :slug))
|
slug: element |> outline_slug(kind) |> normalize_optional_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
children =
|
children =
|
||||||
@@ -164,6 +161,33 @@ defmodule BDS.Menu do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp render_outline_attributes(item) do
|
||||||
|
kind = Map.get(item, :kind)
|
||||||
|
|
||||||
|
[
|
||||||
|
{"text", item.label},
|
||||||
|
{"type", render_outline_kind(kind)},
|
||||||
|
{"pageSlug", render_page_slug(kind, item.slug)},
|
||||||
|
{"categoryName", render_category_name(kind, item.slug)}
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
defp outline_kind(element), do: xml_attr(element, :type) || xml_attr(element, :kind)
|
||||||
|
|
||||||
|
defp outline_slug(element, :category_archive), do: xml_attr(element, :categoryName) || xml_attr(element, :slug)
|
||||||
|
defp outline_slug(element, :home), do: xml_attr(element, :pageSlug) || xml_attr(element, :slug)
|
||||||
|
defp outline_slug(element, _kind), do: xml_attr(element, :pageSlug) || xml_attr(element, :slug)
|
||||||
|
|
||||||
|
defp render_outline_kind(:category_archive), do: "category-archive"
|
||||||
|
defp render_outline_kind(kind), do: to_string(kind)
|
||||||
|
|
||||||
|
defp render_page_slug(:home, _slug), do: "home"
|
||||||
|
defp render_page_slug(kind, slug) when kind in [:page], do: slug
|
||||||
|
defp render_page_slug(_kind, _slug), do: nil
|
||||||
|
|
||||||
|
defp render_category_name(:category_archive, slug), do: slug
|
||||||
|
defp render_category_name(_kind, _slug), do: nil
|
||||||
|
|
||||||
defp xml_attr(element, name) do
|
defp xml_attr(element, name) do
|
||||||
element
|
element
|
||||||
|> xmlElement(:attributes)
|
|> xmlElement(:attributes)
|
||||||
@@ -176,10 +200,16 @@ defmodule BDS.Menu do
|
|||||||
|
|
||||||
defp normalize_kind(kind) when is_atom(kind) and kind in @valid_kinds, do: kind
|
defp normalize_kind(kind) when is_atom(kind) and kind in @valid_kinds, do: kind
|
||||||
|
|
||||||
|
defp normalize_kind(nil), do: :page
|
||||||
|
|
||||||
defp normalize_kind(kind) when is_binary(kind) do
|
defp normalize_kind(kind) when is_binary(kind) do
|
||||||
kind
|
case kind do
|
||||||
|
"category-archive" -> :category_archive
|
||||||
|
other ->
|
||||||
|
other
|
||||||
|> String.to_existing_atom()
|
|> String.to_existing_atom()
|
||||||
|> normalize_kind()
|
|> normalize_kind()
|
||||||
|
end
|
||||||
rescue
|
rescue
|
||||||
_error -> :page
|
_error -> :page
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ defmodule BDS.Metadata do
|
|||||||
persist_setting(project_id, "category_meta", category_meta_from_files, now)
|
persist_setting(project_id, "category_meta", category_meta_from_files, now)
|
||||||
persist_setting(project_id, "publishing", publishing_from_files, now)
|
persist_setting(project_id, "publishing", publishing_from_files, now)
|
||||||
write_project_json(updated_project, project_metadata_from_files)
|
write_project_json(updated_project, project_metadata_from_files)
|
||||||
write_categories_json(updated_project, categories_from_files["categories"] || @default_categories)
|
write_categories_json(updated_project, normalized_categories(categories_from_files))
|
||||||
write_category_meta_json(updated_project, category_meta_from_files["categories"] || %{})
|
write_category_meta_json(updated_project, normalized_category_settings(category_meta_from_files))
|
||||||
write_publishing_json(updated_project, publishing_from_files)
|
write_publishing_json(updated_project, publishing_from_files)
|
||||||
load_state(updated_project)
|
load_state(updated_project)
|
||||||
end)
|
end)
|
||||||
@@ -248,7 +248,8 @@ defmodule BDS.Metadata do
|
|||||||
"post_template_slug" =>
|
"post_template_slug" =>
|
||||||
Map.get(settings, :post_template_slug, Map.get(settings, "post_template_slug")),
|
Map.get(settings, :post_template_slug, Map.get(settings, "post_template_slug")),
|
||||||
"list_template_slug" =>
|
"list_template_slug" =>
|
||||||
Map.get(settings, :list_template_slug, Map.get(settings, "list_template_slug"))
|
Map.get(settings, :list_template_slug, Map.get(settings, "list_template_slug")),
|
||||||
|
"title" => Map.get(settings, :title, Map.get(settings, "title"))
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -284,18 +285,18 @@ defmodule BDS.Metadata do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp write_project_json(project, project_json),
|
defp write_project_json(project, project_json),
|
||||||
do: write_json(project, "project.json", project_json)
|
do: write_json(project, "project.json", render_project_metadata_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", Enum.sort(categories))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp write_category_meta_json(project, category_settings) do
|
defp write_category_meta_json(project, category_settings) do
|
||||||
write_json(project, "category-meta.json", %{"categories" => category_settings})
|
write_json(project, "category-meta.json", render_category_meta_json(category_settings))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp write_publishing_json(project, publishing_preferences) do
|
defp write_publishing_json(project, publishing_preferences) do
|
||||||
write_json(project, "publishing.json", publishing_preferences)
|
write_json(project, "publishing.json", render_publishing_json(publishing_preferences))
|
||||||
end
|
end
|
||||||
|
|
||||||
defp write_json(project, file_name, payload) do
|
defp write_json(project, file_name, payload) do
|
||||||
@@ -308,11 +309,121 @@ defmodule BDS.Metadata do
|
|||||||
path = Path.join([Projects.project_data_dir(project), "meta", file_name])
|
path = Path.join([Projects.project_data_dir(project), "meta", file_name])
|
||||||
|
|
||||||
case File.read(path) do
|
case File.read(path) do
|
||||||
{:ok, contents} -> Jason.decode!(contents)
|
{:ok, contents} -> contents |> Jason.decode!() |> normalize_json(file_name)
|
||||||
{:error, :enoent} -> nil
|
{:error, :enoent} -> nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp normalize_json(payload, "project.json"), do: parse_project_metadata_json(payload)
|
||||||
|
defp normalize_json(payload, "categories.json"), do: parse_categories_json(payload)
|
||||||
|
defp normalize_json(payload, "category-meta.json"), do: parse_category_meta_json(payload)
|
||||||
|
defp normalize_json(payload, "publishing.json"), do: parse_publishing_json(payload)
|
||||||
|
defp normalize_json(payload, _file_name), do: payload
|
||||||
|
|
||||||
|
defp parse_project_metadata_json(payload) when is_map(payload) do
|
||||||
|
%{
|
||||||
|
"name" => Map.get(payload, "name"),
|
||||||
|
"description" => Map.get(payload, "description"),
|
||||||
|
"public_url" => Map.get(payload, "publicUrl"),
|
||||||
|
"main_language" => Map.get(payload, "mainLanguage"),
|
||||||
|
"default_author" => Map.get(payload, "defaultAuthor"),
|
||||||
|
"max_posts_per_page" => Map.get(payload, "maxPostsPerPage", @default_max_posts_per_page),
|
||||||
|
"blogmark_category" => Map.get(payload, "blogmarkCategory"),
|
||||||
|
"pico_theme" => Map.get(payload, "picoTheme"),
|
||||||
|
"semantic_similarity_enabled" => Map.get(payload, "semanticSimilarityEnabled", false),
|
||||||
|
"blog_languages" => Map.get(payload, "blogLanguages", [])
|
||||||
|
}
|
||||||
|
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||||
|
|> Map.new()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_categories_json(payload) when is_list(payload), do: %{"categories" => payload}
|
||||||
|
defp parse_categories_json(_payload), do: %{"categories" => @default_categories}
|
||||||
|
|
||||||
|
defp parse_category_meta_json(payload) when is_map(payload) do
|
||||||
|
%{"categories" => normalized_category_settings(payload)}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp parse_publishing_json(payload) when is_map(payload) do
|
||||||
|
%{
|
||||||
|
"ssh_host" => Map.get(payload, "sshHost"),
|
||||||
|
"ssh_user" => Map.get(payload, "sshUser"),
|
||||||
|
"ssh_remote_path" => Map.get(payload, "sshRemotePath"),
|
||||||
|
"ssh_mode" => Map.get(payload, "sshMode", "scp")
|
||||||
|
}
|
||||||
|
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||||
|
|> Map.new()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp normalized_categories(%{"categories" => categories}) when is_list(categories), do: categories
|
||||||
|
defp normalized_categories(categories) when is_list(categories), do: categories
|
||||||
|
defp normalized_categories(_payload), do: @default_categories
|
||||||
|
|
||||||
|
defp normalized_category_settings(%{"categories" => settings}) when is_map(settings),
|
||||||
|
do: normalized_category_settings(settings)
|
||||||
|
|
||||||
|
defp normalized_category_settings(settings) when is_map(settings) do
|
||||||
|
Map.new(settings, fn {category, category_settings} ->
|
||||||
|
{category,
|
||||||
|
%{
|
||||||
|
"render_in_lists" =>
|
||||||
|
Map.get(category_settings, "render_in_lists", Map.get(category_settings, "renderInLists", true)),
|
||||||
|
"show_title" =>
|
||||||
|
Map.get(category_settings, "show_title", Map.get(category_settings, "showTitle", true)),
|
||||||
|
"post_template_slug" =>
|
||||||
|
Map.get(category_settings, "post_template_slug", Map.get(category_settings, "postTemplateSlug")),
|
||||||
|
"list_template_slug" =>
|
||||||
|
Map.get(category_settings, "list_template_slug", Map.get(category_settings, "listTemplateSlug")),
|
||||||
|
"title" => Map.get(category_settings, "title")
|
||||||
|
}
|
||||||
|
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||||
|
|> Map.new()}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_project_metadata_json(project_metadata) when is_map(project_metadata) do
|
||||||
|
%{
|
||||||
|
"name" => Map.get(project_metadata, "name"),
|
||||||
|
"description" => Map.get(project_metadata, "description"),
|
||||||
|
"publicUrl" => Map.get(project_metadata, "public_url"),
|
||||||
|
"mainLanguage" => Map.get(project_metadata, "main_language"),
|
||||||
|
"defaultAuthor" => Map.get(project_metadata, "default_author"),
|
||||||
|
"maxPostsPerPage" => Map.get(project_metadata, "max_posts_per_page", @default_max_posts_per_page),
|
||||||
|
"blogmarkCategory" => Map.get(project_metadata, "blogmark_category"),
|
||||||
|
"picoTheme" => Map.get(project_metadata, "pico_theme"),
|
||||||
|
"semanticSimilarityEnabled" => Map.get(project_metadata, "semantic_similarity_enabled", false),
|
||||||
|
"blogLanguages" => Map.get(project_metadata, "blog_languages", [])
|
||||||
|
}
|
||||||
|
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||||
|
|> Map.new()
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_category_meta_json(category_settings) when is_map(category_settings) do
|
||||||
|
Map.new(category_settings, fn {category, settings} ->
|
||||||
|
{category,
|
||||||
|
%{
|
||||||
|
"renderInLists" => Map.get(settings, "render_in_lists", true),
|
||||||
|
"showTitle" => Map.get(settings, "show_title", true),
|
||||||
|
"postTemplateSlug" => Map.get(settings, "post_template_slug"),
|
||||||
|
"listTemplateSlug" => Map.get(settings, "list_template_slug"),
|
||||||
|
"title" => Map.get(settings, "title")
|
||||||
|
}
|
||||||
|
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||||
|
|> Map.new()}
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp render_publishing_json(publishing_preferences) when is_map(publishing_preferences) do
|
||||||
|
%{
|
||||||
|
"sshHost" => Map.get(publishing_preferences, "ssh_host"),
|
||||||
|
"sshUser" => Map.get(publishing_preferences, "ssh_user"),
|
||||||
|
"sshRemotePath" => Map.get(publishing_preferences, "ssh_remote_path"),
|
||||||
|
"sshMode" => Map.get(publishing_preferences, "ssh_mode", "scp")
|
||||||
|
}
|
||||||
|
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|
||||||
|
|> Map.new()
|
||||||
|
end
|
||||||
|
|
||||||
defp load_setting(project_id, suffix) do
|
defp load_setting(project_id, suffix) do
|
||||||
case Repo.get(Setting, setting_key(project_id, suffix)) do
|
case Repo.get(Setting, setting_key(project_id, suffix)) do
|
||||||
nil -> nil
|
nil -> nil
|
||||||
|
|||||||
@@ -605,20 +605,20 @@ defmodule BDS.Posts do
|
|||||||
defp serialize_post_file(post, published_at) do
|
defp serialize_post_file(post, published_at) do
|
||||||
Frontmatter.serialize_document(
|
Frontmatter.serialize_document(
|
||||||
[
|
[
|
||||||
{:id, post.id},
|
{"id", post.id},
|
||||||
{:title, post.title},
|
{"title", post.title},
|
||||||
{:slug, post.slug},
|
{"slug", post.slug},
|
||||||
{:excerpt, post.excerpt},
|
{"excerpt", post.excerpt},
|
||||||
{:status, :published},
|
{"status", :published},
|
||||||
{:author, post.author},
|
{"author", post.author},
|
||||||
{:language, post.language},
|
{"language", post.language},
|
||||||
{:do_not_translate, post.do_not_translate},
|
{"doNotTranslate", post.do_not_translate},
|
||||||
{:template_slug, post.template_slug},
|
{"templateSlug", post.template_slug},
|
||||||
{:created_at, post.created_at},
|
{"createdAt", post.created_at},
|
||||||
{:updated_at, post.updated_at},
|
{"updatedAt", post.updated_at},
|
||||||
{:published_at, published_at},
|
{"publishedAt", published_at},
|
||||||
{:tags, post.tags || []},
|
{"tags", post.tags || []},
|
||||||
{:categories, post.categories || []}
|
{"categories", post.categories || []}
|
||||||
],
|
],
|
||||||
post.content
|
post.content
|
||||||
)
|
)
|
||||||
@@ -641,9 +641,8 @@ defmodule BDS.Posts do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp upsert_post_from_file(project_id, project, path) do
|
defp upsert_post_from_file(project_id, project, path) do
|
||||||
project
|
rebuild_file = parse_rebuild_file(project, path)
|
||||||
|> parse_rebuild_file(path)
|
upsert_post_from_rebuild_file(project_id, rebuild_file)
|
||||||
|> upsert_post_from_rebuild_file(project_id)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp upsert_post_from_rebuild_file(project_id, rebuild_file) do
|
defp upsert_post_from_rebuild_file(project_id, rebuild_file) do
|
||||||
@@ -749,8 +748,8 @@ defmodule BDS.Posts do
|
|||||||
{"updatedAt", "updated_at"},
|
{"updatedAt", "updated_at"},
|
||||||
{"publishedAt", "published_at"}
|
{"publishedAt", "published_at"}
|
||||||
]
|
]
|
||||||
|> Enum.reduce(fields, fn {legacy_key, current_key}, acc ->
|
|> Enum.reduce(fields, fn {file_key, current_key}, acc ->
|
||||||
case Map.fetch(acc, legacy_key) do
|
case Map.fetch(acc, file_key) do
|
||||||
{:ok, value} -> Map.put_new(acc, current_key, normalize_rebuild_field_value(current_key, value))
|
{:ok, value} -> Map.put_new(acc, current_key, normalize_rebuild_field_value(current_key, value))
|
||||||
:error -> acc
|
:error -> acc
|
||||||
end
|
end
|
||||||
@@ -875,15 +874,15 @@ defmodule BDS.Posts do
|
|||||||
defp serialize_translation_file(translation, published_at) do
|
defp serialize_translation_file(translation, published_at) do
|
||||||
Frontmatter.serialize_document(
|
Frontmatter.serialize_document(
|
||||||
[
|
[
|
||||||
{:id, translation.id},
|
{"id", translation.id},
|
||||||
{:translation_for, translation.translation_for},
|
{"translationFor", translation.translation_for},
|
||||||
{:language, translation.language},
|
{"language", translation.language},
|
||||||
{:title, translation.title},
|
{"title", translation.title},
|
||||||
{:excerpt, translation.excerpt},
|
{"excerpt", translation.excerpt},
|
||||||
{:status, :published},
|
{"status", :published},
|
||||||
{:created_at, translation.created_at},
|
{"createdAt", translation.created_at},
|
||||||
{:updated_at, translation.updated_at},
|
{"updatedAt", translation.updated_at},
|
||||||
{:published_at, published_at}
|
{"publishedAt", published_at}
|
||||||
],
|
],
|
||||||
translation.content
|
translation.content
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
|
|
||||||
import Ecto.Query
|
import Ecto.Query
|
||||||
|
|
||||||
|
@compiled_env Application.compile_env(:bds, :current_env, Mix.env())
|
||||||
|
|
||||||
alias BDS.AI
|
alias BDS.AI
|
||||||
alias BDS.Desktop.FolderPicker
|
alias BDS.Desktop.FolderPicker
|
||||||
alias BDS.Desktop.MenuBar
|
alias BDS.Desktop.MenuBar
|
||||||
@@ -1704,6 +1706,10 @@ defmodule BDS.Scripting.Capabilities do
|
|||||||
end
|
end
|
||||||
|
|
||||||
defp test_mode? do
|
defp test_mode? do
|
||||||
Application.get_env(:bds, :test_mode, false)
|
Application.get_env(:bds, :test_mode, false) or current_env() == :test
|
||||||
|
end
|
||||||
|
|
||||||
|
defp current_env do
|
||||||
|
Application.get_env(:bds, :current_env_override) || @compiled_env
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -174,15 +174,16 @@ defmodule BDS.Scripts do
|
|||||||
defp serialize_script_file(script, content) do
|
defp serialize_script_file(script, content) do
|
||||||
Frontmatter.serialize_document(
|
Frontmatter.serialize_document(
|
||||||
[
|
[
|
||||||
{:id, script.id},
|
{"id", script.id},
|
||||||
{:slug, script.slug},
|
{"projectId", script.project_id},
|
||||||
{:title, script.title},
|
{"slug", script.slug},
|
||||||
{:kind, script.kind},
|
{"title", script.title},
|
||||||
{:entrypoint, script.entrypoint},
|
{"kind", script.kind},
|
||||||
{:enabled, script.enabled},
|
{"entrypoint", script.entrypoint},
|
||||||
{:version, script.version},
|
{"enabled", script.enabled},
|
||||||
{:created_at, script.created_at},
|
{"version", script.version},
|
||||||
{:updated_at, script.updated_at}
|
{"createdAt", script.created_at},
|
||||||
|
{"updatedAt", script.updated_at}
|
||||||
],
|
],
|
||||||
content
|
content
|
||||||
)
|
)
|
||||||
@@ -203,6 +204,7 @@ defmodule BDS.Scripts do
|
|||||||
defp upsert_script_from_file(project_id, project, path) do
|
defp upsert_script_from_file(project_id, project, path) do
|
||||||
contents = File.read!(path)
|
contents = File.read!(path)
|
||||||
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
||||||
|
fields = normalize_script_fields(fields)
|
||||||
relative_path = Path.relative_to(path, Projects.project_data_dir(project))
|
relative_path = Path.relative_to(path, Projects.project_data_dir(project))
|
||||||
now = Persistence.now_ms()
|
now = Persistence.now_ms()
|
||||||
|
|
||||||
@@ -234,6 +236,20 @@ defmodule BDS.Scripts do
|
|||||||
defp parse_script_kind("utility"), do: :utility
|
defp parse_script_kind("utility"), do: :utility
|
||||||
defp parse_script_kind("transform"), do: :transform
|
defp parse_script_kind("transform"), do: :transform
|
||||||
|
|
||||||
|
defp normalize_script_fields(fields) when is_map(fields) do
|
||||||
|
[
|
||||||
|
{"createdAt", "created_at"},
|
||||||
|
{"updatedAt", "updated_at"},
|
||||||
|
{"projectId", "project_id"}
|
||||||
|
]
|
||||||
|
|> Enum.reduce(fields, fn {file_key, current_key}, acc ->
|
||||||
|
case Map.fetch(acc, file_key) do
|
||||||
|
{:ok, value} -> Map.put_new(acc, current_key, value)
|
||||||
|
:error -> acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp list_matching_files(dir, pattern) do
|
defp list_matching_files(dir, pattern) do
|
||||||
if File.dir?(dir) do
|
if File.dir?(dir) do
|
||||||
Path.join(dir, pattern)
|
Path.join(dir, pattern)
|
||||||
|
|||||||
@@ -145,5 +145,8 @@ defmodule BDS.Sidecar do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
defp timestamp_key?(key), do: String.ends_with?(to_string(key), "_at")
|
defp timestamp_key?(key) do
|
||||||
|
rendered = to_string(key)
|
||||||
|
String.ends_with?(rendered, "_at") or String.ends_with?(rendered, "At")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -215,17 +215,15 @@ defmodule BDS.Tags do
|
|||||||
path = Path.join([Projects.project_data_dir(project), "meta", "tags.json"])
|
path = Path.join([Projects.project_data_dir(project), "meta", "tags.json"])
|
||||||
:ok = File.mkdir_p(Path.dirname(path))
|
:ok = File.mkdir_p(Path.dirname(path))
|
||||||
|
|
||||||
payload = %{
|
payload =
|
||||||
"tags" =>
|
|
||||||
project_id
|
project_id
|
||||||
|> list_tags()
|
|> list_tags()
|
||||||
|> Enum.sort_by(&String.downcase(&1.name))
|
|> Enum.sort_by(&String.downcase(&1.name))
|
||||||
|> Enum.map(fn tag ->
|
|> Enum.map(fn tag ->
|
||||||
%{"name" => tag.name}
|
%{"name" => tag.name}
|
||||||
|> maybe_put("color", tag.color)
|
|> maybe_put("color", tag.color)
|
||||||
|> maybe_put("post_template_slug", tag.post_template_slug)
|
|> maybe_put("postTemplateSlug", tag.post_template_slug)
|
||||||
end)
|
end)
|
||||||
}
|
|
||||||
|
|
||||||
:ok = Persistence.atomic_write(path, Jason.encode!(payload))
|
:ok = Persistence.atomic_write(path, Jason.encode!(payload))
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -219,14 +219,15 @@ defmodule BDS.Templates do
|
|||||||
defp serialize_template_file(template, content) do
|
defp serialize_template_file(template, content) do
|
||||||
Frontmatter.serialize_document(
|
Frontmatter.serialize_document(
|
||||||
[
|
[
|
||||||
{:id, template.id},
|
{"id", template.id},
|
||||||
{:slug, template.slug},
|
{"projectId", template.project_id},
|
||||||
{:title, template.title},
|
{"slug", template.slug},
|
||||||
{:kind, template.kind},
|
{"title", template.title},
|
||||||
{:enabled, template.enabled},
|
{"kind", template.kind},
|
||||||
{:version, template.version},
|
{"enabled", template.enabled},
|
||||||
{:created_at, template.created_at},
|
{"version", template.version},
|
||||||
{:updated_at, template.updated_at}
|
{"createdAt", template.created_at},
|
||||||
|
{"updatedAt", template.updated_at}
|
||||||
],
|
],
|
||||||
content
|
content
|
||||||
)
|
)
|
||||||
@@ -342,6 +343,7 @@ defmodule BDS.Templates do
|
|||||||
defp upsert_template_from_file(project_id, project, path) do
|
defp upsert_template_from_file(project_id, project, path) do
|
||||||
contents = File.read!(path)
|
contents = File.read!(path)
|
||||||
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
{:ok, %{fields: fields}} = Frontmatter.parse_document(contents)
|
||||||
|
fields = normalize_template_fields(fields)
|
||||||
relative_path = Path.relative_to(path, Projects.project_data_dir(project))
|
relative_path = Path.relative_to(path, Projects.project_data_dir(project))
|
||||||
now = Persistence.now_ms()
|
now = Persistence.now_ms()
|
||||||
|
|
||||||
@@ -373,6 +375,20 @@ defmodule BDS.Templates do
|
|||||||
defp parse_template_kind("not_found"), do: :not_found
|
defp parse_template_kind("not_found"), do: :not_found
|
||||||
defp parse_template_kind("partial"), do: :partial
|
defp parse_template_kind("partial"), do: :partial
|
||||||
|
|
||||||
|
defp normalize_template_fields(fields) when is_map(fields) do
|
||||||
|
[
|
||||||
|
{"createdAt", "created_at"},
|
||||||
|
{"updatedAt", "updated_at"},
|
||||||
|
{"projectId", "project_id"}
|
||||||
|
]
|
||||||
|
|> Enum.reduce(fields, fn {file_key, current_key}, acc ->
|
||||||
|
case Map.fetch(acc, file_key) do
|
||||||
|
{:ok, value} -> Map.put_new(acc, current_key, value)
|
||||||
|
:error -> acc
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp list_matching_files(dir, pattern) do
|
defp list_matching_files(dir, pattern) do
|
||||||
if File.dir?(dir) do
|
if File.dir?(dir) do
|
||||||
Path.join(dir, pattern)
|
Path.join(dir, pattern)
|
||||||
|
|||||||
@@ -43,16 +43,17 @@ defmodule BDS.MediaTest do
|
|||||||
|
|
||||||
sidecar = File.read!(Path.join(temp_dir, media.sidecar_path))
|
sidecar = File.read!(Path.join(temp_dir, media.sidecar_path))
|
||||||
assert sidecar =~ "id: #{media.id}\n"
|
assert sidecar =~ "id: #{media.id}\n"
|
||||||
assert sidecar =~ "original_name: sample.txt\n"
|
assert sidecar =~ "originalName: sample.txt\n"
|
||||||
assert sidecar =~ "mime_type: text/plain\n"
|
assert sidecar =~ "mimeType: text/plain\n"
|
||||||
assert sidecar =~ "title: Sample\n"
|
assert sidecar =~ "title: Sample\n"
|
||||||
assert sidecar =~ "alt: Alt text\n"
|
assert sidecar =~ "alt: Alt text\n"
|
||||||
assert sidecar =~ "caption: Caption\n"
|
assert sidecar =~ "caption: Caption\n"
|
||||||
assert sidecar =~ "author: Writer\n"
|
assert sidecar =~ "author: Writer\n"
|
||||||
assert sidecar =~ "language: en\n"
|
assert sidecar =~ "language: en\n"
|
||||||
assert sidecar =~ "tags:\n - alpha\n"
|
assert sidecar =~ "tags:\n - alpha\n"
|
||||||
assert sidecar =~ ~r/created_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert sidecar =~ "linkedPostIds:\n"
|
||||||
assert sidecar =~ ~r/updated_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert sidecar =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
|
assert sidecar =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
refute File.exists?(Path.join(temp_dir, media.sidecar_path <> ".tmp"))
|
refute File.exists?(Path.join(temp_dir, media.sidecar_path <> ".tmp"))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -130,8 +131,8 @@ defmodule BDS.MediaTest do
|
|||||||
sidecar_path,
|
sidecar_path,
|
||||||
[
|
[
|
||||||
"id: media-from-file",
|
"id: media-from-file",
|
||||||
"original_name: original.jpg",
|
"originalName: original.jpg",
|
||||||
"mime_type: image/jpeg",
|
"mimeType: image/jpeg",
|
||||||
"size: #{byte_size(tiny_jpeg_binary())}",
|
"size: #{byte_size(tiny_jpeg_binary())}",
|
||||||
"width: 3",
|
"width: 3",
|
||||||
"height: 2",
|
"height: 2",
|
||||||
@@ -140,8 +141,10 @@ defmodule BDS.MediaTest do
|
|||||||
"caption: Recovered caption",
|
"caption: Recovered caption",
|
||||||
"author: Writer",
|
"author: Writer",
|
||||||
"language: en",
|
"language: en",
|
||||||
"created_at: 2024-03-30T21:20:00.000Z",
|
"createdAt: 2024-03-30T21:20:00.000Z",
|
||||||
"updated_at: 2024-03-31T21:20:00.000Z",
|
"updatedAt: 2024-03-31T21:20:00.000Z",
|
||||||
|
"linkedPostIds:",
|
||||||
|
" - post-a",
|
||||||
"tags:",
|
"tags:",
|
||||||
" - alpha",
|
" - alpha",
|
||||||
""
|
""
|
||||||
@@ -152,7 +155,7 @@ defmodule BDS.MediaTest do
|
|||||||
File.write!(
|
File.write!(
|
||||||
binary_path <> ".de.meta",
|
binary_path <> ".de.meta",
|
||||||
[
|
[
|
||||||
"translation_for: media-from-file",
|
"translationFor: media-from-file",
|
||||||
"language: de",
|
"language: de",
|
||||||
"title: Titel",
|
"title: Titel",
|
||||||
"alt: Alt text",
|
"alt: Alt text",
|
||||||
@@ -371,7 +374,7 @@ defmodule BDS.MediaTest do
|
|||||||
|
|
||||||
translated_sidecar_path = Path.join(temp_dir, media.file_path <> ".de.meta")
|
translated_sidecar_path = Path.join(temp_dir, media.file_path <> ".de.meta")
|
||||||
contents = File.read!(translated_sidecar_path)
|
contents = File.read!(translated_sidecar_path)
|
||||||
assert contents =~ "translation_for: #{media.id}\n"
|
assert contents =~ "translationFor: #{media.id}\n"
|
||||||
assert contents =~ "language: de\n"
|
assert contents =~ "language: de\n"
|
||||||
assert contents =~ "title: Titel\n"
|
assert contents =~ "title: Titel\n"
|
||||||
assert contents =~ "alt: Alt text\n"
|
assert contents =~ "alt: Alt text\n"
|
||||||
|
|||||||
@@ -44,16 +44,16 @@ defmodule BDS.MenuTest do
|
|||||||
assert File.exists?(opml_path)
|
assert File.exists?(opml_path)
|
||||||
|
|
||||||
contents = File.read!(opml_path)
|
contents = File.read!(opml_path)
|
||||||
assert contents =~ ~s(<outline kind="home" text="Home")
|
assert contents =~ ~s(<outline text="Home" type="home" pageSlug="home")
|
||||||
assert contents =~ ~s(<outline kind="page" text="About" slug="about")
|
assert contents =~ ~s(<outline text="About" type="page" pageSlug="about")
|
||||||
assert contents =~ ~s(<outline kind="submenu" text="Sections")
|
assert contents =~ ~s(<outline text="Sections" type="submenu")
|
||||||
assert contents =~ ~s(<outline kind="category_archive" text="Notes" slug="notes")
|
assert contents =~ ~s(<outline text="Notes" type="category-archive" categoryName="notes")
|
||||||
|
|
||||||
assert {:ok, loaded} = BDS.Menu.get_menu(project.id)
|
assert {:ok, loaded} = BDS.Menu.get_menu(project.id)
|
||||||
assert loaded == menu
|
assert loaded == menu
|
||||||
end
|
end
|
||||||
|
|
||||||
test "sync_menu_from_filesystem loads canonical OPML and preserves a prepended Home entry", %{
|
test "sync_menu_from_filesystem loads canonical bDS OPML and preserves a prepended Home entry", %{
|
||||||
project: project,
|
project: project,
|
||||||
temp_dir: temp_dir
|
temp_dir: temp_dir
|
||||||
} do
|
} do
|
||||||
@@ -65,10 +65,14 @@ defmodule BDS.MenuTest do
|
|||||||
[
|
[
|
||||||
~s(<?xml version="1.0" encoding="UTF-8"?>),
|
~s(<?xml version="1.0" encoding="UTF-8"?>),
|
||||||
~s(<opml version="2.0">),
|
~s(<opml version="2.0">),
|
||||||
|
~s( <head>),
|
||||||
|
~s( <title>Blog Menu</title>),
|
||||||
|
~s( </head>),
|
||||||
~s( <body>),
|
~s( <body>),
|
||||||
~s( <outline kind="page" text="Blog" slug="blog" />),
|
~s( <outline id="menu-home" text="Home" type="home" pageSlug="home"/>),
|
||||||
~s( <outline kind="submenu" text="Topics">),
|
~s( <outline id="menu-topics" text="Topics" type="submenu">),
|
||||||
~s( <outline kind="category_archive" text="Elixir" slug="elixir" />),
|
~s( <outline id="menu-page" text="Blog" type="page" pageId="page-1" pageSlug="blog"/>),
|
||||||
|
~s( <outline id="menu-cat" text="Elixir" type="category-archive" categoryName="elixir"/>),
|
||||||
~s( </outline>),
|
~s( </outline>),
|
||||||
~s( </body>),
|
~s( </body>),
|
||||||
~s(</opml>)
|
~s(</opml>)
|
||||||
@@ -80,12 +84,12 @@ defmodule BDS.MenuTest do
|
|||||||
|
|
||||||
assert menu.items == [
|
assert menu.items == [
|
||||||
%{kind: :home, label: "Home", slug: nil},
|
%{kind: :home, label: "Home", slug: nil},
|
||||||
%{kind: :page, label: "Blog", slug: "blog"},
|
|
||||||
%{
|
%{
|
||||||
kind: :submenu,
|
kind: :submenu,
|
||||||
label: "Topics",
|
label: "Topics",
|
||||||
slug: nil,
|
slug: nil,
|
||||||
children: [
|
children: [
|
||||||
|
%{kind: :page, label: "Blog", slug: "blog"},
|
||||||
%{kind: :category_archive, label: "Elixir", slug: "elixir"}
|
%{kind: :category_archive, label: "Elixir", slug: "elixir"}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,14 +38,14 @@ defmodule BDS.MetadataTest do
|
|||||||
assert %{
|
assert %{
|
||||||
"name" => "Renamed Blog",
|
"name" => "Renamed Blog",
|
||||||
"description" => "Description",
|
"description" => "Description",
|
||||||
"public_url" => "https://example.com",
|
"publicUrl" => "https://example.com",
|
||||||
"main_language" => "en",
|
"mainLanguage" => "en",
|
||||||
"default_author" => "Writer",
|
"defaultAuthor" => "Writer",
|
||||||
"max_posts_per_page" => 25,
|
"maxPostsPerPage" => 25,
|
||||||
"blogmark_category" => "links",
|
"blogmarkCategory" => "links",
|
||||||
"pico_theme" => "blue",
|
"picoTheme" => "blue",
|
||||||
"semantic_similarity_enabled" => true,
|
"semanticSimilarityEnabled" => true,
|
||||||
"blog_languages" => ["de", "fr"]
|
"blogLanguages" => ["de", "fr"]
|
||||||
} = Jason.decode!(File.read!(project_json_path))
|
} = Jason.decode!(File.read!(project_json_path))
|
||||||
|
|
||||||
assert {:ok, loaded} = BDS.Metadata.get_project_metadata(project.id)
|
assert {:ok, loaded} = BDS.Metadata.get_project_metadata(project.id)
|
||||||
@@ -78,25 +78,23 @@ defmodule BDS.MetadataTest do
|
|||||||
category_meta_path = Path.join([temp_dir, "meta", "category-meta.json"])
|
category_meta_path = Path.join([temp_dir, "meta", "category-meta.json"])
|
||||||
publishing_path = Path.join([temp_dir, "meta", "publishing.json"])
|
publishing_path = Path.join([temp_dir, "meta", "publishing.json"])
|
||||||
|
|
||||||
assert %{"categories" => ["article", "aside", "news", "page", "picture"]} =
|
assert ["article", "aside", "news", "page", "picture"] =
|
||||||
Jason.decode!(File.read!(categories_path))
|
Jason.decode!(File.read!(categories_path))
|
||||||
|
|
||||||
assert %{
|
assert %{
|
||||||
"categories" => %{
|
|
||||||
"news" => %{
|
"news" => %{
|
||||||
"render_in_lists" => false,
|
"renderInLists" => false,
|
||||||
"show_title" => true,
|
"showTitle" => true,
|
||||||
"post_template_slug" => "article",
|
"postTemplateSlug" => "article",
|
||||||
"list_template_slug" => "listing"
|
"listTemplateSlug" => "listing"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} = Jason.decode!(File.read!(category_meta_path))
|
} = Jason.decode!(File.read!(category_meta_path))
|
||||||
|
|
||||||
assert %{
|
assert %{
|
||||||
"ssh_host" => "example.com",
|
"sshHost" => "example.com",
|
||||||
"ssh_user" => "deploy",
|
"sshUser" => "deploy",
|
||||||
"ssh_remote_path" => "/srv/site",
|
"sshRemotePath" => "/srv/site",
|
||||||
"ssh_mode" => "rsync"
|
"sshMode" => "rsync"
|
||||||
} = Jason.decode!(File.read!(publishing_path))
|
} = Jason.decode!(File.read!(publishing_path))
|
||||||
|
|
||||||
assert {:ok, synced} = BDS.Metadata.sync_project_metadata_from_filesystem(project.id)
|
assert {:ok, synced} = BDS.Metadata.sync_project_metadata_from_filesystem(project.id)
|
||||||
@@ -117,6 +115,87 @@ defmodule BDS.MetadataTest do
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "sync_project_metadata_from_filesystem reads canonical bDS metadata file shapes", %{
|
||||||
|
project: project,
|
||||||
|
temp_dir: temp_dir
|
||||||
|
} do
|
||||||
|
meta_dir = Path.join(temp_dir, "meta")
|
||||||
|
File.mkdir_p!(meta_dir)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(meta_dir, "project.json"),
|
||||||
|
Jason.encode!(%{
|
||||||
|
"name" => "Legacy Blog",
|
||||||
|
"description" => "Imported",
|
||||||
|
"publicUrl" => "https://legacy.example",
|
||||||
|
"mainLanguage" => "de",
|
||||||
|
"defaultAuthor" => "Legacy Writer",
|
||||||
|
"maxPostsPerPage" => 17,
|
||||||
|
"blogmarkCategory" => "aside",
|
||||||
|
"picoTheme" => "slate",
|
||||||
|
"semanticSimilarityEnabled" => true,
|
||||||
|
"blogLanguages" => ["en"]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(meta_dir, "categories.json"),
|
||||||
|
Jason.encode!(["article", "aside", "legacy", "page", "picture"])
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(meta_dir, "category-meta.json"),
|
||||||
|
Jason.encode!(%{
|
||||||
|
"legacy" => %{
|
||||||
|
"renderInLists" => false,
|
||||||
|
"showTitle" => true,
|
||||||
|
"postTemplateSlug" => "feature-view",
|
||||||
|
"listTemplateSlug" => "feature-list",
|
||||||
|
"title" => "Legacy"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
File.write!(
|
||||||
|
Path.join(meta_dir, "publishing.json"),
|
||||||
|
Jason.encode!(%{
|
||||||
|
"sshHost" => "legacy.example",
|
||||||
|
"sshUser" => "deploy",
|
||||||
|
"sshRemotePath" => "/srv/legacy",
|
||||||
|
"sshMode" => "rsync"
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
assert {:ok, synced} = BDS.Metadata.sync_project_metadata_from_filesystem(project.id)
|
||||||
|
|
||||||
|
assert synced.name == "Legacy Blog"
|
||||||
|
assert synced.description == "Imported"
|
||||||
|
assert synced.public_url == "https://legacy.example"
|
||||||
|
assert synced.main_language == "de"
|
||||||
|
assert synced.default_author == "Legacy Writer"
|
||||||
|
assert synced.max_posts_per_page == 17
|
||||||
|
assert synced.blogmark_category == "aside"
|
||||||
|
assert synced.pico_theme == "slate"
|
||||||
|
assert synced.semantic_similarity_enabled == true
|
||||||
|
assert synced.blog_languages == ["en"]
|
||||||
|
assert synced.categories == ["article", "aside", "legacy", "page", "picture"]
|
||||||
|
|
||||||
|
assert synced.category_settings["legacy"] == %{
|
||||||
|
"render_in_lists" => false,
|
||||||
|
"show_title" => true,
|
||||||
|
"post_template_slug" => "feature-view",
|
||||||
|
"list_template_slug" => "feature-list",
|
||||||
|
"title" => "Legacy"
|
||||||
|
}
|
||||||
|
|
||||||
|
assert synced.publishing_preferences == %{
|
||||||
|
"ssh_host" => "legacy.example",
|
||||||
|
"ssh_user" => "deploy",
|
||||||
|
"ssh_remote_path" => "/srv/legacy",
|
||||||
|
"ssh_mode" => "rsync"
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
test "enabling semantic similarity backfills embeddings for existing published posts", %{
|
test "enabling semantic similarity backfills embeddings for existing published posts", %{
|
||||||
project: project
|
project: project
|
||||||
} do
|
} do
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
assert File.exists?(translation_path)
|
assert File.exists?(translation_path)
|
||||||
|
|
||||||
translation_contents = File.read!(translation_path)
|
translation_contents = File.read!(translation_path)
|
||||||
|
assert translation_contents =~ "translationFor: #{post.id}\n"
|
||||||
assert translation_contents =~ "title: Kanonischer Beitrag\n"
|
assert translation_contents =~ "title: Kanonischer Beitrag\n"
|
||||||
assert translation_contents =~ "language: de\n"
|
assert translation_contents =~ "language: de\n"
|
||||||
assert translation_contents =~ "status: published\n"
|
assert translation_contents =~ "status: published\n"
|
||||||
@@ -124,13 +125,13 @@ defmodule BDS.PostTranslationsTest do
|
|||||||
[
|
[
|
||||||
"---",
|
"---",
|
||||||
"id: orphan-translation",
|
"id: orphan-translation",
|
||||||
"translation_for: missing-post",
|
"translationFor: missing-post",
|
||||||
"language: fr",
|
"language: fr",
|
||||||
"title: Orpheline",
|
"title: Orpheline",
|
||||||
"status: published",
|
"status: published",
|
||||||
"created_at: 1711843200",
|
"createdAt: 1711843200",
|
||||||
"updated_at: 1711929600",
|
"updatedAt: 1711929600",
|
||||||
"published_at: 1712016000",
|
"publishedAt: 1712016000",
|
||||||
"---",
|
"---",
|
||||||
"Texte orphelin",
|
"Texte orphelin",
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -147,13 +147,13 @@ defmodule BDS.PostsTest do
|
|||||||
assert file_contents =~ "excerpt: Summary\n"
|
assert file_contents =~ "excerpt: Summary\n"
|
||||||
assert file_contents =~ "author: Writer\n"
|
assert file_contents =~ "author: Writer\n"
|
||||||
assert file_contents =~ "language: en\n"
|
assert file_contents =~ "language: en\n"
|
||||||
assert file_contents =~ "do_not_translate: true\n"
|
assert file_contents =~ "doNotTranslate: true\n"
|
||||||
assert file_contents =~ "template_slug: article\n"
|
assert file_contents =~ "templateSlug: article\n"
|
||||||
assert file_contents =~ "tags:\n - alpha\n"
|
assert file_contents =~ "tags:\n - alpha\n"
|
||||||
assert file_contents =~ "categories:\n - notes\n"
|
assert file_contents =~ "categories:\n - notes\n"
|
||||||
assert file_contents =~ ~r/created_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert file_contents =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
assert file_contents =~ ~r/updated_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert file_contents =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
assert file_contents =~ ~r/published_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert file_contents =~ ~r/publishedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
assert file_contents =~ "\n---\nHello from markdown\n"
|
assert file_contents =~ "\n---\nHello from markdown\n"
|
||||||
|
|
||||||
refute File.exists?(full_path <> ".tmp")
|
refute File.exists?(full_path <> ".tmp")
|
||||||
@@ -274,11 +274,11 @@ defmodule BDS.PostsTest do
|
|||||||
"status: published",
|
"status: published",
|
||||||
"author: Writer",
|
"author: Writer",
|
||||||
"language: en",
|
"language: en",
|
||||||
"do_not_translate: true",
|
"doNotTranslate: true",
|
||||||
"template_slug: article",
|
"templateSlug: article",
|
||||||
"created_at: 2024-03-30T21:20:00.000Z",
|
"createdAt: 2024-03-30T21:20:00.000Z",
|
||||||
"updated_at: 2024-03-31T21:20:00.000Z",
|
"updatedAt: 2024-03-31T21:20:00.000Z",
|
||||||
"published_at: 2024-04-01T21:20:00.000Z",
|
"publishedAt: 2024-04-01T21:20:00.000Z",
|
||||||
"tags:",
|
"tags:",
|
||||||
" - alpha",
|
" - alpha",
|
||||||
"categories:",
|
"categories:",
|
||||||
@@ -313,7 +313,7 @@ defmodule BDS.PostsTest do
|
|||||||
assert post.content == nil
|
assert post.content == nil
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rebuild_posts_from_files imports legacy old-app translation files alongside canonical posts" do
|
test "rebuild_posts_from_files imports canonical bDS translation files alongside canonical posts" do
|
||||||
temp_dir =
|
temp_dir =
|
||||||
Path.join(System.tmp_dir!(), "bds-post-rebuild-legacy-#{System.unique_integer([:positive])}")
|
Path.join(System.tmp_dir!(), "bds-post-rebuild-legacy-#{System.unique_integer([:positive])}")
|
||||||
|
|
||||||
|
|||||||
@@ -65,14 +65,15 @@ defmodule BDS.ScriptsTest do
|
|||||||
|
|
||||||
contents = File.read!(full_path)
|
contents = File.read!(full_path)
|
||||||
assert contents =~ "---\nid: #{published.id}\n"
|
assert contents =~ "---\nid: #{published.id}\n"
|
||||||
|
assert contents =~ "projectId: #{project.id}\n"
|
||||||
assert contents =~ "slug: process-feed\n"
|
assert contents =~ "slug: process-feed\n"
|
||||||
assert contents =~ "title: Process Feed\n"
|
assert contents =~ "title: Process Feed\n"
|
||||||
assert contents =~ "kind: utility\n"
|
assert contents =~ "kind: utility\n"
|
||||||
assert contents =~ "entrypoint: main\n"
|
assert contents =~ "entrypoint: main\n"
|
||||||
assert contents =~ "enabled: true\n"
|
assert contents =~ "enabled: true\n"
|
||||||
assert contents =~ "version: 1\n"
|
assert contents =~ "version: 1\n"
|
||||||
assert contents =~ ~r/created_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert contents =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
assert contents =~ ~r/updated_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert contents =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
assert contents =~ "\n---\nfunction main() return 'ok' end\n"
|
assert contents =~ "\n---\nfunction main() return 'ok' end\n"
|
||||||
refute File.exists?(full_path <> ".tmp")
|
refute File.exists?(full_path <> ".tmp")
|
||||||
end
|
end
|
||||||
@@ -139,14 +140,15 @@ defmodule BDS.ScriptsTest do
|
|||||||
[
|
[
|
||||||
"---",
|
"---",
|
||||||
"id: script-from-file",
|
"id: script-from-file",
|
||||||
|
"projectId: #{project.id}",
|
||||||
"slug: recovered",
|
"slug: recovered",
|
||||||
"title: Recovered Script",
|
"title: Recovered Script",
|
||||||
"kind: utility",
|
"kind: utility",
|
||||||
"entrypoint: main",
|
"entrypoint: main",
|
||||||
"enabled: true",
|
"enabled: true",
|
||||||
"version: 4",
|
"version: 4",
|
||||||
"created_at: 1970-01-01T00:00:00.301Z",
|
"createdAt: 1970-01-01T00:00:00.301Z",
|
||||||
"updated_at: 1970-01-01T00:00:00.404Z",
|
"updatedAt: 1970-01-01T00:00:00.404Z",
|
||||||
"---",
|
"---",
|
||||||
"function main() return 'restored' end",
|
"function main() return 'restored' end",
|
||||||
""
|
""
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ defmodule BDS.TagsTest do
|
|||||||
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
||||||
assert File.exists?(tags_path)
|
assert File.exists?(tags_path)
|
||||||
|
|
||||||
assert %{"tags" => [%{"name" => "Alpha"}, %{"color" => "#000000", "name" => "Zebra"}]} =
|
assert [%{"name" => "Alpha"}, %{"color" => "#000000", "name" => "Zebra"}] =
|
||||||
Jason.decode!(File.read!(tags_path))
|
Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -59,11 +59,9 @@ defmodule BDS.TagsTest do
|
|||||||
|
|
||||||
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
||||||
|
|
||||||
assert %{
|
assert [
|
||||||
"tags" => [
|
%{"name" => "Alpha", "color" => "#112233", "postTemplateSlug" => "article"}
|
||||||
%{"name" => "Alpha", "color" => "#112233", "post_template_slug" => "article"}
|
] =
|
||||||
]
|
|
||||||
} =
|
|
||||||
Jason.decode!(File.read!(tags_path))
|
Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -93,7 +91,7 @@ defmodule BDS.TagsTest do
|
|||||||
assert contents =~ "\n---\nBody\n"
|
assert 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" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
|
assert [%{"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",
|
test "merge_tags moves source tags onto the target, deduplicates post tags, deletes sources, and refreshes tags.json",
|
||||||
@@ -126,7 +124,7 @@ defmodule BDS.TagsTest do
|
|||||||
assert contents =~ "\n---\nBody\n"
|
assert 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" => "Gamma"}]} = Jason.decode!(File.read!(tags_path))
|
assert [%{"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",
|
test "delete_tag removes the tag from posts, rewrites published files, deletes the row, and refreshes tags.json",
|
||||||
@@ -157,7 +155,7 @@ defmodule BDS.TagsTest do
|
|||||||
assert contents =~ "\n---\nBody\n"
|
assert 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" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
|
assert [%{"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",
|
test "sync_tags_from_posts creates missing tags from post tag arrays and refreshes tags.json",
|
||||||
@@ -196,17 +194,15 @@ defmodule BDS.TagsTest do
|
|||||||
|
|
||||||
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
||||||
|
|
||||||
assert %{
|
assert [
|
||||||
"tags" => [
|
|
||||||
%{"name" => "Another"},
|
%{"name" => "Another"},
|
||||||
%{
|
%{
|
||||||
"name" => "Existing",
|
"name" => "Existing",
|
||||||
"color" => "#112233",
|
"color" => "#112233",
|
||||||
"post_template_slug" => "feature-view"
|
"postTemplateSlug" => "feature-view"
|
||||||
},
|
},
|
||||||
%{"name" => "Missing"}
|
%{"name" => "Missing"}
|
||||||
]
|
] = Jason.decode!(File.read!(tags_path))
|
||||||
} = Jason.decode!(File.read!(tags_path))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defp errors_on(changeset) do
|
defp errors_on(changeset) do
|
||||||
|
|||||||
@@ -66,13 +66,14 @@ defmodule BDS.TemplatesTest do
|
|||||||
|
|
||||||
contents = File.read!(full_path)
|
contents = File.read!(full_path)
|
||||||
assert contents =~ "---\nid: #{published.id}\n"
|
assert contents =~ "---\nid: #{published.id}\n"
|
||||||
|
assert contents =~ "projectId: #{project.id}\n"
|
||||||
assert contents =~ "slug: landing-page\n"
|
assert contents =~ "slug: landing-page\n"
|
||||||
assert contents =~ "title: Landing Page\n"
|
assert contents =~ "title: Landing Page\n"
|
||||||
assert contents =~ "kind: list\n"
|
assert contents =~ "kind: list\n"
|
||||||
assert contents =~ "enabled: true\n"
|
assert contents =~ "enabled: true\n"
|
||||||
assert contents =~ "version: 1\n"
|
assert contents =~ "version: 1\n"
|
||||||
assert contents =~ ~r/created_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert contents =~ ~r/createdAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
assert contents =~ ~r/updated_at: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
assert contents =~ ~r/updatedAt: \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\n/
|
||||||
assert contents =~ "\n---\n<section>{{ page_title }}</section>\n"
|
assert contents =~ "\n---\n<section>{{ page_title }}</section>\n"
|
||||||
refute File.exists?(full_path <> ".tmp")
|
refute File.exists?(full_path <> ".tmp")
|
||||||
end
|
end
|
||||||
@@ -153,7 +154,7 @@ 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"}]} = Jason.decode!(File.read!(tags_path))
|
assert [%{"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",
|
test "update_template cascades slug changes to posts and tags and renames the published file",
|
||||||
@@ -208,12 +209,12 @@ defmodule BDS.TemplatesTest do
|
|||||||
assert template_contents =~ "\n---\n<article>{{ content }}</article>\n"
|
assert template_contents =~ "\n---\n<article>{{ content }}</article>\n"
|
||||||
|
|
||||||
post_contents = File.read!(Path.join(temp_dir, reloaded_post.file_path))
|
post_contents = File.read!(Path.join(temp_dir, reloaded_post.file_path))
|
||||||
assert post_contents =~ "template_slug: feature-view\n"
|
assert post_contents =~ "templateSlug: feature-view\n"
|
||||||
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 [%{"name" => "Feature", "postTemplateSlug" => "feature-view"}] =
|
||||||
Jason.decode!(File.read!(tags_path))
|
Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -231,13 +232,14 @@ defmodule BDS.TemplatesTest do
|
|||||||
[
|
[
|
||||||
"---",
|
"---",
|
||||||
"id: template-from-file",
|
"id: template-from-file",
|
||||||
|
"projectId: #{project.id}",
|
||||||
"slug: recovered-view",
|
"slug: recovered-view",
|
||||||
"title: Recovered View",
|
"title: Recovered View",
|
||||||
"kind: list",
|
"kind: list",
|
||||||
"enabled: true",
|
"enabled: true",
|
||||||
"version: 3",
|
"version: 3",
|
||||||
"created_at: 1970-01-01T00:00:00.101Z",
|
"createdAt: 1970-01-01T00:00:00.101Z",
|
||||||
"updated_at: 1970-01-01T00:00:00.202Z",
|
"updatedAt: 1970-01-01T00:00:00.202Z",
|
||||||
"---",
|
"---",
|
||||||
"<section>Recovered</section>",
|
"<section>Recovered</section>",
|
||||||
""
|
""
|
||||||
|
|||||||
Reference in New Issue
Block a user