duplicate reports

This commit is contained in:
byte[] 2019-11-30 23:51:44 -05:00
parent b972c85675
commit bec983fdf1
8 changed files with 538 additions and 0 deletions

View file

@ -0,0 +1,57 @@
defmodule PhilomenaWeb.DuplicateReportController do
use PhilomenaWeb, :controller
alias Philomena.DuplicateReports
alias Philomena.DuplicateReports.DuplicateReport
alias Philomena.Images.Image
alias Philomena.Repo
import Ecto.Query
@valid_states ~W(open rejected accepted claimed)
plug PhilomenaWeb.FilterBannedUsersPlug when action in [:create]
plug :load_resource, model: DuplicateReport, only: [:show], preload: [:user, image: :tags, duplicate_of_image: :tags]
def index(conn, params) do
states =
params["states"]
|> wrap()
|> Enum.filter(&Enum.member?(@valid_states, &1))
duplicate_reports =
DuplicateReport
|> where([d], d.state in ^states)
|> preload([:user, image: :tags, duplicate_of_image: :tags])
|> order_by(desc: :created_at)
|> Repo.paginate(conn.assigns.pagination)
render(conn, "index.html", duplicate_reports: duplicate_reports, layout_class: "layout--wide")
end
def create(conn, %{"duplicate_report" => duplicate_report_params}) do
attribution = conn.assigns.attribution
source = Repo.get!(Image, duplicate_report_params["image_id"])
target = Repo.get!(Image, duplicate_report_params["duplicate_of_image_id"])
case DuplicateReports.create_duplicate_report(source, target, attribution, duplicate_report_params) do
{:ok, duplicate_report} ->
conn
|> put_flash(:info, "Duplicate report created successfully.")
|> redirect(to: Routes.image_path(conn, :show, duplicate_report.image_id))
{:error, _changeset} ->
conn
|> put_flash(:error, "Failed to submit duplicate report")
|> redirect(external: conn.assigns.referrer)
end
end
def show(conn, _params) do
dr = conn.assigns.duplicate_report
render(conn, "show.html", duplicate_report: dr, layout_class: "layout--wide")
end
defp wrap(list) when is_list(list), do: list
defp wrap(not_a_list), do: [not_a_list]
end

View file

@ -155,6 +155,7 @@ defmodule PhilomenaWeb.Router do
resources "/stats", StatController, only: [:index]
resources "/channels", ChannelController, only: [:index, :show]
resources "/settings", SettingController, only: [:edit, :update], singleton: true
resources "/duplicate_reports", DuplicateReportController, only: [:index, :show]
get "/:id", ImageController, :show
# get "/:forum_id", ForumController, :show # impossible to do without constraints

View file

@ -0,0 +1,29 @@
.grid--dupe-report-list__cell.flex.flex--column.flex--spaced-out.flex--centered.flex--no-wrap.center.dr__image-cell.border-vertical
p
- if is_nil(@image) do
| (Image now hard-deleted)
- else
| #
= @image.id
= render PhilomenaWeb.ImageView, "_image_container.html", image: @image, size: @thumb_small, conn: @conn
p
= @image.image_width
| x
= @image.image_height
p
= render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @image, conn: @conn
/- if report.valid? && can_manage_dr
- if source
a href=duplicate_report_accept_reverse_path(report) data-method="post"
button.button
' Keep Source
i.fa.fa-arrow-left
- else
a href=duplicate_report_accept_path(report) data-method="post"
button.button
i.fa.fa-arrow-right>
| Keep Target

View file

@ -0,0 +1,155 @@
.grid.grid--dupe-report-list
p Source image
p Target image
p Diff
p Status/options
= for report <- @duplicate_reports do
- background_class = background_class(report)
= render PhilomenaWeb.DuplicateReportView, "_image_cell.html", image: report.image, source: true, report: report, conn: @conn
= render PhilomenaWeb.DuplicateReportView, "_image_cell.html", image: report.duplicate_of_image, source: false, report: report, conn: @conn
.grid--dupe-report-list__cell.dr__diff.border-vertical
table.table
tr
= if same_aspect_ratio?(report) do
td.success
a href=Routes.duplicate_report_path(@conn, :show, report)
' Visual diff
| (Same aspect ratio)
- else
td.warning Different aspect ratio
tr
= cond do
- both_are_edits?(report) ->
td.warning Both are edits
- target_is_edit?(report) ->
td.danger Target is an edit
- source_is_edit?(report) ->
td.danger Source is an edit
- true ->
td.success Neither is an edit
tr
= cond do
- both_are_alternate_versions?(report) ->
td.warning Both are alternate versions
- target_is_alternate_version?(report) ->
td.danger Target is an alternate version
- source_is_alternate_version?(report) ->
td.danger Source is an alternate version
- true ->
td.success Neither is an alternate version
tr
= cond do
- same_res?(report) ->
td.sucecss Same resolution
- higher_res?(report) ->
td.warning Target resolution better
- true ->
td.warning Source resolution better
tr
= cond do
- same_format?(report) ->
td.success
' Same format
= file_types(report)
- better_format?(report) ->
td.warning
' Target format better
= file_types(report)
- true ->
td.warning
' Source format better
= file_types(report)
tr
= cond do
- same_artist_tags?(report) ->
td.success Same artist tags
- more_artist_tags_on_target?(report) ->
td.warning More artist tags on target
- more_artist_tags_on_source?(report) ->
td.warning More artist tags on source
- true ->
td.danger Different artist tags
tr
= cond do
- neither_have_source?(report) ->
td.warning Neither have sources
- same_source?(report) ->
td.success Same sources
- similar_source?(report) ->
td.warning Same hostname
- source_on_target?(report) ->
td.warning Target has a source
- source_on_source?(report) ->
td.warning Source has a source
- true ->
td.danger Different sources
tr
= if same_rating_tags?(report) do
td.success Same rating tags
- else
td.danger Different rating tags
tr
= if forward_merge?(report) do
td.warning Target newer
- else
td.success Target older
.flex.flex--column.grid--dupe-report-list__cell.border-vertical id="report_options_#{report.id}"
.dr__status-options class=background_class
= String.capitalize(report.state)
/- if can_manage_dr && report.modifier.present?
' by
= report.modifier.name
/- if can_manage_dr
div
- if report.state == 'open'
a href=duplicate_report_claim_path(report, target: "report_options_#{report.id}") data-method="post"
button.button.button--separate-right
i.fa.fa-clipboard>
| Claim
- if report.state != 'rejected'
a href=duplicate_report_reject_path(report) data-method="post"
button.button
i.fa.fa-times>
| Reject
.dr__status-options
div
' Reported
=> pretty_time(report.created_at)
= if report.user do
' by
=< link report.user.name, to: Routes.profile_path(@conn, :show, report.user)
= report.reason

View file

@ -0,0 +1,24 @@
h1 Duplicate Reports
- route = fn p -> Routes.duplicate_report_path(@conn, :index, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @duplicate_reports, route: route, conn: @conn
.block
.block__header
= pagination
span.block__header__title Display only:
=> link "Open (All)", to: Routes.duplicate_report_path(@conn, :index, states: ~W(open claimed))
=> link "Open (Unclaimed)", to: Routes.duplicate_report_path(@conn, :index, states: ~W(open))
=> link "Open (Claimed)", to: Routes.duplicate_report_path(@conn, :index, states: ~W(claimed))
=> link "Open + Rejected", to: Routes.duplicate_report_path(@conn, :index, states: ~W(open rejected))
=> link "Rejected", to: Routes.duplicate_report_path(@conn, :index, states: ~W(rejected))
=> link "Rejected + Accepted", to: Routes.duplicate_report_path(@conn, :index, states: ~W(rejected accepted))
=> link "Accepted", to: Routes.duplicate_report_path(@conn, :index, states: ~W(accepted))
= link "All", to: Routes.duplicate_report_path(@conn, :index, states: ~W(open rejected accepted claimed))
= render PhilomenaWeb.DuplicateReportView, "_list.html", duplicate_reports: @duplicate_reports, conn: @conn
.block
.block__header.block__header--light
= pagination

View file

@ -0,0 +1,49 @@
elixir:
source_url = comparison_url(@conn, @duplicate_report.image)
target_url = comparison_url(@conn, @duplicate_report.duplicate_of_image)
{width, height} = largest_dimensions([@duplicate_report.image, @duplicate_report.duplicate_of_image])
h1 Difference
.difference
svg.difference__image viewBox="0 0 #{width} #{height}" height=height
defs
filter#overlay-diff
feImage#source xlink:href=source_url result="source" width="100%" height="100%" x="0" y="0"
feImage#target xlink:href=target_url result="target" width="100%" height="100%" x="0" y="0"
feBlend in="source" in2="target" mode="difference" result="diff"
/ Contrast-boost matrix = (5I|0) [4x5]
feColorMatrix in="diff" type="matrix" values="5 0 0 0 0 0 5 0 0 0 0 0 5 0 0 0 0 0 5 0"
rect width=width height=height filter="url(#overlay-diff)"
h1 Swipe
.swipe
svg.swipe__image viewBox="0 0 #{width} #{height}" height=height
defs
pattern#checkerboard width="16" height="16" patternUnits="userSpaceOnUse"
rect width="8" height="8" x="0" y="0" fill="#ffffff44"
rect width="8" height="8" x="0" y="8" fill="#00000044"
rect width="8" height="8" x="8" y="0" fill="#00000044"
rect width="8" height="8" x="8" y="8" fill="#ffffff44"
clipPath#clip
rect width=div(width, 2) height=height
rect width=width height=height fill="url(#checkerboard)"
image#target width="100%" height="100%" xlink:href=target_url
image#source width="100%" height="100%" xlink:href=source_url clip-path="url(#clip)"
rect#divider width="3" height=height x=div(width, 2) fill="#000" stroke="#fff" stroke-width="1"
h1 Onion Skin
.onion-skin
svg.onion-skin__image viewBox="0 0 #{width} #{height}" height=height
defs
pattern#checkerboard width="16" height="16" patternUnits="userSpaceOnUse"
rect width="8" height="8" x="0" y="0" fill="#ffffff44"
rect width="8" height="8" x="0" y="8" fill="#00000044"
rect width="8" height="8" x="8" y="0" fill="#00000044"
rect width="8" height="8" x="8" y="8" fill="#ffffff44"
rect width=width height=height fill="url(#checkerboard)"
image#source width="100%" height="100%" xlink:href=source_url
image#target width="100%" height="100%" xlink:href=target_url
input.onion-skin__slider type="range" min="0" max="1" step="0.01"
p Left is source, right is target

View file

@ -0,0 +1,135 @@
defmodule PhilomenaWeb.DuplicateReportView do
use PhilomenaWeb, :view
alias PhilomenaWeb.ImageView
@formats_order ~W(video/webm image/svg+xml image/png image/gif image/jpeg other)
def comparison_url(conn, image),
do: ImageView.thumb_url(image, can?(conn, :show, image), :full)
def largest_dimensions(images) do
images
|> Enum.map(&{&1.image_width, &1.image_height})
|> Enum.max_by(fn {w, h} -> w * h end)
end
def background_class(%{state: "rejected"}), do: "background-danger"
def background_class(%{state: "accepted"}), do: "background-success"
def background_class(%{state: "claimed"}), do: "background-warning"
def background_class(_duplicate_report), do: nil
def file_types(%{image: image, duplicate_of_image: duplicate_of_image}) do
source_type = String.upcase(image.image_format)
target_type = String.upcase(duplicate_of_image.image_format)
"(#{source_type}, #{target_type})"
end
def forward_merge?(%{image_id: image_id, duplicate_of_image_id: duplicate_of_image_id}),
do: duplicate_of_image_id > image_id
def higher_res?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: duplicate_of_image.image_width > image.image_width or duplicate_of_image.image_height > image.image_height
def same_res?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: duplicate_of_image.image_width == image.image_width and duplicate_of_image.image_height == image.image_height
def same_format?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: duplicate_of_image.image_mime_type == image.image_mime_type
def better_format?(%{image: image, duplicate_of_image: duplicate_of_image}) do
source_index = Enum.find_index(@formats_order, image.image_mime_type) || length(@formats_order) - 1
target_index = Enum.find_index(@formats_order, duplicate_of_image.image_mime_type) || length(@formats_order) - 1
target_index < source_index
end
def same_aspect_ratio?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: abs(duplicate_of_image.image_aspect_ratio - image.image_aspect_ratio) <= 0.009
def neither_have_source?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: blank?(duplicate_of_image.source_url) and blank?(image.source_url)
def same_source?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: to_string(duplicate_of_image.source_url) == to_string(image.source_url)
def similar_source?(%{image: image, duplicate_of_image: duplicate_of_image}) do
host1 = URI.parse(image.source_url).host
host2 = URI.parse(duplicate_of_image.source_url).host
host1 == host2
end
def source_on_target?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: present?(duplicate_of_image.source_url) and blank?(image.source_url)
def source_on_source?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: blank?(duplicate_of_image.source_url) && present?(image.source_url)
def same_artist_tags?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: MapSet.equal?(artist_tags(image), artist_tags(duplicate_of_image))
def more_artist_tags_on_target?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: proper_subset?(artist_tags(image), artist_tags(duplicate_of_image))
def more_artist_tags_on_source?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: proper_subset?(artist_tags(duplicate_of_image), artist_tags(image))
def same_rating_tags?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: MapSet.equal?(rating_tags(image), rating_tags(duplicate_of_image))
def target_is_edit?(%{duplicate_of_image: duplicate_of_image}),
do: edit?(duplicate_of_image)
def source_is_edit?(%{image: image}),
do: edit?(image)
def both_are_edits?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: edit?(image) and edit?(duplicate_of_image)
def target_is_alternate_version?(%{duplicate_of_image: duplicate_of_image}),
do: alternate_version?(duplicate_of_image)
def source_is_alternate_version?(%{image: image}),
do: alternate_version?(image)
def both_are_alternate_versions?(%{image: image, duplicate_of_image: duplicate_of_image}),
do: alternate_version?(image) and alternate_version?(duplicate_of_image)
defp artist_tags(%{tags: tags}) do
tags
|> Enum.filter(& &1.namespace == "artist")
|> Enum.map(& &1.name)
|> MapSet.new()
end
defp rating_tags(%{tags: tags}) do
tags
|> Enum.filter(& &1.category == "rating")
|> Enum.map(& &1.name)
|> MapSet.new()
end
defp edit?(%{tags: tags}) do
tags
|> Enum.filter(& &1.name == "edit")
|> Enum.any?()
end
defp alternate_version?(%{tags: tags}) do
tags
|> Enum.filter(& &1.name == "alternate version")
|> Enum.any?()
end
defp blank?(nil), do: true
defp blank?(""), do: true
defp blank?(str) when is_binary(str), do: String.trim(str) == ""
defp blank?(_object), do: false
defp present?(object), do: not blank?(object)
defp proper_subset?(set1, set2),
do: MapSet.subset?(set1, set2) and not MapSet.equal?(set1, set2)
end

View file

@ -0,0 +1,88 @@
defmodule PhilomenaWeb.DuplicateReportControllerTest do
use PhilomenaWeb.ConnCase
alias Philomena.DuplicateReports
@create_attrs %{}
@update_attrs %{}
@invalid_attrs %{}
def fixture(:duplicate_report) do
{:ok, duplicate_report} = DuplicateReports.create_duplicate_report(@create_attrs)
duplicate_report
end
describe "index" do
test "lists all duplicate_reports", %{conn: conn} do
conn = get(conn, Routes.duplicate_report_path(conn, :index))
assert html_response(conn, 200) =~ "Listing Duplicate reports"
end
end
describe "new duplicate_report" do
test "renders form", %{conn: conn} do
conn = get(conn, Routes.duplicate_report_path(conn, :new))
assert html_response(conn, 200) =~ "New Duplicate report"
end
end
describe "create duplicate_report" do
test "redirects to show when data is valid", %{conn: conn} do
conn = post(conn, Routes.duplicate_report_path(conn, :create), duplicate_report: @create_attrs)
assert %{id: id} = redirected_params(conn)
assert redirected_to(conn) == Routes.duplicate_report_path(conn, :show, id)
conn = get(conn, Routes.duplicate_report_path(conn, :show, id))
assert html_response(conn, 200) =~ "Show Duplicate report"
end
test "renders errors when data is invalid", %{conn: conn} do
conn = post(conn, Routes.duplicate_report_path(conn, :create), duplicate_report: @invalid_attrs)
assert html_response(conn, 200) =~ "New Duplicate report"
end
end
describe "edit duplicate_report" do
setup [:create_duplicate_report]
test "renders form for editing chosen duplicate_report", %{conn: conn, duplicate_report: duplicate_report} do
conn = get(conn, Routes.duplicate_report_path(conn, :edit, duplicate_report))
assert html_response(conn, 200) =~ "Edit Duplicate report"
end
end
describe "update duplicate_report" do
setup [:create_duplicate_report]
test "redirects when data is valid", %{conn: conn, duplicate_report: duplicate_report} do
conn = put(conn, Routes.duplicate_report_path(conn, :update, duplicate_report), duplicate_report: @update_attrs)
assert redirected_to(conn) == Routes.duplicate_report_path(conn, :show, duplicate_report)
conn = get(conn, Routes.duplicate_report_path(conn, :show, duplicate_report))
assert html_response(conn, 200)
end
test "renders errors when data is invalid", %{conn: conn, duplicate_report: duplicate_report} do
conn = put(conn, Routes.duplicate_report_path(conn, :update, duplicate_report), duplicate_report: @invalid_attrs)
assert html_response(conn, 200) =~ "Edit Duplicate report"
end
end
describe "delete duplicate_report" do
setup [:create_duplicate_report]
test "deletes chosen duplicate_report", %{conn: conn, duplicate_report: duplicate_report} do
conn = delete(conn, Routes.duplicate_report_path(conn, :delete, duplicate_report))
assert redirected_to(conn) == Routes.duplicate_report_path(conn, :index)
assert_error_sent 404, fn ->
get(conn, Routes.duplicate_report_path(conn, :show, duplicate_report))
end
end
end
defp create_duplicate_report(_) do
duplicate_report = fixture(:duplicate_report)
{:ok, duplicate_report: duplicate_report}
end
end