admin dnp queue

This commit is contained in:
byte[] 2019-12-12 16:44:50 -05:00
parent 3a55001ec2
commit aa4af5a2e6
16 changed files with 488 additions and 22 deletions

View file

@ -49,9 +49,11 @@ defmodule Philomena.DnpEntries do
{:error, %Ecto.Changeset{}}
"""
def create_dnp_entry(attrs \\ %{}) do
def create_dnp_entry(user, tags, attrs \\ %{}) do
tag = Enum.find(tags, &to_string(&1.id) == attrs["tag_id"])
%DnpEntry{}
|> DnpEntry.changeset(attrs)
|> DnpEntry.creation_changeset(attrs, tag, user)
|> Repo.insert()
end
@ -67,9 +69,17 @@ defmodule Philomena.DnpEntries do
{:error, %Ecto.Changeset{}}
"""
def update_dnp_entry(%DnpEntry{} = dnp_entry, attrs) do
def update_dnp_entry(%DnpEntry{} = dnp_entry, tags, attrs) do
tag = Enum.find(tags, &to_string(&1.id) == attrs["tag_id"])
dnp_entry
|> DnpEntry.changeset(attrs)
|> DnpEntry.update_changeset(attrs, tag)
|> Repo.update()
end
def transition_dnp_entry(%DnpEntry{} = dnp_entry, user, new_state) do
dnp_entry
|> DnpEntry.transition_changeset(user, new_state)
|> Repo.update()
end

View file

@ -11,12 +11,12 @@ defmodule Philomena.DnpEntries.DnpEntry do
belongs_to :tag, Tag
field :aasm_state, :string, default: "requested"
field :dnp_type, :string
field :conditions, :string
field :reason, :string
field :dnp_type, :string, default: ""
field :conditions, :string, default: ""
field :reason, :string, default: ""
field :hide_reason, :boolean, default: false
field :instructions, :string
field :feedback, :string
field :instructions, :string, default: ""
field :feedback, :string, default: ""
timestamps(inserted_at: :created_at)
end
@ -27,4 +27,67 @@ defmodule Philomena.DnpEntries.DnpEntry do
|> cast(attrs, [])
|> validate_required([])
end
def update_changeset(dnp_entry, attrs, tag) do
dnp_entry
|> cast(attrs, [:conditions, :reason, :hide_reason, :instructions, :feedback, :dnp_type])
|> put_change(:tag_id, tag.id)
|> validate_required([:reason, :dnp_type])
|> validate_inclusion(:dnp_type, types())
|> validate_conditions()
|> foreign_key_constraint(:tag_id, name: "fk_rails_473a736b4a")
end
def creation_changeset(dnp_entry, attrs, tag, user) do
dnp_entry
|> change(requesting_user_id: user.id)
|> update_changeset(attrs, tag)
end
def transition_changeset(dnp_entry, user, new_state) do
dnp_entry
|> change(modifying_user_id: user.id)
|> change(aasm_state: new_state)
|> validate_inclusion(:aasm_state, states())
end
defp validate_conditions(%Ecto.Changeset{changes: %{dnp_type: "Other"}} = changeset),
do: validate_required(changeset, [:conditions])
defp validate_conditions(changeset),
do: changeset
def types do
[
"No Edits",
"Artist Tag Change",
"Uploader Credit Change",
"Certain Type/Location Only",
"With Permission Only",
"Artist Upload Only",
"Other"
]
end
def reasons do
[
{"No Edits", "I would like to prevent edited versions of my artwork from being uploaded in the future"},
{"Artist Tag Change", "I would like my artist tag to be changed to something that can not be connected to my current name"},
{"Uploader Credit Change", "I would like the uploader credit for already existing uploads of my art to be assigned to me"},
{"Certain Type/Location Only", "I only want to allow art of a certain type or from a certain location to be uploaded to Derpibooru"},
{"With Permission Only", "I only want people with my permission to be allowed to upload my art to Derpibooru"},
{"Artist Upload Only", "I want to be the only person allowed to upload my art to Derpibooru"},
{"Other", "I would like a DNP entry under other conditions"}
]
end
def states do
[
"requested",
"claimed",
"listed",
"rescinded",
"acknowledged",
"closed"
]
end
end

View file

@ -4,6 +4,7 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
alias Philomena.Commissions.Commission
alias Philomena.Conversations.Conversation
alias Philomena.DuplicateReports.DuplicateReport
alias Philomena.DnpEntries.DnpEntry
alias Philomena.Images.Image
alias Philomena.Forums.Forum
alias Philomena.Topics.Topic
@ -60,6 +61,11 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
# Reveal anon users
def can?(%User{role: "moderator"}, :reveal_anon, _object), do: true
# Show the DNP list
def can?(%User{role: "moderator"}, :index, DnpEntry), do: true
def can?(%User{role: "moderator"}, :edit, %DnpEntry{}), do: true
def can?(%User{role: "moderator"}, :update, %DnpEntry{}), do: true
#
# Assistants can...
#
@ -172,7 +178,7 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
def can?(_user, :show, %User{}), do: true
# View and create DNP entries
def can?(%User{}, action, DnpEntry) when action in [:new, :create, :index], do: true
def can?(%User{}, action, DnpEntry) when action in [:new, :create], do: true
def can?(%User{id: id}, :show, %DnpEntry{requesting_user_id: id}), do: true
def can?(%User{id: id}, :show_reason, %DnpEntry{requesting_user_id: id}), do: true
def can?(%User{id: id}, :show_feedback, %DnpEntry{requesting_user_id: id}), do: true

View file

@ -0,0 +1,30 @@
defmodule PhilomenaWeb.Admin.DnpEntry.TransitionController do
use PhilomenaWeb, :controller
alias Philomena.DnpEntries.DnpEntry
alias Philomena.DnpEntries
plug :verify_authorized
plug :load_resource, model: DnpEntry, only: [:create], id_name: "dnp_entry_id", persisted: true
def create(conn, %{"state" => new_state}) do
case DnpEntries.transition_dnp_entry(conn.assigns.dnp_entry, conn.assigns.current_user, new_state) do
{:ok, dnp_entry} ->
conn
|> put_flash(:info, "Successfully updated DNP entry.")
|> redirect(to: Routes.dnp_entry_path(conn, :show, dnp_entry))
{:error, _changeset} ->
conn
|> put_flash(:error, "Failed to update DNP entry!")
|> redirect(external: conn.assigns.referrer)
end
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :index, DnpEntry) do
true -> conn
_false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
end

View file

@ -0,0 +1,60 @@
defmodule PhilomenaWeb.Admin.DnpEntryController do
use PhilomenaWeb, :controller
alias Philomena.Textile.Renderer
alias Philomena.DnpEntries.DnpEntry
alias Philomena.Repo
import Ecto.Query
plug :verify_authorized
plug :load_resource, model: DnpEntry, only: [:show, :edit, :update]
def index(conn, %{"states" => states}) when is_list(states) do
DnpEntry
|> where([d], d.aasm_state in ^states)
|> load_entries(conn)
end
def index(conn, %{"q" => q}) when is_binary(q) do
q = to_ilike(q)
DnpEntry
|> join(:inner, [d], _ in assoc(d, :tag))
|> join(:inner, [d, _t], _ in assoc(d, :requesting_user))
|> where([d, t, u], ilike(u.name, ^q) or ilike(t.name, ^q) or ilike(d.reason, ^q) or ilike(d.conditions, ^q) or ilike(d.instructions, ^q))
|> load_entries(conn)
end
def index(conn, _params) do
DnpEntry
|> where([d], d.aasm_state in ["requested", "claimed", "rescinded", "acknowledged"])
|> load_entries(conn)
end
defp load_entries(dnp_entries, conn) do
dnp_entries =
dnp_entries
|> preload([:tag, :requesting_user, :modifying_user])
|> order_by(desc: :updated_at)
|> Repo.paginate(conn.assigns.scrivener)
bodies =
dnp_entries
|> Enum.map(&%{body: &1.conditions})
|> Renderer.render_collection(conn)
dnp_entries =
%{dnp_entries | entries: Enum.zip(bodies, dnp_entries.entries)}
render(conn, "index.html", layout_class: "layout--wide", dnp_entries: dnp_entries)
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :index, DnpEntry) do
true -> conn
_false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
defp to_ilike(query), do: "%" <> query <> "%"
end

View file

@ -1,23 +1,35 @@
defmodule PhilomenaWeb.DnpEntryController do
use PhilomenaWeb, :controller
# alias Philomena.DnpEntries
alias Philomena.DnpEntries.DnpEntry
alias Philomena.Textile.Renderer
alias Philomena.DnpEntries
alias Philomena.Tags.Tag
alias Philomena.Repo
import Ecto.Query
plug :load_and_authorize_resource, model: DnpEntry, only: [:show], preload: [:tag]
plug PhilomenaWeb.FilterBannedUsersPlug when action in [:new, :create]
plug :load_and_authorize_resource, model: DnpEntry, only: [:show, :edit, :update], preload: [:tag]
def index(%{assigns: %{current_user: user}} = conn, %{"mine" => _mine}) when not is_nil(user) do
DnpEntry
|> where(requesting_user_id: ^user.id)
|> preload(:tag)
|> order_by(asc: :created_at)
|> load_entries(conn, true)
end
def index(conn, _params) do
dnp_entries =
DnpEntry
|> where(aasm_state: "listed")
|> join(:inner, [d], t in Tag, on: d.tag_id == t.id)
|> preload([:tag])
|> order_by([d, t], asc: t.name_in_namespace)
|> Repo.paginate(conn.assigns.scrivener)
DnpEntry
|> where(aasm_state: "listed")
|> join(:inner, [d], t in Tag, on: d.tag_id == t.id)
|> preload(:tag)
|> order_by([_d, t], asc: t.name_in_namespace)
|> load_entries(conn, false)
end
defp load_entries(dnp_entries, conn, status) do
dnp_entries = Repo.paginate(dnp_entries, conn.assigns.scrivener)
bodies =
dnp_entries
@ -27,7 +39,7 @@ defmodule PhilomenaWeb.DnpEntryController do
dnp_entries =
%{dnp_entries | entries: Enum.zip(bodies, dnp_entries.entries)}
render(conn, "index.html", layout_class: "layout--medium", dnp_entries: dnp_entries)
render(conn, "index.html", layout_class: "layout--medium", dnp_entries: dnp_entries, status_column: status)
end
def show(conn, _params) do
@ -45,4 +57,51 @@ defmodule PhilomenaWeb.DnpEntryController do
render(conn, "show.html", dnp_entry: dnp_entry, conditions: conditions, reason: reason, instructions: instructions)
end
def new(conn, _params) do
changeset = DnpEntries.change_dnp_entry(%DnpEntry{})
render(conn, "new.html", changeset: changeset, selectable_tags: selectable_tags(conn))
end
def create(conn, %{"dnp_entry" => dnp_entry_params}) do
case DnpEntries.create_dnp_entry(conn.assigns.current_user, selectable_tags(conn), dnp_entry_params) do
{:ok, dnp_entry} ->
conn
|> put_flash(:info, "Successfully submitted DNP request.")
|> redirect(to: Routes.dnp_entry_path(conn, :show, dnp_entry))
{:error, changeset} ->
render(conn, "new.html", changeset: changeset, selectable_tags: selectable_tags(conn))
end
end
def edit(conn, _params) do
changeset = DnpEntries.change_dnp_entry(conn.assigns.dnp_entry)
render(conn, "edit.html", changeset: changeset, selectable_tags: selectable_tags(conn))
end
def update(conn, %{"dnp_entry" => dnp_entry_params}) do
case DnpEntries.update_dnp_entry(conn.assigns.dnp_entry, selectable_tags(conn), dnp_entry_params) do
{:ok, dnp_entry} ->
conn
|> put_flash(:info, "Successfully submupdateditted DNP request.")
|> redirect(to: Routes.dnp_entry_path(conn, :show, dnp_entry))
{:error, changeset} ->
render(conn, "edit.html", changeset: changeset, selectable_tags: selectable_tags(conn))
end
end
defp selectable_tags(conn) do
case not is_nil(conn.params["tag_id"]) and Canada.Can.can?(conn.assigns.current_user, :index, DnpEntry) do
true -> [Repo.get!(Tag, conn.params["tag_id"])]
false -> linked_tags(conn)
end
end
defp linked_tags(conn) do
conn.assigns.current_user
|> Repo.preload(:linked_tags)
|> Map.get(:linked_tags)
end
end

View file

@ -170,6 +170,8 @@ defmodule PhilomenaWeb.Router do
resources "/subscription", Channel.SubscriptionController, only: [:create, :delete], singleton: true
end
resources "/dnp", DnpEntryController, only: [:new, :create, :edit, :update]
resources "/ip_profiles", IpProfileController, only: [:show]
resources "/fingerprint_profiles", FingerprintProfileController, only: [:show]
@ -184,6 +186,10 @@ defmodule PhilomenaWeb.Router do
resources "/contact", UserLink.ContactController, only: [:create], singleton: true
resources "/reject", UserLink.RejectController, only: [:create], singleton: true
end
resources "/dnp_entries", DnpEntryController, only: [:index] do
resources "/transition", DnpEntry.TransitionController, only: [:create], singleton: true
end
end
resources "/duplicate_reports", DuplicateReportController, only: [] do

View file

@ -0,0 +1,90 @@
h2 Do-Not-Post Requests
= form_for :dnp_entry, Routes.admin_dnp_entry_path(@conn, :index), [method: "get", class: "hform"], fn f ->
.field
= text_input f, :q, name: :q, value: @conn.params["q"], class: "input hform__text", placeholder: "Search query", autocapitalize: "none"
= submit "Search", class: "hform__button button"
- route = fn p -> Routes.admin_dnp_entry_path(@conn, :index, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @dnp_entries, route: route, params: [states: state_param(@conn.params["states"])]
.block
.block__header
= pagination
span.block__header__title Display Only:
=> link "All Open", to: Routes.admin_dnp_entry_path(@conn, :index, states: ~W(requested claimed rescinded acknowledged))
=> link "Listed", to: Routes.admin_dnp_entry_path(@conn, :index, states: ~W(listed))
=> link "Rescinded", to: Routes.admin_dnp_entry_path(@conn, :index, states: ~W(rescinded acknowledged))
=> link "Closed", to: Routes.admin_dnp_entry_path(@conn, :index, states: ~W(closed))
.block__content
table.table
thead
tr
td Tag
td Requesting User
td Type
td Conditions
td Status
td Created
td Modified
td Options
tbody
= for {body, request} <- @dnp_entries do
tr
td
= render PhilomenaWeb.TagView, "_tag.html", tag: request.tag, conn: @conn
td
= link request.requesting_user.name, to: Routes.profile_path(@conn, :show, request.requesting_user)
td
= request.dnp_type
td
== body
td class=dnp_entry_row_class(request)
=> pretty_state(request)
= if request.modifying_user do
' by
= link request.modifying_user.name, to: Routes.profile_path(@conn, :show, request.modifying_user)
td
= pretty_time(request.created_at)
td
= pretty_time(request.updated_at)
td
=> link "Show", to: Routes.dnp_entry_path(@conn, :show, request)
' &bull;
=> link "Send PM", to: Routes.conversation_path(@conn, :new, recipient: request.requesting_user.name)
= case request.aasm_state do
- s when s in ["requested", "claimed"] ->
' &bull;
=> link "Claim", to: Routes.admin_dnp_entry_transition_path(@conn, :create, request, state: "claimed"), data: [method: "post", confirm: "Are you really, really sure?"]
' &bull;
=> link "Approve", to: Routes.admin_dnp_entry_transition_path(@conn, :create, request, state: "listed"), data: [method: "post", confirm: "Are you really, really sure?"]
' &bull;
=> link "Close", to: Routes.admin_dnp_entry_transition_path(@conn, :create, request, state: "closed"), data: [method: "post", confirm: "Are you really, really sure?"]
- "listed" ->
' &bull;
=> link "Rescind", to: Routes.admin_dnp_entry_transition_path(@conn, :create, request, state: "rescinded"), data: [method: "post", confirm: "Are you really, really sure?"]
' &bull;
= link "Close", to: Routes.admin_dnp_entry_transition_path(@conn, :create, request, state: "closed"), data: [method: "post", confirm: "Are you really, really sure?"]
- s when s in ["rescinded", "acknowledged"] ->
' &bull;
=> link "Claim", to: Routes.admin_dnp_entry_transition_path(@conn, :create, request, state: "acknowledged"), data: [method: "post", confirm: "Are you really, really sure?"]
' &bull;
= link "Close", to: Routes.admin_dnp_entry_transition_path(@conn, :create, request, state: "closed"), data: [method: "post", confirm: "Are you really, really sure?"]
- _state ->
' &bull;
=> link "Claim", to: Routes.admin_dnp_entry_transition_path(@conn, :create, request, state: "claimed"), data: [method: "post", confirm: "Are you really, really sure?"]

View file

@ -0,0 +1,85 @@
= if show_steps?(@changeset) do
.block.block--fixed.block--success.walloftext
h3 Getting on the DNP list?
p
' We offer the DNP list in order to give artists the last say on their art, but we
' won't deny that we are sad whenever we see good art and artists disappear.
' Because we know that many who seek a DNP do so on their first contact with the site,
' we'd be very grateful if you as the artist would like to give the site a shot first.
' Why not upload some of your own art? The community might appreciate it more than you think.
p
' Do you wish to submit a DNP Request?
button.button.button--separate-left.js-dnp-yes data-click-show=".js-dnp-common-options" data-click-disable=".js-dnp-yes"
' Yes
.block.block--fixed.walloftext.js-dnp-common-options.hidden
h3 Options
p
' We are aware of several common reasons for why artists seek a DNP.
' They include things such as unwanted edits or a wish to disassociate
' their name from their artwork. Many of these concerns can be addressed
' without removing art.
p We have created several options for you below:
ul.line-spacing
= for {type, description} <- reasons() do
li
= link description, to: "#", data: [click_show: ".js-dnp-form", click_hide: ".js-dnp-common-options", click_inputvalue: ".js-dnp-type", set_value: type]
= form_for @changeset, @action, fn f ->
= if @changeset.action do
.alert.alert-danger
p Oops, something went wrong! Please check the errors below.
.block.js-dnp-form class=form_class(@changeset)
.block__header
span.block__header__title DNP Request Form
.block__content
/ Artist Tag
strong Artist Tag
p Select the artist tag you would like to request a DNP entry for
.field
= select f, :tag_id, selectable_options(@selectable_tags), class: "input"
= error_tag f, :tag_id
hr
/ Conditions
strong Conditions
p If you selected "Other", or have other conditions applicable to your request, please enter them here. If someone always has permission to upload your artwork, enter their name here. (Optional)
.field
= textarea f, :conditions, class: "input input--wide", placeholder: "Conditions"
= error_tag f, :conditions
hr
/ Reason
strong Reason For Request
p Please indicate the reason why you would like your artwork to be removed. (Required)
.field
= textarea f, :reason, class: "input input--wide", placeholder: "Reason", required: true
= error_tag f, :reason
.field
=> checkbox f, :hide_reason, class: "checkbox"
= label f, :hide_reason, "Hide request reason"
p Only select this box if your request contains sensitive or private information that you do not wish to be publicly available. Reasons that do not contain such information will not be hidden.
hr
/ Instructions
strong Instructions
p If you would like existing artwork to be removed under this request, please provide instructions, such as "Remove all art with my artist tag that contain the "edit" tag." (Optional)
.field
= textarea f, :instructions, class: "input input--wide", placeholder: "Instructions"
= error_tag f, :instructions
hr
/ Feedback
strong Feedback
p If you have any additional feedback for the site staff, you may enter it here. (Optional)
.field
= textarea f, :feedback, class: "input input--wide", placeholder: "Feedback"
= error_tag f, :feedback
/ Option (hidden, set by script depending on the option chosen above)
= hidden_input f, :dnp_type, class: "js-dnp-type"
hr
= submit "Submit Request", class: "button"

View file

@ -0,0 +1,2 @@
h2 Edit DNP Request
= render PhilomenaWeb.DnpEntryView, "_form.html", changeset: @changeset, action: Routes.dnp_entry_path(@conn, :update, @dnp_entry), conn: @conn, selectable_tags: @selectable_tags

View file

@ -26,16 +26,29 @@ h3 The List
th Tag
th Restriction
th Conditions
= if @status_column do
th Status
th Created
th Options
tbody
= for {body, entry} <- @dnp_entries do
tr
td
= render PhilomenaWeb.TagView, "_tag.html", tag: entry.tag, conn: @conn
td
= entry.dnp_type
td
== body
= if @status_column do
td
= pretty_state(entry)
td
= pretty_time(entry.created_at)
td
= link "More Info", to: Routes.dnp_entry_path(@conn, :show, entry)

View file

@ -0,0 +1,2 @@
h2 New DNP Request
= render PhilomenaWeb.DnpEntryView, "_form.html", changeset: @changeset, action: Routes.dnp_entry_path(@conn, :create), conn: @conn, selectable_tags: @selectable_tags

View file

@ -5,6 +5,9 @@ h2
.block
.block__header
span.block__header__title DNP Information
= if can?(@conn, :edit, @dnp_entry) do
= link "Edit listing", to: Routes.dnp_entry_path(@conn, :edit, @dnp_entry, tag_id: @dnp_entry.tag_id)
= link "Back to DNP List", to: Routes.dnp_entry_path(@conn, :index)
.block__content
@ -45,4 +48,4 @@ h2
tr
td Status:
td
= String.capitalize(@dnp_entry.aasm_state)
= String.capitalize(@dnp_entry.aasm_state)

View file

@ -62,7 +62,7 @@
span.header__counter__admin
= @user_link_count
= if @dnp_entry_count do
= link to: "#", class: "header__link", title: "DNP Requests" do
= link to: Routes.admin_dnp_entry_path(@conn, :index), class: "header__link", title: "DNP Requests" do
' S
span.header__counter__admin
= @dnp_entry_count

View file

@ -0,0 +1,12 @@
defmodule PhilomenaWeb.Admin.DnpEntryView do
use PhilomenaWeb, :view
import PhilomenaWeb.DnpEntryView, only: [pretty_state: 1]
def dnp_entry_row_class(%{aasm_state: state}) when state in ["closed", "listed"], do: "success"
def dnp_entry_row_class(%{aasm_state: state}) when state in ["claimed", "acknowledged"], do: "warning"
def dnp_entry_row_class(_dnp_entry), do: "danger"
def state_param(states) when is_list(states), do: states
def state_param(_states), do: []
end

View file

@ -1,3 +1,28 @@
defmodule PhilomenaWeb.DnpEntryView do
use PhilomenaWeb, :view
def reasons do
Philomena.DnpEntries.DnpEntry.reasons()
end
def form_class(changeset) do
case show_steps?(changeset) do
true -> "hidden"
false -> nil
end
end
def selectable_options(tags) do
Enum.map(tags, &{&1.name, &1.id})
end
def pretty_state(%{aasm_state: "claimed"}), do: "Claimed"
def pretty_state(%{aasm_state: "listed"}), do: "Listed"
def pretty_state(%{aasm_state: "closed"}), do: "Closed"
def pretty_state(%{aasm_state: "acknowledged"}), do: "Claimed (Rescinded)"
def pretty_state(_dnp_entry), do: "Requested"
def show_steps?(changeset) do
not is_nil(changeset.action) and not changeset.data.state == :loaded
end
end