From 624b698bb34e0b2a68c1261ab82b3ea6a5c6ab44 Mon Sep 17 00:00:00 2001
From: Chili Palmer
Date: Fri, 24 Apr 2026 06:30:41 +0200
Subject: [PATCH] feat: more complete metadata diff, scp publishing and
rendering context
---
config/config.exs | 3 +-
config/dev.exs | 3 +-
lib/bds/generation.ex | 229 ++++++++++++++---
lib/bds/maintenance.ex | 124 ++++++++--
lib/bds/media.ex | 43 ++--
lib/bds/media/media.ex | 13 +-
lib/bds/media/translation.ex | 29 ++-
lib/bds/menu.ex | 8 +-
lib/bds/metadata.ex | 56 ++++-
lib/bds/posts.ex | 107 ++++++--
lib/bds/posts/post.ex | 10 +-
lib/bds/posts/translation.ex | 36 ++-
lib/bds/preview.ex | 26 +-
lib/bds/projects.ex | 5 +-
lib/bds/projects/project.ex | 4 +-
lib/bds/publishing.ex | 154 ++++++++++--
lib/bds/rendering.ex | 314 +++++++++++++++++++-----
lib/bds/rendering/filters.ex | 85 +++++--
lib/bds/scripting.ex | 7 +-
lib/bds/scripting/job_store.ex | 9 +-
lib/bds/scripting/lua.ex | 6 +-
lib/bds/scripts.ex | 39 ++-
lib/bds/scripts/script.ex | 32 ++-
lib/bds/search.ex | 85 +++++--
lib/bds/tags.ex | 35 ++-
lib/bds/tags/tag.ex | 4 +-
lib/bds/tasks.ex | 54 +++-
lib/bds/templates.ex | 92 +++++--
lib/bds/templates/template.ex | 30 ++-
lib/bds/types/string_list.ex | 3 +-
test/bds/generation_test.exs | 96 ++++++--
test/bds/maintenance_test.exs | 153 +++++++++---
test/bds/media_test.exs | 119 ++++++---
test/bds/menu_test.exs | 8 +-
test/bds/metadata_test.exs | 8 +-
test/bds/post_translations_test.exs | 11 +-
test/bds/posts_test.exs | 48 ++--
test/bds/preview_test.exs | 97 ++++++--
test/bds/projects_test.exs | 39 ++-
test/bds/publishing_test.exs | 150 ++++++++++-
test/bds/rendering_test.exs | 152 ++++++++++++
test/bds/repo/schema_migration_test.exs | 24 +-
test/bds/scripting/job_test.exs | 8 +-
test/bds/scripts_test.exs | 19 +-
test/bds/search_test.exs | 42 +++-
test/bds/tags_test.exs | 38 ++-
test/bds/tasks_test.exs | 10 +-
test/bds/templates_test.exs | 31 ++-
48 files changed, 2193 insertions(+), 505 deletions(-)
create mode 100644 test/bds/rendering_test.exs
diff --git a/config/config.exs b/config/config.exs
index c6551be..8efaf7b 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -9,8 +9,7 @@ config :bds, BDS.Repo,
stacktrace: true,
show_sensitive_data_on_connection_error: true
-config :bds, BDS.Application,
- desktop_adapter: :pending_selection
+config :bds, BDS.Application, desktop_adapter: :pending_selection
config :bds, :scripting,
runtime: BDS.Scripting.Lua,
diff --git a/config/dev.exs b/config/dev.exs
index b2203c8..9933bc0 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -1,4 +1,3 @@
import Config
-config :bds, BDS.Repo,
- pool_size: 5
+config :bds, BDS.Repo, pool_size: 5
diff --git a/lib/bds/generation.ex b/lib/bds/generation.ex
index b286f40..5e41a1d 100644
--- a/lib/bds/generation.ex
+++ b/lib/bds/generation.ex
@@ -14,7 +14,8 @@ defmodule BDS.Generation do
@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)
{:ok, metadata} = Metadata.get_project_metadata(project_id)
{:ok, generated_files} = list_generated_files(project_id)
@@ -27,14 +28,15 @@ defmodule BDS.Generation do
language: metadata.main_language,
blog_languages: normalize_blog_languages(metadata.main_language, metadata.blog_languages),
max_posts_per_page: metadata.max_posts_per_page,
- categories: metadata.categories,
+ categories: metadata.categories,
pico_theme: metadata.pico_theme,
sections: normalize_sections(sections),
generated_files: generated_files
}}
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
outputs = build_outputs(plan)
@@ -106,7 +108,8 @@ defmodule BDS.Generation do
)}
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)
case File.rm(output_path(project, relative_path)) do
@@ -117,7 +120,9 @@ defmodule BDS.Generation do
Repo.delete_all(
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
@@ -146,8 +151,10 @@ defmodule BDS.Generation do
build_archive_outputs(plan, published_posts)
urls =
- core_outputs ++ single_outputs ++ archive_outputs
- |> Enum.map(fn {relative_path, _content} -> url_for_output(plan.base_url, relative_path) end)
+ (core_outputs ++ single_outputs ++ archive_outputs)
+ |> Enum.map(fn {relative_path, _content} ->
+ url_for_output(plan.base_url, relative_path)
+ end)
sitemap =
if :core in plan.sections do
@@ -199,9 +206,42 @@ defmodule BDS.Generation do
Enum.with_index(paginated_posts, 1)
|> Enum.flat_map(fn {page_posts, page_number} ->
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),
- render_archive_page(plan, category, page_posts, language, "category")
+ archive_path(
+ route_language(plan.language, language),
+ ["category", category_slug],
+ page_number
+ ),
+ render_archive_page(plan, category, page_posts, language, "category", pagination)
}
end)
end)
@@ -216,11 +256,12 @@ defmodule BDS.Generation do
Enum.flat_map(tag_posts, fn {tag, posts} ->
tag_slug = Slug.slugify(tag)
+ pagination = pagination_for_posts(posts)
Enum.map(languages, fn language ->
{
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)
@@ -232,20 +273,24 @@ defmodule BDS.Generation do
year_outputs =
Enum.flat_map(years, fn {year, posts} ->
+ pagination = pagination_for_posts(posts)
+
Enum.map(languages, fn language ->
{
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)
month_outputs =
Enum.flat_map(months, fn {{year, month}, posts} ->
+ pagination = pagination_for_posts(posts)
+
Enum.map(languages, fn language ->
{
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)
@@ -259,7 +304,16 @@ defmodule BDS.Generation do
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)},
{"feed.xml", render_feed(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)
[
- {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, "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)}
+ {Path.join(localized_language, "index.html"),
+ render_list_output(
+ plan,
+ 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
@@ -284,9 +350,21 @@ defmodule BDS.Generation do
body = load_body(project_id, post.file_path, post.content)
{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_page(post.title, body, post.slug, post.language)
- end)}
+ 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)
+ end
+ )}
end)
translation_outputs =
@@ -300,9 +378,21 @@ defmodule BDS.Generation do
[
{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_page(translation.title, body, post.slug, translation.language)
- end)}
+ 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)
+ end
+ )}
]
end
end)
@@ -434,7 +524,7 @@ defmodule BDS.Generation do
|> IO.iodata_to_binary()
end
- defp render_archive_page(plan, title, posts, language, kind) do
+ defp render_archive_page(plan, title, posts, language, kind, pagination) do
fallback = fn ->
items =
posts
@@ -460,14 +550,23 @@ defmodule BDS.Generation do
language,
title,
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),
%{kind: kind, name: title},
+ pagination,
fallback
)
end
- defp render_date_archive_page(plan, label, posts, language) do
+ defp render_date_archive_page(plan, label, posts, language, pagination) do
fallback = fn ->
items =
posts
@@ -491,21 +590,37 @@ defmodule BDS.Generation do
language,
label,
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),
%{kind: "date", name: label},
+ pagination,
fallback
)
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
case file_path do
- nil -> ""
- "" -> ""
+ nil ->
+ ""
+
+ "" ->
+ ""
+
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
{:ok, contents} -> parse_frontmatter_body(contents)
{:error, _reason} -> ""
@@ -529,7 +644,9 @@ defmodule BDS.Generation do
defp month_key(created_at) do
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
defp build_list_posts(base_url, posts, language_prefix) do
@@ -552,25 +669,46 @@ defmodule BDS.Generation do
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
case Rendering.render_list_page(project_id, %{
language: language,
language_prefix: language_prefix(language, main_language),
page_title: page_title,
posts: posts,
- archive_context: archive_context
+ archive_context: archive_context,
+ pagination: pagination
}) do
{:ok, rendered} -> rendered
{:error, _reason} -> fallback.()
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)
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
{:error, _reason} -> render_not_found_page(language)
end
@@ -582,6 +720,25 @@ defmodule BDS.Generation do
defp language_prefix(nil, _main_language), do: ""
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(base_url, relative_path) do
diff --git a/lib/bds/maintenance.ex b/lib/bds/maintenance.ex
index 009d0f5..1d881cd 100644
--- a/lib/bds/maintenance.ex
+++ b/lib/bds/maintenance.ex
@@ -53,7 +53,8 @@ defmodule BDS.Maintenance do
defp post_diff_reports(project_id, project) do
Repo.all(
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 ->
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("status", post.status, Map.get(fields, "status")),
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("categories", post.categories, Map.get(fields, "categories", []))
]
@@ -86,7 +90,9 @@ defmodule BDS.Maintenance do
defp media_diff_reports(project_id, project) do
Repo.all(
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 ->
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("author", media.author, Map.get(fields, "author")),
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", []))
]
|> Enum.reject(&is_nil/1)
@@ -117,7 +125,9 @@ defmodule BDS.Maintenance do
defp post_translation_diff_reports(project_id, project) do
Repo.all(
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 ->
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("language", translation.language, Map.get(fields, "language")),
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)
if differences == [] do
[]
else
- [%{entity_type: "post_translation", entity_id: translation.id, differences: differences}]
+ [
+ %{
+ entity_type: "post_translation",
+ entity_id: translation.id,
+ differences: differences
+ }
+ ]
end
{:error, _reason} ->
@@ -157,14 +184,24 @@ defmodule BDS.Maintenance do
diff_field("alt", translation.alt, Map.get(fields, "alt")),
diff_field("caption", translation.caption, Map.get(fields, "caption")),
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)
if differences == [] do
[]
else
- [%{entity_type: "media_translation", entity_id: translation.id, differences: differences}]
+ [
+ %{
+ entity_type: "media_translation",
+ entity_id: translation.id,
+ differences: differences
+ }
+ ]
end
_ ->
@@ -176,7 +213,9 @@ defmodule BDS.Maintenance do
defp script_diff_reports(project_id, project) do
Repo.all(
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 ->
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("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)
@@ -204,7 +245,9 @@ defmodule BDS.Maintenance do
defp template_diff_reports(project_id, project) do
Repo.all(
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 ->
case read_frontmatter_document(project, template.file_path) do
@@ -212,7 +255,9 @@ defmodule BDS.Maintenance do
differences =
[
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)
@@ -229,12 +274,44 @@ defmodule BDS.Maintenance do
end
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))
- 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))
+ post_paths =
+ MapSet.new(
+ 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))
- 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 =
project
@@ -276,7 +353,9 @@ defmodule BDS.Maintenance do
|> Enum.map(&Path.relative_to(&1, Projects.project_data_dir(project)))
|> 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.map(&%{file_path: &1})
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_integer(value), do: Integer.to_string(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 read_frontmatter_document(project, relative_path) do
@@ -345,7 +427,11 @@ defmodule BDS.Maintenance do
end
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
file_path -> "#{file_path}.#{translation.language}.meta"
end
diff --git a/lib/bds/media.ex b/lib/bds/media.ex
index afc761e..8798bb5 100644
--- a/lib/bds/media.ex
+++ b/lib/bds/media.ex
@@ -68,16 +68,17 @@ defmodule BDS.Media do
{:error, :not_found}
media ->
- updates = %{}
- |> maybe_put(:title, attr(attrs, :title))
- |> maybe_put(:alt, attr(attrs, :alt))
- |> maybe_put(:caption, attr(attrs, :caption))
- |> maybe_put(:author, attr(attrs, :author))
- |> maybe_put(:language, attr(attrs, :language))
- |> maybe_put(:tags, attr(attrs, :tags))
- |> maybe_put(:width, attr(attrs, :width))
- |> maybe_put(:height, attr(attrs, :height))
- |> Map.put(:updated_at, System.system_time(:second))
+ updates =
+ %{}
+ |> maybe_put(:title, attr(attrs, :title))
+ |> maybe_put(:alt, attr(attrs, :alt))
+ |> maybe_put(:caption, attr(attrs, :caption))
+ |> maybe_put(:author, attr(attrs, :author))
+ |> maybe_put(:language, attr(attrs, :language))
+ |> maybe_put(:tags, attr(attrs, :tags))
+ |> maybe_put(:width, attr(attrs, :width))
+ |> maybe_put(:height, attr(attrs, :height))
+ |> Map.put(:updated_at, System.system_time(:second))
project = Projects.get_project!(media.project_id)
@@ -104,14 +105,21 @@ defmodule BDS.Media do
{:error, :not_found}
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.sidecar_path)
delete_thumbnail_files(media.project_id, media)
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)
end)
@@ -243,7 +251,9 @@ defmodule BDS.Media do
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.changeset(attrs)
@@ -278,7 +288,12 @@ defmodule BDS.Media do
end
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))
atomic_write(
diff --git a/lib/bds/media/media.ex b/lib/bds/media/media.ex
index 3ccc813..80ba16d 100644
--- a/lib/bds/media/media.ex
+++ b/lib/bds/media/media.ex
@@ -58,7 +58,18 @@ defmodule BDS.Media.Media do
],
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)
end
end
diff --git a/lib/bds/media/translation.ex b/lib/bds/media/translation.ex
index 96c5b01..8b20e49 100644
--- a/lib/bds/media/translation.ex
+++ b/lib/bds/media/translation.ex
@@ -8,7 +8,11 @@ defmodule BDS.Media.Translation do
@foreign_key_type :string
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 :language, :string
field :title, :string
@@ -20,10 +24,29 @@ defmodule BDS.Media.Translation do
def changeset(translation, attrs) do
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]
)
- |> 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)
|> unique_constraint(:language, name: :media_translations_translation_language_idx)
end
diff --git a/lib/bds/menu.ex b/lib/bds/menu.ex
index f94e84a..12bf4ff 100644
--- a/lib/bds/menu.ex
+++ b/lib/bds/menu.ex
@@ -6,7 +6,11 @@ defmodule BDS.Menu do
alias BDS.Projects
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]
@@ -187,7 +191,7 @@ defmodule BDS.Menu do
|> String.replace("&", "&")
|> String.replace("<", "<")
|> String.replace(">", ">")
- |> String.replace(~s(") , """)
+ |> String.replace(~s("), """)
end
defp attr(attrs, key) do
diff --git a/lib/bds/metadata.ex b/lib/bds/metadata.ex
index b89e839..5348564 100644
--- a/lib/bds/metadata.ex
+++ b/lib/bds/metadata.ex
@@ -21,13 +21,28 @@ defmodule BDS.Metadata do
project_metadata =
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))
Repo.transaction(fn ->
updated_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!()
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)
now = System.system_time(:second)
- project_metadata_from_files = read_json(project, "project.json") || stringify_project_metadata(default_project_metadata(project))
- categories_from_files = read_json(project, "categories.json") || %{"categories" => @default_categories}
+ project_metadata_from_files =
+ 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" => %{}}
publishing_from_files = read_json(project, "publishing.json") || %{"ssh_mode" => "scp"}
@@ -125,8 +145,14 @@ defmodule BDS.Metadata do
end
defp load_state(project) do
- project_metadata = load_setting(project.id, "project") || stringify_project_metadata(default_project_metadata(project))
- categories = (load_setting(project.id, "categories") || %{"categories" => @default_categories})["categories"]
+ project_metadata =
+ load_setting(project.id, "project") ||
+ stringify_project_metadata(default_project_metadata(project))
+
+ categories =
+ (load_setting(project.id, "categories") || %{"categories" => @default_categories})[
+ "categories"
+ ]
category_settings =
(load_setting(project.id, "category_meta") || %{"categories" => %{}})["categories"]
@@ -139,10 +165,12 @@ defmodule BDS.Metadata do
public_url: Map.get(project_metadata, "public_url"),
main_language: Map.get(project_metadata, "main_language"),
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"),
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", []),
categories: categories,
category_settings: category_settings,
@@ -182,10 +210,13 @@ defmodule BDS.Metadata 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)),
- "post_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"))
+ "post_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
@@ -220,7 +251,8 @@ defmodule BDS.Metadata do
write_publishing_json(project, state.publishing_preferences)
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
write_json(project, "categories.json", %{"categories" => Enum.sort(categories)})
diff --git a/lib/bds/posts.ex b/lib/bds/posts.ex
index 22ea01d..1eb3d93 100644
--- a/lib/bds/posts.ex
+++ b/lib/bds/posts.ex
@@ -37,7 +37,7 @@ defmodule BDS.Posts do
categories: attr(attrs, :categories) || [],
template_slug: attr(attrs, :template_slug),
language: attr(attrs, :language),
- do_not_translate: false,
+ do_not_translate: attr(attrs, :do_not_translate) || false,
published_title: nil,
published_content: nil,
published_tags: nil,
@@ -63,6 +63,7 @@ defmodule BDS.Posts do
post ->
with :ok <- validate_slug_change(post, attrs) do
now = System.system_time(:second)
+
updates =
attrs
|> normalize_updates(post)
@@ -100,7 +101,12 @@ defmodule BDS.Posts do
body = publishable_post_body(post, full_path, project)
: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.changeset(%{
@@ -197,16 +203,21 @@ defmodule BDS.Posts do
{:error,
post
|> 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 ->
now = System.system_time(:second)
normalized_language = normalize_language(language)
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.changeset(updates)
@@ -253,7 +264,9 @@ defmodule BDS.Posts do
where: post.project_id == ^project_id,
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 =
metadata.blog_languages
@@ -268,7 +281,9 @@ defmodule BDS.Posts do
available = Map.get(translation_languages, post.id, [])
cond do
- post.do_not_translate -> []
+ post.do_not_translate ->
+ []
+
true ->
required_languages
|> 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)
body = published_post_body(post, 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
:ok
@@ -328,7 +351,8 @@ defmodule BDS.Posts do
|> maybe_put(:published_excerpt, attr(attrs, :published_excerpt))
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
nil ->
:ok
@@ -357,12 +381,25 @@ defmodule BDS.Posts do
defp maybe_reopen_published_post(updates, _post), do: updates
defp published_content_change?(updates, post) do
- Enum.any?([:title, :excerpt, :content, :author, :language, :template_slug, :tags, :categories, :do_not_translate], fn field ->
- case Map.fetch(updates, field) do
- {:ok, value} -> value != Map.get(post, field)
- :error -> false
+ Enum.any?(
+ [
+ :title,
+ :excerpt,
+ :content,
+ :author,
+ :language,
+ :template_slug,
+ :tags,
+ :categories,
+ :do_not_translate
+ ],
+ fn field ->
+ case Map.fetch(updates, field) do
+ {:ok, value} -> value != Map.get(post, field)
+ :error -> false
+ end
end
- end)
+ )
end
defp unique_slug(project_id, base_slug) do
@@ -386,7 +423,9 @@ defmodule BDS.Posts do
end
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
defp maybe_put(map, _key, nil), do: map
@@ -409,7 +448,8 @@ defmodule BDS.Posts do
Path.join(["posts", year, month, "#{slug}.md"])
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
source_path =
@@ -444,7 +484,8 @@ defmodule BDS.Posts do
)
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
case File.read(full_path) do
@@ -512,7 +553,8 @@ defmodule BDS.Posts do
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
project = Projects.get_project!(post.project_id)
@@ -532,7 +574,8 @@ defmodule BDS.Posts do
|> maybe_put(:excerpt, attr(attrs, :excerpt))
|> 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(),
@@ -580,7 +623,15 @@ defmodule BDS.Posts do
body = publishable_translation_body(translation, 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.changeset(%{
@@ -619,7 +670,8 @@ defmodule BDS.Posts do
)
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
case File.read(full_path) do
@@ -634,7 +686,8 @@ defmodule BDS.Posts do
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
project = Projects.get_project!(translation.project_id)
@@ -649,7 +702,15 @@ defmodule BDS.Posts do
defp orphan_translation_files(project_id) do
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
|> Projects.project_data_dir()
diff --git a/lib/bds/posts/post.ex b/lib/bds/posts/post.ex
index 9b0f2f7..4c3c44f 100644
--- a/lib/bds/posts/post.ex
+++ b/lib/bds/posts/post.ex
@@ -67,7 +67,15 @@ defmodule BDS.Posts.Post do
],
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)
|> unique_constraint(:slug, name: :posts_project_slug_idx)
end
diff --git a/lib/bds/posts/translation.ex b/lib/bds/posts/translation.ex
index d9e23dd..443b1fe 100644
--- a/lib/bds/posts/translation.ex
+++ b/lib/bds/posts/translation.ex
@@ -9,7 +9,10 @@ defmodule BDS.Posts.Translation do
@statuses [:draft, :published]
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 :language, :string
@@ -26,22 +29,35 @@ defmodule BDS.Posts.Translation do
def changeset(translation, attrs) do
translation
- |> cast(attrs, [
+ |> cast(
+ attrs,
+ [
+ :id,
+ :project_id,
+ :translation_for,
+ :language,
+ :title,
+ :excerpt,
+ :content,
+ :status,
+ :created_at,
+ :updated_at,
+ :published_at,
+ :file_path,
+ :checksum
+ ],
+ empty_values: [nil]
+ )
+ |> validate_required([
:id,
:project_id,
:translation_for,
:language,
:title,
- :excerpt,
- :content,
:status,
:created_at,
- :updated_at,
- :published_at,
- :file_path,
- :checksum
- ], empty_values: [nil])
- |> validate_required([:id, :project_id, :translation_for, :language, :title, :status, :created_at, :updated_at])
+ :updated_at
+ ])
|> foreign_key_constraint(:translation_for)
|> unique_constraint(:language, name: :post_translations_translation_language_idx)
end
diff --git a/lib/bds/preview.ex b/lib/bds/preview.ex
index 6bdc462..80a7adb 100644
--- a/lib/bds/preview.ex
+++ b/lib/bds/preview.ex
@@ -16,7 +16,11 @@ defmodule BDS.Preview do
def start_preview(project_id) when is_binary(project_id) do
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
def stop_preview(project_id) when is_binary(project_id) do
@@ -58,7 +62,15 @@ defmodule BDS.Preview do
state = stop_current_server(state)
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)
server = %{
@@ -145,7 +157,9 @@ defmodule BDS.Preview do
end
case full_path do
- {:error, :not_found} -> {:error, :not_found}
+ {:error, :not_found} ->
+ {:error, :not_found}
+
resolved_path ->
case read_response(resolved_path) do
{:error, :not_found} -> render_not_found_response(server.project_id)
@@ -258,7 +272,11 @@ defmodule BDS.Preview do
path = uri.path || "/"
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)
{:error, :not_found} -> http_error_response(404)
{:error, :not_running} -> http_error_response(503)
diff --git a/lib/bds/projects.ex b/lib/bds/projects.ex
index 6e56dc8..878ebcb 100644
--- a/lib/bds/projects.ex
+++ b/lib/bds/projects.ex
@@ -101,7 +101,10 @@ defmodule BDS.Projects do
|> Multi.update_all(:clear_previous, from(p in Project, where: p.is_active == true),
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()
|> case do
{:ok, %{activate: active_project}} -> {:ok, active_project}
diff --git a/lib/bds/projects/project.ex b/lib/bds/projects/project.ex
index e547162..f16ddad 100644
--- a/lib/bds/projects/project.ex
+++ b/lib/bds/projects/project.ex
@@ -21,7 +21,9 @@ defmodule BDS.Projects.Project do
def changeset(project, attrs) do
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]
)
|> validate_required([:id, :name, :slug, :created_at, :updated_at, :is_active])
diff --git a/lib/bds/publishing.ex b/lib/bds/publishing.ex
index d1b2350..8c81e49 100644
--- a/lib/bds/publishing.ex
+++ b/lib/bds/publishing.ex
@@ -10,7 +10,8 @@ defmodule BDS.Publishing do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
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)
normalized_credentials = normalize_credentials(credentials)
targets = build_upload_targets(Projects.project_data_dir(project), normalized_credentials)
@@ -23,7 +24,7 @@ defmodule BDS.Publishing do
@impl true
def init(_state) do
- {:ok, %{jobs: %{}}}
+ {:ok, %{jobs: %{}, scp_uploads: %{}}}
end
@impl true
@@ -41,15 +42,32 @@ defmodule BDS.Publishing do
{:reply, :ok, next_state}
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
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 = %{
id: job_id,
project_id: project_id,
status: :pending,
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,
targets: Enum.map(targets, & &1.kind),
error: nil,
@@ -58,12 +76,16 @@ defmodule BDS.Publishing do
}
{:ok, task} =
- Tasks.submit_task("publish #{project_id}", fn report ->
- run_upload(job_id, credentials, targets, uploader, report)
- end, %{
- group_id: project_id,
- group_name: "Publishing"
- })
+ Tasks.submit_task(
+ "publish #{project_id}",
+ fn report ->
+ run_upload(job_id, credentials, targets, uploader, report)
+ end,
+ %{
+ group_id: project_id,
+ group_name: "Publishing"
+ }
+ )
next_job = %{job | task_id: task.id}
{:reply, {:ok, next_job}, put_in(state, [:jobs, job_id], next_job)}
@@ -104,9 +126,10 @@ defmodule BDS.Publishing do
nil ->
runner = Keyword.get(opts, :command_runner, &System.cmd/3)
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 ->
- run_command_upload(target, files, credentials, runner, ssh_auth_sock)
+ run_command_upload(project_id, target, files, credentials, runner, ssh_auth_sock)
end
uploader ->
@@ -114,22 +137,60 @@ defmodule BDS.Publishing do
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 =
["--update", "--compress", "--verbose"] ++
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)
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 ->
local_path = Path.join(target.local_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
- :ok -> {:cont, :ok}
+ 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)
+
+ case run_command(runner, "scp", ["-q", local_path, remote_path], ssh_auth_sock) do
+ :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}}
end
end)
@@ -147,10 +208,49 @@ defmodule BDS.Publishing do
end
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 normalize_command_error(command, _output, status), do: "#{command} exited with status #{status}"
+ 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 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(_target), do: []
@@ -172,8 +272,16 @@ defmodule BDS.Publishing do
[
%{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
@@ -184,7 +292,9 @@ defmodule BDS.Publishing do
|> Path.wildcard(match_dot: true)
|> Enum.filter(&File.regular?/1)
|> 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()
else
[]
diff --git a/lib/bds/rendering.ex b/lib/bds/rendering.ex
index 3e45541..c5bf218 100644
--- a/lib/bds/rendering.ex
+++ b/lib/bds/rendering.ex
@@ -17,23 +17,28 @@ defmodule BDS.Rendering do
alias BDS.Posts.Translation
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),
- {: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}
end
end
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),
- {: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}
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),
- {: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}
end
end
@@ -49,7 +54,8 @@ defmodule BDS.Rendering do
Repo.one(
from template in Template,
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,
limit: 1
) || select_template(project_id, kind, nil)
@@ -59,14 +65,16 @@ defmodule BDS.Rendering do
Repo.one(
from template in Template,
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,
order_by: [asc: template.created_at, asc: template.slug],
limit: 1
)
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
project = Projects.get_project!(template.project_id)
@@ -105,17 +113,32 @@ defmodule BDS.Rendering do
defp post_assigns(project_id, assigns) do
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
post_record = load_post_record(assigns)
post_categories = Map.get(post_record || %{}, :categories, []) || []
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_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", Map.get(assigns, :title, Map.get(assigns, "title")))),
- pico_stylesheet_href: nil,
+ 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", Map.get(assigns, :title, Map.get(assigns, "title")))
+ ),
+ pico_stylesheet_href: default_pico_stylesheet_href(),
html_theme_attribute: html_theme_attribute(metadata.pico_theme),
blog_languages: blog_languages(metadata, language),
alternate_links: [],
@@ -126,34 +149,40 @@ defmodule BDS.Rendering do
post_tags: post_tags,
tag_color_by_name: tag_color_by_name(project_id),
backlinks: [],
- canonical_post_path_by_slug: canonical_post_path_by_slug(project_id, main_language),
- canonical_media_path_by_source_path: canonical_media_path_by_source_path(project_id),
- post_data_json_by_id: post_data_json(assigns),
- post: %{
- 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
- }
+ canonical_post_path_by_slug: canonical_post_paths,
+ canonical_media_path_by_source_path: canonical_media_paths,
+ post_data_json_by_id: post_data_json(assigns, post_record),
+ post: build_post_context(assigns, post_record)
}
end
defp list_assigns(project_id, assigns) do
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
posts = normalize_list_posts(Map.get(assigns, :posts, Map.get(assigns, "posts", [])))
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_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")),
posts: posts,
- pico_stylesheet_href: nil,
+ pico_stylesheet_href: default_pico_stylesheet_href(),
html_theme_attribute: html_theme_attribute(metadata.pico_theme),
blog_languages: blog_languages(metadata, language),
alternate_links: [],
@@ -165,15 +194,20 @@ defmodule BDS.Rendering do
min_date: nil,
max_date: nil,
is_list_page: true,
- is_first_page: true,
- is_last_page: true,
- has_prev_page: false,
- has_next_page: false,
- prev_page_href: "",
- next_page_href: "",
- canonical_post_path_by_slug: canonical_post_path_by_slug(project_id, main_language),
- canonical_media_path_by_source_path: canonical_media_path_by_source_path(project_id),
- post_data_json_by_id: Enum.into(posts, %{}, fn post -> {post.id, Jason.encode!(Map.take(post, [:id, :slug, :title, :content]))} end),
+ is_first_page: pagination.current_page <= 1,
+ is_last_page: pagination.current_page >= pagination.total_pages,
+ has_prev_page: pagination.has_prev_page,
+ has_next_page: pagination.has_next_page,
+ prev_page_href: pagination.prev_page_href,
+ next_page_href: pagination.next_page_href,
+ current_page: pagination.current_page,
+ total_pages: pagination.total_pages,
+ 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: [
%{
date_label: "",
@@ -187,20 +221,46 @@ defmodule BDS.Rendering do
defp not_found_assigns(project_id, assigns) do
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
%{
- 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_prefix: Map.get(assigns, :language_prefix, Map.get(assigns, "language_prefix", language_prefix(language, main_language))),
- pico_stylesheet_href: nil,
+ language_prefix:
+ 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),
blog_languages: blog_languages(metadata, language),
menu_items: menu_items(project_id),
alternate_links: [],
- not_found_message: Map.get(assigns, :not_found_message, Map.get(assigns, "not_found_message")),
- not_found_back_label: Map.get(assigns, :not_found_back_label, Map.get(assigns, "not_found_back_label"))
+ not_found_message:
+ 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
@@ -232,8 +292,13 @@ defmodule BDS.Rendering do
end
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(_item), do: "#"
@@ -243,11 +308,13 @@ defmodule BDS.Rendering do
|> Enum.uniq()
|> Enum.map(fn language ->
normalized = I18n.normalize_language(language)
+ href_prefix = language_prefix(normalized, metadata.main_language || current_language)
%{
code: 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)
}
end)
@@ -265,26 +332,39 @@ defmodule BDS.Rendering do
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"))
if is_binary(id) do
%{
- id =>
- 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"))
- })
+ id => post_data_json_value(build_post_context(assigns, post_record))
}
else
%{}
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
- 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 =
Repo.all(
@@ -311,6 +391,7 @@ defmodule BDS.Rendering do
Repo.all(from media in MediaAsset, where: media.project_id == ^project_id)
|> Enum.reduce(%{}, fn media, acc ->
datetime = DateTime.from_unix!(media.created_at)
+
source_key =
Path.join([
"media",
@@ -324,7 +405,8 @@ defmodule BDS.Rendering do
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)])
end
@@ -338,7 +420,7 @@ defmodule BDS.Rendering do
post.slug,
"index.html"
])
- |> then(&"/" <> String.trim_trailing(&1, "index.html"))
+ |> then(&("/" <> String.trim_trailing(&1, "index.html")))
end
defp post_path(post, language, main_language) do
@@ -348,16 +430,119 @@ defmodule BDS.Rendering do
defp normalize_list_posts(posts) do
Enum.map(posts, fn post ->
+ post_record = load_post_record(post)
+
%{
id: Map.get(post, :id, Map.get(post, "id")),
slug: Map.get(post, :slug, Map.get(post, "slug")),
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", "")))),
- show_title: true
+ content:
+ 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
+ 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(%{} = archive_context), do: archive_context
@@ -365,10 +550,19 @@ defmodule BDS.Rendering do
defp html_theme_attribute(""), do: nil
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_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_year_from_posts([post | _rest]), do: calendar_initial_year(post)
diff --git a/lib/bds/rendering/filters.ex b/lib/bds/rendering/filters.ex
index 3a4efb0..31533b8 100644
--- a/lib/bds/rendering/filters.ex
+++ b/lib/bds/rendering/filters.ex
@@ -15,7 +15,16 @@ defmodule BDS.Rendering.Filters do
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
|> to_string()
|> replace_built_in_macros(language, context)
@@ -24,21 +33,37 @@ defmodule BDS.Rendering.Filters do
end
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)
case String.downcase(macro_name) do
"youtube" ->
- render_macro_template("macros/youtube", %{
- "id" => Map.get(params, "id", ""),
- "title" => default_macro_title(Map.get(params, "title"), language, "render.video.youtubeTitle")
- }, context)
+ render_macro_template(
+ "macros/youtube",
+ %{
+ "id" => Map.get(params, "id", ""),
+ "title" =>
+ default_macro_title(
+ Map.get(params, "title"),
+ language,
+ "render.video.youtubeTitle"
+ )
+ },
+ context
+ )
"vimeo" ->
- render_macro_template("macros/vimeo", %{
- "id" => Map.get(params, "id", ""),
- "title" => default_macro_title(Map.get(params, "title"), language, "render.video.vimeoTitle")
- }, context)
+ render_macro_template(
+ "macros/vimeo",
+ %{
+ "id" => Map.get(params, "id", ""),
+ "title" =>
+ default_macro_title(Map.get(params, "title"), language, "render.video.vimeoTitle")
+ },
+ context
+ )
_other ->
full_match
@@ -46,8 +71,12 @@ defmodule BDS.Rendering.Filters do
end)
end
- defp default_macro_title(nil, 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(nil, 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 parse_macro_params(nil), do: %{}
@@ -63,8 +92,12 @@ defmodule BDS.Rendering.Filters do
defp render_macro_template(template_path, assigns, context) do
case Map.get(assigns, "id") do
- "" -> ""
- nil -> ""
+ "" ->
+ ""
+
+ nil ->
+ ""
+
_id ->
template_source = Liquex.FileSystem.read_template_file(context.file_system, template_path)
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
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))
end
@@ -91,8 +127,12 @@ defmodule BDS.Rendering.Filters do
defp normalize_post_href(raw_href, canonical_post_paths, canonical_media_paths) do
cond do
- raw_href == "" -> raw_href
- external_or_special_url?(raw_href) -> raw_href
+ raw_href == "" ->
+ raw_href
+
+ external_or_special_url?(raw_href) ->
+ raw_href
+
true ->
{path_part, suffix} = split_path_suffix(raw_href)
@@ -103,7 +143,8 @@ defmodule BDS.Rendering.Filters do
canonical -> canonical <> suffix
end
- _other -> raw_href
+ _other ->
+ raw_href
end
end
end
@@ -136,8 +177,12 @@ defmodule BDS.Rendering.Filters do
defp normalize_media_src(raw_src, canonical_media_paths) do
cond do
- raw_src == "" -> raw_src
- external_or_special_url?(raw_src) -> raw_src
+ raw_src == "" ->
+ raw_src
+
+ external_or_special_url?(raw_src) ->
+ raw_src
+
true ->
{path_part, suffix} = split_path_suffix(raw_src)
diff --git a/lib/bds/scripting.ex b/lib/bds/scripting.ex
index ead8daa..5adf45b 100644
--- a/lib/bds/scripting.ex
+++ b/lib/bds/scripting.ex
@@ -64,7 +64,9 @@ defmodule BDS.Scripting do
opts: batch_job_defaults(opts)}
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} ->
:ok =
BDS.Scripting.JobStore.update_job(job_id, %{
@@ -91,7 +93,8 @@ defmodule BDS.Scripting do
_job -> {:error, :not_running}
end
- pid -> BDS.Scripting.JobRunner.cancel(pid)
+ pid ->
+ BDS.Scripting.JobRunner.cancel(pid)
end
end
diff --git a/lib/bds/scripting/job_store.ex b/lib/bds/scripting/job_store.ex
index 4f9f021..c83ea39 100644
--- a/lib/bds/scripting/job_store.ex
+++ b/lib/bds/scripting/job_store.ex
@@ -50,10 +50,11 @@ defmodule BDS.Scripting.JobStore do
end
def handle_call({:update_job, job_id, attrs}, _from, state) do
- next_state = update_in(state, [:jobs, job_id], fn
- nil -> nil
- job -> Map.merge(job, attrs)
- end)
+ next_state =
+ update_in(state, [:jobs, job_id], fn
+ nil -> nil
+ job -> Map.merge(job, attrs)
+ end)
{:reply, :ok, next_state}
end
diff --git a/lib/bds/scripting/lua.ex b/lib/bds/scripting/lua.ex
index 71bc06d..5509276 100644
--- a/lib/bds/scripting/lua.ex
+++ b/lib/bds/scripting/lua.ex
@@ -66,7 +66,8 @@ defmodule BDS.Scripting.Lua do
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}
@@ -81,7 +82,8 @@ defmodule BDS.Scripting.Lua do
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
if Enum.all?(payload, &match?({key, _value} when is_binary(key) or is_atom(key), &1)) do
diff --git a/lib/bds/scripts.ex b/lib/bds/scripts.ex
index 5b2f84e..85e1a9b 100644
--- a/lib/bds/scripts.ex
+++ b/lib/bds/scripts.ex
@@ -50,11 +50,19 @@ defmodule BDS.Scripts do
:ok =
File.write(
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.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()
end
end
@@ -75,16 +83,20 @@ defmodule BDS.Scripts do
content_changed? = has_attr?(attrs, :content) and attr(attrs, :content) != script.content
now = System.system_time(:second)
- updates = %{}
- |> maybe_put(:title, attr(attrs, :title))
- |> maybe_put(:kind, attr(attrs, :kind))
- |> maybe_put(:entrypoint, attr(attrs, :entrypoint))
- |> maybe_put(:enabled, attr(attrs, :enabled))
- |> maybe_put(:content, attr(attrs, :content))
- |> Map.put(:slug, next_slug)
- |> Map.put(:version, script.version + 1)
- |> Map.put(:updated_at, now)
- |> maybe_put(:status, if(script.status == :published and content_changed?, do: :draft, else: nil))
+ updates =
+ %{}
+ |> maybe_put(:title, attr(attrs, :title))
+ |> maybe_put(:kind, attr(attrs, :kind))
+ |> maybe_put(:entrypoint, attr(attrs, :entrypoint))
+ |> maybe_put(:enabled, attr(attrs, :enabled))
+ |> maybe_put(:content, attr(attrs, :content))
+ |> Map.put(:slug, next_slug)
+ |> Map.put(:version, script.version + 1)
+ |> Map.put(:updated_at, now)
+ |> maybe_put(
+ :status,
+ if(script.status == :published and content_changed?, do: :draft, else: nil)
+ )
script
|> Script.changeset(updates)
@@ -141,7 +153,8 @@ defmodule BDS.Scripts do
end
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 =
case exclude_id do
diff --git a/lib/bds/scripts/script.ex b/lib/bds/scripts/script.ex
index 078442f..af76d53 100644
--- a/lib/bds/scripts/script.ex
+++ b/lib/bds/scripts/script.ex
@@ -25,10 +25,38 @@ defmodule BDS.Scripts.Script do
def changeset(script, attrs) do
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]
)
- |> 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)
|> unique_constraint(:slug, name: :scripts_project_slug_idx)
end
diff --git a/lib/bds/search.ex b/lib/bds/search.ex
index ebb3353..6e50573 100644
--- a/lib/bds/search.ex
+++ b/lib/bds/search.ex
@@ -96,8 +96,15 @@ defmodule BDS.Search do
end
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!("DELETE FROM media_fts WHERE media_id IN (SELECT id FROM media WHERE project_id = ?)", [project_id])
+ Repo.query!(
+ "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)
|> Enum.each(&sync_post/1)
@@ -241,7 +248,11 @@ defmodule BDS.Search do
matches_month?(post, filters.month) and
matches_from?(post, filters.from) 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
@@ -270,7 +281,13 @@ defmodule BDS.Search do
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{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
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})",
post_ids
).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
defp paginate(items, filters) do
@@ -300,16 +319,27 @@ defmodule BDS.Search do
post_language = normalize_language(post.language)
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()
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()
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()
tags = stem(Enum.join(post.tags || [], " "), post_language)
@@ -320,15 +350,25 @@ defmodule BDS.Search do
defp media_index_fields(media) do
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)
- title = [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()
+ title =
+ [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 =
- [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()
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{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
|> Projects.get_project!()
|> Projects.project_data_dir()
@@ -366,9 +407,11 @@ defmodule BDS.Search 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
|> Projects.get_project!()
|> Projects.project_data_dir()
@@ -403,7 +446,7 @@ defmodule BDS.Search do
|> Enum.map_join(" OR ", fn tokens ->
tokens
|> Enum.map_join(" AND ", "ed_term/1)
- |> then(&"(" <> &1 <> ")")
+ |> then(&("(" <> &1 <> ")"))
end)
end
@@ -495,7 +538,10 @@ defmodule BDS.Search do
end
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_timestamp(nil, _position), do: nil
@@ -508,7 +554,8 @@ defmodule BDS.Search do
{:ok, datetime} = DateTime.new(date, time, "Etc/UTC")
DateTime.to_unix(datetime)
- {:error, _reason} -> nil
+ {:error, _reason} ->
+ nil
end
end
diff --git a/lib/bds/tags.ex b/lib/bds/tags.ex
index d1b6ad3..1208b6b 100644
--- a/lib/bds/tags.ex
+++ b/lib/bds/tags.ex
@@ -185,7 +185,10 @@ defmodule BDS.Tags do
target_tag ->
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 ->
source_names = Enum.map(source_tags, & &1.name)
@@ -227,10 +230,21 @@ defmodule BDS.Tags do
end
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,
%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")}
else
:ok
@@ -238,10 +252,21 @@ defmodule BDS.Tags do
end
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,
%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")}
else
:ok
diff --git a/lib/bds/tags/tag.ex b/lib/bds/tags/tag.ex
index a4b46bf..d382e51 100644
--- a/lib/bds/tags/tag.ex
+++ b/lib/bds/tags/tag.ex
@@ -19,7 +19,9 @@ defmodule BDS.Tags.Tag do
def changeset(tag, attrs) do
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]
)
|> validate_required([:id, :project_id, :name, :created_at, :updated_at])
diff --git a/lib/bds/tasks.ex b/lib/bds/tasks.ex
index 5814f15..529107a 100644
--- a/lib/bds/tasks.ex
+++ b/lib/bds/tasks.ex
@@ -10,7 +10,8 @@ defmodule BDS.Tasks do
GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
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})
end
@@ -57,7 +58,8 @@ defmodule BDS.Tasks do
if map_size(next_state.running) < max_concurrent() do
{:reply, {:ok, public_task(task)}, start_task(next_state, task.id, work)}
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
@@ -83,7 +85,9 @@ defmodule BDS.Tasks do
next_state =
state
|> 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()
{:reply, :ok, next_state}
@@ -109,7 +113,11 @@ defmodule BDS.Tasks do
def handle_call({:complete_task, task_id}, _from, state) do
next_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()
{:reply, :ok, next_state}
@@ -118,7 +126,11 @@ defmodule BDS.Tasks do
def handle_call({:fail_task, task_id, error_message}, _from, state) do
next_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()
{:reply, :ok, next_state}
@@ -146,8 +158,16 @@ defmodule BDS.Tasks do
_status ->
attrs =
case normalize_result(result) do
- {:ok, value} -> %{status: :completed, result: value, progress: 1.0, finished_at: DateTime.utc_now()}
- {:error, reason} -> %{status: :failed, error: reason, finished_at: DateTime.utc_now()}
+ {:ok, value} ->
+ %{
+ status: :completed,
+ result: value,
+ progress: 1.0,
+ finished_at: DateTime.utc_now()
+ }
+
+ {:error, reason} ->
+ %{status: :failed, error: reason, finished_at: DateTime.utc_now()}
end
update_task(state, task_id, attrs)
@@ -176,7 +196,11 @@ defmodule BDS.Tasks do
state
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
|> remove_running(task_id, ref)
|> start_queued_tasks()
@@ -186,7 +210,9 @@ defmodule BDS.Tasks do
end
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.Supervisor.async_nolink(BDS.Tasks.TaskSupervisor, fn ->
@@ -209,6 +235,7 @@ defmodule BDS.Tasks do
true ->
[{task_id, work} | remaining] = state.queue
+
state
|> Map.put(:queue, remaining)
|> start_task(task_id, work)
@@ -231,8 +258,13 @@ defmodule BDS.Tasks do
now_ms = System.monotonic_time(:millisecond)
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
- update_task(state, task_id, %{progress: value, message: message, last_reported_at: now_ms})
+ if is_nil(last_reported_at) or now_ms - last_reported_at >= progress_throttle_ms() or
+ value == 1.0 do
+ update_task(state, task_id, %{
+ progress: value,
+ message: message,
+ last_reported_at: now_ms
+ })
else
state
end
diff --git a/lib/bds/templates.ex b/lib/bds/templates.ex
index c1cdd6d..84cf093 100644
--- a/lib/bds/templates.ex
+++ b/lib/bds/templates.ex
@@ -50,11 +50,19 @@ defmodule BDS.Templates do
:ok =
File.write(
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.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()
end
end
@@ -67,27 +75,41 @@ defmodule BDS.Templates do
template ->
next_slug =
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
template.slug
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
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)
- updates = %{}
- |> maybe_put(:title, attr(attrs, :title))
- |> maybe_put(:kind, attr(attrs, :kind))
- |> maybe_put(:enabled, attr(attrs, :enabled))
- |> maybe_put(:content, attr(attrs, :content))
- |> Map.put(:file_path, next_file_path)
- |> Map.put(:slug, next_slug)
- |> Map.put(:version, template.version + 1)
- |> Map.put(:updated_at, now)
- |> Map.put(:status, next_status)
+ updates =
+ %{}
+ |> maybe_put(:title, attr(attrs, :title))
+ |> maybe_put(:kind, attr(attrs, :kind))
+ |> maybe_put(:enabled, attr(attrs, :enabled))
+ |> maybe_put(:content, attr(attrs, :content))
+ |> Map.put(:file_path, next_file_path)
+ |> Map.put(:slug, next_slug)
+ |> Map.put(:version, template.version + 1)
+ |> Map.put(:updated_at, now)
+ |> Map.put(:status, next_status)
Repo.transaction(fn ->
updated_template =
@@ -172,7 +194,9 @@ defmodule BDS.Templates do
end
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 =
case exclude_id do
@@ -210,11 +234,23 @@ defmodule BDS.Templates do
end
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
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
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])
- 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])
Enum.each(affected_posts, fn post ->
@@ -246,17 +286,23 @@ defmodule BDS.Templates do
affected_posts =
Repo.all(
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,
- 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])
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])
diff --git a/lib/bds/templates/template.ex b/lib/bds/templates/template.ex
index c220b6b..17709b1 100644
--- a/lib/bds/templates/template.ex
+++ b/lib/bds/templates/template.ex
@@ -24,10 +24,36 @@ defmodule BDS.Templates.Template do
def changeset(template, attrs) do
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]
)
- |> 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)
|> unique_constraint(:slug, name: :templates_project_slug_idx)
end
diff --git a/lib/bds/types/string_list.ex b/lib/bds/types/string_list.ex
index 0aae140..150a60e 100644
--- a/lib/bds/types/string_list.ex
+++ b/lib/bds/types/string_list.ex
@@ -27,7 +27,8 @@ defmodule BDS.Types.StringList do
:error
end
- _ -> :error
+ _ ->
+ :error
end
end
diff --git a/test/bds/generation_test.exs b/test/bds/generation_test.exs
index 8891ac7..511ba25 100644
--- a/test/bds/generation_test.exs
+++ b/test/bds/generation_test.exs
@@ -10,7 +10,10 @@ defmodule BDS.GenerationTest do
setup do
: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)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -18,8 +21,13 @@ defmodule BDS.GenerationTest do
%{project: project, temp_dir: temp_dir}
end
- test "write_generated_file writes under html output and skips unchanged content by hash", %{project: project, temp_dir: temp_dir} do
- assert {:ok, first_write} = BDS.Generation.write_generated_file(project.id, "index.html", "hello")
+ test "write_generated_file writes under html output and skips unchanged content by hash", %{
+ project: project,
+ temp_dir: temp_dir
+ } do
+ assert {:ok, first_write} =
+ BDS.Generation.write_generated_file(project.id, "index.html", "hello")
+
assert first_write.written? == true
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.content_hash == first_write.content_hash
- assert {:ok, second_write} = BDS.Generation.write_generated_file(project.id, "index.html", "hello")
+ assert {:ok, second_write} =
+ BDS.Generation.write_generated_file(project.id, "index.html", "hello")
+
assert second_write.written? == false
assert second_write.content_hash == first_write.content_hash
- assert {:ok, third_write} = BDS.Generation.write_generated_file(project.id, "index.html", "updated")
+ assert {:ok, third_write} =
+ BDS.Generation.write_generated_file(project.id, "index.html", "updated")
+
assert third_write.written? == true
assert third_write.content_hash != first_write.content_hash
assert File.read!(output_path) == "updated"
end
- test "delete_generated_file removes tracked output and forgets its hash", %{project: project, temp_dir: temp_dir} do
- assert {:ok, _write} = BDS.Generation.write_generated_file(project.id, "tag/elixir/index.html", "tag")
+ test "delete_generated_file removes tracked output and forgets its hash", %{
+ project: project,
+ temp_dir: temp_dir
+ } do
+ assert {:ok, _write} =
+ BDS.Generation.write_generated_file(
+ project.id,
+ "tag/elixir/index.html",
+ "tag"
+ )
output_path = Path.join([temp_dir, "html", "tag", "elixir", "index.html"])
assert File.exists?(output_path)
@@ -52,7 +72,8 @@ defmodule BDS.GenerationTest do
assert files == []
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} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
@@ -88,7 +109,8 @@ defmodule BDS.GenerationTest do
"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
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/"
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} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
@@ -110,7 +135,8 @@ defmodule BDS.GenerationTest do
project_id: project.id,
title: "List View",
kind: :list,
- content: "{{ page_title }}
{% for post in posts %}{{ post.title }}{% endfor %}"
+ content:
+ "{{ page_title }}
{% for post in posts %}{{ post.title }}{% endfor %}"
})
assert {:ok, _published_list} = BDS.Templates.publish_template(list_template.id)
@@ -120,7 +146,8 @@ defmodule BDS.GenerationTest do
project_id: project.id,
title: "Post View",
kind: :post,
- content: "{{ post.title }}
{{ post.content }}
"
+ content:
+ "{{ post.title }}
{{ post.content }}
"
})
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"
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} =
BDS.Menu.update_menu(project.id, [
%{kind: :page, label: "Notes", slug: "notes"}
@@ -199,7 +229,8 @@ defmodule BDS.GenerationTest do
assert post_html =~ "Language"
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} =
Metadata.update_project_metadata(project.id, %{
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)
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} =
Posts.create_post(%{
@@ -252,7 +286,9 @@ defmodule BDS.GenerationTest do
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(href="#{canonical_post_href}")
assert post_html =~ ~s(src="/#{media.file_path}")
@@ -262,7 +298,10 @@ defmodule BDS.GenerationTest do
assert not_found_html =~ "Back to preview home"
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} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
@@ -291,14 +330,18 @@ defmodule BDS.GenerationTest do
post_path = BDS.Generation.post_output_path(published_post)
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", translation_path])) =~ "Hallo generierte Welt"
assert File.read!(Path.join([temp_dir, "html", post_path])) =~ ~s(data-pagefind-body)
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} =
Metadata.update_project_metadata(project.id, %{
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
- 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)
end
@@ -341,8 +388,13 @@ defmodule BDS.GenerationTest do
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", "page", "2", "index.html"])) =~ "Archive 3"
+ assert File.read!(Path.join([temp_dir, "html", "category", "notes", "index.html"])) =~
+ "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", "2026", "04", "index.html"])) =~ "2026-04"
end
diff --git a/test/bds/maintenance_test.exs b/test/bds/maintenance_test.exs
index f454acb..9a1c014 100644
--- a/test/bds/maintenance_test.exs
+++ b/test/bds/maintenance_test.exs
@@ -7,7 +7,10 @@ defmodule BDS.MaintenanceTest do
setup do
: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)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -15,7 +18,10 @@ defmodule BDS.MaintenanceTest do
%{project: project, temp_dir: temp_dir}
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"])
File.mkdir_p!(posts_dir)
@@ -60,6 +66,7 @@ defmodule BDS.MaintenanceTest do
template_dir = Path.join(temp_dir, "templates")
File.mkdir_p!(template_dir)
+
File.write!(
Path.join(template_dir, "dispatch-view.liquid"),
[
@@ -81,6 +88,7 @@ defmodule BDS.MaintenanceTest do
script_dir = Path.join(temp_dir, "scripts")
File.mkdir_p!(script_dir)
+
File.write!(
Path.join(script_dir, "dispatch.lua"),
[
@@ -120,10 +128,14 @@ defmodule BDS.MaintenanceTest do
end
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
- 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")
File.write!(source_path, "hello media")
@@ -192,6 +204,7 @@ defmodule BDS.MaintenanceTest do
assert {:ok, published_template} = BDS.Templates.publish_template(template.id)
post_path = Path.join(temp_dir, published_post.file_path)
+
File.write!(
post_path,
[
@@ -205,9 +218,9 @@ defmodule BDS.MaintenanceTest do
"language: de",
"do_not_translate: false",
"template_slug: ",
- "created_at: #{published_post.created_at}",
- "updated_at: #{published_post.updated_at}",
- "published_at: #{published_post.published_at}",
+ "created_at: #{published_post.created_at + 10}",
+ "updated_at: #{published_post.updated_at + 20}",
+ "published_at: #{published_post.published_at + 30}",
"tags:",
" - beta",
"categories:",
@@ -220,6 +233,7 @@ defmodule BDS.MaintenanceTest do
)
post_translation_path = Path.join(temp_dir, published_post_translation.file_path)
+
File.write!(
post_translation_path,
[
@@ -241,6 +255,7 @@ defmodule BDS.MaintenanceTest do
)
media_sidecar_path = Path.join(temp_dir, media.sidecar_path)
+
File.write!(
media_sidecar_path,
[
@@ -262,7 +277,9 @@ defmodule BDS.MaintenanceTest do
|> 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!(
media_translation_sidecar_path,
[
@@ -277,6 +294,7 @@ defmodule BDS.MaintenanceTest do
)
script_path = Path.join(temp_dir, published_script.file_path)
+
File.write!(
script_path,
[
@@ -298,6 +316,7 @@ defmodule BDS.MaintenanceTest do
)
template_path = Path.join(temp_dir, published_template.file_path)
+
File.write!(
template_path,
[
@@ -317,50 +336,128 @@ defmodule BDS.MaintenanceTest do
|> 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!(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\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"
+ )
- 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\n"
+ )
+
+ assert {:ok, %{diff_reports: diff_reports, orphan_reports: orphan_reports}} =
+ BDS.Maintenance.metadata_diff(project.id)
assert Enum.any?(diff_reports, fn report ->
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?(report.differences, &(&1.name == "excerpt" and &1.db_value == "Original summary" and &1.file_value == "Edited summary"))
+ Enum.any?(
+ 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)
assert Enum.any?(diff_reports, fn report ->
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"))
end)
assert Enum.any?(diff_reports, fn report ->
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?(report.differences, &(&1.name == "entrypoint" and &1.file_value == "run"))
+ Enum.any?(
+ 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)
assert Enum.any?(diff_reports, fn report ->
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"))
end)
assert Enum.any?(diff_reports, fn report ->
- report.entity_type == "post_translation" and report.entity_id == published_post_translation.id and
- 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"))
+ report.entity_type == "post_translation" and
+ report.entity_id == published_post_translation.id and
+ 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)
assert Enum.any?(diff_reports, fn report ->
- report.entity_type == "media_translation" and report.entity_id == media_translation.id and
- 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"))
+ report.entity_type == "media_translation" and
+ report.entity_id == media_translation.id and
+ 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)
orphan_paths = Enum.map(orphan_reports, & &1.file_path)
diff --git a/test/bds/media_test.exs b/test/bds/media_test.exs
index 18c7654..ddb3daa 100644
--- a/test/bds/media_test.exs
+++ b/test/bds/media_test.exs
@@ -13,7 +13,10 @@ defmodule BDS.MediaTest do
%{project: project, temp_dir: temp_dir}
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")
File.write!(source_path, "hello media")
@@ -54,7 +57,8 @@ defmodule BDS.MediaTest do
source_path = Path.join(temp_dir, "sample.txt")
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} =
BDS.Media.update_media(media.id, %{
@@ -76,11 +80,15 @@ defmodule BDS.MediaTest do
assert sidecar =~ "tags:\n - beta\n"
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")
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} =
BDS.Media.upsert_media_translation(media.id, "de", %{
@@ -103,7 +111,10 @@ defmodule BDS.MediaTest do
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"])
File.mkdir_p!(media_dir)
@@ -181,16 +192,27 @@ defmodule BDS.MediaTest do
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")
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)
- 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.large == "thumbnails/#{String.slice(media.id, 0, 2)}/#{media.id}-large.webp"
+
+ 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.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"
Enum.each(Map.values(thumbnail_paths), fn path ->
@@ -198,11 +220,15 @@ defmodule BDS.MediaTest do
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")
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.width == 3
@@ -230,11 +256,13 @@ defmodule BDS.MediaTest do
assert Path.extname(thumbnail_paths.ai) == ".jpg"
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")
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.height == 3
@@ -248,11 +276,15 @@ defmodule BDS.MediaTest do
assert_images_match!(actual_thumbnail, expected_thumbnail)
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")
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)
File.rm!(Path.join(temp_dir, thumbnail_paths.small))
@@ -266,12 +298,17 @@ defmodule BDS.MediaTest do
end)
end
- test "import_media generates thumbnails for png and webp sources", %{project: project, temp_dir: temp_dir} do
- Enum.each([{ ".png", "image/png"}, {".webp", "image/webp"}], fn {extension, mime_type} ->
+ test "import_media generates thumbnails for png and webp sources", %{
+ 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}")
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.width == 2
assert media.height == 3
@@ -282,29 +319,39 @@ defmodule BDS.MediaTest do
end)
end
- test "import_media detects supported TIFF, BMP, HEIC, and HEIF extensions", %{project: project, temp_dir: temp_dir} do
- Enum.each([
- {"asset.tif", "image/tiff"},
- {"asset.tiff", "image/tiff"},
- {"asset.bmp", "image/bmp"},
- {"asset.heic", "image/heic"},
- {"asset.heif", "image/heif"}
- ], fn {file_name, mime_type} ->
- source_path = Path.join(temp_dir, file_name)
- File.write!(source_path, "placeholder")
+ test "import_media detects supported TIFF, BMP, HEIC, and HEIF extensions", %{
+ project: project,
+ temp_dir: temp_dir
+ } do
+ Enum.each(
+ [
+ {"asset.tif", "image/tiff"},
+ {"asset.tiff", "image/tiff"},
+ {"asset.bmp", "image/bmp"},
+ {"asset.heic", "image/heic"},
+ {"asset.heif", "image/heif"}
+ ],
+ fn {file_name, mime_type} ->
+ source_path = Path.join(temp_dir, file_name)
+ File.write!(source_path, "placeholder")
- assert {:ok, media} = BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
- assert media.mime_type == mime_type
- assert media.width == nil
- assert media.height == nil
- end)
+ assert {:ok, media} =
+ BDS.Media.import_media(%{project_id: project.id, source_path: source_path})
+
+ assert media.mime_type == mime_type
+ assert media.width == nil
+ assert media.height == nil
+ 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")
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} =
BDS.Media.upsert_media_translation(media.id, "de", %{
diff --git a/test/bds/menu_test.exs b/test/bds/menu_test.exs
index cdf0072..fbe19f7 100644
--- a/test/bds/menu_test.exs
+++ b/test/bds/menu_test.exs
@@ -11,7 +11,8 @@ defmodule BDS.MenuTest do
%{project: project, temp_dir: temp_dir}
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} =
BDS.Menu.update_menu(project.id, [
%{kind: :page, label: "About", slug: "about"},
@@ -52,7 +53,10 @@ defmodule BDS.MenuTest do
assert loaded == menu
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")
File.mkdir_p!(meta_dir)
diff --git a/test/bds/metadata_test.exs b/test/bds/metadata_test.exs
index 36b8b1b..c86d406 100644
--- a/test/bds/metadata_test.exs
+++ b/test/bds/metadata_test.exs
@@ -11,7 +11,10 @@ defmodule BDS.MetadataTest do
%{project: project, temp_dir: temp_dir}
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} =
BDS.Metadata.update_project_metadata(project.id, %{
name: "Renamed Blog",
@@ -51,7 +54,8 @@ defmodule BDS.MetadataTest do
assert loaded.blog_languages == ["de", "fr"]
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} =
diff --git a/test/bds/post_translations_test.exs b/test/bds/post_translations_test.exs
index 103eb55..10e9756 100644
--- a/test/bds/post_translations_test.exs
+++ b/test/bds/post_translations_test.exs
@@ -6,7 +6,10 @@ defmodule BDS.PostTranslationsTest do
setup do
: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)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -14,7 +17,8 @@ defmodule BDS.PostTranslationsTest do
%{project: project, temp_dir: temp_dir}
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} =
Posts.create_post(%{
project_id: project.id,
@@ -75,7 +79,8 @@ defmodule BDS.PostTranslationsTest do
assert {:ok, []} = Posts.list_post_translations(post.id)
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} =
Metadata.update_project_metadata(project.id, %{
main_language: "en",
diff --git a/test/bds/posts_test.exs b/test/bds/posts_test.exs
index 96a8cc5..3ce4947 100644
--- a/test/bds/posts_test.exs
+++ b/test/bds/posts_test.exs
@@ -13,7 +13,9 @@ defmodule BDS.PostsTest do
%{project: project, temp_dir: temp_dir}
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} =
BDS.Posts.create_post(%{
project_id: project.id,
@@ -48,7 +50,10 @@ defmodule BDS.PostsTest do
assert duplicate_slug_post.categories == []
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 first.title == ""
assert first.slug == "untitled"
@@ -59,12 +64,15 @@ defmodule BDS.PostsTest do
other_temp_dir = Path.join(temp_dir, "elsewhere")
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 other_post.slug == "untitled"
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} =
BDS.Posts.create_post(%{
project_id: project.id,
@@ -97,11 +105,14 @@ defmodule BDS.PostsTest do
end
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)
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} =
BDS.Posts.create_post(%{
@@ -144,7 +155,9 @@ defmodule BDS.PostsTest do
end
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)
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 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)
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} =
BDS.Posts.create_post(%{
@@ -200,7 +216,9 @@ defmodule BDS.PostsTest do
end
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)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -227,7 +245,9 @@ defmodule BDS.PostsTest do
end
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)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -279,9 +299,9 @@ defmodule BDS.PostsTest do
assert post.language == "en"
assert post.do_not_translate == true
assert post.template_slug == "article"
- assert post.created_at == 1711843200
- assert post.updated_at == 1711929600
- assert post.published_at == 1712016000
+ assert post.created_at == 1_711_843_200
+ assert post.updated_at == 1_711_929_600
+ assert post.published_at == 1_712_016_000
assert post.tags == ["alpha"]
assert post.categories == ["notes"]
assert post.file_path == "posts/2026/04/recovered-post.md"
diff --git a/test/bds/preview_test.exs b/test/bds/preview_test.exs
index 89208f7..40a5edf 100644
--- a/test/bds/preview_test.exs
+++ b/test/bds/preview_test.exs
@@ -16,7 +16,8 @@ defmodule BDS.PreviewTest do
%{project: project, temp_dir: temp_dir}
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} =
Metadata.update_project_metadata(project.id, %{
public_url: "https://example.com/blog",
@@ -24,10 +25,29 @@ defmodule BDS.PreviewTest do
blog_languages: ["en", "de"]
})
- assert {:ok, _} = Generation.write_generated_file(project.id, "index.html", "home")
- assert {:ok, _} = Generation.write_generated_file(project.id, "de/index.html", "startseite")
- assert {:ok, _} = Generation.write_generated_file(project.id, "tag/elixir/index.html", "tag archive")
- assert {:ok, _} = Generation.write_generated_file(project.id, "pagefind/pagefind-ui.js", "console.log('pagefind')")
+ assert {:ok, _} =
+ Generation.write_generated_file(project.id, "index.html", "home")
+
+ assert {:ok, _} =
+ Generation.write_generated_file(
+ project.id,
+ "de/index.html",
+ "startseite"
+ )
+
+ assert {:ok, _} =
+ Generation.write_generated_file(
+ project.id,
+ "tag/elixir/index.html",
+ "tag archive"
+ )
+
+ assert {:ok, _} =
+ Generation.write_generated_file(
+ project.id,
+ "pagefind/pagefind-ui.js",
+ "console.log('pagefind')"
+ )
media_dir = Path.join([temp_dir, "media", "2026", "04"])
File.mkdir_p!(media_dir)
@@ -46,11 +66,20 @@ defmodule BDS.PreviewTest do
assert server.port == 4123
assert server.is_running == true
- assert {:ok, %{body: "home", content_type: "text/html"}} = BDS.Preview.request(project.id, "/")
- assert {:ok, %{body: "startseite", content_type: "text/html"}} = BDS.Preview.request(project.id, "/de/")
- assert {:ok, %{body: "tag archive", 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: "home", content_type: "text/html"}} =
+ BDS.Preview.request(project.id, "/")
+
+ assert {:ok, %{body: "startseite", content_type: "text/html"}} =
+ BDS.Preview.request(project.id, "/de/")
+
+ assert {:ok, %{body: "tag archive", 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"}} =
BDS.Preview.preview_draft(project.id, "/draft/draft-post", post.id)
@@ -67,7 +96,8 @@ defmodule BDS.PreviewTest do
project_id: project.id,
title: "Preview Post",
kind: :post,
- content: "{{ post.title }}
{{ post.content }}
"
+ content:
+ "{{ post.title }}
{{ post.content }}
"
})
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)
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} =
BDS.Menu.update_menu(project.id, [
%{kind: :page, label: "Notes", slug: "notes"}
@@ -126,7 +158,8 @@ defmodule BDS.PreviewTest do
assert :ok = BDS.Preview.stop_preview(project.id)
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()
assert {:ok, _metadata} =
@@ -157,7 +190,10 @@ defmodule BDS.PreviewTest do
assert {:ok, published_linked_post} = Posts.publish_post(linked_post.id)
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} =
Posts.create_post(%{
@@ -190,14 +226,21 @@ defmodule BDS.PreviewTest do
assert missing_body =~ ~s(data-template="not-found")
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 :ok = BDS.Preview.stop_preview(project.id)
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()
assert {:ok, _metadata} =
@@ -207,7 +250,8 @@ defmodule BDS.PreviewTest do
blog_languages: ["en"]
})
- assert {:ok, _} = Generation.write_generated_file(project.id, "index.html", "http home")
+ assert {:ok, _} =
+ Generation.write_generated_file(project.id, "index.html", "http home")
assert {:ok, post} =
Posts.create_post(%{
@@ -220,13 +264,26 @@ defmodule BDS.PreviewTest do
assert {:ok, server} = BDS.Preview.start_preview(project.id)
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 == "http home"
- 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}} =
- :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"
diff --git a/test/bds/projects_test.exs b/test/bds/projects_test.exs
index 43c5cc5..c7db178 100644
--- a/test/bds/projects_test.exs
+++ b/test/bds/projects_test.exs
@@ -18,13 +18,16 @@ defmodule BDS.ProjectsTest do
%{temp_root: temp_root}
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")
second_dir = Path.join(temp_root, "second")
File.mkdir_p!(first_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.slug == "foo-bar-blog"
@@ -33,16 +36,21 @@ defmodule BDS.ProjectsTest do
assert is_integer(first.created_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.is_active == false
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")
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", "post-list.liquid"]))
@@ -52,18 +60,25 @@ defmodule BDS.ProjectsTest do
assert File.exists?(Path.join([temp_dir, "templates", "macros", "gallery.liquid"]))
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 {"post-list", :list} in starter_slugs
assert {"not-found", :not_found} in starter_slugs
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")
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"])
original_contents = File.read!(template_path)
@@ -75,11 +90,15 @@ defmodule BDS.ProjectsTest do
reinstalled_contents = File.read!(template_path)
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"]
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")
second_dir = Path.join(temp_root, "active-second")
File.mkdir_p!(first_dir)
diff --git a/test/bds/publishing_test.exs b/test/bds/publishing_test.exs
index 79aa989..2382844 100644
--- a/test/bds/publishing_test.exs
+++ b/test/bds/publishing_test.exs
@@ -3,7 +3,10 @@ defmodule BDS.PublishingTest do
setup do
: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)
on_exit(fn -> File.rm_rf(temp_dir) end)
@@ -11,7 +14,10 @@ defmodule BDS.PublishingTest do
%{project: project, temp_dir: temp_dir}
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()
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")
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
end
@@ -46,7 +56,10 @@ defmodule BDS.PublishingTest do
assert_receive {:uploaded, :media, "/srv/blog/media", ["asset.jpg"], :rsync}
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()
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_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_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 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
- 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()
html_index = Path.join([temp_dir, "html", "index.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))
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_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}
- refute_receive {:command_run, "scp", ["-q", _, "deploy@example.com:/srv/blog/media/asset.jpg"], _opts_d}
+
+ 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}
+
+ refute_receive {:command_run, "scp",
+ ["-q", _, "deploy@example.com:/srv/blog/media/asset.jpg"], _opts_d}
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.write!(Path.join([temp_dir, "html", "index.html"]), "")
File.mkdir_p!(Path.join([temp_dir, "thumbnails"]))
@@ -161,6 +216,75 @@ defmodule BDS.PublishingTest do
assert failed_job.error == "thumbnail failure"
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, "
")
+ 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) when attempts > 0 do
diff --git a/test/bds/rendering_test.exs b/test/bds/rendering_test.exs
new file mode 100644
index 0000000..59f4560
--- /dev/null
+++ b/test/bds/rendering_test.exs
@@ -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
diff --git a/test/bds/repo/schema_migration_test.exs b/test/bds/repo/schema_migration_test.exs
index a31431d..e84f538 100644
--- a/test/bds/repo/schema_migration_test.exs
+++ b/test/bds/repo/schema_migration_test.exs
@@ -180,7 +180,13 @@ defmodule BDS.Repo.SchemaMigrationTest do
"ai_model_modalities" => ["provider", "model_id", "direction", "modality"],
"ai_catalog_meta" => ["key", "value"],
"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" => [
"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("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(
"generated_file_hashes",
@@ -345,7 +359,9 @@ defmodule BDS.Repo.SchemaMigrationTest do
defp unique_index_columns(table, index_name) do
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}"
query_rows("PRAGMA index_info(#{index_name})")
diff --git a/test/bds/scripting/job_test.exs b/test/bds/scripting/job_test.exs
index 5c47513..183b6f7 100644
--- a/test/bds/scripting/job_test.exs
+++ b/test/bds/scripting/job_test.exs
@@ -58,7 +58,13 @@ defmodule BDS.Scripting.JobTest do
assert {:ok, job} = BDS.Scripting.start_job("irrelevant", "main")
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
completed_job = wait_for_job(job.id, &(&1.status == :completed))
diff --git a/test/bds/scripts_test.exs b/test/bds/scripts_test.exs
index 8d1ea50..6554db0 100644
--- a/test/bds/scripts_test.exs
+++ b/test/bds/scripts_test.exs
@@ -42,7 +42,10 @@ defmodule BDS.ScriptsTest do
assert macro_script.slug == "render-card"
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} =
BDS.Scripts.create_script(%{
project_id: project.id,
@@ -73,7 +76,9 @@ defmodule BDS.ScriptsTest do
assert contents =~ "\n---\nfunction main() return 'ok' end\n"
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} =
BDS.Scripts.create_script(%{
project_id: project.id,
@@ -99,7 +104,10 @@ defmodule BDS.ScriptsTest do
assert updated.updated_at >= published.updated_at
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} =
BDS.Scripts.create_script(%{
project_id: project.id,
@@ -116,7 +124,10 @@ defmodule BDS.ScriptsTest do
refute File.exists?(Path.join(temp_dir, published.file_path))
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")
File.mkdir_p!(script_dir)
diff --git a/test/bds/search_test.exs b/test/bds/search_test.exs
index 6ef75d5..6b34e0a 100644
--- a/test/bds/search_test.exs
+++ b/test/bds/search_test.exs
@@ -13,7 +13,8 @@ defmodule BDS.SearchTest do
%{project: project, temp_dir: temp_dir}
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} =
BDS.Posts.create_post(%{
project_id: project.id,
@@ -54,14 +55,25 @@ defmodule BDS.SearchTest do
assert results.limit == 50
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 tag_results.total == 2
- assert Enum.sort(Enum.map(tag_results.posts, & &1.id)) == Enum.sort([draft_post.id, published_post.id])
+ assert {:ok, tag_results} =
+ BDS.Search.search_posts(project.id, "galaxy", %{
+ 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 {: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.offset == 1
assert paged_results.limit == 1
@@ -120,12 +132,17 @@ defmodule BDS.SearchTest do
assert Enum.map(results.posts, & &1.id) == [post.id]
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]
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")
File.write!(source_path, "hero")
@@ -164,7 +181,10 @@ defmodule BDS.SearchTest do
assert deleted_results.total == 0
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"])
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"]
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} =
BDS.Posts.create_post(%{
project_id: project.id,
diff --git a/test/bds/tags_test.exs b/test/bds/tags_test.exs
index 0e9e9cf..8a47890 100644
--- a/test/bds/tags_test.exs
+++ b/test/bds/tags_test.exs
@@ -14,8 +14,13 @@ defmodule BDS.TagsTest do
%{project: project, temp_dir: temp_dir}
end
- test "create_tag persists the row and rewrites meta/tags.json sorted by name", %{project: project, temp_dir: temp_dir} do
- assert {:ok, zebra} = BDS.Tags.create_tag(%{project_id: project.id, name: "Zebra", color: "#000000"})
+ test "create_tag persists the row and rewrites meta/tags.json sorted by name", %{
+ 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 zebra.name == "Zebra"
@@ -35,7 +40,10 @@ defmodule BDS.TagsTest do
assert "has already been taken" in errors_on(changeset).name
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, updated} =
@@ -51,11 +59,16 @@ defmodule BDS.TagsTest do
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))
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, post} =
@@ -83,7 +96,8 @@ defmodule BDS.TagsTest do
assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
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_b} = BDS.Tags.create_tag(%{project_id: project.id, name: "Beta"})
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))
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, _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))
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} =
BDS.Tags.create_tag(%{
project_id: project.id,
@@ -183,7 +199,11 @@ defmodule BDS.TagsTest do
assert %{
"tags" => [
%{"name" => "Another"},
- %{"name" => "Existing", "color" => "#112233", "post_template_slug" => "feature-view"},
+ %{
+ "name" => "Existing",
+ "color" => "#112233",
+ "post_template_slug" => "feature-view"
+ },
%{"name" => "Missing"}
]
} = Jason.decode!(File.read!(tags_path))
diff --git a/test/bds/tasks_test.exs b/test/bds/tasks_test.exs
index 9007278..649bc9f 100644
--- a/test/bds/tasks_test.exs
+++ b/test/bds/tasks_test.exs
@@ -94,7 +94,11 @@ defmodule BDS.TasksTest do
end
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.group_id == "generation"
@@ -106,7 +110,9 @@ defmodule BDS.TasksTest do
assert progressed.status == :running
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
defp receive_started do
diff --git a/test/bds/templates_test.exs b/test/bds/templates_test.exs
index 089029e..7b93464 100644
--- a/test/bds/templates_test.exs
+++ b/test/bds/templates_test.exs
@@ -33,12 +33,20 @@ defmodule BDS.TemplatesTest do
assert template.content == "{{ content }}"
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"
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} =
BDS.Templates.create_template(%{
project_id: project.id,
@@ -68,7 +76,9 @@ defmodule BDS.TemplatesTest do
assert contents =~ "\n---\n\n"
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} =
BDS.Templates.create_template(%{
project_id: project.id,
@@ -94,7 +104,8 @@ defmodule BDS.TemplatesTest do
assert updated.updated_at >= published.updated_at
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} =
BDS.Templates.create_template(%{
project_id: project.id,
@@ -122,7 +133,8 @@ defmodule BDS.TemplatesTest do
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)
@@ -143,7 +155,8 @@ defmodule BDS.TemplatesTest do
assert %{"tags" => [%{"name" => "Feature"}]} = Jason.decode!(File.read!(tags_path))
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} =
BDS.Templates.create_template(%{
project_id: project.id,
@@ -198,11 +211,15 @@ defmodule BDS.TemplatesTest do
assert post_contents =~ "\n---\nBody\n"
tags_path = Path.join([temp_dir, "meta", "tags.json"])
+
assert %{"tags" => [%{"name" => "Feature", "post_template_slug" => "feature-view"}]} =
Jason.decode!(File.read!(tags_path))
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")
File.mkdir_p!(template_dir)