reworking publications

This commit is contained in:
Sacha Greif 2016-02-17 11:28:00 +09:00
parent 38af000a4f
commit 9f8a094ab8
23 changed files with 372 additions and 290 deletions

View file

@ -41,7 +41,9 @@ telescope:comments
# telescope:spiderable
# telescope:subscribe-to-posts # notifications
# telescope:tagline-banner
# telescope:tags
telescope:tags
# telescope:theme-base
# telescope:theme-hubble
# telescope:update-prompt

View file

@ -112,6 +112,7 @@ telescope:lib@0.25.7
telescope:posts@0.25.7
telescope:search@0.25.7
telescope:settings@0.25.7
telescope:tags@0.25.7
telescope:users@0.25.7
templating@1.1.6-modules.8
templating-tools@1.0.1-modules.8

View file

@ -1,21 +1,4 @@
Telescope is an open-source, real-time social news site built with [Meteor](http://meteor.com)
# Telescope: Nova
**Note:** Telescope is beta software. Most of it should work but it's still a little unpolished and you'll probably find some bugs. Use at your own risk :)
**Nova** is a top-secret, highly unstable experimental branch of Telescope with a really cool name.
Note that Telescope is distributed under the [MIT License](http://opensource.org/licenses/MIT)
### Getting Started
Note that while simply cloning this repository will work, it is recommended you clone the [sample project](https://github.com/TelescopeJS/sample-project/) repository instead for a simpler workflow.
Please refer to [the documentation](http://telescope.readme.io/v0.20/docs/installing-telescope) for more instructions on installing Telescope.
### Learn More
- [Homepage](http://telescopeapp.org)
- [Demo](http://demo2.telescopeapp.org)
- [Sample Project](https://github.com/TelescopeJS/sample-project/)
- [Documentation](http://telescope.readme.io)
- [Roadmap](https://trello.com/b/oLMMqjVL/telescope-roadmap)
- [Slack](http://slack.telescopeapp.org/)
- [Meta](http://meta.telescopeapp.org/) Discussions about Telescope

View file

@ -0,0 +1,11 @@
const CategoriesList = props => {
return (
<ul>
{props.results.map(category => <li key={category._id}>{category.name}</li>)}
</ul>
)
};
module.exports = CategoriesList;

View file

@ -1,6 +1,6 @@
const Header = props => {
const Logo = Telescope.getComponent("Logo");
({Logo, ListContainer, CategoriesList} = Telescope.components);
const logoUrl = Telescope.settings.get("logoUrl");
const siteTitle = Telescope.settings.get("title", "Telescope");
@ -13,9 +13,7 @@ const Header = props => {
{tagline ? <h2 className="tagline">{tagline}</h2> : "" }
</div>
<div className="nav">
<ul>
<li>Nav link</li>
</ul>
<ListContainer collection={Categories} publication="categories" component={CategoriesList} limit={0}/>
</div>
</header>
)

View file

@ -19,4 +19,8 @@ Telescope.registerComponent("PostEdit", require('./posts/PostEdit.jsx'));
// comments
Telescope.registerComponent("CommentItem", require('./comments/list/CommentItem.jsx'));
Telescope.registerComponent("CommentList", require('./comments/list/CommentList.jsx'));
Telescope.registerComponent("CommentList", require('./comments/list/CommentList.jsx'));
// categories
Telescope.registerComponent("CategoriesList", require('./categories/list/CategoriesList.jsx'));

View file

@ -14,7 +14,8 @@ Comments.schema = new SimpleSchema({
*/
_id: {
type: String,
optional: true
optional: true,
public: true,
},
/**
The `_id` of the parent comment, if there is one
@ -25,6 +26,7 @@ Comments.schema = new SimpleSchema({
max: 500,
editableBy: ["member", "admin"],
optional: true,
public: true,
autoform: {
omit: true // never show this
}
@ -38,6 +40,7 @@ Comments.schema = new SimpleSchema({
max: 500,
editableBy: ["member", "admin"],
optional: true,
public: true,
autoform: {
omit: true // never show this
}
@ -47,14 +50,16 @@ Comments.schema = new SimpleSchema({
*/
createdAt: {
type: Date,
optional: true
optional: true,
public: true,
},
/**
The timestamp of the comment being posted. For now, comments are always created and posted at the same time
*/
postedAt: {
type: Date,
optional: true
optional: true,
public: true,
},
/**
The comment body (Markdown)
@ -63,6 +68,7 @@ Comments.schema = new SimpleSchema({
type: String,
max: 3000,
editableBy: ["member", "admin"],
public: true,
autoform: {
rows: 5,
afFormGroup: {
@ -75,7 +81,8 @@ Comments.schema = new SimpleSchema({
*/
htmlBody: {
type: String,
optional: true
optional: true,
public: true,
},
/**
The comment's base score (doesn't factor in comment age)
@ -83,7 +90,8 @@ Comments.schema = new SimpleSchema({
baseScore: {
type: Number,
decimal: true,
optional: true
optional: true,
public: true,
},
/**
The comment's current score (factors in comment age)
@ -91,49 +99,56 @@ Comments.schema = new SimpleSchema({
score: {
type: Number,
decimal: true,
optional: true
optional: true,
public: true,
},
/**
The number of upvotes the comment has received
*/
upvotes: {
type: Number,
optional: true
optional: true,
public: true,
},
/**
An array containing the `_id`s of upvoters
*/
upvoters: {
type: [String],
optional: true
optional: true,
public: true,
},
/**
The number of downvotes the comment has received
*/
downvotes: {
type: Number,
optional: true
optional: true,
public: true,
},
/**
An array containing the `_id`s of downvoters
*/
downvoters: {
type: [String],
optional: true
optional: true,
public: true,
},
/**
The comment author's name
*/
author: {
type: String,
optional: true
optional: true,
public: true,
},
/**
Whether the comment is inactive. Inactive comments' scores gets recalculated less often
*/
inactive: {
type: Boolean,
optional: true
optional: true,
public: true,
},
/**
The post's `_id`
@ -141,6 +156,7 @@ Comments.schema = new SimpleSchema({
postId: {
type: String,
optional: true,
public: true,
// regEx: SimpleSchema.RegEx.Id,
max: 500,
// editableBy: ["member", "admin"], // TODO: should users be able to set postId, but not modify it?
@ -153,14 +169,16 @@ Comments.schema = new SimpleSchema({
*/
userId: {
type: String,
optional: true
optional: true,
public: true,
},
/**
Whether the comment is deleted. Delete comments' content doesn't appear on the site.
*/
isDeleted: {
type: Boolean,
optional: true
optional: true,
public: true,
}
});

View file

@ -0,0 +1,27 @@
Posts.addField([
/**
Count of the post's comments
*/
{
fieldName: "commentCount",
fieldSchema: {
type: Number,
optional: true,
public: true
}
},
/**
An array containing the `_id`s of commenters
*/
{
fieldName: "commenters",
fieldSchema: {
type: [String],
optional: true,
public: true
}
}
]);
Posts.publicationFields.list.push("commentCount", "commenters");
Posts.publicationFields.single.push("commentCount", "commenters");

View file

@ -13,8 +13,6 @@ Meteor.publish('comments.list', function(terms) {
if(Users.can.viewById(this.userId)){
var parameters = Comments.parameters.get(terms);
console.log(terms)
console.log(parameters)
var comments = Comments.find(parameters.find, parameters.options);
// if there are comments, find out which posts were commented on

View file

@ -13,6 +13,7 @@ Package.onUse(function (api) {
'telescope:lib@0.25.7',
// 'telescope:i18n@0.25.7',
'telescope:settings@0.25.7',
'telescope:posts@0.25.7',
'telescope:users@0.25.7'
]);
@ -23,6 +24,7 @@ Package.onUse(function (api) {
'lib/views.js',
'lib/parameters.js',
'lib/helpers.js',
'lib/custom_fields.js',
// 'lib/routes.js'
], ['client', 'server']);

View file

@ -23,12 +23,17 @@ const ListContainer = React.createClass({
mixins: [ReactMeteorData],
getMeteorData() {
const terms = {...this.props.terms, limit: this.state.limit};
const parameters = this.props.collection.parameters.get(terms);
const find = parameters.find;
const options = parameters.options;
options.limit = this.state.limit;
let terms = {...this.props.terms, limit: this.state.limit};
let find = {};
let options = {limit: this.state.limit};
if (this.props.collection.parameters) {
const parameters = this.props.collection.parameters.get(terms);
find = parameters.find;
options = parameters.options;
}
const subscription = Meteor.subscribe(this.props.publication, terms);
const totalCount = Counts.get(this.props.publication);

View file

@ -48,4 +48,4 @@ Telescope.subscriptions = [];
*/
Telescope.subscriptions.preload = function (subscription, args) {
Telescope.subscriptions.push({name: subscription, arguments: args});
};
};

View file

@ -252,3 +252,11 @@ Telescope.getNestedProperty = function (obj, desc) {
while(arr.length && (obj = obj[arr.shift()]));
return obj;
};
/**
* Convert an array of fields to publish into a Mongo fields specifier
* @param {Array} fieldsArray
*/
Telescope.utils.arrayToFields = function (fieldsArray) {
return _.object(fieldsArray, _.map(fieldsArray, function () {return true}));
};

View file

@ -0,0 +1,29 @@
Posts.publicationFields = {};
/**
* Specify which fields should be published by the posts.list publication
* @array Posts.publicationFields.list
*/
Posts.publicationFields.list = [
"_id",
"postedAt",
"url",
"title",
"slug",
"htmlBody",
"viewCount",
"lastCommentedAt",
"clickCount",
"baseScore",
"score",
"status",
"sticky",
"author",
"userId"
];
/**
* Specify which fields should be published by the posts.single publication
* @array Posts.publicationFields.single
*/
Posts.publicationFields.single = Posts.simpleSchema().getPublicFields();

View file

@ -8,14 +8,16 @@ Posts.schema = new SimpleSchema({
*/
_id: {
type: String,
optional: true
optional: true,
public: true
},
/**
Timetstamp of post creation
*/
createdAt: {
type: Date,
optional: true
optional: true,
public: true
},
/**
Timestamp of post first appearing on the site (i.e. being approved)
@ -24,10 +26,11 @@ Posts.schema = new SimpleSchema({
type: Date,
optional: true,
editableBy: ["admin"],
// autoform: {
// group: 'admin',
// type: "bootstrap-datetimepicker"
// }
public: true,
autoform: {
group: 'admin',
type: "bootstrap-datetimepicker"
}
},
/**
URL
@ -37,10 +40,11 @@ Posts.schema = new SimpleSchema({
optional: true,
max: 500,
editableBy: ["member", "admin"],
// autoform: {
// type: "bootstrap-url",
// order: 10
// }
public: true,
autoform: {
type: "bootstrap-url",
order: 10
}
},
/**
Title
@ -50,16 +54,18 @@ Posts.schema = new SimpleSchema({
optional: false,
max: 500,
editableBy: ["member", "admin"],
// autoform: {
// order: 20
// }
public: true,
autoform: {
order: 20
}
},
/**
Slug
*/
slug: {
type: String,
optional: true
optional: true,
public: true,
},
/**
Post body (markdown)
@ -69,52 +75,43 @@ Posts.schema = new SimpleSchema({
optional: true,
max: 3000,
editableBy: ["member", "admin"],
// autoform: {
// rows: 5,
// order: 30
// }
public: true,
autoform: {
rows: 5,
order: 30
}
},
/**
HTML version of the post body
*/
htmlBody: {
type: String,
optional: true
optional: true,
public: true,
},
/**
Count of how many times the post's page was viewed
*/
viewCount: {
type: Number,
optional: true
},
/**
Count of the post's comments
*/
commentCount: {
type: Number,
optional: true
},
/**
An array containing the `_id`s of commenters
*/
commenters: {
type: [String],
optional: true
optional: true,
public: true,
},
/**
Timestamp of the last comment
*/
lastCommentedAt: {
type: Date,
optional: true
optional: true,
public: true,
},
/**
Count of how many times the post's link was clicked
*/
clickCount: {
type: Number,
optional: true
optional: true,
public: true,
},
/**
The post's base score (not factoring in the post's age)
@ -122,35 +119,8 @@ Posts.schema = new SimpleSchema({
baseScore: {
type: Number,
decimal: true,
optional: true
},
/**
How many upvotes the post has received
*/
upvotes: {
type: Number,
optional: true
},
/**
An array containing the `_id`s of the post's upvoters
*/
upvoters: {
type: [String],
optional: true
},
/**
How many downvotes the post has received
*/
downvotes: {
type: Number,
optional: true
},
/**
An array containing the `_id`s of the post's downvoters
*/
downvoters: {
type: [String],
optional: true
optional: true,
public: true,
},
/**
The post's current score (factoring in age)
@ -158,7 +128,8 @@ Posts.schema = new SimpleSchema({
score: {
type: Number,
decimal: true,
optional: true
optional: true,
public: true,
},
/**
The post's status. One of pending (`1`), approved (`2`), or deleted (`3`)
@ -167,6 +138,7 @@ Posts.schema = new SimpleSchema({
type: Number,
optional: true,
editableBy: ["admin"],
public: true,
autoValue: function () {
// only provide a default value
// 1) this is an insert operation
@ -175,11 +147,11 @@ Posts.schema = new SimpleSchema({
if (this.isInsert && !this.isSet)
return Posts.getDefaultStatus(user);
},
// autoform: {
// noselect: true,
// options: Posts.config.postStatuses,
// group: 'admin'
// }
autoform: {
noselect: true,
options: Posts.config.postStatuses,
group: 'admin'
}
},
/**
Whether the post is sticky (pinned to the top of posts lists)
@ -189,39 +161,45 @@ Posts.schema = new SimpleSchema({
optional: true,
defaultValue: false,
editableBy: ["admin"],
// autoform: {
// group: 'admin',
// leftLabel: "Sticky"
// }
public: true,
autoform: {
group: 'admin',
leftLabel: "Sticky"
}
},
/**
Whether the post is inactive. Inactive posts see their score recalculated less often
*/
inactive: {
type: Boolean,
optional: true
optional: true,
public: false,
},
/**
Save info for later spam checking on a post. We will use this for the akismet package
*/
userIP: {
type: String,
optional: true
optional: true,
public: false,
},
userAgent: {
type: String,
optional: true
optional: true,
public: false,
},
referrer: {
type: String,
optional: true
optional: true,
public: false,
},
/**
The post author's name
*/
author: {
type: String,
optional: true
optional: true,
public: true,
},
/**
The post author's `_id`.
@ -231,17 +209,18 @@ Posts.schema = new SimpleSchema({
optional: true,
// regEx: SimpleSchema.RegEx.Id,
editableBy: ["admin"],
// autoform: {
// group: 'admin',
// options: function () {
// return Meteor.users.find().map(function (user) {
// return {
// value: user._id,
// label: Users.getDisplayName(user)
// };
// });
// }
// }
public: true,
autoform: {
group: 'admin',
options: function () {
return Meteor.users.find().map(function (user) {
return {
value: user._id,
label: Users.getDisplayName(user)
};
});
}
}
}
});

View file

@ -1,124 +1,99 @@
Posts._ensureIndex({"status": 1, "postedAt": 1});
// ------------------------------------- Helpers -------------------------------- //
/**
* Get all users relevant to a list of posts
* (authors of the listed posts, and first four commenters of each post)
* @param {Object} posts
*/
const getPostsListUsers = posts => {
// add the userIds of each post authors
let userIds = _.pluck(posts.fetch(), 'userId');
// for each post, also add first four commenter's userIds to userIds array
posts.forEach(function (post) {
userIds = userIds.concat(_.first(post.commenters,4));
});
userIds = _.unique(userIds);
return Meteor.users.find({_id: {$in: userIds}}, {fields: Users.pubsub.avatarProperties});
};
/**
* Get all users relevant to a single post
* (author of the current post, authors of its comments, and upvoters & downvoters of the post)
* @param {Object} post
*/
const getSinglePostUsers = post => {
let users = [post.userId]; // publish post author's ID
// get IDs from all commenters on the post
const comments = Comments.find({postId: post._id}).fetch();
if (comments.length) {
users = users.concat(_.pluck(comments, "userId"));
}
// add upvoters
if (post.upvoters && post.upvoters.length) {
users = users.concat(post.upvoters);
}
// add downvoters
if (post.downvoters && post.downvoters.length) {
users = users.concat(post.downvoters);
}
// remove any duplicate IDs
users = _.unique(users);
return Meteor.users.find({_id: {$in: users}}, {fields: Users.pubsub.publicProperties});
};
// ------------------------------------- Publications -------------------------------- //
/**
* posts.list publication
* @param {Object} terms
*/
Meteor.publish('posts.list', function(terms) {
var parameters = Posts.parameters.get(terms),
posts = Posts.find(parameters.find, parameters.options);
this.unblock();
Counts.publish(this, 'posts.list', Posts.find(parameters.find, parameters.options));
const currentUser = Meteor.users.findOne(this.userId);
return posts;
terms.currentUserId = this.userId; // add currentUserId to terms
({find, options} = Posts.parameters.get(terms));
Counts.publish(this, 'posts.list', Posts.find(find, options));
options.fields = Telescope.utils.arrayToFields(Posts.publicationFields.list);
const posts = Posts.find(find, options);
const users = getPostsListUsers(posts);
return Users.can.view(currentUser) ? [posts, users] : [];
});
/**
* posts.item publication
* @param {Object} terms
*/
Meteor.publish('posts.single', function(terms) {
return Posts.find(terms);
check(terms, {_id: String});
});
this.unblock();
// Publish a list of posts
const currentUser = Meteor.users.findOne(this.userId);
const options = {fields: Telescope.utils.arrayToFields(Posts.publicationFields.single)};
const post = Posts.find(terms, options);
const users = getSinglePostUsers(post);
// Meteor.publish('postsList', function(terms) {
return Users.can.viewPost(currentUser, post) ? [post, users] : [];
// this.unblock();
// if (this.userId) { // add currentUserId to terms if a user is logged in
// terms.currentUserId = this.userId;
// }
// if(Users.can.viewById(this.userId)){
// var parameters = Posts.parameters.get(terms),
// posts = Posts.find(parameters.find, parameters.options);
// return posts;
// }
// return [];
// });
// // Publish all the users that have posted the currently displayed list of posts
// // plus the commenters for each post
// Meteor.publish('postsListUsers', function(terms) {
// this.unblock();
// if (this.userId) {
// terms.currentUserId = this.userId; // add userId to terms
// }
// if(Users.can.viewById(this.userId)){
// var parameters = Posts.parameters.get(terms),
// posts = Posts.find(parameters.find, parameters.options),
// userIds = _.pluck(posts.fetch(), 'userId');
// // for each post, add first four commenter's userIds to userIds array
// posts.forEach(function (post) {
// userIds = userIds.concat(_.first(post.commenters,4));
// });
// userIds = _.unique(userIds);
// return Meteor.users.find({_id: {$in: userIds}}, {fields: Users.pubsub.avatarProperties, multi: true});
// }
// return [];
// });
// // Publish a single post
// Meteor.publish('singlePost', function(postId) {
// check(postId, String);
// this.unblock();
// var user = Meteor.users.findOne(this.userId);
// var post = Posts.findOne(postId);
// if (Users.can.viewPost(user, post)){
// return Posts.find(postId);
// } else {
// return [];
// }
// });
// // Publish author of the current post, authors of its comments, and upvoters of the post
// Meteor.publish('postUsers', function(postId) {
// check(postId, String);
// this.unblock();
// if (Users.can.viewById(this.userId)){
// // publish post author and post commenters
// var post = Posts.findOne(postId);
// var users = [];
// if (post) {
// users.push(post.userId); // publish post author's ID
// // get IDs from all commenters on the post
// var comments = Comments.find({postId: post._id}).fetch();
// if (comments.length) {
// users = users.concat(_.pluck(comments, "userId"));
// }
// // publish upvoters
// if (post.upvoters && post.upvoters.length) {
// users = users.concat(post.upvoters);
// }
// // publish downvoters
// if (post.downvoters && post.downvoters.length) {
// users = users.concat(post.downvoters);
// }
// }
// // remove any duplicate IDs
// users = _.unique(users);
// return Meteor.users.find({_id: {$in: users}}, {fields: Users.pubsub.publicProperties});
// }
// return [];
// });
});

View file

@ -29,6 +29,7 @@ Package.onUse(function (api) {
'lib/parameters.js',
'lib/views.js',
'lib/helpers.js',
'lib/fields.js',
// 'lib/modules.js',
// 'lib/callbacks.js',
// 'lib/methods.js',

View file

@ -47,22 +47,14 @@ Categories.schema = new SimpleSchema({
}
});
Meteor.startup(function(){
Categories.internationalize();
});
// Meteor.startup(function(){
// Categories.internationalize();
// });
Categories.attachSchema(Categories.schema);
Meteor.startup(function () {
Categories.allow({
insert: Users.is.adminById,
update: Users.is.adminById,
remove: Users.is.adminById
});
});
Settings.addField([
Telescope.settings.collection.addField([
{
fieldName: 'categoriesBehavior',
fieldSchema: {

View file

@ -22,3 +22,6 @@ Posts.addField(
}
}
);
Posts.publicationFields.list.push("categories");
Posts.publicationFields.single.push("categories");

View file

@ -14,43 +14,43 @@ Package.onUse(function (api) {
api.addFiles([
'lib/categories.js',
'lib/helpers.js',
'lib/callbacks.js',
'lib/parameters.js',
'lib/custom_fields.js',
'lib/methods.js',
'lib/modules.js',
'lib/routes.js',
'package-tap.i18n'
// 'lib/callbacks.js',
// 'lib/parameters.js',
// 'lib/custom_fields.js',
// 'lib/methods.js',
// 'lib/modules.js',
// 'lib/routes.js',
// 'package-tap.i18n'
], ['client', 'server']);
api.addFiles([
'lib/client/scss/categories.scss',
'lib/client/templates/categories_admin.html',
'lib/client/templates/categories_admin.js',
'lib/client/templates/category_item.html',
'lib/client/templates/category_item.js',
'lib/client/templates/categories_menu.html',
'lib/client/templates/categories_menu.js',
'lib/client/templates/categories_menu_item.html',
'lib/client/templates/categories_menu_item.js',
'lib/client/templates/category_title.html',
'lib/client/templates/category_title.js',
'lib/client/templates/posts_category.html',
'lib/client/templates/post_categories.html',
'lib/client/templates/post_categories.js',
'lib/client/templates/autoform_category.html',
'lib/client/templates/autoform_category.js'
// 'lib/client/scss/categories.scss',
// 'lib/client/templates/categories_admin.html',
// 'lib/client/templates/categories_admin.js',
// 'lib/client/templates/category_item.html',
// 'lib/client/templates/category_item.js',
// 'lib/client/templates/categories_menu.html',
// 'lib/client/templates/categories_menu.js',
// 'lib/client/templates/categories_menu_item.html',
// 'lib/client/templates/categories_menu_item.js',
// 'lib/client/templates/category_title.html',
// 'lib/client/templates/category_title.js',
// 'lib/client/templates/posts_category.html',
// 'lib/client/templates/post_categories.html',
// 'lib/client/templates/post_categories.js',
// 'lib/client/templates/autoform_category.html',
// 'lib/client/templates/autoform_category.js'
], ['client']);
api.addFiles([
'lib/server/publications.js'
], ['server']);
var languages = ["ar", "bg", "cs", "da", "de", "el", "en", "es", "et", "fr", "hu", "id", "it", "ja", "kk", "ko", "nl", "pl", "pt-BR", "ro", "ru", "sl", "sv", "th", "tr", "vi", "zh-CN"];
var languagesPaths = languages.map(function (language) {
return "i18n/"+language+".i18n.json";
});
api.addFiles(languagesPaths, ["client", "server"]);
// var languages = ["ar", "bg", "cs", "da", "de", "el", "en", "es", "et", "fr", "hu", "id", "it", "ja", "kk", "ko", "nl", "pl", "pt-BR", "ro", "ru", "sl", "sv", "th", "tr", "vi", "zh-CN"];
// var languagesPaths = languages.map(function (language) {
// return "i18n/"+language+".i18n.json";
// });
// api.addFiles(languagesPaths, ["client", "server"]);
api.export([
'Categories'

View file

@ -27,7 +27,7 @@ Package.onUse(function (api) {
// 'lib/modules.js',
'lib/helpers.js',
// 'lib/menus.js',
// 'lib/pubsub.js',
'lib/pubsub.js',
// 'lib/methods.js',
// 'lib/routes.js'
], ['client', 'server']);

View file

@ -0,0 +1,45 @@
Telescope.settings.addField([
/**
How many upvotes the post has received
*/
{
fieldName: "upvotes",
fieldSchema: {
type: Number,
optional: true
}
},
/**
An array containing the `_id`s of the post's upvoters
*/
{
fieldName: "upvoters",
fieldSchema: {
type: [String],
optional: true
}
},
/**
How many downvotes the post has received
*/
{
fieldName: "downvotes",
fieldSchema: {
type: Number,
optional: true
}
},
/**
An array containing the `_id`s of the post's downvoters
*/
{
fieldName: "downvoters",
fieldSchema: {
type: [String],
optional: true
}
},
]);
Posts.publicationFields.list.push("upvotes", "downvotes");
Posts.publicationFields.single.push("upvotes", "upvoters", "downvotes", "downvoters");

View file

@ -15,7 +15,8 @@ Package.onUse(function (api) {
api.addFiles([
// 'package-tap.i18n',
'lib/vote.js'
'lib/vote.js',
'lib/custom_fields.js'
], ['client', 'server']);
api.addFiles([