diff --git a/lib/philomena/artist_links.ex b/lib/philomena/artist_links.ex index 47f4d286..9a9de4a6 100644 --- a/lib/philomena/artist_links.ex +++ b/lib/philomena/artist_links.ex @@ -9,39 +9,19 @@ defmodule Philomena.ArtistLinks do alias Philomena.ArtistLinks.ArtistLink alias Philomena.ArtistLinks.AutomaticVerifier - alias Philomena.Badges.Badge - alias Philomena.Badges.Award - alias Philomena.Tags.Tag + alias Philomena.ArtistLinks.BadgeAwarder + alias Philomena.Tags @doc """ - Check links pending verification to see if the user placed - the appropriate code on the page. + Updates all artist links pending verification, by transitioning to link verified state + or resetting next update time. """ def automatic_verify! do - now = DateTime.utc_now() |> DateTime.truncate(:second) - - # Automatically retry in an hour if we don't manage to - # successfully verify any given link - recheck_time = DateTime.add(now, 3600, :second) - - recheck_query = - from ul in ArtistLink, - where: ul.aasm_state == "unverified", - where: ul.next_check_at < ^now - - recheck_query - |> Repo.all() - |> Enum.map(fn link -> - ArtistLink.automatic_verify_changeset( - link, - AutomaticVerifier.check_link(link, recheck_time) - ) - end) - |> Enum.map(&Repo.update!/1) + Enum.each(AutomaticVerifier.generate_updates(), &Repo.update!/1) end @doc """ - Gets a single artist_link. + Gets a single artist link. Raises `Ecto.NoResultsError` if the Artist link does not exist. @@ -57,7 +37,7 @@ defmodule Philomena.ArtistLinks do def get_artist_link!(id), do: Repo.get!(ArtistLink, id) @doc """ - Creates a artist_link. + Creates an artist link. ## Examples @@ -69,7 +49,7 @@ defmodule Philomena.ArtistLinks do """ def create_artist_link(user, attrs \\ %{}) do - tag = fetch_tag(attrs["tag_name"]) + tag = Tags.get_tag_or_alias_by_name(attrs["tag_name"]) %ArtistLink{} |> ArtistLink.creation_changeset(attrs, user, tag) @@ -77,7 +57,7 @@ defmodule Philomena.ArtistLinks do end @doc """ - Updates a artist_link. + Updates an artist link. ## Examples @@ -89,47 +69,71 @@ defmodule Philomena.ArtistLinks do """ def update_artist_link(%ArtistLink{} = artist_link, attrs) do - tag = fetch_tag(attrs["tag_name"]) + tag = Tags.get_tag_or_alias_by_name(attrs["tag_name"]) artist_link |> ArtistLink.edit_changeset(attrs, tag) |> Repo.update() end - def verify_artist_link(%ArtistLink{} = artist_link, user) do - artist_link_changeset = - artist_link - |> ArtistLink.verify_changeset(user) + @doc """ + Transitions an artist link to the verified state. + + ## Examples + + iex> verify_artist_link(artist_link, verifying_user) + {:ok, %ArtistLink{}} + + iex> verify_artist_link(artist_link, verifying_user) + :error + + """ + def verify_artist_link(%ArtistLink{} = artist_link, verifying_user) do + artist_link_changeset = ArtistLink.verify_changeset(artist_link, verifying_user) Multi.new() |> Multi.update(:artist_link, artist_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(limit(Badge, 1), title: "Artist"), - nil <- repo.get_by(limit(Award, 1), badge_id: badge.id, user_id: artist_link.user_id) do - %Award{ - badge_id: badge.id, - user_id: artist_link.user_id, - awarded_by_id: user.id, - awarded_on: now - } - |> Award.changeset(%{}) - |> repo.insert() - else - _ -> - {:ok, nil} - end - end) + |> Multi.run(:add_award, fn _repo, _changes -> BadgeAwarder.award_badge(artist_link) end) |> Repo.transaction() + |> case do + {:ok, %{artist_link: artist_link}} -> + {:ok, artist_link} + + {:error, _operation, _value, _changes} -> + :error + end end + @doc """ + Transitions an artist link to the rejected state. + + ## Examples + + iex> reject_artist_link(artist_link) + {:ok, %ArtistLink{}} + + iex> reject_artist_link(artist_link) + {:error, %Ecto.Changeset{}} + + """ def reject_artist_link(%ArtistLink{} = artist_link) do artist_link |> ArtistLink.reject_changeset() |> Repo.update() end + @doc """ + Transitions an artist link to the contacted state. + + ## Examples + + iex> contact_artist_link(artist_link) + {:ok, %ArtistLink{}} + + iex> contact_artist_link(artist_link) + {:error, %Ecto.Changeset{}} + + """ def contact_artist_link(%ArtistLink{} = artist_link, user) do artist_link |> ArtistLink.contact_changeset(user) @@ -137,7 +141,7 @@ defmodule Philomena.ArtistLinks do end @doc """ - Deletes a ArtistLink. + Deletes an artist link. ## Examples @@ -153,7 +157,7 @@ defmodule Philomena.ArtistLinks do end @doc """ - Returns an `%Ecto.Changeset{}` for tracking artist_link changes. + Returns an `%Ecto.Changeset{}` for tracking artist link changes. ## Examples @@ -165,24 +169,26 @@ defmodule Philomena.ArtistLinks do ArtistLink.changeset(artist_link, %{}) end + @doc """ + Counts the number of artist links which are pending moderation action, or + nil if the user is not permitted to moderate artist links. + + ## Examples + + iex> count_artist_links(normal_user) + nil + + iex> count_artist_links(admin_user) + 0 + + """ def count_artist_links(user) do if Canada.Can.can?(user, :index, %ArtistLink{}) do ArtistLink |> where([ul], ul.aasm_state in ^["unverified", "link_verified"]) - |> Repo.aggregate(:count, :id) + |> Repo.aggregate(:count) else nil end end - - defp fetch_tag(name) do - Tag - |> preload(:aliased_tag) - |> where(name: ^name) - |> Repo.one() - |> case do - nil -> nil - tag -> tag.aliased_tag || tag - end - end end diff --git a/lib/philomena/artist_links/automatic_verifier.ex b/lib/philomena/artist_links/automatic_verifier.ex index f2a5bebd..de9afcc9 100644 --- a/lib/philomena/artist_links/automatic_verifier.ex +++ b/lib/philomena/artist_links/automatic_verifier.ex @@ -1,5 +1,47 @@ defmodule Philomena.ArtistLinks.AutomaticVerifier do - def check_link(artist_link, recheck_time) do + @moduledoc """ + Artist link automatic verification. + + Artist links contain a random code which is generated when the link is created. If the user + places the code on their linked page and this verifier finds it, this expedites the process + of verifying a link for the moderator, as they can simply use the presence of the code in a + field controlled by the artist to ascertain the validity of the artist link. + """ + + alias Philomena.ArtistLinks.ArtistLink + alias Philomena.Repo + import Ecto.Query + + @doc """ + Check links pending verification to see if the user placed the appropriate code on the page. + + Polls each artist link in unverified state and generates a changeset to either set it to + link verified, if the code was found on the page, or reset the next check time, if the code + was not found. + + Returns a list of changesets with updated links. + """ + def generate_updates do + # Automatically retry in an hour if we don't manage to + # successfully verify any given link + now = DateTime.utc_now(:second) + recheck_time = DateTime.add(now, 3600, :second) + + Enum.map(links_to_check(now), fn link -> + ArtistLink.automatic_verify_changeset(link, check_link(link, recheck_time)) + end) + end + + defp links_to_check(now) do + recheck_query = + from ul in ArtistLink, + where: ul.aasm_state == "unverified", + where: ul.next_check_at < ^now + + Repo.all(recheck_query) + end + + defp check_link(artist_link, recheck_time) do artist_link.uri |> PhilomenaProxy.Http.get() |> contains_verification_code?(artist_link.verification_code) diff --git a/lib/philomena/artist_links/badge_awarder.ex b/lib/philomena/artist_links/badge_awarder.ex new file mode 100644 index 00000000..ae231c74 --- /dev/null +++ b/lib/philomena/artist_links/badge_awarder.ex @@ -0,0 +1,28 @@ +defmodule Philomena.ArtistLinks.BadgeAwarder do + @moduledoc """ + Handles awarding a badge to the user of an associated artist link. + """ + + alias Philomena.Badges + + @badge_title "Artist" + + @doc """ + Awards a badge to an artist with a verified link. + + If the badge with the title `"Artist"` does not exist, no award will be created. + If the user already has an award with that badge title, no award will be created. + + Returns `{:ok, award}`, `{:ok, nil}`, or `{:error, changeset}`. The return value is + suitable for use as the return value to an `Ecto.Multi.run/3` callback. + """ + def award_badge(artist_link) do + with badge when not is_nil(badge) <- Badges.get_badge_by_title(@badge_title), + award when is_nil(award) <- Badges.get_badge_award_for(badge, artist_link.user) do + Badges.create_badge_award(artist_link.user, artist_link.user, %{badge_id: badge.id}) + else + _ -> + {:ok, nil} + end + end +end diff --git a/lib/philomena/badges.ex b/lib/philomena/badges.ex index d917f147..494dd752 100644 --- a/lib/philomena/badges.ex +++ b/lib/philomena/badges.ex @@ -38,6 +38,22 @@ defmodule Philomena.Badges do """ def get_badge!(id), do: Repo.get!(Badge, id) + @doc """ + Gets a single badge by its title. + + Returns nil if the Badge does not exist. + + ## Examples + + iex> get_badge_by_title("Artist") + %Badge{} + + iex> get_badge_by_title("Nonexistent") + nil + + """ + def get_badge_by_title(title), do: Repo.get_by(Badge, title: title) + @doc """ Creates a badge. @@ -162,6 +178,24 @@ defmodule Philomena.Badges do """ def get_badge_award!(id), do: Repo.get!(Award, id) + @doc """ + Gets a the badge_award with the given badge type belonging to the user. + + Raises nil if the Badge award does not exist. + + ## Examples + + iex> get_badge_award_for(badge, user) + %Award{} + + iex> get_badge_award_for(badge, user) + nil + + """ + def get_badge_award_for(badge, user) do + Repo.get_by(Award, badge_id: badge.id, user_id: user.id) + end + @doc """ Creates a badge_award. diff --git a/lib/philomena/tags.ex b/lib/philomena/tags.ex index de7a0171..0d759e93 100644 --- a/lib/philomena/tags.ex +++ b/lib/philomena/tags.ex @@ -81,6 +81,31 @@ defmodule Philomena.Tags do """ def get_tag!(id), do: Repo.get!(Tag, id) + @doc """ + Gets a single tag by its name, or the tag it is aliased to, if it is aliased. + + Returns nil if the tag does not exist. + + ## Examples + + iex> get_tag_or_alias_by_name("safe") + %Tag{} + + iex> get_tag_or_alias_by_name("nonexistent") + nil + + """ + def get_tag_or_alias_by_name(name) do + Tag + |> where(name: ^name) + |> preload(:aliased_tag) + |> Repo.one() + |> case do + nil -> nil + tag -> tag.aliased_tag || tag + end + end + @doc """ Creates a tag. diff --git a/lib/philomena_web/controllers/admin/artist_link/verification_controller.ex b/lib/philomena_web/controllers/admin/artist_link/verification_controller.ex index a1a2f86a..3694f812 100644 --- a/lib/philomena_web/controllers/admin/artist_link/verification_controller.ex +++ b/lib/philomena_web/controllers/admin/artist_link/verification_controller.ex @@ -13,12 +13,12 @@ defmodule PhilomenaWeb.Admin.ArtistLink.VerificationController do preload: [:user] def create(conn, _params) do - {:ok, result} = + {:ok, artist_link} = ArtistLinks.verify_artist_link(conn.assigns.artist_link, conn.assigns.current_user) conn |> put_flash(:info, "Artist link successfully verified.") - |> moderation_log(details: &log_details/2, data: result.artist_link) + |> moderation_log(details: &log_details/2, data: artist_link) |> redirect(to: ~p"/admin/artist_links") end