approval queue stuff

This commit is contained in:
Luna D 2022-03-24 17:31:57 +01:00
parent eeb2f851e0
commit 26deaaf588
No known key found for this signature in database
GPG key ID: 4B1C63448394F688
44 changed files with 449 additions and 82 deletions

View file

@ -470,6 +470,7 @@ span.stat {
@import "text"; @import "text";
@import "~views/adverts"; @import "~views/adverts";
@import "~views/approval";
@import "~views/badges"; @import "~views/badges";
@import "~views/channels"; @import "~views/channels";
@import "~views/comments"; @import "~views/comments";

View file

@ -0,0 +1,49 @@
.approval-grid {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr;
gap: var(--padding-small);
text-align: center;
}
.approval-items--main {
display: grid;
grid-template-columns: auto 1fr 1fr 1fr;
gap: var(--padding-small);
justify-content: center;
}
.approval-items--main span {
margin: auto;
}
.approval-items--footer {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: 1fr;
gap: var(--padding-normal);
}
.approval-items--footer * {
height: 2rem;
margin-top: auto;
margin-bottom: auto;
}
@media (max-width: $min_px_width_for_desktop_layout) {
.approval-grid {
grid-template-columns: 1fr;
grid-template-rows: auto;
grid-template-areas:
"main"
"footer";
}
.approval-items--main {
grid-area: main;
}
.approval-items--footer {
grid-area: footer;
}
}

26
assets/js/pmwarning.js Normal file
View file

@ -0,0 +1,26 @@
/**
* PmWarning
*
* Warn users that their PM will be reviewed.
*/
import { $ } from './utils/dom';
function warnAboutPMs() {
const textarea = $('.js-toolbar-input');
const warning = $('.js-hidden-warning');
const imageEmbedRegex = /!+\[/g;
if (!warning || !textarea) return;
textarea.addEventListener('input', () => {
const value = textarea.value;
if (value.match(imageEmbedRegex))
warning.classList.remove('hidden');
else if (!warning.classList.contains('hidden'))
warning.classList.add('hidden');
});
}
export { warnAboutPMs };

View file

@ -34,6 +34,7 @@ import { setupSearch } from './search';
import { setupToolbar } from './markdowntoolbar'; import { setupToolbar } from './markdowntoolbar';
import { hideStaffTools } from './staffhider'; import { hideStaffTools } from './staffhider';
import { pollOptionCreator } from './poll'; import { pollOptionCreator } from './poll';
import { warnAboutPMs } from './pmwarning';
whenReady(() => { whenReady(() => {
@ -66,5 +67,6 @@ whenReady(() => {
setupToolbar(); setupToolbar();
hideStaffTools(); hideStaffTools();
pollOptionCreator(); pollOptionCreator();
warnAboutPMs();
}); });

View file

@ -9,6 +9,7 @@ defmodule Philomena.Comments do
alias Philomena.Elasticsearch alias Philomena.Elasticsearch
alias Philomena.Reports.Report alias Philomena.Reports.Report
alias Philomena.UserStatistics
alias Philomena.Comments.Comment alias Philomena.Comments.Comment
alias Philomena.Comments.ElasticsearchIndex, as: CommentIndex alias Philomena.Comments.ElasticsearchIndex, as: CommentIndex
alias Philomena.IndexWorker alias Philomena.IndexWorker
@ -212,6 +213,8 @@ defmodule Philomena.Comments do
|> Repo.transaction() |> Repo.transaction()
|> case do |> case do
{:ok, %{comment: comment, reports: {_count, reports}}} -> {:ok, %{comment: comment, reports: {_count, reports}}} ->
notify_comment(comment)
UserStatistics.inc_stat(comment.user, :comments_posted)
Reports.reindex_reports(reports) Reports.reindex_reports(reports)
reindex_comment(comment) reindex_comment(comment)
@ -222,6 +225,8 @@ defmodule Philomena.Comments do
end end
end end
def report_non_approved(%Comment{approved: true}), do: false
def report_non_approved(comment) do def report_non_approved(comment) do
Reports.create_system_report( Reports.create_system_report(
comment.id, comment.id,

View file

@ -6,7 +6,7 @@ defmodule Philomena.Conversations do
import Ecto.Query, warn: false import Ecto.Query, warn: false
alias Ecto.Multi alias Ecto.Multi
alias Philomena.Repo alias Philomena.Repo
alias Philomena.Reports
alias Philomena.Conversations.Conversation alias Philomena.Conversations.Conversation
@doc """ @doc """
@ -187,6 +187,12 @@ defmodule Philomena.Conversations do
Ecto.build_assoc(conversation, :messages) Ecto.build_assoc(conversation, :messages)
|> Message.creation_changeset(attrs, user) |> Message.creation_changeset(attrs, user)
show_as_read =
case message do
%{changes: %{approved: true}} -> false
_ -> true
end
conversation_query = conversation_query =
Conversation Conversation
|> where(id: ^conversation.id) |> where(id: ^conversation.id)
@ -196,11 +202,43 @@ defmodule Philomena.Conversations do
Multi.new() Multi.new()
|> Multi.insert(:message, message) |> Multi.insert(:message, message)
|> Multi.update_all(:conversation, conversation_query, |> Multi.update_all(:conversation, conversation_query,
set: [from_read: false, to_read: false, last_message_at: now] set: [from_read: show_as_read, to_read: show_as_read, last_message_at: now]
) )
|> Repo.transaction() |> Repo.transaction()
end end
def approve_conversation_message(message) do
message_query =
message
|> Message.approve_changeset()
conversation_query =
Conversation
|> where(id: ^message.conversation_id)
now = DateTime.utc_now()
Multi.new()
|> Multi.update(:message, message_query)
|> Multi.update_all(:conversation, conversation_query, set: [to_read: false])
|> Repo.transaction()
end
def report_non_approved(id) do
Reports.create_system_report(
id,
"Conversation",
"Approval",
"PM contains externally-embedded images and has been flagged for review."
)
end
def set_as_read(conversation) do
conversation
|> Conversation.to_read_changeset()
|> Repo.update()
end
@doc """ @doc """
Updates a message. Updates a message.

View file

@ -37,6 +37,11 @@ defmodule Philomena.Conversations.Conversation do
|> cast(attrs, [:from_read, :to_read]) |> cast(attrs, [:from_read, :to_read])
end end
def to_read_changeset(conversation) do
change(conversation)
|> put_change(:to_read, true)
end
def hidden_changeset(conversation, attrs) do def hidden_changeset(conversation, attrs) do
conversation conversation
|> cast(attrs, [:from_hidden, :to_hidden]) |> cast(attrs, [:from_hidden, :to_hidden])

View file

@ -11,6 +11,7 @@ defmodule Philomena.Conversations.Message do
belongs_to :from, User belongs_to :from, User
field :body, :string field :body, :string
field :approved, :boolean, default: false
timestamps(inserted_at: :created_at, type: :utc_datetime) timestamps(inserted_at: :created_at, type: :utc_datetime)
end end
@ -31,4 +32,8 @@ defmodule Philomena.Conversations.Message do
|> validate_length(:body, max: 300_000, count: :bytes) |> validate_length(:body, max: 300_000, count: :bytes)
|> Approval.maybe_put_approval(user) |> Approval.maybe_put_approval(user)
end end
def approve_changeset(message) do
change(message, approved: true)
end
end end

View file

@ -115,7 +115,7 @@ defmodule Philomena.Images do
repair_image(image) repair_image(image)
reindex_image(image) reindex_image(image)
Tags.reindex_tags(image.added_tags) Tags.reindex_tags(image.added_tags)
UserStatistics.inc_stat(attribution[:user], :uploads) maybe_approve_image(image, attribution[:user])
result result
@ -135,6 +135,55 @@ defmodule Philomena.Images do
multi multi
end end
def approve_image(image) do
image
|> Repo.preload(:user)
|> Image.approve_changeset()
|> Repo.update()
|> case do
{:ok, image} ->
reindex_image(image)
increment_user_stats(image.user)
maybe_suggest_user_verification(image.user)
{:ok, image}
error ->
error
end
end
defp maybe_approve_image(image, nil), do: false
defp maybe_approve_image(image, %User{verified: false, role: role}) when role == "user",
do: false
defp maybe_approve_image(image, _user), do: approve_image(image)
defp increment_user_stats(nil), do: false
defp increment_user_stats(%User{} = user) do
UserStatistics.inc_stat(user, :uploads)
end
defp maybe_suggest_user_verification(%User{id: id, uploads_count: 5, verified: false}) do
Reports.create_system_report(
id,
"User",
"Verification",
"User has uploaded enough approved images to be considered for verification."
)
end
defp maybe_suggest_user_verification(_user), do: false
def count_pending_approvals() do
Image
|> where(hidden_from_users: false)
|> where(approved: false)
|> Repo.aggregate(:count)
end
def feature_image(featurer, %Image{} = image) do def feature_image(featurer, %Image{} = image) do
%ImageFeature{user_id: featurer.id, image_id: image.id} %ImageFeature{user_id: featurer.id, image_id: image.id}
|> ImageFeature.changeset(%{}) |> ImageFeature.changeset(%{})

View file

@ -326,6 +326,12 @@ defmodule Philomena.Images.Image do
cast(image, attrs, [:anonymous]) cast(image, attrs, [:anonymous])
end end
def approve_changeset(image) do
change(image)
|> put_change(:approved, true)
|> put_change(:first_seen_at, DateTime.truncate(DateTime.utc_now(), :second))
end
def cache_changeset(image) do def cache_changeset(image) do
changeset = change(image) changeset = change(image)
image = apply_changes(changeset) image = apply_changes(changeset)

View file

@ -10,6 +10,7 @@ defmodule Philomena.Posts do
alias Philomena.Elasticsearch alias Philomena.Elasticsearch
alias Philomena.Topics.Topic alias Philomena.Topics.Topic
alias Philomena.Topics alias Philomena.Topics
alias Philomena.UserStatistics
alias Philomena.Posts.Post alias Philomena.Posts.Post
alias Philomena.Posts.ElasticsearchIndex, as: PostIndex alias Philomena.Posts.ElasticsearchIndex, as: PostIndex
alias Philomena.IndexWorker alias Philomena.IndexWorker
@ -117,6 +118,8 @@ defmodule Philomena.Posts do
Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id]) Exq.enqueue(Exq, "notifications", NotificationWorker, ["Posts", post.id])
end end
def report_non_approved(%Post{approved: true}), do: false
def report_non_approved(post) do def report_non_approved(post) do
Reports.create_system_report( Reports.create_system_report(
post.id, post.id,
@ -261,6 +264,8 @@ defmodule Philomena.Posts do
|> Repo.transaction() |> Repo.transaction()
|> case do |> case do
{:ok, %{post: post, reports: {_count, reports}}} -> {:ok, %{post: post, reports: {_count, reports}}} ->
notify_post(post)
UserStatistics.inc_stat(post.user, :forum_posts)
Reports.reindex_reports(reports) Reports.reindex_reports(reports)
reindex_post(post) reindex_post(post)

View file

@ -31,7 +31,8 @@ defmodule Philomena.Reports.ElasticsearchIndex do
reportable_type: %{type: "keyword"}, reportable_type: %{type: "keyword"},
reportable_id: %{type: "keyword"}, reportable_id: %{type: "keyword"},
open: %{type: "boolean"}, open: %{type: "boolean"},
reason: %{type: "text", analyzer: "snowball"} reason: %{type: "text", analyzer: "snowball"},
system: %{type: "boolean"}
} }
} }
} }
@ -53,7 +54,8 @@ defmodule Philomena.Reports.ElasticsearchIndex do
reportable_id: report.reportable_id, reportable_id: report.reportable_id,
fingerprint: report.fingerprint, fingerprint: report.fingerprint,
open: report.open, open: report.open,
reason: report.reason reason: report.reason,
system: report.system
} }
end end

View file

@ -75,6 +75,7 @@ defmodule Philomena.Topics do
|> case do |> case do
{:ok, %{topic: topic}} = result -> {:ok, %{topic: topic}} = result ->
Posts.reindex_post(hd(topic.posts)) Posts.reindex_post(hd(topic.posts))
Posts.report_non_approved(hd(topic.posts))
result result

View file

@ -31,7 +31,6 @@ defmodule Philomena.Topics.Topic do
field :slug, :string field :slug, :string
field :anonymous, :boolean, default: false field :anonymous, :boolean, default: false
field :hidden_from_users, :boolean, default: false field :hidden_from_users, :boolean, default: false
field :approved, :boolean
timestamps(inserted_at: :created_at, type: :utc_datetime) timestamps(inserted_at: :created_at, type: :utc_datetime)
end end

View file

@ -273,9 +273,6 @@ defimpl Canada.Can, for: [Atom, Philomena.Users.User] do
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :hide, %Topic{}), def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :hide, %Topic{}),
do: true do: true
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :approve, %Topic{}),
do: true
def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :show, %Post{}), def can?(%User{role: "assistant", role_map: %{"Topic" => "moderator"}}, :show, %Post{}),
do: true do: true

View file

@ -0,0 +1,28 @@
defmodule PhilomenaWeb.Admin.ApprovalController do
use PhilomenaWeb, :controller
alias Philomena.Images.Image
alias Philomena.Repo
import Ecto.Query
plug :verify_authorized
def index(conn, _params) do
images =
Image
|> where(hidden_from_users: false)
|> where(approved: false)
|> order_by(desc: :id)
|> preload([:user, tags: [:aliases, :aliased_tag]])
|> Repo.paginate(conn.assigns.scrivener)
render(conn, "index.html", title: "Admin - Approval Queue", images: images)
end
defp verify_authorized(conn, _opts) do
case Canada.Can.can?(conn.assigns.current_user, :approve, %Image{}) do
true -> conn
false -> PhilomenaWeb.NotAuthorizedPlug.call(conn)
end
end
end

View file

@ -42,7 +42,10 @@ defmodule PhilomenaWeb.Admin.ReportController do
%{ %{
bool: %{ bool: %{
must: %{term: %{open: true}}, must: %{term: %{open: true}},
must_not: %{term: %{admin_id: user.id}} must_not: [
%{term: %{admin_id: user.id}},
%{term: %{system: true}}
]
} }
} }
] ]
@ -59,11 +62,20 @@ defmodule PhilomenaWeb.Admin.ReportController do
|> Repo.all() |> Repo.all()
|> Polymorphic.load_polymorphic(reportable: [reportable_id: :reportable_type]) |> Polymorphic.load_polymorphic(reportable: [reportable_id: :reportable_type])
system_reports =
Report
|> where(open: true, system: true)
|> preload([:admin, user: :linked_tags])
|> order_by(desc: :created_at)
|> Repo.all()
|> Polymorphic.load_polymorphic(reportable: [reportable_id: :reportable_type])
render(conn, "index.html", render(conn, "index.html",
title: "Admin - Reports", title: "Admin - Reports",
layout_class: "layout--wide", layout_class: "layout--wide",
reports: reports, reports: reports,
my_reports: my_reports my_reports: my_reports,
system_reports: system_reports
) )
end end

View file

@ -1,16 +1,16 @@
defmodule PhilomenaWeb.Conversation.ApproveController do defmodule PhilomenaWeb.Conversation.Message.ApproveController do
use PhilomenaWeb, :controller use PhilomenaWeb, :controller
alias Philomena.Conversations.Conversation alias Philomena.Conversations.Message
alias Philomena.Conversations alias Philomena.Conversations
plug PhilomenaWeb.CanaryMapPlug, create: :approve plug PhilomenaWeb.CanaryMapPlug, create: :approve
plug :load_and_authorize_resource, plug :load_and_authorize_resource,
model: Conversation, model: Message,
id_field: "slug", id_name: "message_id",
id_name: "conversation_id", persisted: true,
persisted: true preload: [:conversation]
def create(conn, _params) do def create(conn, _params) do
message = conn.assigns.message message = conn.assigns.message

View file

@ -20,7 +20,11 @@ defmodule PhilomenaWeb.Conversation.MessageController do
user = conn.assigns.current_user user = conn.assigns.current_user
case Conversations.create_message(conversation, user, message_params) do case Conversations.create_message(conversation, user, message_params) do
{:ok, _result} -> {:ok, %{message: message}} ->
if not message.approved do
Conversations.report_non_approved(message.conversation_id)
end
count = count =
Message Message
|> where(conversation_id: ^conversation.id) |> where(conversation_id: ^conversation.id)

View file

@ -107,6 +107,11 @@ defmodule PhilomenaWeb.ConversationController do
case Conversations.create_conversation(user, conversation_params) do case Conversations.create_conversation(user, conversation_params) do
{:ok, conversation} -> {:ok, conversation} ->
if not hd(conversation.messages).approved do
Conversations.report_non_approved(conversation.id)
Conversations.set_as_read(conversation)
end
conn conn
|> put_flash(:info, "Conversation successfully created.") |> put_flash(:info, "Conversation successfully created.")
|> redirect(to: Routes.conversation_path(conn, :show, conversation)) |> redirect(to: Routes.conversation_path(conn, :show, conversation))

View file

@ -15,7 +15,7 @@ defmodule PhilomenaWeb.Image.ApproveController do
conn conn
|> put_flash(:info, "Image has been approved.") |> put_flash(:info, "Image has been approved.")
|> moderation_log(details: &log_details/3, data: image) |> moderation_log(details: &log_details/3, data: image)
|> redirect(to: Routes.image_path(conn, :show, image)) |> redirect(to: Routes.admin_approval_path(conn, :index))
end end
defp log_details(conn, _action, image) do defp log_details(conn, _action, image) do

View file

@ -78,11 +78,11 @@ defmodule PhilomenaWeb.Image.CommentController do
PhilomenaWeb.Api.Json.CommentView.render("show.json", %{comment: comment}) PhilomenaWeb.Api.Json.CommentView.render("show.json", %{comment: comment})
) )
Comments.notify_comment(comment)
Comments.reindex_comment(comment) Comments.reindex_comment(comment)
Images.reindex_image(conn.assigns.image) Images.reindex_image(conn.assigns.image)
if comment.approved do if comment.approved do
Comments.notify_comment(comment)
UserStatistics.inc_stat(conn.assigns.current_user, :comments_posted) UserStatistics.inc_stat(conn.assigns.current_user, :comments_posted)
else else
Comments.report_non_approved(comment) Comments.report_non_approved(comment)

View file

@ -1,45 +0,0 @@
defmodule PhilomenaWeb.Topic.ApproveController do
import Plug.Conn
use PhilomenaWeb, :controller
alias Philomena.Forums.Forum
alias Philomena.Topics.Topic
alias Philomena.Topics
plug PhilomenaWeb.CanaryMapPlug, create: :show
plug :load_and_authorize_resource,
model: Forum,
id_name: "forum_id",
id_field: "short_name",
persisted: true
plug PhilomenaWeb.LoadTopicPlug
plug PhilomenaWeb.CanaryMapPlug, create: :approve
plug :authorize_resource, model: Topic, persisted: true
def create(conn, _params) do
topic = conn.assigns.topic
user = conn.assigns.current_user
case Topics.approve_topic(topic) do
{:ok, topic} ->
conn
|> put_flash(:info, "Topic successfully approved!")
|> moderation_log(details: &log_details/3, data: topic)
|> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic))
{:error, _changeset} ->
conn
|> put_flash(:error, "Unable to approve the topic!")
|> redirect(to: Routes.forum_topic_path(conn, :show, topic.forum, topic))
end
end
defp log_details(conn, action, topic) do
%{
body: "Approved topic '#{topic.title}' in #{topic.forum.name}",
subject_path: Routes.forum_topic_path(conn, :show, topic.forum, topic)
}
end
end

View file

@ -18,8 +18,6 @@ defmodule PhilomenaWeb.Topic.Post.ApproveController do
case Posts.approve_post(post, user) do case Posts.approve_post(post, user) do
{:ok, post} -> {:ok, post} ->
UserStatistics.inc_stat(post.user(:forum_posts))
conn conn
|> put_flash(:info, "Post successfully approved.") |> put_flash(:info, "Post successfully approved.")
|> moderation_log(details: &log_details/3, data: post) |> moderation_log(details: &log_details/3, data: post)

View file

@ -35,9 +35,8 @@ defmodule PhilomenaWeb.Topic.PostController do
case Posts.create_post(topic, attributes, post_params) do case Posts.create_post(topic, attributes, post_params) do
{:ok, %{post: post}} -> {:ok, %{post: post}} ->
Posts.notify_post(post)
if post.approved do if post.approved do
Posts.notify_post(post)
UserStatistics.inc_stat(conn.assigns.current_user, :forum_posts) UserStatistics.inc_stat(conn.assigns.current_user, :forum_posts)
else else
Posts.report_non_approved(post) Posts.report_non_approved(post)

View file

@ -63,6 +63,7 @@ defmodule PhilomenaWeb.ImageLoader do
] ]
|> maybe_show_deleted(show_hidden?, del) |> maybe_show_deleted(show_hidden?, del)
|> maybe_custom_hide(user, hidden) |> maybe_custom_hide(user, hidden)
|> hide_non_approved()
end end
# Allow moderators to index hidden images # Allow moderators to index hidden images
@ -94,6 +95,10 @@ defmodule PhilomenaWeb.ImageLoader do
defp maybe_custom_hide(filters, _user, _param), defp maybe_custom_hide(filters, _user, _param),
do: filters do: filters
# Hide all images that aren't approved from all search queries.
defp hide_non_approved(filters),
do: [%{term: %{approved: false}} | filters]
# TODO: the search parser should try to optimize queries # TODO: the search parser should try to optimize queries
defp search_tag_name(%{term: %{"namespaced_tags.name" => tag_name}}), do: [tag_name] defp search_tag_name(%{term: %{"namespaced_tags.name" => tag_name}}), do: [tag_name]
defp search_tag_name(_other_query), do: [] defp search_tag_name(_other_query), do: []

View file

@ -75,7 +75,7 @@ defmodule PhilomenaWeb.ImageSorter do
end end
defp parse_sf(_params, sd, query) do defp parse_sf(_params, sd, query) do
%{query: query, sorts: [%{"id" => sd}]} %{query: query, sorts: [%{"first_seen_at" => sd}]}
end end
defp random_query(seed, sd, query) do defp random_query(seed, sd, query) do

View file

@ -58,6 +58,9 @@ defmodule PhilomenaWeb.MarkdownRenderer do
image.hidden_from_users -> image.hidden_from_users ->
" (deleted)" " (deleted)"
not image.approved ->
" (pending approval)"
true -> true ->
"" ""
end end
@ -75,7 +78,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do
cond do cond do
img != nil -> img != nil ->
case group do case group do
[_id, "p"] when not img.hidden_from_users -> [_id, "p"] when not img.hidden_from_users and img.approved ->
Phoenix.View.render(@image_view, "_image_target.html", Phoenix.View.render(@image_view, "_image_target.html",
image: img, image: img,
size: :medium, size: :medium,
@ -83,7 +86,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do
) )
|> safe_to_string() |> safe_to_string()
[_id, "t"] when not img.hidden_from_users -> [_id, "t"] when not img.hidden_from_users and img.approved ->
Phoenix.View.render(@image_view, "_image_target.html", Phoenix.View.render(@image_view, "_image_target.html",
image: img, image: img,
size: :small, size: :small,
@ -91,7 +94,7 @@ defmodule PhilomenaWeb.MarkdownRenderer do
) )
|> safe_to_string() |> safe_to_string()
[_id, "s"] when not img.hidden_from_users -> [_id, "s"] when not img.hidden_from_users and img.approved ->
Phoenix.View.render(@image_view, "_image_target.html", Phoenix.View.render(@image_view, "_image_target.html",
image: img, image: img,
size: :thumb_small, size: :thumb_small,
@ -99,6 +102,9 @@ defmodule PhilomenaWeb.MarkdownRenderer do
) )
|> safe_to_string() |> safe_to_string()
[id, suffix] when not img.approved ->
">>#{img.id}#{suffix}#{link_suffix(img)}"
[_id, ""] -> [_id, ""] ->
link(">>#{img.id}#{link_suffix(img)}", to: "/images/#{img.id}") link(">>#{img.id}#{link_suffix(img)}", to: "/images/#{img.id}")
|> safe_to_string() |> safe_to_string()

View file

@ -9,6 +9,7 @@ defmodule PhilomenaWeb.AdminCountersPlug do
alias Philomena.Reports alias Philomena.Reports
alias Philomena.ArtistLinks alias Philomena.ArtistLinks
alias Philomena.DnpEntries alias Philomena.DnpEntries
alias Philomena.Images
import Plug.Conn, only: [assign: 3] import Plug.Conn, only: [assign: 3]
@ -31,12 +32,14 @@ defmodule PhilomenaWeb.AdminCountersPlug do
defp maybe_assign_admin_metrics(conn, _user, false), do: conn defp maybe_assign_admin_metrics(conn, _user, false), do: conn
defp maybe_assign_admin_metrics(conn, user, true) do defp maybe_assign_admin_metrics(conn, user, true) do
pending_approvals = Images.count_pending_approvals()
duplicate_reports = DuplicateReports.count_duplicate_reports(user) duplicate_reports = DuplicateReports.count_duplicate_reports(user)
reports = Reports.count_reports(user) reports = Reports.count_reports(user)
artist_links = ArtistLinks.count_artist_links(user) artist_links = ArtistLinks.count_artist_links(user)
dnps = DnpEntries.count_dnp_entries(user) dnps = DnpEntries.count_dnp_entries(user)
conn conn
|> assign(:pending_approval_count, pending_approvals)
|> assign(:duplicate_report_count, duplicate_reports) |> assign(:duplicate_report_count, duplicate_reports)
|> assign(:report_count, reports) |> assign(:report_count, reports)
|> assign(:artist_link_count, artist_links) |> assign(:artist_link_count, artist_links)

View file

@ -177,10 +177,15 @@ defmodule PhilomenaWeb.Router do
resources "/conversations", ConversationController, only: [:index, :show, :new, :create] do resources "/conversations", ConversationController, only: [:index, :show, :new, :create] do
resources "/reports", Conversation.ReportController, only: [:new, :create] resources "/reports", Conversation.ReportController, only: [:new, :create]
resources "/messages", Conversation.MessageController, only: [:create]
resources "/messages", Conversation.MessageController, only: [:create] do
resources "/approve", Conversation.Message.ApproveController,
only: [:create],
singleton: true
end
resources "/read", Conversation.ReadController, only: [:create, :delete], singleton: true resources "/read", Conversation.ReadController, only: [:create, :delete], singleton: true
resources "/hide", Conversation.HideController, only: [:create, :delete], singleton: true resources "/hide", Conversation.HideController, only: [:create, :delete], singleton: true
resources "/approve", Conversation.ApproveController, only: [:create], singleton: true
end end
resources "/images", ImageController, only: [] do resources "/images", ImageController, only: [] do
@ -240,7 +245,6 @@ defmodule PhilomenaWeb.Router do
resources "/stick", Topic.StickController, only: [:create, :delete], singleton: true resources "/stick", Topic.StickController, only: [:create, :delete], singleton: true
resources "/lock", Topic.LockController, only: [:create, :delete], singleton: true resources "/lock", Topic.LockController, only: [:create, :delete], singleton: true
resources "/hide", Topic.HideController, only: [:create, :delete], singleton: true resources "/hide", Topic.HideController, only: [:create, :delete], singleton: true
resources "/approve", Topic.ApproveController, only: [:create], singleton: true
resources "/posts", Topic.PostController, only: [:edit, :update] do resources "/posts", Topic.PostController, only: [:edit, :update] do
resources "/hide", Topic.Post.HideController, only: [:create, :delete], singleton: true resources "/hide", Topic.Post.HideController, only: [:create, :delete], singleton: true
@ -340,6 +344,8 @@ defmodule PhilomenaWeb.Router do
resources "/close", Report.CloseController, only: [:create], singleton: true resources "/close", Report.CloseController, only: [:create], singleton: true
end end
resources "/approvals", ApprovalController, only: [:index]
resources "/artist_links", ArtistLinkController, only: [:index] do resources "/artist_links", ArtistLinkController, only: [:index] do
resources "/verification", ArtistLink.VerificationController, resources "/verification", ArtistLink.VerificationController,
only: [:create], only: [:create],

View file

@ -0,0 +1,32 @@
.block
.block__header
.block__header__title.approval-grid
.approval-items--main
span ID
span Image
span Uploader
span Time
.approval-items--footer.hide-mobile
span.hide-mobile Actions
= for image <- @images do
.block__content.alternating-color
.approval-grid
.approval-items--main
span = link ">>#{image.id}", to: Routes.image_path(@conn, :show, image)
span = image_thumb(@conn, image)
span
= if image.user do
= link image.user.name, to: Routes.profile_path(@conn, :show, image.user)
- else
em>
= truncated_ip_link(@conn, image.ip)
= link_to_fingerprint(@conn, image.fingerprint)
span = pretty_time(image.created_at)
.approval-items--footer
= if can?(@conn, :approve, image) do
= button_to "Approve", Routes.image_approve_path(@conn, :create, image), method: "post", class: "button button--state-success"
= if can?(@conn, :hide, image) do
= form_for :image, Routes.image_delete_path(@conn, :create, image), [method: "post"], fn f ->
.field.field--inline
= text_input f, :deletion_reason, class: "input input--wide", placeholder: "Rule violation", required: true
= submit "Delete", class: "button button--state-danger button--separate-left"

View file

@ -0,0 +1,16 @@
- route = fn p -> Routes.admin_approval_path(@conn, :index, p) end
- pagination = render PhilomenaWeb.PaginationView, "_pagination.html", page: @images, route: route
h1 Approval Queue
.block
.block__header
= pagination
= if Enum.any?(@images) do
= render PhilomenaWeb.Admin.ApprovalView, "_approvals.html", images: @images, conn: @conn
- else
' No images are pending approval. Good job!
.block__header.block__header--light
= pagination

View file

@ -10,6 +10,13 @@ h1 Reports
.block__content .block__content
= render PhilomenaWeb.Admin.ReportView, "_reports.html", reports: @my_reports, conn: @conn = render PhilomenaWeb.Admin.ReportView, "_reports.html", reports: @my_reports, conn: @conn
= if Enum.any?(@system_reports) do
.block
.block__header.block--danger
span.block__header__title System Reports
.block__content
= render PhilomenaWeb.Admin.ReportView, "_reports.html", reports: @system_reports, conn: @conn
.block .block
.block__header .block__header
span.block__header__title All Reports span.block__header__title All Reports

View file

@ -5,6 +5,21 @@ h1 New Conversation
' &raquo; ' &raquo;
span.block__header__title New Conversation span.block__header__title New Conversation
= case DateTime.compare(DateTime.utc_now(), DateTime.add(@conn.assigns.current_user.created_at, 1_209_600)) do
- :lt ->
.block.block--fixed.block--warning.hidden.js-hidden-warning
h2 Warning!
p
strong> Your account is too new, so your PM will need to be reviewed by staff members.
' This is because it contains an external image. If you are not okay with a moderator viewing this PM conversation, please consider linking the image instead of embedding it (change
code<> ![
' to
code<
| [
| ).
- _ ->
/ Nothing
= form_for @changeset, Routes.conversation_path(@conn, :create), fn f -> = form_for @changeset, Routes.conversation_path(@conn, :create), fn f ->
= if @changeset.action do = if @changeset.action do
.alert.alert-danger .alert.alert-danger

View file

@ -31,6 +31,21 @@ h1 = @conversation.title
.block__header.block__header--light.page__header .block__header.block__header--light.page__header
.page__pagination = pagination .page__pagination = pagination
= case DateTime.compare(DateTime.utc_now(), DateTime.add(@conn.assigns.current_user.created_at, 1_209_600)) do
- :lt ->
.block.block--fixed.block--warning.hidden.js-hidden-warning
h2 Warning!
p
strong> Your account is too new, so your PM will need to be reviewed by staff members.
' This is because it contains an external image. If you are not okay with a moderator viewing this PM conversation, please consider linking the image instead of embedding it (change
code<> ![
' to
code<
| [
| ).
- _ ->
/ Nothing
= cond do = cond do
- @conn.assigns.current_ban -> - @conn.assigns.current_ban ->
= render PhilomenaWeb.BanView, "_ban_reason.html", conn: @conn = render PhilomenaWeb.BanView, "_ban_reason.html", conn: @conn

View file

@ -0,0 +1,12 @@
= if not @image.approved and not @image.hidden_from_users do
.block.block--fixed.block--warning
h2 Hold up!
p This image is pending approval from a staff member. It will appear on the site once it's reviewed and approved.
p
' Don't worry,
strong
' the image will not lose any viewership,
' it will appear on the homepage and in search results as normal (as if it was uploaded at the time of approval).
p
a href="/pages/approval"
strong Click here to learn more about image approval and verification.

View file

@ -145,5 +145,7 @@
br br
.flex.flex--spaced-out .flex.flex--spaced-out
= link "Lock specific tags", to: Routes.image_tag_lock_path(@conn, :show, @image), class: "button" = link "Lock specific tags", to: Routes.image_tag_lock_path(@conn, :show, @image), class: "button"
= if not @image.approved and can?(@conn, :approve, @image) do
= button_to "Approve image", Routes.image_approve_path(@conn, :create, @image), method: "post", class: "button button--state-success", data: [confirm: "Are you sure?"]
= if @image.hidden_from_users and can?(@conn, :destroy, @image) do = if @image.hidden_from_users and can?(@conn, :destroy, @image) do
= button_to "Destroy image", Routes.image_destroy_path(@conn, :create, @image), method: "post", class: "button button--state-danger", data: [confirm: "This action is IRREVERSIBLE. Are you sure?"] = button_to "Destroy image", Routes.image_destroy_path(@conn, :create, @image), method: "post", class: "button button--state-danger", data: [confirm: "This action is IRREVERSIBLE. Are you sure?"]

View file

@ -1,3 +1,4 @@
= render PhilomenaWeb.ImageView, "_image_approval_banner.html", image: @image, conn: @conn
= render PhilomenaWeb.ImageView, "_image_meta.html", image: @image, watching: @watching, user_galleries: @user_galleries, changeset: @image_changeset, conn: @conn = render PhilomenaWeb.ImageView, "_image_meta.html", image: @image, watching: @watching, user_galleries: @user_galleries, changeset: @image_changeset, conn: @conn
= render PhilomenaWeb.ImageView, "_image_page.html", image: @image, conn: @conn = render PhilomenaWeb.ImageView, "_image_page.html", image: @image, conn: @conn

View file

@ -46,6 +46,12 @@
i.fa.fa-fw.fa-list-alt> i.fa.fa-fw.fa-list-alt>
' Mod Logs ' Mod Logs
= if @pending_approval_count do
= link to: Routes.admin_approval_path(@conn, :index), class: "header__link", title: "Approval Queue" do
' Q
span.header__counter__admin
= @pending_approval_count
= if @duplicate_report_count do = if @duplicate_report_count do
= link to: Routes.duplicate_report_path(@conn, :index), class: "header__link", title: "Duplicates" do = link to: Routes.duplicate_report_path(@conn, :index), class: "header__link", title: "Duplicates" do
' D ' D

View file

@ -1,4 +1,18 @@
article.block.communication article.block.communication
= if not @message.approved and (can?(@conn, :approve, @message) or @message.from_id == @conn.assigns.current_user.id) do
.block__content
.block.block--fixed.block--danger
p
i.fas.fa-exclamation-triangle>
' This private message is pending approval from a staff member.
= if can?(@conn, :approve, @message) do
p
ul.horizontal-list
li
= link(to: Routes.conversation_message_approve_path(@conn, :create, @message.conversation_id, @message), data: [confirm: "Are you sure?"], method: "post", class: "button") do
i.fas.fa-check>
' Approve
.block__content.flex.flex--no-wrap .block__content.flex.flex--no-wrap
.flex__fixed.spacing-right .flex__fixed.spacing-right
= render PhilomenaWeb.UserAttributionView, "_user_avatar.html", object: %{user: @message.from}, conn: @conn, class: "avatar--100px" = render PhilomenaWeb.UserAttributionView, "_user_avatar.html", object: %{user: @message.from}, conn: @conn, class: "avatar--100px"

View file

@ -133,9 +133,9 @@ h1 Search
end end
sort_fields = [ sort_fields = [
"Sort by initial post date": :first_seen_at,
"Sort by image ID": :id, "Sort by image ID": :id,
"Sort by last modification date": :updated_at, "Sort by last modification date": :updated_at,
"Sort by initial post date": :first_seen_at,
"Sort by aspect ratio": :aspect_ratio, "Sort by aspect ratio": :aspect_ratio,
"Sort by fave count": :faves, "Sort by fave count": :faves,
"Sort by upvotes": :upvotes, "Sort by upvotes": :upvotes,

View file

@ -0,0 +1,16 @@
defmodule PhilomenaWeb.Admin.ApprovalView do
use PhilomenaWeb, :view
alias PhilomenaWeb.Admin.ReportView
# Shamelessly copied from ReportView
def truncated_ip_link(conn, ip), do: ReportView.truncated_ip_link(conn, ip)
def image_thumb(conn, image) do
render(PhilomenaWeb.ImageView, "_image_container.html",
image: image,
size: :thumb_tiny,
conn: conn
)
end
end

View file

@ -18,12 +18,18 @@ defmodule Philomena.Repo.Migrations.AddApprovalQueue do
add :approved, :boolean, default: false add :approved, :boolean, default: false
end end
alter table("topics") do alter table("messages") do
add :approved, :boolean, default: false add :approved, :boolean, default: false
end end
alter table("users") do alter table("users") do
add :verified, :boolean, default: false add :verified, :boolean, default: false
end end
create index(:images, [:hidden_from_users, :approved],
where: "hidden_from_users = false and approved = false"
)
create index(:reports, [:system], where: "system = true")
end end
end end

View file

@ -1007,7 +1007,8 @@ CREATE TABLE public.messages (
updated_at timestamp without time zone NOT NULL, updated_at timestamp without time zone NOT NULL,
from_id integer NOT NULL, from_id integer NOT NULL,
conversation_id integer NOT NULL, conversation_id integer NOT NULL,
body character varying NOT NULL body character varying NOT NULL,
approved boolean DEFAULT false
); );
@ -1680,8 +1681,7 @@ CREATE TABLE public.topics (
deleted_by_id integer, deleted_by_id integer,
locked_by_id integer, locked_by_id integer,
last_post_id integer, last_post_id integer,
hidden_from_users boolean DEFAULT false NOT NULL, hidden_from_users boolean DEFAULT false NOT NULL
approved boolean DEFAULT false
); );
@ -2903,6 +2903,13 @@ CREATE UNIQUE INDEX image_tag_locks_image_id_tag_id_index ON public.image_tag_lo
CREATE INDEX image_tag_locks_tag_id_index ON public.image_tag_locks USING btree (tag_id); CREATE INDEX image_tag_locks_tag_id_index ON public.image_tag_locks USING btree (tag_id);
--
-- Name: images_hidden_from_users_approved_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX images_hidden_from_users_approved_index ON public.images USING btree (hidden_from_users, approved) WHERE ((hidden_from_users = false) AND (approved = false));
-- --
-- Name: index_adverts_on_restrictions; Type: INDEX; Schema: public; Owner: - -- Name: index_adverts_on_restrictions; Type: INDEX; Schema: public; Owner: -
-- --
@ -4100,6 +4107,13 @@ CREATE INDEX moderation_logs_user_id_created_at_index ON public.moderation_logs
CREATE INDEX moderation_logs_user_id_index ON public.moderation_logs USING btree (user_id); CREATE INDEX moderation_logs_user_id_index ON public.moderation_logs USING btree (user_id);
--
-- Name: reports_system_index; Type: INDEX; Schema: public; Owner: -
--
CREATE INDEX reports_system_index ON public.reports USING btree (system) WHERE (system = true);
-- --
-- Name: user_tokens_context_token_index; Type: INDEX; Schema: public; Owner: - -- Name: user_tokens_context_token_index; Type: INDEX; Schema: public; Owner: -
-- --