feat: more complete metadata diff, scp publishing and rendering context
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)})
|
||||
|
||||
107
lib/bds/posts.ex
107
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
[]
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,7 +27,8 @@ defmodule BDS.Types.StringList do
|
||||
:error
|
||||
end
|
||||
|
||||
_ -> :error
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
Reference in New Issue
Block a user