add tag alias validations

This commit is contained in:
byte[] 2020-09-06 14:19:21 -04:00
parent 6670b050a1
commit 0d359ee81e
4 changed files with 129 additions and 58 deletions

View file

@ -180,64 +180,78 @@ defmodule Philomena.Tags do
end end
def alias_tag(%Tag{} = tag, attrs) do def alias_tag(%Tag{} = tag, attrs) do
target_tag = Repo.get_by!(Tag, name: attrs["target_tag"]) target_tag = Repo.get_by(Tag, name: String.downcase(attrs["target_tag"]))
if tag.id == target_tag.id do tag
tag |> Repo.preload(:aliased_tag)
else |> Tag.alias_changeset(target_tag)
filters_hidden = |> Repo.update()
where(Filter, [f], fragment("? @> ARRAY[?]::integer[]", f.hidden_tag_ids, ^tag.id)) |> case do
{:ok, tag} ->
spawn(fn ->
perform_alias(tag, target_tag)
end)
filters_spoilered = {:ok, tag}
where(Filter, [f], fragment("? @> ARRAY[?]::integer[]", f.spoilered_tag_ids, ^tag.id))
users_watching = error ->
where(User, [u], fragment("? @> ARRAY[?]::integer[]", u.watched_tag_ids, ^tag.id)) error
array_replace(filters_hidden, :hidden_tag_ids, tag.id, target_tag.id)
array_replace(filters_spoilered, :spoilered_tag_ids, tag.id, target_tag.id)
array_replace(users_watching, :watched_tag_ids, tag.id, target_tag.id)
# Manual insert all because ecto won't do it for us
Repo.query!(
"INSERT INTO image_taggings (image_id, tag_id) " <>
"SELECT i.id, #{target_tag.id} FROM images i " <>
"INNER JOIN image_taggings it on it.image_id = i.id " <>
"WHERE it.tag_id = #{tag.id} " <>
"ON CONFLICT DO NOTHING"
)
# Delete taggings on the source tag
Tagging
|> where(tag_id: ^tag.id)
|> Repo.delete_all()
# Update other assocations
UserLink
|> where(tag_id: ^tag.id)
|> Repo.update_all(set: [tag_id: target_tag.id])
DnpEntry
|> where(tag_id: ^tag.id)
|> Repo.update_all(set: [tag_id: target_tag.id])
Channel
|> where(associated_artist_tag_id: ^tag.id)
|> Repo.update_all(set: [associated_artist_tag_id: target_tag.id])
# Update counter
Tag
|> where(id: ^tag.id)
|> Repo.update_all(
set: [images_count: 0, aliased_tag_id: target_tag.id, updated_at: DateTime.utc_now()]
)
# Finally, reindex
reindex_tag_images(target_tag)
reindex_tags([tag, target_tag])
end end
end end
defp perform_alias(tag, target_tag) do
filters_hidden =
where(Filter, [f], fragment("? @> ARRAY[?]::integer[]", f.hidden_tag_ids, ^tag.id))
filters_spoilered =
where(Filter, [f], fragment("? @> ARRAY[?]::integer[]", f.spoilered_tag_ids, ^tag.id))
users_watching =
where(User, [u], fragment("? @> ARRAY[?]::integer[]", u.watched_tag_ids, ^tag.id))
array_replace(filters_hidden, :hidden_tag_ids, tag.id, target_tag.id)
array_replace(filters_spoilered, :spoilered_tag_ids, tag.id, target_tag.id)
array_replace(users_watching, :watched_tag_ids, tag.id, target_tag.id)
# Manual insert all because ecto won't do it for us
Repo.query!(
"INSERT INTO image_taggings (image_id, tag_id) " <>
"SELECT i.id, #{target_tag.id} FROM images i " <>
"INNER JOIN image_taggings it on it.image_id = i.id " <>
"WHERE it.tag_id = #{tag.id} " <>
"ON CONFLICT DO NOTHING"
)
# Delete taggings on the source tag
Tagging
|> where(tag_id: ^tag.id)
|> Repo.delete_all()
# Update other assocations
UserLink
|> where(tag_id: ^tag.id)
|> Repo.update_all(set: [tag_id: target_tag.id])
DnpEntry
|> where(tag_id: ^tag.id)
|> Repo.update_all(set: [tag_id: target_tag.id])
Channel
|> where(associated_artist_tag_id: ^tag.id)
|> Repo.update_all(set: [associated_artist_tag_id: target_tag.id])
# Update counter
Tag
|> where(id: ^tag.id)
|> Repo.update_all(
set: [images_count: 0, aliased_tag_id: target_tag.id, updated_at: DateTime.utc_now()]
)
# Finally, reindex
reindex_tag_images(target_tag)
reindex_tags([tag, target_tag])
end
def reindex_tag_images(%Tag{} = tag) do def reindex_tag_images(%Tag{} = tag) do
# First recount the tag # First recount the tag
image_count = image_count =

View file

@ -1,12 +1,14 @@
defmodule Philomena.Tags.Tag do defmodule Philomena.Tags.Tag do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query
alias Philomena.Channels.Channel alias Philomena.Channels.Channel
alias Philomena.DnpEntries.DnpEntry alias Philomena.DnpEntries.DnpEntry
alias Philomena.UserLinks.UserLink alias Philomena.UserLinks.UserLink
alias Philomena.Tags.Tag alias Philomena.Tags.Tag
alias Philomena.Slug alias Philomena.Slug
alias Philomena.Repo
@namespaces [ @namespaces [
"artist", "artist",
@ -111,6 +113,15 @@ defmodule Philomena.Tags.Tag do
|> put_change(:image, nil) |> put_change(:image, nil)
end end
def alias_changeset(tag, target_tag) do
change(tag)
|> put_assoc(:aliased_tag, target_tag)
|> validate_required([:aliased_tag])
|> validate_not_aliased_to_self()
|> validate_alias_not_transitive()
|> validate_incoming_aliases()
end
def unalias_changeset(tag) do def unalias_changeset(tag) do
change(tag, aliased_tag_id: nil) change(tag, aliased_tag_id: nil)
end end
@ -245,4 +256,46 @@ defmodule Philomena.Tags.Tag do
category -> change(changeset, category: category) category -> change(changeset, category: category)
end end
end end
defp validate_not_aliased_to_self(changeset) do
aliased_tag = get_field(changeset, :aliased_tag)
id = get_field(changeset, :id)
case aliased_tag do
%{id: ^id} ->
add_error(changeset, :aliased_tag, "is the same tag as the source")
_tag ->
changeset
end
end
defp validate_alias_not_transitive(changeset) do
case get_field(changeset, :aliased_tag) do
%{aliased_tag_id: tag} when not is_nil(tag) ->
add_error(
changeset,
:aliased_tag,
"is itself aliased and would create a transitive alias"
)
_tag ->
changeset
end
end
defp validate_incoming_aliases(changeset) do
id = get_field(changeset, :id)
count =
Tag
|> where(aliased_tag_id: ^id)
|> Repo.aggregate(:count, :id)
if count > 0 do
add_error(changeset, :tag, "has incoming aliases and cannot be aliased")
else
changeset
end
end
end end

View file

@ -19,13 +19,15 @@ defmodule PhilomenaWeb.Tag.AliasController do
end end
def update(conn, %{"tag" => tag_params}) do def update(conn, %{"tag" => tag_params}) do
spawn(fn -> case Tags.alias_tag(conn.assigns.tag, tag_params) do
Tags.alias_tag(conn.assigns.tag, tag_params) {:ok, tag} ->
end) conn
|> put_flash(:info, "Tag alias queued.")
|> redirect(to: Routes.tag_alias_path(conn, :edit, tag))
conn {:error, changeset} ->
|> put_flash(:info, "Tag alias queued.") render(conn, "edit.html", changeset: changeset)
|> redirect(to: Routes.tag_alias_path(conn, :edit, conn.assigns.tag)) end
end end
def delete(conn, _params) do def delete(conn, _params) do

View file

@ -10,6 +10,8 @@ h1
.field .field
=> label f, "Alias target:" => label f, "Alias target:"
= text_input f, :target_tag, value: alias_target(@tag), class: "input" = text_input f, :target_tag, value: alias_target(@tag), class: "input"
= error_tag f, :tag
= error_tag f, :aliased_tag
.field .field
=> submit "Alias tag", class: "button" => submit "Alias tag", class: "button"