feat: tag discovery from posts
This commit is contained in:
@@ -47,6 +47,52 @@ defmodule BDS.Tags do
|
|||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sync_tags_from_posts(project_id) do
|
||||||
|
Repo.transaction(fn ->
|
||||||
|
existing_names =
|
||||||
|
project_id
|
||||||
|
|> list_tags()
|
||||||
|
|> Enum.map(&String.downcase(&1.name))
|
||||||
|
|> MapSet.new()
|
||||||
|
|
||||||
|
missing_names =
|
||||||
|
project_id
|
||||||
|
|> post_tag_names()
|
||||||
|
|> Enum.reduce({existing_names, []}, fn tag_name, {seen_names, acc} ->
|
||||||
|
normalized = String.downcase(tag_name)
|
||||||
|
|
||||||
|
if MapSet.member?(seen_names, normalized) do
|
||||||
|
{seen_names, acc}
|
||||||
|
else
|
||||||
|
{MapSet.put(seen_names, normalized), [tag_name | acc]}
|
||||||
|
end
|
||||||
|
end)
|
||||||
|
|> elem(1)
|
||||||
|
|> Enum.reverse()
|
||||||
|
|
||||||
|
now = System.system_time(:second)
|
||||||
|
|
||||||
|
Enum.each(missing_names, fn name ->
|
||||||
|
%Tag{}
|
||||||
|
|> Tag.changeset(%{
|
||||||
|
id: Ecto.UUID.generate(),
|
||||||
|
project_id: project_id,
|
||||||
|
name: name,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now
|
||||||
|
})
|
||||||
|
|> Repo.insert!()
|
||||||
|
end)
|
||||||
|
|
||||||
|
write_tags_json(project_id)
|
||||||
|
list_tags(project_id)
|
||||||
|
end)
|
||||||
|
|> case do
|
||||||
|
{:ok, tags} -> {:ok, tags}
|
||||||
|
{:error, reason} -> {:error, reason}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update_tag(tag_id, attrs) do
|
def update_tag(tag_id, attrs) do
|
||||||
case Repo.get(Tag, tag_id) do
|
case Repo.get(Tag, tag_id) do
|
||||||
nil ->
|
nil ->
|
||||||
@@ -212,6 +258,16 @@ defmodule BDS.Tags do
|
|||||||
|> Enum.filter(fn post -> Enum.any?(post.tags || [], &(&1 in tag_names)) end)
|
|> Enum.filter(fn post -> Enum.any?(post.tags || [], &(&1 in tag_names)) end)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp post_tag_names(project_id) do
|
||||||
|
Repo.all(from post in Post, where: post.project_id == ^project_id)
|
||||||
|
|> Enum.flat_map(fn post ->
|
||||||
|
post.tags
|
||||||
|
|> Kernel.||([])
|
||||||
|
|> Enum.map(&String.trim/1)
|
||||||
|
|> Enum.reject(&(&1 == ""))
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
|
||||||
defp replace_tag(tags, old_name, new_name) do
|
defp replace_tag(tags, old_name, new_name) do
|
||||||
tags
|
tags
|
||||||
|> Enum.map(fn tag -> if tag == old_name, do: new_name, else: tag end)
|
|> Enum.map(fn tag -> if tag == old_name, do: new_name, else: tag end)
|
||||||
|
|||||||
@@ -145,6 +145,50 @@ defmodule BDS.TagsTest do
|
|||||||
assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
|
assert %{"tags" => [%{"name" => "Beta"}]} = Jason.decode!(File.read!(tags_path))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "sync_tags_from_posts creates missing tags from post tag arrays and refreshes tags.json", %{project: project, temp_dir: temp_dir} do
|
||||||
|
assert {:ok, existing} =
|
||||||
|
BDS.Tags.create_tag(%{
|
||||||
|
project_id: project.id,
|
||||||
|
name: "Existing",
|
||||||
|
color: "#112233",
|
||||||
|
post_template_slug: "feature-view"
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, _post_a} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "First",
|
||||||
|
content: "Body",
|
||||||
|
tags: ["Existing", "Missing", "Missing"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, _post_b} =
|
||||||
|
BDS.Posts.create_post(%{
|
||||||
|
project_id: project.id,
|
||||||
|
title: "Second",
|
||||||
|
content: "Body",
|
||||||
|
tags: ["Another", "Missing"]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:ok, synced_tags} = BDS.Tags.sync_tags_from_posts(project.id)
|
||||||
|
|
||||||
|
assert Enum.map(synced_tags, & &1.name) == ["Another", "Existing", "Missing"]
|
||||||
|
|
||||||
|
reloaded_existing = Repo.get!(BDS.Tags.Tag, existing.id)
|
||||||
|
assert reloaded_existing.color == "#112233"
|
||||||
|
assert reloaded_existing.post_template_slug == "feature-view"
|
||||||
|
|
||||||
|
tags_path = Path.join([temp_dir, "meta", "tags.json"])
|
||||||
|
|
||||||
|
assert %{
|
||||||
|
"tags" => [
|
||||||
|
%{"name" => "Another"},
|
||||||
|
%{"name" => "Existing", "color" => "#112233", "post_template_slug" => "feature-view"},
|
||||||
|
%{"name" => "Missing"}
|
||||||
|
]
|
||||||
|
} = Jason.decode!(File.read!(tags_path))
|
||||||
|
end
|
||||||
|
|
||||||
defp errors_on(changeset) do
|
defp errors_on(changeset) do
|
||||||
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
Ecto.Changeset.traverse_errors(changeset, fn {message, opts} ->
|
||||||
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
|
Regex.replace(~r"%{(\w+)}", message, fn _, key ->
|
||||||
|
|||||||
Reference in New Issue
Block a user