diff --git a/lib/philomena/images.ex b/lib/philomena/images.ex index 12bdf399d..569993c82 100644 --- a/lib/philomena/images.ex +++ b/lib/philomena/images.ex @@ -265,15 +265,25 @@ defmodule Philomena.Images do |> Repo.transaction() end + def update_locked_tags(%Image{} = image, attrs) do + new_tags = Tags.get_or_create_tags(attrs["tag_input"]) + + image + |> Repo.preload(:locked_tags) + |> Image.locked_tags_changeset(attrs, new_tags) + |> Repo.update() + end + def update_tags(%Image{} = image, attribution, attrs) do old_tags = Tags.get_or_create_tags(attrs["old_tag_input"]) new_tags = Tags.get_or_create_tags(attrs["tag_input"]) Multi.new() |> Multi.run(:image, fn repo, _chg -> + image = repo.preload(image, [:tags, :locked_tags]) + image - |> repo.preload(:tags) - |> Image.tag_changeset(%{}, old_tags, new_tags) + |> Image.tag_changeset(%{}, old_tags, new_tags, image.locked_tags) |> repo.update() |> case do {:ok, image} -> diff --git a/lib/philomena/images/image.ex b/lib/philomena/images/image.ex index 3d559d098..bbb7cba84 100644 --- a/lib/philomena/images/image.ex +++ b/lib/philomena/images/image.ex @@ -38,6 +38,7 @@ defmodule Philomena.Images.Image do has_many :favers, through: [:faves, :user] has_many :hiders, through: [:hides, :user] many_to_many :tags, Tag, join_through: "image_taggings", on_replace: :delete + many_to_many :locked_tags, Tag, join_through: "image_tag_locks", on_replace: :delete has_one :intensity, ImageIntensity has_many :galleries, through: [:gallery_interactions, :image] @@ -179,14 +180,20 @@ defmodule Philomena.Images.Image do |> validate_format(:source_url, ~r/\Ahttps?:\/\//) end - def tag_changeset(image, attrs, old_tags, new_tags) do + def tag_changeset(image, attrs, old_tags, new_tags, excluded_tags \\ []) do image |> cast(attrs, []) - |> TagDiffer.diff_input(old_tags, new_tags) + |> TagDiffer.diff_input(old_tags, new_tags, excluded_tags) |> TagValidator.validate_tags() |> cache_changeset() end + def locked_tags_changeset(image, attrs, locked_tags) do + image + |> cast(attrs, []) + |> put_assoc(:locked_tags, locked_tags) + end + def dnp_changeset(image, user) do image |> change() diff --git a/lib/philomena/images/tag_differ.ex b/lib/philomena/images/tag_differ.ex index 93d4ef6d2..831d28a40 100644 --- a/lib/philomena/images/tag_differ.ex +++ b/lib/philomena/images/tag_differ.ex @@ -5,13 +5,15 @@ defmodule Philomena.Images.TagDiffer do alias Philomena.Tags.Tag alias Philomena.Repo - def diff_input(changeset, old_tags, new_tags) do + def diff_input(changeset, old_tags, new_tags, excluded_tags) do + excluded_ids = Enum.map(excluded_tags, & &1.id) + old_set = to_set(old_tags) new_set = to_set(new_tags) tags = changeset |> get_field(:tags) - added_tags = added_set(old_set, new_set) - removed_tags = removed_set(old_set, new_set) + added_tags = added_set(old_set, new_set, excluded_ids) + removed_tags = removed_set(old_set, new_set, excluded_ids) {tags, actually_added, actually_removed} = apply_changes(tags, added_tags, removed_tags) @@ -21,7 +23,7 @@ defmodule Philomena.Images.TagDiffer do |> put_assoc(:tags, tags) end - defp added_set(old_set, new_set) do + defp added_set(old_set, new_set, excluded_ids) do # new_tags - old_tags added_set = new_set @@ -40,12 +42,16 @@ defmodule Philomena.Images.TagDiffer do |> Enum.filter(fn {_k, v} -> v.namespace == "oc" end) |> get_oc_tag() - Map.merge(added_and_implied_set, oc_set) + added_and_implied_set + |> Map.merge(oc_set) + |> Map.drop(excluded_ids) end - defp removed_set(old_set, new_set) do + defp removed_set(old_set, new_set, excluded_ids) do # old_tags - new_tags - old_set |> Map.drop(Map.keys(new_set)) + old_set + |> Map.drop(Map.keys(new_set)) + |> Map.drop(excluded_ids) end defp get_oc_tag([]), do: Map.new() diff --git a/lib/philomena/images/tag_lock.ex b/lib/philomena/images/tag_lock.ex new file mode 100644 index 000000000..dc5e3d4c4 --- /dev/null +++ b/lib/philomena/images/tag_lock.ex @@ -0,0 +1,21 @@ +defmodule Philomena.Images.TagLock do + use Ecto.Schema + import Ecto.Changeset + + alias Philomena.Images.Image + alias Philomena.Tags.Tag + + @primary_key false + + schema "image_tag_locks" do + belongs_to :image, Image, primary_key: true + belongs_to :tag, Tag, primary_key: true + end + + @doc false + def changeset(tag_lock, attrs) do + tag_lock + |> cast(attrs, []) + |> validate_required([]) + end +end diff --git a/lib/philomena_web/controllers/image/tag_controller.ex b/lib/philomena_web/controllers/image/tag_controller.ex index e21d042f5..a13c11ac4 100644 --- a/lib/philomena_web/controllers/image/tag_controller.ex +++ b/lib/philomena_web/controllers/image/tag_controller.ex @@ -23,7 +23,7 @@ defmodule PhilomenaWeb.Image.TagController do plug :load_and_authorize_resource, model: Image, id_name: "image_id", - preload: [:user, tags: :aliases] + preload: [:user, :locked_tags, tags: :aliases] def update(conn, %{"image" => image_params}) do attributes = conn.assigns.attributes diff --git a/lib/philomena_web/controllers/image/tag_lock_controller.ex b/lib/philomena_web/controllers/image/tag_lock_controller.ex index e4193ca12..06017d38a 100644 --- a/lib/philomena_web/controllers/image/tag_lock_controller.ex +++ b/lib/philomena_web/controllers/image/tag_lock_controller.ex @@ -4,8 +4,27 @@ defmodule PhilomenaWeb.Image.TagLockController do 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 + plug PhilomenaWeb.CanaryMapPlug, show: :hide, update: :hide, create: :hide, delete: :hide + + plug :load_and_authorize_resource, + model: Image, + id_name: "image_id", + persisted: true, + preload: [:locked_tags] + + def show(conn, _params) do + changeset = Images.change_image(conn.assigns.image) + + render(conn, "show.html", title: "Locking image tags", changeset: changeset) + end + + def update(conn, %{"image" => image_attrs}) do + {:ok, image} = Images.update_locked_tags(conn.assigns.image, image_attrs) + + conn + |> put_flash(:info, "Successfully updated list of locked tags.") + |> redirect(to: Routes.image_path(conn, :show, image)) + end def create(conn, _params) do {:ok, image} = Images.lock_tags(conn.assigns.image, true) diff --git a/lib/philomena_web/controllers/image_controller.ex b/lib/philomena_web/controllers/image_controller.ex index eecd5c722..31e1d0743 100644 --- a/lib/philomena_web/controllers/image_controller.ex +++ b/lib/philomena_web/controllers/image_controller.ex @@ -181,7 +181,7 @@ defmodule PhilomenaWeb.ImageController do [i, _], _ in fragment("SELECT COUNT(*) FROM source_changes s WHERE s.image_id = ?", i.id) ) - |> preload([:deleter, user: [awards: :badge], tags: :aliases]) + |> preload([:deleter, :locked_tags, user: [awards: :badge], tags: :aliases]) |> select([i, t, s], {i, t.count, s.count}) |> Repo.one() |> case do diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 39f1dd298..2479a2d15 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -212,7 +212,9 @@ defmodule PhilomenaWeb.Router do only: [:create, :delete], singleton: true - resources "/tag_lock", Image.TagLockController, only: [:create, :delete], singleton: true + resources "/tag_lock", Image.TagLockController, + only: [:show, :update, :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 31efc9ddb..020a7bcb2 100644 --- a/lib/philomena_web/templates/image/_options.html.slime +++ b/lib/philomena_web/templates/image/_options.html.slime @@ -142,7 +142,8 @@ - else = button_to "Unlock tag editing", Routes.image_tag_lock_path(@conn, :delete, @image), method: "delete", class: "button" - = if @image.hidden_from_users and can?(@conn, :destroy, @image) do - br - .flex.flex--spaced-out - = button_to "Destroy image", Routes.image_destroy_path(@conn, :create, @image), method: "post", class: "button button--state-danger", data: [confirm: "This action is IRREVERSIBLE. Are you sure?"] + br + .flex.flex--spaced-out + = link "Lock specific tags", to: Routes.image_tag_lock_path(@conn, :show, @image), class: "button" + = if @image.hidden_from_users and can?(@conn, :destroy, @image) do + = button_to "Destroy image", Routes.image_destroy_path(@conn, :create, @image), method: "post", class: "button button--state-danger", data: [confirm: "This action is IRREVERSIBLE. Are you sure?"] diff --git a/lib/philomena_web/templates/image/_tags.html.slime b/lib/philomena_web/templates/image/_tags.html.slime index ae3cf38c9..ed586fd95 100644 --- a/lib/philomena_web/templates/image/_tags.html.slime +++ b/lib/philomena_web/templates/image/_tags.html.slime @@ -6,6 +6,12 @@ .js-imageform class=form_class = if can?(@conn, :edit_metadata, @image) and !@conn.assigns.current_ban do + = if Enum.any?(@image.locked_tags) do + .block.block--fixed.block--warning + i.fa.fa-lock> + ' The following tags have been restricted on this image: + code= Enum.map_join(@image.locked_tags, ", ", & &1.name) + = form_for @changeset, Routes.image_tag_path(@conn, :update, @image), [id: "tags-form", method: "put", data: [remote: true]], fn f -> = if @changeset.action do .alert.alert-danger diff --git a/lib/philomena_web/templates/image/tag_lock/show.html.slime b/lib/philomena_web/templates/image/tag_lock/show.html.slime new file mode 100644 index 000000000..81a52cdfc --- /dev/null +++ b/lib/philomena_web/templates/image/tag_lock/show.html.slime @@ -0,0 +1,13 @@ +- tag_input = Enum.map_join(@image.locked_tags, ", ", & &1.name) + +h1 + | Editing locked tags on image # + = @image.id + += form_for @changeset, Routes.image_tag_lock_path(@conn, :update, @image), fn f -> + .field + = render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :tag_input, type: :edit, extra: [value: tag_input] + = error_tag f, :tag_input + + .actions + = submit "Update", class: "button", autocomplete: "off", data: [disable_with: "Please wait..."] diff --git a/lib/philomena_web/views/image/tag_lock_view.ex b/lib/philomena_web/views/image/tag_lock_view.ex new file mode 100644 index 000000000..044965be2 --- /dev/null +++ b/lib/philomena_web/views/image/tag_lock_view.ex @@ -0,0 +1,3 @@ +defmodule PhilomenaWeb.Image.TagLockView do + use PhilomenaWeb, :view +end diff --git a/priv/repo/migrations/20210301012137_add_tag_locks.exs b/priv/repo/migrations/20210301012137_add_tag_locks.exs new file mode 100644 index 000000000..160ff1796 --- /dev/null +++ b/priv/repo/migrations/20210301012137_add_tag_locks.exs @@ -0,0 +1,13 @@ +defmodule Philomena.Repo.Migrations.AddTagLocks do + use Ecto.Migration + + def change do + create table("image_tag_locks", primary_key: false) do + add :image_id, references(:images, on_delete: :delete_all), null: false + add :tag_id, references(:tags, on_delete: :delete_all), null: false + end + + create index("image_tag_locks", [:image_id, :tag_id], unique: true) + create index("image_tag_locks", [:tag_id]) + end +end diff --git a/priv/repo/structure.sql b/priv/repo/structure.sql index a886498b1..980129d31 100644 --- a/priv/repo/structure.sql +++ b/priv/repo/structure.sql @@ -858,6 +858,16 @@ CREATE TABLE public.image_subscriptions ( ); +-- +-- Name: image_tag_locks; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.image_tag_locks ( + image_id bigint NOT NULL, + tag_id bigint NOT NULL +); + + -- -- Name: image_taggings; Type: TABLE; Schema: public; Owner: - -- @@ -2796,6 +2806,20 @@ CREATE INDEX image_intensities_index ON public.image_intensities USING btree (nw CREATE UNIQUE INDEX image_sources_image_id_source_index ON public.image_sources USING btree (image_id, source); +-- +-- Name: image_tag_locks_image_id_tag_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX image_tag_locks_image_id_tag_id_index ON public.image_tag_locks USING btree (image_id, tag_id); + + +-- +-- Name: image_tag_locks_tag_id_index; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX image_tag_locks_tag_id_index ON public.image_tag_locks USING btree (tag_id); + + -- -- Name: index_adverts_on_restrictions; Type: INDEX; Schema: public; Owner: - -- @@ -4772,6 +4796,22 @@ ALTER TABLE ONLY public.image_sources ADD CONSTRAINT image_sources_image_id_fkey FOREIGN KEY (image_id) REFERENCES public.images(id); +-- +-- Name: image_tag_locks image_tag_locks_image_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.image_tag_locks + ADD CONSTRAINT image_tag_locks_image_id_fkey FOREIGN KEY (image_id) REFERENCES public.images(id) ON DELETE CASCADE; + + +-- +-- Name: image_tag_locks image_tag_locks_tag_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.image_tag_locks + ADD CONSTRAINT image_tag_locks_tag_id_fkey FOREIGN KEY (tag_id) REFERENCES public.tags(id) ON DELETE CASCADE; + + -- -- Name: user_tokens user_tokens_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -4802,3 +4842,4 @@ INSERT INTO public."schema_migrations" (version) VALUES (20200817213256); INSERT INTO public."schema_migrations" (version) VALUES (20200905214139); INSERT INTO public."schema_migrations" (version) VALUES (20201124224116); INSERT INTO public."schema_migrations" (version) VALUES (20210121200815); +INSERT INTO public."schema_migrations" (version) VALUES (20210301012137);