diff --git a/README.md b/README.md index 6589cddb9..0b77e95b7 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ You can view product ideas and our longer term roadmap here: https://trello.com/ ### Local Dependencies Node + Mongo ### Getting Started @@ -26,17 +27,17 @@ 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_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook +- `TALK_FACEBOOK_APP_ID` (*required*) - the Facebook app id for your Facebook Login enabled app. -`TALK_FACEBOOK_APP_SECRET` (*required*) - the Facebook app secret for your +- `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 +- `TALK_ROOT_URL` (*required*) - root url of the installed application externally available in the format: `://` 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. +- `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` @@ -48,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` diff --git a/client/coral-admin/src/containers/Configure/Configure.js b/client/coral-admin/src/containers/Configure/Configure.js index a9e2f3ef2..e171fc27d 100644 --- a/client/coral-admin/src/containers/Configure/Configure.js +++ b/client/coral-admin/src/containers/Configure/Configure.js @@ -28,6 +28,7 @@ class Configure extends React.Component { // InfoBox has two settings. Enable or not and the content of it if it is enable. this.updateInfoBoxEnable = this.updateInfoBoxEnable.bind(this); this.updateInfoBoxContent = this.updateInfoBoxContent.bind(this); + this.updateClosedMessage = this.updateClosedMessage.bind(this); this.saveSettings = this.saveSettings.bind(this); } @@ -51,6 +52,11 @@ class Configure extends React.Component { this.props.dispatch(updateSettings({infoBoxContent})); } + updateClosedMessage (event) { + const closedMessage = event.target.value; + this.props.dispatch(updateSettings({closedMessage})); + } + saveSettings () { this.props.dispatch(saveSettingsToServer()); } @@ -78,6 +84,16 @@ class Configure extends React.Component {

+ + + {lang.t('configure.closed-comments-desc')} + + + >>>>>> master } } } diff --git a/client/coral-embed-stream/src/CommentStream.js b/client/coral-embed-stream/src/CommentStream.js index 829bd5de1..fb8a1962f 100644 --- a/client/coral-embed-stream/src/CommentStream.js +++ b/client/coral-embed-stream/src/CommentStream.js @@ -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 banned = (this.props.userData.status === 'banned'); @@ -137,7 +137,7 @@ class CommentStream extends Component { /> - :

Comments are closed for this thread.

+ :

{closedMessage}

} {!loggedIn && } { diff --git a/client/coral-framework/actions/config.js b/client/coral-framework/actions/config.js index 6dc880434..e004fa1eb 100644 --- a/client/coral-framework/actions/config.js +++ b/client/coral-framework/actions/config.js @@ -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'; diff --git a/client/coral-framework/reducers/config.js b/client/coral-framework/reducers/config.js index 9620f5b97..7fbccaa49 100644 --- a/client/coral-framework/reducers/config.js +++ b/client/coral-framework/reducers/config.js @@ -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) => { diff --git a/docker-compose.yml b/docker-compose.yml index d7a180277..16df60095 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,7 @@ services: restart: always ports: - "5000:5000" + - "2525:2525" environment: - "TALK_PORT=5000" - "TALK_MONGO_URL=mongodb://mongo" diff --git a/models/asset.js b/models/asset.js index 0cdc77d7b..77b8ebff3 100644 --- a/models/asset.js +++ b/models/asset.js @@ -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, @@ -56,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). diff --git a/models/setting.js b/models/setting.js index 86dc761f3..c7589ca56 100644 --- a/models/setting.js +++ b/models/setting.js @@ -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: { diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index 7b8898823..f5668552d 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -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. diff --git a/services/mailer.js b/services/mailer.js index b7d621954..fe07f063b 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -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); diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js index 988320e34..37713e899 100644 --- a/tests/routes/api/comments/index.js +++ b/tests/routes/api/comments/index.js @@ -19,6 +19,7 @@ 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) @@ -143,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'); @@ -158,7 +167,7 @@ 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'); @@ -187,6 +196,43 @@ describe('/api/v1/comments', () => { 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); + }); + }); }); });