diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index 4fc61da8..7bb6025a 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -4,9 +4,13 @@ defmodule Philomena.Tags do """ import Ecto.Query, warn: false + alias Ecto.Multi alias Philomena.Repo alias Philomena.Tags.Tag + alias Philomena.Tags.Uploader + alias Philomena.Images + alias Philomena.Images.Image @spec get_or_create_tags(String.t()) :: List.t() def get_or_create_tags(tag_list) do @@ -96,11 +100,44 @@ defmodule Philomena.Tags do """ def update_tag(%Tag{} = tag, attrs) do + tag_input = Tag.parse_tag_list(attrs["implied_tag_list"]) + implied_tags = + Tag + |> where([t], t.name in ^tag_input) + |> Repo.all() + tag - |> Tag.changeset(attrs) + |> Tag.changeset(attrs, implied_tags) |> Repo.update() end + def update_tag_image(%Tag{} = tag, attrs) do + changeset = Uploader.analyze_upload(tag, attrs) + + Multi.new + |> Multi.update(:tag, changeset) + |> Multi.run(:update_file, fn _repo, %{tag: tag} -> + Uploader.persist_upload(tag) + Uploader.unpersist_old_upload(tag) + + {:ok, nil} + end) + |> Repo.isolated_transaction(:serializable) + end + + def remove_tag_image(%Tag{} = tag) do + changeset = Tag.remove_image_changeset(tag) + + Multi.new + |> Multi.update(:tag, changeset) + |> Multi.run(:remove_file, fn _repo, %{tag: tag} -> + Uploader.unpersist_old_upload(tag) + + {:ok, nil} + end) + |> Repo.isolated_transaction(:serializable) + end + @doc """ Deletes a Tag. @@ -114,7 +151,19 @@ defmodule Philomena.Tags do """ def delete_tag(%Tag{} = tag) do - Repo.delete(tag) + image_ids = + Image + |> join(:inner, [i], _ in assoc(i, :tags)) + |> where([_i, t], t.id == ^tag.id) + |> select([i, _t], i.id) + |> Repo.all() + + {:ok, _tag} = Repo.delete(tag) + + Image + |> where([i], i.id in ^image_ids) + |> preload(^Images.indexing_preloads()) + |> Image.reindex() end @doc """ diff --git a/lib/philomena/tags/tag.ex b/lib/philomena/tags/tag.ex index 553bab02..22b16135 100644 --- a/lib/philomena/tags/tag.ex +++ b/lib/philomena/tags/tag.ex @@ -50,7 +50,7 @@ defmodule Philomena.Tags.Tag do schema "tags" do belongs_to :aliased_tag, Tag, source: :aliased_tag_id has_many :aliases, Tag, foreign_key: :aliased_tag_id - many_to_many :implied_tags, Tag, join_through: "tags_implied_tags", join_keys: [tag_id: :id, implied_tag_id: :id] + many_to_many :implied_tags, Tag, join_through: "tags_implied_tags", join_keys: [tag_id: :id, implied_tag_id: :id], on_replace: :delete many_to_many :implied_by_tags, Tag, join_through: "tags_implied_tags", join_keys: [implied_tag_id: :id, tag_id: :id] has_many :public_links, UserLink, where: [public: true, aasm_state: "verified"] has_many :dnp_entries, DnpEntry, where: [aasm_state: "listed"] @@ -68,17 +68,42 @@ defmodule Philomena.Tags.Tag do field :image_mime_type, :string field :mod_notes, :string + field :uploaded_image, :string, virtual: true + field :removed_image, :string, virtual: true + + field :implied_tag_list, :string, virtual: true + timestamps(inserted_at: :created_at) end @doc false def changeset(tag, attrs) do tag - |> cast(attrs, []) + |> cast(attrs, [:category, :description, :short_description, :mod_notes]) + |> put_change(:implied_tag_list, Enum.map_join(tag.implied_tags, ",", & &1.name)) |> validate_required([]) end - @doc false + def changeset(tag, attrs, implied_tags) do + tag + |> cast(attrs, [:category, :description, :short_description, :mod_notes]) + |> put_assoc(:implied_tags, implied_tags) + |> validate_required([]) + end + + def image_changeset(tag, attrs) do + tag + |> cast(attrs, [:image, :image_format, :image_mime_type, :uploaded_image]) + |> validate_required([:image, :image_format, :image_mime_type]) + |> validate_inclusion(:image_mime_type, ~W(image/gif image/jpeg image/png)) + end + + def remove_image_changeset(tag) do + change(tag) + |> put_change(:removed_image, tag.image) + |> put_change(:image, nil) + end + def creation_changeset(tag, attrs) do tag |> cast(attrs, [:name]) @@ -112,6 +137,20 @@ defmodule Philomena.Tags.Tag do }) end + def categories do + [ + "error", + "rating", + "origin", + "character", + "oc", + "species", + "content-fanmade", + "content-official", + "spoiler" + ] + end + def clean_tag_name(name) do # Downcase, replace extra runs of spaces, replace unicode quotes # with ascii quotes, trim space from end diff --git a/lib/philomena/tags/uploader.ex b/lib/philomena/tags/uploader.ex new file mode 100644 index 00000000..d6149221 --- /dev/null +++ b/lib/philomena/tags/uploader.ex @@ -0,0 +1,24 @@ +defmodule Philomena.Tags.Uploader do + @moduledoc """ + Upload and processing callback logic for Tag images. + """ + + alias Philomena.Tags.Tag + alias Philomena.Uploader + + def analyze_upload(tag, params) do + Uploader.analyze_upload(tag, "image", params["image"], &Tag.image_changeset/2) + end + + def persist_upload(tag) do + Uploader.persist_upload(tag, tag_file_root(), "image") + end + + def unpersist_old_upload(tag) do + Uploader.unpersist_old_upload(tag, tag_file_root(), "image") + end + + defp tag_file_root do + Application.get_env(:philomena, :tag_file_root) + end +end diff --git a/lib/philomena/users/ability.ex b/lib/philomena/users/ability.ex index 174cd8d3..93a80a43 100644 --- a/lib/philomena/users/ability.ex +++ b/lib/philomena/users/ability.ex @@ -80,6 +80,9 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do # Hide topics def can?(%User{role: "moderator"}, :hide, %Topic{}), do: true + # Edit tags + def can?(%User{role: "moderator"}, :edit, %Tag{}), do: true + # # Assistants can... # diff --git a/lib/philomena_web/controllers/admin/report/claim_controller.ex b/lib/philomena_web/controllers/admin/report/claim_controller.ex index 3ef4d4ac..c31e8dad 100644 --- a/lib/philomena_web/controllers/admin/report/claim_controller.ex +++ b/lib/philomena_web/controllers/admin/report/claim_controller.ex @@ -14,7 +14,7 @@ defmodule PhilomenaWeb.Admin.Report.ClaimController do conn |> put_flash(:info, "Successfully marked report as in progress") - |> redirect(to: Routes.admin_report_path(conn, :show, report)) + |> redirect(to: Routes.admin_report_path(conn, :index)) {:error, _changeset} -> conn @@ -31,4 +31,4 @@ defmodule PhilomenaWeb.Admin.Report.ClaimController do |> put_flash(:info, "Successfully released report.") |> redirect(to: Routes.admin_report_path(conn, :show, report)) end -end \ No newline at end of file +end diff --git a/lib/philomena_web/controllers/tag_controller.ex b/lib/philomena_web/controllers/tag_controller.ex index 57288ce3..3cab5d18 100644 --- a/lib/philomena_web/controllers/tag_controller.ex +++ b/lib/philomena_web/controllers/tag_controller.ex @@ -5,10 +5,9 @@ defmodule PhilomenaWeb.TagController do alias Philomena.{Tags, Tags.Tag} alias Philomena.Textile.Renderer alias Philomena.Interactions - alias Philomena.Repo - import Ecto.Query plug PhilomenaWeb.RecodeParameterPlug, [name: "id"] when action in [:show] + plug :load_and_authorize_resource, model: Tag, id_field: "slug", only: [:show, :edit, :update, :delete], preload: [:aliases, :implied_tags, :implied_by_tags, :dnp_entries, public_links: :user] def index(conn, params) do query_string = params["tq"] || "*" @@ -32,14 +31,9 @@ defmodule PhilomenaWeb.TagController do end end - def show(conn, %{"id" => slug}) do + def show(conn, _params) do user = conn.assigns.current_user - - tag = - Tag - |> where(slug: ^slug) - |> preload([:aliases, :implied_tags, :implied_by_tags, :dnp_entries, public_links: :user]) - |> Repo.one() + tag = conn.assigns.tag {images, _tags} = ImageLoader.query(conn, %{term: %{"namespaced_tags.name" => tag.name}}) @@ -73,6 +67,35 @@ defmodule PhilomenaWeb.TagController do ) end + def edit(conn, _params) do + changeset = Tags.change_tag(conn.assigns.tag) + render(conn, "edit.html", changeset: changeset) + end + + def update(conn, %{"tag" => tag_params}) do + case Tags.update_tag(conn.assigns.tag, tag_params) do + {:ok, tag} -> + Tags.reindex_tag(tag) + + conn + |> put_flash(:info, "Tag successfully updated.") + |> redirect(to: Routes.tag_path(conn, :show, tag)) + + {:error, changeset} -> + render(conn, "edit.html", changeset: changeset) + end + end + + def delete(conn, _params) do + spawn fn -> + Tags.delete_tag(conn.assigns.tag) + end + + conn + |> put_flash(:info, "Tag scheduled for deletion.") + |> redirect(to: "/") + end + def escape_name(%{name: name}) do name = name diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index 58a955c8..fa3a5d65 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -208,6 +208,8 @@ defmodule PhilomenaWeb.Router do resources "/reject", DuplicateReport.RejectController, only: [:create], singleton: true resources "/claim", DuplicateReport.ClaimController, only: [:create, :delete], singleton: true end + + resources "/tags", TagController, only: [:edit, :update, :delete] end scope "/", PhilomenaWeb do diff --git a/lib/philomena_web/templates/tag/_tag_info_row.html.slime b/lib/philomena_web/templates/tag/_tag_info_row.html.slime index bbbb1874..56ce52cc 100644 --- a/lib/philomena_web/templates/tag/_tag_info_row.html.slime +++ b/lib/philomena_web/templates/tag/_tag_info_row.html.slime @@ -8,6 +8,9 @@ .flex__grow = render PhilomenaWeb.TagView, "_tag.html", tag: @tag, conn: @conn = link "Tag changes", to: Routes.tag_tag_change_path(@conn, :index, @tag), class: "detail-link" + = if manages_tags?(@conn) do + = link "Edit details", to: Routes.tag_path(@conn, :edit, @tag), class: "detail-link" + br = if @tag.short_description not in [nil, ""] do @@ -81,4 +84,4 @@ | ( = link "more info", to: "#" - | ) \ No newline at end of file + | ) diff --git a/lib/philomena_web/templates/tag/edit.html.slime b/lib/philomena_web/templates/tag/edit.html.slime new file mode 100644 index 00000000..093c9c17 --- /dev/null +++ b/lib/philomena_web/templates/tag/edit.html.slime @@ -0,0 +1,65 @@ +h1 Editing Tag + += form_for @changeset, Routes.tag_path(@conn, :update, @tag), [class: "form"], fn f -> + = if @changeset.action do + .alert.alert-danger + p Oops, something went wrong! Please check the errors below. + + h2 + = @tag.name + + .field + ' Category: + = select f, :category, tag_categories(), class: "input" + + h4 Description + .field + => label f, :short_description, "Short description:" + = text_input f, :short_description, class: "input input--wide" + + .field + => label f, :description, "Long description:" + = textarea f, :description, class: "input input--wide" + + .field + => label f, :mod_notes, "Mod notes:" + = textarea f, :mod_notes, class: "input input--wide" + + h4 Implied Tags + .fieldlabel Tags in this list will be added when this tag is added to an image. + .field + = render PhilomenaWeb.TagView, "_tag_editor.html", f: f, name: :implied_tag_list, type: :edit, conn: @conn + + /- if can? :manage, Tag + h4 Tag Merging (Aliasing) + .fieldlabel Merge with target tag for searches, user links, filters, etc.; soft keeps the tag around for redirection purposes as an "alias" + .field + = select_tag :merge_mode, options_for_select({ Soft: :alias, Hard: :hard_merge }, :alias), class: "input" + = text_input f, :target_tag_name, class: "input", placeholder: "Target tag name", autocapitalize: "none", value: @tag.aliased_tag_name + + br + = submit "Save Tag", class: "button button--state-primary" + + / not ready yet + br + br + input.toggle-box#tag-management checked="false" type="checkbox" + label for="tag-management" Tag Processing + .toggle-box-container + .toggle-box-container__content + = link_to "Rebuild index", admin_tag_reindex_path(@tag), class: "button", data: { confirm: t("are_you_sure") }, method: :post + p Use this if the tag displays the wrong number of images or returns the wrong search results. + = link_to "Recreate slug", admin_tag_slug_path(@tag), class: "button", method: :post + p + | Use this for old tags with invalid slugs ( + code + ' /mlp/ → + = Tag.generate_slug "/mlp/" + | ) + = link_to "Destroy tag", admin_tag_path(@tag), class: "button button--state-danger", data: { confirm: t("are_you_sure") }, method: :delete + p + strong Irreversible. Use with extreme caution! + ul + li Intended use is removing garbage tags. + li Will remove tag changes on the tag, but not on images or profiles. + li Will fail if the tag is the target of an alias, is implied by other tags, or is a rating tag. diff --git a/lib/philomena_web/views/tag_view.ex b/lib/philomena_web/views/tag_view.ex index b84ad2c0..bd7e0219 100644 --- a/lib/philomena_web/views/tag_view.ex +++ b/lib/philomena_web/views/tag_view.ex @@ -6,6 +6,14 @@ defmodule PhilomenaWeb.TagView do alias Philomena.Repo import Ecto.Query + def tag_categories do + [[key: "-", value: ""] | Tag.categories] + end + + def manages_tags?(conn) do + can?(conn, :edit, %Tag{}) + end + def tag_image(%{image: image}) do tag_url_root() <> "/" <> image end