diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index c51c8747..fc1d2b94 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.ImageFeatures.ImageFeature alias Philomena.SourceChanges.SourceChange alias Philomena.TagChanges.TagChange alias Philomena.Tags @@ -84,6 +85,91 @@ defmodule Philomena.Images do |> Repo.isolated_transaction(:serializable) end + def feature_image(featurer, %Image{} = image) do + image = Repo.preload(image, :tags) + [featured] = Tags.get_or_create_tags("featured image") + + feature = + %ImageFeature{user_id: featurer.id, image_id: image.id} + |> ImageFeature.changeset(%{}) + + image = + image + |> Image.tag_changeset(%{}, image.tags, [featured | image.tags]) + |> Image.cache_changeset() + + Multi.new() + |> Multi.insert(:feature, feature) + |> Multi.update(:image, image) + |> Multi.run(:added_tag_count, fn repo, %{image: image} -> + tag_ids = image.added_tags |> Enum.map(& &1.id) + tags = Tag |> where([t], t.id in ^tag_ids) + + {count, nil} = repo.update_all(tags, inc: [images_count: 1]) + + {:ok, count} + end) + |> Repo.isolated_transaction(:serializable) + end + + def lock_comments(%Image{} = image, locked) do + image + |> Image.lock_comments_changeset(locked) + |> Repo.update() + end + + def lock_description(%Image{} = image, locked) do + image + |> Image.lock_description_changeset(locked) + |> Repo.update() + end + + def lock_tags(%Image{} = image, locked) do + image + |> Image.lock_tags_changeset(locked) + |> Repo.update() + end + + def remove_hash(%Image{} = image) do + image + |> Image.remove_hash_changeset() + |> Repo.update() + end + + def update_scratchpad(%Image{} = image, attrs) do + image + |> Image.scratchpad_changeset(attrs) + |> Repo.update() + end + + def remove_source_history(%Image{} = image) do + image + |> Repo.preload(:source_changes) + |> Image.remove_source_history_changeset() + |> Repo.update() + end + + def repair_image(%Image{} = image) do + Philomena.Images.Thumbnailer.generate_thumbnails(image.id) + end + + def update_file(%Image{} = image, attrs) do + image = + image + |> Image.changeset(attrs) + |> Uploader.analyze_upload(attrs) + + Multi.new() + |> Multi.update(:image, image) + |> Multi.run(:after, fn _repo, %{image: image} -> + Uploader.persist_upload(image) + Uploader.unpersist_old_upload(image) + + {:ok, nil} + end) + |> Repo.isolated_transaction(:serializable) + end + @doc """ Updates a image. diff --git a/lib/philomena/images/image.ex b/lib/philomena/images/image.ex index fa707ce8..b4668142 100644 --- a/lib/philomena/images/image.ex +++ b/lib/philomena/images/image.ex @@ -35,7 +35,7 @@ defmodule Philomena.Images.Image do has_many :hides, ImageHide has_many :gallery_interactions, Galleries.Interaction has_many :subscriptions, Subscription - has_many :source_changes, SourceChange + has_many :source_changes, SourceChange, on_replace: :delete has_many :tag_changes, TagChange has_many :upvoters, through: [:upvotes, :user] has_many :downvoters, through: [:downvotes, :user] @@ -204,6 +204,32 @@ defmodule Philomena.Images.Image do |> put_change(:duplicate_id, nil) end + def lock_comments_changeset(image, locked) do + change(image, commenting_allowed: not locked) + end + + def lock_description_changeset(image, locked) do + change(image, description_editing_allowed: not locked) + end + + def lock_tags_changeset(image, locked) do + change(image, tag_editing_allowed: not locked) + end + + def remove_hash_changeset(image) do + change(image, image_orig_sha512_hash: nil) + end + + def scratchpad_changeset(image, attrs) do + cast(image, attrs, [:scratchpad]) + end + + def remove_source_history_changeset(image) do + change(image) + |> put_change(:source_url, nil) + |> put_assoc(:source_changes, []) + end + def cache_changeset(image) do changeset = change(image) image = apply_changes(changeset) diff --git a/lib/philomena_web/controllers/image/comment_lock_controller.ex b/lib/philomena_web/controllers/image/comment_lock_controller.ex new file mode 100644 index 00000000..fc47461c --- /dev/null +++ b/lib/philomena_web/controllers/image/comment_lock_controller.ex @@ -0,0 +1,25 @@ +defmodule PhilomenaWeb.Image.CommentLockController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, create: :hide, delete: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + + def create(conn, _params) do + {:ok, image} = Images.lock_comments(conn.assigns.image, true) + + conn + |> put_flash(:info, "Successfully locked comments.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end + + def delete(conn, _params) do + {:ok, image} = Images.lock_comments(conn.assigns.image, false) + + conn + |> put_flash(:info, "Successfully unlocked comments.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end +end diff --git a/lib/philomena_web/controllers/image/description_lock_controller.ex b/lib/philomena_web/controllers/image/description_lock_controller.ex new file mode 100644 index 00000000..f509370c --- /dev/null +++ b/lib/philomena_web/controllers/image/description_lock_controller.ex @@ -0,0 +1,25 @@ +defmodule PhilomenaWeb.Image.DescriptionLockController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, create: :hide, delete: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + + def create(conn, _params) do + {:ok, image} = Images.lock_description(conn.assigns.image, true) + + conn + |> put_flash(:info, "Successfully locked description.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end + + def delete(conn, _params) do + {:ok, image} = Images.lock_description(conn.assigns.image, false) + + conn + |> put_flash(:info, "Successfully unlocked description.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end +end diff --git a/lib/philomena_web/controllers/image/feature_controller.ex b/lib/philomena_web/controllers/image/feature_controller.ex new file mode 100644 index 00000000..b2ddb33e --- /dev/null +++ b/lib/philomena_web/controllers/image/feature_controller.ex @@ -0,0 +1,32 @@ +defmodule PhilomenaWeb.Image.FeatureController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + alias Philomena.Tags + + plug PhilomenaWeb.CanaryMapPlug, create: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + plug :verify_not_deleted + + def create(conn, _params) do + {:ok, %{image: image}} = Images.feature_image(conn.assigns.current_user, conn.assigns.image) + Tags.reindex_tags(image.added_tags) + + conn + |> put_flash(:info, "Image marked as featured image.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end + + defp verify_not_deleted(conn, _opts) do + case conn.assigns.image.hidden_from_users do + true -> + conn + |> put_flash(:error, "Cannot feature a hidden image.") + |> redirect(to: Routes.image_path(conn, :show, conn.assigns.image)) + + _false -> + conn + end + end +end diff --git a/lib/philomena_web/controllers/image/file_controller.ex b/lib/philomena_web/controllers/image/file_controller.ex new file mode 100644 index 00000000..ddbef660 --- /dev/null +++ b/lib/philomena_web/controllers/image/file_controller.ex @@ -0,0 +1,40 @@ +defmodule PhilomenaWeb.Image.FileController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, update: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + plug :verify_not_deleted + + def update(conn, %{"image" => image_params}) do + case Images.update_file(conn.assigns.image, image_params) do + {:ok, %{image: image}} -> + spawn fn -> + Images.repair_image(image) + end + + conn + |> put_flash(:info, "Successfully updated file.") + |> redirect(to: Routes.image_path(conn, :show, image)) + + _error -> + conn + |> put_flash(:error, "Failed to update file!") + |> redirect(to: Routes.image_path(conn, :show, conn.assigns.image)) + end + end + + defp verify_not_deleted(conn, _opts) do + case conn.assigns.image.hidden_from_users do + true -> + conn + |> put_flash(:error, "Cannot replace a hidden image.") + |> redirect(to: Routes.image_path(conn, :show, conn.assigns.image)) + + _false -> + conn + end + end +end diff --git a/lib/philomena_web/controllers/image/hash_controller.ex b/lib/philomena_web/controllers/image/hash_controller.ex new file mode 100644 index 00000000..569ed12e --- /dev/null +++ b/lib/philomena_web/controllers/image/hash_controller.ex @@ -0,0 +1,17 @@ +defmodule PhilomenaWeb.Image.HashController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, delete: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + + def delete(conn, _params) do + {:ok, image} = Images.remove_hash(conn.assigns.image) + + conn + |> put_flash(:info, "Successfully cleared hash.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end +end diff --git a/lib/philomena_web/controllers/image/repair_controller.ex b/lib/philomena_web/controllers/image/repair_controller.ex new file mode 100644 index 00000000..2cd80bb6 --- /dev/null +++ b/lib/philomena_web/controllers/image/repair_controller.ex @@ -0,0 +1,32 @@ +defmodule PhilomenaWeb.Image.RepairController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, create: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + plug :verify_not_deleted + + def create(conn, _params) do + spawn fn -> + Images.repair_image(conn.assigns.image) + end + + conn + |> put_flash(:info, "Repair job started.") + |> redirect(to: Routes.image_path(conn, :show, conn.assigns.image)) + end + + defp verify_not_deleted(conn, _opts) do + case conn.assigns.image.hidden_from_users do + true -> + conn + |> put_flash(:error, "Cannot repair a hidden image.") + |> redirect(to: Routes.image_path(conn, :show, conn.assigns.image)) + + _false -> + conn + end + end +end diff --git a/lib/philomena_web/controllers/image/scratchpad_controller.ex b/lib/philomena_web/controllers/image/scratchpad_controller.ex new file mode 100644 index 00000000..09040150 --- /dev/null +++ b/lib/philomena_web/controllers/image/scratchpad_controller.ex @@ -0,0 +1,22 @@ +defmodule PhilomenaWeb.Image.ScratchpadController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, edit: :hide, update: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + + def edit(conn, _params) do + changeset = Images.change_image(conn.assigns.image) + render(conn, "edit.html", changeset: changeset) + end + + def update(conn, %{"image" => image_params}) do + {:ok, image} = Images.update_scratchpad(conn.assigns.image, image_params) + + conn + |> put_flash(:info, "Successfully updated moderation notes.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end +end diff --git a/lib/philomena_web/controllers/image/source_history_controller.ex b/lib/philomena_web/controllers/image/source_history_controller.ex new file mode 100644 index 00000000..4a2ad5b7 --- /dev/null +++ b/lib/philomena_web/controllers/image/source_history_controller.ex @@ -0,0 +1,19 @@ +defmodule PhilomenaWeb.Image.SourceHistoryController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, delete: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + + def delete(conn, _params) do + {:ok, image} = Images.remove_source_history(conn.assigns.image) + + Images.reindex_image(image) + + conn + |> put_flash(:info, "Successfully deleted source history.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end +end diff --git a/lib/philomena_web/controllers/image/tag_lock_controller.ex b/lib/philomena_web/controllers/image/tag_lock_controller.ex new file mode 100644 index 00000000..e4193ca1 --- /dev/null +++ b/lib/philomena_web/controllers/image/tag_lock_controller.ex @@ -0,0 +1,25 @@ +defmodule PhilomenaWeb.Image.TagLockController do + use PhilomenaWeb, :controller + + alias Philomena.Images.Image + alias Philomena.Images + + plug PhilomenaWeb.CanaryMapPlug, create: :hide, delete: :hide + plug :load_and_authorize_resource, model: Image, id_name: "image_id", persisted: true + + def create(conn, _params) do + {:ok, image} = Images.lock_tags(conn.assigns.image, true) + + conn + |> put_flash(:info, "Successfully locked tags.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end + + def delete(conn, _params) do + {:ok, image} = Images.lock_tags(conn.assigns.image, false) + + conn + |> put_flash(:info, "Successfully unlocked tags.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end +end diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index e0d0315d..cae9fd09 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -120,6 +120,17 @@ defmodule PhilomenaWeb.Router do resources "/delete", Image.Comment.DeleteController, only: [:create], singleton: true end resources "/delete", Image.DeleteController, only: [:create, :delete], singleton: true + + resources "/hash", Image.HashController, only: [:delete], singleton: true + resources "/source_history", Image.SourceHistoryController, only: [:delete], singleton: true + resources "/repair", Image.RepairController, only: [:create], singleton: true + resources "/feature", Image.FeatureController, only: [:create], singleton: true + resources "/file", Image.FileController, only: [:update], singleton: true + resources "/scratchpad", Image.ScratchpadController, only: [:edit, :update], singleton: true + + resources "/comment_lock", Image.CommentLockController, only: [:create, :delete], singleton: true + resources "/description_lock", Image.DescriptionLockController, only: [:create, :delete], singleton: true + resources "/tag_lock", Image.TagLockController, only: [:create, :delete], singleton: true end resources "/forums", ForumController, only: [] do diff --git a/lib/philomena_web/templates/image/_options.html.slime b/lib/philomena_web/templates/image/_options.html.slime index b82a3997..1dec4a2c 100644 --- a/lib/philomena_web/templates/image/_options.html.slime +++ b/lib/philomena_web/templates/image/_options.html.slime @@ -65,15 +65,18 @@ br 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}" + = 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...' } + = form_for @changeset, Routes.image_file_path(@conn, :update, @image), [method: "put", multipart: true], fn f -> + #js-image-upload-previews + p Upload a file from your computer + .field = file_input f, :image, class: "input js-scraper" + = submit "Save changes", class: "button", data: [disable_with: raw("Replacing…")] .block__tab.hidden data-tab="administration" .block.block--danger - a.button.button--link> href="#" + a.button.button--link> href=Routes.image_scratchpad_path(@conn, :edit, @image) i.far.fa-edit = if present?(@image.scratchpad) do strong> Mod notes: @@ -90,3 +93,37 @@ - else = button_to "Restore", Routes.image_delete_path(@conn, :delete, @image), method: "delete", class: "button button--state-success" + + .flex.flex--spaced-out.flex--wrap + = if not @image.hidden_from_users do + = form_for @changeset, Routes.image_feature_path(@conn, :create, @image), [method: "post"], fn _f -> + .field + p Automatically tags 'featured image' + = submit "Feature", data: [confirm: "Are you really, really sure?"], class: "button button--state-success" + + = form_for @changeset, Routes.image_repair_path(@conn, :create, @image), [method: "post"], fn _f -> + .field + = submit "Repair", class: "button button--state-success" + + = form_for @changeset, Routes.image_hash_path(@conn, :delete, @image), [method: "delete"], fn _f -> + .field + p Allows reuploading the image + .flex.flex--end-bunched + = submit "Clear hash", data: [confirm: "Are you really, really sure?"], class: "button button--state-danger" + + br + .flex.flex--spaced-out + = if @image.commenting_allowed do + = button_to "Lock commenting", Routes.image_comment_lock_path(@conn, :create, @image), method: "post", class: "button" + - else + = button_to "Unlock commenting", Routes.image_comment_lock_path(@conn, :delete, @image), method: "delete", class: "button" + + = if @image.description_editing_allowed do + = button_to "Lock description editing", Routes.image_description_lock_path(@conn, :create, @image), method: "post", class: "button" + - else + = button_to "Unlock description editing", Routes.image_description_lock_path(@conn, :delete, @image), method: "delete", class: "button" + + = if @image.tag_editing_allowed do + = button_to "Lock tag editing", Routes.image_tag_lock_path(@conn, :create, @image), method: "post", class: "button" + - else + = button_to "Unlock tag editing", Routes.image_tag_lock_path(@conn, :delete, @image), method: "delete", class: "button" diff --git a/lib/philomena_web/templates/image/_source.html.slime b/lib/philomena_web/templates/image/_source.html.slime index a9061a03..0149d1b6 100644 --- a/lib/philomena_web/templates/image/_source.html.slime +++ b/lib/philomena_web/templates/image/_source.html.slime @@ -41,3 +41,9 @@ | History ( = @source_change_count | ) + + = if can?(@conn, :hide, @image) do + = form_for @changeset, Routes.image_source_history_path(@conn, :delete, @image), [method: "delete"], fn _f -> + button.button.button--state-danger.button--separate-left type="submit" data-confirm="Are you really, really sure?" title="Wipe sources" + i.fas.fa-eraser> + ' Wipe diff --git a/lib/philomena_web/templates/image/scratchpad/edit.html.slime b/lib/philomena_web/templates/image/scratchpad/edit.html.slime new file mode 100644 index 00000000..31e37f9f --- /dev/null +++ b/lib/philomena_web/templates/image/scratchpad/edit.html.slime @@ -0,0 +1,9 @@ +h1 + ' Editing moderation notes for image + = link "##{@image.id}", to: Routes.image_path(@conn, :show, @image) + += form_for @changeset, Routes.image_scratchpad_path(@conn, :update, @image), fn f -> + .field + = textarea f, :scratchpad, placeholder: "Scratchpad contents", class: "input input--wide" + + = submit "Update", class: "button", data: [disable_with: raw("Saving…")] diff --git a/lib/philomena_web/views/image/scratchpad_view.ex b/lib/philomena_web/views/image/scratchpad_view.ex new file mode 100644 index 00000000..35437917 --- /dev/null +++ b/lib/philomena_web/views/image/scratchpad_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.Image.ScratchpadView do + use PhilomenaWeb, :view +end