Adding some site oriented settings.

- Adding option to close registration. Fixes #350
- Adding option to disable showing NSFW buttons. Fixes #364
- Adding option to disable downvotes. Fixes #239
This commit is contained in:
Dessalines 2019-12-11 12:21:47 -08:00
parent e9f4765663
commit fca8e6a0a9
25 changed files with 349 additions and 167 deletions

View file

@ -0,0 +1,16 @@
-- Drop the columns
drop view site_view;
alter table site drop column enable_downvotes;
alter table site drop column open_registration;
alter table site drop column enable_nsfw;
-- Rebuild the views
create view site_view as
select *,
(select name from user_ u where s.creator_id = u.id) as creator_name,
(select count(*) from user_) as number_of_users,
(select count(*) from post) as number_of_posts,
(select count(*) from comment) as number_of_comments,
(select count(*) from community) as number_of_communities
from site s;

View file

@ -0,0 +1,16 @@
-- Add the column
alter table site add column enable_downvotes boolean default true not null;
alter table site add column open_registration boolean default true not null;
alter table site add column enable_nsfw boolean default true not null;
-- Reload the view
drop view site_view;
create view site_view as
select *,
(select name from user_ u where s.creator_id = u.id) as creator_name,
(select count(*) from user_) as number_of_users,
(select count(*) from post) as number_of_posts,
(select count(*) from comment) as number_of_comments,
(select count(*) from community) as number_of_communities
from site s;

View file

@ -298,6 +298,14 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
let user_id = claims.id;
// Don't do a downvote if site has downvotes disabled
if data.score == -1 {
let site = SiteView::read(&conn)?;
if site.enable_downvotes == false {
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
}
}
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {

View file

@ -8,6 +8,8 @@ use crate::db::moderator_views::*;
use crate::db::password_reset_request::*;
use crate::db::post::*;
use crate::db::post_view::*;
use crate::db::site::*;
use crate::db::site_view::*;
use crate::db::user::*;
use crate::db::user_mention::*;
use crate::db::user_mention_view::*;

View file

@ -265,6 +265,14 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
let user_id = claims.id;
// Don't do a downvote if site has downvotes disabled
if data.score == -1 {
let site = SiteView::read(&conn)?;
if site.enable_downvotes == false {
return Err(APIError::err(&self.op, "downvotes_disabled"))?;
}
}
// Check for a community ban
let post = Post::read(&conn, data.post_id)?;
if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {

View file

@ -56,6 +56,9 @@ pub struct GetModlogResponse {
pub struct CreateSite {
name: String,
description: Option<String>,
enable_downvotes: bool,
open_registration: bool,
enable_nsfw: bool,
auth: String,
}
@ -63,6 +66,9 @@ pub struct CreateSite {
pub struct EditSite {
name: String,
description: Option<String>,
enable_downvotes: bool,
open_registration: bool,
enable_nsfw: bool,
auth: String,
}
@ -208,6 +214,9 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
name: data.name.to_owned(),
description: data.description.to_owned(),
creator_id: user_id,
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
updated: None,
};
@ -255,6 +264,9 @@ impl Perform<SiteResponse> for Oper<EditSite> {
description: data.description.to_owned(),
creator_id: found_site.creator_id,
updated: Some(naive_now()),
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
enable_nsfw: data.enable_nsfw,
};
match Site::update(&conn, 1, &site_form) {
@ -431,6 +443,9 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
description: read_site.description,
creator_id: data.user_id,
updated: Some(naive_now()),
enable_downvotes: read_site.enable_downvotes,
open_registration: read_site.open_registration,
enable_nsfw: read_site.enable_nsfw,
};
match Site::update(&conn, 1, &site_form) {

View file

@ -193,6 +193,13 @@ impl Perform<LoginResponse> for Oper<Register> {
let data: &Register = &self.data;
let conn = establish_connection();
// Make sure site has open registration
if let Ok(site) = SiteView::read(&conn) {
if !site.open_registration {
return Err(APIError::err(&self.op, "registration_closed"))?;
}
}
// Make sure passwords match
if &data.password != &data.password_verify {
return Err(APIError::err(&self.op, "passwords_dont_match"))?;

View file

@ -1,5 +1,5 @@
use super::*;
use crate::schema::{community, community_follower, community_moderator, community_user_ban, site};
use crate::schema::{community, community_follower, community_moderator, community_user_ban};
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "community"]
@ -202,50 +202,6 @@ impl Followable<CommunityFollowerForm> for CommunityFollower {
}
}
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "site"]
pub struct Site {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name = "site"]
pub struct SiteForm {
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub updated: Option<chrono::NaiveDateTime>,
}
impl Crud<SiteForm> for Site {
fn read(conn: &PgConnection, _site_id: i32) -> Result<Self, Error> {
use crate::schema::site::dsl::*;
site.first::<Self>(conn)
}
fn delete(conn: &PgConnection, site_id: i32) -> Result<usize, Error> {
use crate::schema::site::dsl::*;
diesel::delete(site.find(site_id)).execute(conn)
}
fn create(conn: &PgConnection, new_site: &SiteForm) -> Result<Self, Error> {
use crate::schema::site::dsl::*;
insert_into(site).values(new_site).get_result::<Self>(conn)
}
fn update(conn: &PgConnection, site_id: i32, new_site: &SiteForm) -> Result<Self, Error> {
use crate::schema::site::dsl::*;
diesel::update(site.find(site_id))
.set(new_site)
.get_result::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use super::super::user::*;

View file

@ -59,22 +59,6 @@ table! {
}
}
table! {
site_view (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
creator_name -> Varchar,
number_of_users -> BigInt,
number_of_posts -> BigInt,
number_of_comments -> BigInt,
number_of_communities -> BigInt,
}
}
#[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)]
@ -328,28 +312,3 @@ impl CommunityUserBanView {
.first::<Self>(conn)
}
}
#[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)]
#[table_name = "site_view"]
pub struct SiteView {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub creator_name: String,
pub number_of_users: i64,
pub number_of_posts: i64,
pub number_of_comments: i64,
pub number_of_communities: i64,
}
impl SiteView {
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
use super::community_view::site_view::dsl::*;
site_view.first::<Self>(conn)
}
}

View file

@ -14,6 +14,8 @@ pub mod moderator_views;
pub mod password_reset_request;
pub mod post;
pub mod post_view;
pub mod site;
pub mod site_view;
pub mod user;
pub mod user_mention;
pub mod user_mention_view;

52
server/src/db/site.rs Normal file
View file

@ -0,0 +1,52 @@
use super::*;
use crate::schema::site;
#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
#[table_name = "site"]
pub struct Site {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
#[table_name = "site"]
pub struct SiteForm {
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub updated: Option<chrono::NaiveDateTime>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
}
impl Crud<SiteForm> for Site {
fn read(conn: &PgConnection, _site_id: i32) -> Result<Self, Error> {
use crate::schema::site::dsl::*;
site.first::<Self>(conn)
}
fn delete(conn: &PgConnection, site_id: i32) -> Result<usize, Error> {
use crate::schema::site::dsl::*;
diesel::delete(site.find(site_id)).execute(conn)
}
fn create(conn: &PgConnection, new_site: &SiteForm) -> Result<Self, Error> {
use crate::schema::site::dsl::*;
insert_into(site).values(new_site).get_result::<Self>(conn)
}
fn update(conn: &PgConnection, site_id: i32, new_site: &SiteForm) -> Result<Self, Error> {
use crate::schema::site::dsl::*;
diesel::update(site.find(site_id))
.set(new_site)
.get_result::<Self>(conn)
}
}

View file

@ -0,0 +1,48 @@
use super::*;
table! {
site_view (id) {
id -> Int4,
name -> Varchar,
description -> Nullable<Text>,
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
enable_downvotes -> Bool,
open_registration -> Bool,
enable_nsfw -> Bool,
creator_name -> Varchar,
number_of_users -> BigInt,
number_of_posts -> BigInt,
number_of_comments -> BigInt,
number_of_communities -> BigInt,
}
}
#[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)]
#[table_name = "site_view"]
pub struct SiteView {
pub id: i32,
pub name: String,
pub description: Option<String>,
pub creator_id: i32,
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
pub creator_name: String,
pub number_of_users: i64,
pub number_of_posts: i64,
pub number_of_comments: i64,
pub number_of_communities: i64,
}
impl SiteView {
pub fn read(conn: &PgConnection) -> Result<Self, Error> {
use super::site_view::site_view::dsl::*;
site_view.first::<Self>(conn)
}
}

View file

@ -3,8 +3,8 @@ extern crate rss;
use super::*;
use crate::db::comment_view::{ReplyQueryBuilder, ReplyView};
use crate::db::community::Community;
use crate::db::community_view::SiteView;
use crate::db::post_view::{PostQueryBuilder, PostView};
use crate::db::site_view::SiteView;
use crate::db::user::User_;
use crate::db::user_mention_view::{UserMentionQueryBuilder, UserMentionView};
use crate::db::{establish_connection, ListingType, SortType};

View file

@ -118,7 +118,7 @@ impl Settings {
.unwrap_or("3600".to_string())
.parse()
.unwrap(),
email_config: email_config,
email_config,
}
}
fn api_endpoint(&self) -> String {

View file

@ -1,5 +1,5 @@
use crate::db::community_view::SiteView;
use crate::db::establish_connection;
use crate::db::site_view::SiteView;
use crate::version;
use crate::Settings;
use actix_web::body::Body;

View file

@ -246,6 +246,9 @@ table! {
creator_id -> Int4,
published -> Timestamp,
updated -> Nullable<Timestamp>,
enable_downvotes -> Bool,
open_registration -> Bool,
enable_nsfw -> Bool,
}
}

View file

@ -102,16 +102,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<div class={`font-weight-bold text-muted`}>
{node.comment.score}
</div>
<button
className={`btn p-0 ${
node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(node, this.handleCommentDisLike)}
>
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
</svg>
</button>
{WebSocketService.Instance.site.enable_downvotes && (
<button
className={`btn p-0 ${
node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(node, this.handleCommentDisLike)}
>
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
</svg>
</button>
)}
</div>
)}
<div

View file

@ -156,21 +156,24 @@ export class CommunityForm extends Component<
</select>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.communityForm.nsfw}
onChange={linkEvent(this, this.handleCommunityNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="nsfw">#</T>
</label>
{WebSocketService.Instance.site.enable_nsfw && (
<div class="form-group row">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.communityForm.nsfw}
onChange={linkEvent(this, this.handleCommunityNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="nsfw">#</T>
</label>
</div>
</div>
</div>
</div>
)}
<div class="form-group row">
<div class="col-12">
<button type="submit" class="btn btn-secondary mr-2">

View file

@ -205,21 +205,23 @@ export class Login extends Component<any, State> {
/>
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.registerForm.show_nsfw}
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
{WebSocketService.Instance.site.enable_nsfw && (
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.registerForm.show_nsfw}
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
</div>
</div>
</div>
</div>
)}
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary">

View file

@ -280,21 +280,23 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</div>
</div>
)}
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.postForm.nsfw}
onChange={linkEvent(this, this.handlePostNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="nsfw">#</T>
</label>
{WebSocketService.Instance.site.enable_nsfw && (
<div class="form-group row">
<div class="col-sm-10">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.postForm.nsfw}
onChange={linkEvent(this, this.handlePostNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="nsfw">#</T>
</label>
</div>
</div>
</div>
</div>
)}
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-secondary mr-2">

View file

@ -114,16 +114,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</svg>
</button>
<div class={`font-weight-bold text-muted`}>{post.score}</div>
<button
className={`btn p-0 ${
post.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(this, this.handlePostDisLike)}
>
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
</svg>
</button>
{WebSocketService.Instance.site.enable_downvotes && (
<button
className={`btn p-0 ${
post.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(this, this.handlePostDisLike)}
>
<svg class="icon downvote">
<use xlinkHref="#icon-arrow-down"></use>
</svg>
</button>
)}
</div>
{post.url && isImage(post.url) && !post.nsfw && !post.community_nsfw && (
<span

View file

@ -19,6 +19,9 @@ interface SiteFormState {
export class SiteForm extends Component<SiteFormProps, SiteFormState> {
private emptyState: SiteFormState = {
siteForm: {
enable_downvotes: true,
open_registration: true,
enable_nsfw: true,
name: null,
},
loading: false,
@ -31,6 +34,9 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
this.state.siteForm = {
name: this.props.site.name,
description: this.props.site.description,
enable_downvotes: this.props.site.enable_downvotes,
open_registration: this.props.site.open_registration,
enable_nsfw: this.props.site.enable_nsfw,
};
}
}
@ -77,6 +83,54 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
/>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.siteForm.enable_downvotes}
onChange={linkEvent(this, this.handleSiteEnableDownvotesChange)}
/>
<label class="form-check-label">
<T i18nKey="enable_downvotes">#</T>
</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.siteForm.enable_nsfw}
onChange={linkEvent(this, this.handleSiteEnableNsfwChange)}
/>
<label class="form-check-label">
<T i18nKey="enable_nsfw">#</T>
</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.siteForm.open_registration}
onChange={linkEvent(
this,
this.handleSiteOpenRegistrationChange
)}
/>
<label class="form-check-label">
<T i18nKey="open_registration">#</T>
</label>
</div>
</div>
</div>
<div class="form-group row">
<div class="col-12">
<button type="submit" class="btn btn-secondary mr-2">
@ -126,6 +180,21 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
i.setState(i.state);
}
handleSiteEnableNsfwChange(i: SiteForm, event: any) {
i.state.siteForm.enable_nsfw = event.target.checked;
i.setState(i.state);
}
handleSiteOpenRegistrationChange(i: SiteForm, event: any) {
i.state.siteForm.open_registration = event.target.checked;
i.setState(i.state);
}
handleSiteEnableDownvotesChange(i: SiteForm, event: any) {
i.state.siteForm.enable_downvotes = event.target.checked;
i.setState(i.state);
}
handleCancel(i: SiteForm) {
i.props.onCancel();
}

View file

@ -496,24 +496,26 @@ export class User extends Component<any, UserState> {
/>
</div>
</form>
<div class="form-group">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.userSettingsForm.show_nsfw}
onChange={linkEvent(
this,
this.handleUserSettingsShowNsfwChange
)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
{WebSocketService.Instance.site.enable_nsfw && (
<div class="form-group">
<div class="col-12">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
checked={this.state.userSettingsForm.show_nsfw}
onChange={linkEvent(
this,
this.handleUserSettingsShowNsfwChange
)}
/>
<label class="form-check-label">
<T i18nKey="show_nsfw">#</T>
</label>
</div>
</div>
</div>
</div>
)}
<div class="form-group">
<div class="col-12">
<button

View file

@ -199,6 +199,9 @@ export interface Site {
number_of_posts: number;
number_of_comments: number;
number_of_communities: number;
enable_downvotes: boolean;
open_registration: boolean;
enable_nsfw: boolean;
}
export enum BanType {
@ -625,9 +628,9 @@ export interface CreatePostLikeResponse {
export interface SiteForm {
name: string;
description?: string;
removed?: boolean;
reason?: string;
expires?: number;
enable_downvotes: boolean;
open_registration: boolean;
enable_nsfw: boolean;
auth?: string;
}

View file

@ -127,6 +127,11 @@ export const en = {
expires: 'Expires',
language: 'Language',
browser_default: 'Browser Default',
downvotes_disabled: 'Downvotes disabled',
enable_downvotes: 'Enable Downvotes',
open_registration: 'Open Registration',
registration_closed: 'Registration closed',
enable_nsfw: 'Enable NSFW',
url: 'URL',
body: 'Body',
copy_suggested_title: 'copy suggested title: {{title}}',