From 81b5a58fab34fe72e9bd6adb3c749db8d0b5479c Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Sun, 22 Dec 2019 00:09:01 -0500 Subject: [PATCH] tag change mass reversion --- lib/philomena/tag_changes.ex | 88 ++++++++++++++++-- .../tag_change/revert_controller.ex | 39 ++++++++ lib/philomena_web/router.ex | 2 + .../templates/tag_change/index.html.slime | 92 ++++++++++--------- lib/philomena_web/views/tag_change_view.ex | 3 + 5 files changed, 175 insertions(+), 49 deletions(-) create mode 100644 lib/philomena_web/controllers/tag_change/revert_controller.ex diff --git a/lib/philomena/tag_changes.ex b/lib/philomena/tag_changes.ex index 287cd79a..4d96cf4a 100644 --- a/lib/philomena/tag_changes.ex +++ b/lib/philomena/tag_changes.ex @@ -7,18 +7,90 @@ defmodule Philomena.TagChanges do alias Philomena.Repo alias Philomena.TagChanges.TagChange + alias Philomena.Images.Tagging + alias Philomena.Tags.Tag + alias Philomena.Images - @doc """ - Returns the list of tag_changes. + # TODO: this is substantially similar to Images.batch_update/4. + # Perhaps it should be extracted. + def mass_revert(ids, attributes) do + now = NaiveDateTime.utc_now() |> NaiveDateTime.truncate(:second) + tag_change_attributes = Map.merge(attributes, %{created_at: now, updated_at: now}) + tag_attributes = %{name: "", slug: "", created_at: now, updated_at: now} - ## Examples + tag_changes = + TagChange + |> join(:inner, [tc], _ in assoc(tc, :image)) + |> where([tc, i], tc.id in ^ids and i.hidden_from_users == false) + |> order_by(desc: :created_at) + |> Repo.all() + |> Enum.reject(&is_nil(&1.tag_id)) + |> Enum.uniq_by(&{&1.image_id, &1.tag_id}) - iex> list_tag_changes() - [%TagChange{}, ...] + {added, removed} = Enum.split_with(tag_changes, & &1.added) - """ - def list_tag_changes do - Repo.all(TagChange) + image_ids = + tag_changes + |> Enum.map(& &1.image_id) + |> Enum.uniq() + + to_remove = + added + |> Enum.map(&{&1.image_id, &1.tag_id}) + |> Enum.reduce(Tagging, fn {image_id, tag_id}, q -> + or_where(q, image_id: ^image_id, tag_id: ^tag_id) + end) + |> select([t], [t.image_id, t.tag_id]) + + to_add = + Enum.map(removed, &%{image_id: &1.image_id, tag_id: &1.tag_id}) + + Repo.transaction(fn -> + {_count, inserted} = Repo.insert_all(Tagging, to_add, on_conflict: :nothing, returning: [:image_id, :tag_id]) + {_count, deleted} = Repo.delete_all(to_remove) + + 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) + + Repo.insert_all(TagChange, added_changes ++ removed_changes) + + # In order to merge into the existing tables here in one go, insert_all + # is used with a query that is guaranteed to conflict on every row by + # using the primary key. + + added_upserts = + inserted + |> Enum.group_by(fn [_image_id, tag_id] -> tag_id end) + |> Enum.map(fn {tag_id, instances} -> Map.merge(tag_attributes, %{id: tag_id, images_count: length(instances)}) end) + + removed_upserts = + deleted + |> Enum.group_by(fn [_image_id, tag_id] -> tag_id end) + |> Enum.map(fn {tag_id, instances} -> Map.merge(tag_attributes, %{id: tag_id, images_count: -length(instances)}) end) + + update_query = + update(Tag, inc: [images_count: fragment("EXCLUDED.images_count")]) + + upserts = added_upserts ++ removed_upserts + + Repo.insert_all(Tag, upserts, on_conflict: update_query, conflict_target: [:id]) + end) + |> case do + {:ok, _result} -> + Images.reindex_images(image_ids) + + {:ok, tag_changes} + + error -> + error + end end @doc """ diff --git a/lib/philomena_web/controllers/tag_change/revert_controller.ex b/lib/philomena_web/controllers/tag_change/revert_controller.ex new file mode 100644 index 00000000..9c23a33c --- /dev/null +++ b/lib/philomena_web/controllers/tag_change/revert_controller.ex @@ -0,0 +1,39 @@ +defmodule PhilomenaWeb.TagChange.RevertController do + use PhilomenaWeb, :controller + + alias Philomena.TagChanges.TagChange + alias Philomena.TagChanges + + plug :verify_authorized + plug PhilomenaWeb.UserAttributionPlug + + def create(conn, %{"ids" => ids}) when is_list(ids) do + attributes = conn.assigns.attributes + attributes = %{ + ip: attributes[:ip], + fingerprint: attributes[:fingerprint], + referrer: attributes[:referrer], + user_agent: attributes[:referrer], + user_id: attributes[:user].id + } + + case TagChanges.mass_revert(ids, attributes) do + {:ok, tag_changes} -> + conn + |> put_flash(:info, "Successfully reverted #{length(tag_changes)} tag changes.") + |> redirect(external: conn.assigns.referrer) + + _error -> + conn + |> put_flash(:error, "Couldn't revert those tag changes!") + |> redirect(external: conn.assigns.referrer) + end + end + + defp verify_authorized(conn, _params) do + case Canada.Can.can?(conn.assigns.current_user, :revert, TagChange) 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 799079e6..57e698b2 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -260,6 +260,8 @@ defmodule PhilomenaWeb.Router do resources "/reindex", Tag.ReindexController, only: [:create], singleton: true end + resources "/tag_changes/revert", TagChange.RevertController, as: :tag_change_revert, only: [:create], singleton: true + resources "/pages", PageController, only: [:index, :new, :create, :edit, :update] resources "/channels", ChannelController, only: [:new, :create, :edit, :update] end diff --git a/lib/philomena_web/templates/tag_change/index.html.slime b/lib/philomena_web/templates/tag_change/index.html.slime index 39326c54..5fef9aca 100644 --- a/lib/philomena_web/templates/tag_change/index.html.slime +++ b/lib/philomena_web/templates/tag_change/index.html.slime @@ -1,52 +1,62 @@ .block__header = @pagination -.block__content - table.table - thead - tr - th colspan=2 Image - th Tag - th Action - th Timestamp - th User - - tbody - = for tag_change <- @tag_changes do += form_for :tag_changes, Routes.tag_change_revert_path(@conn, :create), fn _f -> + .block__content + table.table + thead tr - td.center - = link tag_change.image_id, to: Routes.image_path(@conn, :show, tag_change.image) - td.center - = render PhilomenaWeb.ImageView, "_image_container.html", image: tag_change.image, size: :thumb_tiny, conn: @conn + = if reverts_tag_changes?(@conn) do + th Revert? + th colspan=2 Image + th Tag + th Action + th Timestamp + th User - td - = if tag_change.tag do - = render PhilomenaWeb.TagView, "_tag.html", tag: tag_change.tag, conn: @conn + tbody + = for tag_change <- @tag_changes do + tr + = if reverts_tag_changes?(@conn) do + td.center + input type="checkbox" name="ids[]" value=tag_change.id + + td.center + = link tag_change.image_id, to: Routes.image_path(@conn, :show, tag_change.image) + td.center + = render PhilomenaWeb.ImageView, "_image_container.html", image: tag_change.image, size: :thumb_tiny, conn: @conn + + td + = if tag_change.tag do + = render PhilomenaWeb.TagView, "_tag.html", tag: tag_change.tag, conn: @conn + - else + = tag_change.tag_name_cache || "Unknown tag" + + = if tag_change.added do + td.success Added - else - = tag_change.tag_name_cache || "Unknown tag" - - = if tag_change.added do - td.success Added - - else - td.danger Removed - - td - = pretty_time(tag_change.created_at) + td.danger Removed - td class=user_column_class(tag_change) - => render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: tag_change, conn: @conn + td + = pretty_time(tag_change.created_at) - = if can?(@conn, :show, :ip_address) do - => link_to_ip @conn, tag_change.ip - => link_to_fingerprint @conn, tag_change.fingerprint + td class=user_column_class(tag_change) + => render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: tag_change, conn: @conn - = if staff?(tag_change) do - br - small - strong> Stop! - ' This user is a staff member. + = if can?(@conn, :show, :ip_address) do + => link_to_ip @conn, tag_change.ip + => link_to_fingerprint @conn, tag_change.fingerprint + + = if staff?(tag_change) do br - ' Ask them before reverting their changes. + small + strong> Stop! + ' This user is a staff member. + br + ' Ask them before reverting their changes. -.block__header - = @pagination + .block__header + = @pagination + + = if reverts_tag_changes?(@conn) do + = submit "Revert selected", class: "button", data: [confirm: "Are you really, really sure?"] diff --git a/lib/philomena_web/views/tag_change_view.ex b/lib/philomena_web/views/tag_change_view.ex index 6ac18308..f3ffb411 100644 --- a/lib/philomena_web/views/tag_change_view.ex +++ b/lib/philomena_web/views/tag_change_view.ex @@ -10,4 +10,7 @@ defmodule PhilomenaWeb.TagChangeView do false -> nil end end + + def reverts_tag_changes?(conn), + do: can?(conn, :revert, Philomena.TagChanges.TagChange) end