mirror of
https://github.com/philomena-dev/philomena.git
synced 2025-02-21 12:53:11 +01:00
image hiding
This commit is contained in:
parent
756d2c91a6
commit
f9f6518d97
11 changed files with 270 additions and 12 deletions
|
@ -9,12 +9,14 @@ defmodule Philomena.Images do
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
|
|
||||||
alias Philomena.Images.Image
|
alias Philomena.Images.Image
|
||||||
|
alias Philomena.Images.Hider
|
||||||
alias Philomena.Images.Uploader
|
alias Philomena.Images.Uploader
|
||||||
alias Philomena.SourceChanges.SourceChange
|
alias Philomena.SourceChanges.SourceChange
|
||||||
alias Philomena.TagChanges.TagChange
|
alias Philomena.TagChanges.TagChange
|
||||||
alias Philomena.Tags
|
alias Philomena.Tags
|
||||||
alias Philomena.Tags.Tag
|
alias Philomena.Tags.Tag
|
||||||
alias Philomena.Notifications
|
alias Philomena.Notifications
|
||||||
|
alias Philomena.Interactions
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Gets a single image.
|
Gets a single image.
|
||||||
|
@ -199,6 +201,73 @@ defmodule Philomena.Images do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hide_image(%Image{} = image, user, attrs) do
|
||||||
|
Image.hide_changeset(image, attrs, user)
|
||||||
|
|> internal_hide_image()
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_image(%Image{} = image, duplicate_of_image) do
|
||||||
|
result =
|
||||||
|
Image.merge_changeset(image, duplicate_of_image)
|
||||||
|
|> internal_hide_image()
|
||||||
|
|
||||||
|
case result do
|
||||||
|
{:ok, _changes} ->
|
||||||
|
Interactions.migrate_interactions(image, duplicate_of_image)
|
||||||
|
result
|
||||||
|
|
||||||
|
_error ->
|
||||||
|
result
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp internal_hide_image(changeset) do
|
||||||
|
Multi.new
|
||||||
|
|> Multi.update(:image, changeset)
|
||||||
|
|> Multi.run(:tags, fn repo, %{image: image} ->
|
||||||
|
image = Repo.preload(image, :tags, force: true)
|
||||||
|
|
||||||
|
# I'm not convinced this is a good idea. It leads
|
||||||
|
# to way too much drift, and the index has to be
|
||||||
|
# maintained.
|
||||||
|
tag_ids = Enum.map(image.tags, & &1.id)
|
||||||
|
query = where(Tag, [t], t.id in ^tag_ids)
|
||||||
|
|
||||||
|
repo.update_all(query, inc: [images_count: -1])
|
||||||
|
|
||||||
|
{:ok, image.tags}
|
||||||
|
end)
|
||||||
|
|> Multi.run(:file, fn _repo, %{image: image} ->
|
||||||
|
Hider.hide_thumbnails(image, image.hidden_image_key)
|
||||||
|
|
||||||
|
{:ok, nil}
|
||||||
|
end)
|
||||||
|
|> Repo.isolated_transaction(:serializable)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unhide_image(%Image{} = image) do
|
||||||
|
key = image.hidden_image_key
|
||||||
|
|
||||||
|
Multi.new
|
||||||
|
|> Multi.update(:image, Image.unhide_changeset(image))
|
||||||
|
|> Multi.run(:tags, fn repo, %{image: image} ->
|
||||||
|
image = Repo.preload(image, :tags, force: true)
|
||||||
|
|
||||||
|
tag_ids = Enum.map(image.tags, & &1.id)
|
||||||
|
query = where(Tag, [t], t.id in ^tag_ids)
|
||||||
|
|
||||||
|
repo.update_all(query, inc: [images_count: 1])
|
||||||
|
|
||||||
|
{:ok, image.tags}
|
||||||
|
end)
|
||||||
|
|> Multi.run(:file, fn _repo, %{image: image} ->
|
||||||
|
Hider.unhide_thumbnails(image, key)
|
||||||
|
|
||||||
|
{:ok, nil}
|
||||||
|
end)
|
||||||
|
|> Repo.isolated_transaction(:serializable)
|
||||||
|
end
|
||||||
|
|
||||||
@doc """
|
@doc """
|
||||||
Deletes a Image.
|
Deletes a Image.
|
||||||
|
|
||||||
|
|
34
lib/philomena/images/hider.ex
Normal file
34
lib/philomena/images/hider.ex
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
defmodule Philomena.Images.Hider do
|
||||||
|
@moduledoc """
|
||||||
|
Hiding logic for images.
|
||||||
|
"""
|
||||||
|
|
||||||
|
alias Philomena.Images.Image
|
||||||
|
|
||||||
|
def hide_thumbnails(image, key) do
|
||||||
|
source = image_thumb_dir(image)
|
||||||
|
target = image_thumb_dir(image, key)
|
||||||
|
|
||||||
|
File.rename!(source, target)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unhide_thumbnails(image, key) do
|
||||||
|
source = image_thumb_dir(image, key)
|
||||||
|
target = image_thumb_dir(image)
|
||||||
|
|
||||||
|
File.rename!(source, target)
|
||||||
|
end
|
||||||
|
|
||||||
|
# fixme: these are copied from the thumbnailer
|
||||||
|
defp image_thumb_dir(%Image{created_at: created_at, id: id}),
|
||||||
|
do: Path.join([image_thumbnail_root(), time_identifier(created_at), to_string(id)])
|
||||||
|
|
||||||
|
defp image_thumb_dir(%Image{created_at: created_at, id: id}, key),
|
||||||
|
do: Path.join([image_thumbnail_root(), time_identifier(created_at), to_string(id) <> "-" <> key])
|
||||||
|
|
||||||
|
defp time_identifier(time),
|
||||||
|
do: Enum.join([time.year, time.month, time.day], "/")
|
||||||
|
|
||||||
|
defp image_thumbnail_root,
|
||||||
|
do: Application.get_env(:philomena, :image_file_root) <> "/thumbs"
|
||||||
|
end
|
|
@ -179,6 +179,30 @@ defmodule Philomena.Images.Image do
|
||||||
|> validate_length(:description, max: 50_000, count: :bytes)
|
|> validate_length(:description, max: 50_000, count: :bytes)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def hide_changeset(image, attrs, user) do
|
||||||
|
image
|
||||||
|
|> cast(attrs, [:deletion_reason])
|
||||||
|
|> put_change(:deleter_id, user.id)
|
||||||
|
|> put_change(:hidden_image_key, create_key())
|
||||||
|
|> put_change(:hidden_from_users, true)
|
||||||
|
|> validate_required([:deletion_reason, :deleter_id])
|
||||||
|
end
|
||||||
|
|
||||||
|
def merge_changeset(image, duplicate_of_image) do
|
||||||
|
change(image)
|
||||||
|
|> put_change(:duplicate_id, duplicate_of_image.id)
|
||||||
|
|> put_change(:hidden_image_key, create_key())
|
||||||
|
|> put_change(:hidden_from_users, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unhide_changeset(image) do
|
||||||
|
change(image)
|
||||||
|
|> put_change(:deleter_id, nil)
|
||||||
|
|> put_change(:hidden_image_key, nil)
|
||||||
|
|> put_change(:hidden_from_users, false)
|
||||||
|
|> put_change(:deletion_reason, nil)
|
||||||
|
end
|
||||||
|
|
||||||
def cache_changeset(image) do
|
def cache_changeset(image) do
|
||||||
changeset = change(image)
|
changeset = change(image)
|
||||||
image = apply_changes(changeset)
|
image = apply_changes(changeset)
|
||||||
|
@ -225,4 +249,8 @@ defmodule Philomena.Images.Image do
|
||||||
|
|
||||||
{tag_list_cache, tag_list_plus_alias_cache, file_name_cache}
|
{tag_list_cache, tag_list_plus_alias_cache, file_name_cache}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp create_key do
|
||||||
|
Base.encode16(:crypto.strong_rand_bytes(6), case: :lower)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,9 @@ defmodule Philomena.Interactions do
|
||||||
alias Philomena.ImageHides.ImageHide
|
alias Philomena.ImageHides.ImageHide
|
||||||
alias Philomena.ImageFaves.ImageFave
|
alias Philomena.ImageFaves.ImageFave
|
||||||
alias Philomena.ImageVotes.ImageVote
|
alias Philomena.ImageVotes.ImageVote
|
||||||
|
alias Philomena.Images.Image
|
||||||
alias Philomena.Repo
|
alias Philomena.Repo
|
||||||
|
alias Ecto.Multi
|
||||||
|
|
||||||
def user_interactions(_images, nil),
|
def user_interactions(_images, nil),
|
||||||
do: []
|
do: []
|
||||||
|
@ -53,6 +55,46 @@ defmodule Philomena.Interactions do
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def migrate_interactions(source, target) do
|
||||||
|
now = DateTime.utc_now()
|
||||||
|
source = Repo.preload(source, [:hiders, :favers, :upvoters, :downvoters])
|
||||||
|
|
||||||
|
new_hides = Enum.map(source.hiders, &%{image_id: target.id, user_id: &1.id, created_at: now})
|
||||||
|
new_faves = Enum.map(source.favers, &%{image_id: target.id, user_id: &1.id, created_at: now})
|
||||||
|
new_upvotes = Enum.map(source.upvoters, &%{image_id: target.id, user_id: &1.id, created_at: now, up: true})
|
||||||
|
new_downvotes = Enum.map(source.downvoters, &%{image_id: target.id, user_id: &1.id, created_at: now, up: false})
|
||||||
|
|
||||||
|
Multi.new
|
||||||
|
|> Multi.run(:hides, fn repo, %{} ->
|
||||||
|
{count, nil} = repo.insert_all(ImageHide, new_hides, on_conflict: :nothing)
|
||||||
|
|
||||||
|
{:ok, count}
|
||||||
|
end)
|
||||||
|
|> Multi.run(:faves, fn repo, %{} ->
|
||||||
|
{count, nil} = repo.insert_all(ImageFave, new_faves, on_conflict: :nothing)
|
||||||
|
|
||||||
|
{:ok, count}
|
||||||
|
end)
|
||||||
|
|> Multi.run(:upvotes, fn repo, %{} ->
|
||||||
|
{count, nil} = repo.insert_all(ImageVote, new_upvotes, on_conflict: :nothing)
|
||||||
|
|
||||||
|
{:ok, count}
|
||||||
|
end)
|
||||||
|
|> Multi.run(:downvotes, fn repo, %{} ->
|
||||||
|
{count, nil} = repo.insert_all(ImageVote, new_downvotes, on_conflict: :nothing)
|
||||||
|
|
||||||
|
{:ok, count}
|
||||||
|
end)
|
||||||
|
|> Multi.run(:image, fn repo, %{hides: hides, faves: faves, upvotes: upvotes, downvotes: downvotes} ->
|
||||||
|
image_query = where(Image, id: ^target.id)
|
||||||
|
|
||||||
|
repo.update_all(image_query, inc: [hides: hides, faves: faves, upvotes: upvotes, downvotes: downvotes, score: upvotes - downvotes])
|
||||||
|
|
||||||
|
{:ok, nil}
|
||||||
|
end)
|
||||||
|
|> Repo.isolated_transaction(:serializable)
|
||||||
|
end
|
||||||
|
|
||||||
defp union_all_queries([query]),
|
defp union_all_queries([query]),
|
||||||
do: query
|
do: query
|
||||||
defp union_all_queries([query | rest]),
|
defp union_all_queries([query | rest]),
|
||||||
|
|
43
lib/philomena_web/controllers/image/delete_controller.ex
Normal file
43
lib/philomena_web/controllers/image/delete_controller.ex
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
defmodule PhilomenaWeb.Image.DeleteController do
|
||||||
|
use PhilomenaWeb, :controller
|
||||||
|
|
||||||
|
# N.B.: this would be Image.Hide, because it hides the image, but that is
|
||||||
|
# taken by the user action
|
||||||
|
|
||||||
|
alias Philomena.Images.Image
|
||||||
|
alias Philomena.Images
|
||||||
|
alias Philomena.Tags
|
||||||
|
|
||||||
|
plug PhilomenaWeb.CanaryMapPlug, create: :hide, delete: :hide
|
||||||
|
plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true
|
||||||
|
|
||||||
|
def create(conn, %{"image" => image_params}) do
|
||||||
|
image = conn.assigns.image
|
||||||
|
user = conn.assigns.current_user
|
||||||
|
|
||||||
|
case Images.hide_image(image, user, image_params) do
|
||||||
|
{:ok, %{image: image, tags: tags}} ->
|
||||||
|
Images.reindex_image(image)
|
||||||
|
Tags.reindex_tags(tags)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_flash(:info, "Image successfully hidden.")
|
||||||
|
|> redirect(to: Routes.image_path(conn, :show, image))
|
||||||
|
|
||||||
|
{:error, :image, changeset, _changes} ->
|
||||||
|
render(conn, "new.html", image: image, changeset: changeset)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete(conn, _params) do
|
||||||
|
image = conn.assigns.image
|
||||||
|
|
||||||
|
{:ok, %{image: image, tags: tags}} = Images.unhide_image(image)
|
||||||
|
Images.reindex_image(image)
|
||||||
|
Tags.reindex_tags(tags)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> put_flash(:info, "Image successfully unhidden.")
|
||||||
|
|> redirect(to: Routes.image_path(conn, :show, image))
|
||||||
|
end
|
||||||
|
end
|
|
@ -73,9 +73,7 @@ defmodule PhilomenaWeb.ImageController do
|
||||||
|
|
||||||
{user_galleries, image_galleries} = image_and_user_galleries(image, conn.assigns.current_user)
|
{user_galleries, image_galleries} = image_and_user_galleries(image, conn.assigns.current_user)
|
||||||
|
|
||||||
render(
|
assigns = [
|
||||||
conn,
|
|
||||||
"show.html",
|
|
||||||
image: image,
|
image: image,
|
||||||
comments: comments,
|
comments: comments,
|
||||||
image_changeset: image_changeset,
|
image_changeset: image_changeset,
|
||||||
|
@ -86,7 +84,13 @@ defmodule PhilomenaWeb.ImageController do
|
||||||
interactions: interactions,
|
interactions: interactions,
|
||||||
watching: watching,
|
watching: watching,
|
||||||
layout_class: "layout--wide"
|
layout_class: "layout--wide"
|
||||||
)
|
]
|
||||||
|
|
||||||
|
if image.hidden_from_users do
|
||||||
|
render(conn, "deleted.html", assigns)
|
||||||
|
else
|
||||||
|
render(conn, "show.html", assigns)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def new(conn, _params) do
|
def new(conn, _params) do
|
||||||
|
@ -157,9 +161,6 @@ defmodule PhilomenaWeb.ImageController do
|
||||||
|> put_flash(:info, "The image you were looking for has been marked a duplicate of the image below")
|
|> put_flash(:info, "The image you were looking for has been marked a duplicate of the image below")
|
||||||
|> redirect(to: Routes.image_path(conn, :show, image.duplicate_id))
|
|> redirect(to: Routes.image_path(conn, :show, image.duplicate_id))
|
||||||
|
|
||||||
image.hidden_from_users ->
|
|
||||||
render(conn, "deleted.html", image: image)
|
|
||||||
|
|
||||||
true ->
|
true ->
|
||||||
conn
|
conn
|
||||||
|> assign(:image, image)
|
|> assign(:image, image)
|
||||||
|
|
|
@ -105,6 +105,7 @@ defmodule PhilomenaWeb.Router do
|
||||||
resources "/subscription", Image.SubscriptionController, only: [:create, :delete], singleton: true
|
resources "/subscription", Image.SubscriptionController, only: [:create, :delete], singleton: true
|
||||||
resources "/read", Image.ReadController, only: [:create], singleton: true
|
resources "/read", Image.ReadController, only: [:create], singleton: true
|
||||||
resources "/comments", Image.CommentController, only: [:edit, :update]
|
resources "/comments", Image.CommentController, only: [:edit, :update]
|
||||||
|
resources "/delete", Image.DeleteController, only: [:create, :delete], singleton: true
|
||||||
end
|
end
|
||||||
|
|
||||||
resources "/forums", ForumController, only: [] do
|
resources "/forums", ForumController, only: [] do
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
=< link("your current filter", to: Routes.filter_path(@conn, :show, @conn.assigns.current_filter), class: "filter-link")
|
=< link("your current filter", to: Routes.filter_path(@conn, :show, @conn.assigns.current_filter), class: "filter-link")
|
||||||
' .
|
' .
|
||||||
|
|
||||||
#image_target.hidden.image-show data-scaled="true" data-uris=Jason.encode!(thumb_urls(@image, false)) data-width=@image.image_width data-height=@image.image_height
|
#image_target.hidden.image-show data-scaled="true" data-uris=Jason.encode!(thumb_urls(@image, can?(@conn, :hide, @image))) data-width=@image.image_width data-height=@image.image_height
|
||||||
= if @image.image_mime_type == "video/webm" do
|
= if @image.image_mime_type == "video/webm" do
|
||||||
video controls=true
|
video controls=true
|
||||||
- else
|
- else
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
- display_mod_tools? = can?(@conn, :hide, @image)
|
||||||
|
|
||||||
#image_options_area
|
#image_options_area
|
||||||
.block__header.block__header--js-tabbed
|
.block__header.block__header--js-tabbed
|
||||||
a href="#" data-click-tab="reporting" data-load-tab=Routes.image_reporting_path(@conn, :show, @image)
|
a href="#" data-click-tab="reporting" data-load-tab=Routes.image_reporting_path(@conn, :show, @image)
|
||||||
|
@ -9,6 +11,16 @@
|
||||||
a href="#" data-click-tab="favoriters" data-load-tab=Routes.image_favorites_path(@conn, :index, @image)
|
a href="#" data-click-tab="favoriters" data-load-tab=Routes.image_favorites_path(@conn, :index, @image)
|
||||||
i.fa.fa-star>
|
i.fa.fa-star>
|
||||||
| List favoriters
|
| List favoriters
|
||||||
|
= if display_mod_tools? do
|
||||||
|
a href="#" data-click-tab="replace"
|
||||||
|
i.fa.fa-upload>
|
||||||
|
| Replace
|
||||||
|
a href="#" data-click-tab="administration"
|
||||||
|
i.fa.fa-toolbox>
|
||||||
|
| Administrate
|
||||||
|
= if present?(@image.scratchpad) do
|
||||||
|
i.fa.fa-sticky-note.fa--important<
|
||||||
|
i.fa.fa-exclamation.fa--important
|
||||||
|
|
||||||
.block__tab.hidden data-tab="favoriters"
|
.block__tab.hidden data-tab="favoriters"
|
||||||
p Loading...
|
p Loading...
|
||||||
|
@ -53,3 +65,28 @@
|
||||||
br
|
br
|
||||||
textarea.input.input--wide.input--separate-top#bbcode_embed_thumbnail_tag rows="2" cols="100" readonly="readonly"
|
textarea.input.input--wide.input--separate-top#bbcode_embed_thumbnail_tag rows="2" cols="100" readonly="readonly"
|
||||||
= "[img]#{thumb_url(@image, false, :medium)}[/img]\n[url=#{Routes.image_url(@conn, :show, @image)}]View on Derpibooru[/url]#{source_link}"
|
= "[img]#{thumb_url(@image, false, :medium)}[/img]\n[url=#{Routes.image_url(@conn, :show, @image)}]View on Derpibooru[/url]#{source_link}"
|
||||||
|
= if display_mod_tools? do
|
||||||
|
.block__tab.hidden data-tab="replace"
|
||||||
|
/= form_tag image_file_path(@image), method: :put, multipart: true do
|
||||||
|
= render partial: 'layouts/image_upload', locals: { form: nil, field: :image }
|
||||||
|
= submit_tag 'Save changes', class: 'button', autocomplete: 'off', data: { disable_with: 'Replacing...' }
|
||||||
|
|
||||||
|
.block__tab.hidden data-tab="administration"
|
||||||
|
.block.block--danger
|
||||||
|
a.button.button--link> href="#"
|
||||||
|
i.far.fa-edit
|
||||||
|
= if present?(@image.scratchpad) do
|
||||||
|
strong> Mod notes:
|
||||||
|
= escape_nl2br @image.scratchpad
|
||||||
|
- else
|
||||||
|
em No mod notes present
|
||||||
|
|
||||||
|
= if not @image.hidden_from_users do
|
||||||
|
= form_for @changeset, Routes.image_delete_path(@conn, :create, @image), [method: "post"], fn f ->
|
||||||
|
= label f, :deletion_reason, "Deletion reason (cannot be empty)"
|
||||||
|
.field.field--inline
|
||||||
|
= text_input f, :deletion_reason, class: "input input--wide", placeholder: "Rule violation", required: true
|
||||||
|
= submit "Delete", class: "button button--state-danger button--separate-left"
|
||||||
|
|
||||||
|
- else
|
||||||
|
= button_to "Restore", Routes.image_delete_path(@conn, :delete, @image), method: "delete", class: "button button--state-success"
|
|
@ -6,7 +6,7 @@
|
||||||
strong
|
strong
|
||||||
= @image.deletion_reason || "Unknown (likely deleted in error). Please contact a moderator."
|
= @image.deletion_reason || "Unknown (likely deleted in error). Please contact a moderator."
|
||||||
|
|
||||||
= if can?(@conn, :undelete, @image) do
|
= if can?(@conn, :hide, @image) do
|
||||||
p
|
p
|
||||||
strong> Spoilers!
|
strong> Spoilers!
|
||||||
' Done by:
|
' Done by:
|
||||||
|
@ -21,3 +21,6 @@
|
||||||
' and
|
' and
|
||||||
= link "rules of the site", to: "/pages/rules"
|
= link "rules of the site", to: "/pages/rules"
|
||||||
' . Other useful links can be found at the bottom of the page.
|
' . Other useful links can be found at the bottom of the page.
|
||||||
|
|
||||||
|
= if can?(@conn, :hide, @image) do
|
||||||
|
= render PhilomenaWeb.ImageView, "show.html", assigns
|
|
@ -11,7 +11,7 @@
|
||||||
|
|
||||||
= render PhilomenaWeb.ImageView, "_tags.html", image: @image, tag_change_count: @tag_change_count, changeset: @image_changeset, conn: @conn
|
= render PhilomenaWeb.ImageView, "_tags.html", image: @image, tag_change_count: @tag_change_count, changeset: @image_changeset, conn: @conn
|
||||||
= render PhilomenaWeb.ImageView, "_source.html", image: @image, source_change_count: @source_change_count, changeset: @image_changeset, conn: @conn
|
= render PhilomenaWeb.ImageView, "_source.html", image: @image, source_change_count: @source_change_count, changeset: @image_changeset, conn: @conn
|
||||||
= render PhilomenaWeb.ImageView, "_options.html", image: @image, conn: @conn
|
= render PhilomenaWeb.ImageView, "_options.html", image: @image, changeset: @image_changeset, conn: @conn
|
||||||
|
|
||||||
h4 Comments
|
h4 Comments
|
||||||
= cond do
|
= cond do
|
||||||
|
|
Loading…
Reference in a new issue