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

View file

@ -6,40 +6,45 @@ Template[getTemplate('comment_form')].helpers({
Template[getTemplate('comment_form')].events({
'submit form': function(e, instance){
var $commentForm = instance.$('#comment');
e.preventDefault();
$(e.target).addClass('disabled');
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 comment = {};
var $commentForm = instance.$('#comment');
var $submitButton = instance.$('.btn-submit');
var body = $commentForm.val();
// now that the form is latency compensated, we don't actually need to show this
// $commentForm.prop('disabled', true);
// $submitButton.addClass('loading');
$commentForm.val('');
var post = postObject;
Meteor.call('comment', post._id, null, content, function(error, newComment){
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);
Session.set('scrollToCommentId', newComment._id);
$commentForm.val('');
}
});
}
}
});

View file

@ -1,7 +1,7 @@
<template name="post_edit">
<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 class="grid grid-module">

View file

@ -1,9 +1,10 @@
AutoForm.hooks({
editPostForm: {
onSubmit: function(insertDoc, updateDoc, currentDoc) {
var updateObject = updateDoc;
var submit = this;
before: {
editPost: function(doc, template) {
var post = doc;
// ------------------------------ Checks ------------------------------ //
@ -14,33 +15,18 @@ AutoForm.hooks({
// ------------------------------ Callbacks ------------------------------ //
// run all post edit client callbacks on updateObject object successively
updateObject = postEditClientCallbacks.reduce(function(result, currentFunction) {
// run all post edit client callbacks on post object successively
post = postEditClientCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, updateObject);
}, post);
// ------------------------------ 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 post;
}
});
return false
},
onSuccess: function(operation, result, template) {
// not used right now because I can't find a way to pass the "post" object to this callback
// console.log(result)
// trackEvent("new post", {'postId': post._id});
// if(post.status === STATUS_PENDING)
// throwError('Thanks, your post is awaiting approval.');
// Router.go('/posts/'+post._id);
onSuccess: function(operation, post, template) {
trackEvent("edit post", {'postId': post._id});
Router.go('post_page', {_id: post._id});
},
onError: function(operation, error, template) {
@ -49,16 +35,10 @@ AutoForm.hooks({
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({
'click .delete-link': function(e){
var post = this.post;

View file

@ -1,7 +1,7 @@
<template name="post_submit">
<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>
</template>

View file

@ -1,9 +1,12 @@
AutoForm.hooks({
submitPostForm: {
onSubmit: function(insertDoc, updateDoc, currentDoc) {
var properties = insertDoc;
var submit = this;
before: {
submitPost: function(doc, template) {
template.$('button[type=submit]').addClass('loading');
var post = doc;
// ------------------------------ Checks ------------------------------ //
@ -15,40 +18,25 @@ AutoForm.hooks({
// ------------------------------ Callbacks ------------------------------ //
// run all post submit client callbacks on properties object successively
properties = postSubmitClientCallbacks.reduce(function(result, currentFunction) {
post = postSubmitClientCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, properties);
}, post);
// console.log(properties)
return post;
}
},
// ------------------------------ Insert ------------------------------ //
Meteor.call('submitPost', properties, function(error, post) {
if(error){
submit.done(error);
}else{
// note: find a way to do this in onSuccess instead?
onSuccess: function(operation, post, template) {
template.$('button[type=submit]').removeClass('loading');
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
},
onSuccess: function(operation, result, template) {
// not used right now because I can't find a way to pass the "post" object to this callback
// console.log(post)
// 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) {
template.$('button[type=submit]').removeClass('loading');
flashMessage(error.message.split('|')[0], 'error'); // workaround because error.details returns undefined
clearSeenMessages();
// $(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,8 +87,12 @@ Comments.allow({
remove: canEditById
});
// ------------------------------------------------------------------------------------------- //
// ------------------------------------------ Hooks ------------------------------------------ //
// ------------------------------------------------------------------------------------------- //
Comments.before.insert(function (userId, doc) {
if(Meteor.isServer)
// note: only actually sanitizes on the server
doc.htmlBody = sanitize(marked(doc.body));
});
@ -100,43 +104,55 @@ Comments.before.update(function (userId, doc, fieldNames, modifier, options) {
}
});
Meteor.methods({
comment: function(postId, parentCommentId, text){
var user = Meteor.user(),
post = Posts.findOne(postId),
postUser = Meteor.users.findOne(post.userId),
timeSinceLastComment = timeSinceLast(user, Comments),
commentInterval = Math.abs(parseInt(getSetting('commentInterval',15))),
now = new Date();
commentAfterSubmitMethodCallbacks.push(function (comment) {
// check that user can comment
if (!user || !canComment(user))
throw new Meteor.Error(i18n.t('you_need_to_login_or_be_invited_to_post_new_comments'));
var userId = comment.userId,
commentAuthor = Meteor.users.findOne(userId);
// check that user waits more than 15 seconds between comments
if(!this.isSimulation && (timeSinceLastComment < commentInterval))
throw new Meteor.Error(704, i18n.t('please_wait')+(commentInterval-timeSinceLastComment)+i18n.t('seconds_before_commenting_again'));
// 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 (!text)
if (!comment.body)
throw new Meteor.Error(704,i18n.t('your_comment_is_empty'));
var comment = {
postId: postId,
body: text,
userId: user._id,
createdAt: now,
postedAt: now,
// ------------------------------ Properties ------------------------------ //
var defaultProperties = {
createdAt: new Date(),
postedAt: new Date(),
upvotes: 0,
downvotes: 0,
baseScore: 0,
score: 0,
author: getDisplayName(user)
author: getDisplayNameById(userId)
};
if(parentCommentId)
comment.parentCommentId = parentCommentId;
comment = _.extend(defaultProperties, comment);
// ------------------------------ Callbacks ------------------------------ //
@ -145,31 +161,76 @@ Meteor.methods({
return currentFunction(result);
}, comment);
// -------------------------------- Insert ------------------------------- //
// -------------------------------- Insert -------------------------------- //
comment._id = Comments.insert(comment);
// ------------------------------ Callbacks ------------------------------ //
// --------------------- 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);
// 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);
}, 1);
}
return comment;
}
// ------------------------------------------------------------------------------------------- //
// ----------------------------------------- Methods ----------------------------------------- //
// ------------------------------------------------------------------------------------------- //
Meteor.methods({
submitComment: function(comment){
// required properties:
// postId
// content
// optional properties:
// parentCommentId
var user = Meteor.user(),
hasAdminRights = isAdmin(user);
// ------------------------------ Checks ------------------------------ //
// check that user can comment
if (!user || !canComment(user))
throw new Meteor.Error(i18n.t('you_need_to_login_or_be_invited_to_post_new_comments'));
// ------------------------------ Rate Limiting ------------------------------ //
if (!hasAdminRights) {
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'));
}
// ------------------------------ Properties ------------------------------ //
// admin-only properties
// userId
// if user is not admin, clear restricted properties
if (!hasAdminRights) {
delete comment.userId;
}
// if no userId has been set, default to current user id
if (!comment.userId) {
comment.userId = user._id
}
return submitComment(comment);
},
removeComment: function(commentId){
var comment = Comments.findOne(commentId);

View file

@ -251,7 +251,8 @@ getPostProperties = function (post) {
// default status for new posts
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
return STATUS_APPROVED
} else {
@ -273,19 +274,26 @@ getPostLink = function (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
var sixMonthsAgo = moment().subtract(6, 'months').toDate();
var postWithSameLink = Posts.findOne({url: url, postedAt: {$gte: sixMonthsAgo}});
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
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 ------------------------------------------ //
// ------------------------------------------------------------------------------------------- //
@ -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 ----------------------------------------- //
// ------------------------------------------------------------------------------------------- //
@ -313,15 +401,19 @@ postViews = [];
Meteor.methods({
submitPost: function(post){
var title = cleanUp(post.title),
body = post.body,
userId = this.userId,
user = Meteor.users.findOne(userId),
timeSinceLastPost=timeSinceLast(user, Posts),
numberOfPostsInPast24Hours=numberOfItemsInPast24Hours(user, Posts),
postInterval = Math.abs(parseInt(getSetting('postInterval', 30))),
maxPostsPer24Hours = Math.abs(parseInt(getSetting('maxPostsPerDay', 30))),
postId = '';
// required properties:
// title
// optional properties
// URL
// body
// categories
// thumbnailUrl
// NOTE: the current user and the post author user might be two different users!
var user = Meteor.user(),
hasAdminRights = isAdmin(user);
// ------------------------------ Checks ------------------------------ //
@ -329,109 +421,58 @@ Meteor.methods({
if (!user || !canPost(user))
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 -------------------------- //
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
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'));
// 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'));
}
// ------------------------------ Properties ------------------------------ //
// Basic Properties
properties = {
title: title,
body: body,
userId: userId,
author: getDisplayNameById(userId),
upvotes: 0,
downvotes: 0,
commentCount: 0,
clickCount: 0,
viewCount: 0,
baseScore: 0,
score: 0,
inactive: false
};
// admin-only properties
// status
// postedAt
// userId
// sticky (default to false)
// UserId
if(isAdmin(Meteor.user()) && !!post.userId){ // only let admins post as other users
properties.userId = post.userId;
// if user is not admin, clear restricted properties
if (!hasAdminRights) {
delete post.status;
delete post.postedAt;
delete post.userId;
delete post.sticky;
}
// Status
if(!!post.status && isAdmin(Meteor.user())){
// if a custom status has been set, and user is admin, use that
properties.status = post.status;
}else{
// else use default status
properties.status = getDefaultPostStatus(Meteor.user());
// if no post status has been set, set it now
if (!post.status) {
post.status = getDefaultPostStatus(user);
}
// CreatedAt
properties.createdAt = new Date();
// 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();
}
// if no userId has been set, default to current user id
if (!post.userId) {
post.userId = user._id
}
post = _.extend(post, properties);
// ------------------------------ 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;
return submitPost(post);
},
editPost: function (postId, updateObject) {
editPost: function (post, modifier, postId) {
var user = Meteor.user();
// console.log(updateObject)
// ------------------------------ Checks ------------------------------ //
// check that user can edit
@ -440,23 +481,21 @@ Meteor.methods({
// ------------------------------ Callbacks ------------------------------ //
// run all post submit server callbacks on updateObject successively
updateObject = postEditMethodCallbacks.reduce(function(result, currentFunction) {
// run all post submit server callbacks on modifier successively
modifier = postEditMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, updateObject);
console.log(updateObject)
}, modifier);
// ------------------------------ Update ------------------------------ //
Posts.update(postId, updateObject);
Posts.update(postId, modifier);
// ------------------------------ Callbacks ------------------------------ //
// run all post submit server callbacks on updateObject successively
updateObject = postAfterEditMethodCallbacks.reduce(function(result, currentFunction) {
// run all post submit server callbacks on modifier object successively
modifier = postAfterEditMethodCallbacks.reduce(function(result, currentFunction) {
return currentFunction(result);
}, updateObject);
}, modifier);
// ------------------------------ After Update ------------------------------ //

View file

@ -81,7 +81,7 @@ getPostCommentUrl = function(postId, commentId){
slugify = function(text) {
if(text){
text = text.replace(/[^-_a-zA-Z0-9,&\s]+/ig, '');
text = text.replace(/\s/gi, "+");
text = text.replace(/\s/gi, "-");
text = text.toLowerCase();
}
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 = function (terms) {
getPostsParameters = function (terms, user) {
var hasAdminRights = typeof user !== 'undefined' && isAdmin(user);
var maxLimit = 200;
// console.log(terms)
@ -24,6 +27,11 @@ getPostsParameters = function (terms) {
if (typeof terms.limit !== 'undefined')
_.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
if (!parameters.showFuture && !parameters.find.postedAt)
parameters.find.postedAt = {$lte: new Date()};

View file

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

View file

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

View file

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

View file

@ -1,46 +1,45 @@
// returns how much "power" a user's votes have
var getVotePower = function (user) {
// returns how much "power" a user's votes have
var getVotePower = function (user) {
// return isAdmin(user) ? 5 : 1;
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}});
};
};
var hasUpvotedItem = function (item, user) {
var hasUpvotedItem = function (item, user) {
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;
};
};
var addVote = function (userId, vote, collection, upOrDown) {
var addVote = function (userId, vote, collection, upOrDown) {
var field = 'votes.' + upOrDown + 'voted' + collection;
var add = {};
add[field] = vote;
var result = Meteor.users.update({_id: userId}, {
$addToSet: add
});
};
};
var removeVote = function (userId, itemId, collection, upOrDown) {
var removeVote = function (userId, itemId, collection, upOrDown) {
var field = 'votes.' + upOrDown + 'voted' + collection;
var remove = {};
remove[field] = {itemId: itemId};
Meteor.users.update({_id: userId}, {
$pull: remove
});
};
};
var upvoteItem = function (collection, item) {
var user = Meteor.user(),
upvoteItem = 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);
// make sure user has rights to upvote first
if (!user || !canUpvote(user, collection, true) || hasUpvotedItem(item, user))
return false;
@ -85,10 +84,10 @@
}
// console.log(collection.findOne(item._id));
return true;
};
};
var downvoteItem = function (collection, item) {
var user = Meteor.user(),
downvoteItem = 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);
@ -125,10 +124,10 @@
}
// console.log(collection.findOne(item._id));
return true;
};
};
var cancelUpvote = function (collection, item) {
var user = Meteor.user(),
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);
@ -157,10 +156,10 @@
}
// console.log(collection.findOne(item._id));
return true;
};
};
var cancelDownvote = function (collection, item) {
var user = Meteor.user(),
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);
@ -189,38 +188,38 @@
}
// 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) {
// 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) {
Meteor.methods({
upvotePost: function (post) {
return upvoteItem.call(this, Posts, post);
},
downvotePost: function (post, user) {
downvotePost: function (post) {
return downvoteItem.call(this, Posts, post);
},
cancelUpvotePost: function (post, user) {
cancelUpvotePost: function (post) {
return cancelUpvote.call(this, Posts, post);
},
cancelDownvotePost: function (post, user) {
cancelDownvotePost: function (post) {
return cancelDownvote.call(this, Posts, post);
},
upvoteComment: function (comment, user) {
upvoteComment: function (comment) {
return upvoteItem.call(this, Comments, comment);
},
downvoteComment: function (comment, user) {
downvoteComment: function (comment) {
return downvoteItem.call(this, Comments, comment);
},
cancelUpvoteComment: function (comment, user) {
cancelUpvoteComment: function (comment) {
return cancelUpvote.call(this, Comments, comment);
},
cancelDownvoteComment: function (comment, user) {
cancelDownvoteComment: function (comment) {
return cancelDownvote.call(this, Comments, comment);
}
});
});

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ Package.describe({
Package.onUse(function(api) {
api.use('templating@1.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('jquery');
// api.use('tsega:bootstrap3-datetimepicker');

View file

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

View file

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

View file

@ -40,7 +40,7 @@ $small-break: 30em;
height: 36px;
width: 36px;
margin: -18px 0 0 -18px;
line-height: 32px;
line-height: 38px;
text-align: center;
color: white;
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
var addMediaOnSubmit = function (post) {
// For security reason, we use a separate server-side API call to set the media object,
// 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){
var data = getEmbedlyData(post.url);
if(!!data && !!data.media.html)
post.media = data.media
if (!!data) {
// 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;
}
postSubmitMethodCallbacks.push(addMediaOnSubmit);
postAfterSubmitMethodCallbacks.push(addMediaAfterSubmit);
// TODO: find a way to only do this is URL has actually changed?
var updateMediaOnEdit = function (updateObject) {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,24 +1,9 @@
<template name="categories">
<div class="grid-small grid-module dialog admin">
<h2>Categories</h2>
<form class="form-block">
<h3>Add new category</h3>
<div class="control-group">
<label>Name</label>
<div class="controls"><input id="name" type="text" value="" /></div>
<div class="grid-small grid-module dialog admin add-category">
<h3>Add new category:</h3>
{{> quickForm collection="Categories" id="insertCategoryForm" type="insert" label-class="control-label" input-col-class="controls" template="telescope"}}
</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>
</template>

View file

@ -1,15 +1,6 @@
<template name="categoryItem">
<li>
<form>
<div class="control-group inline">
<a class="button submit edit-link btn" href="#">Save</a>
<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 class="grid-small grid-module">
{{> quickForm collection="Categories" id=formId type="update" doc=this label-class="control-label" input-col-class="controls" template="telescope"}}
<a href="#" class="delete-link">Delete</a>
</div>
<span class="category-slug small">Slug: /{{slug}}</span>
</div>
</form>
</li>
</template>

View file

@ -1,16 +1,15 @@
Meteor.startup(function () {
Template[getTemplate('categoryItem')].helpers({
formId: function () {
return 'updateCategory-'+ this._id
}
});
Template[getTemplate('categoryItem')].events({
'click .edit-link': function(e, instance){
'click .delete-link': function(e, instance){
e.preventDefault();
var categoryId = instance.data._id;
var name = $('#name_'+categoryId).val();
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);
if (confirm("Delete category?")) {
Categories.remove(instance.data._id);
}
}
});

View file

@ -1,29 +1,34 @@
// category schema
categorySchema = new SimpleSchema({
_id: {
name: {
type: String
},
description: {
type: String,
optional: true
optional: true,
autoform: {
rows: 3
}
},
order: {
type: Number,
optional: true
},
slug: {
type: String
},
name: {
type: String
},
description: {
type: String,
optional: true,
autoform: {
rows: 5
}
}
});
Categories = new Meteor.Collection("categories", {
schema: categorySchema
Categories = new Meteor.Collection("categories");
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
@ -80,9 +85,9 @@ addToPostSchema.push(
Meteor.startup(function () {
Categories.allow({
insert: isAdminById
, update: isAdminById
, remove: isAdminById
insert: isAdminById,
update: isAdminById,
remove: isAdminById
});
Meteor.methods({

View file

@ -6,8 +6,10 @@ Package.onUse(function (api) {
'telescope-lib',
'telescope-base',
'aldeed:simple-schema',
'aldeed:autoform',
'tap:i18n',
'fourseven:scss'
'fourseven:scss',
'matb33:collection-hooks'
], ['client', 'server']);
api.use([
@ -46,5 +48,12 @@ Package.onUse(function (api) {
"i18n/zh-CN.i18n.json",
], ["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": [
[
"aldeed:autoform",
"4.2.0"
],
[
"aldeed:simple-schema",
"1.1.0"
"1.2.0"
],
[
"application-configuration",
@ -82,19 +86,19 @@
],
[
"iron:core",
"1.0.3"
"1.0.4"
],
[
"iron:dynamic-template",
"1.0.3"
"1.0.5"
],
[
"iron:layout",
"1.0.3"
"1.0.5"
],
[
"iron:location",
"1.0.3"
"1.0.4"
],
[
"iron:middleware-stack",
@ -106,7 +110,7 @@
],
[
"iron:url",
"1.0.3"
"1.0.4"
],
[
"jquery",
@ -116,10 +120,18 @@
"json",
"1.0.1"
],
[
"livedata",
"1.0.11"
],
[
"logging",
"1.0.5"
],
[
"matb33:collection-hooks",
"0.7.6"
],
[
"meteor",
"1.1.3"
@ -136,6 +148,10 @@
"mongo",
"1.0.9"
],
[
"mrt:moment",
"2.8.1"
],
[
"observe-sequence",
"1.0.3"

View file

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

View file

@ -12,3 +12,8 @@
line-height: 24px;
}
}
.user-avatar {
.avatar-initials {
line-height: 40px;
}
}

View file

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

View file

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

View file

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

View file

@ -2,7 +2,7 @@
"dependencies": [
[
"aldeed:simple-schema",
"1.1.0"
"1.2.0"
],
[
"application-configuration",
@ -82,19 +82,19 @@
],
[
"iron:core",
"1.0.3"
"1.0.4"
],
[
"iron:dynamic-template",
"1.0.3"
"1.0.5"
],
[
"iron:layout",
"1.0.3"
"1.0.5"
],
[
"iron:location",
"1.0.3"
"1.0.4"
],
[
"iron:middleware-stack",
@ -106,7 +106,7 @@
],
[
"iron:url",
"1.0.3"
"1.0.4"
],
[
"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
Meteor.publish('postsList', function(terms) {
var user = Meteor.users.findOne(this.userId);
if(canViewById(this.userId)){
var parameters = getPostsParameters(terms),
var parameters = getPostsParameters(terms, user),
posts = Posts.find(parameters.find, parameters.options);
// console.log('//-------- Subscription Parameters:');

3
server/start_cron.js Normal file
View file

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