diff --git a/.eslintrc.json b/.eslintrc.json index 5df3444a6..23bdea410 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -55,6 +55,7 @@ "no-var": [2], "no-lonely-if": [2], "curly": [2], + "no-unused-vars": ["error", { "argsIgnorePattern": "next" }], "no-multiple-empty-lines": [ "error", {"max": 1} diff --git a/Dockerfile b/Dockerfile index 985e55159..3be5916a8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,15 +11,9 @@ EXPOSE 5000 # Install app dependencies COPY package.json /usr/src/app/ -RUN npm install +RUN npm install --production # Bundle app source COPY . /usr/src/app -# Compile static assets -RUN npm run build - -# Prune development dependancies -RUN npm prune --production - CMD [ "npm", "start" ] diff --git a/README.md b/README.md index d295ef326..f4d8b31de 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) A commenting platform from The Coral Project. [https://coralproject.net](https://coralproject.net) +## Contributing to Talk + +### Local Dependencies +Node +Mongo + ### Getting Started `npm install` Run it once to install the dependencies. @@ -8,16 +14,22 @@ Run it once to install the dependencies. `npm start` Runs Talk. +### Running with Docker +Make sure you have Docker running first and then run `docker-compose up -d` + ### Testing `npm test` ### Lint `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/ + ### Docs `swagger.yaml` -### Mantainers - ### License **Apache-2.0** diff --git a/TERMINOLOGY.md b/TERMINOLOGY.md new file mode 100644 index 000000000..5c1e9aaa8 --- /dev/null +++ b/TERMINOLOGY.md @@ -0,0 +1,66 @@ +# Product's Terminology + +This is a guide to have a common language to talk about "Talk". + +## Definitions + +* Site - a top level site, aka nytimes.com +* Section - the section of a site, aka, Politics. +* Subsection - the section of a site, aka, Politics. +* Asset - An article/video/etc identified by URL. + +* Embed - Things we put on a asset: comment box, ToS, Stream, etc… +* Stream - All the activity on a certain asset. Container for Comments, actions, user +* Thread - defined by a parent and everything below. All replies to a comment and their replies, etc… +* Comment - a kind of user-generated content submitted by a comment author + * A parent comment has replies to it + * A child comments is a reply to another comment + * A comment can be both a parent comment and a child of another comment + * A top-level comment is a comment that is not a reply to any other comment + * A nth-level comment refers to the number of replies away from the top-level comment + +* User - an item to represent a person using Talk. It could be a moderator, reader, etc. +* User Roles: + * Active: some who takes action (logged in or not) + * Passive: some who just reads, no actions performed + * Comment Author: The user who wrote the comment + * Staff Member: someone who works for an organization (tagged for leverage in trust) + * Moderator: someone with the ability to access the moderation queue and perform moderation actions + * Administrator: has the ability to change the setup of their coral space +* Public Profile: information about users shown in public +* Private Profile: information about users shown only to user about themselves +* Protected Profile: information about users that only moderators and admins can see + +* Queue - Group of items based on a query, aka - moderation queue +* Target - The item/s on which an action is performed.. + +## Actions + +Actions are performed by users on items. Actions themselves are items. This requires two relationships: action on item, and user performs action. + +### Flag +* A Flagger(user) performs a Flag +* A Flag is performed on a Comment or a username or profile content + + +## Moderation Actions and Status + +Comments contain a field `status`. As moderation actions are peformed, the status changes. + +* Initial status is empty. +* When a moderator Approves, the status is set to 'approved'. +* When a moderator Rejects, the status is set to 'reject'. + +### Pre and post moderation + +Comments can be set to be premoderated or postmoderated. + +Premoderation means that moderation has to occur _before_ a comment is shown on the site: + +* New comments are shown in the moderator queues immediately. +* The are not shown to users until (aka in streams) until they are approved by a moderator. + +Postmoderation means that comments appear on the site _before_ any moderation action is taken. + +* New comments appear in comment streams immediately. +* New comments do not appear in moderation queues unless they are flagged by other users. diff --git a/app.js b/app.js index 838819e35..29817c970 100644 --- a/app.js +++ b/app.js @@ -8,11 +8,60 @@ const app = express(); // Middleware declarations. app.use(morgan('dev')); app.use(bodyParser.json()); +app.set('views', path.join(__dirname, 'views')); +app.set('view engine', 'ejs'); -// API Routes. +// Routes. app.use('/api/v1', require('./routes/api')); +app.use('/client', express.static(path.join(__dirname, 'dist'))); +app.use('/admin', require('./routes/admin')); -// Static Routes. -app.use('/client/', express.static(path.join(__dirname, 'dist'))); +//============================================================================== +// ERROR HANDLING +//============================================================================== + +const ErrNotFound = new Error('Not Found'); +ErrNotFound.status = 404; + +// Catch 404 and forward to error handler. +app.use((req, res, next) => { + next(ErrNotFound); +}); + +// General error handler. Respond with the message and error if we have it while +// returning a status code that makes sense. +if (app.get('env') === 'development') { + app.use('/api', (err, req, res, next) => { + res.status(err.status || 500); + res.json({ + message: err.message, + error: err + }); + }); + + app.use('/', (err, req, res, next) => { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: err + }); + }); +} + +app.use('/api', (err, req, res, next) => { + res.status(err.status || 500); + res.json({ + message: err.message, + error: {} + }); +}); + +app.use('/', (err, req, res, next) => { + res.status(err.status || 500); + res.render('error', { + message: err.message, + error: {} + }); +}); module.exports = app; diff --git a/circle.yml b/circle.yml index 9de9eae4c..b9cea6fed 100644 --- a/circle.yml +++ b/circle.yml @@ -8,6 +8,7 @@ test: override: - MOCHA_FILE=$CIRCLE_TEST_REPORTS/junit/test-results.xml ./node_modules/.bin/mocha tests --reporter mocha-junit-reporter - npm run lint + - npm run build deployment: release: diff --git a/client/coral-admin/.gitignore b/client/coral-admin/.gitignore deleted file mode 100644 index c9c7cb580..000000000 --- a/client/coral-admin/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -node_modules -public/bundle.js -public/embed/comment-stream -.DS_Store -npm-debug.log -config.json -yarn.lock diff --git a/client/coral-admin/config.json b/client/coral-admin/config.json new file mode 100644 index 000000000..156740de5 --- /dev/null +++ b/client/coral-admin/config.json @@ -0,0 +1,3 @@ +{ + "basePath": "admin" +} diff --git a/client/coral-admin/config.sample.json b/client/coral-admin/config.sample.json deleted file mode 100644 index 55d39db05..000000000 --- a/client/coral-admin/config.sample.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "basePath": "client/coral-admin" -} diff --git a/client/coral-admin/package.json b/client/coral-admin/package.json deleted file mode 100644 index 048359526..000000000 --- a/client/coral-admin/package.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "name": "coral-admin", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "build": "./node_modules/.bin/webpack --config webpack.config.js", - "start": "./node_modules/.bin/webpack-dev-server --config webpack.config.dev.js --inline --hot --content-base public --port 3142" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "hammerjs": "2.0.8", - "immutable": "3.8.1", - "keymaster": "1.6.2", - "material-design-lite": "1.2.1", - "react": "^15.3.2", - "react-dom": "^15.3.2", - "react-mdl": "^1.7.2", - "react-redux": "^4.4.5", - "react-router": "^3.0.0", - "redux": "3.6.0", - "redux-thunk": "2.1.0", - "timeago.js": "2.0.2" - }, - "devDependencies": { - "autoprefixer": "6.5.0", - "babel-core": "^6.18.2", - "babel-loader": "^6.2.7", - "copy-webpack-plugin": "3.0.1", - "css-loader": "0.25.0", - "json-loader": "0.5.4", - "postcss-loader": "0.13.0", - "postcss-modules": "0.5.2", - "precss": "1.4.0", - "standard": "8.2.0", - "style-loader": "0.13.1", - "webpack": "^1.13.3", - "webpack-dev-server": "1.16.1" - } -} diff --git a/client/coral-admin/src/AppRouter.js b/client/coral-admin/src/AppRouter.js new file mode 100644 index 000000000..40465bd19 --- /dev/null +++ b/client/coral-admin/src/AppRouter.js @@ -0,0 +1,23 @@ +import React from 'react' +import { Router, Route, Redirect, IndexRoute, browserHistory } from 'react-router' + +import ModerationQueue from 'containers/ModerationQueue' +import CommentStream from 'containers/CommentStream' +import EmbedLink from 'components/EmbedLink' +import Configure from 'containers/Configure' +import CommunityContainer from 'containers/CommunityContainer' +import LayoutContainer from 'containers/LayoutContainer' + +const routes = ( + + + + + + + +) + +const AppRouter = () => + +export default AppRouter diff --git a/client/coral-admin/src/components/App.js b/client/coral-admin/src/components/App.js index 4b7c01e84..1fbbe1448 100644 --- a/client/coral-admin/src/components/App.js +++ b/client/coral-admin/src/components/App.js @@ -1,25 +1,16 @@ - import React from 'react' import { Provider } from 'react-redux' import 'material-design-lite' -import { Router, Route, browserHistory } from 'react-router' -import ModerationQueue from 'containers/ModerationQueue' +import { Layout } from 'react-mdl' import store from 'services/store' -import CommentStream from 'containers/CommentStream' -import EmbedLink from 'components/EmbedLink' -import Configure from 'containers/Configure' -import config from 'services/config' + +import AppRouter from '../AppRouter' export default class App extends React.Component { render (props) { return ( - - - - - - + ) } diff --git a/client/coral-admin/src/components/Header.js b/client/coral-admin/src/components/Header.js deleted file mode 100644 index 0e52686d8..000000000 --- a/client/coral-admin/src/components/Header.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import { Layout, Navigation, Drawer, Header } from 'react-mdl' -import { Link } from 'react-router' -import styles from './Header.css' -import config from 'services/config' - -// App header. If we add a navbar it should be here -export default (props) => ( - -
- - Moderate - Configure - -
- - - Moderate - Configure - - - {props.children} -
-) diff --git a/client/coral-admin/src/components/Page.js b/client/coral-admin/src/components/Page.js deleted file mode 100644 index 733cec677..000000000 --- a/client/coral-admin/src/components/Page.js +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react' -import {Layout} from 'react-mdl' -import 'material-design-lite' -import Header from 'components/Header' - -export default (props) => ( - -
- {props.children} -
-
-) diff --git a/client/coral-admin/src/components/ui/Drawer.js b/client/coral-admin/src/components/ui/Drawer.js new file mode 100644 index 000000000..b1c87baaf --- /dev/null +++ b/client/coral-admin/src/components/ui/Drawer.js @@ -0,0 +1,14 @@ +import React from 'react' +import { Navigation, Drawer } from 'react-mdl' +import { Link } from 'react-router' +import styles from './Header.css' + +export default () => ( + + + Moderate + Community + Configure + + +) diff --git a/client/coral-admin/src/components/Header.css b/client/coral-admin/src/components/ui/Header.css similarity index 100% rename from client/coral-admin/src/components/Header.css rename to client/coral-admin/src/components/ui/Header.css diff --git a/client/coral-admin/src/components/ui/Header.js b/client/coral-admin/src/components/ui/Header.js new file mode 100644 index 000000000..1455721d8 --- /dev/null +++ b/client/coral-admin/src/components/ui/Header.js @@ -0,0 +1,14 @@ +import React from 'react' +import { Navigation, Header } from 'react-mdl' +import { Link } from 'react-router' +import styles from './Header.css' + +export default () => ( +
+ + Moderate + Community + Configure + +
+) diff --git a/client/coral-admin/src/components/ui/Layout.js b/client/coral-admin/src/components/ui/Layout.js new file mode 100644 index 000000000..fd963b667 --- /dev/null +++ b/client/coral-admin/src/components/ui/Layout.js @@ -0,0 +1,12 @@ +import React from 'react' +import { Layout as LayoutMDL} from 'react-mdl' +import Header from './Header' +import Drawer from './Drawer' + +export const Layout = ({ children }) => ( + +
+ + {children} + +) \ No newline at end of file diff --git a/client/coral-admin/src/containers/CommentStream.js b/client/coral-admin/src/containers/CommentStream.js index 4f0a03698..33963f422 100644 --- a/client/coral-admin/src/containers/CommentStream.js +++ b/client/coral-admin/src/containers/CommentStream.js @@ -6,7 +6,6 @@ import { connect } from 'react-redux' import { createComment, flagComment } from 'actions/comments' import CommentList from 'components/CommentList' import CommentBox from 'components/CommentBox' -import Page from 'components/Page' /** * Renders a comment stream using a CommentList component @@ -44,19 +43,17 @@ class CommentStream extends React.Component { // Render the comment box along with the CommentList render ({ comments }, { snackbar, snackbarMsg }) { return ( - -
- - - {snackbarMsg} -
-
+
+ + + {snackbarMsg} +
) } } diff --git a/client/coral-admin/src/containers/CommunityContainer.js b/client/coral-admin/src/containers/CommunityContainer.js new file mode 100644 index 000000000..bd13ca04b --- /dev/null +++ b/client/coral-admin/src/containers/CommunityContainer.js @@ -0,0 +1,14 @@ +import React, { Component } from 'react' +import { connect } from 'react-redux' +import I18n from 'coral-framework/i18n/i18n' +import translations from '../translations' + +export default class CommunityContainer extends Component { + render() { + return ( +
+

Community

+
+ ) + } +} \ No newline at end of file diff --git a/client/coral-admin/src/containers/Configure.js b/client/coral-admin/src/containers/Configure.js index 4a36e56e7..5c86019c8 100644 --- a/client/coral-admin/src/containers/Configure.js +++ b/client/coral-admin/src/containers/Configure.js @@ -10,8 +10,9 @@ import { Button, Icon } from 'react-mdl' -import Page from 'components/Page' import styles from './Configure.css' +import I18n from 'coral-framework/i18n/i18n' +import translations from '../translations' class Configure extends React.Component { constructor (props) { @@ -40,12 +41,28 @@ class Configure extends React.Component { } + copyToClipBoard (event) { + const copyTextarea = document.querySelector('.' + styles.embedInput) + copyTextarea.select() + + try { + document.execCommand('copy') + } catch (err) { + console.error('Unable to copy') + } + } + getEmbed () { + const embedText = + `
` + return

Copy and paste code below into your CMS to embed your comment box in your articles

- - + +
} @@ -60,7 +77,6 @@ class Configure extends React.Component { : 'Embed Comment Stream' return ( -
@@ -88,9 +104,10 @@ class Configure extends React.Component { }
-
) } } export default connect(x => x)(Configure) + +const lang = new I18n(translations) diff --git a/client/coral-admin/src/containers/LayoutContainer.js b/client/coral-admin/src/containers/LayoutContainer.js new file mode 100644 index 000000000..7373c30c2 --- /dev/null +++ b/client/coral-admin/src/containers/LayoutContainer.js @@ -0,0 +1,19 @@ +import React, { Component }from 'react' +import { connect } from 'react-redux' + +import { Layout } from '../components/ui/Layout' + +class LayoutContainer extends Component { + render () { + return + } +} + +LayoutContainer.propTypes = {} + +const mapStateToProps = state => ({ data: {} }) + +const mapDispatchToProps = (dispatch, ownProps) => ({ dispatch }) + +export default connect(mapStateToProps, mapDispatchToProps)(LayoutContainer) + diff --git a/client/coral-admin/src/containers/ModerationQueue.js b/client/coral-admin/src/containers/ModerationQueue.js index 061786543..d051e5639 100644 --- a/client/coral-admin/src/containers/ModerationQueue.js +++ b/client/coral-admin/src/containers/ModerationQueue.js @@ -2,7 +2,6 @@ import React from 'react' import { connect } from 'react-redux' import ModerationKeysModal from 'components/ModerationKeysModal' -import Page from 'components/Page' import CommentList from 'components/CommentList' import { updateStatus } from 'actions/comments' import styles from './ModerationQueue.css' @@ -20,8 +19,6 @@ class ModerationQueue extends React.Component { constructor (props) { super(props) - console.log('ModerationQueue', props) - this.state = { activeTab: 'pending', singleView: false, modalOpen: false } } @@ -60,10 +57,9 @@ class ModerationQueue extends React.Component { render () { const { comments } = this.props const { activeTab, singleView, modalOpen } = this.state - console.log('moderation queue', styles) return ( - +
) } } diff --git a/client/coral-admin/webpack.config.dev.js b/client/coral-admin/webpack.config.dev.js deleted file mode 100644 index bd5913b5a..000000000 --- a/client/coral-admin/webpack.config.dev.js +++ /dev/null @@ -1,42 +0,0 @@ - -const path = require('path') -const fs = require('fs') -const autoprefixer = require('autoprefixer') -const precss = require('precss') -const config = require('./config.json') - -// doing a string replace here because I spent a day trying to do it the "webpack" way -// ond nothing works. just trying to replace a string in an index.html file is -// apparently something no one has ever done in the js community. -let templateString = fs.readFileSync(path.join(__dirname, 'index.ejs')).toString() -templateString = templateString.replace('<%= basePath %>', config.basePath) -fs.writeFileSync(path.join(__dirname, 'public/index.html'), templateString) - -module.exports = { - entry: { - 'bundle': path.join(__dirname, 'src', 'index') - }, - output: { - path: path.join(__dirname, '..', '..', 'dist', 'coral-admin'), - filename: '[name].js' - }, - module: { - loaders: [ - { test: /.js$/, loader: 'babel', include: path.join(__dirname, 'src'), exclude: /node_modules/ }, - { test: /\.json$/, loaders: 'json', include: __dirname, exclude: /node_modules/ }, - { test: /.css$/, loaders: ['style-loader', 'css-loader?importLoaders=1', 'postcss-loader'] } - ] - }, - plugins: [ autoprefixer, precss ], - resolve: { - root: [ - path.resolve('./src'), - path.resolve('../') - ] - }, - devServer: { - historyApiFallback: { - index: '/' - } - } -} diff --git a/client/coral-admin/webpack.config.js b/client/coral-admin/webpack.config.js deleted file mode 100644 index 2c81bf4fc..000000000 --- a/client/coral-admin/webpack.config.js +++ /dev/null @@ -1,32 +0,0 @@ -const path = require('path') -const fs = require('fs') -const devConfig = require('./webpack.config.dev') -const autoprefixer = require('autoprefixer') -const precss = require('precss') -const Copy = require('copy-webpack-plugin') -const config = require('./config.json') - -// doing a string replace here because I spent a day trying to do it the "webpack" way -// ond nothing works. just trying to replace a string in an index.html file is -// apparently something no one has ever done in the js community. -let templateString = fs.readFileSync(path.join(__dirname, 'index.ejs')).toString() -templateString = templateString.replace('<%= basePath %>', config.basePath) -fs.writeFileSync(path.join(__dirname, 'public/index.html'), templateString) - -module.exports = Object.assign({}, devConfig, { - module: { - context: __dirname, - loaders: [ - { test: /.js$/, loader: 'babel', include: [path.join(__dirname, 'src'), path.join(__dirname, '../', 'coral-framework')], exclude: /node_modules/ }, - { test: /.json$/, loader: 'json', include: __dirname, exclude: /node_modules/ }, - { test: /.css$/, loaders: ['style-loader', 'css-loader?importLoaders=1', 'postcss-loader'] } - ] - }, - plugins: [ - new Copy([{ - from: path.join(__dirname, '..', 'coral-embed-stream', 'dist'), - to: './embed/comment-stream' - }]), - autoprefixer, precss - ] -}) diff --git a/client/coral-embed-stream/.gitignore b/client/coral-embed-stream/.gitignore deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/coral-embed-stream/dev-server.js b/client/coral-embed-stream/dev-server.js deleted file mode 100644 index 926033fda..000000000 --- a/client/coral-embed-stream/dev-server.js +++ /dev/null @@ -1,41 +0,0 @@ -var path = require('path') -var express = require('express') -var http = require('http') -var webpack = require('webpack') -var config = require('./webpack.config.dev') -var Dashboard = require('webpack-dashboard') -var DashboardPlugin = require('webpack-dashboard/plugin') - -var app = express() -var server = http.Server(app) - -var compiler = webpack(config) -var dashboard = new Dashboard() -compiler.apply(new DashboardPlugin(dashboard.setData)) - -app.use(express.static('public')) - -app.use(require('webpack-dev-middleware')(compiler, { - noInfo: true, - quiet: true, - publicPath: config.output.publicPath -})) - -app.use(require('webpack-hot-middleware')(compiler, {log: () => {}})) - -app.get('/default.css', function (req, res) { - res.sendFile(path.join(__dirname, '/style/default.css')) -}) - -app.get('*', function (req, res) { - res.sendFile(path.join(__dirname, 'index.html')) -}) - -server.listen(6182, 'localhost', function (err) { - if (err) { - console.log(err) - return - } - - console.log('Listening at http://localhost:6182') -}) diff --git a/client/coral-embed-stream/index.html b/client/coral-embed-stream/index.html deleted file mode 100644 index 6e15cba9f..000000000 --- a/client/coral-embed-stream/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - -
- - - diff --git a/client/coral-embed-stream/src/app.js b/client/coral-embed-stream/src/index.js similarity index 100% rename from client/coral-embed-stream/src/app.js rename to client/coral-embed-stream/src/index.js diff --git a/client/coral-embed-stream/webpack.config.dev.js b/client/coral-embed-stream/webpack.config.dev.js deleted file mode 100644 index dd5534529..000000000 --- a/client/coral-embed-stream/webpack.config.dev.js +++ /dev/null @@ -1,70 +0,0 @@ -var path = require('path') -var webpack = require('webpack') -const Copy = require('copy-webpack-plugin') - -module.exports = { - devtool: 'eval', - entry: [ - 'babel-polyfill', - 'webpack-hot-middleware/client', - path.join(__dirname, 'src', 'app') - ], - output: { - path: path.join(__dirname, '..', '..','dist', 'coral-embed-stream'), - filename: 'bundle.js', - publicPath: '/' - }, - resolve: { - root: [ - path.resolve(__dirname, 'src'), - path.resolve(__dirname, '..') - ], - extensions: ['', '.js', '.jsx'] - }, - plugins: [ - new Copy([{ - from: path.join(__dirname, 'index.html') - }, - { - from: path.join(__dirname, 'style', 'default.css') - }, - { - from: path.join(__dirname, 'public'), - to: './' - }]), - new webpack.DefinePlugin({ - 'process.env': { - 'NODE_ENV': JSON.stringify('development') - } - }), - new webpack.HotModuleReplacementPlugin(), - new webpack.ProvidePlugin({ - 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' - }), - new webpack.NoErrorsPlugin() - ], - module: { - loaders: [{ - test: /\.(js|jsx)$/, - - loaders: ['babel'], - exclude: /node_modules/, - include: path.join(__dirname, '../') - }, { - test: /\.css$/, - loader: 'style-loader!css-loader' - }, { - test: /\.png$/, - loader: 'url-loader?limit=100000' - }, { - test: /\.(jpg|png|gif|svg)$/, - loader: 'file-loader' - }, { - test: /\.json$/, - loader: 'json-loader' - }, { - test: /\.woff$/, - loader: 'url?limit=100000' - }] - } -} diff --git a/client/coral-embed-stream/webpack.config.js b/client/coral-embed-stream/webpack.config.js deleted file mode 100644 index 2db94e6af..000000000 --- a/client/coral-embed-stream/webpack.config.js +++ /dev/null @@ -1,73 +0,0 @@ -const path = require('path') -const webpack = require('webpack') -const Copy = require('copy-webpack-plugin') - -//Keeping this file for reference, it should move to a global webpack. - -module.exports = { - devtool: 'source-map', - entry: [ - 'babel-polyfill', - path.join(__dirname, 'src', 'app') - ], - output: { - path: path.join(__dirname, '..', '..','dist', 'coral-embed-stream'), - filename: 'bundle.js', - publicPath: '/dist/' - }, - resolve: { - root: [ - path.resolve(__dirname, 'src') - ], - extensions: ['', '.js', '.jsx'] - }, - plugins: [ - new Copy([{ - from: path.join(__dirname, 'index.html') - }, - { - from: path.join(__dirname, 'style', 'default.css') - }, - { - from: path.join(__dirname, 'public'), - to: './' - }, - { - from: path.resolve(__dirname, '..', 'coral-framework', 'i18n', 'translations'), - to: './translations' - }]), - new webpack.optimize.OccurenceOrderPlugin(), - new webpack.DefinePlugin({ - 'process.env': { - 'NODE_ENV': JSON.stringify('production') - } - }), - new webpack.ProvidePlugin({ - 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' - }), - new webpack.ExtendedAPIPlugin() - ], - module: { - loaders: [{ - test: /\.(js|jsx)$/, - loaders: ['babel'], - exclude: /node_modules/, - include: path.join(__dirname, '../') - }, { - test: /\.css$/, - loader: 'style-loader!css-loader' - }, { - test: /\.png$/, - loader: 'url-loader?limit=100000' - }, { - test: /\.jpg$/, - loader: 'file-loader' - }, { - test: /\.json$/, - loader: 'json-loader' - }, { - test: /\.woff$/, - loader: 'url?limit=100000' - }] - } -} diff --git a/dist/coral-admin/manifest.json b/dist/coral-admin/manifest.json deleted file mode 100644 index 5da846121..000000000 --- a/dist/coral-admin/manifest.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "short_name": "Talk", - "name": "Talk", - "icons": [ - { - "src": "https://coralproject.net/images/icon-coral-white.svg", - "sizes": "150x150" - } - ], - "start_url": "./", - "display": "standalone" -} diff --git a/models/action.js b/models/action.js index cc310c5a8..4005aa3b2 100644 --- a/models/action.js +++ b/models/action.js @@ -68,6 +68,32 @@ ActionSchema.statics.getActionSummaries = function(item_ids) { }); }; +/* + * Finds all comments for a specific action. + * @param {String} action_type type of action + * @param {String} item_type type of item the action is on +*/ +ActionSchema.statics.findByType = function(action_type, item_type) { + return Action.find({ + 'action_type': action_type, + 'item_type': item_type + }); +}; + +/** + * Finds all comments ids for a specific action. + * @param {String} action_type type of action + * @param {String} item_type type of item the action is on +*/ +ActionSchema.statics.findCommentsIdByActionType = function(action_type, item_type) { + return Action.find({ + 'action_type': action_type, + 'item_type': item_type + }, + 'item_id' + ); +}; + const Action = mongoose.model('Action', ActionSchema); module.exports = Action; diff --git a/models/comment.js b/models/comment.js index 6ddc3b0ee..3aad5d978 100644 --- a/models/comment.js +++ b/models/comment.js @@ -31,6 +31,23 @@ const CommentSchema = new Schema({ } }); +//============================================================================== +// New Statics +//============================================================================== + +/** + * Create a comment. + * @param {String} body content of comment +*/ +CommentSchema.statics.new = function(body, author_id, asset_id, parent_id, status, username) { + let comment = new Comment({body, author_id, asset_id, parent_id, status, username}); + return comment.save(); +}; + +//============================================================================== +// Find Statics +//============================================================================== + /** * Finds a comment by the id. * @param {String} id identifier of comment (uuid) @@ -47,6 +64,28 @@ CommentSchema.statics.findByAssetId = function(asset_id) { return Comment.find({asset_id}); }; +/** + * Find comments by an action that was performed on them. + * @param {String} action_type the type of action that was performed on the comment +*/ +CommentSchema.statics.findByActionType = function(action_type) { + return Action.findCommentsIdByActionType(action_type, 'comment').then((actions) => { + return Comment.find({'id': {'$in': actions.map(function(a){return a.item_id;})}}); + }); +}; + +/** + * Find comments by their status. + * @param {String} status the status to search for +*/ +CommentSchema.statics.findByStatus = function(status) { + return Comment.find({'status': status}); +}; + +//============================================================================== +// Update Statics +//============================================================================== + /** * Change the status of a comment. * @param {String} id identifier of the comment (uuid) @@ -72,6 +111,19 @@ CommentSchema.statics.addAction = function(id, user_id, action_type) { return action.save(); }; +//============================================================================== +// Remove Statics +//============================================================================== + +/** + * Change the status of a comment. + * @param {String} id identifier of the comment (uuid) + * @param {String} status the new status of the comment +*/ +CommentSchema.statics.removeById = function(id) { + return Comment.remove({'id': id}); +}; + const Comment = mongoose.model('Comment', CommentSchema); module.exports = Comment; diff --git a/package.json b/package.json index 0c7aab592..979475b5b 100644 --- a/package.json +++ b/package.json @@ -5,12 +5,13 @@ "main": "app.js", "scripts": { "start": "./bin/www", - "build": "./node_modules/webpack/bin/webpack.js --config ./client/coral-embed-stream/webpack.config.js && cd client/coral-admin && npm run build", + "build": "webpack --config webpack.config.js --bail", + "build-watch": "webpack --config webpack.config.dev.js --watch", "lint": "eslint .", "pretest": "npm install", "test": "mocha tests --recursive", "test-watch": "mocha tests --recursive -w", - "embed-start": "./node_modules/webpack/bin/webpack.js --config ./client/coral-embed-stream/webpack.config.dev.js && ./bin/www" + "embed-start": "npm run build && ./bin/www" }, "config": { "pre-git": { @@ -44,48 +45,56 @@ "dependencies": { "body-parser": "^1.15.2", "debug": "^2.2.0", + "ejs": "^2.5.2", "express": "^4.14.0", "mongoose": "^4.6.5", - "uuid": "^2.0.3", - "morgan": "^1.7.0" + "morgan": "^1.7.0", + "uuid": "^2.0.3" }, "devDependencies": { - "babel-core": "6.14.0", - "babel-jest": "^15.0.0", - "babel-loader": "6.2.5", - "babel-plugin-transform-async-to-generator": "^6.8.0", + "autoprefixer": "^6.5.0", + "babel-core": "^6.18.2", + "babel-loader": "^6.2.7", + "babel-plugin-transform-async-to-generator": "^6.16.0", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-object-assign": "^6.8.0", "babel-plugin-transform-react-jsx": "^6.8.0", - "babel-polyfill": "^6.13.0", - "babel-preset-es2015": "6.13.0", + "babel-polyfill": "^6.16.0", + "babel-preset-es2015": "^6.18.0", "babel-preset-es2015-minimal": "^2.1.0", "babel-preset-stage-0": "^6.16.0", "chai": "^3.5.0", "chai-http": "^3.0.0", - "copy-webpack-plugin": "^3.0.1", + "copy-webpack-plugin": "^4.0.0", + "css-loader": "^0.25.0", "eslint": "^3.9.1", "exports-loader": "^0.6.3", + "hammerjs": "^2.0.8", "immutable": "^3.8.1", "imports-loader": "^0.6.5", "json-loader": "^0.5.4", + "keymaster": "^1.6.2", + "material-design-lite": "^1.2.1", "mocha": "^3.1.2", "mocha-junit-reporter": "^1.12.1", + "postcss-loader": "^1.1.0", + "postcss-modules": "^0.5.2", + "postcss-smart-import": "^0.5.1", "pre-git": "^3.10.0", + "precss": "^1.4.0", "pym.js": "^1.1.1", "react": "15.3.2", "react-dom": "15.3.2", + "react-mdl": "^1.7.2", "react-redux": "^4.4.5", + "react-router": "^3.0.0", "redux": "^3.6.0", "redux-thunk": "^2.1.0", "regenerator": "^0.8.46", + "style-loader": "^0.13.1", "supertest": "^2.0.1", "timeago.js": "^2.0.3", - "webpack": "^1.13.2", - "webpack-dashboard": "^0.2.0", - "webpack-dev-middleware": "^1.8.3", - "webpack-hot-middleware": "^2.12.2", - "webpack-module-hot-accept": "^1.0.4", + "webpack": "^1.13.3", "whatwg-fetch": "^1.0.0" }, "engines": { diff --git a/client/coral-admin/postcss.config.js b/postcss.config.js similarity index 98% rename from client/coral-admin/postcss.config.js rename to postcss.config.js index d5267160a..79fae36cb 100644 --- a/client/coral-admin/postcss.config.js +++ b/postcss.config.js @@ -4,4 +4,4 @@ module.exports = { require('precss')({ /* ...options */ }), require('autoprefixer')({ /* ...options */ }) ] -} +}; diff --git a/routes/admin/index.js b/routes/admin/index.js new file mode 100644 index 000000000..52957f54a --- /dev/null +++ b/routes/admin/index.js @@ -0,0 +1,13 @@ +const express = require('express'); + +const router = express.Router(); + +router.get('/embed/stream/preview', (req, res) => { + res.render('embed-stream', {basePath: '/client/embed/stream'}); +}); + +router.get('*', (req, res) => { + res.render('admin', {basePath: '/client/coral-admin'}); +}); + +module.exports = router; diff --git a/routes/api/comments/index.js b/routes/api/comments/index.js index bc35a78f9..b3e2bdee7 100644 --- a/routes/api/comments/index.js +++ b/routes/api/comments/index.js @@ -4,7 +4,7 @@ const Comment = require('../../../models/comment'); const router = express.Router(); //============================================================================== -// Routes +// Get Routes //============================================================================== router.get('/', (req, res, next) => { @@ -23,16 +23,54 @@ router.get('/:comment_id', (req, res, next) => { }); }); -router.post('/', (req, res, next) => { - const {body, author_id, asset_id, parent_id, status, username} = req.body; - let comment = new Comment({body, author_id, asset_id, parent_id, status, username}); - comment.save().then(({id}) => { - res.status(200).send({'id': id}); +//============================================================================== +// Moderation Queues Routes +//============================================================================== + +router.get('/action/:action_type', (req, res, next) => { + Comment.findByActionType(req.params.action_type).then((comments) => { + res.status(200).json(comments); }).catch(error => { next(error); }); }); +router.get('/status/rejected', (req, res, next) => { + Comment.findByStatus('rejected').then((comments) => { + res.status(200).json(comments); + }).catch(error => { + next(error); + }); +}); + +router.get('/status/pending', (req, res, next) => { + Comment.findByStatus('').then((comments) => { + res.status(200).json(comments); + }).catch(error => { + next(error); + }); +}); + +//============================================================================== +// Post Routes +//============================================================================== + +router.post('/', (req, res, next) => { + const {body, author_id, asset_id, parent_id, status, username} = req.body; + Comment.new(body, author_id, asset_id, parent_id, status, username).then((comment) => { + res.status(200).send({'id': comment.id}); + }).catch(error => { + next(error); + }); + + // let comment = new Comment({body, author_id, asset_id, parent_id, status, username}); + // comment.save().then(({id}) => { + // res.status(200).send({'id': id}); + // }).catch(error => { + // next(error); + // }); +}); + router.post('/:comment_id', (req, res, next) => { Comment.findById(req.params.comment_id).then((comment) => { comment.body = req.body.body; @@ -48,14 +86,6 @@ router.post('/:comment_id', (req, res, next) => { }); }); -router.delete('/:comment_id', (req, res, next) => { - Comment.remove(req.params.comment_id).then(() => { - res.status(201).send('OK. Deleted'); - }).catch(error => { - next(error); - }); -}); - router.post('/:comment_id/status', (req, res, next) => { Comment.changeStatus(req.params.comment_id, req.body.status).then((comment) => { res.status(200).send(comment); @@ -72,4 +102,16 @@ router.post('/:comment_id/actions', (req, res, next) => { }); }); +//============================================================================== +// Delete Routes +//============================================================================== + +router.delete('/:comment_id', (req, res, next) => { + Comment.removeById(req.params.comment_id).then(() => { + res.status(201).send('OK. Removed'); + }).catch(error => { + next(error); + }); +}); + module.exports = router; diff --git a/tests/routes/api/comments/index.js b/tests/routes/api/comments/index.js index b31fb522a..378860bcb 100644 --- a/tests/routes/api/comments/index.js +++ b/tests/routes/api/comments/index.js @@ -66,6 +66,86 @@ describe('Get /comments', () => { }); }); +describe('Get moderation queues rejected, pending, flags', () => { + const comments = [{ + id: 'abc', + body: 'comment 10', + asset_id: 'asset', + author_id: '123', + status: 'rejected' + }, { + id: 'def', + body: 'comment 20', + asset_id: 'asset', + author_id: '456' + }, { + id: 'hij', + body: 'comment 30', + asset_id: '456', + status: 'accepted' + }]; + + const users = [{ + id: '123', + display_name: 'Ana', + }, { + id: '456', + display_name: 'Maria', + }]; + + const actions = [{ + action_type: 'flag', + item_id: 'abc', + item_type: 'comment' + }, { + action_type: 'like', + item_id: 'hij', + item_type: 'comment' + }]; + + beforeEach(() => { + return Comment.create(comments).then(() => { + return User.create(users); + }).then(() => { + return Action.create(actions); + }); + }); + + it('should return all the rejected comments', function(done){ + chai.request(app) + .get('/api/v1/comments/status/rejected') + .end(function(err, res){ + expect(err).to.be.null; + expect(res).to.have.status(200); + expect(res.body[0]).to.have.property('id', 'abc'); + done(); + }); + }); + + it('should return all the pending comments', function(done){ + chai.request(app) + .get('/api/v1/comments/status/pending') + .end(function(err, res){ + expect(err).to.be.null; + expect(res).to.have.status(200); + expect(res.body[0]).to.have.property('id', 'def'); + done(); + }); + }); + + it('should return all the flagged comments', function(done){ + chai.request(app) + .get('/api/v1/comments/action/flag') + .end(function(err, res){ + expect(res).to.have.status(200); + expect(err).to.be.null; + expect(res.body.length).to.equal(1); + expect(res.body[0]).to.have.property('id', 'abc'); + done(); + }); + }); +}); + describe('Post /comments', () => { const users = [{ id: '123', @@ -128,10 +208,12 @@ describe('Get /:comment_id', () => { const actions = [{ action_type: 'flag', - item_id: 'abc' + item_id: 'abc', + item_type: 'comment' }, { action_type: 'like', - item_id: 'hij' + item_id: 'hij', + item_type: 'comment' }]; beforeEach(() => { @@ -144,14 +226,12 @@ describe('Get /:comment_id', () => { it('should return the right comment for the comment_id', function(done){ chai.request(app) - .get('/api/v1/comments') - .query({'comment_id': 'abc'}) + .get('/api/v1/comments/abc') .end(function(err, res){ - const sorted = res.body.sort((a, b) => a.body - b.body); expect(err).to.be.null; expect(res).to.have.status(200); - expect(sorted[0]).to.have.property('body') - .and.to.equal('comment 10'); + expect(res).to.have.property('body'); + expect(res.body).to.have.property('body', 'comment 10'); done(); }); }); @@ -206,14 +286,13 @@ describe('Put /:comment_id', () => { .end(function(err, res){ expect(err).to.be.null; expect(res).to.have.status(200); - expect(res.body).to.have.property('body'); - expect(res.body.body).to.equal('Something body.'); + expect(res.body).to.have.property('body', 'Something body.'); done(); }); }); }); -describe('Delete /:comment_id', () => { +describe('Remove /:comment_id', () => { const comments = [{ id: 'abc', @@ -259,9 +338,10 @@ describe('Delete /:comment_id', () => { chai.request(app) .delete('/api/v1/comments/abc') .end(function(err, res){ + expect(err).to.be.null; expect(res).to.have.status(201); - Comment.findById({'id': 'abc'}).then((comment) => { - expect(comment).to.be.null; + Comment.findById('abc').then((comment) => { + expect(comment).to.be.empty; }); done(); }); @@ -321,8 +401,7 @@ describe('Post /:comment_id/status', () => { expect(err).to.be.null; expect(res).to.have.status(200); expect(res).to.have.body; - expect(res.body).to.have.property('status'); - expect(res.body.status).to.equal('accepted'); + expect(res.body).to.have.property('status', 'accepted'); done(); }); }); @@ -381,14 +460,10 @@ describe('Post /:comment_id/actions', () => { expect(err).to.be.null; expect(res).to.have.status(200); expect(res).to.have.body; - expect(res.body).to.have.property('item_type'); - expect(res.body.item_type).to.equal('comment'); - expect(res.body).to.have.property('action_type'); - expect(res.body.action_type).to.equal('flag'); - expect(res.body).to.have.property('item_id'); - expect(res.body.item_id).to.equal('abc'); - expect(res.body).to.have.property('user_id'); - expect(res.body.user_id).to.equal('456'); + expect(res.body).to.have.property('item_type', 'comment'); + expect(res.body).to.have.property('action_type', 'flag'); + expect(res.body).to.have.property('item_id', 'abc'); + expect(res.body).to.have.property('user_id', '456'); done(); }); }); diff --git a/tests/routes/api/settings/index.js b/tests/routes/api/settings/index.js index 8c2cd221e..5bcf9de28 100644 --- a/tests/routes/api/settings/index.js +++ b/tests/routes/api/settings/index.js @@ -24,8 +24,7 @@ describe('GET /settings', () => { expect(err).to.be.null; expect(res).to.have.status(200); expect(res).to.be.json; - expect(res.body).to.have.property('moderation'); - expect(res.body.moderation).to.equal('pre'); + expect(res.body).to.have.property('moderation', 'pre'); done(err); }); }); @@ -33,30 +32,24 @@ describe('GET /settings', () => { // update the settings. describe('update settings', () => { + it('should respond ok to a PUT', () => { + return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true}) + .then(() => { + return chai.request(app) + .put('/api/v1/settings') + .send({moderation: 'post'}) + .then(res => { + expect(res).to.have.status(204); - before(() => { - return Setting.update({id: '1'}, {$setOnInsert: defaults}, {upsert: true}); - }); + return Setting.getSettings(); - it('should respond to a PUT with new settings', () => { - chai.request(app) - .put('/api/v1/settings') - .send({moderation: 'post'}, (err, res) => { - expect(err).to.be.null; - expect(res).to.have.status(204); - done(err); - }); - }); - - it('should have updates settings', () => { - chai.request(app) - .get('/api/v1/settings') - .end((err, res) => { - expect(err).to.be.null; - expect(res).to.have.status(200); - expect(res).to.be.json; - expect(res.body).to.have.property('moderation'); - expect(res.body.moderation).to.equal('post'); + }).then(settings => { + // confirm updated settings in db + expect(settings).to.have.property('moderation'); + expect(settings.moderation).to.equal('post'); + }).catch(err => { + throw err; + }); }); }); }); diff --git a/tests/routes/api/stream/index.js b/tests/routes/api/stream/index.js index 9744abb0c..b1348e88a 100644 --- a/tests/routes/api/stream/index.js +++ b/tests/routes/api/stream/index.js @@ -60,7 +60,6 @@ describe('api/stream: routes', () => { .end(function(err, res){ expect(err).to.be.null; expect(res).to.have.status(200); - if (err) {return done(err);} done(); }); }); diff --git a/client/coral-admin/index.ejs b/views/admin.ejs similarity index 92% rename from client/coral-admin/index.ejs rename to views/admin.ejs index 9b5fe84bf..038fb58d7 100644 --- a/client/coral-admin/index.ejs +++ b/views/admin.ejs @@ -10,8 +10,8 @@ body, #root { width: 100%; height: 100%; - margin: 0; - background: #fff; + margin: 0; + background: #fff; } diff --git a/client/coral-admin/public/index.html b/views/admin.ejs~HEAD similarity index 82% rename from client/coral-admin/public/index.html rename to views/admin.ejs~HEAD index ef1adffec..038fb58d7 100644 --- a/client/coral-admin/public/index.html +++ b/views/admin.ejs~HEAD @@ -10,13 +10,13 @@ body, #root { width: 100%; height: 100%; - margin: 0; - background: #fff; + margin: 0; + background: #fff; }
- + diff --git a/dist/coral-admin/index.html b/views/embed-stream.ejs similarity index 76% rename from dist/coral-admin/index.html rename to views/embed-stream.ejs index 3be58a0c8..47bda6bee 100644 --- a/dist/coral-admin/index.html +++ b/views/embed-stream.ejs @@ -4,9 +4,9 @@ Talk - Coral Admin - + -
- +
+ diff --git a/views/error.ejs b/views/error.ejs new file mode 100644 index 000000000..18d70b831 --- /dev/null +++ b/views/error.ejs @@ -0,0 +1,20 @@ + + + + + + + Error + + + + +
+ +
<%= message %>
+ +
<%= error.stack %>
+ +
+ + diff --git a/webpack.config.dev.js b/webpack.config.dev.js new file mode 100644 index 000000000..e2c92b396 --- /dev/null +++ b/webpack.config.dev.js @@ -0,0 +1,95 @@ +const path = require('path'); +const autoprefixer = require('autoprefixer'); +const precss = require('precss'); +const Copy = require('copy-webpack-plugin'); +const webpack = require('webpack'); + +// Edit the build targets and embeds below. + +const buildTargets = [ + 'coral-admin' +]; + +const buildEmbeds = [ + 'stream' +]; + +module.exports = { + devtool: 'inline-source-map', + entry: buildTargets.reduce((entry, target) => { + + // Add the entry for the bundle. + entry[`${target}/bundle`] = [ + 'babel-polyfill', + path.join(__dirname, 'client/', target, '/src/index') + ]; + + return entry; + }, buildEmbeds.reduce((entry, embed) => { + + // Add the entry for the bundle. + entry[`embed/${embed}/bundle`] = [ + 'babel-polyfill', + path.join(__dirname, 'client/', `coral-embed-${embed}`, '/src/index') + ]; + + return entry; + }, {})), + output: { + path: path.join(__dirname, 'dist'), + filename: '[name].js' + }, + module: { + loaders: [ + { + loader: 'babel', + exclude: /node_modules/, + test: /\.js$/ + }, + { + loader: 'json', + test: /\.json$/, + exclude: /node_modules/ + }, + { + loaders: [ + 'style-loader', + 'css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', + 'postcss-loader' + ], + test: /.css$/, + }, + { + loader: 'url-loader?limit=100000', + test: /\.png$/ + }, + { + loader: 'file-loader', + test: /\.(jpg|png|gif|svg)$/ + }, + { + loader: 'url?limit=100000', + test: /\.woff$/ + } + ] + }, + plugins: [ + new Copy(buildEmbeds.map(embed => ({ + from: path.join(__dirname, 'client', `coral-embed-${embed}`, 'style'), + to: path.join(__dirname, 'dist', 'embed', embed) + }))), + autoprefixer, + precss, + new webpack.ProvidePlugin({ + 'fetch': 'imports?this=>global!exports?global.fetch!whatwg-fetch' + }) + ], + resolve: { + root: [ + path.join(__dirname, 'client'), + ...buildTargets.map(target => path.join(__dirname, 'client', target, 'src')), + ...buildEmbeds.map(embed => path.join(__dirname, 'client', `coral-embed-${embed}`, 'src')) + ] + }, + postcss: require('./postcss.config.js') +}; diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 000000000..434da160d --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,23 @@ +const webpack = require('webpack'); +const devConfig = require('./webpack.config.dev'); + +// Disable source maps. +devConfig.devtool = null; + +devConfig.plugins = devConfig.plugins.concat([ + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': `"${'production'}"`, + 'VERSION': `"${require('./package.json')}"` + } + }), + new webpack.optimize.UglifyJsPlugin({ + compress: { + warnings: false + } + }), + new webpack.optimize.OccurrenceOrderPlugin(), + new webpack.optimize.DedupePlugin() +]); + +module.exports = devConfig;