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
* Added karma redistribution.

View file

@ -140,6 +140,7 @@ var filters = {
canView: function() {
if(Session.get('settingsLoaded') && !canView()){
console.log('cannot view')
this.render('no_rights');
this.stop();
}
@ -174,7 +175,6 @@ var filters = {
hasCompletedProfile: function() {
var user = Meteor.user();
if (user && ! Meteor.loggingIn() && ! userProfileComplete(user)){
// Session.set('selectedUserId', user._id);
this.render('user_email');
this.stop();
}
@ -187,6 +187,13 @@ var filters = {
Router.load( function () {
clearSeenErrors(); // set all errors who have already been seen to not show anymore
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
@ -199,6 +206,7 @@ Router.before(filters.nProgressHook, {only: [
'posts_pending',
'posts_digest',
'posts_category',
'search',
'post_page',
'post_edit',
'comment_page',
@ -209,9 +217,9 @@ Router.before(filters.nProgressHook, {only: [
'all-users'
]});
Router.before(filters.canView);
Router.before(filters.hasCompletedProfile);
Router.before(filters.isLoggedIn, {only: ['comment_reply','post_submit']});
Router.before(filters.canView);
Router.before(filters.isLoggedOut, {only: ['signin', 'signup']});
Router.before(filters.canPost, {only: ['posts_pending', 'comment_reply', 'post_submit']});
Router.before(filters.canEditPost, {only: ['post_edit']});
@ -240,30 +248,24 @@ PostsListController = RouteController.extend({
template:'posts_list',
waitOn: function () {
// 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
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 [
Meteor.subscribe('postsList', parameters.find, parameters.options),
Meteor.subscribe('postsListUsers', parameters.find, parameters.options)
Meteor.subscribe('postsList', this._terms),
Meteor.subscribe('postsListUsers', this._terms)
]
},
data: function () {
var view = this.path == '/' ? 'top' : this.path.split('/')[1],
limit = this.params.limit || getSetting('postsPerPage', 10),
parameters = getParameters(view, limit, this.params.slug),
var parameters = getParameters(this._terms),
posts = Posts.find(parameters.find, parameters.options);
postsCount = posts.count();
Session.set('postsLimit', limit);
// get posts and decorate them with rank property
// note: not actually used;
// posts = posts.map(function (post, index) {
// post.rank = index;
// return post;
// });
Session.set('postsLimit', this._terms.limit);
return {
postsList: posts,
@ -282,16 +284,25 @@ PostsDigestController = RouteController.extend({
template: 'posts_digest',
waitOn: function() {
// 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 parameters = getDigestParameters(currentDate);
var currentDate = this.params.day ? new Date(this.params.year, this.params.month-1, this.params.day) : Session.get('today'),
terms = {
view: 'digest',
after: moment(currentDate).startOf('day').valueOf(),
before: moment(currentDate).endOf('day').valueOf()
}
return [
Meteor.subscribe('postsList', parameters.find, parameters.options),
Meteor.subscribe('postsListUsers', parameters.find, parameters.options)
Meteor.subscribe('postsList', terms),
Meteor.subscribe('postsListUsers', terms)
]
},
data: function() {
var currentDate = this.params.day ? new Date(this.params.year, this.params.month-1, this.params.day) : Session.get('today');
var parameters = getDigestParameters(currentDate);
var currentDate = this.params.day ? new Date(this.params.year, this.params.month-1, this.params.day) : Session.get('today'),
terms = {
view: 'digest',
after: moment(currentDate).startOf('day').valueOf(),
before: moment(currentDate).endOf('day').valueOf()
},
parameters = getParameters(terms);
Session.set('currentDate', currentDate);
return {
posts: Posts.find(parameters.find, parameters.options)
@ -349,8 +360,13 @@ UserPageController = RouteController.extend({
data: function() {
var findById = Meteor.users.findOne(this.params._idOrSlug);
var findBySlug = Meteor.users.findOne({slug: this.params._idOrSlug});
return {
user: (typeof findById == "undefined") ? findBySlug : findById
if(typeof findById !== "undefined"){
// 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() {
// -------------------------------------------- Post Lists -------------------------------------------- //
// Top
this.route('posts_top', {
@ -406,6 +424,12 @@ Router.map(function() {
// TODO: enable /category/new, /category/best, etc. views
// Search
this.route('search', {
path: '/search/:limit?',
controller: PostsListController
});
// Digest
@ -520,9 +544,8 @@ Router.map(function() {
path: '/all-users/:limit?',
template: 'users',
waitOn: function() {
var limit = parseInt(this.params.limit) || 20,
parameters = getUsersParameters(this.params.filterBy, this.params.sortBy, limit);
return Meteor.subscribe('allUsers', parameters.find, parameters.options);
var limit = parseInt(this.params.limit) || 20;
return Meteor.subscribe('allUsers', this.params.filterBy, this.params.sortBy, limit);
},
data: function() {
var limit = parseInt(this.params.limit) || 20,

View file

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

View file

@ -136,6 +136,7 @@
>li{
@include horizontal-list-item;
margin-right:10px;
>a{
color:white;
font-size:16px;
@ -154,3 +155,29 @@
}
}
}
.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;
@include border-radius(3px);
padding: 8px 15px;
font-size: 14px;
font-size: 13px;
a, span{
display: inline-block;
margin-right: 20px;
&.active{
border-bottom: 2px solid $blue;
}
&:last-child{
margin-right: 0;
}
}
.filter{
float: left;
@ -55,3 +58,9 @@ table{
}
}
.user-table{
font-size: 13px;
tr{
border-bottom: 1px solid #eee;
}
}

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 */
.nav > li.last {
padding-right: 0; }
/* line 139, ../sass/modules/_header.scss */
/* line 140, ../sass/modules/_header.scss */
.nav > li > a {
color: white;
font-size: 16px;
line-height: 26px;
height: 26px;
font-weight: normal; }
/* line 147, ../sass/modules/_header.scss */
/* line 148, ../sass/modules/_header.scss */
.nav > li > a.intercom em:before {
content: '('; }
/* line 150, ../sass/modules/_header.scss */
/* line 151, ../sass/modules/_header.scss */
.nav > li > a.intercom em:after {
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 */
.empty-notice {
text-align: center;
@ -1621,7 +1646,7 @@ input[type="submit"], button, .button, .auth-buttons #login-buttons #login-butto
-o-border-radius: 3px;
border-radius: 3px;
padding: 8px 15px;
font-size: 14px; }
font-size: 13px; }
/* line 3, ../sass/partials/_mixins.scss */
.filter-sort:before, .filter-sort:after {
content: "";
@ -1636,14 +1661,17 @@ input[type="submit"], button, .button, .auth-buttons #login-buttons #login-butto
/* line 10, ../sass/modules/_users.scss */
.filter-sort a.active, .filter-sort span.active {
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 {
float: left; }
/* line 17, ../sass/modules/_users.scss */
/* line 20, ../sass/modules/_users.scss */
.filter-sort .sort {
float: right; }
/* line 24, ../sass/modules/_users.scss */
/* line 27, ../sass/modules/_users.scss */
.user-list .user .user-avatar, .user-table .user .user-avatar {
height: 30px;
width: 30px;
@ -1655,13 +1683,20 @@ input[type="submit"], button, .button, .auth-buttons #login-buttons #login-butto
-o-border-radius: 30px;
border-radius: 30px; }
/* line 38, ../sass/modules/_users.scss */
/* line 41, ../sass/modules/_users.scss */
table tr td {
padding: 10px; }
/* line 44, ../sass/modules/_users.scss */
/* line 47, ../sass/modules/_users.scss */
table thead tr td {
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 */
.user-profile .user-avatar {
height: 80px;

View file

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

View file

@ -1,13 +1,6 @@
<template name="toolbox">
<div class="grid-small grid-block dialog admin toolbox">
<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>
<li><a href="#" class="give-invites">Give 1 Invite to Everybody</a></li>
</ul>

View file

@ -5,10 +5,17 @@
<ul class="nav site-nav desktop">
{{#if canView}}
<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>
<li class="dropdown">
<a class="View" href="/">{{i18n "View"}}</a>
<div class="dropdown-menu">
<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 hasCategories}}
<li class="dropdown">
@ -36,6 +43,9 @@
</div>
</li>
{{/if}}
<li>
{{> search}}
</li>
</ul>
{{#if logo_url}}

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({
site_title: function(){
return getSetting('title');
@ -60,3 +36,28 @@ Template.nav.helpers({
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;
},
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);
},
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);
},
inactiveClass: function(){

View file

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

View file

@ -17,14 +17,17 @@ Template.user_edit.helpers({
profileUrl: function(){
return Meteor.absoluteUrl()+"users/"+this.slug;
},
hasNotificationsUsers : function(){
return getUserSetting('notifications.users', '', this) ? 'checked' : '';
},
hasNotificationsPosts : function(){
return getUserSetting('notifications.posts') ? 'checked' : '';
return getUserSetting('notifications.notifications_posts', '', this) ? 'checked' : '';
},
hasNotificationsComments : function(){
return getUserSetting('notifications.comments') ? 'checked' : '';
return getUserSetting('notifications.comments', '', this) ? 'checked' : '';
},
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.github": $target.find('[name=github]').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.comments": $('input[name=notifications_comments]: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();

View file

@ -1,3 +1,9 @@
Template.user_email.helpers({
user: function(){
return Meteor.user();
}
});
Template.user_email.events = {
'submit form': function(e){
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">
<tr class="user">
<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>{{getEmail}}</td>
<td>{{postCount}}</td>
<td>{{commentCount}}</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>{{#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>
<td><a class="delete-link" href="#">{{i18n "Delete User"}}</a></td>
<td>
{{#if invites}}
<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>
</template>

View file

@ -20,35 +20,22 @@ Template.user_item.helpers({
userIsAdmin: function(){
return isAdmin(this);
},
profileUrl: function () {
getProfileUrl: function () {
return getProfileUrl(this);
},
getKarma: function() {
return Math.round(100*this.karma)/100;
},
getInvitedUserProfileUrl: function () {
var user = Meteor.users.findOne(this.invitedId);
return getProfileUrl(user);
}
});
Template.user_item.events({
'click .invite-link': function(e, instance){
e.preventDefault();
var user = Meteor.users.findOne(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")
});
}
});
Meteor.call('inviteUser', instance.data._id);
},
'click .uninvite-link': function(e, instance){
e.preventDefault();

View file

@ -6,16 +6,22 @@
<tr>
<td colspan="2"><img class="user-avatar" src="{{avatarUrl}}"/></td>
</tr>
{{#if isAdmin}}
<tr>
<td>{{i18n "ID"}}: </td>
<td>{{_id}}</td>
</tr>
{{/if}}
<tr>
<td>{{i18n "Name"}}: </td>
<td>{{i18n "Name:"}}</td>
<td>{{profile.name}}</td>
</tr>
<tr>
<td>{{i18n "Member since"}}: </td>
<td>{{i18n "Member since"}}:</td>
<td>{{createdAtFormatted}}</td>
</tr>
<tr>
<td>{{i18n "Bio"}}: </td>
<td>{{i18n "Bio:"}}</td>
<td>{{profile.bio}}</td>
</tr>
{{#if getTwitterName}}
@ -26,13 +32,13 @@
{{/if}}
{{#if getGitHubName}}
<tr>
<td>GitHub: </td>
<td>{{i18n "GitHub"}}:</td>
<td><a href="http://github.com/{{getGitHubName}}">{{getGitHubName}}</a></td>
</tr>
{{/if}}
{{#if site}}
<tr>
<td>Site: </td>
<td>{{i18n "Site"}}:</td>
<td><a href="{{profile.site}}">{{profile.site}}</a></td>
</tr>
{{/if}}
@ -41,8 +47,8 @@
<a class="button inline" href="/users/{{slug}}/edit">{{i18n "Edit profile"}}</a>
{{/if}}
{{#if canInvite}}
{{#if invitesCount}}
<a class="button inline invite-link" href="#">{{i18n "Invite "}}({{invitesCount}} {{i18n "left"}})</a>
{{#if inviteCount}}
<a class="button inline invite-link" href="#">{{i18n "Invite"}} ({{inviteCount}} {{i18n "left"}})</a>
{{else}}
<a class="button inline disabled" href="#">{{i18n "Invite (none left)"}}</a>
{{/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
return Meteor.user() && Meteor.user()._id != this._id && !isInvited(this) && invitesEnabled() && canInvite(Meteor.user());
},
invitesCount: function() {
return Meteor.user().invitesCount;
inviteCount: function() {
return Meteor.user().inviteCount;
},
getTwitterName: function () {
return getTwitterName(this);
@ -27,5 +27,6 @@ Template.user_profile.helpers({
Template.user_profile.events({
'click .invite-link': function(e, instance){
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 'postCount'}}" href="{{sortBy 'postCount'}}">{{i18n "Posts"}}</a>
<a class="{{activeClass 'commentCount'}}" href="{{sortBy 'commentCount'}}">{{i18n "Comments"}}</a>
<a class="{{activeClass 'invitedCount'}}" href="{{sortBy 'invitedCount'}}">{{i18n "InvitedCount"}}</a>
</p>
</div>
<table>
@ -24,13 +25,13 @@
<tr>
<td colspan="2">{{i18n "Name"}}</td>
<td>{{i18n "Member since"}}</td>
<td>{{i18n "Email"}}</td>
<td>{{i18n "Posts"}}</td>
<td>{{i18n "Comments"}}</td>
<td>{{i18n "Karma"}}</td>
<td>{{i18n "Is Invited?"}}</td>
<td>{{i18n "Is Admin?"}}</td>
<td>{{i18n "Delete"}}</td>
<td>{{i18n "Invites"}}</td>
<td>{{i18n "Invited?"}}</td>
<td>{{i18n "Admin?"}}</td>
<td>{{i18n "Actions"}}</td>
</tr>
</thead>
<tbody>

View file

@ -9,44 +9,71 @@ Notifications.allow({
, remove: canEditById
});
getNotification = function(event, properties, context){
var notification = {};
// the default context to display notifications is the notification sidebar
var context = typeof context === 'undefined' ? 'sidebar' : context;
var p = properties;
getNotificationContents = function(notification, context){
// the same notifications can be displayed in multiple contexts: on-site in the sidebar, sent by email, etc.
var event = notification.event,
p = notification.properties,
context = typeof context === 'undefined' ? 'sidebar' : context,
userToNotify = Meteor.users.findOne(notification.userId);
switch(event){
case 'newReply':
notification.subject = i18n.t('Someone replied to your comment on')+' "'+p.postHeadline+'"';
notification.text = p.commentAuthorName+i18n.t(' has replied to your comment on')+' "'+p.postHeadline+'": '+getPostCommentUrl(p.postId, p.commentId);
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>';
if(context === 'email')
notification.html += '<p>'+p.commentExcerpt+'</p><a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">'+i18n.t('Read more')+'</a>';
break;
var n = {
subject: 'Someone replied to your comment on "'+p.postHeadline+'"',
text: p.commentAuthorName+' has replied to your comment on "'+p.postHeadline+'": '+getPostCommentUrl(p.postId, p.commentId),
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>'
}
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':
notification.subject = i18n.t('A new comment on your post')+' "'+p.postHeadline+'"';
notification.text = i18n.t('You have a new comment by ')+p.commentAuthorName+i18n.t(' on your post')+' "'+p.postHeadline+'": '+getPostCommentUrl(p.postId, p.commentId);
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>';
if(context === 'email')
notification.html += '<p>'+p.commentExcerpt+'</p><a href="'+getPostCommentUrl(p.postId, p.commentId)+'" class="action-link">'+i18n.t('Read more')+'</a>';
break;
var n = {
subject: 'A new comment on your post "'+p.postHeadline+'"',
text: 'You have a new comment by '+p.commentAuthorName+' on your post "'+p.postHeadline+'": '+getPostCommentUrl(p.postId, p.commentId),
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>'
}
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':
notification.subject = p.postAuthorName+i18n.t(' has created a new post')+': "'+p.postHeadline+'"';
notification.text = p.postAuthorName+i18n.t(' has created a new post')+': "'+p.postHeadline+'" '+getPostUrl(p.postId);
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>".';
break;
var n = {
subject: p.postAuthorName+' has created a new post: "'+p.postHeadline+'"',
text: p.postAuthorName+' has created a new post: "'+p.postHeadline+'" '+getPostUrl(p.postId),
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':
notification.subject = i18n.t('Your account has been approved.');
notification.text = i18n.t('Welcome to ')+getSetting('title')+'! '+i18n.t('Your account has just 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>';
break;
var n = {
subject: 'Your account has been approved.',
text: 'Welcome to '+getSetting('title')+'! Your account has just been approved.',
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:
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({

View file

@ -97,15 +97,17 @@ Meteor.methods({
if(getSetting('emailNotifications', false)){
// notify users of new posts
var properties = {
postAuthorName : getDisplayName(postAuthor),
postAuthorId : post.userId,
postHeadline : headline,
postId : postId
var notification = {
event: 'newPost',
properties: {
postAuthorName : getDisplayName(postAuthor),
postAuthorId : post.userId,
postHeadline : headline,
postId : postId
}
}
var notification = getNotification('newPost', properties);
// call a server method because we do not have access to admin users' info on the client
Meteor.call('notifyUsers', notification, Meteor.user(), function(error, result){
// call a server method because we do not have access to users' info on the client
Meteor.call('newPostNotify', notification, function(error, result){
//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() {
return Router._currentController.template;
}
getCurrentRoute = function() {
return Router._currentController.path;
}
clearSeenErrors = function(){
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
return Meteor.absoluteUrl()+'posts/'+postId+'/comment/'+commentId;
}
getUserUrl = function(id){
return Meteor.absoluteUrl()+'users/'+id;
}
getCategoryUrl = function(slug){
return Meteor.absoluteUrl()+'category/'+slug;
}
slugify = function(text) {
text = text.replace(/[^-a-zA-Z0-9,&\s]+/ig, '');
text = text.replace(/-/gi, "_");
text = text.replace(/\s/gi, "-");
text = text.toLowerCase();
if(text){
text = text.replace(/[^-a-zA-Z0-9,&\s]+/ig, '');
text = text.replace(/-/gi, "_");
text = text.replace(/\s/gi, "-");
text = text.toLowerCase();
}
return text;
}
getShortUrl = function(post){
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 = 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 = {
find: {
@ -12,64 +17,74 @@ getParameters = function (view, limit, category) {
}
}
switch (view) {
switch (terms.view) {
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;
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;
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;
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;
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;
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
$.extend(true, parameters, {options: {sort: {_id: -1}}})
deepExtend(true, parameters, {options: {sort: {_id: -1}}})
if(typeof limit != 'undefined')
_.extend(parameters.options, {limit: parseInt(limit)});
if(typeof terms.limit != 'undefined' && !!terms.limit)
_.extend(parameters.options, {limit: parseInt(terms.limit)});
if(typeof category != 'undefined')
_.extend(parameters.find, {'categories.slug': category});
if(typeof terms.category != 'undefined' && !!terms.category)
_.extend(parameters.find, {'categories.slug': terms.category});
// console.log(parameters.options.sort)
// console.log(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) {
var find = {},
sort = {createdAt: -1};
@ -99,6 +114,8 @@ getUsersParameters = function(filterBy, sortBy, limit) {
break;
case 'commentCount':
sort = {commentCount: -1};
case 'invitedCount':
sort = {invitedCount: -1};
}
return {
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;
// 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.

View file

@ -24,7 +24,13 @@ getDisplayNameById = function(userId){
return getDisplayName(Meteor.users.findOne(userId));
}
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){
// return twitter name provided by user, or else the one used for twitter login
@ -67,7 +73,7 @@ getAvatarUrl = function(user){
}else{
return Gravatar.getGravatar(user, {
d: 'http://demo.telesc.pe/img/default_avatar.png',
s: 30
s: 80
});
}
}

View file

@ -1,21 +1,19 @@
Meteor.methods({
inviteUser: function (userId) {
var currentUser = Meteor.user();
var invitedUser = Meteor.users.findOne(userId);
var invite = {
invited: invitedUser._id,
invitedId: invitedUser._id,
invitedName: getDisplayName(invitedUser),
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(currentUser && currentUser.invitesCount > 0 && canInvite(currentUser) && !isInvited(invitedUser)){
if(currentUser && currentUser.inviteCount > 0 && canInvite(currentUser) && !isInvited(invitedUser)){
// update invinting user
Meteor.users.update(Meteor.userId(), {$inc:{invitesCount: -1}});
Meteor.users.update(Meteor.userId(), {$push:{invites: invite}});
Meteor.users.update(Meteor.userId(), {$inc:{inviteCount: -1}, $inc:{invitedCount: 1}, $push:{invites: invite}});
// update invited user
Meteor.users.update(userId, {$set: {

View file

@ -106,6 +106,39 @@ Meteor.startup(function () {
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(properties);
// console.log(sendEmail);
var notification= {
var notification = {
timestamp: new Date().getTime(),
userId: userToNotify._id,
event: event,
@ -26,38 +26,45 @@ Meteor.methods({
// send the notification if notifications are activated,
// the notificationsFrequency is set to 1, or if it's undefined (legacy compatibility)
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){
// 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});
if(user){
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 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
_.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)){
// don't send users notifications for their own posts
sendEmail(getEmail(user), notification.subject, notification.text, notification.html);
properties.userId = user._id;
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,
emails: false,
notifications: false,
invitesCount: false,
inviteCount: false,
'profile.email': false,
'services.twitter.accessToken': 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
return findById.count() ? findById : findBySlug;
}
return [];
});
// 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 [];
});
// Publish author of the current comment
@ -60,34 +62,42 @@ Meteor.publish('commentUser', function(commentId) {
var comment = Comments.findOne(commentId);
return Meteor.users.find({_id: comment && comment.userId}, {fields: privacyOptions});
}
return [];
});
// 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)){
var posts = Posts.find(find, options);
var userIds = _.pluck(posts.fetch(), 'userId');
var parameters = getParameters(terms),
posts = Posts.find(parameters.find, parameters.options),
userIds = _.pluck(posts.fetch(), 'userId');
return Meteor.users.find({_id: {$in: userIds}}, {fields: privacyOptions, multi: true});
}
return [];
});
// Publish all users
Meteor.publish('allUsers', function(find, options) {
Meteor.publish('allUsers', function(filterBy, sortBy, limit) {
if(canViewById(this.userId)){
var parameters = getUsersParameters(filterBy, sortBy, limit);
if (!isAdminById(this.userId)) // if user is not admin, filter out sensitive info
options = _.extend(options, {fields: privacyOptions});
return Meteor.users.find(find, options);
parameters.options = _.extend(parameters.options, {fields: privacyOptions});
return Meteor.users.find(parameters.find, parameters.options);
}
return [];
});
// publish all users for admins to make autocomplete work
// TODO: find a better way
Meteor.publish('allUsersAdmin', function() {
if (isAdminById(this.userId))
if (isAdminById(this.userId)) {
return Meteor.users.find();
} else {
return [];
}
});
// -------------------------------------------- Posts -------------------------------------------- //
@ -98,6 +108,7 @@ Meteor.publish('singlePost', function(id) {
if(canViewById(this.userId)){
return Posts.find(id);
}
return [];
});
// Publish the post related to the current comment
@ -107,29 +118,26 @@ Meteor.publish('commentPost', function(commentId) {
var comment = Comments.findOne(commentId);
return Posts.find({_id: comment && comment.post});
}
return [];
});
// Publish a list of posts
Meteor.publish('postsList', function(find, options) {
Meteor.publish('postsList', function(terms) {
if(canViewById(this.userId)){
options = options || {};
var posts = Posts.find(find, options);
var parameters = getParameters(terms),
posts = Posts.find(parameters.find, parameters.options);
// console.log('//-------- Subscription Parameters:');
// console.log(find);
// console.log(options);
// console.log(parameters.find);
// console.log(parameters.options);
// console.log('Found '+posts.fetch().length+ ' posts:');
// posts.rewind();
// console.log(_.pluck(posts.fetch(), 'headline'));
// posts.rewind();
return posts;
}
return [];
});
// -------------------------------------------- Comments -------------------------------------------- //
// Publish comments for a specific post
@ -138,6 +146,7 @@ Meteor.publish('postComments', function(postId) {
if(canViewById(this.userId)){
return Comments.find({post: postId});
}
return [];
});
// Publish a single comment
@ -146,12 +155,22 @@ Meteor.publish('singleComment', function(commentId) {
if(canViewById(this.userId)){
return Comments.find(commentId);
}
return [];
});
// -------------------------------------------- Other -------------------------------------------- //
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() {
@ -159,10 +178,12 @@ Meteor.publish('notifications', function() {
if(canViewById(this.userId)){
return Notifications.find({userId:this.userId});
}
return [];
});
Meteor.publish('categories', function() {
if(canViewById(this.userId)){
return Categories.find();
}
return [];
});

View file

@ -14,38 +14,6 @@ Meteor.methods({
},
giveInvites: function () {
if(isAdmin(Meteor.user()))
Meteor.users.update({}, {$inc:{invitesCount: 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
Meteor.users.update({}, {$inc:{inviteCount: 1}}, {multi:true});
}
})

View file

@ -1,19 +1,32 @@
Accounts.onCreateUser(function(options, user){
user.profile = options.profile || {};
user.karma = 0;
// users start pending and need to be invited
user.isInvited = false;
user.isAdmin = false;
var userProperties = {
profile: options.profile || {},
karma: 0,
isInvited: false,
isAdmin: false,
postCount: 0,
commentCount: 0,
invitedCount: 0
}
user = _.extend(user, userProperties);
if (options.email)
user.profile.email = options.email;
if (user.profile.email)
user.email_hash = CryptoJS.MD5(user.profile.email.trim().toLowerCase()).toString();
if (getEmail(user))
user.email_hash = getEmailHash(user);
if (!user.profile.name)
user.profile.name = user.username;
// set notifications default preferences
user.profile.notifications = {
users: false,
posts: false,
comments: true,
replies: true
}
// create slug from username
user.slug = slugify(getUserName(user));
@ -22,16 +35,39 @@ Accounts.onCreateUser(function(options, user){
user.isAdmin = true;
// 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});
// add new user to MailChimp list
addToMailChimpList(user);
// if user has already filled in their email, add them to MailChimp list
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;
});
getEmailHash = function(user){
// todo: add some kind of salt in here
return CryptoJS.MD5(getEmail(user).trim().toLowerCase() + user.createdAt).toString();
}
addToMailChimpList = function(user){
// add a user to a MailChimp list.
// 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);
return Math.abs(object.score - newScore);
},
generateEmailHash: function(){
var email_hash = CryptoJS.MD5(getEmail(Meteor.user()).trim().toLowerCase()).toString();
Meteor.users.update(Meteor.userId(), {$set : {email_hash : email_hash}});
setEmailHash: function(user){
var email_hash = CryptoJS.MD5(getEmail(user).trim().toLowerCase()).toString();
Meteor.users.update(user._id, {$set : {email_hash : email_hash}});
},
addCurrentUserToMailChimpList: function(){
addToMailChimpList(Meteor.user());