mirror of
https://github.com/vale981/Vulcan
synced 2025-03-06 01:51:40 -05:00
Big voting refactor
This commit is contained in:
parent
7bfa4afba3
commit
071a0fd720
14 changed files with 320 additions and 206 deletions
|
@ -1,6 +1,6 @@
|
||||||
accounts-base@1.3.1
|
accounts-base@1.3.1
|
||||||
accounts-password@1.4.0
|
accounts-password@1.4.0
|
||||||
allow-deny@1.0.6
|
allow-deny@1.0.9
|
||||||
autoupdate@1.3.12
|
autoupdate@1.3.12
|
||||||
babel-compiler@6.19.4
|
babel-compiler@6.19.4
|
||||||
babel-runtime@1.0.1
|
babel-runtime@1.0.1
|
||||||
|
|
|
@ -47,8 +47,8 @@ class Vote extends PureComponent {
|
||||||
this.props.flash(this.context.intl.formatMessage({id: 'users.please_log_in'}));
|
this.props.flash(this.context.intl.formatMessage({id: 'users.please_log_in'}));
|
||||||
// this.stopLoading();
|
// this.stopLoading();
|
||||||
} else {
|
} else {
|
||||||
const voteType = hasUpvoted(user, document) ? 'cancelUpvote' : 'upvote';
|
const operationType = this.props.document.currentUserVotes.length ? 'cancelVote' : 'upvote';
|
||||||
this.props.vote({document, voteType, collection, currentUser: this.props.currentUser}).then(result => {
|
this.props.vote({document, operationType, collection, currentUser: this.props.currentUser}).then(result => {
|
||||||
// this.stopLoading();
|
// this.stopLoading();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,9 @@ class Vote extends PureComponent {
|
||||||
{this.state.loading ? <Components.Icon name="spinner" /> : <Components.Icon name="upvote" /> }
|
{this.state.loading ? <Components.Icon name="spinner" /> : <Components.Icon name="upvote" /> }
|
||||||
<div className="sr-only">Upvote</div>
|
<div className="sr-only">Upvote</div>
|
||||||
<div className="vote-count">{this.props.document.baseScore || 0}</div>
|
<div className="vote-count">{this.props.document.baseScore || 0}</div>
|
||||||
|
<div>{this.props.document.currentUserVotes ? this.props.document.currentUserVotes.map(vote =>
|
||||||
|
<p key={vote._id}>{vote.votedAt.toString()}, {vote.power}</p>
|
||||||
|
) : null}</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,19 +6,20 @@ Categories parameter
|
||||||
|
|
||||||
import { addCallback, getSetting, registerSetting, getFragment, runQuery } from 'meteor/vulcan:core';
|
import { addCallback, getSetting, registerSetting, getFragment, runQuery } from 'meteor/vulcan:core';
|
||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
|
import Categories from './collection.js';
|
||||||
|
|
||||||
registerSetting('forum.categoriesFilter', 'union', 'Display posts belonging to all (“intersection”) or at least one of (“union”) the selected categories');
|
registerSetting('forum.categoriesFilter', 'union', 'Display posts belonging to all (“intersection”) or at least one of (“union”) the selected categories');
|
||||||
|
|
||||||
// Category Posts Parameters
|
// Category Posts Parameters
|
||||||
// Add a 'categories' property to terms which can be used to filter *all* existing Posts views.
|
// Add a 'categories' property to terms which can be used to filter *all* existing Posts views.
|
||||||
async function PostsCategoryParameter(parameters, terms, apolloClient) {
|
function PostsCategoryParameter(parameters, terms, apolloClient) {
|
||||||
|
|
||||||
// get category slugs
|
// get category slugs
|
||||||
const cat = terms.cat || terms['cat[]'];
|
const cat = terms.cat || terms['cat[]'];
|
||||||
const categoriesSlugs = Array.isArray(cat) ? cat : [cat];
|
const categoriesSlugs = Array.isArray(cat) ? cat : [cat];
|
||||||
let allCategories = [];
|
let allCategories = [];
|
||||||
|
|
||||||
if (cat.length) {
|
if (cat && cat.length) {
|
||||||
|
|
||||||
// get all categories
|
// get all categories
|
||||||
// note: specify all arguments, see https://github.com/apollographql/apollo-client/issues/2051
|
// note: specify all arguments, see https://github.com/apollographql/apollo-client/issues/2051
|
||||||
|
@ -38,9 +39,11 @@ async function PostsCategoryParameter(parameters, terms, apolloClient) {
|
||||||
variables: {terms: {limit: 0, itemsPerPage: 0}}
|
variables: {terms: {limit: 0, itemsPerPage: 0}}
|
||||||
}).CategoriesList;
|
}).CategoriesList;
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: figure out how to make this async without messing up withList on the client
|
||||||
// get categories through GraphQL API using runQuery
|
// get categories through GraphQL API using runQuery
|
||||||
const results = await runQuery(query);
|
// const results = await runQuery(query);
|
||||||
allCategories = results.data.CategoriesList;
|
// allCategories = results.data.CategoriesList;
|
||||||
|
allCategories = Categories.find().fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
// get corresponding category ids
|
// get corresponding category ids
|
||||||
|
|
|
@ -26,15 +26,5 @@ registerFragment(`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# vulcan:voting
|
# vulcan:voting
|
||||||
upvoters {
|
|
||||||
_id
|
|
||||||
}
|
|
||||||
downvoters {
|
|
||||||
_id
|
|
||||||
}
|
|
||||||
#upvotes
|
|
||||||
#downvotes
|
|
||||||
#baseScore
|
|
||||||
#score
|
|
||||||
}
|
}
|
||||||
`);
|
`);
|
||||||
|
|
|
@ -31,14 +31,9 @@ registerFragment(`
|
||||||
...UsersMinimumInfo
|
...UsersMinimumInfo
|
||||||
}
|
}
|
||||||
# voting
|
# voting
|
||||||
upvoters {
|
currentUserVotes{
|
||||||
_id
|
...VoteFragment
|
||||||
}
|
}
|
||||||
downvoters {
|
|
||||||
_id
|
|
||||||
}
|
|
||||||
upvotes
|
|
||||||
downvotes
|
|
||||||
baseScore
|
baseScore
|
||||||
score
|
score
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,6 +25,7 @@ Package.onUse(function (api) {
|
||||||
'check',
|
'check',
|
||||||
'http',
|
'http',
|
||||||
'email',
|
'email',
|
||||||
|
'random',
|
||||||
'ecmascript@0.8.2',
|
'ecmascript@0.8.2',
|
||||||
'service-configuration',
|
'service-configuration',
|
||||||
'shell-server@0.2.4',
|
'shell-server@0.2.4',
|
||||||
|
|
|
@ -1,25 +1,22 @@
|
||||||
import React, { PropTypes, Component } from 'react';
|
import React, { PropTypes, Component } from 'react';
|
||||||
import { graphql } from 'react-apollo';
|
import { graphql } from 'react-apollo';
|
||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
import { operateOnItem } from '../modules/vote.js';
|
import { voteOnItem } from '../modules/vote.js';
|
||||||
import { VoteableCollections } from '../modules/make_voteable.js';
|
import { VoteableCollections } from '../modules/make_voteable.js';
|
||||||
|
|
||||||
const withVote = component => {
|
const withVote = component => {
|
||||||
|
|
||||||
return graphql(gql`
|
return graphql(gql`
|
||||||
mutation vote($documentId: String, $voteType: String, $collectionName: String) {
|
mutation vote($documentId: String, $operationType: String, $collectionName: String) {
|
||||||
vote(documentId: $documentId, voteType: $voteType, collectionName: $collectionName) {
|
vote(documentId: $documentId, operationType: $operationType, collectionName: $collectionName) {
|
||||||
${VoteableCollections.map(collection => `
|
${VoteableCollections.map(collection => `
|
||||||
... on ${collection.typeName} {
|
... on ${collection.typeName} {
|
||||||
__typename
|
__typename
|
||||||
_id
|
_id
|
||||||
upvotes
|
currentUserVotes{
|
||||||
upvoters {
|
|
||||||
_id
|
|
||||||
}
|
|
||||||
downvotes
|
|
||||||
downvoters {
|
|
||||||
_id
|
_id
|
||||||
|
voteType
|
||||||
|
power
|
||||||
}
|
}
|
||||||
baseScore
|
baseScore
|
||||||
}
|
}
|
||||||
|
@ -28,19 +25,19 @@ const withVote = component => {
|
||||||
}
|
}
|
||||||
`, {
|
`, {
|
||||||
props: ({ownProps, mutate}) => ({
|
props: ({ownProps, mutate}) => ({
|
||||||
vote: ({document, voteType, collection, currentUser}) => {
|
vote: ({document, operationType, collection, currentUser}) => {
|
||||||
const voteResult = operateOnItem(collection, document, currentUser, voteType, true);
|
|
||||||
|
const voteResult = voteOnItem(collection, document, currentUser, operationType, true);
|
||||||
|
|
||||||
return mutate({
|
return mutate({
|
||||||
variables: {
|
variables: {
|
||||||
documentId: document._id,
|
documentId: document._id,
|
||||||
voteType,
|
operationType,
|
||||||
collectionName: collection._name,
|
collectionName: collection._name,
|
||||||
},
|
},
|
||||||
optimisticResponse: {
|
optimisticResponse: {
|
||||||
__typename: 'Mutation',
|
__typename: 'Mutation',
|
||||||
vote: {
|
vote: voteResult.document,
|
||||||
...voteResult,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
9
packages/vulcan-voting/lib/modules/fragments.js
Normal file
9
packages/vulcan-voting/lib/modules/fragments.js
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import { registerFragment } from 'meteor/vulcan:core';
|
||||||
|
|
||||||
|
registerFragment(`
|
||||||
|
fragment VoteFragment on Vote {
|
||||||
|
_id
|
||||||
|
voteType
|
||||||
|
power
|
||||||
|
}
|
||||||
|
`);
|
|
@ -1,6 +1,8 @@
|
||||||
import './custom_fields.js';
|
import './custom_fields.js';
|
||||||
import './permissions.js';
|
import './permissions.js';
|
||||||
|
import './fragments.js';
|
||||||
|
|
||||||
|
export { default as Votes } from './votes/collection.js';
|
||||||
export * from './make_voteable.js';
|
export * from './make_voteable.js';
|
||||||
export { default as withVote } from '../containers/withVote.js';
|
export { default as withVote } from '../containers/withVote.js';
|
||||||
export * from './helpers.js';
|
export * from './helpers.js';
|
||||||
|
|
|
@ -6,83 +6,95 @@ export const makeVoteable = collection => {
|
||||||
|
|
||||||
collection.addField([
|
collection.addField([
|
||||||
/**
|
/**
|
||||||
How many upvotes the document has received
|
The current user's votes on the document, if they exists
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
fieldName: 'upvotes',
|
fieldName: 'currentUserVotes',
|
||||||
fieldSchema: {
|
fieldSchema: {
|
||||||
type: Number,
|
type: Array,
|
||||||
optional: true,
|
optional: true,
|
||||||
defaultValue: 0,
|
|
||||||
viewableBy: ['guests'],
|
viewableBy: ['guests'],
|
||||||
|
resolveAs: {
|
||||||
|
type: '[Vote]',
|
||||||
|
resolver: async (document, args, { Users, Votes, currentUser }) => {
|
||||||
|
const votes = Votes.find({userId: currentUser._id, itemId: document._id}).fetch();
|
||||||
|
console.log('// currentUserVotes')
|
||||||
|
console.log('// currentUser._id', currentUser._id)
|
||||||
|
console.log('// document._id', document._id)
|
||||||
|
console.log('// votes', votes)
|
||||||
|
if (!votes.length) return null;
|
||||||
|
return votes;
|
||||||
|
// return Users.restrictViewableFields(currentUser, Votes, votes);
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'currentUserVotes.$',
|
||||||
|
fieldSchema: {
|
||||||
|
type: Object,
|
||||||
|
optional: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
All votes on the document
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
fieldName: 'allVotes',
|
||||||
|
fieldSchema: {
|
||||||
|
type: Array,
|
||||||
|
optional: true,
|
||||||
|
viewableBy: ['guests'],
|
||||||
|
resolveAs: {
|
||||||
|
type: 'Vote',
|
||||||
|
resolver: async (document, args, { Users, Votes, currentUser }) => {
|
||||||
|
const votes = Votes.find({itemId: document._id}).fetch();
|
||||||
|
if (!votes.length) return null;
|
||||||
|
return votes;
|
||||||
|
// return Users.restrictViewableFields(currentUser, Votes, votes);
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fieldName: 'allVotes.$',
|
||||||
|
fieldSchema: {
|
||||||
|
type: Object,
|
||||||
|
optional: true,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
An array containing the `_id`s of the document's upvoters
|
An array containing the `_id`s of the document's upvoters
|
||||||
*/
|
*/
|
||||||
{
|
{
|
||||||
fieldName: 'upvoters',
|
fieldName: 'voters',
|
||||||
fieldSchema: {
|
fieldSchema: {
|
||||||
type: Array,
|
type: Array,
|
||||||
optional: true,
|
optional: true,
|
||||||
viewableBy: ['guests'],
|
viewableBy: ['guests'],
|
||||||
resolveAs: {
|
resolveAs: {
|
||||||
fieldName: 'upvoters',
|
|
||||||
type: '[User]',
|
type: '[User]',
|
||||||
resolver: async (document, args, {currentUser, Users}) => {
|
resolver: async (document, args, {currentUser, Users}) => {
|
||||||
if (!document.upvoters) return [];
|
const votes = Votes.find({itemId: document._id}).fetch();
|
||||||
const upvoters = await Users.loader.loadMany(document.upvoters);
|
const votersIds = _.pluck(votes, 'userId');
|
||||||
return Users.restrictViewableFields(currentUser, Users, upvoters);
|
const voters = Users.find({_id: {$in: votersIds}});
|
||||||
|
return voters;
|
||||||
|
// if (!document.upvoters) return [];
|
||||||
|
// const upvoters = await Users.loader.loadMany(document.upvoters);
|
||||||
|
// return Users.restrictViewableFields(currentUser, Users, upvoters);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
fieldName: 'upvoters.$',
|
fieldName: 'voters.$',
|
||||||
fieldSchema: {
|
fieldSchema: {
|
||||||
type: String,
|
type: String,
|
||||||
optional: true
|
optional: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
How many downvotes the document has received
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
fieldName: 'downvotes',
|
|
||||||
fieldSchema: {
|
|
||||||
type: Number,
|
|
||||||
optional: true,
|
|
||||||
defaultValue: 0,
|
|
||||||
viewableBy: ['guests'],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
An array containing the `_id`s of the document's downvoters
|
|
||||||
*/
|
|
||||||
{
|
|
||||||
fieldName: 'downvoters',
|
|
||||||
fieldSchema: {
|
|
||||||
type: Array,
|
|
||||||
optional: true,
|
|
||||||
viewableBy: ['guests'],
|
|
||||||
resolveAs: {
|
|
||||||
fieldName: 'downvoters',
|
|
||||||
type: '[User]',
|
|
||||||
resolver: async (document, args, {currentUser, Users}) => {
|
|
||||||
if (!document.downvoters) return [];
|
|
||||||
const downvoters = await Users.loader.loadMany(document.downvoters);
|
|
||||||
return Users.restrictViewableFields(currentUser, Users, downvoters);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldName: 'downvoters.$',
|
|
||||||
fieldSchema: {
|
|
||||||
type: String,
|
|
||||||
optional: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
/**
|
||||||
The document's base score (not factoring in the document's age)
|
The document's base score (not factoring in the document's age)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import Users from 'meteor/vulcan:users';
|
import Users from 'meteor/vulcan:users';
|
||||||
import { hasUpvoted, hasDownvoted } from './helpers.js';
|
import { hasUpvoted, hasDownvoted } from './helpers.js';
|
||||||
import { runCallbacks, runCallbacksAsync } from 'meteor/vulcan:core';
|
import { runCallbacks, runCallbacksAsync, registerSetting, getSetting } from 'meteor/vulcan:core';
|
||||||
import update from 'immutability-helper';
|
import update from 'immutability-helper';
|
||||||
|
import Votes from './votes/collection.js';
|
||||||
|
|
||||||
|
registerSetting('voting.maxVotes', 1, 'How many times a user can vote on the same document');
|
||||||
|
|
||||||
// The equation to determine voting power. Defaults to returning 1 for everybody
|
// The equation to determine voting power. Defaults to returning 1 for everybody
|
||||||
export const getVotePower = function (user) {
|
export const getVotePower = (user, operationType) => {
|
||||||
return 1;
|
return operationType === 'upvote' ? 1 : -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const keepVoteProperties = item => _.pick(item, '__typename', '_id', 'upvoters', 'downvoters', 'upvotes', 'downvotes', 'baseScore');
|
const keepVoteProperties = item => _.pick(item, '__typename', '_id', 'upvoters', 'downvoters', 'upvotes', 'downvotes', 'baseScore');
|
||||||
|
@ -15,114 +18,111 @@ const keepVoteProperties = item => _.pick(item, '__typename', '_id', 'upvoters',
|
||||||
Runs all the operation and returns an objects without affecting the db.
|
Runs all the operation and returns an objects without affecting the db.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export const operateOnItem = function (collection, originalItem, user, operation, isClient = false) {
|
export const voteOnItem = function (collection, document, user, operationType = 'upvote') {
|
||||||
|
|
||||||
user = typeof user === "undefined" ? Meteor.user() : user;
|
const collectionName = collection.options.collectionName;
|
||||||
|
let result = {};
|
||||||
|
|
||||||
let item = {
|
// make sure item and user are defined
|
||||||
upvotes: 0,
|
if (!document || !user) {
|
||||||
downvotes: 0,
|
throw new Error(`Cannot perform operation '${collectionName}.${operationType}'`);
|
||||||
upvoters: [],
|
}
|
||||||
downvoters: [],
|
|
||||||
baseScore: 0,
|
/*
|
||||||
...originalItem,
|
|
||||||
|
First, handle vote cancellation.
|
||||||
|
Just remove last vote and subtract its power from the base score
|
||||||
|
|
||||||
|
*/
|
||||||
|
if (operationType === 'cancelVote') {
|
||||||
|
|
||||||
|
// create a "lite" version of the document that only contains relevant fields
|
||||||
|
const newDocument = {
|
||||||
|
_id: document._id,
|
||||||
|
currentUserVotes: document.currentUserVotes || [],
|
||||||
|
// voters: document.voters || [],
|
||||||
|
baseScore: document.baseScore || 0,
|
||||||
|
__typename: collection.options.typeName,
|
||||||
}; // we do not want to affect the original item directly
|
}; // we do not want to affect the original item directly
|
||||||
|
|
||||||
const votePower = getVotePower(user);
|
// if document has votes
|
||||||
const hasUpvotedItem = hasUpvoted(user, item);
|
if (newDocument.currentUserVotes.length) {
|
||||||
const hasDownvotedItem = hasDownvoted(user, item);
|
// remove one vote
|
||||||
const collectionName = collection._name;
|
const cancelledVote = _.last(newDocument.currentUserVotes);
|
||||||
const canDo = Users.canDo(user, `${collectionName}.${operation}`);
|
newDocument.currentUserVotes = _.initial(newDocument.currentUserVotes);
|
||||||
|
result.vote = cancelledVote;
|
||||||
|
|
||||||
// console.log('// operateOnItem')
|
// update base score
|
||||||
// console.log('isClient: ', isClient)
|
newDocument.baseScore -= cancelledVote.power;
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('// voteOnItem')
|
||||||
// console.log('collection: ', collectionName)
|
// console.log('collection: ', collectionName)
|
||||||
// console.log('operation: ', operation)
|
// console.log('document:', document)
|
||||||
// console.log('item: ', item)
|
// console.log('newDocument:', newDocument)
|
||||||
// console.log('user: ', user)
|
|
||||||
// console.log('hasUpvotedItem: ', hasUpvotedItem)
|
result.document = newDocument;
|
||||||
// console.log('hasDownvotedItem: ', hasDownvotedItem)
|
|
||||||
// console.log('canDo: ', canDo)
|
} else {
|
||||||
|
/*
|
||||||
|
|
||||||
|
Next, handle all other vote types (upvote, downvote, etc.)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
const power = getVotePower(user, operationType);
|
||||||
|
|
||||||
|
// create vote object
|
||||||
|
const vote = {
|
||||||
|
_id: Random.id(),
|
||||||
|
itemId: document._id,
|
||||||
|
collectionName,
|
||||||
|
userId: user._id,
|
||||||
|
voteType: operationType,
|
||||||
|
power,
|
||||||
|
votedAt: new Date(),
|
||||||
|
__typename: 'Vote'
|
||||||
|
};
|
||||||
|
|
||||||
|
// create a "lite" version of the document that only contains relevant fields
|
||||||
|
const currentUserVotes = document.currentUserVotes || [];
|
||||||
|
const newDocument = {
|
||||||
|
_id: document._id,
|
||||||
|
currentUserVotes: [...currentUserVotes, vote],
|
||||||
|
// voters: document.voters || [],
|
||||||
|
baseScore: document.baseScore || 0,
|
||||||
|
__typename: collection.options.typeName,
|
||||||
|
}; // we do not want to affect the original item directly
|
||||||
|
|
||||||
|
// update score
|
||||||
|
newDocument.baseScore += power;
|
||||||
|
|
||||||
|
// console.log('// voteOnItem')
|
||||||
|
// console.log('collection: ', collectionName)
|
||||||
|
// console.log('document:', document)
|
||||||
|
// console.log('newDocument:', newDocument)
|
||||||
|
|
||||||
// make sure item and user are defined, and user can perform the operation
|
// make sure item and user are defined, and user can perform the operation
|
||||||
if (
|
if (newDocument.currentUserVotes.length > getSetting('voting.maxVotes')) {
|
||||||
!item ||
|
throw new Error(`Cannot perform operation '${collectionName}.${operationType}'`);
|
||||||
!user ||
|
|
||||||
!canDo ||
|
|
||||||
operation === "upvote" && hasUpvotedItem ||
|
|
||||||
operation === "downvote" && hasDownvotedItem ||
|
|
||||||
operation === "cancelUpvote" && !hasUpvotedItem ||
|
|
||||||
operation === "cancelDownvote" && !hasDownvotedItem
|
|
||||||
) {
|
|
||||||
throw new Error(`Cannot perform operation "${collectionName}.${operation}"`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------------------------ Sync Callbacks ------------------------------ //
|
// ------------------------------ Sync Callbacks ------------------------------ //
|
||||||
|
|
||||||
item = runCallbacks(operation, item, user, operation, isClient);
|
// item = runCallbacks(operation, item, user, operation, isClient);
|
||||||
|
|
||||||
/*
|
result = {
|
||||||
|
document: newDocument,
|
||||||
voters arrays have different structures on client and server:
|
vote
|
||||||
|
};
|
||||||
- client: [{__typename: "User", _id: 'foo123'}]
|
|
||||||
- server: ['foo123']
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
const voter = isClient ? {__typename: "User", _id: user._id} : user._id;
|
|
||||||
const filterFunction = isClient ? u => u._id !== user._id : u => u !== user._id;
|
|
||||||
|
|
||||||
switch (operation) {
|
|
||||||
|
|
||||||
case "upvote":
|
|
||||||
if (hasDownvotedItem) {
|
|
||||||
item = operateOnItem(collection, item, user, "cancelDownvote", isClient);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
item = update(item, {
|
return result;
|
||||||
upvoters: {$push: [voter]},
|
|
||||||
upvotes: {$set: item.upvotes + 1},
|
|
||||||
baseScore: {$set: item.baseScore + votePower},
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
};
|
||||||
|
|
||||||
case "downvote":
|
export const cancelVote = function (collection, document, user, voteType = 'vote') {
|
||||||
if (hasUpvotedItem) {
|
|
||||||
item = operateOnItem(collection, item, user, "cancelUpvote", isClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
item = update(item, {
|
|
||||||
downvoters: {$push: [voter]},
|
|
||||||
downvotes: {$set: item.downvotes + 1},
|
|
||||||
baseScore: {$set: item.baseScore - votePower},
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "cancelUpvote":
|
|
||||||
item = update(item, {
|
|
||||||
upvoters: {$set: item.upvoters.filter(filterFunction)},
|
|
||||||
upvotes: {$set: item.upvotes - 1},
|
|
||||||
baseScore: {$set: item.baseScore - votePower},
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "cancelDownvote":
|
|
||||||
|
|
||||||
item = update(item, {
|
|
||||||
downvoters: {$set: item.downvoters.filter(filterFunction)},
|
|
||||||
downvotes: {$set: item.downvotes - 1},
|
|
||||||
baseScore: {$set: item.baseScore + votePower},
|
|
||||||
});
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// console.log('new item', item);
|
|
||||||
|
|
||||||
return item;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -130,14 +130,14 @@ export const operateOnItem = function (collection, originalItem, user, operation
|
||||||
Call operateOnItem, update the db with the result, run callbacks.
|
Call operateOnItem, update the db with the result, run callbacks.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
export const mutateItem = function (collection, originalItem, user, operation) {
|
// export const mutateItem = function (collection, originalItem, user, operation) {
|
||||||
const newItem = operateOnItem(collection, originalItem, user, operation, false);
|
// const newItem = operateOnItem(collection, originalItem, user, operation, false);
|
||||||
newItem.inactive = false;
|
// newItem.inactive = false;
|
||||||
|
|
||||||
collection.update({_id: newItem._id}, newItem, {bypassCollection2:true});
|
// collection.update({_id: newItem._id}, newItem, {bypassCollection2:true});
|
||||||
|
|
||||||
// --------------------- Server-Side Async Callbacks --------------------- //
|
// // --------------------- Server-Side Async Callbacks --------------------- //
|
||||||
runCallbacksAsync(operation+".async", newItem, user, collection, operation);
|
// runCallbacksAsync(operation+'.async', newItem, user, collection, operation);
|
||||||
|
|
||||||
return newItem;
|
// return newItem;
|
||||||
}
|
// }
|
||||||
|
|
19
packages/vulcan-voting/lib/modules/votes/collection.js
Normal file
19
packages/vulcan-voting/lib/modules/votes/collection.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
import { createCollection, getDefaultResolvers, getDefaultMutations } from 'meteor/vulcan:core';
|
||||||
|
import schema from './schema.js';
|
||||||
|
|
||||||
|
const Votes = createCollection({
|
||||||
|
|
||||||
|
collectionName: 'Votes',
|
||||||
|
|
||||||
|
typeName: 'Vote',
|
||||||
|
|
||||||
|
schema,
|
||||||
|
|
||||||
|
// resolvers: getDefaultResolvers('Votes'),
|
||||||
|
|
||||||
|
// mutations: getDefaultMutations('Votes'),
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default Votes;
|
55
packages/vulcan-voting/lib/modules/votes/schema.js
Normal file
55
packages/vulcan-voting/lib/modules/votes/schema.js
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
const schema = {
|
||||||
|
|
||||||
|
_id: {
|
||||||
|
type: String,
|
||||||
|
viewableBy: ['guests'],
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
The id of the document that was voted on
|
||||||
|
*/
|
||||||
|
itemId: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
The id of the document that was voted on
|
||||||
|
*/
|
||||||
|
collectionName: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
The id of the user that voted
|
||||||
|
*/
|
||||||
|
userId: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
An optional vote type (for Facebook-style reactions)
|
||||||
|
*/
|
||||||
|
voteType: {
|
||||||
|
type: String,
|
||||||
|
optional: true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
The vote power (e.g. 1 = upvote, -1 = downvote, or any other value)
|
||||||
|
*/
|
||||||
|
power: {
|
||||||
|
type: Number,
|
||||||
|
optional: true
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
The vote timestamp
|
||||||
|
*/
|
||||||
|
votedAt: {
|
||||||
|
type: Date,
|
||||||
|
optional: true
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default schema;
|
|
@ -1,20 +1,13 @@
|
||||||
import { addCallback, addGraphQLSchema, addGraphQLResolvers, addGraphQLMutation, Utils } from 'meteor/vulcan:core';
|
import { addCallback, addGraphQLSchema, addGraphQLResolvers, addGraphQLMutation, Utils, registerSetting, getSetting } from 'meteor/vulcan:core';
|
||||||
import { mutateItem } from '../modules/vote.js';
|
import { voteOnItem } from '../modules/vote.js';
|
||||||
import { VoteableCollections } from '../modules/make_voteable.js';
|
import { VoteableCollections } from '../modules/make_voteable.js';
|
||||||
import { createError } from 'apollo-errors';
|
import { createError } from 'apollo-errors';
|
||||||
|
import Votes from '../modules/votes/collection.js';
|
||||||
|
|
||||||
|
|
||||||
function CreateVoteableUnionType() {
|
function CreateVoteableUnionType() {
|
||||||
const voteSchema = `
|
const voteableSchema = VoteableCollections.length ? `union Voteable = ${VoteableCollections.map(collection => collection.typeName).join(' | ')}` : '';
|
||||||
type Vote {
|
addGraphQLSchema(voteableSchema);
|
||||||
itemId: String
|
|
||||||
power: Float
|
|
||||||
votedAt: String
|
|
||||||
}
|
|
||||||
|
|
||||||
${VoteableCollections.length ? `union Voteable = ${VoteableCollections.map(collection => collection.typeName).join(' | ')}` : ''}
|
|
||||||
`;
|
|
||||||
|
|
||||||
addGraphQLSchema(voteSchema);
|
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
addCallback('graphql.init.before', CreateVoteableUnionType);
|
addCallback('graphql.init.before', CreateVoteableUnionType);
|
||||||
|
@ -30,20 +23,55 @@ const resolverMap = {
|
||||||
|
|
||||||
addGraphQLResolvers(resolverMap);
|
addGraphQLResolvers(resolverMap);
|
||||||
|
|
||||||
addGraphQLMutation('vote(documentId: String, voteType: String, collectionName: String) : Voteable');
|
addGraphQLMutation('vote(documentId: String, operationType: String, collectionName: String) : Voteable');
|
||||||
|
|
||||||
const voteResolver = {
|
const voteResolver = {
|
||||||
Mutation: {
|
Mutation: {
|
||||||
vote(root, {documentId, voteType, collectionName}, context) {
|
async vote(root, {documentId, operationType, collectionName}, context) {
|
||||||
|
|
||||||
|
const { currentUser } = context;
|
||||||
const collection = context[Utils.capitalize(collectionName)];
|
const collection = context[Utils.capitalize(collectionName)];
|
||||||
const document = collection.findOne(documentId);
|
|
||||||
|
|
||||||
if (context.Users.canDo(context.currentUser, `${collectionName.toLowerCase()}.${voteType}`)) {
|
// query for document being voted on
|
||||||
|
const document = await collection.queryOne(documentId, {
|
||||||
|
fragmentText: `
|
||||||
|
fragment DocumentVoteFragment on ${collection.typeName} {
|
||||||
|
__typename
|
||||||
|
_id
|
||||||
|
currentUserVotes{
|
||||||
|
_id
|
||||||
|
voteType
|
||||||
|
power
|
||||||
|
}
|
||||||
|
baseScore
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
context
|
||||||
|
});
|
||||||
|
|
||||||
const mutatedDocument = mutateItem(collection, document, context.currentUser, voteType, false);
|
if (context.Users.canDo(currentUser, `${collectionName.toLowerCase()}.${operationType}`)) {
|
||||||
mutatedDocument.__typename = collection.typeName;
|
|
||||||
return mutatedDocument;
|
// put document through voteOnItem and get result
|
||||||
|
const voteResult = voteOnItem(collection, document, currentUser, operationType);
|
||||||
|
|
||||||
|
// get new version of document
|
||||||
|
const newDocument = voteResult.document;
|
||||||
|
newDocument.__typename = collection.typeName;
|
||||||
|
|
||||||
|
// get created or cancelled vote
|
||||||
|
const vote = voteResult.vote;
|
||||||
|
|
||||||
|
if (operationType === 'cancelVote' && vote) {
|
||||||
|
// if a vote has been cancelled, delete it
|
||||||
|
Votes.remove(vote._id);
|
||||||
|
} else {
|
||||||
|
// if a vote has been created, insert it
|
||||||
|
delete vote.__typename;
|
||||||
|
Votes.insert(vote);
|
||||||
|
}
|
||||||
|
|
||||||
|
// in any case, return the document that was voted on
|
||||||
|
return newDocument;
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue