tag change mass reversion

This commit is contained in:
byte[] 2019-12-22 00:09:01 -05:00
parent 563172f283
commit 81b5a58fab
5 changed files with 175 additions and 49 deletions

View file

@ -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 """

View file

@ -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

View file

@ -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

View file

@ -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?"]

View file

@ -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