mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-01-19 22:27:59 +01:00
tag change mass reversion
This commit is contained in:
parent
563172f283
commit
81b5a58fab
5 changed files with 175 additions and 49 deletions
|
@ -7,18 +7,90 @@ defmodule Philomena.TagChanges do
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
|
|
||||||
alias Philomena.TagChanges.TagChange
|
alias Philomena.TagChanges.TagChange
|
||||||
|
alias Philomena.Images.Tagging
|
||||||
|
alias Philomena.Tags.Tag
|
||||||
|
alias Philomena.Images
|
||||||
|
|
||||||
@doc """
|
# TODO: this is substantially similar to Images.batch_update/4.
|
||||||
Returns the list of tag_changes.
|
# 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()
|
{added, removed} = Enum.split_with(tag_changes, & &1.added)
|
||||||
[%TagChange{}, ...]
|
|
||||||
|
|
||||||
"""
|
image_ids =
|
||||||
def list_tag_changes do
|
tag_changes
|
||||||
Repo.all(TagChange)
|
|> 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
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
|
|
|
@ -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
|
|
@ -260,6 +260,8 @@ defmodule PhilomenaWeb.Router do
|
||||||
resources "/reindex", Tag.ReindexController, only: [:create], singleton: true
|
resources "/reindex", Tag.ReindexController, only: [:create], singleton: true
|
||||||
end
|
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 "/pages", PageController, only: [:index, :new, :create, :edit, :update]
|
||||||
resources "/channels", ChannelController, only: [:new, :create, :edit, :update]
|
resources "/channels", ChannelController, only: [:new, :create, :edit, :update]
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,52 +1,62 @@
|
||||||
.block__header
|
.block__header
|
||||||
= @pagination
|
= @pagination
|
||||||
|
|
||||||
.block__content
|
= form_for :tag_changes, Routes.tag_change_revert_path(@conn, :create), fn _f ->
|
||||||
table.table
|
.block__content
|
||||||
thead
|
table.table
|
||||||
tr
|
thead
|
||||||
th colspan=2 Image
|
|
||||||
th Tag
|
|
||||||
th Action
|
|
||||||
th Timestamp
|
|
||||||
th User
|
|
||||||
|
|
||||||
tbody
|
|
||||||
= for tag_change <- @tag_changes do
|
|
||||||
tr
|
tr
|
||||||
td.center
|
= if reverts_tag_changes?(@conn) do
|
||||||
= link tag_change.image_id, to: Routes.image_path(@conn, :show, tag_change.image)
|
th Revert?
|
||||||
td.center
|
th colspan=2 Image
|
||||||
= render PhilomenaWeb.ImageView, "_image_container.html", image: tag_change.image, size: :thumb_tiny, conn: @conn
|
th Tag
|
||||||
|
th Action
|
||||||
|
th Timestamp
|
||||||
|
th User
|
||||||
|
|
||||||
td
|
tbody
|
||||||
= if tag_change.tag do
|
= for tag_change <- @tag_changes do
|
||||||
= render PhilomenaWeb.TagView, "_tag.html", tag: tag_change.tag, conn: @conn
|
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
|
- else
|
||||||
= tag_change.tag_name_cache || "Unknown tag"
|
td.danger Removed
|
||||||
|
|
||||||
= if tag_change.added do
|
|
||||||
td.success Added
|
|
||||||
- else
|
|
||||||
td.danger Removed
|
|
||||||
|
|
||||||
td
|
|
||||||
= pretty_time(tag_change.created_at)
|
|
||||||
|
|
||||||
td class=user_column_class(tag_change)
|
td
|
||||||
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: tag_change, conn: @conn
|
= pretty_time(tag_change.created_at)
|
||||||
|
|
||||||
= if can?(@conn, :show, :ip_address) do
|
td class=user_column_class(tag_change)
|
||||||
=> link_to_ip @conn, tag_change.ip
|
=> render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: tag_change, conn: @conn
|
||||||
=> link_to_fingerprint @conn, tag_change.fingerprint
|
|
||||||
|
|
||||||
= if staff?(tag_change) do
|
= if can?(@conn, :show, :ip_address) do
|
||||||
br
|
=> link_to_ip @conn, tag_change.ip
|
||||||
small
|
=> link_to_fingerprint @conn, tag_change.fingerprint
|
||||||
strong> Stop!
|
|
||||||
' This user is a staff member.
|
= if staff?(tag_change) do
|
||||||
br
|
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
|
.block__header
|
||||||
= @pagination
|
= @pagination
|
||||||
|
|
||||||
|
= if reverts_tag_changes?(@conn) do
|
||||||
|
= submit "Revert selected", class: "button", data: [confirm: "Are you really, really sure?"]
|
||||||
|
|
|
@ -10,4 +10,7 @@ defmodule PhilomenaWeb.TagChangeView do
|
||||||
false -> nil
|
false -> nil
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reverts_tag_changes?(conn),
|
||||||
|
do: can?(conn, :revert, Philomena.TagChanges.TagChange)
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue