From 181931cf6759bfa01ac890025be2ca824f308f8d Mon Sep 17 00:00:00 2001 From: "byte[]" Date: Sun, 8 Dec 2019 12:45:37 -0500 Subject: [PATCH] admin reports --- lib/philomena/reports/attribution.ex | 13 +++ lib/philomena/reports/query.ex | 26 ++++++ lib/philomena/users/ability.ex | 6 ++ lib/philomena/users/user.ex | 1 + .../controllers/admin/report_controller.ex | 83 +++++++++++++++++++ lib/philomena_web/router.ex | 4 + .../admin/report/_reports.html.slime | 51 ++++++++++++ .../templates/admin/report/index.html.slime | 35 ++++++++ .../templates/admin/report/show.html.slime | 43 ++++++++++ .../layout/_header_staff_links.html.slime | 2 +- lib/philomena_web/views/admin/report_view.ex | 23 +++++ lib/philomena_web/views/profile_view.ex | 35 ++++++++ 12 files changed, 321 insertions(+), 1 deletion(-) create mode 100644 lib/philomena/reports/attribution.ex create mode 100644 lib/philomena/reports/query.ex create mode 100644 lib/philomena_web/controllers/admin/report_controller.ex create mode 100644 lib/philomena_web/templates/admin/report/_reports.html.slime create mode 100644 lib/philomena_web/templates/admin/report/index.html.slime create mode 100644 lib/philomena_web/templates/admin/report/show.html.slime create mode 100644 lib/philomena_web/views/admin/report_view.ex diff --git a/lib/philomena/reports/attribution.ex b/lib/philomena/reports/attribution.ex new file mode 100644 index 00000000..900df127 --- /dev/null +++ b/lib/philomena/reports/attribution.ex @@ -0,0 +1,13 @@ +defimpl Philomena.Attribution, for: Philomena.Reports.Report do + def object_identifier(report) do + to_string(report.id) + end + + def best_user_identifier(report) do + to_string(report.user_id || report.fingerprint || report.ip) + end + + def anonymous?(report) do + false + end +end \ No newline at end of file diff --git a/lib/philomena/reports/query.ex b/lib/philomena/reports/query.ex new file mode 100644 index 00000000..b53a7e06 --- /dev/null +++ b/lib/philomena/reports/query.ex @@ -0,0 +1,26 @@ +defmodule Philomena.Reports.Query do + alias Search.Parser + + int_fields = ~W(id image_id) + date_fields = ~W(created_at) + literal_fields = ~W(state user user_id admin admin_id reportable_type reportable_id fingerprint) + ip_fields = ~W(ip) + bool_fields = ~W(open) + ngram_fields = ~W(reason) + custom_fields = ~W(author user_id) + default_field = "reason" + + @parser Parser.parser( + int_fields: int_fields, + date_fields: date_fields, + literal_fields: literal_fields, + ip_fields: ip_fields, + bool_fields: bool_fields, + ngram_fields: ngram_fields, + default_field: default_field + ) + + def compile(query_string) do + Parser.parse(@parser, query_string || "", %{}) + end +end diff --git a/lib/philomena/users/ability.ex b/lib/philomena/users/ability.ex index 63380d35..948fe9c7 100644 --- a/lib/philomena/users/ability.ex +++ b/lib/philomena/users/ability.ex @@ -13,6 +13,7 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do alias Philomena.DnpEntries.DnpEntry alias Philomena.UserLinks.UserLink alias Philomena.Tags.Tag + alias Philomena.Reports.Report # Admins can do anything def can?(%User{role: "admin"}, _action, _model), do: true @@ -41,6 +42,11 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do # View IP addresses and fingerprints def can?(%User{role: "moderator"}, :show, :ip_address), do: true + # View reports + def can?(%User{role: "moderator"}, :index, Report), do: true + def can?(%User{role: "moderator"}, :show, %Report{}), do: true + def can?(%User{role: "moderator"}, :edit, %Report{}), do: true + # # Assistants can... # diff --git a/lib/philomena/users/user.ex b/lib/philomena/users/user.ex index a9da7bbb..2e312456 100644 --- a/lib/philomena/users/user.ex +++ b/lib/philomena/users/user.ex @@ -34,6 +34,7 @@ defmodule Philomena.Users.User do has_many :awards, Badges.Award has_many :unread_notifications, UnreadNotification has_many :notifications, through: [:unread_notifications, :notification] + has_many :linked_tags, through: [:verified_links, :tag] has_one :commission, Commission many_to_many :roles, Role, join_through: "users_roles" diff --git a/lib/philomena_web/controllers/admin/report_controller.ex b/lib/philomena_web/controllers/admin/report_controller.ex new file mode 100644 index 00000000..2302929e --- /dev/null +++ b/lib/philomena_web/controllers/admin/report_controller.ex @@ -0,0 +1,83 @@ +defmodule PhilomenaWeb.Admin.ReportController do + use PhilomenaWeb, :controller + + alias Philomena.Textile.Renderer + alias Philomena.Reports.Report + alias Philomena.Reports.Query + alias Philomena.Polymorphic + alias Philomena.Repo + import Ecto.Query + + plug :load_and_authorize_resource, model: Report, preload: [:admin, user: [:linked_tags, awards: :badge]] + + def index(conn, %{"rq" => query_string}) do + {:ok, query} = Query.compile(query_string) + + reports = load_reports(conn, query) + + render(conn, "index.html", layout_class: "layout--wide", reports: reports, my_reports: []) + end + + def index(conn, _params) do + user = conn.assigns.current_user + + query = + %{ + bool: %{ + should: [ + %{term: %{open: false}}, + %{ + bool: %{ + must: %{term: %{open: true}}, + must_not: %{term: %{admin_id: user.id}} + } + } + ] + } + } + + reports = load_reports(conn, query) + + my_reports = + Report + |> where(open: true, admin_id: ^user.id) + |> preload([:admin, user: :linked_tags]) + |> order_by(desc: :created_at) + |> Repo.all() + |> Polymorphic.load_polymorphic(reportable: [reportable_id: :reportable_type]) + + render(conn, "index.html", layout_class: "layout--wide", reports: reports, my_reports: my_reports) + end + + def show(conn, _params) do + [report] = Polymorphic.load_polymorphic([conn.assigns.report], reportable: [reportable_id: :reportable_type]) + body = Renderer.render_one(%{body: report.reason}, conn) + + render(conn, "show.html", report: report, body: body) + end + + defp load_reports(conn, query) do + reports = + Report.search_records( + %{ + query: query, + sort: sorts() + }, + conn.assigns.pagination, + Report |> preload([:admin, user: :linked_tags]) + ) + + entries = + Polymorphic.load_polymorphic(reports, reportable: [reportable_id: :reportable_type]) + + %{reports | entries: entries} + end + + defp sorts do + [ + %{open: :desc}, + %{state: :desc}, + %{created_at: :desc} + ] + end +end \ No newline at end of file diff --git a/lib/philomena_web/router.ex b/lib/philomena_web/router.ex index a1b9a462..f145cca5 100644 --- a/lib/philomena_web/router.ex +++ b/lib/philomena_web/router.ex @@ -156,6 +156,10 @@ defmodule PhilomenaWeb.Router do resources "/ip_profiles", IpProfileController, only: [:show] resources "/fingerprint_profiles", FingerprintProfileController, only: [:show] + + scope "/admin", Admin, as: :admin do + resources "/reports", ReportController, only: [:index, :show] + end end scope "/", PhilomenaWeb do diff --git a/lib/philomena_web/templates/admin/report/_reports.html.slime b/lib/philomena_web/templates/admin/report/_reports.html.slime new file mode 100644 index 00000000..66bac4d9 --- /dev/null +++ b/lib/philomena_web/templates/admin/report/_reports.html.slime @@ -0,0 +1,51 @@ +table.table + thead + tr + th Thing + th Reason + th User + th.hide-mobile Opened + th State + th Options + tbody + = for report <- @reports do + tr + td + = link_to_reported_thing @conn, report.reportable + td + span title=report.reason + = truncate(report.reason) + td + = if report.user do + = link report.user.name, to: Routes.profile_path(@conn, :show, report.user) + - else + em> + = truncated_ip_link(@conn, report.ip) + = link_to_fingerprint(@conn, report.fingerprint) + + = if not is_nil(report.user) and Enum.any?(report.user.linked_tags) do + = render PhilomenaWeb.TagView, "_tag_list.html", tags: ordered_tags(report.user.linked_tags), conn: @conn + + td.hide-mobile + = pretty_time report.created_at + + td class=report_row_class(report) + => pretty_state(report) + = user_abbrv report.admin + td + = link "Show", to: Routes.admin_report_path(@conn, :show, report) + /- if report.open + - if report.user + ' • + = link_to 'Send PM', new_conversation_path(title: "Your Report of #{reported_thing(report.reportable)}", recipient: report.user.name) + - if report.admin != current_user + ' • + - if report.admin.present? + = link_to 'Claim', admin_report_claim_path(report), method: :post, data: { confirm: t('admin.reports.change_owner') } + - else + = link_to 'Claim', admin_report_claim_path(report), method: :post + - if report.admin == current_user + ' • + = link_to 'Release', admin_report_claim_path(report), method: :delete + ' • + = link_to t('close'), admin_report_close_path(report), data: { confirm: t('are_you_sure') }, method: :post diff --git a/lib/philomena_web/templates/admin/report/index.html.slime b/lib/philomena_web/templates/admin/report/index.html.slime new file mode 100644 index 00000000..90ca2009 --- /dev/null +++ b/lib/philomena_web/templates/admin/report/index.html.slime @@ -0,0 +1,35 @@ +- route = fn p -> Routes.admin_report_path(@conn, :index, p) end +- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @reports + +h1 Reports + += if Enum.any?(@my_reports) do + .block + .block__header + span.block__header__title Your Reports + .block__content + = render PhilomenaWeb.Admin.ReportView, "_reports.html", reports: @my_reports, conn: @conn + +.block + .block__header + span.block__header__title All Reports + = pagination + .block__content + = if Enum.any?(@reports) do + = render PhilomenaWeb.Admin.ReportView, "_reports.html", reports: @reports, conn: @conn + - else + p We couldn't find any reports for you, sorry! + + .block__header.block__header--light + = pagination + += form_for :report, Routes.admin_report_path(@conn, :index), [method: "get", class: "hform"], fn f -> + .field + = text_input f, :rq, name: :rq, value: @conn.params["rq"], class: "input hform__text", placeholder: "Search reports", autocapitalize: "none" + = submit "Search", class: "hform__button button" + +.field + label for="rq" + ' Searchable fields: id, created_at, reason, state, open, user, user_id, admin, admin_id, ip, fingerprint, reportable_type, reportable_id, image_id + br + ' Report reason is used if you don't specify a field. \ No newline at end of file diff --git a/lib/philomena_web/templates/admin/report/show.html.slime b/lib/philomena_web/templates/admin/report/show.html.slime new file mode 100644 index 00000000..03a00fb3 --- /dev/null +++ b/lib/philomena_web/templates/admin/report/show.html.slime @@ -0,0 +1,43 @@ +h1 Showing Report +p + = link_to_reported_thing @conn, @report.reportable + +article.block.communication + .block__content.flex.flex--no-wrap + .flex__fixed.spacing-right + = render PhilomenaWeb.UserAttributionView, "_anon_user_avatar.html", object: @report, conn: @conn + .flex__grow.communication__body + span.communication__body__sender-name = render PhilomenaWeb.UserAttributionView, "_anon_user.html", object: @report, awards: true, conn: @conn + br + = render PhilomenaWeb.UserAttributionView, "_anon_user_title.html", object: @report, conn: @conn + .communication__body__text + ==<> @body + + .block__content.communication__options + .flex.flex--wrap.flex--spaced-out + div + ' Reported + = pretty_time @report.created_at + + .flex__right + => link_to_ip @conn, @report.ip + => link_to_fingerprint @conn, @report.fingerprint + + div + ' User-Agent: + code + = @report.user_agent + +p + = if @report.user do + => link "Send PM", to: Routes.conversation_path(@conn, :new, recipient: @report.user.name), class: "button button--link" + + = if @report.open do + => link "Close", to: "#", class: "button", data: [method: "post"] + + = if current?(@report.admin, @conn.assigns.current_user) do + => link "Release", to: "#", class: "button", data: [method: "delete"] + - else + => link "Claim", to: "#", class: "button", data: [method: "post"] + += link "Back", to: Routes.admin_report_path(@conn, :index), class: "button button-link" \ No newline at end of file diff --git a/lib/philomena_web/templates/layout/_header_staff_links.html.slime b/lib/philomena_web/templates/layout/_header_staff_links.html.slime index 78f424ec..efba0773 100644 --- a/lib/philomena_web/templates/layout/_header_staff_links.html.slime +++ b/lib/philomena_web/templates/layout/_header_staff_links.html.slime @@ -52,7 +52,7 @@ span.header__counter__admin = @duplicate_report_count = if @report_count do - = link to: "#", class: "header__link", title: "Reports" do + = link to: Routes.admin_report_path(@conn, :index), class: "header__link", title: "Reports" do ' R span.header__counter__admin = @report_count diff --git a/lib/philomena_web/views/admin/report_view.ex b/lib/philomena_web/views/admin/report_view.ex new file mode 100644 index 00000000..a254328a --- /dev/null +++ b/lib/philomena_web/views/admin/report_view.ex @@ -0,0 +1,23 @@ +defmodule PhilomenaWeb.Admin.ReportView do + use PhilomenaWeb, :view + + import PhilomenaWeb.ReportView, only: [link_to_reported_thing: 2, report_row_class: 1, pretty_state: 1] + import PhilomenaWeb.ProfileView, only: [user_abbrv: 1, current?: 2] + + def truncate(<>), do: string <> "..." + def truncate(string), do: string + + def truncated_ip_link(conn, ip) do + case to_string(ip) do + <> = ip -> + link(string <> "...", to: Routes.ip_profile_path(conn, :show, ip)) + + ip -> + link(ip, to: Routes.ip_profile_path(conn, :show, ip)) + end + end + + def ordered_tags(tags) do + Enum.sort_by(tags, & &1.name) + end +end diff --git a/lib/philomena_web/views/profile_view.ex b/lib/philomena_web/views/profile_view.ex index babf8aca..107c2091 100644 --- a/lib/philomena_web/views/profile_view.ex +++ b/lib/philomena_web/views/profile_view.ex @@ -50,6 +50,41 @@ defmodule PhilomenaWeb.ProfileView do Enum.map_join(tags, " || ", & &1.name) end + def user_abbrv(%{name: name}) do + String.upcase(initials_abbrv(name) || uppercase_abbrv(name) || first_letters_abbrv(name)) + end + def user_abbrv(_user), do: content_tag(:span, "(n/a)") + + defp initials_abbrv(name) do + case String.split(name, " ", parts: 4) do + [<>, <>, <>, <>] -> + <> + + [<>, <>, <>] -> + <> + + [<>, <>] -> + <> + + _ -> + nil + end + end + + defp uppercase_abbrv(name) do + case Regex.scan(~r/[A-Z]/, name, capture: :all_but_first) do + [] -> + nil + + list -> + Enum.join(list) + end + end + + defp first_letters_abbrv(name) do + String.slice(name, 0, 4) + end + defp zero_div(_num, 0), do: 0 defp zero_div(num, den), do: div(num, den)