mirror of
https://github.com/wassname/talk.git
synced 2026-07-07 04:48:24 +08:00
Merge branch 'master' of github.com:coralproject/talk into wordlist
This commit is contained in:
@@ -3,8 +3,14 @@ A commenting platform from The Coral Project. [https://coralproject.net](https:/
|
||||
|
||||
## Contributing to Talk
|
||||
|
||||
### Product Roadmap
|
||||
You can view what the Coral Team is working on next here: https://www.pivotaltracker.com/n/projects/1863625
|
||||
|
||||
You can view product ideas and our longer term roadmap here: https://trello.com/b/ILND751a/talk
|
||||
|
||||
### Local Dependencies
|
||||
Node
|
||||
|
||||
Mongo
|
||||
|
||||
### Getting Started
|
||||
@@ -19,13 +25,19 @@ Runs Talk.
|
||||
The Talk application requires specific configuration options to be available
|
||||
inside the environment in order to run, those variables are listed here:
|
||||
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies
|
||||
- `TALK_SESSION_SECRET` (*required*) - a random string which will be used to
|
||||
secure cookies.
|
||||
- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook
|
||||
Login enabled app.
|
||||
Login enabled app.
|
||||
- `TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - Root url of the installed application externally available in the format: `<scheme>://<host>` without the path.
|
||||
Facebook Login enabled app.
|
||||
- `TALK_ROOT_URL` (*required*) - root url of the installed application externally
|
||||
available in the format: `<scheme>://<host>` without the path.
|
||||
- `TALK_SMTP_PROVIDER` (*required*) - SMTP provider name.
|
||||
- `TALK_SMTP_USERNAME` (*required*) - username of the SMTP provider you are using.
|
||||
- `TALK_SMTP_PASSWORD` (*required*) - password for the SMTP provider you are using.
|
||||
- `TALK_SMTP_HOST` (*required*) - SMTP host url with format `smtp.domain.com`.
|
||||
- `TALK_SMTP_PORT` (*required*) - SMTP port.
|
||||
|
||||
### Running with Docker
|
||||
Make sure you have Docker running first and then run `docker-compose up -d`
|
||||
@@ -37,9 +49,11 @@ Make sure you have Docker running first and then run `docker-compose up -d`
|
||||
`npm run lint`
|
||||
|
||||
### Helpful URLs
|
||||
Bare comment stream: http://localhost:5000/client/coral-embed-stream/
|
||||
Comment stream embedded on sample article: http://localhost:5000/client/coral-embed-stream/samplearticle.html
|
||||
Moderator view: http://localhost:5000/admin/
|
||||
Comment stream: http://localhost:3000/
|
||||
|
||||
Comment stream embedded on sample article: http://localhost:3000/assets/samplearticle.html
|
||||
|
||||
Moderator view: http://localhost:3000/admin
|
||||
|
||||
### Docs
|
||||
`swagger.yaml`
|
||||
|
||||
@@ -94,7 +94,9 @@ app.use((req, res, next) => {
|
||||
// returning a status code that makes sense.
|
||||
app.use('/api', (err, req, res, next) => {
|
||||
if (err !== ErrNotFound) {
|
||||
console.error(err);
|
||||
if (app.get('env') !== 'test') {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
res.status(err.status || 500);
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
@@ -0,0 +1 @@
|
||||
<mxfile userAgent="Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/54.0.2840.98 Safari/537.36" version="6.0.1.8" editor="www.draw.io" type="device"><diagram name="Page-1">7Vpbc6M2FP41nrYP6wFksP1oO0k7nd1O2nTa7qMMCtZGICpE4vTXVwIJkCVnnQbbWU/z4MDRBen7zk0HRmCVbX9ksNh8ogkio8BLtiNwNQqC6dwTv1Lw3AjCYNoIUoaTRuR3gjv8D1JCNS6tcIJKoyOnlHBcmMKY5jmKuSGDjNEns9s9JeZTC5giS3AXQ2JL/8QJ3zTSWeh18p8QTjf6yb6nWtYwfkgZrXL1vFEA7uu/pjmDei7Vv9zAhD71ROB6BFaMUt5cZdsVIhJaDVsz7mZPa7tuhnJ+yIBJ1Ix4hKRCesn1wvizBkOMELiLm6VYbSGFMaGVmGD5tMEc3RUwlsInoQlCtuEZEXe+uFRTI8bRdu/6/HbXQpkQzRBnz6KLGhBpjdCK1Nw9daQEGvpNn5C56giVIqTtzB0Y4kLh4cYGOKBIhJqoW8r4hqY0h+S6ky5r7pGcwTPBQFvM/5LicajuPuuWXCys1yRvP6sJSg4ZX0iNrmGHZYljLb7BpJ08T+xOQtjr8gVx/qwsDVacClG3g4+UFqpfyRl9QCtKKKv3DLz6r23R9tARbLEpIKMVixVmCkWx4hSpXoHSOgnni1rAEIEcP5pW+RZOw/kxOPV7jHrjaWiQalA6Bt8IqTvU/BeWQ+9EpEb+8UkNTFLHfmTyGrbtt4hhsQXEviETHoDtJpKcgO3AiliLohCCO8REqBkFERErWib4UVym8vL7X0R6MgpWcp/bgqGyHH8pf9AdxeN6fS1FOijEOdCyot7eEAemZojzdZbRC3L+xBXkZt7b0Zx8Pf4fH4I2fVIQABcEMxcEkwEgiCbHdx8vhYTatVyil/AnjtB/Ii/huxTbcg2LJMP5SObIN+JXHmOYWAbN9zkHewLpet6JI/F1dqwdSTCzrShwWFE0hBGF5zWi6cUaUXhGIwotI1pRBmWX62yNkvIdaDmYOWLFsbTcn9vJRyWeGni3jD5i4T7OAUmkE2Bt+L59TPY9YEMCBoCkXW4HySeIiUrIsNj1OQAJDECmDhVxlQ2GwCME53WEl+oHdc3gLCfMwC6T/UzXwta9XyskJI5Dx0OF3tcxY7aTYx/qN6cDGAVwBBKCxWa+kxjeib1biIhtcRMEU8dymkvLuRe6uiOCBKe51GkxvzyFLyVIOIZkoRoynCS12blQ34Pra4qWhxxnpg6kgyGQjo7hfuyi5WW4mDe4DjX0lmIxY0t9sHOYB7ucNi5Njepoff1EjR+0Jqr1o93PYSWAo5TPTJVpg5Sjzn056mRGLDC3I1ZzLD1BxJrYdZz/X14ci9TwRKQC+/hxw2jtNq7zRJ5CSJXivHTlIwzBWHS8YSiptoemJfqFXxOLE1qt6xbf0BV/TyQd9k3g7rkvmJzw3DeZWsCrYpFQNFU+imAmd52vy8JdK1rRLKtyzJ/39f1qtekTzGHa1bgPJK/KyCKWC+0yoY9wjcgtLXFd7AJXa8o5zUQHIhuW7UvsniWp19h2NsXpDtntO21pd4V+GyKVA+epEg+gEMAMkD6w9QEcSx1mdjrbI9eiDWXrujLwrmkbnhLgOHcfixKdYvco+Q0l2C7HaHgTyGEp4N13Dhj4O4adigSIbGjmcxsa3x8AG/2sgXMBqyRxifE+1J8fnaH8GjpKazRP6dV+p3FqrTYNPnAWHl3v7QZR68CCx3C+PXyivyv5RdVSFhU+qPrAQvQg6J53rbuR975JrexpZMOHslZPOYs/Kbb2LLpS/jskD+LfgsWSj5hXjfgKw5TBrBcUmseZS3ghJTjz9v4QLWNfUkiKDXzlNi6p1mNmIbZbb2VvrDTL3L39LrA55nffXoLrfwE=</diagram></mxfile>
|
||||
+1
-1
@@ -15,7 +15,7 @@ const mongoose = require('../mongoose');
|
||||
const Setting = require('../models/setting');
|
||||
const util = require('../util');
|
||||
|
||||
// Regeister the shutdown criteria.
|
||||
// Register the shutdown criteria.
|
||||
util.onshutdown([
|
||||
() => mongoose.disconnect()
|
||||
]);
|
||||
|
||||
+22
-9
@@ -34,6 +34,7 @@ function createUser(options) {
|
||||
email: options.email,
|
||||
password: options.password,
|
||||
displayName: options.name,
|
||||
role: options.role
|
||||
});
|
||||
}
|
||||
|
||||
@@ -62,6 +63,11 @@ function createUser(options) {
|
||||
name: 'displayName',
|
||||
description: 'Display Name',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'role',
|
||||
description: 'User Role',
|
||||
required: false
|
||||
}
|
||||
], (err, result) => {
|
||||
if (err) {
|
||||
@@ -76,15 +82,21 @@ function createUser(options) {
|
||||
});
|
||||
})
|
||||
.then((result) => {
|
||||
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim());
|
||||
})
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
util.shutdown();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
return User.createLocalUser(result.email.trim(), result.password.trim(), result.displayName.trim())
|
||||
.then((user) => {
|
||||
console.log(`Created user ${user.id}.`);
|
||||
|
||||
return User
|
||||
.addRoleToUser(user.id, result.role.trim())
|
||||
.then(() => {
|
||||
console.log(`Added the admin ${result.role.trim()} to User ${user.id}.`);
|
||||
util.shutdown();
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
util.shutdown();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,6 +342,7 @@ program
|
||||
.option('--email [email]', 'Email to use')
|
||||
.option('--password [password]', 'Password to use')
|
||||
.option('--name [name]', 'Name to use')
|
||||
.option('--role [role]', 'Role to add')
|
||||
.option('-f, --flag_mode', 'Source from flags instead of prompting')
|
||||
.description('create a new user')
|
||||
.action(createUser);
|
||||
|
||||
@@ -26,11 +26,16 @@ const updateInfoBoxContent = (props) => (event) => {
|
||||
props.updateSettings({infoBoxContent});
|
||||
};
|
||||
|
||||
const StateLess = (props) => <List>
|
||||
const updateClosedMessage = (props) => (event) => {
|
||||
const closedMessage = event.target.value;
|
||||
props.updateSettings({closedMessage});
|
||||
};
|
||||
|
||||
const CommentSettings = (props) => <List>
|
||||
<ListItem className={styles.configSetting}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateModeration(props)}
|
||||
onClick={updateModeration}
|
||||
checked={props.settings.moderation === 'pre'} />
|
||||
</ListItemAction>
|
||||
{lang.t('configure.enable-pre-moderation')}
|
||||
@@ -38,7 +43,7 @@ const StateLess = (props) => <List>
|
||||
<ListItem threeLine className={styles.configSettingInfoBox}>
|
||||
<ListItemAction>
|
||||
<Checkbox
|
||||
onClick={updateInfoBoxEnable(props)}
|
||||
onClick={updateInfoBoxEnable}
|
||||
checked={props.settings.infoBoxEnable} />
|
||||
</ListItemAction>
|
||||
<ListItemContent>
|
||||
@@ -48,10 +53,20 @@ const StateLess = (props) => <List>
|
||||
</p>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${props.settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItem className={styles.configSettingInfoBox}>
|
||||
<ListItemContent>
|
||||
{lang.t('configure.closed-comments-desc')}
|
||||
<Textfield
|
||||
onChange={updateClosedMessage}
|
||||
value={props.settings.closedMessage}
|
||||
label={lang.t('configure.closed-comments-label')}
|
||||
rows={3}/>
|
||||
</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={`${styles.configSettingInfoBox} ${this.props.settings.infoBoxEnable ? null : styles.hidden}`} >
|
||||
<ListItemContent>
|
||||
<Textfield
|
||||
onChange={updateInfoBoxContent(props)}
|
||||
onChange={updateInfoBoxContent}
|
||||
value={props.settings.infoBoxContent}
|
||||
label={lang.t('configure.include-text')}
|
||||
rows={3}/>
|
||||
@@ -59,6 +74,6 @@ const StateLess = (props) => <List>
|
||||
</ListItem>
|
||||
</List>;
|
||||
|
||||
export default StateLess;
|
||||
export default CommentSettings;
|
||||
|
||||
const lang = new I18n(translations);
|
||||
|
||||
@@ -18,21 +18,19 @@ import Wordlist from './Wordlist';
|
||||
class Configure extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
activeSection: 'comments',
|
||||
wordlist: [],
|
||||
changed: false
|
||||
};
|
||||
this.saveSettings = this.saveSettings.bind(this);
|
||||
this.onChangeWordlist = this.onChangeWordlist.bind(this);
|
||||
this.onSettingUpdate = this.onSettingUpdate.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
componentWillMount = () => {
|
||||
this.props.dispatch(fetchSettings());
|
||||
}
|
||||
|
||||
componentWillUpdate (newProps) {
|
||||
componentWillUpdate = (newProps) => {
|
||||
if ((!this.props.settings
|
||||
|| !this.props.settings.wordlist)
|
||||
&& newProps.settings.wordlist
|
||||
@@ -41,16 +39,16 @@ class Configure extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
saveSettings () {
|
||||
saveSettings = () => {
|
||||
this.props.dispatch(saveSettingsToServer());
|
||||
this.setState({changed: false});
|
||||
}
|
||||
|
||||
changeSection (activeSection) {
|
||||
changeSection = (activeSection) => () => {
|
||||
this.setState({activeSection});
|
||||
}
|
||||
|
||||
onChangeWordlist (event) {
|
||||
onChangeWordlist = (event) => {
|
||||
event.preventDefault();
|
||||
const newlist = event.target.value;
|
||||
this.setState({wordlist: newlist.toLowerCase(), changed: true});
|
||||
@@ -61,12 +59,12 @@ class Configure extends React.Component {
|
||||
}));
|
||||
}
|
||||
|
||||
onSettingUpdate (setting) {
|
||||
onSettingUpdate = (setting) => {
|
||||
this.setState({changed: true});
|
||||
this.props.dispatch(updateSettings(setting));
|
||||
}
|
||||
|
||||
getSection (section) {
|
||||
getSection = (section) => {
|
||||
switch(section){
|
||||
case 'comments':
|
||||
return <CommentSettings
|
||||
@@ -81,7 +79,7 @@ class Configure extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
getPageTitle (section) {
|
||||
getPageTitle = (section) => {
|
||||
switch(section) {
|
||||
case 'comments':
|
||||
return lang.t('configure.comment-settings');
|
||||
@@ -106,17 +104,17 @@ class Configure extends React.Component {
|
||||
<List>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'comments')}
|
||||
onClick={this.changeSection('comments')}
|
||||
icon='settings'>{lang.t('configure.comment-settings')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'embed')}
|
||||
onClick={this.changeSection('embed')}
|
||||
icon='code'>{lang.t('configure.embed-comment-stream')}</ListItemContent>
|
||||
</ListItem>
|
||||
<ListItem className={styles.settingOption}>
|
||||
<ListItemContent
|
||||
onClick={this.changeSection.bind(this, 'wordlist')}
|
||||
onClick={this.changeSection('wordlist')}
|
||||
icon='settings'>{lang.t('configure.wordlist')}</ListItemContent>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
@@ -81,9 +81,11 @@ class ModerationQueue extends React.Component {
|
||||
singleView={singleView}
|
||||
commentIds={
|
||||
comments.get('ids')
|
||||
.filter(id => !comments.get('byId')
|
||||
.filter(id =>
|
||||
comments
|
||||
.get('byId')
|
||||
.get(id)
|
||||
.get('status'))
|
||||
.get('status') === 'premod')
|
||||
}
|
||||
comments={comments.get('byId')}
|
||||
users={users.get('byId')}
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
"copy-and-paste": "Copy and paste code below into your CMS to embed your comment box in your articles",
|
||||
"moderate": "Moderate",
|
||||
"configure": "Configure",
|
||||
"community": "Community"
|
||||
"community": "Community",
|
||||
"closed-comments-desc": "Write a message for closed threads",
|
||||
"closed-comments-label": "Write a message..."
|
||||
}
|
||||
},
|
||||
"es": {
|
||||
@@ -81,7 +83,9 @@
|
||||
"copy-and-paste": "Copiar y pegar el código de más abajo en tu CMS para colocar la caja de comentarios en tus articulos",
|
||||
"moderate": "Moderar",
|
||||
"configure": "Configurar",
|
||||
"community": "Comunidad"
|
||||
"community": "Comunidad",
|
||||
"closed-comments-desc": "Escribe un mensaje para cuando los comentarios se encuentran cerrados",
|
||||
"closed-comments-label": "Escribe un mensaje..."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ class CommentStream extends Component {
|
||||
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
|
||||
const {actions, users, comments} = this.props.items;
|
||||
const {loggedIn, user, showSignInDialog, signInOffset} = this.props.auth;
|
||||
const {status} = this.props.config;
|
||||
const {status, closedMessage} = this.props.config;
|
||||
const {activeTab} = this.state;
|
||||
const expandForLogin = showSignInDialog ? {
|
||||
minHeight: document.body.scrollHeight + 150
|
||||
@@ -133,7 +133,7 @@ class CommentStream extends Component {
|
||||
author={user}
|
||||
/>
|
||||
</div>
|
||||
: <p>Comments are closed for this thread.</p>
|
||||
: <p>{closedMessage}</p>
|
||||
}
|
||||
{!loggedIn && <SignInContainer offset={signInOffset} />}
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ import coralApi from '../helpers/response';
|
||||
* Action name constants
|
||||
*/
|
||||
|
||||
export const UPDATE_SETTINGS = 'UPDATE_SETTINGS';
|
||||
export const OPEN_COMMENTS = 'OPEN_COMMENTS';
|
||||
export const CLOSE_COMMENTS = 'CLOSE_COMMENTS';
|
||||
export const ADD_ITEM = 'ADD_ITEM';
|
||||
|
||||
@@ -5,7 +5,8 @@ import * as actions from '../actions/config';
|
||||
|
||||
const initialState = Map({
|
||||
features: Map({}),
|
||||
status: 'open'
|
||||
status: 'open',
|
||||
closedMessage: ''
|
||||
});
|
||||
|
||||
export default (state = initialState, action) => {
|
||||
|
||||
@@ -7,6 +7,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "2525:2525"
|
||||
environment:
|
||||
- "TALK_PORT=5000"
|
||||
- "TALK_MONGO_URL=mongodb://mongo"
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
// The maximum depth to recurse into nested objects checking for mongoose
|
||||
// objects.
|
||||
const maxRecursion = 3;
|
||||
|
||||
/**
|
||||
* Middleware to wrap the `res.json` function to ensure that we can filter the
|
||||
* payload response first based on user and role.
|
||||
*/
|
||||
module.exports = (req, res, next) => {
|
||||
/**
|
||||
* Updates the original document based on filtering out for roles.
|
||||
* @param {Mixed} o original object to be modified
|
||||
* @param {Integer} l current level of depth in the first object
|
||||
* @return {Mixed} (possibly) modified object
|
||||
*/
|
||||
const wrap = (o, d = 0) => {
|
||||
if (d > maxRecursion) {
|
||||
return o;
|
||||
}
|
||||
|
||||
// If this is an array, we need to walk over all the object's elements.
|
||||
if (Array.isArray(o)) {
|
||||
|
||||
// Map each of the array elements.
|
||||
return o.map((ob) => wrap(ob, d + 1));
|
||||
|
||||
} else if (o && o.constructor && o.constructor.name === 'model') {
|
||||
|
||||
// The object here is definitly a mongoose model.
|
||||
|
||||
// Check to see if it has a `filterForUser` method.
|
||||
if (typeof o.filterForUser === 'function') {
|
||||
|
||||
// The object here actually has the `filterForUser` function, so filter
|
||||
// the object!
|
||||
o = o.filterForUser(req.user);
|
||||
}
|
||||
|
||||
} else if (typeof o === 'object') {
|
||||
|
||||
// Iterate over the props, find only properties owned by the object.
|
||||
for (let prop in o) {
|
||||
|
||||
// If and only if the object owns the property do we actually pull the
|
||||
// property out.
|
||||
if (typeof o.hasOwnProperty === 'function' && o.hasOwnProperty(prop)) {
|
||||
|
||||
// Wrap the property with one more layer down.
|
||||
o[prop] = wrap(o[prop], d + 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return o;
|
||||
};
|
||||
|
||||
// Save a reference to the original json function.
|
||||
const json = res.json;
|
||||
|
||||
// Override the original json function.
|
||||
res.json = (payload) => {
|
||||
|
||||
// Restore the old pointer.
|
||||
res.json = json;
|
||||
|
||||
// Send it down the pipe after we've filtered it.
|
||||
res.json(wrap(payload));
|
||||
};
|
||||
|
||||
// Now that we've overridden the `res.json`, let's hand it down.
|
||||
next();
|
||||
};
|
||||
+17
-6
@@ -29,6 +29,14 @@ const AssetSchema = new Schema({
|
||||
type: Schema.Types.Mixed,
|
||||
default: null
|
||||
},
|
||||
closedAt: {
|
||||
type: Date,
|
||||
default: null
|
||||
},
|
||||
closedMessage: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
title: String,
|
||||
description: String,
|
||||
image: String,
|
||||
@@ -36,11 +44,7 @@ const AssetSchema = new Schema({
|
||||
subsection: String,
|
||||
author: String,
|
||||
publication_date: Date,
|
||||
modified_date: Date,
|
||||
status: {
|
||||
type: String,
|
||||
default: 'open'
|
||||
}
|
||||
modified_date: Date
|
||||
}, {
|
||||
versionKey: false,
|
||||
timestamps: {
|
||||
@@ -60,6 +64,13 @@ AssetSchema.index({
|
||||
background: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns true if the asset is closed, false else.
|
||||
*/
|
||||
AssetSchema.virtual('isClosed').get(function() {
|
||||
return this.closedAt && this.closedAt.getTime() <= new Date().getTime();
|
||||
});
|
||||
|
||||
/**
|
||||
* Finds an asset by its id.
|
||||
* @param {String} id identifier of the asset (uuid).
|
||||
@@ -85,7 +96,7 @@ AssetSchema.statics.rectifySettings = (assetQuery) => Promise.all([
|
||||
|
||||
// If the asset exists and has settings then return the merged object.
|
||||
if (asset && asset.settings) {
|
||||
return Object.assign({}, settings, asset.settings);
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
|
||||
return settings;
|
||||
|
||||
+32
-29
@@ -1,5 +1,6 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const Schema = mongoose.Schema;
|
||||
const _ = require('lodash');
|
||||
const uuid = require('uuid');
|
||||
const Action = require('./action');
|
||||
|
||||
@@ -45,7 +46,7 @@ const CommentSchema = new Schema({
|
||||
},
|
||||
asset_id: String,
|
||||
author_id: String,
|
||||
status: [StatusSchema],
|
||||
status_history: [StatusSchema],
|
||||
parent_id: String
|
||||
}, {
|
||||
timestamps: {
|
||||
@@ -59,22 +60,7 @@ const CommentSchema = new Schema({
|
||||
* output.
|
||||
*/
|
||||
CommentSchema.options.toJSON = {};
|
||||
CommentSchema.options.toJSON.hide = '_id status';
|
||||
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
delete ret[prop];
|
||||
});
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* toJSON overrides to remove fields from the json
|
||||
* output.
|
||||
*/
|
||||
CommentSchema.options.toJSON = {};
|
||||
CommentSchema.options.toJSON.virtuals = true;
|
||||
CommentSchema.options.toJSON.hide = '_id';
|
||||
CommentSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
@@ -86,14 +72,31 @@ CommentSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
return ret;
|
||||
};
|
||||
|
||||
/**
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
CommentSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['id', 'body', 'asset_id', 'author_id', 'parent_id', 'status']);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* Sets up a virtual getter function on a comment such that when you try and
|
||||
* access the `comment.last_status` it returns the last status in the array
|
||||
* of status's on the comment, or `null` if there was no status.
|
||||
* of status's on the comment, or `null` if there was no status_history.
|
||||
*/
|
||||
CommentSchema.virtual('last_status').get(function() {
|
||||
if (this.status && this.status.length > 0) {
|
||||
return this.status[this.status.length - 1].type;
|
||||
CommentSchema.virtual('status').get(function() {
|
||||
|
||||
// Here we are taking advantage of the fact that when documents are inserted
|
||||
// for the new status on a comment that they are always appended to the end
|
||||
// of the list in the order that they are inserted, hence, the last status
|
||||
// is always the most recent.
|
||||
if (this.status_history && this.status_history.length > 0) {
|
||||
return this.status_history[this.status_history.length - 1].type;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -123,7 +126,7 @@ CommentSchema.statics.publicCreate = (comment) => {
|
||||
body,
|
||||
asset_id,
|
||||
parent_id,
|
||||
status: status ? [{
|
||||
status_history: status ? [{
|
||||
type: status,
|
||||
created_at: new Date()
|
||||
}] : [],
|
||||
@@ -157,7 +160,7 @@ CommentSchema.statics.findByAssetId = (asset_id) => Comment.find({
|
||||
*/
|
||||
CommentSchema.statics.findAcceptedByAssetId = (asset_id) => Comment.find({
|
||||
asset_id,
|
||||
'status.type': 'accepted'
|
||||
'status_history.type': 'accepted'
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -169,10 +172,10 @@ CommentSchema.statics.findAcceptedAndNewByAssetId = (asset_id) => Comment.find({
|
||||
asset_id,
|
||||
$or: [
|
||||
{
|
||||
'status.type': 'accepted'
|
||||
'status_history.type': 'accepted'
|
||||
},
|
||||
{
|
||||
status: {
|
||||
status_history: {
|
||||
$size: 0
|
||||
}
|
||||
}
|
||||
@@ -202,7 +205,7 @@ CommentSchema.statics.findIdsByActionType = (action_type) => Action
|
||||
.then((actions) => actions.map(a => a.item_id));
|
||||
|
||||
/**
|
||||
* Find comments by their status.
|
||||
* Find comments by their status_history.
|
||||
* @param {String} status the status of the comment to search for
|
||||
* @return {Promise}
|
||||
*/
|
||||
@@ -210,9 +213,9 @@ CommentSchema.statics.findByStatus = (status = false) => {
|
||||
let q = {};
|
||||
|
||||
if (status) {
|
||||
q['status.type'] = status;
|
||||
q['status_history.type'] = status;
|
||||
} else {
|
||||
q.status = {$size: 0};
|
||||
q.status_history = {$size: 0};
|
||||
}
|
||||
|
||||
return Comment.find(q);
|
||||
@@ -269,7 +272,7 @@ CommentSchema.statics.moderationQueue = (moderation, asset_id = false) => {
|
||||
*/
|
||||
CommentSchema.statics.pushStatus = (id, status, assigned_by = null) => Comment.update({id}, {
|
||||
$push: {
|
||||
status: {
|
||||
status_history: {
|
||||
type: status,
|
||||
created_at: new Date(),
|
||||
assigned_by
|
||||
|
||||
+49
-9
@@ -28,6 +28,16 @@ const SettingSchema = new Schema({
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
closedTimeout: {
|
||||
type: Number,
|
||||
|
||||
// Two weeks default expiry.
|
||||
default: 60 * 60 * 24 * 7 * 2
|
||||
},
|
||||
closedMessage: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
wordlist: [String]
|
||||
}, {
|
||||
timestamps: {
|
||||
@@ -36,6 +46,42 @@ const SettingSchema = new Schema({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* toJSON provides settings overrides to this object's serialization methods.
|
||||
*/
|
||||
SettingSchema.options.toJSON = {};
|
||||
SettingSchema.options.toJSON.virtuals = true;
|
||||
|
||||
/**
|
||||
* Merges two settings objects.
|
||||
*/
|
||||
SettingSchema.method('merge', function(src) {
|
||||
SettingSchema.eachPath((path) => {
|
||||
|
||||
// Exclude internal fields...
|
||||
if (['id', '_id', '__v', 'created_at', 'updated_at'].includes(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the source object contains the path, shallow copy it.
|
||||
if (path in src) {
|
||||
this[path] = src[path];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
SettingSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['moderation', 'infoBoxEnable', 'infoBoxContent']);
|
||||
}
|
||||
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
/**
|
||||
* The Mongo Mongoose object.
|
||||
*/
|
||||
@@ -62,7 +108,9 @@ const EXPIRY_TIME = 60 * 2;
|
||||
* Gets the entire settings record and sends it back
|
||||
* @return {Promise} settings the whole settings record
|
||||
*/
|
||||
SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => Setting.findOne(selector));
|
||||
SettingService.retrieve = () => cache.wrap('settings', EXPIRY_TIME, () => {
|
||||
return Setting.findOne(selector);
|
||||
}).then((setting) => new Setting(setting));
|
||||
|
||||
/**
|
||||
* This will update the settings object with whatever you pass in
|
||||
@@ -83,14 +131,6 @@ SettingService.update = (settings) => Setting.findOneAndUpdate(selector, {
|
||||
.then(() => settings);
|
||||
});
|
||||
|
||||
/**
|
||||
* Filters the document to ensure that the resulting document is indeed ready
|
||||
* for non authenticated users.
|
||||
* @param {Object} settings the source settings object
|
||||
* @return {Object} the filtered settings object
|
||||
*/
|
||||
SettingService.public = (settings) => _.pick(settings, ['moderation', 'infoBoxEnable', 'infoBoxContent']);
|
||||
|
||||
/**
|
||||
* This is run once when the app starts to ensure settings are populated.
|
||||
* @return {Promise} null initialize the global settings object
|
||||
|
||||
+10
-12
@@ -1,5 +1,6 @@
|
||||
const mongoose = require('../mongoose');
|
||||
const uuid = require('uuid');
|
||||
const _ = require('lodash');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
@@ -105,7 +106,8 @@ UserSchema.index({
|
||||
* output.
|
||||
*/
|
||||
UserSchema.options.toJSON = {};
|
||||
UserSchema.options.toJSON.hide = '_id password profiles roles disabled';
|
||||
UserSchema.options.toJSON.hide = '_id password';
|
||||
UserSchema.options.toJSON.virtuals = true;
|
||||
UserSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
@@ -117,20 +119,16 @@ UserSchema.options.toJSON.transform = (doc, ret, options) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* toObject overrides to remove the password field from the toObject
|
||||
* output.
|
||||
* Filters the object for the given user only allowing those with the allowed
|
||||
* roles/permissions to access particular parameters.
|
||||
*/
|
||||
UserSchema.options.toObject = {};
|
||||
UserSchema.options.toObject.hide = 'password';
|
||||
UserSchema.options.toObject.transform = (doc, ret, options) => {
|
||||
if (options.hide) {
|
||||
options.hide.split(' ').forEach((prop) => {
|
||||
delete ret[prop];
|
||||
});
|
||||
UserSchema.method('filterForUser', function(user = false) {
|
||||
if (!user || !user.roles.includes('admin')) {
|
||||
return _.pick(this.toJSON(), ['id', 'displayName', 'settings', 'created_at', 'updated_at']);
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
return this.toJSON();
|
||||
});
|
||||
|
||||
// Create the User model.
|
||||
const UserModel = mongoose.model('User', UserSchema);
|
||||
|
||||
@@ -80,7 +80,20 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
status = Promise.resolve('rejected');
|
||||
} else {
|
||||
status = Asset
|
||||
.rectifySettings(Asset.findById(asset_id))
|
||||
.rectifySettings(Asset.findById(asset_id).then((asset) => {
|
||||
if (!asset) {
|
||||
return Promise.reject(new Error('asset referenced is not found'));
|
||||
}
|
||||
|
||||
// Check to see if the asset has closed commenting...
|
||||
if (asset.isClosed) {
|
||||
|
||||
// They have, ensure that we send back an error.
|
||||
return Promise.reject(new Error(`asset has commenting closed because: ${asset.closedMessage}`));
|
||||
}
|
||||
|
||||
return asset;
|
||||
}))
|
||||
|
||||
// Return `premod` if pre-moderation is enabled and an empty "new" status
|
||||
// in the event that it is not in pre-moderation mode.
|
||||
@@ -96,7 +109,7 @@ router.post('/', wordlist.filter('body'), (req, res, next) => {
|
||||
}))
|
||||
.then((comment) => {
|
||||
// The comment was created! Send back the created comment.
|
||||
res.status(201).send(comment);
|
||||
res.status(201).json(comment);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
const express = require('express');
|
||||
const authorization = require('../../middleware/authorization');
|
||||
const payloadFilter = require('../../middleware/payload-filter');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Filter all content going down the pipe based on user roles.
|
||||
router.use(payloadFilter);
|
||||
|
||||
router.use('/asset', authorization.needed('admin'), require('./asset'));
|
||||
router.use('/settings', authorization.needed('admin'), require('./settings'));
|
||||
router.use('/queue', authorization.needed('admin'), require('./queue'));
|
||||
|
||||
@@ -1,21 +1,32 @@
|
||||
const express = require('express');
|
||||
const _ = require('lodash');
|
||||
const scraper = require('../../../services/scraper');
|
||||
const url = require('url');
|
||||
|
||||
const Comment = require('../../../models/comment');
|
||||
const User = require('../../../models/user');
|
||||
const Action = require('../../../models/action');
|
||||
const Asset = require('../../../models/asset');
|
||||
const Setting = require('../../../models/setting');
|
||||
const ErrInvalidAssetURL = new Error('asset_url is invalid');
|
||||
ErrInvalidAssetURL.status = 400;
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', (req, res, next) => {
|
||||
|
||||
let asset_url = decodeURIComponent(req.query.asset_url);
|
||||
|
||||
// Verify that the asset_url is parsable.
|
||||
let parsed_asset_url = url.parse(asset_url);
|
||||
if (!parsed_asset_url.protocol) {
|
||||
return next(ErrInvalidAssetURL);
|
||||
}
|
||||
|
||||
// Get the asset_id for this url (or create it if it doesn't exist)
|
||||
Promise.all([
|
||||
// Find or create the asset by url.
|
||||
Asset.findOrCreateByUrl(decodeURIComponent(req.query.asset_url))
|
||||
Asset.findOrCreateByUrl(asset_url)
|
||||
|
||||
// Add the found asset to the scraper if it's not already scraped.
|
||||
.then((asset) => {
|
||||
@@ -34,7 +45,7 @@ router.get('/', (req, res, next) => {
|
||||
// Merge the asset specific settings with the returned settings object in
|
||||
// the event that the asset that was returned also had settings.
|
||||
if (asset && asset.settings) {
|
||||
settings = Object.assign({}, settings, asset.settings);
|
||||
settings.merge(asset.settings);
|
||||
}
|
||||
|
||||
// Fetch the appropriate comments stream.
|
||||
@@ -98,7 +109,7 @@ router.get('/', (req, res, next) => {
|
||||
comments,
|
||||
users,
|
||||
actions,
|
||||
settings: Setting.public(settings)
|
||||
settings
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
|
||||
+20
-27
@@ -26,26 +26,15 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
.limit(limit),
|
||||
User.count()
|
||||
])
|
||||
.then(([data, count]) => {
|
||||
const users = data.map((user) => {
|
||||
const {id, displayName, created_at} = user;
|
||||
return {
|
||||
id,
|
||||
displayName,
|
||||
created_at,
|
||||
profiles: user.toObject().profiles,
|
||||
roles: user.toObject().roles
|
||||
};
|
||||
});
|
||||
.then(([result, count]) => {
|
||||
|
||||
res.json({
|
||||
result: users,
|
||||
result,
|
||||
limit: Number(limit),
|
||||
count,
|
||||
page: Number(page),
|
||||
totalPages: Math.ceil(count / limit)
|
||||
totalPages: Math.ceil(count / (limit === 0 ? 1 : limit))
|
||||
});
|
||||
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
@@ -53,8 +42,8 @@ router.get('/', authorization.needed('admin'), (req, res, next) => {
|
||||
router.post('/:user_id/role', authorization.needed('admin'), (req, res, next) => {
|
||||
User
|
||||
.addRoleToUser(req.params.user_id, req.body.role)
|
||||
.then(role => {
|
||||
res.send(role);
|
||||
.then(() => {
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
@@ -65,13 +54,17 @@ router.post('/', (req, res, next) => {
|
||||
User
|
||||
.createLocalUser(email, password, displayName)
|
||||
.then(user => {
|
||||
res.status(201).send(user);
|
||||
|
||||
res.status(201).json(user);
|
||||
})
|
||||
.catch(err => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
const ErrPasswordTooShort = new Error('password must be at least 8 characters');
|
||||
ErrPasswordTooShort.status = 400;
|
||||
|
||||
/**
|
||||
* expects 2 fields in the body of the request
|
||||
* 1) the token that was in the url of the email link {String}
|
||||
@@ -81,7 +74,7 @@ router.post('/update-password', (req, res, next) => {
|
||||
const {token, password} = req.body;
|
||||
|
||||
if (!password || password.length < 8) {
|
||||
return res.status(400).send('Password must be at least 8 characters');
|
||||
return next(ErrPasswordTooShort);
|
||||
}
|
||||
|
||||
User.verifyPasswordResetToken(token)
|
||||
@@ -93,7 +86,8 @@ router.post('/update-password', (req, res, next) => {
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
res.status(401).send('Not Authorized');
|
||||
|
||||
next(authorization.ErrNotAuthorized);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -133,10 +127,8 @@ router.post('/request-password-reset', (req, res, next) => {
|
||||
// if we fail on missing emails, it would reveal if people are registered or not.
|
||||
res.status(204).end();
|
||||
})
|
||||
.catch(error => {
|
||||
const errorMsg = typeof error === 'string' ? error : error.message;
|
||||
|
||||
res.status(500).json({error: errorMsg});
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,10 +142,11 @@ router.put('/:user_id/bio', (req, res, next) => {
|
||||
|
||||
User
|
||||
.addBio(user_id, bio)
|
||||
.then(user => res.status(200).send(user))
|
||||
.catch(error => {
|
||||
const errorMsg = typeof error === 'string' ? error : error.message;
|
||||
res.status(500).json({error: errorMsg});
|
||||
.then(user => {
|
||||
res.json(user);
|
||||
})
|
||||
.catch((err) => {
|
||||
next(err);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+4
-8
@@ -3,7 +3,7 @@ const nodemailer = require('nodemailer');
|
||||
const smtpRequiredProps = [
|
||||
'TALK_SMTP_USERNAME',
|
||||
'TALK_SMTP_PASSWORD',
|
||||
'TALK_SMTP_PROVIDER'
|
||||
'TALK_SMTP_HOST'
|
||||
];
|
||||
|
||||
smtpRequiredProps.forEach(prop => {
|
||||
@@ -13,9 +13,7 @@ smtpRequiredProps.forEach(prop => {
|
||||
});
|
||||
|
||||
const options = {
|
||||
// list of providers here:
|
||||
// https://github.com/nodemailer/nodemailer-wellknown#supported-services
|
||||
service: process.env.TALK_SMTP_PROVIDER,
|
||||
host: process.env.TALK_SMTP_HOST,
|
||||
auth: {
|
||||
user: process.env.TALK_SMTP_USERNAME,
|
||||
pass: process.env.TALK_SMTP_PASSWORD
|
||||
@@ -24,10 +22,8 @@ const options = {
|
||||
|
||||
if (process.env.TALK_SMTP_PORT) {
|
||||
options.port = process.env.TALK_SMTP_PORT;
|
||||
}
|
||||
|
||||
if (process.env.TALK_SMTP_HOST) {
|
||||
options.host = process.env.TALK_SMTP_HOST;
|
||||
} else {
|
||||
options.port = 25;
|
||||
}
|
||||
|
||||
const defaultTransporter = nodemailer.createTransport(options);
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ const scraper = {
|
||||
title: `Scrape for asset ${asset.id}`,
|
||||
asset_id: asset.id
|
||||
})
|
||||
.attempts(10)
|
||||
.attempts(3)
|
||||
.delay(1000)
|
||||
.backoff({type: 'exponential'})
|
||||
.save((err) => {
|
||||
@@ -106,7 +106,7 @@ const scraper = {
|
||||
|
||||
// Handle errors that occur.
|
||||
.catch((err) => {
|
||||
console.error(`Failed to scrape on Job[${job.id}] for Asset[${job.data.asset_id}]:`, err);
|
||||
debug(`Failed to scrape on Job[${job.id}] for Asset[${job.data.asset_id}]:`, err);
|
||||
|
||||
done(err);
|
||||
});
|
||||
|
||||
+20
-24
@@ -11,14 +11,14 @@ describe('models.Comment', () => {
|
||||
const comments = [{
|
||||
body: 'comment 10',
|
||||
asset_id: '123',
|
||||
status: [],
|
||||
status_history: [],
|
||||
parent_id: '',
|
||||
author_id: '123',
|
||||
id: '1'
|
||||
}, {
|
||||
body: 'comment 20',
|
||||
asset_id: '123',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}],
|
||||
parent_id: '',
|
||||
@@ -27,14 +27,14 @@ describe('models.Comment', () => {
|
||||
}, {
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: [],
|
||||
status_history: [],
|
||||
parent_id: '',
|
||||
author_id: '456',
|
||||
id: '3'
|
||||
}, {
|
||||
body: 'comment 40',
|
||||
asset_id: '123',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}],
|
||||
parent_id: '',
|
||||
@@ -43,7 +43,7 @@ describe('models.Comment', () => {
|
||||
}, {
|
||||
body: 'comment 50',
|
||||
asset_id: '1234',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}],
|
||||
parent_id: '',
|
||||
@@ -52,7 +52,7 @@ describe('models.Comment', () => {
|
||||
}, {
|
||||
body: 'comment 60',
|
||||
asset_id: '1234',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}],
|
||||
parent_id: '',
|
||||
@@ -99,8 +99,7 @@ describe('models.Comment', () => {
|
||||
expect(c).to.not.be.null;
|
||||
expect(c.id).to.not.be.null;
|
||||
expect(c.id).to.be.uuid;
|
||||
expect(c.status).to.have.length(1);
|
||||
expect(c.status[0]).to.have.property('type', 'accepted');
|
||||
expect(c.status).to.be.equal('accepted');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,17 +115,15 @@ describe('models.Comment', () => {
|
||||
}]).then(([c1, c2, c3]) => {
|
||||
expect(c1).to.not.be.null;
|
||||
expect(c1.id).to.be.uuid;
|
||||
expect(c1.status).to.have.length(1);
|
||||
expect(c1.status[0]).to.have.property('type', 'accepted');
|
||||
expect(c1.status).to.be.equal('accepted');
|
||||
|
||||
expect(c2).to.not.be.null;
|
||||
expect(c2.id).to.be.uuid;
|
||||
expect(c2.status).to.have.length(0);
|
||||
expect(c2.status).to.be.null;
|
||||
|
||||
expect(c3).to.not.be.null;
|
||||
expect(c3.id).to.be.uuid;
|
||||
expect(c3.status).to.have.length(1);
|
||||
expect(c3.status[0]).to.have.property('type', 'rejected');
|
||||
expect(c3.status).to.be.equal('rejected');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -220,17 +217,16 @@ describe('models.Comment', () => {
|
||||
|
||||
return Comment.findById(comment_id)
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.have.length(0);
|
||||
expect(c.status).to.be.null;
|
||||
|
||||
return Comment.pushStatus(comment_id, 'rejected', '123');
|
||||
})
|
||||
.then(() => Comment.findById(comment_id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.have.length(1);
|
||||
expect(c.status[0]).to.have.property('type', 'rejected');
|
||||
expect(c.status[0]).to.have.property('assigned_by', '123');
|
||||
expect(c.status_history).to.have.length(1);
|
||||
expect(c.status_history[0]).to.have.property('type', 'rejected');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -238,13 +234,13 @@ describe('models.Comment', () => {
|
||||
return Comment.pushStatus(comments[1].id, 'rejected', '123')
|
||||
.then(() => Comment.findById(comments[1].id))
|
||||
.then((c) => {
|
||||
expect(c).to.have.property('status');
|
||||
expect(c.status).to.have.length(2);
|
||||
expect(c.status[0]).to.have.property('type', 'accepted');
|
||||
expect(c.status[0]).to.have.property('assigned_by', null);
|
||||
expect(c).to.have.property('status_history');
|
||||
expect(c.status_history).to.have.length(2);
|
||||
expect(c.status_history[0]).to.have.property('type', 'accepted');
|
||||
expect(c.status_history[0]).to.have.property('assigned_by', null);
|
||||
|
||||
expect(c.status[1]).to.have.property('type', 'rejected');
|
||||
expect(c.status[1]).to.have.property('assigned_by', '123');
|
||||
expect(c.status_history[1]).to.have.property('type', 'rejected');
|
||||
expect(c.status_history[1]).to.have.property('assigned_by', '123');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,4 +39,18 @@ describe('models.Setting', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#merge', () => {
|
||||
it('should merge a settings object and its overrides', () => {
|
||||
return Setting
|
||||
.retrieve()
|
||||
.then((settings) => {
|
||||
let ovrSett = {moderation: 'post'};
|
||||
|
||||
settings.merge(ovrSett);
|
||||
|
||||
expect(settings).to.have.property('moderation', 'post');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,6 +19,12 @@ const settings = {id: '1', moderation: 'pre'};
|
||||
|
||||
describe('/api/v1/comments', () => {
|
||||
|
||||
// Ensure that the settings are always available.
|
||||
beforeEach(() => Promise.all([
|
||||
wordlist.insert(['bad words']),
|
||||
Setting.init(settings)
|
||||
]));
|
||||
|
||||
describe('#get', () => {
|
||||
const comments = [{
|
||||
body: 'comment 10',
|
||||
@@ -32,13 +38,13 @@ describe('/api/v1/comments', () => {
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}];
|
||||
@@ -75,11 +81,7 @@ describe('/api/v1/comments', () => {
|
||||
|
||||
return Action.create(actions);
|
||||
}),
|
||||
User.createLocalUsers(users),
|
||||
wordlist.insert([
|
||||
'bad words'
|
||||
]),
|
||||
Setting.init(settings)
|
||||
User.createLocalUsers(users)
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -142,11 +144,19 @@ describe('/api/v1/comments', () => {
|
||||
|
||||
describe('#post', () => {
|
||||
|
||||
let asset_id;
|
||||
|
||||
beforeEach(() => Asset.findOrCreateByUrl('https://coralproject.net/section/article-is-the-best').then((asset) => {
|
||||
|
||||
// Update the asset id.
|
||||
asset_id = asset.id;
|
||||
}));
|
||||
|
||||
it('should create a comment', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset_id, 'parent_id': ''})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
@@ -157,12 +167,11 @@ describe('/api/v1/comments', () => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': '1', 'parent_id': ''})
|
||||
.send({'body': 'bad words are the baddest', 'author_id': '123', 'asset_id': asset_id, 'parent_id': ''})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('status').and.to.have.length(1);
|
||||
expect(res.body.status[0]).to.have.property('type', 'rejected');
|
||||
expect(res.body).to.have.property('status', 'rejected');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,10 +193,46 @@ describe('/api/v1/comments', () => {
|
||||
expect(res).to.have.status(201);
|
||||
expect(res.body).to.have.property('id');
|
||||
expect(res.body).to.have.property('asset_id');
|
||||
expect(res.body).to.have.property('status').and.to.have.length(1);
|
||||
expect(res.body.status[0]).to.have.property('type', 'premod');
|
||||
expect(res.body).to.have.property('status', 'premod');
|
||||
});
|
||||
});
|
||||
|
||||
it('shouldn\'t create a comment when the asset has expired commenting', () => {
|
||||
return Asset.create({
|
||||
closedAt: new Date().setDate(0),
|
||||
closedMessage: 'tests said expired!'
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(500);
|
||||
})
|
||||
.catch((err) => {
|
||||
expect(err.response.body).to.not.be.null;
|
||||
expect(err.response.body).to.have.property('message');
|
||||
expect(err.response.body.message).to.contain('tests said expired!');
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a comment when the asset has not expired yet', () => {
|
||||
return Asset.create({
|
||||
closedAt: new Date().setDate(32),
|
||||
closedMessage: 'tests said expired!'
|
||||
})
|
||||
.then((asset) => {
|
||||
return chai.request(app)
|
||||
.post('/api/v1/comments')
|
||||
.set(passport.inject({roles: []}))
|
||||
.send({'body': 'Something body.', 'author_id': '123', 'asset_id': asset.id, 'parent_id': ''});
|
||||
})
|
||||
.then((res) => {
|
||||
expect(res).to.have.status(201);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -301,20 +346,20 @@ describe('/api/v1/comments/:comment_id/actions', () => {
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '123',
|
||||
status: []
|
||||
status_history: []
|
||||
}, {
|
||||
id: 'def',
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}];
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('/api/v1/queue', () => {
|
||||
body: 'comment 10',
|
||||
asset_id: 'asset',
|
||||
author_id: '123',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}, {
|
||||
@@ -29,14 +29,14 @@ describe('/api/v1/queue', () => {
|
||||
body: 'comment 20',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'premod'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 30',
|
||||
asset_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}];
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('/api/v1/stream', () => {
|
||||
body: 'comment 10',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}, {
|
||||
@@ -33,21 +33,21 @@ describe('/api/v1/stream', () => {
|
||||
body: 'comment 20',
|
||||
author_id: '',
|
||||
parent_id: '',
|
||||
status: []
|
||||
status_history: []
|
||||
}, {
|
||||
id: 'uio',
|
||||
body: 'comment 30',
|
||||
asset_id: 'asset',
|
||||
author_id: '456',
|
||||
parent_id: '',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'accepted'
|
||||
}]
|
||||
}, {
|
||||
id: 'hij',
|
||||
body: 'comment 40',
|
||||
asset_id: '456',
|
||||
status: [{
|
||||
status_history: [{
|
||||
type: 'rejected'
|
||||
}]
|
||||
}];
|
||||
@@ -116,6 +116,16 @@ describe('/api/v1/stream', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject requests without a scheme in the asset_url', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
.query({asset_url: 'test.com'})
|
||||
.catch((err) => {
|
||||
expect(err).to.have.status(400);
|
||||
expect(err.response.body.message).to.contain('asset_url is invalid');
|
||||
});
|
||||
});
|
||||
|
||||
it('should merge the settings when the asset contains settings to override it with', () => {
|
||||
return chai.request(app)
|
||||
.get('/api/v1/stream')
|
||||
@@ -126,6 +136,7 @@ describe('/api/v1/stream', () => {
|
||||
expect(res.body.comments.length).to.equal(1);
|
||||
expect(res.body.users.length).to.equal(1);
|
||||
expect(res.body.settings).to.have.property('moderation', 'pre');
|
||||
expect(res.body.settings).to.not.have.property('wordlist');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user