mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-23 20:18:00 +01:00
admin reports
This commit is contained in:
parent
3e2dfb3fd9
commit
181931cf67
12 changed files with 321 additions and 1 deletions
13
lib/philomena/reports/attribution.ex
Normal file
13
lib/philomena/reports/attribution.ex
Normal file
|
@ -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
|
26
lib/philomena/reports/query.ex
Normal file
26
lib/philomena/reports/query.ex
Normal file
|
@ -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
|
|
@ -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...
|
||||
#
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
83
lib/philomena_web/controllers/admin/report_controller.ex
Normal file
83
lib/philomena_web/controllers/admin/report_controller.ex
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
51
lib/philomena_web/templates/admin/report/_reports.html.slime
Normal file
51
lib/philomena_web/templates/admin/report/_reports.html.slime
Normal file
|
@ -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
|
35
lib/philomena_web/templates/admin/report/index.html.slime
Normal file
35
lib/philomena_web/templates/admin/report/index.html.slime
Normal file
|
@ -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.
|
43
lib/philomena_web/templates/admin/report/show.html.slime
Normal file
43
lib/philomena_web/templates/admin/report/show.html.slime
Normal file
|
@ -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"
|
|
@ -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
|
||||
|
|
23
lib/philomena_web/views/admin/report_view.ex
Normal file
23
lib/philomena_web/views/admin/report_view.ex
Normal file
|
@ -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(<<string::binary-size(50), _rest::binary>>), do: string <> "..."
|
||||
def truncate(string), do: string
|
||||
|
||||
def truncated_ip_link(conn, ip) do
|
||||
case to_string(ip) do
|
||||
<<string::binary-size(25), _rest::binary>> = 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
|
|
@ -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
|
||||
[<<a1::utf8, _rest::binary>>, <<a2::utf8, _rest::binary>>, <<a3::utf8, _rest::binary>>, <<a4::utf8, _rest::binary>>] ->
|
||||
<<a1::utf8, a2::utf8, a3::utf8, a4::utf8>>
|
||||
|
||||
[<<a1::utf8, _rest::binary>>, <<a2::utf8, _rest::binary>>, <<a3::utf8, _rest::binary>>] ->
|
||||
<<a1::utf8, a2::utf8, a3::utf8>>
|
||||
|
||||
[<<a1::utf8, _rest::binary>>, <<a2::utf8, _rest::binary>>] ->
|
||||
<<a1::utf8, a2::utf8>>
|
||||
|
||||
_ ->
|
||||
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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue