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},
extensions::{CommentExt, ProfileExt, SubmissionExt},
nav::NavState,
pagination::{PageNum, SearchPage},
pagination::{Page, PageNum, PageSource, SearchPage},
views::{OwnedProfileView, OwnedSubmissionView},
ActixLoader, State,
};
@ -23,7 +23,8 @@ pub use hyaenidae_profiles::store::Report;
mod pagination;
use pagination::{
BlockedPager, FederatedPager, InboundPager, KnownPager, OutboundPager, ServerPager,
BlockedPager, ClosedPager, FederatedPager, InboundPager, KnownPager, OpenPager, OutboundPager,
ReportPager, ServerPager,
};
pub(super) fn scope() -> Scope {
@ -123,20 +124,21 @@ async fn discover_server(
Ok(()) as Result<(), Error>
};
let query = query.into_inner();
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
.discover_error(e.to_string())
.discover_value(url2);
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| {
crate::templates::admin::index(
cursor,
&loader,
&open_reports,
&reports_vew,
&server_view,
&federation_view,
&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> {
query.get(name).and_then(|page_str| {
Some(PageNum {
@ -923,7 +936,7 @@ async fn view_report(
) -> Result<HttpResponse, Error> {
let report_id = report.into_inner();
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?;
@ -966,7 +979,7 @@ async fn close_report(
) -> Result<HttpResponse, Error> {
let report_id = report.into_inner();
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();
@ -1110,15 +1123,16 @@ async fn admin_page(
nav_state: NavState,
state: web::Data<State>,
) -> 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 open_reports = ReportsView::new(state).await?;
let reports_vew = ReportsView::new(query, state).await?;
crate::rendered(HttpResponse::Ok(), |cursor| {
crate::templates::admin::index(
cursor,
&loader,
&open_reports,
&reports_vew,
&server_view,
&federation_view,
&nav_state,
@ -1417,7 +1431,10 @@ impl ServerView {
}
pub struct ReportsView {
reports: Vec<Report>,
query: HashMap<String, String>,
open_reports: Page,
closed_reports: Page,
reports: HashMap<Uuid, Report>,
profiles: HashMap<Uuid, Profile>,
submissions: HashMap<Uuid, Submission>,
comments: HashMap<Uuid, Comment>,
@ -1511,8 +1528,94 @@ impl<'a> SubmissionView<'a> {
}
impl ReportsView {
pub(crate) fn reports(&self) -> &[Report] {
&self.reports
pub(crate) fn open_reports<'a>(&'a self) -> impl Iterator<Item = &'a Report> + 'a {
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 {
@ -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 view = web::block(move || {
let mut reports = HashMap::new();
let mut profiles = HashMap::new();
let mut submissions = HashMap::new();
let mut comments = HashMap::new();
let mut files = HashMap::new();
let reports = store
.store
.reports
.all()
.filter_map(|id| {
let report = store.store.reports.by_id(id).ok()?;
let open_reports = Page::from_pagination(
OpenPager(ReportPager {
store: &store.store,
reports: &mut reports,
profiles: &mut profiles,
submissions: &mut submissions,
comments: &mut comments,
files: &mut files,
}),
10,
page_source(&query, "open"),
);
if let Some(id) = report.reporter_profile() {
if !profiles.contains_key(&id) {
let profile = store.store.profiles.by_id(id).ok()??;
profiles.insert(profile.id(), profile);
}
}
match report.kind() {
ReportKind::Profile => {
if !profiles.contains_key(&report.item()) {
let profile = store.store.profiles.by_id(report.item()).ok()??;
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();
let closed_reports = Page::from_pagination(
ClosedPager(ReportPager {
store: &store.store,
reports: &mut reports,
profiles: &mut profiles,
submissions: &mut submissions,
comments: &mut comments,
files: &mut files,
}),
10,
page_source(&query, "closed"),
);
Ok(ReportsView {
query,
open_reports,
closed_reports,
reports,
profiles,
submissions,

View File

@ -1,8 +1,20 @@
use crate::pagination::SearchPagination;
use hyaenidae_profiles::store::Server;
use crate::pagination::{Pagination, SearchPagination};
use hyaenidae_profiles::store::{Comment, File, Profile, Report, ReportKind, Server, Submission};
use std::collections::HashMap;
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) self_id: Uuid,
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 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> {
fn from_term<'a>(&'a mut self, _: &'a str) -> Box<dyn DoubleEndedIterator<Item = Uuid> + 'a> {
Box::new(

View File

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

View File

@ -36,53 +36,55 @@
@:card_title({
@fl!(loader, "admin-reports-heading")
})
@for report in reports_view.reports() {
@for report in reports_view.open_reports() {
@:card_body({
<div class="report">
<div class="report-head">
@if let Some(profile) = reports_view.profile(report) {
@:reporter(reports_view, report, {
@fl!(loader, "admin-reports-reported")
<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())
@: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")
}
@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.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())
@: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")
}
@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())
@: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>
@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">
@:button_group(&[
Button::secondary(&fl!(loader, "admin-reports-view-button")).href(&reports_view.view_path(report)),
@ -91,6 +93,73 @@
</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()), {
<form method="POST" action="@federation_view.discover_path()">

View File

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

View File

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

View File

@ -296,8 +296,11 @@ server-info-description-input = Description
server-info-description-placeholder = Describe your server
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-prev = Previous
admin-reports-next = Next
author-owned = {$author}'s
admin-reports-submission = submission:
admin-reports-comment = comment: