feat: more complete metadata diff, scp publishing and rendering context

This commit is contained in:
2026-04-24 06:30:41 +02:00
parent e101a3db79
commit 624b698bb3
48 changed files with 2193 additions and 505 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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(

View File

@@ -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

View File

@@ -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

View File

@@ -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("&", "&amp;")
|> String.replace("<", "&lt;")
|> String.replace(">", "&gt;")
|> String.replace(~s(") , "&quot;")
|> String.replace(~s("), "&quot;")
end
defp attr(attrs, key) do

View File

@@ -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)})

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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}

View File

@@ -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])

View File

@@ -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
[]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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 ", &quoted_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

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -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])

View File

@@ -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

View File

@@ -27,7 +27,8 @@ defmodule BDS.Types.StringList do
:error
end
_ -> :error
_ ->
:error
end
end