Community moderation fixes.

- Don't show banned communities on main post list. Fixes #95
- Add back in community moderation and editing. Fixes #92
This commit is contained in:
Dessalines 2019-04-21 13:52:55 -07:00
parent 273a38f61f
commit 016920aeb7
9 changed files with 88 additions and 49 deletions

View file

@ -16,6 +16,7 @@ with all_post as
p.*, p.*,
(select name from user_ where p.creator_id = user_.id) as creator_name, (select name from user_ where p.creator_id = user_.id) as creator_name,
(select name from community where p.community_id = community.id) as community_name, (select name from community where p.community_id = community.id) as community_name,
(select removed from community c where p.community_id = c.id) as community_removed,
(select count(*) from comment where comment.post_id = p.id) as number_of_comments, (select count(*) from comment where comment.post_id = p.id) as number_of_comments,
coalesce(sum(pl.score), 0) as score, coalesce(sum(pl.score), 0) as score,
count (case when pl.score = 1 then 1 else null end) as upvotes, count (case when pl.score = 1 then 1 else null end) as upvotes,

View file

@ -25,6 +25,7 @@ table! {
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
creator_name -> Varchar, creator_name -> Varchar,
community_name -> Varchar, community_name -> Varchar,
community_removed -> Bool,
number_of_comments -> BigInt, number_of_comments -> BigInt,
score -> BigInt, score -> BigInt,
upvotes -> BigInt, upvotes -> BigInt,
@ -54,6 +55,7 @@ pub struct PostView {
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub creator_name: String, pub creator_name: String,
pub community_name: String, pub community_name: String,
pub community_removed: bool,
pub number_of_comments: i64, pub number_of_comments: i64,
pub score: i64, pub score: i64,
pub upvotes: i64, pub upvotes: i64,
@ -133,13 +135,11 @@ impl PostView {
.order_by(score.desc()) .order_by(score.desc())
}; };
// TODO make sure community removed isn't fetched either
query = query query = query
.limit(limit) .limit(limit)
.offset(offset) .offset(offset)
.filter(removed.eq(false)); .filter(removed.eq(false))
.filter(community_removed.eq(false));
query.load::<Self>(conn) query.load::<Self>(conn)
} }
@ -255,6 +255,7 @@ mod tests {
removed: false, removed: false,
locked: false, locked: false,
community_name: community_name.to_owned(), community_name: community_name.to_owned(),
community_removed: false,
number_of_comments: 0, number_of_comments: 0,
score: 1, score: 1,
upvotes: 1, upvotes: 1,
@ -280,6 +281,7 @@ mod tests {
creator_name: user_name.to_owned(), creator_name: user_name.to_owned(),
community_id: inserted_community.id, community_id: inserted_community.id,
community_name: community_name.to_owned(), community_name: community_name.to_owned(),
community_removed: false,
number_of_comments: 0, number_of_comments: 0,
score: 1, score: 1,
upvotes: 1, upvotes: 1,

View file

@ -196,7 +196,8 @@ pub struct GetCommunity {
pub struct GetCommunityResponse { pub struct GetCommunityResponse {
op: String, op: String,
community: CommunityView, community: CommunityView,
moderators: Vec<CommunityModeratorView> moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@ -1165,13 +1166,16 @@ impl Perform for GetCommunity {
} }
}; };
let admins = UserView::admins(&conn)?;
// Return the jwt // Return the jwt
Ok( Ok(
serde_json::to_string( serde_json::to_string(
&GetCommunityResponse { &GetCommunityResponse {
op: self.op_type().to_string(), op: self.op_type().to_string(),
community: community_view, community: community_view,
moderators: moderators moderators: moderators,
admins: admins,
} }
)? )?
) )
@ -1817,11 +1821,24 @@ impl Perform for EditCommunity {
} }
// Verify its a mod // Verify its a mod
let moderator_view = CommunityModeratorView::for_community(&conn, self.edit_id)?; let mut editors: Vec<i32> = Vec::new();
let mod_ids: Vec<i32> = moderator_view.into_iter().map(|m| m.user_id).collect(); editors.append(
if !mod_ids.contains(&user_id) { &mut CommunityModeratorView::for_community(&conn, self.edit_id)
return Err(self.error("Incorrect creator."))? ?
}; .into_iter()
.map(|m| m.user_id)
.collect()
);
editors.append(
&mut UserView::admins(&conn)
?
.into_iter()
.map(|a| a.id)
.collect()
);
if !editors.contains(&user_id) {
return Err(self.error("Not allowed to edit community"))?
}
let community_form = CommunityForm { let community_form = CommunityForm {
name: self.name.to_owned(), name: self.name.to_owned(),

View file

@ -210,13 +210,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id; return UserService.Instance.user && this.props.node.comment.creator_id == UserService.Instance.user.id;
} }
get canMod(): boolean {
let adminsThenMods = this.props.admins.map(a => a.id)
.concat(this.props.moderators.map(m => m.user_id));
return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id);
}
get isMod(): boolean { get isMod(): boolean {
return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id); return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.node.comment.creator_id);
} }
@ -225,6 +218,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id); return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
} }
get canMod(): boolean {
let adminsThenMods = this.props.admins.map(a => a.id)
.concat(this.props.moderators.map(m => m.user_id));
return canMod(UserService.Instance.user, adminsThenMods, this.props.node.comment.creator_id);
}
get canAdmin(): boolean { get canAdmin(): boolean {
return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id); return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.node.comment.creator_id);
} }

View file

@ -1,7 +1,7 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Subscription } from "rxjs"; import { Subscription } from "rxjs";
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, CommunityUser} from '../interfaces'; import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, CommunityUser, UserView } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
import { Sidebar } from './sidebar'; import { Sidebar } from './sidebar';
@ -11,6 +11,7 @@ interface State {
community: CommunityI; community: CommunityI;
communityId: number; communityId: number;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
loading: boolean; loading: boolean;
} }
@ -29,9 +30,11 @@ export class Community extends Component<any, State> {
number_of_subscribers: null, number_of_subscribers: null,
number_of_posts: null, number_of_posts: null,
number_of_comments: null, number_of_comments: null,
published: null published: null,
removed: null,
}, },
moderators: [], moderators: [],
admins: [],
communityId: Number(this.props.match.params.id), communityId: Number(this.props.match.params.id),
loading: true loading: true
} }
@ -71,7 +74,11 @@ export class Community extends Component<any, State> {
<PostListings communityId={this.state.communityId} /> <PostListings communityId={this.state.communityId} />
</div> </div>
<div class="col-12 col-md-3"> <div class="col-12 col-md-3">
<Sidebar community={this.state.community} moderators={this.state.moderators} /> <Sidebar
community={this.state.community}
moderators={this.state.moderators}
admins={this.state.admins}
/>
</div> </div>
</div> </div>
} }
@ -90,6 +97,7 @@ export class Community extends Component<any, State> {
let res: GetCommunityResponse = msg; let res: GetCommunityResponse = msg;
this.state.community = res.community; this.state.community = res.community;
this.state.moderators = res.moderators; this.state.moderators = res.moderators;
this.state.admins = res.admins;
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
} else if (op == UserOperation.EditCommunity) { } else if (op == UserOperation.EditCommunity) {

View file

@ -174,6 +174,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id; return UserService.Instance.user && this.props.post.creator_id == UserService.Instance.user.id;
} }
get isMod(): boolean {
return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.post.creator_id);
}
get isAdmin(): boolean {
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.post.creator_id);
}
get canMod(): boolean { get canMod(): boolean {
if (this.props.editable) { if (this.props.editable) {
@ -185,18 +193,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
} else return false; } else return false;
} }
get isMod(): boolean {
return this.props.moderators && isMod(this.props.moderators.map(m => m.user_id), this.props.post.creator_id);
}
get isAdmin(): boolean {
return this.props.admins && isMod(this.props.admins.map(a => a.id), this.props.post.creator_id);
}
get canAdmin(): boolean {
return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.post.creator_id);
}
handlePostLike(i: PostListing) { handlePostLike(i: PostListing) {
let form: CreatePostLikeForm = { let form: CreatePostLikeForm = {

View file

@ -148,7 +148,11 @@ export class Post extends Component<any, PostState> {
sidebar() { sidebar() {
return ( return (
<div class="sticky-top"> <div class="sticky-top">
<Sidebar community={this.state.community} moderators={this.state.moderators} /> <Sidebar
community={this.state.community}
moderators={this.state.moderators}
admins={this.state.admins}
/>
</div> </div>
); );
} }

View file

@ -1,6 +1,6 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Community, CommunityUser, FollowCommunityForm, CommunityForm as CommunityFormI } from '../interfaces'; import { Community, CommunityUser, FollowCommunityForm, CommunityForm as CommunityFormI, UserView } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { mdToHtml, getUnixTime } from '../utils'; import { mdToHtml, getUnixTime } from '../utils';
import { CommunityForm } from './community-form'; import { CommunityForm } from './community-form';
@ -8,6 +8,7 @@ import { CommunityForm } from './community-form';
interface SidebarProps { interface SidebarProps {
community: Community; community: Community;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
} }
interface SidebarState { interface SidebarState {
@ -54,8 +55,9 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
} }
</h5> </h5>
<Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link> <Link className="text-muted" to={`/community/${community.id}`}>/f/{community.name}</Link>
{community.am_mod &&
<ul class="list-inline mb-1 text-muted small font-weight-bold"> <ul class="list-inline mb-1 text-muted small font-weight-bold">
{this.canMod &&
<>
<li className="list-inline-item"> <li className="list-inline-item">
<span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span> <span class="pointer" onClick={linkEvent(this, this.handleEditClick)}>edit</span>
</li> </li>
@ -64,14 +66,18 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
{/* <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> */} {/* <span class="pointer" onClick={linkEvent(this, this.handleDeleteClick)}>delete</span> */}
</li> </li>
} }
</>
}
{this.canAdmin &&
<li className="list-inline-item"> <li className="list-inline-item">
{!this.props.community.removed ? {!this.props.community.removed ?
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> : <span class="pointer" onClick={linkEvent(this, this.handleModRemoveShow)}>remove</span> :
<span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span> <span class="pointer" onClick={linkEvent(this, this.handleModRemoveSubmit)}>restore</span>
} }
</li> </li>
</ul>
} }
</ul>
{this.state.showRemoveDialog && {this.state.showRemoveDialog &&
<form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}> <form onSubmit={linkEvent(this, this.handleModRemoveSubmit)}>
<div class="form-group row"> <div class="form-group row">
@ -156,10 +162,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
return this.props.community.creator_id == UserService.Instance.user.id; return this.props.community.creator_id == UserService.Instance.user.id;
} }
// private get amMod(): boolean { get canMod(): boolean {
// return UserService.Instance.loggedIn && return UserService.Instance.user && this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id);
// this.props.moderators.map(m => m.user_id).includes(UserService.Instance.user.id); }
// }
get canAdmin(): boolean {
return UserService.Instance.user && this.props.admins.map(a => a.id).includes(UserService.Instance.user.id);
}
handleDeleteClick() { handleDeleteClick() {
} }

View file

@ -72,6 +72,7 @@ export interface Post {
updated?: string; updated?: string;
creator_name: string; creator_name: string;
community_name: string; community_name: string;
community_removed: boolean;
number_of_comments: number; number_of_comments: number;
score: number; score: number;
upvotes: number; upvotes: number;
@ -350,6 +351,7 @@ export interface GetCommunityResponse {
op: string; op: string;
community: Community; community: Community;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
} }