Server: Add admin reports, server pagination

This commit is contained in:
asonix 2021-02-04 21:05:10 -06:00
parent c1ac9eeed4
commit 555aba8d36
7 changed files with 423 additions and 116 deletions

View file

@ -2,7 +2,7 @@ use crate::{
error::{Error, OptionExt}, error::{Error, OptionExt},
extensions::{CommentExt, ProfileExt, SubmissionExt}, extensions::{CommentExt, ProfileExt, SubmissionExt},
nav::NavState, nav::NavState,
pagination::{PageNum, SearchPage}, pagination::{Page, PageNum, PageSource, SearchPage},
views::{OwnedProfileView, OwnedSubmissionView}, views::{OwnedProfileView, OwnedSubmissionView},
ActixLoader, State, ActixLoader, State,
}; };
@ -23,7 +23,8 @@ pub use hyaenidae_profiles::store::Report;
mod pagination; mod pagination;
use pagination::{ use pagination::{
BlockedPager, FederatedPager, InboundPager, KnownPager, OutboundPager, ServerPager, BlockedPager, ClosedPager, FederatedPager, InboundPager, KnownPager, OpenPager, OutboundPager,
ReportPager, ServerPager,
}; };
pub(super) fn scope() -> Scope { pub(super) fn scope() -> Scope {
@ -123,20 +124,21 @@ async fn discover_server(
Ok(()) as Result<(), Error> Ok(()) as Result<(), Error>
}; };
let query = query.into_inner();
if let Err(e) = (fallible)().await { if let Err(e) = (fallible)().await {
let mut federation_view = FederationView::build(query.into_inner(), &state2).await?; let mut federation_view = FederationView::build(query.clone(), &state2).await?;
federation_view federation_view
.discover_error(e.to_string()) .discover_error(e.to_string())
.discover_value(url2); .discover_value(url2);
let server_view = ServerView::build(&state2).await?; let server_view = ServerView::build(&state2).await?;
let open_reports = ReportsView::new(state2).await?; let reports_vew = ReportsView::new(query, state2).await?;
return crate::rendered(HttpResponse::Ok(), |cursor| { return crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::admin::index( crate::templates::admin::index(
cursor, cursor,
&loader, &loader,
&open_reports, &reports_vew,
&server_view, &server_view,
&federation_view, &federation_view,
&nav_state, &nav_state,
@ -878,6 +880,17 @@ impl FederationView {
} }
} }
fn page_source(query: &HashMap<String, String>, prefix: &str) -> Option<PageSource> {
query
.get(&format!("{}_min", prefix))
.and_then(|min_str| Some(PageSource::NewerThan(min_str.parse().ok()?)))
.or_else(|| {
query
.get(&format!("{}_max", prefix))
.and_then(|max_str| Some(PageSource::OlderThan(max_str.parse().ok()?)))
})
}
fn page_num(query: &HashMap<String, String>, name: &str) -> Option<PageNum> { fn page_num(query: &HashMap<String, String>, name: &str) -> Option<PageNum> {
query.get(name).and_then(|page_str| { query.get(name).and_then(|page_str| {
Some(PageNum { Some(PageNum {
@ -923,7 +936,7 @@ async fn view_report(
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let report_id = report.into_inner(); let report_id = report.into_inner();
let report_store = state.profiles.store.reports.clone(); let report_store = state.profiles.store.reports.clone();
let report = web::block(move || Ok(report_store.by_id(report_id)?)).await?; let report = web::block(move || report_store.by_id(report_id)?.req()).await?;
let report_view = ReportView::new(report, state).await?; let report_view = ReportView::new(report, state).await?;
@ -966,7 +979,7 @@ async fn close_report(
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let report_id = report.into_inner(); let report_id = report.into_inner();
let report_store = state.profiles.store.reports.clone(); let report_store = state.profiles.store.reports.clone();
let report = web::block(move || Ok(report_store.by_id(report_id)?)).await?; let report = web::block(move || report_store.by_id(report_id)?.req()).await?;
let form = form.into_inner(); let form = form.into_inner();
@ -1110,15 +1123,16 @@ async fn admin_page(
nav_state: NavState, nav_state: NavState,
state: web::Data<State>, state: web::Data<State>,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
let federation_view = FederationView::build(query.into_inner(), &state).await?; let query = query.into_inner();
let federation_view = FederationView::build(query.clone(), &state).await?;
let server_view = ServerView::build(&state).await?; let server_view = ServerView::build(&state).await?;
let open_reports = ReportsView::new(state).await?; let reports_vew = ReportsView::new(query, state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| { crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::admin::index( crate::templates::admin::index(
cursor, cursor,
&loader, &loader,
&open_reports, &reports_vew,
&server_view, &server_view,
&federation_view, &federation_view,
&nav_state, &nav_state,
@ -1417,7 +1431,10 @@ impl ServerView {
} }
pub struct ReportsView { pub struct ReportsView {
reports: Vec<Report>, query: HashMap<String, String>,
open_reports: Page,
closed_reports: Page,
reports: HashMap<Uuid, Report>,
profiles: HashMap<Uuid, Profile>, profiles: HashMap<Uuid, Profile>,
submissions: HashMap<Uuid, Submission>, submissions: HashMap<Uuid, Submission>,
comments: HashMap<Uuid, Comment>, comments: HashMap<Uuid, Comment>,
@ -1511,8 +1528,94 @@ impl<'a> SubmissionView<'a> {
} }
impl ReportsView { impl ReportsView {
pub(crate) fn reports(&self) -> &[Report] { pub(crate) fn open_reports<'a>(&'a self) -> impl Iterator<Item = &'a Report> + 'a {
&self.reports self.open_reports
.items
.iter()
.filter_map(move |report_id| self.reports.get(report_id))
}
pub(crate) fn has_open_reports_nav(&self) -> bool {
self.open_reports.prev.is_some() || self.open_reports.next.is_some()
}
pub(crate) fn open_reports_nav(&self, loader: &ActixLoader) -> Vec<Button> {
let mut btns = vec![];
if let Some(prev) = self.open_reports.prev {
let mut query = self.query.clone();
query.insert("open_min".to_owned(), prev.to_string());
query.remove("open_max");
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
format!("/admin?{}", query)
} else {
"/admin".to_owned()
};
btns.push(Button::secondary(&fl!(loader, "admin-reports-prev")).href(&href));
}
if let Some(next) = self.open_reports.next {
let mut query = self.query.clone();
query.insert("open_max".to_owned(), next.to_string());
query.remove("open_min");
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
format!("/admin?{}", query)
} else {
"/admin".to_owned()
};
btns.push(Button::secondary(&fl!(loader, "admin-reports-next")).href(&href));
}
btns
}
pub(crate) fn closed_reports<'a>(&'a self) -> impl Iterator<Item = &'a Report> + 'a {
self.closed_reports
.items
.iter()
.filter_map(move |report_id| self.reports.get(report_id))
}
pub(crate) fn has_closed_reports_nav(&self) -> bool {
self.closed_reports.prev.is_some() || self.closed_reports.next.is_some()
}
pub(crate) fn closed_reports_nav(&self, loader: &ActixLoader) -> Vec<Button> {
let mut btns = vec![];
if let Some(prev) = self.closed_reports.prev {
let mut query = self.query.clone();
query.insert("closed_min".to_owned(), prev.to_string());
query.remove("closed_max");
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
format!("/admin?{}", query)
} else {
"/admin".to_owned()
};
btns.push(Button::secondary(&fl!(loader, "admin-reports-prev")).href(&href));
}
if let Some(next) = self.closed_reports.next {
let mut query = self.query.clone();
query.insert("closed_max".to_owned(), next.to_string());
query.remove("closed_min");
let href = if let Ok(query) = serde_urlencoded::to_string(query) {
format!("/admin?{}", query)
} else {
"/admin".to_owned()
};
btns.push(Button::secondary(&fl!(loader, "admin-reports-next")).href(&href));
}
btns
} }
pub(crate) fn view_path(&self, report: &Report) -> String { pub(crate) fn view_path(&self, report: &Report) -> String {
@ -1555,80 +1658,46 @@ impl ReportsView {
}) })
} }
async fn new(state: web::Data<State>) -> Result<Self, Error> { async fn new(query: HashMap<String, String>, state: web::Data<State>) -> Result<Self, Error> {
let store = state.profiles.clone(); let store = state.profiles.clone();
let view = web::block(move || { let view = web::block(move || {
let mut reports = HashMap::new();
let mut profiles = HashMap::new(); let mut profiles = HashMap::new();
let mut submissions = HashMap::new(); let mut submissions = HashMap::new();
let mut comments = HashMap::new(); let mut comments = HashMap::new();
let mut files = HashMap::new(); let mut files = HashMap::new();
let reports = store let open_reports = Page::from_pagination(
.store OpenPager(ReportPager {
.reports store: &store.store,
.all() reports: &mut reports,
.filter_map(|id| { profiles: &mut profiles,
let report = store.store.reports.by_id(id).ok()?; submissions: &mut submissions,
comments: &mut comments,
files: &mut files,
}),
10,
page_source(&query, "open"),
);
if let Some(id) = report.reporter_profile() { let closed_reports = Page::from_pagination(
if !profiles.contains_key(&id) { ClosedPager(ReportPager {
let profile = store.store.profiles.by_id(id).ok()??; store: &store.store,
profiles.insert(profile.id(), profile); reports: &mut reports,
} profiles: &mut profiles,
} submissions: &mut submissions,
comments: &mut comments,
match report.kind() { files: &mut files,
ReportKind::Profile => { }),
if !profiles.contains_key(&report.item()) { 10,
let profile = store.store.profiles.by_id(report.item()).ok()??; page_source(&query, "closed"),
profiles.insert(profile.id(), profile); );
}
}
ReportKind::Submission => {
if !submissions.contains_key(&report.item()) {
let submission =
store.store.submissions.by_id(report.item()).ok()??;
if !profiles.contains_key(&submission.profile_id()) {
let profile = store
.store
.profiles
.by_id(submission.profile_id())
.ok()??;
profiles.insert(profile.id(), profile);
}
for file_id in submission.files() {
if !files.contains_key(file_id) {
let file = store.store.files.by_id(*file_id).ok()??;
files.insert(file.id(), file);
}
}
submissions.insert(submission.id(), submission);
}
}
ReportKind::Comment => {
if !comments.contains_key(&report.item()) {
let comment = store.store.comments.by_id(report.item()).ok()??;
if !profiles.contains_key(&comment.profile_id()) {
let profile =
store.store.profiles.by_id(comment.profile_id()).ok()??;
profiles.insert(profile.id(), profile);
}
comments.insert(comment.id(), comment);
}
}
_ => (),
}
Some(report)
})
.rev()
.collect();
Ok(ReportsView { Ok(ReportsView {
query,
open_reports,
closed_reports,
reports, reports,
profiles, profiles,
submissions, submissions,

View file

@ -1,8 +1,20 @@
use crate::pagination::SearchPagination; use crate::pagination::{Pagination, SearchPagination};
use hyaenidae_profiles::store::Server; use hyaenidae_profiles::store::{Comment, File, Profile, Report, ReportKind, Server, Submission};
use std::collections::HashMap; use std::collections::HashMap;
use uuid::Uuid; use uuid::Uuid;
pub(super) struct ReportPager<'b> {
pub(super) store: &'b hyaenidae_profiles::store::Store,
pub(super) reports: &'b mut HashMap<Uuid, Report>,
pub(super) profiles: &'b mut HashMap<Uuid, Profile>,
pub(super) submissions: &'b mut HashMap<Uuid, Submission>,
pub(super) comments: &'b mut HashMap<Uuid, Comment>,
pub(super) files: &'b mut HashMap<Uuid, File>,
}
pub(super) struct OpenPager<'b>(pub(super) ReportPager<'b>);
pub(super) struct ClosedPager<'b>(pub(super) ReportPager<'b>);
pub(super) struct ServerPager<'b> { pub(super) struct ServerPager<'b> {
pub(super) self_id: Uuid, pub(super) self_id: Uuid,
pub(super) store: &'b hyaenidae_profiles::store::Store, pub(super) store: &'b hyaenidae_profiles::store::Store,
@ -15,6 +27,153 @@ pub(super) struct OutboundPager<'a>(pub(super) ServerPager<'a>);
pub(super) struct BlockedPager<'a>(pub(super) ServerPager<'a>); pub(super) struct BlockedPager<'a>(pub(super) ServerPager<'a>);
pub(super) struct KnownPager<'a>(pub(super) ServerPager<'a>); pub(super) struct KnownPager<'a>(pub(super) ServerPager<'a>);
impl<'b> Pagination for OpenPager<'b> {
fn from_max<'a>(&'a mut self, max: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.0
.store
.reports
.open_reports_older_than(max)
.filter_map(move |report_id| self.0.filter_report(report_id)),
)
}
fn from_min<'a>(&'a mut self, min: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.0
.store
.reports
.open_reports_newer_than(min)
.filter_map(move |report_id| self.0.filter_report(report_id)),
)
}
fn from_start<'a>(&'a mut self) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.0
.store
.reports
.open_reports()
.filter_map(move |report_id| self.0.filter_report(report_id)),
)
}
}
impl<'b> Pagination for ClosedPager<'b> {
fn from_max<'a>(&'a mut self, max: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.0
.store
.reports
.closed_reports_older_than(max)
.filter_map(move |report_id| self.0.filter_report(report_id)),
)
}
fn from_min<'a>(&'a mut self, min: Uuid) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.0
.store
.reports
.closed_reports_newer_than(min)
.filter_map(move |report_id| self.0.filter_report(report_id)),
)
}
fn from_start<'a>(&'a mut self) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(
self.0
.store
.reports
.closed_reports()
.filter_map(move |report_id| self.0.filter_report(report_id)),
)
}
}
impl<'b> ReportPager<'b> {
fn filter_report(&mut self, report_id: Uuid) -> Option<Uuid> {
if !self.reports.contains_key(&report_id) {
let report = self.store.reports.by_id(report_id).ok()??;
if let Some(id) = report.reporter_profile() {
self.cache_profile(id)?;
}
match report.kind() {
ReportKind::Profile => {
self.cache_profile(report.item())?;
}
ReportKind::Submission => {
self.cache_submission(report.item())?;
}
ReportKind::Comment => {
self.cache_comment(report.item())?;
}
ReportKind::Post => {
unimplemented!();
}
}
self.reports.insert(report.id(), report);
Some(report_id)
} else {
None
}
}
fn cache_comment(&mut self, comment_id: Uuid) -> Option<()> {
if !self.comments.contains_key(&comment_id) {
let comment = self.store.comments.by_id(comment_id).ok()??;
self.cache_profile(comment.profile_id())?;
self.comments.insert(comment.id(), comment);
}
Some(())
}
fn cache_submission(&mut self, submission_id: Uuid) -> Option<()> {
if !self.submissions.contains_key(&submission_id) {
let submission = self.store.submissions.by_id(submission_id).ok()??;
self.cache_profile(submission.profile_id())?;
for file_id in submission.files() {
self.cache_file(*file_id)?;
}
self.submissions.insert(submission.id(), submission);
}
Some(())
}
fn cache_profile(&mut self, profile_id: Uuid) -> Option<()> {
if !self.profiles.contains_key(&profile_id) {
let profile = self.store.profiles.by_id(profile_id).ok()??;
for file_id in profile.icon().into_iter().chain(profile.banner()) {
self.cache_file(file_id)?;
}
self.profiles.insert(profile.id(), profile);
}
Some(())
}
fn cache_file(&mut self, file_id: Uuid) -> Option<()> {
if !self.files.contains_key(&file_id) {
let file = self.store.files.by_id(file_id).ok()??;
self.files.insert(file.id(), file);
}
Some(())
}
}
impl<'b> SearchPagination for FederatedPager<'b> { impl<'b> SearchPagination for FederatedPager<'b> {
fn from_term<'a>(&'a mut self, _: &'a str) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> { fn from_term<'a>(&'a mut self, _: &'a str) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new( Box::new(

View file

@ -154,6 +154,10 @@ impl NavState {
"/notifications" "/notifications"
} }
pub(crate) fn is_admin(&self) -> bool {
self.admin.is_some()
}
pub(crate) fn admin_button(&self, loader: &ActixLoader) -> Button { pub(crate) fn admin_button(&self, loader: &ActixLoader) -> Button {
Button::link(&fl!(loader, "nav-admin-button")).href("/admin") Button::link(&fl!(loader, "nav-admin-button")).href("/admin")
} }

View file

@ -36,53 +36,55 @@
@:card_title({ @:card_title({
@fl!(loader, "admin-reports-heading") @fl!(loader, "admin-reports-heading")
}) })
@for report in reports_view.reports() { @for report in reports_view.open_reports() {
@:card_body({ @:card_body({
<div class="report"> <div class="report">
<div class="report-head"> <div class="report-left">
@if let Some(profile) = reports_view.profile(report) { <div class="report-head">
@:reporter(reports_view, report, { @if let Some(profile) = reports_view.profile(report) {
@fl!(loader, "admin-reports-reported") @:reporter(reports_view, report, {
@fl!(loader, "admin-reports-reported")
@:link(&Link::new_tab(&profile.view_path()).plain(true), { @:link(&Link::new_tab(&profile.view_path()).plain(true), {
@Html(profile.name()) @Html(profile.name())
})
}) })
}) }
} @if let Some(submission) = reports_view.submission(report) {
@if let Some(submission) = reports_view.submission(report) { @:reporter(reports_view, report, {
@:reporter(reports_view, report, { @fl!(loader, "admin-reports-reported")
@fl!(loader, "admin-reports-reported")
@:link(&Link::new_tab(&submission.author_path()).plain(true), { @:link(&Link::new_tab(&submission.author_path()).plain(true), {
@Html(fl!(loader, "author-owned", author = submission.author_name())) @Html(fl!(loader, "author-owned", author = submission.author_name()))
}) })
@fl!(loader, "admin-reports-submission") @fl!(loader, "admin-reports-submission")
@:link(&Link::new_tab(&submission.view_path()).plain(true), { @:link(&Link::new_tab(&submission.view_path()).plain(true), {
@Html(submission.title()) @Html(submission.title())
})
}) })
}) }
} @if let Some(comment) = reports_view.comment(report) {
@if let Some(comment) = reports_view.comment(report) { @:reporter(reports_view, report, {
@:reporter(reports_view, report, { reported
reported @:link(&Link::new_tab(&comment.author_path()).plain(true), {
@:link(&Link::new_tab(&comment.author_path()).plain(true), { @Html(fl!(loader, "author-owned", author = comment.author_name()))
@Html(fl!(loader, "author-owned", author = comment.author_name())) })
}) @fl!(loader, "admin-reports-comment")
@fl!(loader, "admin-reports-comment")
@:link(&Link::new_tab(&comment.view_path()).plain(true), { @:link(&Link::new_tab(&comment.view_path()).plain(true), {
@Html(comment.body()) @Html(comment.body())
})
}) })
}) }
</div>
@if let Some(note) = report.note() {
<div class="report-description text-section">
<h4>@fl!(loader, "admin-reports-note")</h4>
<p>@Html(note)</p>
</div>
} }
</div> </div>
@if let Some(note) = report.note() {
<div class="report-description text-section">
<h4>@fl!(loader, "admin-reports-note")</h4>
<p>@Html(note)</p>
</div>
}
<div class="button-section report-actions"> <div class="button-section report-actions">
@:button_group(&[ @:button_group(&[
Button::secondary(&fl!(loader, "admin-reports-view-button")).href(&reports_view.view_path(report)), Button::secondary(&fl!(loader, "admin-reports-view-button")).href(&reports_view.view_path(report)),
@ -91,6 +93,73 @@
</div> </div>
}) })
} }
@if reports_view.has_open_reports_nav() {
@:card_body({
@:button_group(&reports_view.open_reports_nav(loader))
})
}
})
@:card(&Card::full_width().dark(nav_state.dark()), {
@:card_title({
@fl!(loader, "admin-closed-reports-heading")
})
@for report in reports_view.closed_reports() {
@:card_body({
<div class="report">
<div class="report-left">
<div class="report-head">
@if let Some(profile) = reports_view.profile(report) {
@:reporter(reports_view, report, {
@fl!(loader, "admin-reports-reported")
@:link(&Link::new_tab(&profile.view_path()).plain(true), {
@Html(profile.name())
})
})
}
@if let Some(submission) = reports_view.submission(report) {
@:reporter(reports_view, report, {
@fl!(loader, "admin-reports-reported")
@:link(&Link::new_tab(&submission.author_path()).plain(true), {
@Html(fl!(loader, "author-owned", author = submission.author_name()))
})
@fl!(loader, "admin-reports-submission")
@:link(&Link::new_tab(&submission.view_path()).plain(true), {
@Html(submission.title())
})
})
}
@if let Some(comment) = reports_view.comment(report) {
@:reporter(reports_view, report, {
reported
@:link(&Link::new_tab(&comment.author_path()).plain(true), {
@Html(fl!(loader, "author-owned", author = comment.author_name()))
})
@fl!(loader, "admin-reports-comment")
@:link(&Link::new_tab(&comment.view_path()).plain(true), {
@Html(comment.body())
})
})
}
</div>
@if let Some(note) = report.note() {
<div class="report-description text-section">
<h4>@fl!(loader, "admin-reports-note")</h4>
<p>@Html(note)</p>
</div>
}
</div>
</div>
})
}
@if reports_view.has_closed_reports_nav() {
@:card_body({
@:button_group(&reports_view.closed_reports_nav(loader))
})
}
}) })
@:card(&Card::full_width().dark(nav_state.dark()), { @:card(&Card::full_width().dark(nav_state.dark()), {
<form method="POST" action="@federation_view.discover_path()"> <form method="POST" action="@federation_view.discover_path()">

View file

@ -21,6 +21,9 @@
<div class="toolkit-button-group"> <div class="toolkit-button-group">
@:button(&nav_state.submission_button(loader)) @:button(&nav_state.submission_button(loader))
@:button(&nav_state.browse_button(loader)) @:button(&nav_state.browse_button(loader))
@if nav_state.is_admin() {
@:button(&nav_state.admin_button(loader))
}
@if nav_state.has_notifications() { @if nav_state.has_notifications() {
@:icon_button("bell", &fl!(loader, "nav-notifications-button"), nav_state.notifications_path()) @:icon_button("bell", &fl!(loader, "nav-notifications-button"), nav_state.notifications_path())
} }

View file

@ -23,7 +23,7 @@
}) })
@:card_body({ @:card_body({
<p>@fl!(loader, "report-description")</p> <p>@fl!(loader, "report-description")</p>
@:text_input(&rview.input(loader)) @:text_input(&rview.input(loader).dark(nav_state.dark()))
}) })
@:card_body({ @:card_body({
@:button_group(&[ @:button_group(&[

View file

@ -296,8 +296,11 @@ server-info-description-input = Description
server-info-description-placeholder = Describe your server server-info-description-placeholder = Describe your server
server-info-submit-button = Save server-info-submit-button = Save
admin-reports-heading = Reports admin-closed-reports-heading = Closed Reports
admin-reports-heading = Open Reports
admin-reports-reported = reported admin-reports-reported = reported
admin-reports-prev = Previous
admin-reports-next = Next
author-owned = {$author}'s author-owned = {$author}'s
admin-reports-submission = submission: admin-reports-submission = submission:
admin-reports-comment = comment: admin-reports-comment = comment: