Merge branch 'devel'

This commit is contained in:
Sacha Greif 2014-12-29 18:25:33 +09:00
commit 16898d4121
74 changed files with 1579 additions and 744 deletions

View file

@ -25,7 +25,7 @@ meteorhacks:fast-render
meteorhacks:subs-manager meteorhacks:subs-manager
meteorhacks:npm meteorhacks:npm
aldeed:autoform@4.0.0-rc1 aldeed:autoform
aldeed:collection2 aldeed:collection2
aldeed:simple-schema aldeed:simple-schema
@ -75,6 +75,6 @@ telescope-kadira
telescope-notifications telescope-notifications
telescope-singleday telescope-singleday
telescope-invites telescope-invites
telescope-post-by-feed
# Custom Packages # Custom Packages

View file

@ -1 +1 @@
METEOR@1.0.1 METEOR@1.0.2.1

View file

@ -1,113 +1,112 @@
accounts-base@1.1.2 accounts-base@1.1.3
accounts-facebook@1.0.2 accounts-facebook@1.0.3
accounts-oauth@1.1.2 accounts-oauth@1.1.3
accounts-password@1.0.4 accounts-password@1.0.5
accounts-twitter@1.0.2 accounts-twitter@1.0.3
accounts-ui-unstyled@1.1.4 accounts-ui@1.1.4
accounts-ui@1.1.3 accounts-ui-unstyled@1.1.5
aldeed:autoform@4.1.0 aldeed:autoform@4.2.2
aldeed:collection2@2.2.0 aldeed:collection2@2.3.0
aldeed:simple-schema@1.1.0 aldeed:simple-schema@1.2.0
application-configuration@1.0.3 application-configuration@1.0.4
artwells:queue@0.0.3 artwells:queue@0.0.3
autoupdate@1.1.3 autoupdate@1.1.4
backbone@1.0.0 backbone@1.0.0
base64@1.0.1 base64@1.0.2
bengott:avatar@0.7.3 bengott:avatar@0.7.3
binary-heap@1.0.1 binary-heap@1.0.2
blaze-tools@1.0.1 blaze@2.0.4
blaze@2.0.3 blaze-tools@1.0.2
boilerplate-generator@1.0.1 boilerplate-generator@1.0.2
callback-hook@1.0.1 callback-hook@1.0.2
ccan:cssreset@1.0.0 ccan:cssreset@1.0.0
check@1.0.2 check@1.0.3
chuangbo:cookie@1.1.0 chuangbo:cookie@1.1.0
chuangbo:marked@0.3.5 chuangbo:marked@0.3.5
cmather:handlebars-server@2.0.0 cmather:handlebars-server@2.0.0
coffeescript@1.0.4 coffeescript@1.0.5
ctl-helper@1.0.4 ddp@1.0.13
ctl@1.0.2 deps@1.0.6
ddp@1.0.12
deps@1.0.5
djedi:sanitize-html@1.3.0 djedi:sanitize-html@1.3.0
ejson@1.0.4 ejson@1.0.5
email@1.0.4 email@1.0.5
facebook@1.1.2 facebook@1.1.3
fastclick@1.0.1 fastclick@1.0.2
follower-livedata@1.0.2 follower-livedata@1.0.3
fourseven:scss@1.0.0 fourseven:scss@1.0.0
geojson-utils@1.0.1 geojson-utils@1.0.2
handlebars@1.0.1 handlebars@1.0.2
html-tools@1.0.2 html-tools@1.0.3
htmljs@1.0.2 htmljs@1.0.3
http@1.0.8 http@1.0.9
id-map@1.0.1 id-map@1.0.2
iron:controller@1.0.3 iron:controller@1.0.6
iron:core@1.0.3 iron:core@1.0.6
iron:dynamic-template@1.0.3 iron:dynamic-template@1.0.6
iron:layout@1.0.3 iron:layout@1.0.6
iron:location@1.0.3 iron:location@1.0.6
iron:middleware-stack@1.0.3 iron:middleware-stack@1.0.6
iron:router@1.0.3 iron:router@1.0.6
iron:url@1.0.3 iron:url@1.0.6
jparker:crypto-core@0.1.0 jparker:crypto-core@0.1.0
jparker:crypto-md5@0.1.1 jparker:crypto-md5@0.1.1
jparker:gravatar@0.3.1 jparker:gravatar@0.3.1
jquery@1.0.1 jquery@1.0.2
json@1.0.1 json@1.0.2
kestanous:herald-email@0.4.2
kestanous:herald@1.1.3 kestanous:herald@1.1.3
launch-screen@1.0.0 kestanous:herald-email@0.4.2
less@1.0.11 launch-screen@1.0.1
livedata@1.0.11 less@1.0.12
localstorage@1.0.1 livedata@1.0.12
logging@1.0.5 localstorage@1.0.2
matb33:collection-hooks@0.7.6 logging@1.0.6
meteor-platform@1.2.0 matb33:collection-hooks@0.7.7
meteor@1.1.3 meteor@1.1.4
meteor-platform@1.2.1
meteorhacks:async@1.0.0 meteorhacks:async@1.0.0
meteorhacks:fast-render@2.1.0 meteorhacks:fast-render@2.1.5
meteorhacks:kadira-binary-deps@1.3.1
meteorhacks:kadira@2.15.1 meteorhacks:kadira@2.15.1
meteorhacks:kadira-binary-deps@1.3.1
meteorhacks:meteorx@1.2.1 meteorhacks:meteorx@1.2.1
meteorhacks:npm@1.2.1 meteorhacks:npm@1.2.2
meteorhacks:subs-manager@1.2.2 meteorhacks:subs-manager@1.2.2
minifiers@1.1.2 minifiers@1.1.3
minimongo@1.0.5 minimongo@1.0.6
mobile-status-bar@1.0.1 mobile-status-bar@1.0.2
mongo-livedata@1.0.6 momentjs:moment@2.8.4
mongo@1.0.9 mongo@1.0.11
mongo-livedata@1.0.7
mrt:cookies@0.3.0 mrt:cookies@0.3.0
mrt:jquery-hotkeys@0.0.1 mrt:jquery-hotkeys@0.0.1
mrt:mailchimp@0.4.0 mrt:mailchimp@0.4.0
mrt:moment@2.8.1 mrt:moment@2.8.1
npm-bcrypt@0.7.7 npm-bcrypt@0.7.7
npm-container@1.0.0 npm-container@1.0.0
oauth1@1.1.2 oauth@1.1.3
oauth2@1.1.1 oauth1@1.1.3
oauth@1.1.2 oauth2@1.1.2
observe-sequence@1.0.3 observe-sequence@1.0.4
ordered-dict@1.0.1 ordered-dict@1.0.2
percolatestudio:synced-cron@1.1.0 percolatestudio:synced-cron@1.1.0
rajit:bootstrap3-datepicker@1.3.1 rajit:bootstrap3-datepicker@1.3.1
random@1.0.1 random@1.0.2
reactive-dict@1.0.4 reactive-dict@1.0.5
reactive-var@1.0.3 reactive-var@1.0.4
reload@1.1.1 reload@1.1.2
retry@1.0.1 retry@1.0.2
routepolicy@1.0.2 routepolicy@1.0.3
sacha:juice@0.1.0 sacha:juice@0.1.1
sacha:spin@2.0.4 sacha:spin@2.0.4
service-configuration@1.0.2 service-configuration@1.0.3
session@1.0.4 session@1.0.5
sha@1.0.1 sha@1.0.2
softwarerero:accounts-t9n@1.0.4 softwarerero:accounts-t9n@1.0.5
spacebars-compiler@1.0.3 spacebars@1.0.4
spacebars@1.0.3 spacebars-compiler@1.0.4
spiderable@1.0.5 spiderable@1.0.6
srp@1.0.1 srp@1.0.2
standard-app-packages@1.0.3 standard-app-packages@1.0.4
stylus@1.0.5 stylus@1.0.6
tap:http-methods@0.0.23 tap:http-methods@0.0.23
tap:i18n@1.2.1 tap:i18n@1.2.1
telescope-api@0.0.0 telescope-api@0.0.0
@ -123,6 +122,7 @@ telescope-lib@0.2.9
telescope-module-share@0.0.0 telescope-module-share@0.0.0
telescope-newsletter@0.1.0 telescope-newsletter@0.1.0
telescope-notifications@0.1.0 telescope-notifications@0.1.0
telescope-post-by-feed@0.0.1
telescope-rss@0.0.0 telescope-rss@0.0.0
telescope-search@0.0.0 telescope-search@0.0.0
telescope-singleday@0.1.0 telescope-singleday@0.1.0
@ -130,14 +130,14 @@ telescope-tags@0.0.0
telescope-theme-base@0.0.0 telescope-theme-base@0.0.0
telescope-theme-hubble@0.0.0 telescope-theme-hubble@0.0.0
telescope-update-prompt@0.1.0 telescope-update-prompt@0.1.0
templating@1.0.9 templating@1.0.10
tracker@1.0.3 tracker@1.0.4
tsega:bootstrap3-datetimepicker@3.1.3_1 tsega:bootstrap3-datetimepicker@3.1.3_3
twitter@1.1.2 twitter@1.1.3
ui@1.0.4 ui@1.0.5
underscore@1.0.1 underscore@1.0.2
url@1.0.2 url@1.0.3
useraccounts:core@1.4.0 useraccounts:core@1.4.1
useraccounts:unstyled@1.4.0 useraccounts:unstyled@1.4.1
webapp-hashing@1.0.1 webapp@1.1.5
webapp@1.1.4 webapp-hashing@1.0.2

View file

@ -1,3 +1,12 @@
## v0.11.1 “FeedScope”
* Post submit and edit forms now submit to their respective methods directly.
* Removed `postSubmitRenderedCallbacks` and `postEditRenderedCallbacks`.
* `telescope-post-by-feed` package now lets you import posts from RSS feeds.
* Adding limit of 200 posts to post list request.
* Refactoring post and comment submit to fix latency compensation issues.
* Tags package now using Autoform.
## v0.11.0 “AvatarScope” ## v0.11.0 “AvatarScope”
* Added new `userCreatedCallbacks` callback hook. * Added new `userCreatedCallbacks` callback hook.

View file

@ -1,9 +1,9 @@
<template name="settings"> <template name="settingsForm">
<div class="grid-small grid-module dialog settings"> <div class="grid-small grid-module dialog settings">
{{#if this.hasSettings}} {{#if this.hasSettings}}
{{> quickForm collection="Settings" id="updateSettingsForm" type="update" doc=this.settings label-class="control-label" input-col-class="controls" template="telescope"}} {{> quickForm collection="Settings" id="updateSettingsForm" type="update" doc=this.settings label-class="control-label" input-col-class="controls" template="telescope"}}
{{else}} {{else}}
{{> quickForm collection="Settings" id="updateSettingsForm" type="insert" template="telescope" label-class="control-label" input-col-class="controls"}} {{> quickForm collection="Settings" id="insertSettingsForm" type="insert" template="telescope" label-class="control-label" input-col-class="controls"}}
{{/if}} {{/if}}
</div> </div>
</template> </template>

View file

@ -0,0 +1,41 @@
AutoForm.hooks({
updateSettingsForm: {
before: {
update: function(docId, modifier, template) {
template.$('button[type=submit]').addClass('loading');
return modifier;
}
},
onSuccess: function(operation, result, template) {
template.$('button[type=submit]').removeClass('loading');
},
onError: function(operation, result, template) {
template.$('button[type=submit]').removeClass('loading');
}
}
});
AutoForm.hooks({
insertSettingsForm: {
before: {
insert: function(doc, template) {
template.$('button[type=submit]').addClass('loading');
return doc;
}
},
onSuccess: function(operation, result, template) {
template.$('button[type=submit]').removeClass('loading');
},
onError: function(operation, result, template) {
template.$('button[type=submit]').removeClass('loading');
}
}
});

View file

@ -6,7 +6,7 @@
<textarea id="comment" rows="3" autofocus="autofocus"></textarea> <textarea id="comment" rows="3" autofocus="autofocus"></textarea>
</div> </div>
<div class="comment-submit"> <div class="comment-submit">
<input type="submit" class="btn btn-primary" value="{{_ "add_comment"}}" title="(⌘+enter)" /> <button type="submit" class="comment-submit-button btn btn-primary btn-submit" value="{{_ "add_comment"}}" title="(⌘+enter)">{{_ "add_comment"}}</button>
</div> </div>
</form> </form>
</div> </div>

View file

@ -6,40 +6,45 @@ Template[getTemplate('comment_form')].helpers({
Template[getTemplate('comment_form')].events({ Template[getTemplate('comment_form')].events({
'submit form': function(e, instance){ 'submit form': function(e, instance){
var $commentForm = instance.$('#comment');
e.preventDefault(); e.preventDefault();
$(e.target).addClass('disabled'); $(e.target).addClass('disabled');
clearSeenMessages(); clearSeenMessages();
var content = $commentForm.val();
if(getCurrentTemplate() == 'comment_reply'){
// child comment
var parentComment = this.comment;
Meteor.call('comment', parentComment.postId, parentComment._id, content, function(error, newComment){
if(error){
console.log(error);
flashMessage(error.reason, "error");
}else{
trackEvent("newComment", newComment);
Router.go('post_page_comment', {
_id: parentComment.postId,
commentId: newComment._id
});
}
});
}else{
// root comment
var post = postObject;
Meteor.call('comment', post._id, null, content, function(error, newComment){
if(error){ var comment = {};
console.log(error); var $commentForm = instance.$('#comment');
flashMessage(error.reason, "error"); var $submitButton = instance.$('.btn-submit');
}else{ var body = $commentForm.val();
trackEvent("newComment", newComment);
Session.set('scrollToCommentId', newComment._id); // now that the form is latency compensated, we don't actually need to show this
$commentForm.val(''); // $commentForm.prop('disabled', true);
} // $submitButton.addClass('loading');
});
$commentForm.val('');
var post = postObject;
comment = {
postId: post._id,
body: body
} }
// child comment
if (getCurrentTemplate() == 'comment_reply') {
comment.parentCommentId = this.comment._id;
}
Meteor.call('submitComment', comment, function(error, newComment){
// $commentForm.prop('disabled', false);
// $submitButton.removeClass('loading');
if(error){
console.log(error);
flashMessage(error.reason, "error");
}else{
trackEvent("newComment", newComment);
}
});
} }
}); });

View file

@ -1,7 +1,7 @@
<template name="post_edit"> <template name="post_edit">
<div class="grid grid-module"> <div class="grid grid-module">
{{> quickForm collection="Posts" doc=post id="editPostForm" template="telescope" label-class="control-label" input-col-class="controls"}} {{> quickForm collection="Posts" doc=post id="editPostForm" template="telescope" label-class="control-label" input-col-class="controls" type="method" meteormethod="editPost"}}
</div> </div>
<div class="grid grid-module"> <div class="grid grid-module">

View file

@ -1,46 +1,32 @@
AutoForm.hooks({ AutoForm.hooks({
editPostForm: { editPostForm: {
onSubmit: function(insertDoc, updateDoc, currentDoc) {
var updateObject = updateDoc; before: {
var submit = this; editPost: function(doc, template) {
// ------------------------------ Checks ------------------------------ // var post = doc;
if (!Meteor.user()) { // ------------------------------ Checks ------------------------------ //
flashMessage(i18n.t('you_must_be_logged_in'), "");
return false;
}
// ------------------------------ Callbacks ------------------------------ // if (!Meteor.user()) {
flashMessage(i18n.t('you_must_be_logged_in'), "");
// run all post edit client callbacks on updateObject object successively return false;
updateObject = postEditClientCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, updateObject);
// ------------------------------ Update ------------------------------ //
Meteor.call('editPost', currentDoc._id, updateObject, function(error, post) {
if(error){
submit.done(error);
}else{
// note: find a way to do this in onSuccess instead?
trackEvent("edit post", {'postId': post._id});
Router.go('post_page', {_id: post._id});
submit.done();
} }
});
return false // ------------------------------ Callbacks ------------------------------ //
// run all post edit client callbacks on post object successively
post = postEditClientCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, post);
return post;
}
}, },
onSuccess: function(operation, result, template) { onSuccess: function(operation, post, template) {
// not used right now because I can't find a way to pass the "post" object to this callback trackEvent("edit post", {'postId': post._id});
// console.log(result) Router.go('post_page', {_id: post._id});
// trackEvent("new post", {'postId': post._id});
// if(post.status === STATUS_PENDING)
// throwError('Thanks, your post is awaiting approval.');
// Router.go('/posts/'+post._id);
}, },
onError: function(operation, error, template) { onError: function(operation, error, template) {
@ -49,16 +35,10 @@ AutoForm.hooks({
clearSeenMessages(); clearSeenMessages();
} }
// Called at the beginning and end of submission, respectively.
// This is the place to disable/enable buttons or the form,
// show/hide a "Please wait" message, etc. If these hooks are
// not defined, then by default the submit button is disabled
// during submission.
// beginSubmit: function(formId, template) {},
// endSubmit: function(formId, template) {}
} }
}); });
// delete link
Template[getTemplate('post_edit')].events({ Template[getTemplate('post_edit')].events({
'click .delete-link': function(e){ 'click .delete-link': function(e){
var post = this.post; var post = this.post;

View file

@ -1,7 +1,7 @@
<template name="post_submit"> <template name="post_submit">
<div class="grid grid-module"> <div class="grid grid-module">
{{> quickForm collection="Posts" id="submitPostForm" template="telescope" label-class="control-label" input-col-class="controls"}} {{> quickForm collection="Posts" id="submitPostForm" template="telescope" label-class="control-label" input-col-class="controls" type="method" meteormethod="submitPost"}}
</div> </div>
</template> </template>

View file

@ -1,54 +1,42 @@
AutoForm.hooks({ AutoForm.hooks({
submitPostForm: { submitPostForm: {
onSubmit: function(insertDoc, updateDoc, currentDoc) {
var properties = insertDoc; before: {
var submit = this; submitPost: function(doc, template) {
// ------------------------------ Checks ------------------------------ // template.$('button[type=submit]').addClass('loading');
if (!Meteor.user()) { var post = doc;
flashMessage(i18n.t('you_must_be_logged_in'), 'error');
return false;
}
// ------------------------------ Callbacks ------------------------------ // // ------------------------------ Checks ------------------------------ //
// run all post submit client callbacks on properties object successively if (!Meteor.user()) {
properties = postSubmitClientCallbacks.reduce(function(result, currentFunction) { flashMessage(i18n.t('you_must_be_logged_in'), 'error');
return currentFunction(result); return false;
}, properties);
// console.log(properties)
// ------------------------------ Insert ------------------------------ //
Meteor.call('submitPost', properties, function(error, post) {
if(error){
submit.done(error);
}else{
// note: find a way to do this in onSuccess instead?
trackEvent("new post", {'postId': post._id});
if (post.status === STATUS_PENDING) {
flashMessage(i18n.t('thanks_your_post_is_awaiting_approval'), 'success');
}
Router.go('post_page', {_id: post._id});
submit.done();
} }
});
return false // ------------------------------ Callbacks ------------------------------ //
// run all post submit client callbacks on properties object successively
post = postSubmitClientCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, post);
return post;
}
}, },
onSuccess: function(operation, result, template) { onSuccess: function(operation, post, template) {
// not used right now because I can't find a way to pass the "post" object to this callback template.$('button[type=submit]').removeClass('loading');
// console.log(post) trackEvent("new post", {'postId': post._id});
// trackEvent("new post", {'postId': post._id}); if (post.status === STATUS_PENDING) {
// if(post.status === STATUS_PENDING) flashMessage(i18n.t('thanks_your_post_is_awaiting_approval'), 'success');
// throwError('Thanks, your post is awaiting approval.'); }
// Router.go('/posts/'+post._id); Router.go('post_page', {_id: post._id});
}, },
onError: function(operation, error, template) { onError: function(operation, error, template) {
template.$('button[type=submit]').removeClass('loading');
flashMessage(error.message.split('|')[0], 'error'); // workaround because error.details returns undefined flashMessage(error.message.split('|')[0], 'error'); // workaround because error.details returns undefined
clearSeenMessages(); clearSeenMessages();
// $(e.target).removeClass('disabled'); // $(e.target).removeClass('disabled');
@ -58,12 +46,5 @@ AutoForm.hooks({
} }
} }
// Called at the beginning and end of submission, respectively.
// This is the place to disable/enable buttons or the form,
// show/hide a "Please wait" message, etc. If these hooks are
// not defined, then by default the submit button is disabled
// during submission.
// beginSubmit: function(formId, template) {},
// endSubmit: function(formId, template) {}
} }
}); });

View file

@ -87,9 +87,13 @@ Comments.allow({
remove: canEditById remove: canEditById
}); });
// ------------------------------------------------------------------------------------------- //
// ------------------------------------------ Hooks ------------------------------------------ //
// ------------------------------------------------------------------------------------------- //
Comments.before.insert(function (userId, doc) { Comments.before.insert(function (userId, doc) {
if(Meteor.isServer) // note: only actually sanitizes on the server
doc.htmlBody = sanitize(marked(doc.body)); doc.htmlBody = sanitize(marked(doc.body));
}); });
Comments.before.update(function (userId, doc, fieldNames, modifier, options) { Comments.before.update(function (userId, doc, fieldNames, modifier, options) {
@ -100,76 +104,133 @@ Comments.before.update(function (userId, doc, fieldNames, modifier, options) {
} }
}); });
commentAfterSubmitMethodCallbacks.push(function (comment) {
var userId = comment.userId,
commentAuthor = Meteor.users.findOne(userId);
// increment comment count
Meteor.users.update({_id: userId}, {
$inc: {'commentCount': 1}
});
// update post
Posts.update(comment.postId, {
$inc: {commentCount: 1},
$set: {lastCommentedAt: new Date()},
$addToSet: {commenters: userId}
});
// upvote comment
upvoteItem(Comments, comment, commentAuthor);
});
// ------------------------------------------------------------------------------------------- //
// -------------------------------------- Submit Comment ------------------------------------- //
// ------------------------------------------------------------------------------------------- //
submitComment = function (comment) {
var userId = comment.userId; // at this stage, a userId is expected
// ------------------------------ Checks ------------------------------ //
// Don't allow empty comments
if (!comment.body)
throw new Meteor.Error(704,i18n.t('your_comment_is_empty'));
// ------------------------------ Properties ------------------------------ //
var defaultProperties = {
createdAt: new Date(),
postedAt: new Date(),
upvotes: 0,
downvotes: 0,
baseScore: 0,
score: 0,
author: getDisplayNameById(userId)
};
comment = _.extend(defaultProperties, comment);
// ------------------------------ Callbacks ------------------------------ //
// run all post submit server callbacks on comment object successively
comment = commentSubmitMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, comment);
// -------------------------------- Insert -------------------------------- //
comment._id = Comments.insert(comment);
// --------------------- Server-side Async Callbacks --------------------- //
// run all post submit server callbacks on comment object successively
if (Meteor.isServer) {
Meteor.setTimeout(function () { // use setTimeout to avoid holding up client
comment = commentAfterSubmitMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, comment);
}, 1);
}
return comment;
}
// ------------------------------------------------------------------------------------------- //
// ----------------------------------------- Methods ----------------------------------------- //
// ------------------------------------------------------------------------------------------- //
Meteor.methods({ Meteor.methods({
comment: function(postId, parentCommentId, text){ submitComment: function(comment){
// required properties:
// postId
// content
// optional properties:
// parentCommentId
var user = Meteor.user(), var user = Meteor.user(),
post = Posts.findOne(postId), hasAdminRights = isAdmin(user);
postUser = Meteor.users.findOne(post.userId),
timeSinceLastComment = timeSinceLast(user, Comments), // ------------------------------ Checks ------------------------------ //
commentInterval = Math.abs(parseInt(getSetting('commentInterval',15))),
now = new Date();
// check that user can comment // check that user can comment
if (!user || !canComment(user)) if (!user || !canComment(user))
throw new Meteor.Error(i18n.t('you_need_to_login_or_be_invited_to_post_new_comments')); throw new Meteor.Error(i18n.t('you_need_to_login_or_be_invited_to_post_new_comments'));
// check that user waits more than 15 seconds between comments // ------------------------------ Rate Limiting ------------------------------ //
if(!this.isSimulation && (timeSinceLastComment < commentInterval))
throw new Meteor.Error(704, i18n.t('please_wait')+(commentInterval-timeSinceLastComment)+i18n.t('seconds_before_commenting_again'));
// Don't allow empty comments
if (!text)
throw new Meteor.Error(704,i18n.t('your_comment_is_empty'));
var comment = {
postId: postId,
body: text,
userId: user._id,
createdAt: now,
postedAt: now,
upvotes: 0,
downvotes: 0,
baseScore: 0,
score: 0,
author: getDisplayName(user)
};
if(parentCommentId) if (!hasAdminRights) {
comment.parentCommentId = parentCommentId;
var timeSinceLastComment = timeSinceLast(user, Comments),
commentInterval = Math.abs(parseInt(getSetting('commentInterval',15)));
// check that user waits more than 15 seconds between comments
if((timeSinceLastComment < commentInterval))
throw new Meteor.Error(704, i18n.t('please_wait')+(commentInterval-timeSinceLastComment)+i18n.t('seconds_before_commenting_again'));
}
// ------------------------------ Callbacks ------------------------------ // // ------------------------------ Properties ------------------------------ //
// run all post submit server callbacks on comment object successively // admin-only properties
comment = commentSubmitMethodCallbacks.reduce(function(result, currentFunction) { // userId
return currentFunction(result);
}, comment);
// -------------------------------- Insert ------------------------------- // // if user is not admin, clear restricted properties
if (!hasAdminRights) {
delete comment.userId;
}
comment._id = Comments.insert(comment); // if no userId has been set, default to current user id
if (!comment.userId) {
comment.userId = user._id
}
// ------------------------------ Callbacks ------------------------------ // return submitComment(comment);
// run all post submit server callbacks on comment object successively
comment = commentAfterSubmitMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, comment);
// increment comment count
Meteor.users.update({_id: user._id}, {
$inc: {'commentCount': 1}
});
Posts.update(postId, {
$inc: {commentCount: 1},
$set: {lastCommentedAt: now},
$addToSet: {commenters: user._id}
});
Meteor.call('upvoteComment', comment);
return comment;
}, },
removeComment: function(commentId){ removeComment: function(commentId){
var comment = Comments.findOne(commentId); var comment = Comments.findOne(commentId);

View file

@ -251,7 +251,8 @@ getPostProperties = function (post) {
// default status for new posts // default status for new posts
getDefaultPostStatus = function (user) { getDefaultPostStatus = function (user) {
if (isAdmin(user) || !getSetting('requirePostsApproval', false)) { var hasAdminRights = typeof user === 'undefined' ? false : isAdmin(user);
if (hasAdminRights || !getSetting('requirePostsApproval', false)) {
// if user is admin, or else post approval is not required // if user is admin, or else post approval is not required
return STATUS_APPROVED return STATUS_APPROVED
} else { } else {
@ -273,19 +274,26 @@ getPostLink = function (post) {
return !!post.url ? getOutgoingUrl(post.url) : getPostPageUrl(post); return !!post.url ? getOutgoingUrl(post.url) : getPostPageUrl(post);
}; };
checkForPostsWithSameUrl = function (url) { // we need the current user so we know who to upvote the existing post as
checkForPostsWithSameUrl = function (url, currentUser) {
// check that there are no previous posts with the same link in the past 6 months // check that there are no previous posts with the same link in the past 6 months
var sixMonthsAgo = moment().subtract(6, 'months').toDate(); var sixMonthsAgo = moment().subtract(6, 'months').toDate();
var postWithSameLink = Posts.findOne({url: url, postedAt: {$gte: sixMonthsAgo}}); var postWithSameLink = Posts.findOne({url: url, postedAt: {$gte: sixMonthsAgo}});
if(typeof postWithSameLink !== 'undefined'){ if(typeof postWithSameLink !== 'undefined'){
Meteor.call('upvotePost', postWithSameLink); upvoteItem(Posts, postWithSameLink, currentUser);
// note: error.details returns undefined on the client, so add post ID to reason // note: error.details returns undefined on the client, so add post ID to reason
throw new Meteor.Error('603', i18n.t('this_link_has_already_been_posted') + '|' + postWithSameLink._id, postWithSameLink._id); throw new Meteor.Error('603', i18n.t('this_link_has_already_been_posted') + '|' + postWithSameLink._id, postWithSameLink._id);
} }
} }
// when on a post page, return the current post
currentPost = function () {
return Posts.findOne(Router.current().data()._id);
}
// ------------------------------------------------------------------------------------------- // // ------------------------------------------------------------------------------------------- //
// ------------------------------------------ Hooks ------------------------------------------ // // ------------------------------------------ Hooks ------------------------------------------ //
// ------------------------------------------------------------------------------------------- // // ------------------------------------------------------------------------------------------- //
@ -303,6 +311,86 @@ Posts.before.update(function (userId, doc, fieldNames, modifier, options) {
} }
}); });
postAfterSubmitMethodCallbacks.push(function (post) {
var userId = post.userId,
postAuthor = Meteor.users.findOne(userId);
// increment posts count
Meteor.users.update({_id: userId}, {$inc: {postCount: 1}});
upvoteItem(Posts, post, postAuthor);
return post;
});
// ------------------------------------------------------------------------------------------- //
// --------------------------------------- Submit Post --------------------------------------- //
// ------------------------------------------------------------------------------------------- //
submitPost = function (post) {
var userId = post.userId, // at this stage, a userId is expected
user = Meteor.users.findOne(userId);
// ------------------------------ Checks ------------------------------ //
// check that a title was provided
if(!post.title)
throw new Meteor.Error(602, i18n.t('please_fill_in_a_title'));
// check that there are no posts with the same URL
if(!!post.url)
checkForPostsWithSameUrl(post.url, user);
// ------------------------------ Properties ------------------------------ //
defaultProperties = {
createdAt: new Date(),
author: getDisplayNameById(userId),
upvotes: 0,
downvotes: 0,
commentCount: 0,
clickCount: 0,
viewCount: 0,
baseScore: 0,
score: 0,
inactive: false,
sticky: false,
status: getDefaultPostStatus(),
postedAt: new Date()
};
post = _.extend(defaultProperties, post);
// clean up post title
post.title = cleanUp(post.title);
// ------------------------------ Callbacks ------------------------------ //
// run all post submit server callbacks on post object successively
post = postSubmitMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, post);
// -------------------------------- Insert ------------------------------- //
post._id = Posts.insert(post);
// --------------------- Server-Side Async Callbacks --------------------- //
if (Meteor.isServer) {
Meteor.defer(function () { // use defer to avoid holding up client
// run all post submit server callbacks on post object successively
post = postAfterSubmitMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, post);
});
}
return post;
}
// ------------------------------------------------------------------------------------------- // // ------------------------------------------------------------------------------------------- //
// ----------------------------------------- Methods ----------------------------------------- // // ----------------------------------------- Methods ----------------------------------------- //
// ------------------------------------------------------------------------------------------- // // ------------------------------------------------------------------------------------------- //
@ -313,15 +401,19 @@ postViews = [];
Meteor.methods({ Meteor.methods({
submitPost: function(post){ submitPost: function(post){
var title = cleanUp(post.title),
body = post.body, // required properties:
userId = this.userId, // title
user = Meteor.users.findOne(userId),
timeSinceLastPost=timeSinceLast(user, Posts), // optional properties
numberOfPostsInPast24Hours=numberOfItemsInPast24Hours(user, Posts), // URL
postInterval = Math.abs(parseInt(getSetting('postInterval', 30))), // body
maxPostsPer24Hours = Math.abs(parseInt(getSetting('maxPostsPerDay', 30))), // categories
postId = ''; // thumbnailUrl
// NOTE: the current user and the post author user might be two different users!
var user = Meteor.user(),
hasAdminRights = isAdmin(user);
// ------------------------------ Checks ------------------------------ // // ------------------------------ Checks ------------------------------ //
@ -329,109 +421,58 @@ Meteor.methods({
if (!user || !canPost(user)) if (!user || !canPost(user))
throw new Meteor.Error(601, i18n.t('you_need_to_login_or_be_invited_to_post_new_stories')); throw new Meteor.Error(601, i18n.t('you_need_to_login_or_be_invited_to_post_new_stories'));
// check that user provided a title
if(!post.title)
throw new Meteor.Error(602, i18n.t('please_fill_in_a_title'));
// check that there are no posts with the same URL
if(!!post.url)
checkForPostsWithSameUrl(post.url);
// --------------------------- Rate Limiting -------------------------- // // --------------------------- Rate Limiting -------------------------- //
if(!isAdmin(Meteor.user())){ if(!hasAdminRights){
var timeSinceLastPost=timeSinceLast(user, Posts),
numberOfPostsInPast24Hours=numberOfItemsInPast24Hours(user, Posts),
postInterval = Math.abs(parseInt(getSetting('postInterval', 30))),
maxPostsPer24Hours = Math.abs(parseInt(getSetting('maxPostsPerDay', 30)));
// check that user waits more than X seconds between posts // check that user waits more than X seconds between posts
if(!this.isSimulation && timeSinceLastPost < postInterval) if(timeSinceLastPost < postInterval)
throw new Meteor.Error(604, i18n.t('please_wait')+(postInterval-timeSinceLastPost)+i18n.t('seconds_before_posting_again')); throw new Meteor.Error(604, i18n.t('please_wait')+(postInterval-timeSinceLastPost)+i18n.t('seconds_before_posting_again'));
// check that the user doesn't post more than Y posts per day // check that the user doesn't post more than Y posts per day
if(!this.isSimulation && numberOfPostsInPast24Hours > maxPostsPer24Hours) if(numberOfPostsInPast24Hours > maxPostsPer24Hours)
throw new Meteor.Error(605, i18n.t('sorry_you_cannot_submit_more_than')+maxPostsPer24Hours+i18n.t('posts_per_day')); throw new Meteor.Error(605, i18n.t('sorry_you_cannot_submit_more_than')+maxPostsPer24Hours+i18n.t('posts_per_day'));
} }
// ------------------------------ Properties ------------------------------ // // ------------------------------ Properties ------------------------------ //
// Basic Properties // admin-only properties
properties = { // status
title: title, // postedAt
body: body, // userId
userId: userId, // sticky (default to false)
author: getDisplayNameById(userId),
upvotes: 0,
downvotes: 0,
commentCount: 0,
clickCount: 0,
viewCount: 0,
baseScore: 0,
score: 0,
inactive: false
};
// UserId // if user is not admin, clear restricted properties
if(isAdmin(Meteor.user()) && !!post.userId){ // only let admins post as other users if (!hasAdminRights) {
properties.userId = post.userId; delete post.status;
delete post.postedAt;
delete post.userId;
delete post.sticky;
} }
// Status // if no post status has been set, set it now
if(!!post.status && isAdmin(Meteor.user())){ if (!post.status) {
// if a custom status has been set, and user is admin, use that post.status = getDefaultPostStatus(user);
properties.status = post.status;
}else{
// else use default status
properties.status = getDefaultPostStatus(Meteor.user());
} }
// CreatedAt // if no userId has been set, default to current user id
properties.createdAt = new Date(); if (!post.userId) {
post.userId = user._id
// PostedAt
if(properties.status == 2){ // only set postedAt if post is approved
if(isAdmin(Meteor.user()) && !!post.postedAt){ // if user is admin and a custom postDate has been set
properties.postedAt = post.postedAt;
}else{ // else use current time
properties.postedAt = new Date();
}
} }
post = _.extend(post, properties); return submitPost(post);
// ------------------------------ Callbacks ------------------------------ //
// run all post submit server callbacks on post object successively
post = postSubmitMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, post);
// ------------------------------ Insert ------------------------------ //
// console.log(post)
post._id = Posts.insert(post);
// ------------------------------ Callbacks ------------------------------ //
// run all post submit server callbacks on post object successively
post = postAfterSubmitMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, post);
// ------------------------------ After Insert ------------------------------ //
// increment posts count
Meteor.users.update({_id: userId}, {$inc: {postCount: 1}});
var postAuthor = Meteor.users.findOne(post.userId);
Meteor.call('upvotePost', post, postAuthor);
return post;
}, },
editPost: function (postId, updateObject) { editPost: function (post, modifier, postId) {
var user = Meteor.user(); var user = Meteor.user();
// console.log(updateObject)
// ------------------------------ Checks ------------------------------ // // ------------------------------ Checks ------------------------------ //
// check that user can edit // check that user can edit
@ -440,23 +481,21 @@ Meteor.methods({
// ------------------------------ Callbacks ------------------------------ // // ------------------------------ Callbacks ------------------------------ //
// run all post submit server callbacks on updateObject successively // run all post submit server callbacks on modifier successively
updateObject = postEditMethodCallbacks.reduce(function(result, currentFunction) { modifier = postEditMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result); return currentFunction(result);
}, updateObject); }, modifier);
console.log(updateObject)
// ------------------------------ Update ------------------------------ // // ------------------------------ Update ------------------------------ //
Posts.update(postId, updateObject); Posts.update(postId, modifier);
// ------------------------------ Callbacks ------------------------------ // // ------------------------------ Callbacks ------------------------------ //
// run all post submit server callbacks on updateObject successively // run all post submit server callbacks on modifier object successively
updateObject = postAfterEditMethodCallbacks.reduce(function(result, currentFunction) { modifier = postAfterEditMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result); return currentFunction(result);
}, updateObject); }, modifier);
// ------------------------------ After Update ------------------------------ // // ------------------------------ After Update ------------------------------ //

View file

@ -81,7 +81,7 @@ getPostCommentUrl = function(postId, commentId){
slugify = function(text) { slugify = function(text) {
if(text){ if(text){
text = text.replace(/[^-_a-zA-Z0-9,&\s]+/ig, ''); text = text.replace(/[^-_a-zA-Z0-9,&\s]+/ig, '');
text = text.replace(/\s/gi, "+"); text = text.replace(/\s/gi, "-");
text = text.toLowerCase(); text = text.toLowerCase();
} }
return text; return text;

View file

@ -1,6 +1,9 @@
// getPostsParameters gives an object containing the appropriate find and options arguments for the subscriptions's Posts.find() // getPostsParameters gives an object containing the appropriate find and options arguments for the subscriptions's Posts.find()
getPostsParameters = function (terms) { getPostsParameters = function (terms, user) {
var hasAdminRights = typeof user !== 'undefined' && isAdmin(user);
var maxLimit = 200;
// console.log(terms) // console.log(terms)
@ -21,9 +24,14 @@ getPostsParameters = function (terms) {
deepExtend(true, parameters, {options: {sort: {_id: -1}}}); deepExtend(true, parameters, {options: {sort: {_id: -1}}});
// if there is a limit, add it too (note: limit=0 means "no limit") // if there is a limit, add it too (note: limit=0 means "no limit")
if (typeof terms.limit !== 'undefined') if (typeof terms.limit !== 'undefined')
_.extend(parameters.options, {limit: parseInt(terms.limit)}); _.extend(parameters.options, {limit: parseInt(terms.limit)});
// limit to "maxLimit" posts at most for non-admin users
if(!hasAdminRights && (parameters.options.limit == 0 || parameters.options.limit > maxLimit || !parameters.options.limit)) {
parameters.options.limit = maxLimit;
}
// hide future scheduled posts unless "showFuture" is set to true or postedAt is already defined // hide future scheduled posts unless "showFuture" is set to true or postedAt is already defined
if (!parameters.showFuture && !parameters.find.postedAt) if (!parameters.showFuture && !parameters.find.postedAt)
parameters.find.postedAt = {$lte: new Date()}; parameters.find.postedAt = {$lte: new Date()};

View file

@ -4,7 +4,7 @@ Meteor.startup(function (){
Router.route('/settings', { Router.route('/settings', {
name: 'settings', name: 'settings',
template: getTemplate('settings'), template: getTemplate('settingsForm'),
data: function () { data: function () {
// we only have one set of settings for now // we only have one set of settings for now
return { return {

View file

@ -114,7 +114,8 @@ PostPageController = RouteController.extend({
}, },
getTitle: function () { getTitle: function () {
return this.post().title + ' - ' + getSetting('title'); if (!!this.post())
return this.post().title + ' - ' + getSetting('title');
}, },
onBeforeAction: function() { onBeforeAction: function() {

View file

@ -1 +1 @@
telescopeVersion = "0.11.0"; telescopeVersion = "0.11.1";

View file

@ -1,226 +1,225 @@
// returns how much "power" a user's votes have // returns how much "power" a user's votes have
var getVotePower = function (user) { var getVotePower = function (user) {
// return isAdmin(user) ? 5 : 1; // return isAdmin(user) ? 5 : 1;
return 1; // for now, leave everybody at 1 including admins; 5 is too unbalanced return 1; // for now, leave everybody at 1 including admins; 5 is too unbalanced
}; };
var modifyKarma = function (userId, karma) { var modifyKarma = function (userId, karma) {
Meteor.users.update({_id: userId}, {$inc: {karma: karma}}); Meteor.users.update({_id: userId}, {$inc: {karma: karma}});
}; };
var hasUpvotedItem = function (item, user) { var hasUpvotedItem = function (item, user) {
return item.upvoters && item.upvoters.indexOf(user._id) != -1; return item.upvoters && item.upvoters.indexOf(user._id) != -1;
}; };
var hasDownvotedItem = function (item, user) { var hasDownvotedItem = function (item, user) {
return item.downvoters && item.downvoters.indexOf(user._id) != -1; return item.downvoters && item.downvoters.indexOf(user._id) != -1;
}; };
var addVote = function (userId, vote, collection, upOrDown) { var addVote = function (userId, vote, collection, upOrDown) {
var field = 'votes.' + upOrDown + 'voted' + collection; var field = 'votes.' + upOrDown + 'voted' + collection;
var add = {}; var add = {};
add[field] = vote; add[field] = vote;
var result = Meteor.users.update({_id: userId}, { var result = Meteor.users.update({_id: userId}, {
$addToSet: add $addToSet: add
}); });
}; };
var removeVote = function (userId, itemId, collection, upOrDown) { var removeVote = function (userId, itemId, collection, upOrDown) {
var field = 'votes.' + upOrDown + 'voted' + collection; var field = 'votes.' + upOrDown + 'voted' + collection;
var remove = {}; var remove = {};
remove[field] = {itemId: itemId}; remove[field] = {itemId: itemId};
Meteor.users.update({_id: userId}, { Meteor.users.update({_id: userId}, {
$pull: remove $pull: remove
}); });
}; };
var upvoteItem = function (collection, item) { upvoteItem = function (collection, item, user) {
var user = Meteor.user(), var user = typeof user === "undefined" ? Meteor.user() : user,
votePower = getVotePower(user), votePower = getVotePower(user),
collectionName = collection._name.slice(0,1).toUpperCase()+collection._name.slice(1); collectionName = collection._name.slice(0,1).toUpperCase()+collection._name.slice(1);
// make sure user has rights to upvote first
if (!user || !canUpvote(user, collection, true) || hasUpvotedItem(item, user))
return false;
// make sure user has rights to upvote first // in case user is upvoting a previously downvoted item, cancel downvote first
if (!user || !canUpvote(user, collection, true) || hasUpvotedItem(item, user)) cancelDownvote(collection, item, user);
return false;
// in case user is upvoting a previously downvoted item, cancel downvote first // Votes & Score
cancelDownvote(collection, item, user); var result = collection.update({_id: item && item._id, upvoters: { $ne: user._id }},{
$addToSet: {upvoters: user._id},
$inc: {upvotes: 1, baseScore: votePower},
$set: {inactive: false}
});
// Votes & Score if (result > 0) {
var result = collection.update({_id: item && item._id, upvoters: { $ne: user._id }},{ // Add item to list of upvoted items
$addToSet: {upvoters: user._id}, var vote = {
$inc: {upvotes: 1, baseScore: votePower}, itemId: item._id,
$set: {inactive: false} votedAt: new Date(),
}); power: votePower
};
addVote(user._id, vote, collectionName, 'up');
if (result > 0) { // extend item with baseScore to help calculate newScore
// Add item to list of upvoted items item = _.extend(item, {baseScore: (item.baseScore + votePower)});
var vote = { updateScore({collection: collection, item: item, forceUpdate: true});
itemId: item._id,
votedAt: new Date(),
power: votePower
};
addVote(user._id, vote, collectionName, 'up');
// extend item with baseScore to help calculate newScore // if the item is being upvoted by its own author, don't give karma
item = _.extend(item, {baseScore: (item.baseScore + votePower)}); if (item.userId != user._id) {
updateScore({collection: collection, item: item, forceUpdate: true}); modifyKarma(item.userId, votePower);
// if the item is being upvoted by its own author, don't give karma // if karma redistribution is enabled, give karma to all previous upvoters of the post
if (item.userId != user._id) { // (but not to the person doing the upvoting)
modifyKarma(item.userId, votePower); if (getSetting('redistributeKarma', false)) {
_.each(item.upvoters, function (upvoterId) {
// if karma redistribution is enabled, give karma to all previous upvoters of the post // share the karma equally among all upvoters, but cap the value at 0.1
// (but not to the person doing the upvoting) var karmaIncrease = Math.min(0.1, votePower/item.upvoters.length);
if (getSetting('redistributeKarma', false)) { modifyKarma(upvoterId, 0.1);
_.each(item.upvoters, function (upvoterId) { });
// share the karma equally among all upvoters, but cap the value at 0.1
var karmaIncrease = Math.min(0.1, votePower/item.upvoters.length);
modifyKarma(upvoterId, 0.1);
});
}
} }
} }
// console.log(collection.findOne(item._id)); }
return true; // console.log(collection.findOne(item._id));
}; return true;
};
var downvoteItem = function (collection, item) { downvoteItem = function (collection, item, user) {
var user = Meteor.user(), var user = typeof user === "undefined" ? Meteor.user() : user,
votePower = getVotePower(user), votePower = getVotePower(user),
collectionName = collection._name.slice(0,1).toUpperCase()+collection._name.slice(1); collectionName = collection._name.slice(0,1).toUpperCase()+collection._name.slice(1);
// make sure user has rights to downvote first // make sure user has rights to downvote first
if (!user || !canDownvote(user, collection, true) || hasDownvotedItem(item, user)) if (!user || !canDownvote(user, collection, true) || hasDownvotedItem(item, user))
return false; return false;
// in case user is downvoting a previously upvoted item, cancel upvote first // in case user is downvoting a previously upvoted item, cancel upvote first
cancelUpvote(collection, item, user); cancelUpvote(collection, item, user);
// Votes & Score // Votes & Score
var result = collection.update({_id: item && item._id, downvoters: { $ne: user._id }},{ var result = collection.update({_id: item && item._id, downvoters: { $ne: user._id }},{
$addToSet: {downvoters: user._id}, $addToSet: {downvoters: user._id},
$inc: {downvotes: 1, baseScore: -votePower}, $inc: {downvotes: 1, baseScore: -votePower},
$set: {inactive: false} $set: {inactive: false}
});
if (result > 0) {
// Add item to list of downvoted items
var vote = {
itemId: item._id,
votedAt: new Date(),
power: votePower
};
addVote(user._id, vote, collectionName, 'down');
// extend item with baseScore to help calculate newScore
item = _.extend(item, {baseScore: (item.baseScore + votePower)});
updateScore({collection: collection, item: item, forceUpdate: true});
// if the item is being upvoted by its own author, don't give karma
if (item.userId != user._id)
modifyKarma(item.userId, votePower);
}
// console.log(collection.findOne(item._id));
return true;
};
var cancelUpvote = function (collection, item) {
var user = Meteor.user(),
votePower = getVotePower(user),
collectionName = collection._name.slice(0,1).toUpperCase()+collection._name.slice(1);
// if user isn't among the upvoters, abort
if (!hasUpvotedItem(item, user))
return false;
// Votes & Score
var result = collection.update({_id: item && item._id, upvoters: user._id},{
$pull: {upvoters: user._id},
$inc: {upvotes: -1, baseScore: -votePower},
$set: {inactive: false}
});
if (result > 0) {
// Remove item from list of upvoted items
removeVote(user._id, item._id, collectionName, 'up');
// extend item with baseScore to help calculate newScore
item = _.extend(item, {baseScore: (item.baseScore + votePower)});
updateScore({collection: collection, item: item, forceUpdate: true});
// if the item is being upvoted by its own author, don't give karma
if (item.userId != user._id)
modifyKarma(item.userId, votePower);
}
// console.log(collection.findOne(item._id));
return true;
};
var cancelDownvote = function (collection, item) {
var user = Meteor.user(),
votePower = getVotePower(user),
collectionName = collection._name.slice(0,1).toUpperCase()+collection._name.slice(1);
// if user isn't among the downvoters, abort
if (!hasDownvotedItem(item, user))
return false;
// Votes & Score
var result = collection.update({_id: item && item._id, downvoters: user._id},{
$pull: {downvoters: user._id},
$inc: {downvotes: 1, baseScore: votePower},
$set: {inactive: false}
});
if (result > 0) {
// Remove item from list of downvoted items
removeVote(user._id, item._id, collectionName, 'down');
// extend item with baseScore to help calculate newScore
item = _.extend(item, {baseScore: (item.baseScore + votePower)});
updateScore({collection: collection, item: item, forceUpdate: true});
// if the item is being upvoted by its own author, don't give karma
if (item.userId != user._id)
modifyKarma(item.userId, votePower);
}
// console.log(collection.findOne(item._id));
return true;
};
// note: doesn't actually seem very useful to enable admins to vote for other users. Remove this?
var getUser = function (user) {
// only let admins specify different users for voting
// if no user is specified, use current user by default
return (isAdmin(Meteor.user()) && typeof user !== 'undefined') ? user : Meteor.user();
};
Meteor.methods({
upvotePost: function (post, user) {
return upvoteItem.call(this, Posts, post);
},
downvotePost: function (post, user) {
return downvoteItem.call(this, Posts, post);
},
cancelUpvotePost: function (post, user) {
return cancelUpvote.call(this, Posts, post);
},
cancelDownvotePost: function (post, user) {
return cancelDownvote.call(this, Posts, post);
},
upvoteComment: function (comment, user) {
return upvoteItem.call(this, Comments, comment);
},
downvoteComment: function (comment, user) {
return downvoteItem.call(this, Comments, comment);
},
cancelUpvoteComment: function (comment, user) {
return cancelUpvote.call(this, Comments, comment);
},
cancelDownvoteComment: function (comment, user) {
return cancelDownvote.call(this, Comments, comment);
}
}); });
if (result > 0) {
// Add item to list of downvoted items
var vote = {
itemId: item._id,
votedAt: new Date(),
power: votePower
};
addVote(user._id, vote, collectionName, 'down');
// extend item with baseScore to help calculate newScore
item = _.extend(item, {baseScore: (item.baseScore + votePower)});
updateScore({collection: collection, item: item, forceUpdate: true});
// if the item is being upvoted by its own author, don't give karma
if (item.userId != user._id)
modifyKarma(item.userId, votePower);
}
// console.log(collection.findOne(item._id));
return true;
};
cancelUpvote = function (collection, item, user) {
var user = typeof user === "undefined" ? Meteor.user() : user,
votePower = getVotePower(user),
collectionName = collection._name.slice(0,1).toUpperCase()+collection._name.slice(1);
// if user isn't among the upvoters, abort
if (!hasUpvotedItem(item, user))
return false;
// Votes & Score
var result = collection.update({_id: item && item._id, upvoters: user._id},{
$pull: {upvoters: user._id},
$inc: {upvotes: -1, baseScore: -votePower},
$set: {inactive: false}
});
if (result > 0) {
// Remove item from list of upvoted items
removeVote(user._id, item._id, collectionName, 'up');
// extend item with baseScore to help calculate newScore
item = _.extend(item, {baseScore: (item.baseScore + votePower)});
updateScore({collection: collection, item: item, forceUpdate: true});
// if the item is being upvoted by its own author, don't give karma
if (item.userId != user._id)
modifyKarma(item.userId, votePower);
}
// console.log(collection.findOne(item._id));
return true;
};
cancelDownvote = function (collection, item, user) {
var user = typeof user === "undefined" ? Meteor.user() : user,
votePower = getVotePower(user),
collectionName = collection._name.slice(0,1).toUpperCase()+collection._name.slice(1);
// if user isn't among the downvoters, abort
if (!hasDownvotedItem(item, user))
return false;
// Votes & Score
var result = collection.update({_id: item && item._id, downvoters: user._id},{
$pull: {downvoters: user._id},
$inc: {downvotes: 1, baseScore: votePower},
$set: {inactive: false}
});
if (result > 0) {
// Remove item from list of downvoted items
removeVote(user._id, item._id, collectionName, 'down');
// extend item with baseScore to help calculate newScore
item = _.extend(item, {baseScore: (item.baseScore + votePower)});
updateScore({collection: collection, item: item, forceUpdate: true});
// if the item is being upvoted by its own author, don't give karma
if (item.userId != user._id)
modifyKarma(item.userId, votePower);
}
// console.log(collection.findOne(item._id));
return true;
};
// note: doesn't actually seem very useful to enable admins to vote for other users. Remove this?
var getUser = function (user) {
// only let admins specify different users for voting
// if no user is specified, use current user by default
return (isAdmin(Meteor.user()) && typeof user !== 'undefined') ? user : Meteor.user();
};
Meteor.methods({
upvotePost: function (post) {
return upvoteItem.call(this, Posts, post);
},
downvotePost: function (post) {
return downvoteItem.call(this, Posts, post);
},
cancelUpvotePost: function (post) {
return cancelUpvote.call(this, Posts, post);
},
cancelDownvotePost: function (post) {
return cancelDownvote.call(this, Posts, post);
},
upvoteComment: function (comment) {
return upvoteItem.call(this, Comments, comment);
},
downvoteComment: function (comment) {
return downvoteItem.call(this, Comments, comment);
},
cancelUpvoteComment: function (comment) {
return cancelUpvote.call(this, Comments, comment);
},
cancelDownvoteComment: function (comment) {
return cancelDownvote.call(this, Comments, comment);
}
});

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",

View file

@ -224,15 +224,13 @@ postMeta = [
] ]
// ------------------------------ Callbacks ------------------------------ // // ------------------------------ Callbacks ------------------------------ //
postSubmitRenderedCallbacks = [];
postSubmitClientCallbacks = []; postSubmitClientCallbacks = [];
postSubmitMethodCallbacks = []; postSubmitMethodCallbacks = [];
postAfterSubmitMethodCallbacks = []; postAfterSubmitMethodCallbacks = []; // runs on server only in a timeout
postEditRenderedCallbacks = []; postEditClientCallbacks = []; // loops over post object
postEditClientCallbacks = []; postEditMethodCallbacks = []; // loops over modifier (i.e. "{$set: {foo: bar}}") object
postEditMethodCallbacks = []; // not used yet postAfterEditMethodCallbacks = []; // loops over modifier object
postAfterEditMethodCallbacks = []; // not used yet
commentSubmitRenderedCallbacks = []; commentSubmitRenderedCallbacks = [];
commentSubmitClientCallbacks = []; commentSubmitClientCallbacks = [];

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",

View file

@ -6,7 +6,7 @@
], ],
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -86,19 +86,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -110,7 +110,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",

View file

@ -7,7 +7,7 @@ Package.describe({
Package.onUse(function(api) { Package.onUse(function(api) {
api.use('templating@1.0.0'); api.use('templating@1.0.0');
api.use('blaze@2.0.0'); api.use('blaze@2.0.0');
api.use('aldeed:autoform@4.0.0'); api.use('aldeed:autoform');
api.use('fourseven:scss'); api.use('fourseven:scss');
// api.use('jquery'); // api.use('jquery');
// api.use('tsega:bootstrap3-datetimepicker'); // api.use('tsega:bootstrap3-datetimepicker');

View file

@ -2,11 +2,11 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:autoform", "aldeed:autoform",
"4.1.0" "4.2.0"
], ],
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"base64", "base64",

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -86,19 +86,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -110,7 +110,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",

View file

@ -40,7 +40,7 @@ $small-break: 30em;
height: 36px; height: 36px;
width: 36px; width: 36px;
margin: -18px 0 0 -18px; margin: -18px 0 0 -18px;
line-height: 32px; line-height: 38px;
text-align: center; text-align: center;
color: white; color: white;
display: block; display: block;

View file

@ -51,16 +51,48 @@ Meteor.methods({
} }
}); });
// For security reason, we use a separate server-side API call to set the media object // For security reason, we use a separate server-side API call to set the media object,
var addMediaOnSubmit = function (post) { // and the thumbnail object if it hasn't already been set
// note: the following function is not used because it would hold up the post submission, use next one instead
// var addMediaOnSubmit = function (post) {
// if(post.url){
// var data = getEmbedlyData(post.url);
// if (!!data) {
// // only add a thumbnailUrl if there isn't one already
// if(!post.thumbnailUrl && !!data.thumbnailUrl)
// post.thumbnailUrl = data.thumbnailUrl
// // add media if necessary
// if(!!data.media.html)
// post.media = data.media
// }
// }
// return post;
// }
// postSubmitMethodCallbacks.push(addMediaOnSubmit);
// Async variant that directly modifies the post object with update()
var addMediaAfterSubmit = function (post) {
var set = {};
if(post.url){ if(post.url){
var data = getEmbedlyData(post.url); var data = getEmbedlyData(post.url);
if(!!data && !!data.media.html) if (!!data) {
post.media = data.media // only add a thumbnailUrl if there isn't one already
if (!post.thumbnailUrl && !!data.thumbnailUrl) {
post.thumbnailUrl = data.thumbnailUrl;
set.thumbnailUrl = data.thumbnailUrl;
}
// add media if necessary
if (!!data.media.html) {
post.media = data.media;
set.media = data.media;
}
}
} }
Posts.update(post._id, {$set: set});
return post; return post;
} }
postSubmitMethodCallbacks.push(addMediaOnSubmit); postAfterSubmitMethodCallbacks.push(addMediaAfterSubmit);
// TODO: find a way to only do this is URL has actually changed? // TODO: find a way to only do this is URL has actually changed?
var updateMediaOnEdit = function (updateObject) { var updateMediaOnEdit = function (updateObject) {

View file

@ -2,11 +2,11 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:autoform", "aldeed:autoform",
"4.1.0" "4.2.0"
], ],
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",

View file

@ -2,11 +2,11 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:autoform", "aldeed:autoform",
"4.1.0" "4.2.0"
], ],
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -82,19 +82,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -106,7 +106,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",

View file

@ -38,16 +38,10 @@ SyncedCron.add({
} }
}); });
Meteor.startup(function() {
if(getSetting('newsletterFrequency', defaultFrequency) != 0) {
SyncedCron.start();
};
});
Meteor.methods({ Meteor.methods({
getNextJob: function () { getNextJob: function () {
var nextJob = SyncedCron.nextScheduledAtDate('scheduleNewsletter'); var nextJob = SyncedCron.nextScheduledAtDate('scheduleNewsletter');
console.log(nextJob); console.log(nextJob);
return nextJob; return nextJob;
} }
}); });

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -86,19 +86,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -110,7 +110,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",

View file

@ -14,7 +14,7 @@ commentAfterSubmitMethodCallbacks.push(function (comment) {
if(Meteor.isServer){ if(Meteor.isServer){
var parentCommentId = comment.parentCommentId; var parentCommentId = comment.parentCommentId;
var user = Meteor.user(); var user = Meteor.users.findOne(comment.userId);
var post = Posts.findOne(comment.postId); var post = Posts.findOne(comment.postId);
var postUser = Meteor.users.findOne(post.userId); var postUser = Meteor.users.findOne(post.userId);

View file

@ -6,7 +6,7 @@
], ],
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -94,19 +94,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -118,7 +118,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",

View file

@ -0,0 +1 @@
.build*

View file

@ -0,0 +1 @@
node_modules

View file

@ -0,0 +1,7 @@
This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.
You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.

View file

@ -0,0 +1,44 @@
{
"dependencies": {
"he": {
"version": "0.5.0"
},
"htmlparser2": {
"version": "3.8.2",
"dependencies": {
"domhandler": {
"version": "2.3.0"
},
"domutils": {
"version": "1.5.0"
},
"domelementtype": {
"version": "1.1.3"
},
"readable-stream": {
"version": "1.1.13",
"dependencies": {
"core-util-is": {
"version": "1.0.1"
},
"isarray": {
"version": "0.0.1"
},
"string_decoder": {
"version": "0.10.31"
},
"inherits": {
"version": "2.0.1"
}
}
},
"entities": {
"version": "1.0.0"
}
}
},
"to-markdown": {
"version": "0.0.2"
}
}
}

View file

@ -0,0 +1,5 @@
{
"feed_already_exists": "A feed with the same URL already exists.",
"you_need_to_login_and_be_an_admin_to_add_a_new_feed": "You need to log in and be an admin to add a new feed.",
"import_new_posts_from_feeds": "Import new posts from feeds."
}

View file

@ -0,0 +1,21 @@
adminNav.push({
route: 'feeds',
label: 'Feeds',
description: 'import_new_posts_from_feeds'
});
Meteor.startup(function () {
Router.onBeforeAction(Router._filters.isAdmin, {only: ['feeds']});
// RSS Urls Admin
Router.route('/feeds', {
name: 'feeds',
waitOn: function() {
return Meteor.subscribe('feeds');
},
template: getTemplate('feeds')
});
});

View file

@ -0,0 +1,5 @@
$light-yellow:#fffcea;
.add-feed{
background: $light-yellow;
}

View file

@ -0,0 +1,6 @@
<template name="feedItem">
<div class="grid-small grid-module">
{{> quickForm collection="Feeds" id=formId type="update" doc=this label-class="control-label" input-col-class="controls" template="telescope"}}
<a href="#" class="delete-link">Delete</a>
</div>
</template>

View file

@ -0,0 +1,16 @@
Meteor.startup(function () {
Template[getTemplate('feedItem')].helpers({
formId: function () {
return 'updateFeed-'+ this._id
}
});
Template[getTemplate('feedItem')].events({
'click .delete-link': function(e, instance){
e.preventDefault();
if (confirm("Delete feed?")) {
Feeds.remove(instance.data._id);
}
}
});
});

View file

@ -0,0 +1,9 @@
<template name="feeds">
<div class="grid-small grid-module dialog admin add-feed">
<h3>Add new feed:</h3>
{{> quickForm collection="Feeds" id="insertFeedForm" type="insert" label-class="control-label" input-col-class="controls" template="telescope"}}
</div>
{{#each feeds}}
{{> UI.dynamic template=feedItem}}
{{/each}}
</template>

View file

@ -0,0 +1,28 @@
Meteor.startup(function () {
Template[getTemplate('feeds')].helpers({
feeds: function(){
return Feeds.find({}, {sort: {url: 1}});
},
feedItem: function () {
return getTemplate('feedItem');
}
});
Template[getTemplate('feeds')].events({
'click input[type=submit]': function(e){
e.preventDefault();
var url = $('#url').val();
Meteor.call('insertFeed', {url: url}, function(error, result) {
if(error){
console.log(error);
flashMessage(error.reason, "error");
clearSeenMessages();
}else{
$('#url').val('');
}
});
}
});
});

View file

@ -0,0 +1,59 @@
var feedSchema = new SimpleSchema({
url: {
type: String,
regEx: SimpleSchema.RegEx.Url
}
});
Feeds = new Meteor.Collection("feeds");
Feeds.attachSchema(feedSchema);
// used to keep track of which feed a post was imported from
var feedIdProperty = {
propertyName: 'feedId',
propertySchema: {
type: String,
label: 'feedId',
optional: true,
autoform: {
omit: true
}
}
}
addToPostSchema.push(feedIdProperty);
// the RSS ID of the post in its original feed
var feedItemIdProperty = {
propertyName: 'feedItemId',
propertySchema: {
type: String,
label: 'feedItemId',
optional: true,
autoform: {
omit: true
}
}
}
addToPostSchema.push(feedItemIdProperty);
Meteor.startup(function () {
Feeds.allow({
insert: isAdminById,
update: isAdminById,
remove: isAdminById
});
Meteor.methods({
insertFeed: function(feedUrl){
check(feedUrl, feedSchema);
if (Feeds.findOne({url: feedSchema.url}))
throw new Meteor.Error('already-exists', i18n.t('feed_already_exists'));
if (!Meteor.user() || !isAdmin(Meteor.user()))
throw new Meteor.Error('login-required', i18n.t('you_need_to_login_and_be_an_admin_to_add_a_new_feed'));
return Feeds.insert(feedUrl);
}
});
});

View file

@ -0,0 +1,9 @@
SyncedCron.add({
name: 'Post by RSS feed',
schedule: function(parser) {
return parser.text('every 30 minutes');
},
job: function() {
fetchFeeds();
}
});

View file

@ -0,0 +1,77 @@
var htmlParser = Npm.require('htmlparser2');
var toMarkdown = Npm.require('to-markdown').toMarkdown;
var he = Npm.require('he')
var getFirstAdminUser = function() {
return Meteor.users.findOne({isAdmin: true}, {sort: {createdAt: 1}});
}
var handleFeed = function(error, feed) {
if (error) return;
var feedItems = _.first(feed.items, 20); // limit feed to 20 items just in case
clog('// Parsing RSS feed: '+ feed.title)
var newItemsCount = 0;
feedItems.forEach(function(item, index, array) {
// check if post already exists
if (!!Posts.findOne({feedItemId: item.id})) {
// clog('// Feed item already imported')
} else {
newItemsCount++;
var post = {
title: item.title,
body: toMarkdown(he.decode(item.description)),
url: item.link,
feedId: feed.id,
feedItemId: item.id,
userId: getFirstAdminUser()._id
}
try {
submitPost(post);
} catch (error) {
// catch errors so they don't stop the loop
clog(error);
}
}
});
clog('// Found ' + newItemsCount + ' new feed items')
};
fetchFeeds = function() {
var content;
Feeds.find().forEach(function(feed) {
try {
content = HTTP.get(feed.url).content;
} catch (e) {
// just go to next url
return true;
}
var feedHandler = new htmlParser.FeedHandler(handleFeed);
var parser = new htmlParser.Parser(feedHandler, {xmlMode: true});
parser.write(content);
parser.end()
});
}
Meteor.methods({
fetchFeeds: function () {
fetchFeeds();
},
testEntities: function (text) {
console.log(he.decode(text));
},
testToMarkdown: function (text) {
console.log(toMarkdown(text));
}
})

View file

@ -0,0 +1,6 @@
Meteor.publish('feeds', function() {
if(isAdminById(this.userId)){
return Feeds.find();
}
return [];
});

View file

@ -0,0 +1,5 @@
{
"translation_function_name": "__",
"helper_name": "_",
"namespace": "project"
}

View file

@ -0,0 +1,64 @@
Package.describe({
summary: 'Auto post via RSS to Telescope',
version: '0.0.1',
name: 'telescope-post-by-feed'
});
Npm.depends({
'htmlparser2': '3.8.2',
'to-markdown': '0.0.2',
'he': '0.5.0'
});
Package.onUse(function(api) {
api.use([
'telescope-base',
'aldeed:simple-schema',
'aldeed:autoform',
'tap:i18n',
'fourseven:scss'
], ['client', 'server']);
api.use([
'iron:router',
'templating'
], 'client');
api.use([
'http',
'mrt:moment',
'percolatestudio:synced-cron'
], 'server');
api.add_files([
'lib/feeds.js'
], ['client', 'server']);
api.add_files([
'lib/client/routes.js',
'lib/client/scss/feeds.scss',
'lib/client/templates/feeds.js',
'lib/client/templates/feeds.html',
'lib/client/templates/feed_item.js',
'lib/client/templates/feed_item.html',
], 'client');
api.add_files([
'lib/server/fetch_feeds.js',
'lib/server/cron.js',
'lib/server/publications.js'
], ['server']);
api.add_files([
"i18n/en.i18n.json"
], ["client", "server"]);
api.export([
'Feeds'
]);
});
Package.onTest(function(api) {
api.use('tinytest');
});

View file

@ -0,0 +1,251 @@
{
"dependencies": [
[
"aldeed:autoform",
"4.2.0"
],
[
"aldeed:simple-schema",
"1.2.0"
],
[
"application-configuration",
"1.0.3"
],
[
"base64",
"1.0.1"
],
[
"binary-heap",
"1.0.1"
],
[
"blaze",
"2.0.3"
],
[
"blaze-tools",
"1.0.1"
],
[
"boilerplate-generator",
"1.0.1"
],
[
"callback-hook",
"1.0.1"
],
[
"check",
"1.0.2"
],
[
"coffeescript",
"1.0.4"
],
[
"ddp",
"1.0.12"
],
[
"deps",
"1.0.5"
],
[
"ejson",
"1.0.4"
],
[
"follower-livedata",
"1.0.2"
],
[
"fourseven:scss",
"1.0.0"
],
[
"geojson-utils",
"1.0.1"
],
[
"html-tools",
"1.0.2"
],
[
"htmljs",
"1.0.2"
],
[
"http",
"1.0.8"
],
[
"id-map",
"1.0.1"
],
[
"iron:controller",
"1.0.3"
],
[
"iron:core",
"1.0.4"
],
[
"iron:dynamic-template",
"1.0.5"
],
[
"iron:layout",
"1.0.5"
],
[
"iron:location",
"1.0.4"
],
[
"iron:middleware-stack",
"1.0.3"
],
[
"iron:router",
"1.0.3"
],
[
"iron:url",
"1.0.4"
],
[
"jquery",
"1.0.1"
],
[
"json",
"1.0.1"
],
[
"livedata",
"1.0.11"
],
[
"logging",
"1.0.5"
],
[
"meteor",
"1.1.3"
],
[
"minifiers",
"1.1.2"
],
[
"minimongo",
"1.0.5"
],
[
"mongo",
"1.0.9"
],
[
"mrt:moment",
"2.8.1"
],
[
"observe-sequence",
"1.0.3"
],
[
"ordered-dict",
"1.0.1"
],
[
"percolatestudio:synced-cron",
"1.1.0"
],
[
"random",
"1.0.1"
],
[
"reactive-dict",
"1.0.4"
],
[
"reactive-var",
"1.0.3"
],
[
"retry",
"1.0.1"
],
[
"routepolicy",
"1.0.2"
],
[
"session",
"1.0.4"
],
[
"spacebars",
"1.0.3"
],
[
"spacebars-compiler",
"1.0.3"
],
[
"tap:http-methods",
"0.0.23"
],
[
"tap:i18n",
"1.2.1"
],
[
"telescope-base",
"0.0.0"
],
[
"telescope-i18n",
"0.0.0"
],
[
"telescope-lib",
"0.2.9"
],
[
"templating",
"1.0.9"
],
[
"tracker",
"1.0.3"
],
[
"ui",
"1.0.4"
],
[
"underscore",
"1.0.1"
],
[
"url",
"1.0.2"
],
[
"webapp",
"1.1.4"
],
[
"webapp-hashing",
"1.0.1"
]
],
"pluginDependencies": [],
"toolVersion": "meteor-tool@1.0.36",
"format": "1.0"
}

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -78,19 +78,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -102,7 +102,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -78,19 +78,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -102,7 +102,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",

View file

@ -1,4 +1,5 @@
$red: #DD3416; $red: #DD3416;
$light-yellow:#fffcea;
.post-category{ .post-category{
display:inline-block; display:inline-block;
@ -12,4 +13,8 @@ $red: #DD3416;
background:$red; background:$red;
color:white; color:white;
} }
}
.add-category{
background: $light-yellow;
} }

View file

@ -1,24 +1,9 @@
<template name="categories"> <template name="categories">
<div class="grid-small grid-module dialog admin"> <div class="grid-small grid-module dialog admin add-category">
<h2>Categories</h2> <h3>Add new category:</h3>
<form class="form-block"> {{> quickForm collection="Categories" id="insertCategoryForm" type="insert" label-class="control-label" input-col-class="controls" template="telescope"}}
<h3>Add new category</h3>
<div class="control-group">
<label>Name</label>
<div class="controls"><input id="name" type="text" value="" /></div>
</div>
<div class="control-group">
<label>Order</label>
<div class="controls"><input id="order" type="number" value="" /></div>
</div>
<div class="form-actions">
<input type="submit" class="button" value="Submit" />
</div>
</form>
<ul>
{{#each categories}}
{{> UI.dynamic template=categoryItem}}
{{/each}}
</ul>
</div> </div>
{{#each categories}}
{{> UI.dynamic template=categoryItem}}
{{/each}}
</template> </template>

View file

@ -1,15 +1,6 @@
<template name="categoryItem"> <template name="categoryItem">
<li> <div class="grid-small grid-module">
<form> {{> quickForm collection="Categories" id=formId type="update" doc=this label-class="control-label" input-col-class="controls" template="telescope"}}
<div class="control-group inline"> <a href="#" class="delete-link">Delete</a>
<a class="button submit edit-link btn" href="#">Save</a> </div>
<div class="controls">
<input class="category-name" id="name_{{_id}}" type="text" value="{{name}}" placeholder="Name"/>
<textarea class="category-description" id="description_{{_id}}">{{description}}</textarea>
<input class="category-number" id="order_{{_id}}" type="number" value="{{order}}" placeholder="0"/>
</div>
<span class="category-slug small">Slug: /{{slug}}</span>
</div>
</form>
</li>
</template> </template>

View file

@ -1,16 +1,15 @@
Meteor.startup(function () { Meteor.startup(function () {
Template[getTemplate('categoryItem')].helpers({
formId: function () {
return 'updateCategory-'+ this._id
}
});
Template[getTemplate('categoryItem')].events({ Template[getTemplate('categoryItem')].events({
'click .edit-link': function(e, instance){ 'click .delete-link': function(e, instance){
e.preventDefault(); e.preventDefault();
var categoryId = instance.data._id; if (confirm("Delete category?")) {
var name = $('#name_'+categoryId).val(); Categories.remove(instance.data._id);
var description = $('#description_'+categoryId).val();
var order = parseInt($('#order_'+categoryId).val());
var slug = slugify(name);
if(name){
Categories.update(categoryId,{ $set: {name: name, slug: slug, order: order, description: description}});
}else{
Categories.remove(categoryId);
} }
} }
}); });

View file

@ -1,29 +1,34 @@
// category schema // category schema
categorySchema = new SimpleSchema({ categorySchema = new SimpleSchema({
_id: { name: {
type: String
},
description: {
type: String, type: String,
optional: true optional: true,
autoform: {
rows: 3
}
}, },
order: { order: {
type: Number, type: Number,
optional: true optional: true
}, },
slug: { slug: {
type: String
},
name: {
type: String
},
description: {
type: String, type: String,
optional: true,
autoform: { autoform: {
rows: 5
} }
} }
}); });
Categories = new Meteor.Collection("categories", { Categories = new Meteor.Collection("categories");
schema: categorySchema Categories.attachSchema(categorySchema);
Categories.before.insert(function (userId, doc) {
// if no slug has been provided, generate one
if (!doc.slug)
doc.slug = slugify(doc.name);
}); });
// we want to wait until categories are all loaded to load the rest of the app // we want to wait until categories are all loaded to load the rest of the app
@ -80,9 +85,9 @@ addToPostSchema.push(
Meteor.startup(function () { Meteor.startup(function () {
Categories.allow({ Categories.allow({
insert: isAdminById insert: isAdminById,
, update: isAdminById update: isAdminById,
, remove: isAdminById remove: isAdminById
}); });
Meteor.methods({ Meteor.methods({

View file

@ -6,8 +6,10 @@ Package.onUse(function (api) {
'telescope-lib', 'telescope-lib',
'telescope-base', 'telescope-base',
'aldeed:simple-schema', 'aldeed:simple-schema',
'aldeed:autoform',
'tap:i18n', 'tap:i18n',
'fourseven:scss' 'fourseven:scss',
'matb33:collection-hooks'
], ['client', 'server']); ], ['client', 'server']);
api.use([ api.use([
@ -46,5 +48,12 @@ Package.onUse(function (api) {
"i18n/zh-CN.i18n.json", "i18n/zh-CN.i18n.json",
], ["client", "server"]); ], ["client", "server"]);
api.export(['preloadSubscriptions', 'adminNav', 'Categories', 'addToPostSchema', 'primaryNav', 'postModules']); api.export([
'preloadSubscriptions',
'adminNav',
'Categories',
'addToPostSchema',
'primaryNav',
'postModules'
]);
}); });

View file

@ -1,8 +1,12 @@
{ {
"dependencies": [ "dependencies": [
[
"aldeed:autoform",
"4.2.0"
],
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -82,19 +86,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -106,7 +110,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",
@ -116,10 +120,18 @@
"json", "json",
"1.0.1" "1.0.1"
], ],
[
"livedata",
"1.0.11"
],
[ [
"logging", "logging",
"1.0.5" "1.0.5"
], ],
[
"matb33:collection-hooks",
"0.7.6"
],
[ [
"meteor", "meteor",
"1.1.3" "1.1.3"
@ -136,6 +148,10 @@
"mongo", "mongo",
"1.0.9" "1.0.9"
], ],
[
"mrt:moment",
"2.8.1"
],
[ [
"observe-sequence", "observe-sequence",
"1.0.3" "1.0.3"

View file

@ -88,6 +88,10 @@ form, .accounts-dialog{
&[type="number"]{ &[type="number"]{
width: 30%; width: 30%;
} }
&:disabled{
background: #eee;
color: $light-text;
}
} }
input[type="text"], input[type="password"], input[type="number"], .login-form input{ input[type="text"], input[type="password"], input[type="number"], .login-form input{
height:30px; height:30px;
@ -98,8 +102,9 @@ form, .accounts-dialog{
background:$lightest-grey; background:$lightest-grey;
} }
textarea{ textarea{
min-height:100px; min-height:50px;
line-height:1.4; line-height:1.4;
font-size: 16px;
} }
.note{ .note{
padding: 5px 0; padding: 5px 0;
@ -119,23 +124,40 @@ input[type="submit"], button, .button, .btn{
font-size:15px; font-size:15px;
cursor:pointer; cursor:pointer;
margin:0; margin:0;
color: white;
// line-height:26px; // line-height:26px;
// height:26px; // height:26px;
font-weight:normal; font-weight:normal;
&.disabled{ &.disabled, &.loading{
background:$lighter-grey; background:$lighter-grey !important;
pointer-events:none; pointer-events:none;
color: $medium-text;
} }
&.inline{ &.inline{
display: inline-block; display: inline-block;
} }
&.btn-primary{ &.btn-primary{
background:$red; background:$red;
color:white;
&:link, &:hover, &:active, &:visited{ &:link, &:hover, &:active, &:visited{
color:white; color:white;
} }
} }
&.loading {
position: relative;
color: $lighter-grey !important;
&:after{
position: absolute;
top: 50%;
left: 50%;
margin: -10px 0 0 -10px;
content: " ";
display: block;
background: url('/img/loading.svg');
height: 20px;
width: 20px;
background-size: 20px 20px;
}
}
} }
input[type="search"]{ input[type="search"]{
font-size:14px; font-size:14px;

View file

@ -11,4 +11,9 @@
font-size: 13px; font-size: 13px;
line-height: 24px; line-height: 24px;
} }
}
.user-avatar {
.avatar-initials {
line-height: 40px;
}
} }

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",

View file

@ -68,7 +68,15 @@
} }
} }
.comment{ .comment{
margin-left:10px; margin-left:0px;
.comment{
@include small{
margin-left: 10px;
}
@include medium-large{
margin-left: 20px;
}
}
position:relative; position:relative;
padding: 0; padding: 0;
background: none; background: none;
@ -155,6 +163,10 @@
position:block; position:block;
float:left; float:left;
margin: 0 10px 0 10px; margin: 0 10px 0 10px;
.avatar{
height: 40px;
width: 40px;
}
// img{ // img{
// height:40px; // height:40px;
// width:40px; // width:40px;
@ -259,7 +271,9 @@
.comment-field{ .comment-field{
} }
.comment-submit{ .comment-submit{
@extend .cf; }
.comment-submit-button{
display: inline-block;
} }
.comment-page &{ .comment-page &{
margin-left:30px; margin-left:30px;

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",

View file

@ -2,7 +2,7 @@
"dependencies": [ "dependencies": [
[ [
"aldeed:simple-schema", "aldeed:simple-schema",
"1.1.0" "1.2.0"
], ],
[ [
"application-configuration", "application-configuration",
@ -82,19 +82,19 @@
], ],
[ [
"iron:core", "iron:core",
"1.0.3" "1.0.4"
], ],
[ [
"iron:dynamic-template", "iron:dynamic-template",
"1.0.3" "1.0.5"
], ],
[ [
"iron:layout", "iron:layout",
"1.0.3" "1.0.5"
], ],
[ [
"iron:location", "iron:location",
"1.0.3" "1.0.4"
], ],
[ [
"iron:middleware-stack", "iron:middleware-stack",
@ -106,7 +106,7 @@
], ],
[ [
"iron:url", "iron:url",
"1.0.3" "1.0.4"
], ],
[ [
"jquery", "jquery",

18
public/img/loading.svg Normal file
View file

@ -0,0 +1,18 @@
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24px" height="30px" viewBox="0 0 24 30" style="enable-background:new 0 0 50 50;" xml:space="preserve">
<rect x="0" y="10" width="4" height="10" fill="#333" opacity="0.2">
<animate attributeName="opacity" attributeType="XML" values="0.2; 1; .2" begin="0s" dur="1s" repeatCount="indefinite" />
<animate attributeName="height" attributeType="XML" values="10; 20; 10" begin="0s" dur="1s" repeatCount="indefinite" />
<animate attributeName="y" attributeType="XML" values="10; 5; 10" begin="0s" dur="1s" repeatCount="indefinite" />
</rect>
<rect x="8" y="10" width="4" height="10" fill="#333" opacity="0.2">
<animate attributeName="opacity" attributeType="XML" values="0.2; 1; .2" begin="0.15s" dur="1s" repeatCount="indefinite" />
<animate attributeName="height" attributeType="XML" values="10; 20; 10" begin="0.15s" dur="1s" repeatCount="indefinite" />
<animate attributeName="y" attributeType="XML" values="10; 5; 10" begin="0.15s" dur="1s" repeatCount="indefinite" />
</rect>
<rect x="16" y="10" width="4" height="10" fill="#333" opacity="0.2">
<animate attributeName="opacity" attributeType="XML" values="0.2; 1; .2" begin="0.3s" dur="1s" repeatCount="indefinite" />
<animate attributeName="height" attributeType="XML" values="10; 20; 10" begin="0.3s" dur="1s" repeatCount="indefinite" />
<animate attributeName="y" attributeType="XML" values="10; 5; 10" begin="0.3s" dur="1s" repeatCount="indefinite" />
</rect>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,8 +1,9 @@
// Publish a list of posts // Publish a list of posts
Meteor.publish('postsList', function(terms) { Meteor.publish('postsList', function(terms) {
var user = Meteor.users.findOne(this.userId);
if(canViewById(this.userId)){ if(canViewById(this.userId)){
var parameters = getPostsParameters(terms), var parameters = getPostsParameters(terms, user),
posts = Posts.find(parameters.find, parameters.options); posts = Posts.find(parameters.find, parameters.options);
// console.log('//-------- Subscription Parameters:'); // console.log('//-------- Subscription Parameters:');

3
server/start_cron.js Normal file
View file

@ -0,0 +1,3 @@
Meteor.startup(function() {
SyncedCron.start();
});