Merge pull request #79 from coralproject/user-integration

User integration
This commit is contained in:
Gabriela Rodríguez Berón
2016-11-22 09:30:27 -08:00
committed by GitHub
16 changed files with 154 additions and 74 deletions
+2 -2
View File
@@ -97,7 +97,7 @@ app.use('/api', (err, req, res, next) => {
res.status(err.status || 500);
res.json({
message: err.message,
error: app.get('env') === 'development' ? err : null
error: app.get('env') === 'development' ? err : {}
});
});
@@ -109,7 +109,7 @@ app.use('/', (err, req, res, next) => {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: app.get('env') === 'development' ? err : null
error: app.get('env') === 'development' ? err : {}
});
});
+15 -13
View File
@@ -61,8 +61,8 @@ class CommentStream extends Component {
// Set up messaging between embedded Iframe an parent component
// Using recommended Pym init code which violates .eslint standards
const pym = new Pym.Child({polling: 100});
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl)[1];
this.props.getStream(path);
const path = /https?\:\/\/([^?]+)/.exec(pym.parentUrl);
this.props.getStream(path && path[1] || window.location);
}
render () {
@@ -84,8 +84,9 @@ class CommentStream extends Component {
const rootItemId = this.props.items.assets && Object.keys(this.props.items.assets)[0];
const rootItem = this.props.items.assets && this.props.items.assets[rootItemId];
const {loggedIn, user} = this.props.auth;
return <div>
const {actions, users, comments} = this.props.items;
const {loggedIn, user, showSignInDialog} = this.props.auth;
return <div className={showSignInDialog ? 'expandForSignin' : ''}>
{
rootItem
? <div>
@@ -105,16 +106,16 @@ class CommentStream extends Component {
id={rootItemId}
premod={this.props.config.moderation}
reply={false}
canPost={loggedIn}
author={user}
/>
{!loggedIn && <SignInContainer />}
</div>
{
rootItem.comments && rootItem.comments.map((commentId) => {
const comment = this.props.items.comments[commentId];
const comment = comments[commentId];
return <div className="comment" key={commentId}>
<hr aria-hidden={true}/>
<AuthorName name={comment.username}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={comment.created_at}/>
<Content body={comment.body}/>
<div className="commentActionsLeft">
@@ -124,7 +125,7 @@ class CommentStream extends Component {
<LikeButton
addNotification={this.props.addNotification}
id={commentId}
like={this.props.items.actions[comment.like]}
like={actions[comment.like]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
@@ -135,7 +136,7 @@ class CommentStream extends Component {
<FlagButton
addNotification={this.props.addNotification}
id={commentId}
flag={this.props.items.actions[comment.flag]}
flag={actions[comment.flag]}
postAction={this.props.postAction}
deleteAction={this.props.deleteAction}
addItem={this.props.addItem}
@@ -151,6 +152,7 @@ class CommentStream extends Component {
appendItemArray={this.props.appendItemArray}
updateItem={this.props.updateItem}
id={rootItemId}
author={user}
parent_id={commentId}
premod={this.props.config.moderation}
showReply={comment.showReply}/>
@@ -160,13 +162,13 @@ class CommentStream extends Component {
let reply = this.props.items.comments[replyId];
return <div className="reply" key={replyId}>
<hr aria-hidden={true}/>
<AuthorName name={reply.username}/>
<AuthorName author={users[comment.author_id]}/>
<PubDate created_at={reply.created_at}/>
<Content body={reply.body}/>
<div className="replyActionsLeft">
<ReplyButton
updateItem={this.props.updateItem}
id={replyId}/>
parent_id={reply.parent_id}/>
<LikeButton
addNotification={this.props.addNotification}
id={replyId}
@@ -188,8 +190,8 @@ class CommentStream extends Component {
updateItem={this.props.updateItem}
currentUser={this.props.auth.user}/>
<PermalinkButton
comment_id={reply.comment_id}
asset_id={reply.comment_id}
comment_id={reply.parent_id}
asset_id={rootItemId}
/>
</div>
</div>;
+6 -2
View File
@@ -3,8 +3,12 @@ body {
font-family: 'Open Sans', sans-serif;
width: 100%;
font-size: 12px;
margin: 0;
min-height: 700px;
margin: 0px;
padding: 0px 0px 50px 0px;
}
.expandForSignin {
min-height: 550px;
}
button {
+5 -1
View File
@@ -3,6 +3,7 @@ import translations from './../translations';
const lang = new I18n(translations);
import * as actions from '../constants/auth';
import {base, handleResp, getInit} from '../helpers/response';
import {addItem} from './items';
// Dialog Actions
export const showSignInDialog = () => ({type: actions.SHOW_SIGNIN_DIALOG});
@@ -29,6 +30,7 @@ export const fetchSignIn = (formData) => dispatch => {
.then(({user}) => {
dispatch(hideSignInDialog());
dispatch(signInSuccess(user));
dispatch(addItem(user, 'users'));
})
.catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError'))));
};
@@ -54,8 +56,10 @@ export const facebookCallback = (err, data) => dispatch => {
return;
}
try {
dispatch(signInFacebookSuccess(JSON.parse(data)));
const user = JSON.parse(data);
dispatch(signInFacebookSuccess(user));
dispatch(hideSignInDialog());
dispatch(addItem(user, 'users'));
} catch (err) {
dispatch(signInFacebookFailure(err));
return;
+10 -2
View File
@@ -106,8 +106,16 @@ export function getStream (assetUrl) {
/* Add items to the store */
const itemTypes = Object.keys(json);
for (let i = 0; i < itemTypes.length; i++ ) {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
dispatch(addItem(json[itemTypes[i]][j], itemTypes[i]));
if (itemTypes[i] === 'actions') {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
let action = json[itemTypes[i]][j];
action.id = `${action.action_type}_${action.item_id}`;
dispatch(addItem(action, 'actions'));
}
} else {
for (let j = 0; j < json[itemTypes[i]].length; j++ ) {
dispatch(addItem(json[itemTypes[i]][j], itemTypes[i]));
}
}
}
@@ -1,9 +1,9 @@
import React from 'react';
const packagename = 'coral-plugin-author-name';
const AuthorName = ({name}) =>
const AuthorName = ({author}) =>
<div className={`${packagename}-text`}>
{name}
{author && author.displayName}
</div>;
export default AuthorName;
+6 -5
View File
@@ -12,7 +12,8 @@ class CommentBox extends Component {
id: PropTypes.string,
comments: PropTypes.array,
reply: PropTypes.bool,
canPost: PropTypes.bool
canPost: PropTypes.bool,
currentUser: PropTypes.object
}
state = {
@@ -21,11 +22,11 @@ class CommentBox extends Component {
}
postComment = () => {
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod} = this.props;
const {postItem, updateItem, id, parent_id, addNotification, appendItemArray, premod, author} = this.props;
let comment = {
body: this.state.body,
asset_id: id,
username: this.state.username
author_id: author.id
};
let related;
let parent_type;
@@ -52,7 +53,7 @@ class CommentBox extends Component {
}
render () {
const {styles, reply, canPost} = this.props;
const {styles, reply, author} = this.props;
// How to handle language in plugins? Should we have a dependency on our central translation file?
return <div>
<div
@@ -73,7 +74,7 @@ class CommentBox extends Component {
rows={3}/>
</div>
<div className={`${name}-button-container`}>
{ canPost && (
{ author && (
<button
className={`${name}-button`}
style={styles && styles.button}
+6 -3
View File
@@ -4,11 +4,14 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification}) => {
const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, addNotification, currentUser}) => {
const flagged = flag && flag.current_user;
const onFlagClick = () => {
if (!currentUser) {
return;
}
if (!flagged) {
postAction(id, 'flag', '123', 'comments')
postAction(id, 'flag', currentUser.id, 'comments')
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: flag ? flag.count + 1 : 1}, 'actions');
@@ -32,7 +35,7 @@ const FlagButton = ({flag, id, postAction, deleteAction, addItem, updateItem, ad
: <span className={`${name}-button-text`}>{lang.t('flag')}</span>
}
<i className={`${name}-icon material-icons ${flagged && 'flaggedIcon'}`}
style={flagged && styles.flaggedIcon}
style={flagged ? styles.flaggedIcon : {}}
aria-hidden={true}>flag</i>
</button>
</div>;
+5 -2
View File
@@ -4,11 +4,14 @@ import translations from './translations.json';
const name = 'coral-plugin-flags';
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem}) => {
const LikeButton = ({like, id, postAction, deleteAction, addItem, updateItem, currentUser}) => {
const liked = like && like.current_user;
const onLikeClick = () => {
if (!currentUser) {
return;
}
if (!liked) {
postAction(id, 'like', '123', 'comments')
postAction(id, 'like', currentUser.id, 'comments')
.then((action) => {
let id = `${action.action_type}_${action.item_id}`;
addItem({id, current_user: action, count: like ? like.count + 1 : 1}, 'actions');
+1 -2
View File
@@ -106,7 +106,7 @@ input.error{
.userBox a {
color: #2c69b6;
cursor: pointer;
margin: 0 5px;
margin: 0px;
}
.attention {
@@ -140,4 +140,3 @@ input.error{
border: 1px solid orange;
background-color: 1px solid coral
}
+51 -24
View File
@@ -42,31 +42,58 @@ ActionSchema.statics.findByItemIdArray = function(item_ids) {
* @param {String} ids array of user identifiers (uuid)
*/
ActionSchema.statics.getActionSummaries = function(item_ids) {
return ActionSchema.statics.findByItemIdArray(item_ids).then((rawActions) => {
// Create an object with a count of each action type for each item
const actionSummaries = rawActions.reduce((actionObj, action) => {
if (!actionObj[action.item_id]) {
actionObj[action.item_id] = {
id: action.id,
item_type: action.item_type,
action_type: action.action_type,
count: 1,
current_user: false //Update this later when we have authentication
};
} else {
actionObj[action.item_id].count ++;
}
return actionObj;
}, {});
return Action.aggregate([
{
// Return an array extracted from the actionSummaries object
return Object.keys(actionSummaries).reduce((actions, key) => {
let actionSummary = actionSummaries[key];
actionSummary.item_id = key;
actions.push(actionSummary);
return actions;
}, []);
});
// only grab items that match the specified item id's
$match: {
item_id: {
$in: item_ids
}
}
},
{
$group: {
// group unique documents by these properties, we are leveraging the
// fact that each uuid is completly unique.
_id: {
item_id: '$item_id',
action_type: '$action_type'
},
// and sum up all actions matching the above grouping criteria
count: {
$sum: 1
},
// we are leveraging the fact that each uuid is completly unique and
// just grabbing the last instance of the item type here.
item_type: {
$last: '$item_type'
}
}
},
{
$project: {
// suppress the _id field
_id: false,
// map the fields from the _id grouping down a level
item_id: '$_id.item_id',
action_type: '$_id.action_type',
// map the field directly
count: '$count',
item_type: '$item_type',
// set the current user to false here
current_user: {$literal: false}
}
}
])
.exec();
};
/*
+11 -1
View File
@@ -402,7 +402,7 @@ UserService.findById = (id) => {
};
/**
* Finds users in an array of idd.
* Finds users in an array of ids.
* @param {Array} ids array of user identifiers (uuid)
*/
UserService.findByIdArray = (ids) => {
@@ -411,6 +411,16 @@ UserService.findByIdArray = (ids) => {
});
};
/**
* Finds public user information by an array of ids.
* @param {Array} ids array of user identifiers (uuid)
*/
UserService.findPublicByIdArray = (ids) => {
return UserModel.find({
'id': {$in: ids}
}, 'id displayName');
};
/**
* Creates a JWT from a user email. Only works for local accounts.
* @param {String} email of the local user
+2 -2
View File
@@ -342,9 +342,9 @@ definitions:
type: string
format: date-time
description: Display name of comment
user_id:
author_id:
type: string
description: Display name of comment
description: User who posted the comment
parent_id:
type: string
description: Display name of comment
@@ -29,21 +29,23 @@ describe('itemActions', () => {
],
actions: [
{
type: 'like',
id: '123',
action_type: 'like',
item_id: '123',
count: 1,
id: 'like_123',
current_user: false
},
{
type: 'flag',
id: '456',
action_type: 'flag',
item_id: '456',
count: 5,
id: 'flag_456',
current_user: true
}
]
};
it('should get an stream from an asset_id and send the appropriate dispatches', () => {
it('should get an stream from an asset_url and send the appropriate dispatches', () => {
fetchMock.get('*', JSON.stringify(response));
return actions.getStream(assetUrl)(store.dispatch)
.then((res) => {
+9 -8
View File
@@ -10,10 +10,6 @@ describe('Action: models', () => {
action_type: 'flag',
item_id: '123',
item_type: 'comments'
}, {
action_type: 'like',
item_id: '789',
item_type: 'comments'
}, {
action_type: 'flag',
item_id: '456',
@@ -22,6 +18,10 @@ describe('Action: models', () => {
action_type: 'flag',
item_id: '123',
item_type: 'comments'
}, {
action_type: 'like',
item_id: '123',
item_type: 'comments'
}]).then((actions) => {
mockActions = actions;
});
@@ -39,7 +39,7 @@ describe('Action: models', () => {
describe('#findByItemIdArray()', () => {
it('should find an array of actions from an array of item_ids', () => {
return Action.findByItemIdArray(['123', '456']).then((result) => {
expect(result).to.have.length(3);
expect(result).to.have.length(4);
});
});
});
@@ -48,16 +48,17 @@ describe('Action: models', () => {
it('should return properly formatted summaries from an array of item_ids', () => {
return Action.getActionSummaries(['123', '789']).then((result) => {
expect(result).to.have.length(2);
const sorted = result.sort((a, b) => a.count - b.count);
delete sorted[0].id;
delete sorted[1].id;
expect(sorted[0]).to.deep.equal({
action_type: 'like',
count: 1,
item_id: '789',
item_id: '123',
item_type: 'comments',
current_user: false
});
expect(sorted[1]).to.deep.equal({
action_type: 'flag',
count: 2,
+16
View File
@@ -43,6 +43,22 @@ describe('User: models', () => {
});
});
describe('#findPublicByIdArray()', () => {
it('should find an array of users from an array of ids', () => {
const ids = mockUsers.map((user) => user.id);
return User.findPublicByIdArray(ids).then((result) => {
expect(result).to.have.length(3);
const sorted = result.sort((a, b) => {
if(a.displayName < b.displayName) {return -1;}
if(a.displayName > b.displayName) {return 1;}
return 0;
});
expect(sorted[0]).to.have.property('displayName')
.and.to.equal('Marvel');
});
});
});
describe('#findLocalUser', () => {
it('should find a user when we give the right credentials', () => {