diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index fc1d2b94..c0fb09fc 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -11,6 +11,7 @@ defmodule Philomena.Images do alias Philomena.Images.Image alias Philomena.Images.Hider alias Philomena.Images.Uploader + alias Philomena.Images.Tagging alias Philomena.ImageFeatures.ImageFeature alias Philomena.SourceChanges.SourceChange alias Philomena.TagChanges.TagChange @@ -224,7 +225,7 @@ defmodule Philomena.Images do Multi.new |> Multi.run(:image, fn repo, _chg -> image - |> repo.preload(:tags, force: true) + |> repo.preload(:tags) |> Image.tag_changeset(%{}, old_tags, new_tags) |> repo.update() |> case do @@ -363,6 +364,48 @@ defmodule Philomena.Images do end def unhide_image(image), do: {:ok, image} + def batch_update(image_ids, added_tags, removed_tags, tag_change_attributes) do + added_tags = Enum.map(added_tags, & &1.id) + removed_tags = Enum.map(removed_tags, & &1.id) + + # Change everything in one go, ignoring any validation errors + + # Note: computing the Cartesian product + insertions = + for tag_id <- added_tags, image_id <- image_ids do + %{tag_id: tag_id, image_id: image_id} + end + + deletions = + Tagging + |> where([t], t.image_id in ^image_ids and t.tag_id in ^removed_tags) + |> select([t], [t.image_id, t.tag_id]) + + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + tag_change_attributes = Map.merge(tag_change_attributes, %{created_at: now, updated_at: now}) + + Repo.transaction fn -> + {added_count, inserted} = Repo.insert_all(Tagging, insertions, on_conflict: :nothing, returning: [:image_id, :tag_id]) + {removed_count, deleted} = Repo.delete_all(deletions) + + inserted = Enum.map(inserted, &[&1.image_id, &1.tag_id]) + + added_changes = Enum.map(inserted, fn [image_id, tag_id] -> + Map.merge(tag_change_attributes, %{image_id: image_id, tag_id: tag_id, added: true}) + end) + + removed_changes = Enum.map(deleted, fn [image_id, tag_id] -> + Map.merge(tag_change_attributes, %{image_id: image_id, tag_id: tag_id, added: false}) + end) + + changes = added_changes ++ removed_changes + + Repo.insert_all(TagChange, changes) + Repo.update_all(where(Tag, [t], t.id in ^added_tags), inc: [images_count: added_count]) + Repo.update_all(where(Tag, [t], t.id in ^removed_tags), inc: [images_count: -removed_count]) + end + end + @doc """ Deletes a Image. diff --git a/lib/philomena/users/ability.ex b/lib/philomena/users/ability.ex index 0d767e38..c3ff4f33 100644 --- a/lib/philomena/users/ability.ex +++ b/lib/philomena/users/ability.ex @@ -147,6 +147,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do # Users and anonymous users can... # + # Batch tag + def can?(%User{role_map: %{"Tag" => "batch_update"}}, :batch_update, Tag), do: true + # Edit their description and personal title def can?(%User{id: id}, :edit_description, %User{id: id}), do: true def can?(%User{id: id}, :edit_title, %User{id: id}), do: true diff --git a/lib/philomena_web/controllers/admin/batch/tag_controller.ex b/lib/philomena_web/controllers/admin/batch/tag_controller.ex new file mode 100644 index 00000000..7e4677bf --- /dev/null +++ b/lib/philomena_web/controllers/admin/batch/tag_controller.ex @@ -0,0 +1,61 @@ +defmodule PhilomenaWeb.Admin.Batch.TagController do + use PhilomenaWeb, :controller + + alias Philomena.Tags.Tag + alias Philomena.Tags + alias Philomena.Images + alias Philomena.Repo + import Ecto.Query + + plug :verify_authorized + plug PhilomenaWeb.UserAttributionPlug + + def update(conn, %{"tags" => tags, "image_ids" => image_ids}) do + tags = Tag.parse_tag_list(tags) + + added_tag_names = Enum.reject(tags, &String.starts_with?(&1, "-")) + removed_tag_names = + tags + |> Enum.filter(&String.starts_with?(&1, "-")) + |> Enum.map(&String.slice(&1, 1..-1)) + + added_tags = + Tag + |> where([t], t.name in ^added_tag_names) + |> Repo.all() + + removed_tags = + Tag + |> where([t], t.name in ^removed_tag_names) + |> Repo.all() + + attributes = conn.assigns.attributes + attributes = %{ + ip: attributes[:ip], + fingerprint: attributes[:fingerprint], + user_agent: attributes[:user_agent], + referrer: attributes[:referrer], + user_id: attributes[:user].id + } + + image_ids = Enum.map(image_ids, &String.to_integer/1) + + case Images.batch_update(image_ids, added_tags, removed_tags, attributes) do + {:ok, _} -> + Images.reindex_images(image_ids) + Tags.reindex_tags(added_tags ++ removed_tags) + + json(conn, %{succeeded: image_ids, failed: []}) + + _error -> + json(conn, %{succeeded: [], failed: image_ids}) + end + end + + defp verify_authorized(conn, _opts) do + case Canada.Can.can?(conn.assigns.current_user, :batch_update, Tag) do + true -> conn + _false -> PhilomenaWeb.NotAuthorizedPlug.call(conn) + end + end +end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 219eec95..ea0747c8 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -220,6 +220,8 @@ defmodule PhilomenaWeb.Router do resources "/users", UserController, only: [:index, :edit, :update] do resources "/avatar", User.AvatarController, only: [:delete], singleton: true end + + resources "/batch/tags", Batch.TagController, only: [:update], singleton: true end resources "/duplicate_reports", DuplicateReportController, only: [] do diff --git a/lib/philomena_web/templates/image/_quick_tag.html.slime b/lib/philomena_web/templates/image/_quick_tag.html.slime new file mode 100644 index 00000000..9483853a --- /dev/null +++ b/lib/philomena_web/templates/image/_quick_tag.html.slime @@ -0,0 +1,9 @@ +a.js-quick-tag href="#" title="Add tags to the images on this page" + i.fa.fa-tags + span.hide-mobile.hide-limited-desktop<> Tag +a.js-quick-tag--abort.hidden href="#" + i.fa.fa-exclamation-triangle + span.hide-mobile.hide-limited-desktop<> Abort Tagging +a.js-quick-tag--submit.hidden href="#" + i.fa.fa-tags + span.hide-mobile.hide-limited-desktop<> Submit Tag Changes diff --git a/lib/philomena_web/templates/image/index.html.slime b/lib/philomena_web/templates/image/index.html.slime index cb6583e9..ac425361 100644 --- a/lib/philomena_web/templates/image/index.html.slime +++ b/lib/philomena_web/templates/image/index.html.slime @@ -12,8 +12,12 @@ elixir: section.block__header.flex span.block__header__title.hide-mobile => header + = pagination + .flex__right + = quick_tag @conn + = info_row @conn, tags .block__content.js-resizable-media-container diff --git a/lib/philomena_web/views/image_view.ex b/lib/philomena_web/views/image_view.ex index 696b60fb..a4a27c43 100644 --- a/lib/philomena_web/views/image_view.ex +++ b/lib/philomena_web/views/image_view.ex @@ -156,6 +156,12 @@ defmodule PhilomenaWeb.ImageView do render PhilomenaWeb.TagView, "_tags_row.html", conn: conn, tags: tags end + def quick_tag(conn) do + if can?(conn, :batch_update, Tag) do + render PhilomenaWeb.ImageView, "_quick_tag.html", conn: conn + end + end + def deleter(%{deleter: %{name: name}}), do: name def deleter(_image), do: "System"