admin user links

This commit is contained in:
byte[] 2019-12-09 20:21:49 -05:00
parent 16843ec216
commit 7d247b777f
24 changed files with 384 additions and 63 deletions

View file

@ -4,9 +4,12 @@ defmodule Philomena.UserLinks do
""" """
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Ecto.Multi
alias Philomena.Repo alias Philomena.Repo
alias Philomena.UserLinks.UserLink alias Philomena.UserLinks.UserLink
alias Philomena.Badges.Badge
alias Philomena.Badges.Award
alias Philomena.Tags.Tag alias Philomena.Tags.Tag
@doc """ @doc """
@ -71,8 +74,46 @@ defmodule Philomena.UserLinks do
""" """
def update_user_link(%UserLink{} = user_link, attrs) do def update_user_link(%UserLink{} = user_link, attrs) do
tag = Repo.get_by(Tag, name: attrs["tag_name"])
user_link user_link
|> UserLink.changeset(attrs) |> UserLink.edit_changeset(attrs, tag)
|> Repo.update()
end
def verify_user_link(%UserLink{} = user_link, user) do
user_link_changeset =
user_link
|> UserLink.verify_changeset(user)
Multi.new()
|> Multi.update(:user_link, user_link_changeset)
|> Multi.run(:add_award, fn repo, _changes ->
now = DateTime.utc_now() |> DateTime.truncate(:second)
with badge when not is_nil(badge) <- repo.get_by(Badge, title: "Artist"),
nil <- repo.get_by(Award, badge_id: badge.id, user_id: user_link.user_id)
do
%Award{badge_id: badge.id, user_id: user_link.user_id, awarded_by_id: user.id, awarded_on: now}
|> Award.changeset()
|> repo.insert()
else
_ ->
{:ok, nil}
end
end)
|> Repo.isolated_transaction(:serializable)
end
def reject_user_link(%UserLink{} = user_link) do
user_link
|> UserLink.reject_changeset()
|> Repo.update()
end
def contact_user_link(%UserLink{} = user_link, user) do
user_link
|> UserLink.contact_changeset(user)
|> Repo.update() |> Repo.update()
end end
@ -106,7 +147,7 @@ defmodule Philomena.UserLinks do
end end
def count_user_links(user) do def count_user_links(user) do
if Canada.Can.can?(user, :edit, UserLink) do if Canada.Can.can?(user, :index, UserLink) do
UserLink UserLink
|> where(aasm_state: "unverified") |> where(aasm_state: "unverified")
|> Repo.aggregate(:count, :id) |> Repo.aggregate(:count, :id)

View file

@ -30,17 +30,44 @@ defmodule Philomena.UserLinks.UserLink do
|> validate_required([]) |> validate_required([])
end end
def edit_changeset(user_link, attrs, tag) do
user_link
|> cast(attrs, [:uri, :public])
|> put_change(:tag_id, tag.id)
|> validate_required([:user, :uri, :public])
|> parse_uri()
end
def creation_changeset(user_link, attrs, user, tag) do def creation_changeset(user_link, attrs, user, tag) do
user_link user_link
|> cast(attrs, [:uri]) |> cast(attrs, [:uri, :public])
|> put_assoc(:tag, tag) |> put_assoc(:tag, tag)
|> put_assoc(:user, user) |> put_assoc(:user, user)
|> validate_required([:user, :uri]) |> validate_required([:user, :uri, :public])
|> parse_uri() |> parse_uri()
|> put_verification_code() |> put_verification_code()
|> put_next_check_at() |> put_next_check_at()
end end
def reject_changeset(user_link) do
change(user_link, aasm_state: "rejected")
end
def verify_changeset(user_link, user) do
change(user_link)
|> put_change(:verified_by_user_id, user.id)
|> put_change(:aasm_state, "verified")
end
def contact_changeset(user_link, user) do
now = DateTime.utc_now() |> DateTime.truncate(:second)
change(user_link)
|> put_change(:contacted_by_user_id, user.id)
|> put_change(:contacted_at, now)
|> put_change(:aasm_state, "contacted")
end
defp parse_uri(changeset) do defp parse_uri(changeset) do
string_uri = get_field(changeset, :uri) |> to_string() string_uri = get_field(changeset, :uri) |> to_string()
uri = URI.parse(string_uri) uri = URI.parse(string_uri)

View file

@ -47,6 +47,12 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
def can?(%User{role: "moderator"}, :show, %Report{}), do: true def can?(%User{role: "moderator"}, :show, %Report{}), do: true
def can?(%User{role: "moderator"}, :edit, %Report{}), do: true def can?(%User{role: "moderator"}, :edit, %Report{}), do: true
# Manage user links
def can?(%User{role: "moderator"}, :create_links, %User{}), do: true
def can?(%User{role: "moderator"}, :edit_links, %User{}), do: true
def can?(%User{role: "moderator"}, :edit, %UserLink{}), do: true
def can?(%User{role: "moderator"}, :index, UserLink), do: true
# #
# Assistants can... # Assistants can...
# #
@ -107,6 +113,7 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
def can?(%User{id: id}, action, %Filter{user_id: id}) when action in [:edit, :update], do: true def can?(%User{id: id}, action, %Filter{user_id: id}) when action in [:edit, :update], do: true
# View user links they've created # View user links they've created
def can?(%User{id: id}, :create_links, %User{id: id}), do: true
def can?(%User{id: id}, :show, %UserLink{user_id: id}), do: true def can?(%User{id: id}, :show, %UserLink{user_id: id}), do: true
# Edit their commissions # Edit their commissions

View file

@ -0,0 +1,17 @@
defmodule PhilomenaWeb.Admin.UserLink.ContactController do
use PhilomenaWeb, :controller
alias Philomena.UserLinks.UserLink
alias Philomena.UserLinks
plug PhilomenaWeb.CanaryMapPlug, create: :edit
plug :load_and_authorize_resource, model: UserLink, id_name: "user_link_id", persisted: true, preload: [:user]
def create(conn, _params) do
{:ok, user_link} = UserLinks.contact_user_link(conn.assigns.user_link, conn.assigns.current_user)
conn
|> put_flash(:info, "User link successfully marked as contacted.")
|> redirect(to: Routes.profile_user_link_path(conn, :show, conn.assigns.user_link.user, user_link))
end
end

View file

@ -0,0 +1,17 @@
defmodule PhilomenaWeb.Admin.UserLink.RejectController do
use PhilomenaWeb, :controller
alias Philomena.UserLinks.UserLink
alias Philomena.UserLinks
plug PhilomenaWeb.CanaryMapPlug, create: :edit
plug :load_and_authorize_resource, model: UserLink, id_name: "user_link_id", persisted: true, preload: [:user]
def create(conn, _params) do
{:ok, user_link} = UserLinks.reject_user_link(conn.assigns.user_link)
conn
|> put_flash(:info, "User link successfully marked as rejected.")
|> redirect(to: Routes.profile_user_link_path(conn, :show, conn.assigns.user_link.user, user_link))
end
end

View file

@ -0,0 +1,17 @@
defmodule PhilomenaWeb.Admin.UserLink.VerificationController do
use PhilomenaWeb, :controller
alias Philomena.UserLinks.UserLink
alias Philomena.UserLinks
plug PhilomenaWeb.CanaryMapPlug, create: :edit
plug :load_and_authorize_resource, model: UserLink, id_name: "user_link_id", persisted: true, preload: [:user]
def create(conn, _params) do
{:ok, %{user_link: user_link}} = UserLinks.verify_user_link(conn.assigns.user_link, conn.assigns.current_user)
conn
|> put_flash(:info, "User link successfully verified.")
|> redirect(to: Routes.profile_user_link_path(conn, :show, conn.assigns.user_link.user, user_link))
end
end

View file

@ -0,0 +1,45 @@
defmodule PhilomenaWeb.Admin.UserLinkController do
use PhilomenaWeb, :controller
alias Philomena.UserLinks.UserLink
alias Philomena.Repo
import Ecto.Query
plug :verify_authorized
def index(conn, %{"all" => _value}) do
load_links(UserLink, conn)
end
def index(conn, %{"q" => query}) do
query = "%#{query}%"
UserLink
|> join(:inner, [ul], _ in assoc(ul, :user))
|> where([ul, u], ilike(u.name, ^query) or ilike(ul.uri, ^query))
|> load_links(conn)
end
def index(conn, _params) do
UserLink
|> where([u], u.aasm_state in ^["unverified", "link_verified", "contacted"])
|> load_links(conn)
end
defp load_links(queryable, conn) do
links =
queryable
|> order_by(desc: :created_at)
|> preload([:tag, :verified_by_user, :contacted_by_user, user: [:linked_tags, awards: :badge]])
|> Repo.paginate(conn.assigns.scrivener)
render(conn, "index.html", user_links: links)
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :index, UserLink) do
true -> conn
false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
end

View file

@ -0,0 +1,72 @@
defmodule PhilomenaWeb.Profile.UserLinkController do
use PhilomenaWeb, :controller
alias Philomena.UserLinks.UserLink
alias Philomena.UserLinks
alias Philomena.Users.User
alias Philomena.Repo
import Ecto.Query
plug PhilomenaWeb.FilterBannedUsersPlug when action in [:new, :create]
plug :load_and_authorize_resource, model: UserLink, only: [:show, :edit, :update], preload: [:user, :tag, :contacted_by_user]
plug PhilomenaWeb.CanaryMapPlug,
index: :create_links,
new: :create_links,
create: :create_links,
show: :create_links,
edit: :edit_links,
update: :edit_links
plug :load_and_authorize_resource, model: User, id_field: "slug", id_name: "profile_id", persisted: true
def index(conn, _params) do
user = conn.assigns.current_user
user_links =
UserLink
|> where(user_id: ^user.id)
|> Repo.all()
render(conn, "index.html", user_links: user_links)
end
def new(conn, _params) do
changeset = UserLinks.change_user_link(%UserLink{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user_link" => user_link_params}) do
case UserLinks.create_user_link(conn.assigns.user, user_link_params) do
{:ok, user_link} ->
conn
|> put_flash(:info, "Link submitted! Please put '#{user_link.verification_code}' on your linked webpage now.")
|> redirect(to: Routes.profile_user_link_path(conn, :show, conn.assigns.user_link, user_link))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def show(conn, _params) do
user_link = conn.assigns.user_link
render(conn, "show.html", user_link: user_link)
end
def edit(conn, _params) do
changeset = UserLinks.change_user_link(conn.assigns.user_link)
render(conn, "edit.html", changeset: changeset)
end
def update(conn, %{"user_link" => user_link_params}) do
case UserLinks.update_user_link(conn.assigns.user_link, user_link_params) do
{:ok, user_link} ->
conn
|> put_flash(:info, "Link successfully updated.")
|> redirect(to: Routes.profile_user_link_path(conn, :show, conn.assigns.user, user_link))
{:error, changeset} ->
render(conn, "edit.html", changeset: changeset)
end
end
end

View file

@ -14,7 +14,7 @@ defmodule PhilomenaWeb.ProfileController do
import Ecto.Query import Ecto.Query
plug :load_and_authorize_resource, model: User, only: :show, id_field: "slug", preload: [ plug :load_and_authorize_resource, model: User, only: :show, id_field: "slug", preload: [
awards: :badge, public_links: :tag, commission: [sheet_image: :tags, items: [example_image: :tags]] awards: :badge, public_links: :tag, verified_links: :tag, commission: [sheet_image: :tags, items: [example_image: :tags]]
] ]
def show(conn, _params) do def show(conn, _params) do

View file

@ -1,45 +0,0 @@
defmodule PhilomenaWeb.UserLinkController do
use PhilomenaWeb, :controller
alias Philomena.UserLinks
alias Philomena.UserLinks.UserLink
alias Philomena.Repo
import Ecto.Query
plug PhilomenaWeb.FilterBannedUsersPlug when action in [:new, :create]
plug :load_and_authorize_resource, model: UserLink, only: [:show], preload: [:user, :tag, :contacted_by_user]
def index(conn, _params) do
user = conn.assigns.current_user
user_links =
UserLink
|> where(user_id: ^user.id)
|> Repo.all()
render(conn, "index.html", user_links: user_links)
end
def new(conn, _params) do
changeset = UserLinks.change_user_link(%UserLink{})
render(conn, "new.html", changeset: changeset)
end
def create(conn, %{"user_link" => user_link_params}) do
user = conn.assigns.current_user
case UserLinks.create_user_link(user, user_link_params) do
{:ok, user_link} ->
conn
|> put_flash(:info, "Link submitted! Please put '#{user_link.verification_code}' on your linked webpage now.")
|> redirect(to: Routes.user_link_path(conn, :show, user_link))
{:error, %Ecto.Changeset{} = changeset} ->
render(conn, "new.html", changeset: changeset)
end
end
def show(conn, _params) do
user_link = conn.assigns.user_link
render(conn, "show.html", user_link: user_link)
end
end

View file

@ -136,6 +136,7 @@ defmodule PhilomenaWeb.Router do
resources "/reports", Profile.Commission.ReportController, only: [:new, :create] resources "/reports", Profile.Commission.ReportController, only: [:new, :create]
end end
resources "/description", Profile.DescriptionController, only: [:edit, :update], singleton: true resources "/description", Profile.DescriptionController, only: [:edit, :update], singleton: true
resources "/user_links", Profile.UserLinkController
end end
scope "/filters", Filter, as: :filter do scope "/filters", Filter, as: :filter do
@ -151,7 +152,6 @@ defmodule PhilomenaWeb.Router do
resources "/avatar", AvatarController, only: [:edit, :update, :delete], singleton: true resources "/avatar", AvatarController, only: [:edit, :update, :delete], singleton: true
resources "/reports", ReportController, only: [:index] resources "/reports", ReportController, only: [:index]
resources "/user_links", UserLinkController, only: [:index, :new, :create, :show]
resources "/galleries", GalleryController, only: [:new, :create, :edit, :update, :delete] do resources "/galleries", GalleryController, only: [:new, :create, :edit, :update, :delete] do
resources "/images", Gallery.ImageController, only: [:create, :delete], singleton: true resources "/images", Gallery.ImageController, only: [:create, :delete], singleton: true
resources "/order", Gallery.OrderController, only: [:update], singleton: true resources "/order", Gallery.OrderController, only: [:update], singleton: true
@ -172,6 +172,12 @@ defmodule PhilomenaWeb.Router do
resources "/claim", Report.ClaimController, only: [:create, :delete], singleton: true resources "/claim", Report.ClaimController, only: [:create, :delete], singleton: true
resources "/close", Report.CloseController, only: [:create], singleton: true resources "/close", Report.CloseController, only: [:create], singleton: true
end end
resources "/user_links", UserLinkController, only: [:index] do
resources "/verification", UserLink.VerificationController, only: [:create], singleton: true
resources "/contact", UserLink.ContactController, only: [:create], singleton: true
resources "/reject", UserLink.RejectController, only: [:create], singleton: true
end
end end
resources "/duplicate_reports", DuplicateReportController, only: [] do resources "/duplicate_reports", DuplicateReportController, only: [] do

View file

@ -0,0 +1,78 @@
h1 User Links
p Link creation is done via the Users menu.
p Verifying a link will automatically award an artist badge if the link is public, no artist badge exists, and an "artist:" tag is specified.
= form_for :user_link, Routes.admin_user_link_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_user_link_path(@conn, :index, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @user_links, route: route, params: link_scope(@conn), conn: @conn
.block
.block__header
= if @conn.params["all"] do
= link "Show unverified only", to: Routes.admin_user_link_path(@conn, :index)
- else
= link "Show all", to: Routes.admin_user_link_path(@conn, :index, all: "true")
= pagination
.block__content
table.table
thead
tr
th State
th User
th URL
th Options
th Mark
th Public
tbody
= for link <- @user_links do
tr
td class=link_state_class(link)
strong
= link_state_name(link)
= if contacted?(link) do
br
' by
= link.contacted_by_user.name
br
| (
= pretty_time link.contacted_at
| )
td
= render PhilomenaWeb.UserAttributionView, "_user.html", object: link, awards: true, conn: @conn
= render PhilomenaWeb.TagView, "_tag_list.html", tags: display_order(link.user.linked_tags), conn: @conn
td
= link String.slice(link.uri, 0, 100), to: link.uri
= if link.tag do
br
= render PhilomenaWeb.TagView, "_tag.html", tag: link.tag, conn: @conn
td
=> link "View", to: Routes.profile_user_link_path(@conn, :show, link.user, link)
' &bull;
= link "Edit", to: Routes.profile_user_link_path(@conn, :edit, link.user, link)
td
=> link "Verify", to: Routes.admin_user_link_verification_path(@conn, :create, link), method: :post
' &bull;
=> link "Reject", to: Routes.admin_user_link_reject_path(@conn, :create, link), method: :post
br
= if not verified?(link) do
= if contacted?(link) do
' Artist contacted
- else
= link "Artist contacted", to: Routes.admin_user_link_contact_path(@conn, :create, link), method: :post
td
= public_text(link)
.block__header.block__header--light
= pagination

View file

@ -75,7 +75,7 @@ header.header
a.header__link href="/posts?pq=my:posts" a.header__link href="/posts?pq=my:posts"
i.fas.fa-fw.fa-pen-square> i.fas.fa-fw.fa-pen-square>
| Posts | Posts
a.header__link href='/user_links' a.header__link href=Routes.profile_user_link_path(@conn, :index, @current_user)
i.fa.fa-fw.fa-link> i.fa.fa-fw.fa-link>
| Links | Links
a.header__link href='/settings/edit' a.header__link href='/settings/edit'

View file

@ -57,7 +57,7 @@
span.header__counter__admin span.header__counter__admin
= @report_count = @report_count
= if @user_link_count do = if @user_link_count do
= link to: "#", class: "header__link", title: "User Links" do = link to: Routes.admin_user_link_path(@conn, :index), class: "header__link", title: "User Links" do
' L ' L
span.header__counter__admin span.header__counter__admin
= @user_link_count = @user_link_count

View file

@ -52,7 +52,7 @@
.block .block
.block__header .block__header
span.block__header__title User Links span.block__header__title User Links
= for link <- @user.public_links do = for link <- @user.verified_links, link.public or can?(@conn, :edit, link) do
.block__content.alternating-color.break-word .block__content.alternating-color.break-word
.center .center
= if link.tag do = if link.tag do

View file

@ -3,7 +3,7 @@ h1
a href=Routes.profile_path(@conn, :show, @user) a href=Routes.profile_path(@conn, :show, @user)
= @user.name = @user.name
- route = fn p -> Routes.profile_source_change_path(@conn, :index, @image, p) end - route = fn p -> Routes.profile_source_change_path(@conn, :index, @user, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @source_changes, route: route, conn: @conn - pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @source_changes, route: route, conn: @conn
= render PhilomenaWeb.SourceChangeView, "index.html", conn: @conn, source_changes: @source_changes, pagination: pagination = render PhilomenaWeb.SourceChangeView, "index.html", conn: @conn, source_changes: @source_changes, pagination: pagination

View file

@ -15,15 +15,18 @@
em artist name here em artist name here
' or a series name ' or a series name
p Should be blank only if your content isn't on the site, generally p Should be blank only if your content isn't on the site, generally
= text_input f, :tag_name, class: "input", autocomplete: "off", placeholder: "artist:name", data: [ac: "true", ac_min_length: "3", ac_source: "/tags/autocomplete?term="] = text_input f, :tag_name, value: assigns[:tag_name], class: "input", autocomplete: "off", placeholder: "artist:name", data: [ac: "true", ac_min_length: "3", ac_source: "/tags/autocomplete?term="]
.field .field
label for="uri" label for="uri"
' URL of your art webpage ' URL of your art webpage
= url_input f, :uri, class: "input input--wide", placeholder: "https://www.deviantart.com/your-name"#, required: true = url_input f, :uri, class: "input input--wide", placeholder: "https://www.deviantart.com/your-name", required: true
= error_tag f, :uri = error_tag f, :uri
.field .field
=> radio_button f, :public, "true" => radio_button f, :public, "true"
=> label f, :public, "Visible to everyone" => label f, :public, "Visible to everyone"
.field .field
=> radio_button f, :public, "false" => radio_button f, :public, "false"
=> label f, :public, "Visible only to site staff" => label f, :public, "Visible only to site staff"

View file

@ -0,0 +1,2 @@
h1 Edit Link
= render PhilomenaWeb.Profile.UserLinkView, "_form.html", conn: @conn, changeset: @changeset, tag_name: @user_link.tag.name, action: Routes.profile_user_link_path(@conn, :update, @user_link.user, @user_link)

View file

@ -1,6 +1,6 @@
h1 Your Links h1 Your Links
p p
a.button href=Routes.user_link_path(@conn, :new) a.button href=Routes.profile_user_link_path(@conn, :new, @user)
' Create a link ' Create a link
p p
' User links associate your account on Derpibooru with tags about content you create and with accounts on sites elsewhere. This allows users to easily identify artists and admins to act more rapidly on takedown requests. ' User links associate your account on Derpibooru with tags about content you create and with accounts on sites elsewhere. This allows users to easily identify artists and admins to act more rapidly on takedown requests.
@ -17,7 +17,7 @@ table.table
= for link <- @user_links do = for link <- @user_links do
tr tr
td = link link.uri, to: link.uri td = link link.uri, to: link.uri
td = link "View Details", to: Routes.user_link_path(@conn, :show, link) td = link "View Details", to: Routes.profile_user_link_path(@conn, :show, @user, link)
td = link.verification_code td = link.verification_code
th = verified_as_string(link) th = verified_as_string(link)
th = public_as_string(link) th = public_as_string(link)

View file

@ -0,0 +1,2 @@
h1 Create Link
= render PhilomenaWeb.UserLinkView, "_form.html", changeset: @changeset, action: Routes.profile_user_link_path(@conn, :create, @user), conn: @conn

View file

@ -56,4 +56,4 @@ h3 Associated tag
- else - else
p There is no tag associated with this link. p There is no tag associated with this link.
= link "Back", to: Routes.user_link_path(@conn, :index) = link "Back", to: Routes.profile_user_link_path(@conn, :index, @user)

View file

@ -1,2 +0,0 @@
h1 Create Link
= render PhilomenaWeb.UserLinkView, "_form.html", changeset: @changeset, action: Routes.user_link_path(@conn, :create)

View file

@ -0,0 +1,34 @@
defmodule PhilomenaWeb.Admin.UserLinkView do
use PhilomenaWeb, :view
import Philomena.Tags.Tag, only: [display_order: 1]
def link_state_class(%{aasm_state: state}) when state in ["verified", "link_verified"], do: "success"
def link_state_class(%{aasm_state: state}) when state in ["unverified", "rejected"], do: "danger"
def link_state_class(%{aasm_state: "contacted"}), do: "warning"
def link_state_class(_link), do: nil
def link_state_name(%{aasm_state: state}) do
state
|> String.replace("_", " ")
|> String.capitalize()
end
def link_scope(conn) do
case conn.params["all"] do
nil -> []
_val -> [all: true]
end
end
def contacted?(%{aasm_state: state}), do: state == "contacted"
def verified?(%{aasm_state: state}), do: state == "verified"
def link_verified?(%{aasm_state: state}), do: state == "link_verified"
def unverified?(%{aasm_state: state}), do: state == "unverified"
def rejected?(%{aasm_state: state}), do: state == "rejected"
def public_text(%{public: true}), do: "Yes"
def public_text(_user_link), do: "No"
def public?(%{public: public}), do: !!public
end

View file

@ -1,4 +1,4 @@
defmodule PhilomenaWeb.UserLinkView do defmodule PhilomenaWeb.Profile.UserLinkView do
use PhilomenaWeb, :view use PhilomenaWeb, :view
def verified?(%{aasm_state: state}), do: state == "verified" def verified?(%{aasm_state: state}), do: state == "verified"