merging with master

This commit is contained in:
Sacha Greif 2013-11-21 11:41:04 +09:00
commit b2afbe6ac7
36 changed files with 698 additions and 314 deletions

View file

@ -1,3 +1,12 @@
## v0.7.3
* Refactored notifications.
* Added notifications for new users creation.
## v0.7.2
* Added basic search (thanks Ry!).
## v0.7.1 ## v0.7.1
* Added karma redistribution. * Added karma redistribution.

View file

@ -140,6 +140,7 @@ var filters = {
canView: function() { canView: function() {
if(Session.get('settingsLoaded') && !canView()){ if(Session.get('settingsLoaded') && !canView()){
console.log('cannot view')
this.render('no_rights'); this.render('no_rights');
this.stop(); this.stop();
} }
@ -174,7 +175,6 @@ var filters = {
hasCompletedProfile: function() { hasCompletedProfile: function() {
var user = Meteor.user(); var user = Meteor.user();
if (user && ! Meteor.loggingIn() && ! userProfileComplete(user)){ if (user && ! Meteor.loggingIn() && ! userProfileComplete(user)){
// Session.set('selectedUserId', user._id);
this.render('user_email'); this.render('user_email');
this.stop(); this.stop();
} }
@ -186,7 +186,14 @@ var filters = {
Router.load( function () { Router.load( function () {
clearSeenErrors(); // set all errors who have already been seen to not show anymore clearSeenErrors(); // set all errors who have already been seen to not show anymore
Session.set('categorySlug', null); Session.set('categorySlug', null);
// if we're not on the search page itself, clear search query and field
if(getCurrentRoute().indexOf('search') == -1){
Session.set('searchQuery', '');
$('.search-field').val('').blur();
}
}); });
// Before Hooks // Before Hooks
@ -198,7 +205,8 @@ Router.before(filters.nProgressHook, {only: [
'posts_best', 'posts_best',
'posts_pending', 'posts_pending',
'posts_digest', 'posts_digest',
'posts_category', 'posts_category',
'search',
'post_page', 'post_page',
'post_edit', 'post_edit',
'comment_page', 'comment_page',
@ -209,9 +217,9 @@ Router.before(filters.nProgressHook, {only: [
'all-users' 'all-users'
]}); ]});
Router.before(filters.canView);
Router.before(filters.hasCompletedProfile); Router.before(filters.hasCompletedProfile);
Router.before(filters.isLoggedIn, {only: ['comment_reply','post_submit']}); Router.before(filters.isLoggedIn, {only: ['comment_reply','post_submit']});
Router.before(filters.canView);
Router.before(filters.isLoggedOut, {only: ['signin', 'signup']}); Router.before(filters.isLoggedOut, {only: ['signin', 'signup']});
Router.before(filters.canPost, {only: ['posts_pending', 'comment_reply', 'post_submit']}); Router.before(filters.canPost, {only: ['posts_pending', 'comment_reply', 'post_submit']});
Router.before(filters.canEditPost, {only: ['post_edit']}); Router.before(filters.canEditPost, {only: ['post_edit']});
@ -240,30 +248,24 @@ PostsListController = RouteController.extend({
template:'posts_list', template:'posts_list',
waitOn: function () { waitOn: function () {
// take the first segment of the path to get the view, unless it's '/' in which case the view default to 'top' // take the first segment of the path to get the view, unless it's '/' in which case the view default to 'top'
var view = this.path == '/' ? 'top' : this.path.split('/')[1];
var limit = this.params.limit || getSetting('postsPerPage', 10);
// note: most of the time this.params.slug will be empty // note: most of the time this.params.slug will be empty
var parameters = getParameters(view, limit, this.params.slug); this._terms = {
view: this.path == '/' ? 'top' : this.path.split('/')[1],
limit: this.params.limit || getSetting('postsPerPage', 10),
category: this.params.slug,
query: Session.get("searchQuery")
}
return [ return [
Meteor.subscribe('postsList', parameters.find, parameters.options), Meteor.subscribe('postsList', this._terms),
Meteor.subscribe('postsListUsers', parameters.find, parameters.options) Meteor.subscribe('postsListUsers', this._terms)
] ]
}, },
data: function () { data: function () {
var view = this.path == '/' ? 'top' : this.path.split('/')[1], var parameters = getParameters(this._terms),
limit = this.params.limit || getSetting('postsPerPage', 10),
parameters = getParameters(view, limit, this.params.slug),
posts = Posts.find(parameters.find, parameters.options); posts = Posts.find(parameters.find, parameters.options);
postsCount = posts.count(); postsCount = posts.count();
Session.set('postsLimit', limit); Session.set('postsLimit', this._terms.limit);
// get posts and decorate them with rank property
// note: not actually used;
// posts = posts.map(function (post, index) {
// post.rank = index;
// return post;
// });
return { return {
postsList: posts, postsList: posts,
@ -282,16 +284,25 @@ PostsDigestController = RouteController.extend({
template: 'posts_digest', template: 'posts_digest',
waitOn: function() { waitOn: function() {
// if day is set, use that. If not default to today // if day is set, use that. If not default to today
var currentDate = this.params.day ? new Date(this.params.year, this.params.month-1, this.params.day) : Session.get('today'); var currentDate = this.params.day ? new Date(this.params.year, this.params.month-1, this.params.day) : Session.get('today'),
var parameters = getDigestParameters(currentDate); terms = {
view: 'digest',
after: moment(currentDate).startOf('day').valueOf(),
before: moment(currentDate).endOf('day').valueOf()
}
return [ return [
Meteor.subscribe('postsList', parameters.find, parameters.options), Meteor.subscribe('postsList', terms),
Meteor.subscribe('postsListUsers', parameters.find, parameters.options) Meteor.subscribe('postsListUsers', terms)
] ]
}, },
data: function() { data: function() {
var currentDate = this.params.day ? new Date(this.params.year, this.params.month-1, this.params.day) : Session.get('today'); var currentDate = this.params.day ? new Date(this.params.year, this.params.month-1, this.params.day) : Session.get('today'),
var parameters = getDigestParameters(currentDate); terms = {
view: 'digest',
after: moment(currentDate).startOf('day').valueOf(),
before: moment(currentDate).endOf('day').valueOf()
},
parameters = getParameters(terms);
Session.set('currentDate', currentDate); Session.set('currentDate', currentDate);
return { return {
posts: Posts.find(parameters.find, parameters.options) posts: Posts.find(parameters.find, parameters.options)
@ -349,8 +360,13 @@ UserPageController = RouteController.extend({
data: function() { data: function() {
var findById = Meteor.users.findOne(this.params._idOrSlug); var findById = Meteor.users.findOne(this.params._idOrSlug);
var findBySlug = Meteor.users.findOne({slug: this.params._idOrSlug}); var findBySlug = Meteor.users.findOne({slug: this.params._idOrSlug});
return { if(typeof findById !== "undefined"){
user: (typeof findById == "undefined") ? findBySlug : findById // redirect to slug-based URL
Router.go(getProfileUrl(findById), {replaceState: true});
}else{
return {
user: (typeof findById == "undefined") ? findBySlug : findById
}
} }
} }
}); });
@ -361,6 +377,8 @@ UserPageController = RouteController.extend({
Router.map(function() { Router.map(function() {
// -------------------------------------------- Post Lists -------------------------------------------- //
// Top // Top
this.route('posts_top', { this.route('posts_top', {
@ -406,6 +424,12 @@ Router.map(function() {
// TODO: enable /category/new, /category/best, etc. views // TODO: enable /category/new, /category/best, etc. views
// Search
this.route('search', {
path: '/search/:limit?',
controller: PostsListController
});
// Digest // Digest
@ -520,9 +544,8 @@ Router.map(function() {
path: '/all-users/:limit?', path: '/all-users/:limit?',
template: 'users', template: 'users',
waitOn: function() { waitOn: function() {
var limit = parseInt(this.params.limit) || 20, var limit = parseInt(this.params.limit) || 20;
parameters = getUsersParameters(this.params.filterBy, this.params.sortBy, limit); return Meteor.subscribe('allUsers', this.params.filterBy, this.params.sortBy, limit);
return Meteor.subscribe('allUsers', parameters.find, parameters.options);
}, },
data: function() { data: function() {
var limit = parseInt(this.params.limit) || 20, var limit = parseInt(this.params.limit) || 20,

View file

@ -2,7 +2,7 @@
Session.set('initialLoad', true); Session.set('initialLoad', true);
Session.set('today', new Date()); Session.set('today', new Date());
Session.set('view', 'top'); Session.set('view', 'top');
Session.set('postsLimit', getSetting('postsPerPage', 2)); Session.set('postsLimit', getSetting('postsPerPage', 10));
Session.set('settingsLoaded', false); Session.set('settingsLoaded', false);
Session.set('sessionId', Meteor.default_connection._lastSessionId); Session.set('sessionId', Meteor.default_connection._lastSessionId);

View file

@ -112,7 +112,7 @@
height:26px; height:26px;
margin-right:10px; margin-right:10px;
cursor:pointer; cursor:pointer;
color:white; color:white;
&:before{ &:before{
color:white(0.5); color:white(0.5);
content:"Welcome, "; content:"Welcome, ";
@ -136,6 +136,7 @@
>li{ >li{
@include horizontal-list-item; @include horizontal-list-item;
margin-right:10px; margin-right:10px;
>a{ >a{
color:white; color:white;
font-size:16px; font-size:16px;
@ -153,4 +154,30 @@
} }
} }
} }
}
.search{
position: relative;
.search-field{
font-size: 14px;
padding: 4px 12px;
line-height: 1.3;
border-radius: 20px;
border: 0px;
width: 100px;
background: white;
&:focus{
outline:none;
}
}
&.empty{
.search-field{
background: white(0.1);
&:focus{
background: white;
}
}
.search-clear{
display: none;
}
}
} }

View file

@ -3,13 +3,16 @@
background: $lightest-grey; background: $lightest-grey;
@include border-radius(3px); @include border-radius(3px);
padding: 8px 15px; padding: 8px 15px;
font-size: 14px; font-size: 13px;
a, span{ a, span{
display: inline-block; display: inline-block;
margin-right: 20px; margin-right: 20px;
&.active{ &.active{
border-bottom: 2px solid $blue; border-bottom: 2px solid $blue;
} }
&:last-child{
margin-right: 0;
}
} }
.filter{ .filter{
float: left; float: left;
@ -54,4 +57,10 @@ table{
} }
} }
}
.user-table{
font-size: 13px;
tr{
border-bottom: 1px solid #eee;
}
} }

View file

@ -34,4 +34,4 @@
@import "partials/mobile"; @import "partials/mobile";
@import "themes/default"; @import "themes/default";
@import "themes/telescope"; @import "themes/telescope";

View file

@ -822,20 +822,45 @@ body.pageslide-open {
/* line 50, ../../../../.rvm/gems/ruby-1.9.3-p194/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/typography/lists/_horizontal-list.scss */ /* line 50, ../../../../.rvm/gems/ruby-1.9.3-p194/gems/compass-0.12.2/frameworks/compass/stylesheets/compass/typography/lists/_horizontal-list.scss */
.nav > li.last { .nav > li.last {
padding-right: 0; } padding-right: 0; }
/* line 139, ../sass/modules/_header.scss */ /* line 140, ../sass/modules/_header.scss */
.nav > li > a { .nav > li > a {
color: white; color: white;
font-size: 16px; font-size: 16px;
line-height: 26px; line-height: 26px;
height: 26px; height: 26px;
font-weight: normal; } font-weight: normal; }
/* line 147, ../sass/modules/_header.scss */ /* line 148, ../sass/modules/_header.scss */
.nav > li > a.intercom em:before { .nav > li > a.intercom em:before {
content: '('; } content: '('; }
/* line 150, ../sass/modules/_header.scss */ /* line 151, ../sass/modules/_header.scss */
.nav > li > a.intercom em:after { .nav > li > a.intercom em:after {
content: ')'; } content: ')'; }
/* line 158, ../sass/modules/_header.scss */
.search {
position: relative; }
/* line 160, ../sass/modules/_header.scss */
.search .search-field {
font-size: 14px;
padding: 4px 12px;
line-height: 1.3;
border-radius: 20px;
border: 0px;
width: 100px;
background: white; }
/* line 168, ../sass/modules/_header.scss */
.search .search-field:focus {
outline: none; }
/* line 173, ../sass/modules/_header.scss */
.search.empty .search-field {
background: rgba(255, 255, 255, 0.1); }
/* line 175, ../sass/modules/_header.scss */
.search.empty .search-field:focus {
background: white; }
/* line 179, ../sass/modules/_header.scss */
.search.empty .search-clear {
display: none; }
/* line 1, ../sass/modules/_posts.scss */ /* line 1, ../sass/modules/_posts.scss */
.empty-notice { .empty-notice {
text-align: center; text-align: center;
@ -1621,7 +1646,7 @@ input[type="submit"], button, .button, .auth-buttons #login-buttons #login-butto
-o-border-radius: 3px; -o-border-radius: 3px;
border-radius: 3px; border-radius: 3px;
padding: 8px 15px; padding: 8px 15px;
font-size: 14px; } font-size: 13px; }
/* line 3, ../sass/partials/_mixins.scss */ /* line 3, ../sass/partials/_mixins.scss */
.filter-sort:before, .filter-sort:after { .filter-sort:before, .filter-sort:after {
content: ""; content: "";
@ -1636,14 +1661,17 @@ input[type="submit"], button, .button, .auth-buttons #login-buttons #login-butto
/* line 10, ../sass/modules/_users.scss */ /* line 10, ../sass/modules/_users.scss */
.filter-sort a.active, .filter-sort span.active { .filter-sort a.active, .filter-sort span.active {
border-bottom: 2px solid #7ac0e4; } border-bottom: 2px solid #7ac0e4; }
/* line 14, ../sass/modules/_users.scss */ /* line 13, ../sass/modules/_users.scss */
.filter-sort a:last-child, .filter-sort span:last-child {
margin-right: 0; }
/* line 17, ../sass/modules/_users.scss */
.filter-sort .filter { .filter-sort .filter {
float: left; } float: left; }
/* line 17, ../sass/modules/_users.scss */ /* line 20, ../sass/modules/_users.scss */
.filter-sort .sort { .filter-sort .sort {
float: right; } float: right; }
/* line 24, ../sass/modules/_users.scss */ /* line 27, ../sass/modules/_users.scss */
.user-list .user .user-avatar, .user-table .user .user-avatar { .user-list .user .user-avatar, .user-table .user .user-avatar {
height: 30px; height: 30px;
width: 30px; width: 30px;
@ -1655,13 +1683,20 @@ input[type="submit"], button, .button, .auth-buttons #login-buttons #login-butto
-o-border-radius: 30px; -o-border-radius: 30px;
border-radius: 30px; } border-radius: 30px; }
/* line 38, ../sass/modules/_users.scss */ /* line 41, ../sass/modules/_users.scss */
table tr td { table tr td {
padding: 10px; } padding: 10px; }
/* line 44, ../sass/modules/_users.scss */ /* line 47, ../sass/modules/_users.scss */
table thead tr td { table thead tr td {
font-weight: bold; } font-weight: bold; }
/* line 61, ../sass/modules/_users.scss */
.user-table {
font-size: 13px; }
/* line 63, ../sass/modules/_users.scss */
.user-table tr {
border-bottom: 1px solid #eee; }
/* line 2, ../sass/modules/_user-profile.scss */ /* line 2, ../sass/modules/_user-profile.scss */
.user-profile .user-avatar { .user-profile .user-avatar {
height: 80px; height: 80px;

View file

@ -29,8 +29,10 @@ Template.settings.events = {
throwError("Settings have been created"); throwError("Settings have been created");
}, },
function(error) { function(error) {
if(error) console.log(error); if(error)
throwError("Settings have been updated"); console.log(error);
throwError("Settings have been updated");
} }
); );
} }

View file

@ -1,13 +1,6 @@
<template name="toolbox"> <template name="toolbox">
<div class="grid-small grid-block dialog admin toolbox"> <div class="grid-small grid-block dialog admin toolbox">
<h2>Toolbox</h2> <h2>Toolbox</h2>
<h4>Updates</h4>
<ul>
<!-- <li><a href="#" class="update-categories">Update Categories</a></li> -->
<li><a href="#" class="update-user-profiles">Update User Profiles</a></li>
<!-- <li><a href="#" class="update-posts-slugs">Update Posts Slugs</a></li> -->
</ul>
<h4>Tools</h4>
<ul> <ul>
<li><a href="#" class="give-invites">Give 1 Invite to Everybody</a></li> <li><a href="#" class="give-invites">Give 1 Invite to Everybody</a></li>
</ul> </ul>

View file

@ -2,13 +2,20 @@
<header class="header grid"> <header class="header grid">
<a id="mobile-menu" href="#menu" class="mobile mobile-button menu"><i class="icon-menu"></i><span>{{i18n "Menu"}}</span></a> <a id="mobile-menu" href="#menu" class="mobile mobile-button menu"><i class="icon-menu"></i><span>{{i18n "Menu"}}</span></a>
<ul class="nav site-nav desktop"> <ul class="nav site-nav desktop">
{{#if canView}} {{#if canView}}
<li><a class="top" href="/top">{{i18n "Top"}}</a></li> <li class="dropdown">
<li><a class="new" href="/new">{{i18n "New"}}</a></li> <a class="View" href="/">{{i18n "View"}}</a>
<li><a class="best" href="/best">{{i18n "Best"}}</a></li> <div class="dropdown-menu">
<li><a class="digest" href="/digest">{{i18n "Digest"}}</a></li> <ul role="menu" aria-labelledby="dLabel">
<li><a class="top" href="/top">{{i18n "Top"}}</a></li>
<li><a class="new" href="/new">{{i18n "New"}}</a></li>
<li><a class="best" href="/best">{{i18n "Best"}}</a></li>
<li><a class="digest" href="/digest">{{i18n "Digest"}}</a></li>
</ul>
</div>
</li>
{{/if}} {{/if}}
{{#if hasCategories}} {{#if hasCategories}}
<li class="dropdown"> <li class="dropdown">
@ -36,8 +43,11 @@
</div> </div>
</li> </li>
{{/if}} {{/if}}
<li>
{{> search}}
</li>
</ul> </ul>
{{#if logo_url}} {{#if logo_url}}
<h1 class="logo image" style="height:{{logo_height}}px; width:{{logo_width}}px; top:{{logo_top}}px; margin-left:{{logo_offset}}px;"> <h1 class="logo image" style="height:{{logo_height}}px; width:{{logo_width}}px; top:{{logo_top}}px; margin-left:{{logo_offset}}px;">
<a href="/"> <a href="/">
@ -47,13 +57,13 @@
{{else}} {{else}}
<h1 class="logo"><a href="/">{{site_title}}</a></h1> <h1 class="logo"><a href="/">{{site_title}}</a></h1>
{{/if}} {{/if}}
{{#if canView}} {{#if canView}}
<ul class="nav user-nav desktop"> <ul class="nav user-nav desktop">
<li><a id="submit" class="submit button" href="/submit">{{i18n "Post"}}</a></li> <li><a id="submit" class="submit button" href="/submit">{{i18n "Post"}}</a></li>
</ul> </ul>
{{/if}} {{/if}}
<div class="auth-buttons"> <div class="auth-buttons">
{{loginButtons align="right"}} {{loginButtons align="right"}}
</div> </div>

View file

@ -1,27 +1,3 @@
Template.nav.events = {
'click #logout': function(event){
event.preventDefault();
Meteor.logout();
},
'click #mobile-menu': function(event){
event.preventDefault();
$('body').toggleClass('mobile-nav-open');
},
'click .login-header': function(e){
e.preventDefault();
Router.go('/account');
}
};
Template.nav.rendered=function(){
if(!Meteor.user()){
$('.login-link-text').text(i18n.t("Sign Up/Sign In"));
}else{
$('#login-buttons-logout').before('<a href="/users/'+Meteor.user().slug+'" class="account-link button">'+i18n.t("View Profile")+'</a>');
$('#login-buttons-logout').before('<a href="/account" class="account-link button">'+i18n.t("Edit Account")+'</a>');
}
};
Template.nav.helpers({ Template.nav.helpers({
site_title: function(){ site_title: function(){
return getSetting('title'); return getSetting('title');
@ -59,4 +35,29 @@ Template.nav.helpers({
categoryLink: function () { categoryLink: function () {
return getCategoryUrl(this.slug); return getCategoryUrl(this.slug);
} }
}); });
Template.nav.rendered=function(){
if(!Meteor.user()){
$('.login-link-text').text("Sign Up/Sign In");
}else{
$('#login-buttons-logout').before('<a href="/users/'+Meteor.user().slug+'" class="account-link button">View Profile</a>');
$('#login-buttons-logout').before('<a href="/account" class="account-link button">Edit Account</a>');
}
};
Template.nav.events = {
'click #logout': function(e){
e.preventDefault();
Meteor.logout();
},
'click #mobile-menu': function(e){
e.preventDefault();
$('body').toggleClass('mobile-nav-open');
},
'click .login-header': function(e){
e.preventDefault();
Router.go('/account');
}
};

View file

@ -0,0 +1,5 @@
<template name="search">
<div class="search {{searchQueryEmpty}}">
<input id="search" type="search" class="search-field" placeholder="search" value="{{searchQuery}}">
</div>
</template>

View file

@ -0,0 +1,43 @@
Template.search.helpers({
searchQuery: function () {
return Session.get("searchQuery");
},
searchQueryEmpty: function () {
return !!Session.get("searchQuery") ? '' : 'empty';
}
});
Template.search.preserve({
'input#search': function (node) { return node.id; }
});
Template.search.events = {
'keyup, search .search-field': function(e){
e.preventDefault();
var val = $(e.target).val(),
$search = $('.search');
if(val==''){
// if search field is empty, just do nothing and show an empty template
$search.addClass('empty');
Session.set('searchQuery', '');
}else{
// if search field is not empty, add a delay to avoid firing new searches for every keystroke
delay(function(){
Session.set('searchQuery', val);
$search.removeClass('empty');
// if we're not already on the search page, go to it
if(getCurrentRoute().indexOf('search') == -1)
Router.go('/search');
}, 500 );
}
}
};
// see: http://stackoverflow.com/questions/1909441/jquery-keyup-delay
var delay = (function(){
var timer = 0;
return function(callback, ms){
clearTimeout (timer);
timer = setTimeout(callback, ms);
};
})();

View file

@ -6,7 +6,7 @@ Template.notification_item.helpers({
return this.properties; return this.properties;
}, },
notificationHTML: function(){ notificationHTML: function(){
return getNotification(this.event, this.properties).html; return getNotificationContents(this).html;
} }
}); });

View file

@ -60,7 +60,8 @@ Template.post_item.helpers({
return _.include(this.upvoters, user._id); return _.include(this.upvoters, user._id);
}, },
userAvatar: function(){ userAvatar: function(){
if(author=Meteor.users.findOne(this.userId), {reactive: false}) var author = Meteor.users.findOne(this.userId, {reactive: false});
if(!!author)
return getAvatarUrl(author); return getAvatarUrl(author);
}, },
inactiveClass: function(){ inactiveClass: function(){

View file

@ -67,6 +67,11 @@
<div class="control-group"> <div class="control-group">
<label class="control-label">{{i18n "Email Notifications"}}</label> <label class="control-label">{{i18n "Email Notifications"}}</label>
<div class="controls"> <div class="controls">
{{#if isAdmin}}
<label class="checkbox">
<input id="notifications_users" type="checkbox" name="notifications_users" {{hasNotificationsUsers}} /> New Users
</label>
{{/if}}
<label class="checkbox"> <label class="checkbox">
<input id="notifications_posts" type="checkbox" name="notifications_posts" {{hasNotificationsPosts}} /> {{i18n "New Posts"}} <input id="notifications_posts" type="checkbox" name="notifications_posts" {{hasNotificationsPosts}} /> {{i18n "New Posts"}}
</label> </label>
@ -83,7 +88,7 @@
<h3>Invites</h3> <h3>Invites</h3>
<label>Invites</label> <label>Invites</label>
<div class="controls"> <div class="controls">
<input name="invitesCount" type="text" value="{{invitesCount}}" /> <input name="inviteCount" type="text" value="{{inviteCount}}" />
</div> </div>
</div> </div>
{{/if}} {{/if}}

View file

@ -17,14 +17,17 @@ Template.user_edit.helpers({
profileUrl: function(){ profileUrl: function(){
return Meteor.absoluteUrl()+"users/"+this.slug; return Meteor.absoluteUrl()+"users/"+this.slug;
}, },
hasNotificationsUsers : function(){
return getUserSetting('notifications.users', '', this) ? 'checked' : '';
},
hasNotificationsPosts : function(){ hasNotificationsPosts : function(){
return getUserSetting('notifications.posts') ? 'checked' : ''; return getUserSetting('notifications.notifications_posts', '', this) ? 'checked' : '';
}, },
hasNotificationsComments : function(){ hasNotificationsComments : function(){
return getUserSetting('notifications.comments') ? 'checked' : ''; return getUserSetting('notifications.comments', '', this) ? 'checked' : '';
}, },
hasNotificationsReplies : function(){ hasNotificationsReplies : function(){
return getUserSetting('notifications.replies') ? 'checked' : ''; return getUserSetting('notifications.replies', '', this) ? 'checked' : '';
} }
}) })
@ -45,10 +48,11 @@ Template.user_edit.events = {
"profile.twitter": $target.find('[name=twitter]').val(), "profile.twitter": $target.find('[name=twitter]').val(),
"profile.github": $target.find('[name=github]').val(), "profile.github": $target.find('[name=github]').val(),
"profile.site": $target.find('[name=site]').val(), "profile.site": $target.find('[name=site]').val(),
"profile.notifications.users": $('input[name=notifications_users]:checked').length, // only actually used for admins
"profile.notifications.posts": $('input[name=notifications_posts]:checked').length, "profile.notifications.posts": $('input[name=notifications_posts]:checked').length,
"profile.notifications.comments": $('input[name=notifications_comments]:checked').length, "profile.notifications.comments": $('input[name=notifications_comments]:checked').length,
"profile.notifications.replies": $('input[name=notifications_replies]:checked').length, "profile.notifications.replies": $('input[name=notifications_replies]:checked').length,
"invitesCount": parseInt($target.find('[name=invitesCount]').val()) "inviteCount": parseInt($target.find('[name=inviteCount]').val())
}; };
var old_password = $target.find('[name=old_password]').val(); var old_password = $target.find('[name=old_password]').val();

View file

@ -1,3 +1,9 @@
Template.user_email.helpers({
user: function(){
return Meteor.user();
}
});
Template.user_email.events = { Template.user_email.events = {
'submit form': function(e){ 'submit form': function(e){
e.preventDefault(); e.preventDefault();
@ -26,16 +32,3 @@ Template.user_email.events = {
} }
}; };
Template.user_email.profileIncomplete = function() {
return Meteor.user() && !this.loading && !userProfileComplete(this);
}
Template.user_email.user = function(){
var current_user=Meteor.user();
if(Session.get('selectedUserId') && !current_user.loading && current_user.isAdmin){
return Meteor.users.findOne(Session.get('selectedUserId'));
}else{
return current_user;
}
}

View file

@ -1,14 +1,33 @@
<template name="user_item"> <template name="user_item">
<tr class="user"> <tr class="user">
<td><span class="user-avatar" style="background-image:url({{avatarUrl}});"></span></td> <td><span class="user-avatar" style="background-image:url({{avatarUrl}});"></span></td>
<td><a href="{{profileUrl}}">{{displayName}}</a></td> <td>
<a href="{{getProfileUrl}}">{{displayName}}</a>
<br/>
<a href="mailto:{{getEmail}}">{{getEmail}}</a>
</td>
<td>{{createdAtFormatted}}</td> <td>{{createdAtFormatted}}</td>
<td>{{getEmail}}</td>
<td>{{postCount}}</td> <td>{{postCount}}</td>
<td>{{commentCount}}</td> <td>{{commentCount}}</td>
<td>{{getKarma}}</td> <td>{{getKarma}}</td>
<td>{{#if isInvited}}<a class="uninvite-link" href="#"><i class="icon-check"></i>{{i18n "Uninvite"}}</a>{{else}}<a href="#" class="invite-link">{{i18n "Invite"}}</a>{{/if}}</td> <td>
<td>{{#if userIsAdmin}}<a class="unadmin-link" href="#"><i class="icon-check unadmin-link"></i>{{i18n "Unadmin"}}</a>{{else}}<a href="#" class="admin-link">{{i18n "Make admin"}}</a>{{/if}}</td> {{#if invites}}
<td><a class="delete-link" href="#">{{i18n "Delete User"}}</a></td> <h4>{{i18n "Invited"}} {{invitedCount}} {{i18n "users"}}:</h4>
<ul>
{{#each invites}}
<li><a href="{{getInvitedUserProfileUrl}}">{{invitedName}}</a></li>
{{/each}}
</ul>
{{/if}}
<p>({{inviteCount}} {{i18n "invites left"}})</p>
</td>
<td>{{#if isInvited}}<i class="icon-check"></i>{{/if}}</td>
<td>{{#if userIsAdmin}}<i class="icon-check"></i>{{/if}}</td>
<td>
<ul>
<li>{{#if isInvited}}<a class="uninvite-link" href="#">{{i18n "Uninvite"}}</a>{{else}}<a href="#" class="invite-link">{{i18n "Invite"}}</a>{{/if}}</li>
<li>{{#if userIsAdmin}}<a class="unadmin-link" href="#">{{i18n "Unadmin"}}</a>{{else}}<a href="#" class="admin-link">{{i18n "Make admin"}}</a>{{/if}}</li>
<li><a class="delete-link" href="#">{{i18n "Delete User"}}</a></li>
</td>
</tr> </tr>
</template> </template>

View file

@ -20,35 +20,22 @@ Template.user_item.helpers({
userIsAdmin: function(){ userIsAdmin: function(){
return isAdmin(this); return isAdmin(this);
}, },
profileUrl: function () { getProfileUrl: function () {
return getProfileUrl(this); return getProfileUrl(this);
}, },
getKarma: function() { getKarma: function() {
return Math.round(100*this.karma)/100; return Math.round(100*this.karma)/100;
},
getInvitedUserProfileUrl: function () {
var user = Meteor.users.findOne(this.invitedId);
return getProfileUrl(user);
} }
}); });
Template.user_item.events({ Template.user_item.events({
'click .invite-link': function(e, instance){ 'click .invite-link': function(e, instance){
e.preventDefault(); e.preventDefault();
var user = Meteor.users.findOne(instance.data._id); Meteor.call('inviteUser', instance.data._id);
Meteor.users.update(user._id,{
$set:{
isInvited: true
}
}, {multi: false}, function(error){
if(error){
throwError();
}else{
Meteor.call('createNotification', {
event: 'accountApproved',
properties: {},
userToNotify: user,
userDoingAction: Meteor.user(),
sendEmail: getSetting("emailNotifications")
});
}
});
}, },
'click .uninvite-link': function(e, instance){ 'click .uninvite-link': function(e, instance){
e.preventDefault(); e.preventDefault();

View file

@ -6,16 +6,22 @@
<tr> <tr>
<td colspan="2"><img class="user-avatar" src="{{avatarUrl}}"/></td> <td colspan="2"><img class="user-avatar" src="{{avatarUrl}}"/></td>
</tr> </tr>
{{#if isAdmin}}
<tr>
<td>{{i18n "ID"}}: </td>
<td>{{_id}}</td>
</tr>
{{/if}}
<tr> <tr>
<td>{{i18n "Name"}}: </td> <td>{{i18n "Name:"}}</td>
<td>{{profile.name}}</td> <td>{{profile.name}}</td>
</tr> </tr>
<tr> <tr>
<td>{{i18n "Member since"}}: </td> <td>{{i18n "Member since"}}:</td>
<td>{{createdAtFormatted}}</td> <td>{{createdAtFormatted}}</td>
</tr> </tr>
<tr> <tr>
<td>{{i18n "Bio"}}: </td> <td>{{i18n "Bio:"}}</td>
<td>{{profile.bio}}</td> <td>{{profile.bio}}</td>
</tr> </tr>
{{#if getTwitterName}} {{#if getTwitterName}}
@ -26,13 +32,13 @@
{{/if}} {{/if}}
{{#if getGitHubName}} {{#if getGitHubName}}
<tr> <tr>
<td>GitHub: </td> <td>{{i18n "GitHub"}}:</td>
<td><a href="http://github.com/{{getGitHubName}}">{{getGitHubName}}</a></td> <td><a href="http://github.com/{{getGitHubName}}">{{getGitHubName}}</a></td>
</tr> </tr>
{{/if}} {{/if}}
{{#if site}} {{#if site}}
<tr> <tr>
<td>Site: </td> <td>{{i18n "Site"}}:</td>
<td><a href="{{profile.site}}">{{profile.site}}</a></td> <td><a href="{{profile.site}}">{{profile.site}}</a></td>
</tr> </tr>
{{/if}} {{/if}}
@ -41,8 +47,8 @@
<a class="button inline" href="/users/{{slug}}/edit">{{i18n "Edit profile"}}</a> <a class="button inline" href="/users/{{slug}}/edit">{{i18n "Edit profile"}}</a>
{{/if}} {{/if}}
{{#if canInvite}} {{#if canInvite}}
{{#if invitesCount}} {{#if inviteCount}}
<a class="button inline invite-link" href="#">{{i18n "Invite "}}({{invitesCount}} {{i18n "left"}})</a> <a class="button inline invite-link" href="#">{{i18n "Invite"}} ({{inviteCount}} {{i18n "left"}})</a>
{{else}} {{else}}
<a class="button inline disabled" href="#">{{i18n "Invite (none left)"}}</a> <a class="button inline disabled" href="#">{{i18n "Invite (none left)"}}</a>
{{/if}} {{/if}}

View file

@ -13,8 +13,8 @@ Template.user_profile.helpers({
// if the user is logged in, the target user hasn't been invited yet, invites are enabled, and user is not viewing their own profile // if the user is logged in, the target user hasn't been invited yet, invites are enabled, and user is not viewing their own profile
return Meteor.user() && Meteor.user()._id != this._id && !isInvited(this) && invitesEnabled() && canInvite(Meteor.user()); return Meteor.user() && Meteor.user()._id != this._id && !isInvited(this) && invitesEnabled() && canInvite(Meteor.user());
}, },
invitesCount: function() { inviteCount: function() {
return Meteor.user().invitesCount; return Meteor.user().inviteCount;
}, },
getTwitterName: function () { getTwitterName: function () {
return getTwitterName(this); return getTwitterName(this);
@ -27,5 +27,6 @@ Template.user_profile.helpers({
Template.user_profile.events({ Template.user_profile.events({
'click .invite-link': function(e, instance){ 'click .invite-link': function(e, instance){
Meteor.call('inviteUser', instance.data.user._id); Meteor.call('inviteUser', instance.data.user._id);
throwError('Thanks, user has been invited.')
} }
}); });

View file

@ -17,6 +17,7 @@
<a class="{{activeClass 'username'}}" href="{{sortBy 'username'}}">{{i18n "Username"}}</a> <a class="{{activeClass 'username'}}" href="{{sortBy 'username'}}">{{i18n "Username"}}</a>
<a class="{{activeClass 'postCount'}}" href="{{sortBy 'postCount'}}">{{i18n "Posts"}}</a> <a class="{{activeClass 'postCount'}}" href="{{sortBy 'postCount'}}">{{i18n "Posts"}}</a>
<a class="{{activeClass 'commentCount'}}" href="{{sortBy 'commentCount'}}">{{i18n "Comments"}}</a> <a class="{{activeClass 'commentCount'}}" href="{{sortBy 'commentCount'}}">{{i18n "Comments"}}</a>
<a class="{{activeClass 'invitedCount'}}" href="{{sortBy 'invitedCount'}}">{{i18n "InvitedCount"}}</a>
</p> </p>
</div> </div>
<table> <table>
@ -24,13 +25,13 @@
<tr> <tr>
<td colspan="2">{{i18n "Name"}}</td> <td colspan="2">{{i18n "Name"}}</td>
<td>{{i18n "Member since"}}</td> <td>{{i18n "Member since"}}</td>
<td>{{i18n "Email"}}</td>
<td>{{i18n "Posts"}}</td> <td>{{i18n "Posts"}}</td>
<td>{{i18n "Comments"}}</td> <td>{{i18n "Comments"}}</td>
<td>{{i18n "Karma"}}</td> <td>{{i18n "Karma"}}</td>
<td>{{i18n "Is Invited?"}}</td> <td>{{i18n "Invites"}}</td>
<td>{{i18n "Is Admin?"}}</td> <td>{{i18n "Invited?"}}</td>
<td>{{i18n "Delete"}}</td> <td>{{i18n "Admin?"}}</td>
<td>{{i18n "Actions"}}</td>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>

View file

@ -9,44 +9,71 @@ Notifications.allow({
, remove: canEditById , remove: canEditById
}); });
getNotification = function(event, properties, context){ getNotificationContents = function(notification, context){
var notification = {}; // the same notifications can be displayed in multiple contexts: on-site in the sidebar, sent by email, etc.
// the default context to display notifications is the notification sidebar var event = notification.event,
var context = typeof context === 'undefined' ? 'sidebar' : context; p = notification.properties,
var p = properties; context = typeof context === 'undefined' ? 'sidebar' : context,
userToNotify = Meteor.users.findOne(notification.userId);
switch(event){ switch(event){
case 'newReply': case 'newReply':
notification.subject = i18n.t('Someone replied to your comment on')+' "'+p.postHeadline+'"'; var n = {
notification.text = p.commentAuthorName+i18n.t(' has replied to your comment on')+' "'+p.postHeadline+'": '+getPostCommentUrl(p.postId, p.commentId); subject: 'Someone replied to your comment on "'+p.postHeadline+'"',
notification.html = '<p><a href="'+getUserUrl(p.commentAuthorId)+'">'+p.commentAuthorName+'</a>'+i18n.t(' has replied to your comment on')+' "<a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">'+p.postHeadline+'</a>"</p>'; text: p.commentAuthorName+' has replied to your comment on "'+p.postHeadline+'": '+getPostCommentUrl(p.postId, p.commentId),
if(context === 'email') html: '<p><a href="'+getProfileUrlById(p.commentAuthorId)+'">'+p.commentAuthorName+'</a> has replied to your comment on "<a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">'+p.postHeadline+'</a>"</p>'
notification.html += '<p>'+p.commentExcerpt+'</p><a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">'+i18n.t('Read more')+'</a>'; }
break; if(context == 'email')
n.html += '<p>'+p.commentExcerpt+'</p><a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">Read more</a>';
break;
case 'newComment': case 'newComment':
notification.subject = i18n.t('A new comment on your post')+' "'+p.postHeadline+'"'; var n = {
notification.text = i18n.t('You have a new comment by ')+p.commentAuthorName+i18n.t(' on your post')+' "'+p.postHeadline+'": '+getPostCommentUrl(p.postId, p.commentId); subject: 'A new comment on your post "'+p.postHeadline+'"',
notification.html = '<p><a href="'+getUserUrl(p.commentAuthorId)+'">'+p.commentAuthorName+'</a> left a new comment on your post "<a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">'+p.postHeadline+'</a>"</p>'; text: 'You have a new comment by '+p.commentAuthorName+' on your post "'+p.postHeadline+'": '+getPostCommentUrl(p.postId, p.commentId),
if(context === 'email') html: '<p><a href="'+getProfileUrlById(p.commentAuthorId)+'">'+p.commentAuthorName+'</a> left a new comment on your post "<a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">'+p.postHeadline+'</a>"</p>'
notification.html += '<p>'+p.commentExcerpt+'</p><a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">'+i18n.t('Read more')+'</a>'; }
break; if(context == 'email')
n.html += '<p>'+p.commentExcerpt+'</p><a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">Read more</a>';
break;
case 'newPost': case 'newPost':
notification.subject = p.postAuthorName+i18n.t(' has created a new post')+': "'+p.postHeadline+'"'; var n = {
notification.text = p.postAuthorName+i18n.t(' has created a new post')+': "'+p.postHeadline+'" '+getPostUrl(p.postId); subject: p.postAuthorName+' has created a new post: "'+p.postHeadline+'"',
notification.html = '<a href="'+getUserUrl(p.postAuthorId)+'">'+p.postAuthorName+'</a>'+i18n.t(' has created a new post')+': "<a href="'+getPostUrl(p.postId)+'" class="action-link">'+p.postHeadline+'</a>".'; text: p.postAuthorName+' has created a new post: "'+p.postHeadline+'" '+getPostUrl(p.postId),
break; html: '<a href="'+getProfileUrlById(p.postAuthorId)+'">'+p.postAuthorName+'</a> has created a new post: "<a href="'+getPostUrl(p.postId)+'" class="action-link">'+p.postHeadline+'</a>".'
}
break;
case 'accountApproved': case 'accountApproved':
notification.subject = i18n.t('Your account has been approved.'); var n = {
notification.text = i18n.t('Welcome to ')+getSetting('title')+'! '+i18n.t('Your account has just been approved.'); subject: 'Your account has been approved.',
notification.html = i18n.t('Welcome to ')+getSetting('title')+'!<br/> '+i18n.t('Your account has just been approved.')+' <a href="'+Meteor.absoluteUrl()+'">'+i18n.t('Start posting.')+'</a>'; text: 'Welcome to '+getSetting('title')+'! Your account has just been approved.',
break; html: 'Welcome to '+getSetting('title')+'!<br/> Your account has just been approved. <a href="'+Meteor.absoluteUrl()+'">Start posting.</a>'
}
break;
case 'newUser':
var n = {
subject: 'New user: '+p.username,
text: 'A new user account has been created: '+p.profileUrl,
html: 'A new user account has been created: <a href="'+p.profileUrl+'">'+p.username+'</a>'
}
break;
default: default:
break; break;
} }
return notification;
// if context is email, append unsubscribe link to all outgoing notifications
if(context == 'email'){
n.to = getEmail(userToNotify);
n.text = n.text + '\n\n Unsubscribe from all notifications: '+getUnsubscribeLink(userToNotify);
n.html = n.html + '<br/><br/><a href="'+getUnsubscribeLink(userToNotify)+'">Unsubscribe from all notifications</a>';
}
return n;
} }
Meteor.methods({ Meteor.methods({

View file

@ -97,15 +97,17 @@ Meteor.methods({
if(getSetting('emailNotifications', false)){ if(getSetting('emailNotifications', false)){
// notify users of new posts // notify users of new posts
var properties = { var notification = {
postAuthorName : getDisplayName(postAuthor), event: 'newPost',
postAuthorId : post.userId, properties: {
postHeadline : headline, postAuthorName : getDisplayName(postAuthor),
postId : postId postAuthorId : post.userId,
postHeadline : headline,
postId : postId
}
} }
var notification = getNotification('newPost', properties); // call a server method because we do not have access to users' info on the client
// call a server method because we do not have access to admin users' info on the client Meteor.call('newPostNotify', notification, function(error, result){
Meteor.call('notifyUsers', notification, Meteor.user(), function(error, result){
//run asynchronously //run asynchronously
}); });
} }

92
lib/deepExtend.js Normal file
View file

@ -0,0 +1,92 @@
// see: http://stackoverflow.com/questions/9399365/deep-extend-like-jquerys-for-nodejs
deepExtend = function () {
var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false,
toString = Object.prototype.toString,
hasOwn = Object.prototype.hasOwnProperty,
push = Array.prototype.push,
slice = Array.prototype.slice,
trim = String.prototype.trim,
indexOf = Array.prototype.indexOf,
class2type = {
"[object Boolean]": "boolean",
"[object Number]": "number",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Date]": "date",
"[object RegExp]": "regexp",
"[object Object]": "object"
},
jQuery = {
isFunction: function (obj) {
return jQuery.type(obj) === "function"
},
isArray: Array.isArray ||
function (obj) {
return jQuery.type(obj) === "array"
},
isWindow: function (obj) {
return obj != null && obj == obj.window
},
isNumeric: function (obj) {
return !isNaN(parseFloat(obj)) && isFinite(obj)
},
type: function (obj) {
return obj == null ? String(obj) : class2type[toString.call(obj)] || "object"
},
isPlainObject: function (obj) {
if (!obj || jQuery.type(obj) !== "object" || obj.nodeType) {
return false
}
try {
if (obj.constructor && !hasOwn.call(obj, "constructor") && !hasOwn.call(obj.constructor.prototype, "isPrototypeOf")) {
return false
}
} catch (e) {
return false
}
var key;
for (key in obj) {}
return key === undefined || hasOwn.call(obj, key)
}
};
if (typeof target === "boolean") {
deep = target;
target = arguments[1] || {};
i = 2;
}
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {}
}
if (length === i) {
target = this;
--i;
}
for (i; i < length; i++) {
if ((options = arguments[i]) != null) {
for (name in options) {
src = target[name];
copy = options[name];
if (target === copy) {
continue
}
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false;
clone = src && jQuery.isArray(src) ? src : []
} else {
clone = src && jQuery.isPlainObject(src) ? src : {};
}
// WARNING: RECURSION
target[name] = deepExtend(deep, clone, copy);
} else if (copy !== undefined) {
target[name] = copy;
}
}
}
}
return target;
}

View file

@ -8,6 +8,9 @@ getSetting = function(setting, defaultValue){
getCurrentTemplate = function() { getCurrentTemplate = function() {
return Router._currentController.template; return Router._currentController.template;
} }
getCurrentRoute = function() {
return Router._currentController.path;
}
clearSeenErrors = function(){ clearSeenErrors = function(){
Errors.update({seen:true}, {$set: {show:false}}, {multi:true}); Errors.update({seen:true}, {$set: {show:false}}, {multi:true});
} }
@ -68,18 +71,18 @@ getPostCommentUrl = function(postId, commentId){
// get link to a comment on a post page // get link to a comment on a post page
return Meteor.absoluteUrl()+'posts/'+postId+'/comment/'+commentId; return Meteor.absoluteUrl()+'posts/'+postId+'/comment/'+commentId;
} }
getUserUrl = function(id){
return Meteor.absoluteUrl()+'users/'+id;
}
getCategoryUrl = function(slug){ getCategoryUrl = function(slug){
return Meteor.absoluteUrl()+'category/'+slug; return Meteor.absoluteUrl()+'category/'+slug;
} }
slugify = function(text) { slugify = function(text) {
text = text.replace(/[^-a-zA-Z0-9,&\s]+/ig, ''); if(text){
text = text.replace(/-/gi, "_"); text = text.replace(/[^-a-zA-Z0-9,&\s]+/ig, '');
text = text.replace(/\s/gi, "-"); text = text.replace(/-/gi, "_");
text = text.toLowerCase(); text = text.replace(/\s/gi, "-");
text = text.toLowerCase();
}
return text; return text;
} }
getShortUrl = function(post){ getShortUrl = function(post){
return post.shortUrl ? post.shortUrl : post.url; return post.shortUrl ? post.shortUrl : post.url;

View file

@ -1,7 +1,12 @@
// getParameters gives an object containing the appropriate find and options arguments for the subscriptions's Posts.find() // getParameters gives an object containing the appropriate find and options arguments for the subscriptions's Posts.find()
getParameters = function (view, limit, category) { getParameters = function (terms) {
// console.log(terms)
// note: using jquery's extend() with "deep" parameter set to true instead of shallow _.extend()
// see: http://api.jquery.com/jQuery.extend/
var baseParameters = { var baseParameters = {
find: { find: {
@ -12,64 +17,74 @@ getParameters = function (view, limit, category) {
} }
} }
switch (view) { switch (terms.view) {
case 'top': case 'top':
var parameters = $.extend(true, baseParameters, {options: {sort: {sticky: -1, score: -1}}}); var parameters = deepExtend(true, baseParameters, {options: {sort: {sticky: -1, score: -1}}});
break; break;
case 'new': case 'new':
var parameters = $.extend(true, baseParameters, {options: {sort: {sticky: -1, submitted: -1}}}); var parameters = deepExtend(true, baseParameters, {options: {sort: {sticky: -1, submitted: -1}}});
break; break;
case 'best': case 'best':
var parameters = $.extend(true, baseParameters, {options: {sort: {sticky: -1, baseScore: -1}}}); var parameters = deepExtend(true, baseParameters, {options: {sort: {sticky: -1, baseScore: -1}}});
break; break;
case 'pending': case 'pending':
var parameters = $.extend(true, baseParameters, {find: {status: 1}, options: {sort: {createdAt: -1}}}); var parameters = deepExtend(true, baseParameters, {find: {status: 1}, options: {sort: {createdAt: -1}}});
break; break;
case 'category': // same as top for now case 'category': // same as top for now
var parameters = $.extend(true, baseParameters, {options: {sort: {sticky: -1, score: -1}}}); var parameters = deepExtend(true, baseParameters, {options: {sort: {sticky: -1, score: -1}}});
break; break;
case 'search': // search results
if(typeof terms.query != 'undefined' && !!terms.query){
var parameters = deepExtend(true, baseParameters, {
find: {
$or: [
{headline: {$regex: terms.query, $options: 'i'}},
{url: {$regex: terms.query, $options: 'i'}},
{body: {$regex: terms.query, $options: 'i'}}
]
}
});
}else{
// if query is empty, just return parameters that will result in an empty collection
var parameters = {find:{_id: 0}};
}
break;
case 'digest':
var parameters = deepExtend(true, baseParameters, {
find: {
submitted: {
$gte: terms.after,
$lt: terms.before
}
},
options: {
sort: {sticky: -1, baseScore: -1}
}
});
break;
} }
// sort by _id to break ties // sort by _id to break ties
$.extend(true, parameters, {options: {sort: {_id: -1}}}) deepExtend(true, parameters, {options: {sort: {_id: -1}}})
if(typeof limit != 'undefined') if(typeof terms.limit != 'undefined' && !!terms.limit)
_.extend(parameters.options, {limit: parseInt(limit)}); _.extend(parameters.options, {limit: parseInt(terms.limit)});
if(typeof category != 'undefined') if(typeof terms.category != 'undefined' && !!terms.category)
_.extend(parameters.find, {'categories.slug': category}); _.extend(parameters.find, {'categories.slug': terms.category});
// console.log(parameters.options.sort) // console.log(parameters)
return parameters; return parameters;
} }
// Special case for digest
// TODO: merge back into general getParameters function
getDigestParameters = function (date) {
var mDate = moment(date);
var parameters = {
find: {
status: 2,
submitted: {
$gte: mDate.startOf('day').valueOf(),
$lt: mDate.endOf('day').valueOf()
}
},
options: {
sort: {sticky: -1, baseScore: -1, _id: 1}
}
};
return parameters;
}
getUsersParameters = function(filterBy, sortBy, limit) { getUsersParameters = function(filterBy, sortBy, limit) {
var find = {}, var find = {},
sort = {createdAt: -1}; sort = {createdAt: -1};
@ -99,6 +114,8 @@ getUsersParameters = function(filterBy, sortBy, limit) {
break; break;
case 'commentCount': case 'commentCount':
sort = {commentCount: -1}; sort = {commentCount: -1};
case 'invitedCount':
sort = {invitedCount: -1};
} }
return { return {
find: find, find: find,

View file

@ -1,4 +1,4 @@
updateScore = function (item, collection, forceUpdate) { updateScore = function (collection, item, forceUpdate) {
var forceUpdate = typeof forceUpdate !== 'undefined' ? forceUpdate : false; var forceUpdate = typeof forceUpdate !== 'undefined' ? forceUpdate : false;
// For performance reasons, the database is only updated if the difference between the old score and the new score // For performance reasons, the database is only updated if the difference between the old score and the new score
// is meaningful enough. To find out, we calculate the "power" of a single vote after n days. // is meaningful enough. To find out, we calculate the "power" of a single vote after n days.

View file

@ -24,7 +24,13 @@ getDisplayNameById = function(userId){
return getDisplayName(Meteor.users.findOne(userId)); return getDisplayName(Meteor.users.findOne(userId));
} }
getProfileUrl = function(user) { getProfileUrl = function(user) {
return '/users/' + slugify(getUserName(user)); return Meteor.absoluteUrl()+'users/' + slugify(getUserName(user));
}
getProfileUrlById = function(id){
return Meteor.absoluteUrl()+'users/'+ id;
}
getProfileUrlBySlug = function(slug) {
return Meteor.absoluteUrl()+'users/' + slug;
} }
getTwitterName = function(user){ getTwitterName = function(user){
// return twitter name provided by user, or else the one used for twitter login // return twitter name provided by user, or else the one used for twitter login
@ -67,7 +73,7 @@ getAvatarUrl = function(user){
}else{ }else{
return Gravatar.getGravatar(user, { return Gravatar.getGravatar(user, {
d: 'http://demo.telesc.pe/img/default_avatar.png', d: 'http://demo.telesc.pe/img/default_avatar.png',
s: 30 s: 80
}); });
} }
} }
@ -115,4 +121,4 @@ getProperty = function(object, property){
// else return property // else return property
return object[array[0]]; return object[array[0]];
} }
} }

View file

@ -1,21 +1,19 @@
Meteor.methods({ Meteor.methods({
inviteUser: function (userId) { inviteUser: function (userId) {
var currentUser = Meteor.user(); var currentUser = Meteor.user();
var invitedUser = Meteor.users.findOne(userId); var invitedUser = Meteor.users.findOne(userId);
var invite = { var invite = {
invited: invitedUser._id, invitedId: invitedUser._id,
invitedName: getDisplayName(invitedUser), invitedName: getDisplayName(invitedUser),
time: new Date() time: new Date()
}; };
// if the current user is logged in, still has available invites and is himself invited (or an admin), and the target user is not invited // if the current user is logged in, still has available invites and is himself invited (or an admin), and the target user is not invited
if(currentUser && currentUser.invitesCount > 0 && canInvite(currentUser) && !isInvited(invitedUser)){ if(currentUser && currentUser.inviteCount > 0 && canInvite(currentUser) && !isInvited(invitedUser)){
// update invinting user // update invinting user
Meteor.users.update(Meteor.userId(), {$inc:{invitesCount: -1}}); Meteor.users.update(Meteor.userId(), {$inc:{inviteCount: -1}, $inc:{invitedCount: 1}, $push:{invites: invite}});
Meteor.users.update(Meteor.userId(), {$push:{invites: invite}});
// update invited user // update invited user
Meteor.users.update(userId, {$set: { Meteor.users.update(userId, {$set: {

View file

@ -106,6 +106,39 @@ Meteor.startup(function () {
console.log("//----------------------------------------------------------------------//") console.log("//----------------------------------------------------------------------//")
} }
// migration updateUserProfiles: update user profiles with slugs and a few other properties
if (!Migrations.findOne({name: "updateUserProfiles"})) {
console.log("//----------------------------------------------------------------------//")
console.log("//------------// Starting updateUserProfiles Migration //-----------//")
console.log("//----------------------------------------------------------------------//")
var allUsers = Meteor.users.find();
console.log('> Found '+allUsers.count()+' users.\n');
allUsers.forEach(function(user){
console.log('> Updating user '+user._id+' ('+user.username+')');
// update user slug
if(getUserName(user))
Meteor.users.update(user._id, {$set:{slug: slugify(getUserName(user))}});
// update user isAdmin flag
if(typeof user.isAdmin === 'undefined')
Meteor.users.update(user._id, {$set: {isAdmin: false}});
// update postCount
var postsByUser = Posts.find({userId: user._id});
Meteor.users.update(user._id, {$set: {postCount: postsByUser.count()}});
// update commentCount
var commentsByUser = Comments.find({userId: user._id});
Meteor.users.update(user._id, {$set: {commentCount: commentsByUser.count()}});
});
Migrations.insert({name: "updateUserProfiles"});
console.log("//----------------------------------------------------------------------//")
console.log("//------------// Ending updateUserProfiles Migration //-----------//")
console.log("//----------------------------------------------------------------------//")
}
}); });

View file

@ -14,7 +14,7 @@ Meteor.methods({
// console.log(userDoingAction); // console.log(userDoingAction);
// console.log(properties); // console.log(properties);
// console.log(sendEmail); // console.log(sendEmail);
var notification= { var notification = {
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
userId: userToNotify._id, userId: userToNotify._id,
event: event, event: event,
@ -26,38 +26,45 @@ Meteor.methods({
// send the notification if notifications are activated, // send the notification if notifications are activated,
// the notificationsFrequency is set to 1, or if it's undefined (legacy compatibility) // the notificationsFrequency is set to 1, or if it's undefined (legacy compatibility)
if(sendEmail){ if(sendEmail){
Meteor.call('sendNotificationEmail', userToNotify, newNotificationId); // get specific notification content for "email" context
var contents = getNotificationContents(notification, 'email');
sendNotification(contents);
} }
}, },
sendNotificationEmail : function(userToNotify, notificationId){
// Note: we query the DB instead of simply passing arguments from the client
// to make sure our email method cannot be used for spam
var notification = Notifications.findOne(notificationId);
var n = getNotification(notification.event, notification.properties, 'email');
var to = getEmail(userToNotify);
var text = n.text + '\n\n Unsubscribe from all notifications: '+getUnsubscribeLink(userToNotify);
var html = n.html + '<br/><br/><a href="'+getUnsubscribeLink(userToNotify)+'">Unsubscribe from all notifications</a>';
sendEmail(to, n.subject, text, html);
},
unsubscribeUser : function(hash){ unsubscribeUser : function(hash){
// TO-DO: currently, if you have somebody's email you can unsubscribe them // TO-DO: currently, if you have somebody's email you can unsubscribe them
// A site-specific salt should be added to the hashing method to prevent this // A user-specific salt should be added to the hashing method to prevent this
var user = Meteor.users.findOne({email_hash: hash}); var user = Meteor.users.findOne({email_hash: hash});
if(user){ if(user){
var update = Meteor.users.update(user._id, { var update = Meteor.users.update(user._id, {
$set: {'profile.notificationsFrequency' : 0} $set: {
'profile.notifications.users' : 0,
'profile.notifications.posts' : 0,
'profile.notifications.comments' : 0,
'profile.notifications.replies' : 0
}
}); });
return true; return true;
} }
return false; return false;
}, },
notifyUsers : function(notification, currentUser){ newPostNotify : function(properties){
var currentUser = Meteor.users.findOne(this.userId);
console.log('newPostNotify')
// send a notification to every user according to their notifications settings // send a notification to every user according to their notifications settings
_.each(Meteor.users.find().fetch(), function(user, index, list){ Meteor.users.find().forEach(function(user) {
// don't send users notifications for their own posts
if(user._id !== currentUser._id && getUserSetting('notifications.posts', false, user)){ if(user._id !== currentUser._id && getUserSetting('notifications.posts', false, user)){
// don't send users notifications for their own posts properties.userId = user._id;
sendEmail(getEmail(user), notification.subject, notification.text, notification.html); var notification = getNotificationContents(properties, 'email');
sendNotification(notification, user);
} }
}); });
} }
}); });
sendNotification = function (notification) {
// console.log('send notification:')
// console.log(notification)
sendEmail(notification.to, notification.subject, notification.text, notification.html);
}

View file

@ -3,7 +3,7 @@ var privacyOptions = { // false means private
isAdmin: false, isAdmin: false,
emails: false, emails: false,
notifications: false, notifications: false,
invitesCount: false, inviteCount: false,
'profile.email': false, 'profile.email': false,
'services.twitter.accessToken': false, 'services.twitter.accessToken': false,
'services.twitter.accessTokenSecret': false, 'services.twitter.accessTokenSecret': false,
@ -31,6 +31,7 @@ Meteor.publish('singleUser', function(userIdOrSlug) {
// if we find something when treating the argument as an ID, return that; else assume it's a slug // if we find something when treating the argument as an ID, return that; else assume it's a slug
return findById.count() ? findById : findBySlug; return findById.count() ? findById : findBySlug;
} }
return [];
}); });
// Publish authors of the current post and its comments // Publish authors of the current post and its comments
@ -51,6 +52,7 @@ Meteor.publish('postUsers', function(postId) {
return Meteor.users.find({_id: {$in: users}}, {fields: privacyOptions}); return Meteor.users.find({_id: {$in: users}}, {fields: privacyOptions});
} }
return [];
}); });
// Publish author of the current comment // Publish author of the current comment
@ -60,34 +62,42 @@ Meteor.publish('commentUser', function(commentId) {
var comment = Comments.findOne(commentId); var comment = Comments.findOne(commentId);
return Meteor.users.find({_id: comment && comment.userId}, {fields: privacyOptions}); return Meteor.users.find({_id: comment && comment.userId}, {fields: privacyOptions});
} }
return [];
}); });
// Publish all the users that have posted the currently displayed list of posts // Publish all the users that have posted the currently displayed list of posts
Meteor.publish('postsListUsers', function(find, options) { Meteor.publish('postsListUsers', function(terms) {
if(canViewById(this.userId)){ if(canViewById(this.userId)){
var posts = Posts.find(find, options); var parameters = getParameters(terms),
var userIds = _.pluck(posts.fetch(), 'userId'); posts = Posts.find(parameters.find, parameters.options),
userIds = _.pluck(posts.fetch(), 'userId');
return Meteor.users.find({_id: {$in: userIds}}, {fields: privacyOptions, multi: true}); return Meteor.users.find({_id: {$in: userIds}}, {fields: privacyOptions, multi: true});
} }
return [];
}); });
// Publish all users // Publish all users
Meteor.publish('allUsers', function(find, options) { Meteor.publish('allUsers', function(filterBy, sortBy, limit) {
if(canViewById(this.userId)){ if(canViewById(this.userId)){
var parameters = getUsersParameters(filterBy, sortBy, limit);
if (!isAdminById(this.userId)) // if user is not admin, filter out sensitive info if (!isAdminById(this.userId)) // if user is not admin, filter out sensitive info
options = _.extend(options, {fields: privacyOptions}); parameters.options = _.extend(parameters.options, {fields: privacyOptions});
return Meteor.users.find(find, options); return Meteor.users.find(parameters.find, parameters.options);
} }
return [];
}); });
// publish all users for admins to make autocomplete work // publish all users for admins to make autocomplete work
// TODO: find a better way // TODO: find a better way
Meteor.publish('allUsersAdmin', function() { Meteor.publish('allUsersAdmin', function() {
if (isAdminById(this.userId)) if (isAdminById(this.userId)) {
return Meteor.users.find(); return Meteor.users.find();
} else {
return [];
}
}); });
// -------------------------------------------- Posts -------------------------------------------- // // -------------------------------------------- Posts -------------------------------------------- //
@ -98,6 +108,7 @@ Meteor.publish('singlePost', function(id) {
if(canViewById(this.userId)){ if(canViewById(this.userId)){
return Posts.find(id); return Posts.find(id);
} }
return [];
}); });
// Publish the post related to the current comment // Publish the post related to the current comment
@ -107,29 +118,26 @@ Meteor.publish('commentPost', function(commentId) {
var comment = Comments.findOne(commentId); var comment = Comments.findOne(commentId);
return Posts.find({_id: comment && comment.post}); return Posts.find({_id: comment && comment.post});
} }
return [];
}); });
// Publish a list of posts // Publish a list of posts
Meteor.publish('postsList', function(find, options) { Meteor.publish('postsList', function(terms) {
if(canViewById(this.userId)){ if(canViewById(this.userId)){
options = options || {}; var parameters = getParameters(terms),
var posts = Posts.find(find, options); posts = Posts.find(parameters.find, parameters.options);
// console.log('//-------- Subscription Parameters:'); // console.log('//-------- Subscription Parameters:');
// console.log(find); // console.log(parameters.find);
// console.log(options); // console.log(parameters.options);
// console.log('Found '+posts.fetch().length+ ' posts:'); // console.log('Found '+posts.fetch().length+ ' posts:');
// posts.rewind(); // posts.rewind();
// console.log(_.pluck(posts.fetch(), 'headline')); // console.log(_.pluck(posts.fetch(), 'headline'));
// posts.rewind();
return posts; return posts;
} }
return [];
}); });
// -------------------------------------------- Comments -------------------------------------------- // // -------------------------------------------- Comments -------------------------------------------- //
// Publish comments for a specific post // Publish comments for a specific post
@ -138,6 +146,7 @@ Meteor.publish('postComments', function(postId) {
if(canViewById(this.userId)){ if(canViewById(this.userId)){
return Comments.find({post: postId}); return Comments.find({post: postId});
} }
return [];
}); });
// Publish a single comment // Publish a single comment
@ -146,12 +155,22 @@ Meteor.publish('singleComment', function(commentId) {
if(canViewById(this.userId)){ if(canViewById(this.userId)){
return Comments.find(commentId); return Comments.find(commentId);
} }
return [];
}); });
// -------------------------------------------- Other -------------------------------------------- // // -------------------------------------------- Other -------------------------------------------- //
Meteor.publish('settings', function() { Meteor.publish('settings', function() {
return Settings.find(); var options = {};
if(!isAdminById(this.userId)){
options = _.extend(options, {
fields: {
mailChimpAPIKey: false,
mailChimpListId: false
}
});
}
return Settings.find({}, options);
}); });
Meteor.publish('notifications', function() { Meteor.publish('notifications', function() {
@ -159,10 +178,12 @@ Meteor.publish('notifications', function() {
if(canViewById(this.userId)){ if(canViewById(this.userId)){
return Notifications.find({userId:this.userId}); return Notifications.find({userId:this.userId});
} }
return [];
}); });
Meteor.publish('categories', function() { Meteor.publish('categories', function() {
if(canViewById(this.userId)){ if(canViewById(this.userId)){
return Categories.find(); return Categories.find();
} }
return [];
}); });

View file

@ -14,38 +14,6 @@ Meteor.methods({
}, },
giveInvites: function () { giveInvites: function () {
if(isAdmin(Meteor.user())) if(isAdmin(Meteor.user()))
Meteor.users.update({}, {$inc:{invitesCount: 1}}, {multi:true}); Meteor.users.update({}, {$inc:{inviteCount: 1}}, {multi:true});
},
updateUserProfiles: function () {
console.log('//--------------------------//\nUpdating user profiles…')
if(isAdmin(Meteor.user())){
var allUsers = Meteor.users.find();
console.log('> Found '+allUsers.count()+' users.\n');
allUsers.forEach(function(user){
console.log('> Updating user '+user._id+' ('+user.username+')');
// update user slug
if(getUserName(user))
Meteor.users.update(user._id, {$set:{slug: slugify(getUserName(user))}});
// update user isAdmin flag
if(typeof user.isAdmin === 'undefined')
Meteor.users.update(user._id, {$set: {isAdmin: false}});
// update postCount
var postsByUser = Posts.find({userId: user._id});
Meteor.users.update(user._id, {$set: {postCount: postsByUser.count()}});
// update commentCount
var commentsByUser = Comments.find({userId: user._id});
Meteor.users.update(user._id, {$set: {commentCount: commentsByUser.count()}});
});
}
console.log('Done updating user profiles.\n//--------------------------//')
},
updatePostsSlugs: function () {
//TODO
} }
}) })

View file

@ -1,19 +1,32 @@
Accounts.onCreateUser(function(options, user){ Accounts.onCreateUser(function(options, user){
user.profile = options.profile || {}; var userProperties = {
user.karma = 0; profile: options.profile || {},
// users start pending and need to be invited karma: 0,
user.isInvited = false; isInvited: false,
user.isAdmin = false; isAdmin: false,
postCount: 0,
commentCount: 0,
invitedCount: 0
}
user = _.extend(user, userProperties);
if (options.email) if (options.email)
user.profile.email = options.email; user.profile.email = options.email;
if (user.profile.email) if (getEmail(user))
user.email_hash = CryptoJS.MD5(user.profile.email.trim().toLowerCase()).toString(); user.email_hash = getEmailHash(user);
if (!user.profile.name) if (!user.profile.name)
user.profile.name = user.username; user.profile.name = user.username;
// set notifications default preferences
user.profile.notifications = {
users: false,
posts: false,
comments: true,
replies: true
}
// create slug from username // create slug from username
user.slug = slugify(getUserName(user)); user.slug = slugify(getUserName(user));
@ -22,16 +35,39 @@ Accounts.onCreateUser(function(options, user){
user.isAdmin = true; user.isAdmin = true;
// give new users a few invites (default to 3) // give new users a few invites (default to 3)
user.invitesCount = getSetting('startInvitesCount', 3); user.inviteCount = getSetting('startInvitesCount', 3);
trackEvent('new user', {username: user.username, email: user.profile.email}); trackEvent('new user', {username: user.username, email: user.profile.email});
// add new user to MailChimp list // if user has already filled in their email, add them to MailChimp list
addToMailChimpList(user); if(user.profile.email)
addToMailChimpList(user);
// send notifications to admins
var admins = Meteor.users.find({isAdmin: true});
admins.forEach(function(admin){
if(getUserSetting('notifications.users', false, admin)){
var notification = getNotificationContents({
event: 'newUser',
properties: {
username: getUserName(user),
profileUrl: getProfileUrl(user)
},
userId: admin._id
}, 'email');
sendNotification(notification, admin);
}
});
return user; return user;
}); });
getEmailHash = function(user){
// todo: add some kind of salt in here
return CryptoJS.MD5(getEmail(user).trim().toLowerCase() + user.createdAt).toString();
}
addToMailChimpList = function(user){ addToMailChimpList = function(user){
// add a user to a MailChimp list. // add a user to a MailChimp list.
// called when a new user is created, or when an existing user fills in their email // called when a new user is created, or when an existing user fills in their email
@ -76,9 +112,9 @@ Meteor.methods({
var newScore = baseScore / Math.pow(ageInHours + 2, 1.3); var newScore = baseScore / Math.pow(ageInHours + 2, 1.3);
return Math.abs(object.score - newScore); return Math.abs(object.score - newScore);
}, },
generateEmailHash: function(){ setEmailHash: function(user){
var email_hash = CryptoJS.MD5(getEmail(Meteor.user()).trim().toLowerCase()).toString(); var email_hash = CryptoJS.MD5(getEmail(user).trim().toLowerCase()).toString();
Meteor.users.update(Meteor.userId(), {$set : {email_hash : email_hash}}); Meteor.users.update(user._id, {$set : {email_hash : email_hash}});
}, },
addCurrentUserToMailChimpList: function(){ addCurrentUserToMailChimpList: function(){
addToMailChimpList(Meteor.user()); addToMailChimpList(Meteor.user());