Merge branch 'master' into mobile

This commit is contained in:
Neil Lalonde 2013-09-05 15:54:22 -04:00
commit 45d7765936
133 changed files with 2458 additions and 1264 deletions

View File

@ -187,7 +187,10 @@ gem 'lru_redux'
# IMPORTANT: mini profiler monkey patches, so it better be required last # IMPORTANT: mini profiler monkey patches, so it better be required last
# If you want to amend mini profiler to do the monkey patches in the railstie # If you want to amend mini profiler to do the monkey patches in the railstie
# we are open to it. by deferring require to the initializer we can configure disourse installs without it # we are open to it. by deferring require to the initializer we can configure disourse installs without it
gem 'rack-mini-profiler', '0.1.29', require: false # require: false #, git: 'git://github.com/SamSaffron/MiniProfiler'
# gem 'rack-mini-profiler', '0.1.30', require: false
gem 'flamegraph', require: false
gem 'rack-mini-profiler', require: false
# used for caching, optional # used for caching, optional
# redis-rack-cache is missing a sane expiry policy, it hogs redis # redis-rack-cache is missing a sane expiry policy, it hogs redis
@ -196,6 +199,7 @@ gem 'redis-rack-cache', git: 'https://github.com/SamSaffron/redis-rack-cache.git
gem 'rack-cache', require: false gem 'rack-cache', require: false
gem 'rack-cors', require: false gem 'rack-cors', require: false
gem 'unicorn', require: false gem 'unicorn', require: false
gem 'puma', require: false
# perftools only works on 1.9 atm # perftools only works on 1.9 atm
group :profile do group :profile do

View File

@ -180,9 +180,14 @@ GEM
fast_blank (0.0.1) fast_blank (0.0.1)
rake rake
rake-compiler rake-compiler
fast_stack (0.0.5)
rake
rake-compiler
fast_xs (0.8.0) fast_xs (0.8.0)
fastimage (1.3.0) fastimage (1.3.0)
ffi (1.8.1) ffi (1.8.1)
flamegraph (0.0.2)
fast_stack
fog (1.14.0) fog (1.14.0)
builder builder
excon (~> 0.25.0) excon (~> 0.25.0)
@ -219,7 +224,7 @@ GEM
librarian (0.1.0) librarian (0.1.0)
highline highline
thor (~> 0.15) thor (~> 0.15)
libv8 (3.11.8.17) libv8 (3.16.14.3)
listen (0.7.3) listen (0.7.3)
lru_redux (0.0.6) lru_redux (0.0.6)
mail (2.4.4) mail (2.4.4)
@ -287,6 +292,8 @@ GEM
pry (~> 0.9.10) pry (~> 0.9.10)
pry-rails (0.2.2) pry-rails (0.2.2)
pry (>= 0.9.10) pry (>= 0.9.10)
puma (2.5.1)
rack (>= 1.1, < 2.0)
qunit-rails (0.0.3) qunit-rails (0.0.3)
railties (>= 3.2.3) railties (>= 3.2.3)
rack (1.4.5) rack (1.4.5)
@ -294,7 +301,7 @@ GEM
rack (>= 0.4) rack (>= 0.4)
rack-cors (0.2.7) rack-cors (0.2.7)
rack rack
rack-mini-profiler (0.1.29) rack-mini-profiler (0.1.31)
rack (>= 1.1.3) rack (>= 1.1.3)
rack-openid (1.3.1) rack-openid (1.3.1)
rack (>= 1.1.0) rack (>= 1.1.0)
@ -423,8 +430,8 @@ GEM
activemodel (~> 3.0) activemodel (~> 3.0)
railties (~> 3.0) railties (~> 3.0)
temple (0.6.4) temple (0.6.4)
therubyracer (0.11.4) therubyracer (0.12.0)
libv8 (~> 3.11.8.12) libv8 (~> 3.16.14.0)
ref ref
thin (1.5.1) thin (1.5.1)
daemons (>= 1.0.9) daemons (>= 1.0.9)
@ -472,6 +479,7 @@ DEPENDENCIES
fast_xor! fast_xor!
fast_xs fast_xs
fastimage fastimage
flamegraph
fog fog
handlebars-source (= 1.0.12) handlebars-source (= 1.0.12)
highline highline
@ -501,10 +509,11 @@ DEPENDENCIES
pg pg
pry-nav pry-nav
pry-rails pry-rails
puma
qunit-rails qunit-rails
rack-cache rack-cache
rack-cors rack-cors
rack-mini-profiler (= 0.1.29) rack-mini-profiler
rails (= 3.2.12) rails (= 3.2.12)
rails_multisite! rails_multisite!
rake rake

View File

@ -33,7 +33,7 @@ GIT
GIT GIT
remote: git://github.com/rails/rails.git remote: git://github.com/rails/rails.git
revision: e36692a7466011ab51393ac8ca6dfffcb9d79ec0 revision: 025b63db308fbbf942a3bc2673d4aadab968c524
branch: 4-0-stable branch: 4-0-stable
specs: specs:
actionmailer (4.0.0) actionmailer (4.0.0)
@ -216,9 +216,14 @@ GEM
fast_blank (0.0.1) fast_blank (0.0.1)
rake rake
rake-compiler rake-compiler
fast_stack (0.0.5)
rake
rake-compiler
fast_xs (0.8.0) fast_xs (0.8.0)
fastimage (1.5.0) fastimage (1.5.0)
ffi (1.9.0) ffi (1.9.0)
flamegraph (0.0.2)
fast_stack
fog (1.14.0) fog (1.14.0)
builder builder
excon (~> 0.25.0) excon (~> 0.25.0)
@ -256,7 +261,7 @@ GEM
librarian (0.1.0) librarian (0.1.0)
highline highline
thor (~> 0.15) thor (~> 0.15)
libv8 (3.11.8.17) libv8 (3.16.14.3)
listen (1.2.2) listen (1.2.2)
rb-fsevent (>= 0.9.3) rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9) rb-inotify (>= 0.9)
@ -267,7 +272,7 @@ GEM
treetop (~> 1.4.8) treetop (~> 1.4.8)
metaclass (0.0.1) metaclass (0.0.1)
method_source (0.8.1) method_source (0.8.1)
mime-types (1.24) mime-types (1.25)
mini_portile (0.5.1) mini_portile (0.5.1)
minitest (4.7.5) minitest (4.7.5)
mocha (0.14.0) mocha (0.14.0)
@ -327,6 +332,8 @@ GEM
pry (~> 0.9.10) pry (~> 0.9.10)
pry-rails (0.3.1) pry-rails (0.3.1)
pry (>= 0.9.10) pry (>= 0.9.10)
puma (2.5.1)
rack (>= 1.1, < 2.0)
qunit-rails (0.0.3) qunit-rails (0.0.3)
railties (>= 3.2.3) railties (>= 3.2.3)
rack (1.5.2) rack (1.5.2)
@ -334,7 +341,7 @@ GEM
rack (>= 0.4) rack (>= 0.4)
rack-cors (0.2.8) rack-cors (0.2.8)
rack rack
rack-mini-profiler (0.1.29) rack-mini-profiler (0.1.31)
rack (>= 1.1.3) rack (>= 1.1.3)
rack-openid (1.3.1) rack-openid (1.3.1)
rack (>= 1.1.0) rack (>= 1.1.0)
@ -432,8 +439,8 @@ GEM
activesupport (>= 3.0) activesupport (>= 3.0)
sprockets (~> 2.8) sprockets (~> 2.8)
temple (0.6.5) temple (0.6.5)
therubyracer (0.11.4) therubyracer (0.12.0)
libv8 (~> 3.11.8.12) libv8 (~> 3.16.14.0)
ref ref
thin (1.5.1) thin (1.5.1)
daemons (>= 1.0.9) daemons (>= 1.0.9)
@ -482,6 +489,7 @@ DEPENDENCIES
fast_xor! fast_xor!
fast_xs fast_xs
fastimage fastimage
flamegraph
fog fog
handlebars-source (= 1.0.12) handlebars-source (= 1.0.12)
highline highline
@ -511,10 +519,11 @@ DEPENDENCIES
pg pg
pry-nav pry-nav
pry-rails pry-rails
puma
qunit-rails qunit-rails
rack-cache rack-cache
rack-cors rack-cors
rack-mini-profiler (= 0.1.29) rack-mini-profiler
rails! rails!
rails-observers rails-observers
rails_multisite! rails_multisite!

View File

@ -3,6 +3,49 @@
@module $.fn.autocomplete @module $.fn.autocomplete
**/ **/
var shiftMap = [];
shiftMap[192] = "~";
shiftMap[49] = "!";
shiftMap[50] = "@";
shiftMap[51] = "#";
shiftMap[52] = "$";
shiftMap[53] = "%";
shiftMap[54] = "^";
shiftMap[55] = "&";
shiftMap[56] = "*";
shiftMap[57] = "(";
shiftMap[48] = ")";
shiftMap[109] = "_";
shiftMap[107] = "+";
shiftMap[219] = "{";
shiftMap[221] = "}";
shiftMap[220] = "|";
shiftMap[59] = ":";
shiftMap[222] = "\"";
shiftMap[188] = "<";
shiftMap[190] = ">";
shiftMap[191] = "?";
shiftMap[32] = " ";
function mapKeyPressToActualCharacter(isShiftKey, characterCode) {
if ( characterCode === 27 || characterCode === 8 || characterCode === 9 || characterCode === 20 || characterCode === 16 || characterCode === 17 || characterCode === 91 || characterCode === 13 || characterCode === 92 || characterCode === 18 ) { return false; }
if (isShiftKey) {
if ( characterCode >= 65 && characterCode <= 90 ) {
return String.fromCharCode(characterCode);
} else {
return shiftMap[characterCode];
}
} else {
if ( characterCode >= 65 && characterCode <= 90 ) {
return String.fromCharCode(characterCode).toLowerCase();
} else {
return String.fromCharCode(characterCode);
}
}
}
$.fn.autocomplete = function(options) { $.fn.autocomplete = function(options) {
var autocompletePlugin = this; var autocompletePlugin = this;
@ -338,11 +381,15 @@ $.fn.autocomplete = function(options) {
} }
term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition); term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
if (e.which >= 48 && e.which <= 90) { if (e.which >= 48 && e.which <= 90) {
term += String.fromCharCode(e.which); term += mapKeyPressToActualCharacter(e.shiftKey, e.which);
} else if (e.which === 187) { } else if (e.which === 187) {
term += "+"; term += "+";
} else if (e.which === 189) { } else if (e.which === 189) {
term += (e.shiftKey) ? "_" : "-"; term += (e.shiftKey) ? "_" : "-";
} else if (e.which === 220) {
term += (e.shiftKey) ? "|" : "]";
} else if (e.which === 222) {
term += (e.shiftKey) ? "\"" : "'";
} else { } else {
if (e.which !== 8) { if (e.which !== 8) {
term += ","; term += ",";

View File

@ -13,13 +13,29 @@ Discourse.Formatter = (function(){
var firstPart = string.substr(0, maxLength); var firstPart = string.substr(0, maxLength);
var betterSplit = firstPart.substr(1).search(/[^a-z]/); // work backward to split stuff like ABPoop to AB Poop
if (betterSplit >= 0) { var i;
var offset = 1; for(i=firstPart.length-1;i>0;i--){
if(string[betterSplit+1] === "_") { if(firstPart[i].match(/[A-Z]/)){
offset = 2; break;
} }
return string.substr(0, betterSplit + offset) + " " + string.substring(betterSplit + offset); }
// work forwards to split stuff like ab111 to ab 111
if(i===0) {
for(i=1;i<firstPart.length;i++){
if(firstPart[i].match(/[^a-z]/)){
break;
}
}
}
if (i > 0 && i < firstPart.length) {
var offset = 0;
if(string[i] === "_") {
offset = 1;
}
return string.substr(0, i + offset) + " " + string.substring(i + offset);
} else { } else {
return firstPart + " " + string.substr(maxLength); return firstPart + " " + string.substr(maxLength);
} }

View File

@ -23,6 +23,10 @@ Discourse.Quote = {
sansQuotes = contents.replace(this.REGEXP, '').trim(); sansQuotes = contents.replace(this.REGEXP, '').trim();
if (sansQuotes.length === 0) return ""; if (sansQuotes.length === 0) return "";
// Escape the content of the quote
sansQuotes = sansQuotes.replace(/</g, "&lt;")
.replace(/>/g, "&gt;");
result = "[quote=\"" + post.get('username') + ", post:" + post.get('post_number') + ", topic:" + post.get('topic_id'); result = "[quote=\"" + post.get('username') + ", post:" + post.get('post_number') + ", topic:" + post.get('topic_id');
/* Strip the HTML from cooked */ /* Strip the HTML from cooked */

View File

@ -8,7 +8,15 @@
@module Discourse @module Discourse
**/ **/
Discourse.AvatarSelectorController = Discourse.Controller.extend(Discourse.ModalFunctionality, { Discourse.AvatarSelectorController = Discourse.Controller.extend(Discourse.ModalFunctionality, {
toggleUseUploadedAvatar: function(toggle) { useUploadedAvatar: function() {
this.set("use_uploaded_avatar", toggle); this.set("use_uploaded_avatar", true);
} },
useGravatar: function() {
this.set("use_uploaded_avatar", false);
},
avatarTemplate: function() {
return this.get("use_uploaded_avatar") ? this.get("uploaded_avatar_template") : this.get("gravatar_template");
}.property("use_uploaded_avatar", "uploaded_avatar_template", "gravatar_template")
}); });

View File

@ -13,7 +13,7 @@ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Disco
if( this.get('details.auto_close_at') ) { if( this.get('details.auto_close_at') ) {
var closeTime = new Date( this.get('details.auto_close_at') ); var closeTime = new Date( this.get('details.auto_close_at') );
if (closeTime > new Date()) { if (closeTime > new Date()) {
this.set('auto_close_days', closeTime.daysSince()); this.set('auto_close_days', Math.round(moment(closeTime).diff(new Date(), 'days', true)));
} }
} else { } else {
this.set('details.auto_close_days', ''); this.set('details.auto_close_days', '');

View File

@ -58,9 +58,11 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc
if (opts) params = $.extend(params, opts); if (opts) params = $.extend(params, opts);
$('#discourse-modal').modal('hide');
postAction.act(params).then(function() { postAction.act(params).then(function() {
flagController.send('closeModal'); flagController.send('closeModal');
}, function(errors) { }, function(errors) {
$('#discourse-modal').modal('show');
flagController.displayErrors(errors); flagController.displayErrors(errors);
}); });
}, },

View File

@ -95,6 +95,11 @@ Discourse.LoginController = Discourse.Controller.extend(Discourse.ModalFunctiona
}, },
authenticationComplete: function(options) { authenticationComplete: function(options) {
if (options.requires_invite) {
this.flash(I18n.t('login.requires_invite'), 'success');
this.set('authenticate', null);
return;
}
if (options.awaiting_approval) { if (options.awaiting_approval) {
this.flash(I18n.t('login.awaiting_approval'), 'success'); this.flash(I18n.t('login.awaiting_approval'), 'success');
this.set('authenticate', null); this.set('authenticate', null);

View File

@ -12,6 +12,7 @@ Discourse.MergeTopicController = Discourse.ObjectController.extend(Discourse.Sel
topicController: Em.computed.alias('controllers.topic'), topicController: Em.computed.alias('controllers.topic'),
selectedPosts: Em.computed.alias('topicController.selectedPosts'), selectedPosts: Em.computed.alias('topicController.selectedPosts'),
selectedReplies: Em.computed.alias('topicController.selectedReplies'),
allPostsSelected: Em.computed.alias('topicController.allPostsSelected'), allPostsSelected: Em.computed.alias('topicController.allPostsSelected'),
buttonDisabled: function() { buttonDisabled: function() {
@ -31,10 +32,13 @@ Discourse.MergeTopicController = Discourse.ObjectController.extend(Discourse.Sel
if (this.get('allPostsSelected')) { if (this.get('allPostsSelected')) {
promise = Discourse.Topic.mergeTopic(this.get('id'), this.get('selectedTopicId')); promise = Discourse.Topic.mergeTopic(this.get('id'), this.get('selectedTopicId'));
} else { } else {
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }); var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }),
replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); });
promise = Discourse.Topic.movePosts(this.get('id'), { promise = Discourse.Topic.movePosts(this.get('id'), {
destination_topic_id: this.get('selectedTopicId'), destination_topic_id: this.get('selectedTopicId'),
post_ids: postIds post_ids: postIds,
reply_post_ids: replyPostIds
}); });
} }

View File

@ -12,6 +12,7 @@ Discourse.SplitTopicController = Discourse.ObjectController.extend(Discourse.Sel
topicController: Em.computed.alias('controllers.topic'), topicController: Em.computed.alias('controllers.topic'),
selectedPosts: Em.computed.alias('topicController.selectedPosts'), selectedPosts: Em.computed.alias('topicController.selectedPosts'),
selectedReplies: Em.computed.alias('topicController.selectedReplies'),
buttonDisabled: function() { buttonDisabled: function() {
if (this.get('saving')) return true; if (this.get('saving')) return true;
@ -30,21 +31,23 @@ Discourse.SplitTopicController = Discourse.ObjectController.extend(Discourse.Sel
movePostsToNewTopic: function() { movePostsToNewTopic: function() {
this.set('saving', true); this.set('saving', true);
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }); var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }),
var splitTopicController = this; replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); }),
self = this;
Discourse.Topic.movePosts(this.get('id'), { Discourse.Topic.movePosts(this.get('id'), {
title: this.get('topicName'), title: this.get('topicName'),
post_ids: postIds post_ids: postIds,
reply_post_ids: replyPostIds
}).then(function(result) { }).then(function(result) {
// Posts moved // Posts moved
splitTopicController.send('closeModal'); self.send('closeModal');
splitTopicController.get('topicController').toggleMultiSelect(); self.get('topicController').toggleMultiSelect();
Em.run.next(function() { Discourse.URL.routeTo(result.url); }); Em.run.next(function() { Discourse.URL.routeTo(result.url); });
}, function() { }, function() {
// Error moving posts // Error moving posts
splitTopicController.flash(I18n.t('topic.split_topic.error')); self.flash(I18n.t('topic.split_topic.error'));
splitTopicController.set('saving', false); self.set('saving', false);
}); });
return false; return false;
} }

View File

@ -11,8 +11,15 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
summaryCollapsed: true, summaryCollapsed: true,
needs: ['header', 'modal', 'composer', 'quoteButton'], needs: ['header', 'modal', 'composer', 'quoteButton'],
allPostsSelected: false, allPostsSelected: false,
selectedPosts: new Em.Set(),
editingTopic: false, editingTopic: false,
selectedPosts: null,
selectedReplies: null,
init: function() {
this._super();
this.set('selectedPosts', new Em.Set());
this.set('selectedReplies', new Em.Set());
},
jumpTopDisabled: function() { jumpTopDisabled: function() {
return (this.get('progressPosition') === 1); return (this.get('progressPosition') === 1);
@ -82,18 +89,48 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
return false; return false;
}.property('postStream.loaded', 'currentPost', 'postStream.filteredPostsCount'), }.property('postStream.loaded', 'currentPost', 'postStream.filteredPostsCount'),
selectPost: function(post) { deselectPost: function(post) {
this.get('selectedPosts').removeObject(post);
var selectedReplies = this.get('selectedReplies');
selectedReplies.removeObject(post);
var selectedReply = selectedReplies.findProperty('post_number', post.get('reply_to_post_number'));
if (selectedReply) { selectedReplies.removeObject(selectedReply); }
this.set('allPostsSelected', false);
},
postSelected: function(post) {
if (this.get('allPostsSelected')) { return true; }
if (this.get('selectedPosts').contains(post)) { return true; }
if (this.get('selectedReplies').findProperty('post_number', post.get('reply_to_post_number'))) { return true; }
return false;
},
toggledSelectedPost: function(post) {
var selectedPosts = this.get('selectedPosts'); var selectedPosts = this.get('selectedPosts');
if (selectedPosts.contains(post)) { if (this.postSelected(post)) {
selectedPosts.removeObject(post); this.deselectPost(post);
this.set('allPostsSelected', false); return false;
} else { } else {
selectedPosts.addObject(post); selectedPosts.addObject(post);
// If the user manually selects all posts, all posts are selected // If the user manually selects all posts, all posts are selected
if (selectedPosts.length === this.get('posts_count')) { if (selectedPosts.length === this.get('posts_count')) {
this.set('allPostsSelected'); this.set('allPostsSelected', true);
} }
return true;
}
},
toggledSelectedPostReplies: function(post) {
var selectedReplies = this.get('selectedReplies');
if (this.toggledSelectedPost(post)) {
selectedReplies.addObject(post);
} else {
selectedReplies.removeObject(post);
} }
}, },
@ -108,6 +145,7 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
deselectAll: function() { deselectAll: function() {
this.get('selectedPosts').clear(); this.get('selectedPosts').clear();
this.get('selectedReplies').clear();
this.set('allPostsSelected', false); this.set('allPostsSelected', false);
}, },
@ -177,19 +215,28 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
}, },
deleteSelected: function() { deleteSelected: function() {
var topicController = this; var self = this;
bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) { bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
if (result) { if (result) {
// If all posts are selected, it's the same thing as deleting the topic // If all posts are selected, it's the same thing as deleting the topic
if (topicController.get('allPostsSelected')) { if (self.get('allPostsSelected')) {
return topicController.deleteTopic(); return self.deleteTopic();
} }
var selectedPosts = topicController.get('selectedPosts'); var selectedPosts = self.get('selectedPosts'),
Discourse.Post.deleteMany(selectedPosts); selectedReplies = self.get('selectedReplies'),
topicController.get('model.postStream').removePosts(selectedPosts); postStream = self.get('postStream'),
topicController.toggleMultiSelect(); toRemove = new Ember.Set();
Discourse.Post.deleteMany(selectedPosts, selectedReplies);
postStream.get('posts').forEach(function (p) {
if (self.postSelected(p)) { toRemove.addObject(p); }
});
postStream.removePosts(toRemove);
self.toggleMultiSelect();
} }
}); });
}, },
@ -410,7 +457,33 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
}, },
deletePost: function(post) { deletePost: function(post) {
post.destroy(Discourse.User.current()); var user = Discourse.User.current(),
replyCount = post.get('reply_count'),
self = this;
// If the user is staff and the post has replies, ask if they want to delete replies too.
if (user.get('staff') && replyCount > 0) {
bootbox.confirm(I18n.t("post.controls.delete_replies.confirm", {count: replyCount}),
I18n.t("post.controls.delete_replies.no_value"),
I18n.t("post.controls.delete_replies.yes_value"),
function(result) {
// If the user wants to delete replies, do that, otherwise delete the post as normal.
if (result) {
Discourse.Post.deleteMany([post], [post]);
self.get('postStream.posts').forEach(function (p) {
if (p === post || p.get('reply_to_post_number') === post.get('post_number')) {
p.setDeletedState(user);
}
});
} else {
post.destroy(user);
}
});
} else {
post.destroy(user);
}
}, },
removeAllowedUser: function(username) { removeAllowedUser: function(username) {

View File

@ -24,5 +24,6 @@ Discourse.UserActivityController = Discourse.ObjectController.extend({
}, },
privateMessagesActive: Em.computed.equal('pmView', 'index'), privateMessagesActive: Em.computed.equal('pmView', 'index'),
privateMessagesSentActive: Em.computed.equal('pmView', 'sent') privateMessagesMineActive: Em.computed.equal('pmView', 'mine'),
privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread')
}); });

View File

@ -1,45 +1,19 @@
/** /**
This addition handles auto linking of text. When included, it will parse out links and create This addition handles auto linking of text. When included, it will parse out links and create
a hrefs for them. a hrefs for them.
@event register
@namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("register", function(event) { var urlReplacerArgs = {
matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
spaceBoundary: true,
var dialect = event.dialect, emitter: function(matches) {
MD = event.MD; var url = matches[1],
displayUrl = url;
/** if (url.match(/^www/)) { url = "http://" + url; }
Parses out links from HTML. return ['a', {href: url}, displayUrl];
}
};
@method autoLink Discourse.Dialect.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs));
@param {String} text the text match Discourse.Dialect.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs));
@param {Array} match the match found
@param {Array} prev the previous jsonML
@return {Array} an array containing how many chars we've replaced and the jsonML content for it.
@namespace Discourse.Dialect
**/
dialect.inline['http'] = dialect.inline['www'] = function autoLink(text, match, prev) {
// We only care about links on boundaries
if (prev && (prev.length > 0)) {
var last = prev[prev.length - 1];
if (typeof last === "string" && (!last.match(/\s$/))) { return; }
}
var pattern = /(^|\s)((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
m = pattern.exec(text);
if (m) {
var url = m[2],
displayUrl = m[2];
if (url.match(/^www/)) { url = "http://" + url; }
return [m[0].length, ['a', {href: url}, displayUrl]];
}
};
});

View File

@ -1,205 +1,114 @@
/** /**
Regsiter all functionality for supporting BBCode in Discourse. Create a simple BBCode tag handler
@event register @method replaceBBCode
@namespace Discourse.Dialect @param {tag} tag the tag we want to match
@param {function} emitter the function that creates JsonML for the tag
**/ **/
Discourse.Dialect.on("register", function(event) { function replaceBBCode(tag, emitter) {
Discourse.Dialect.inlineBetween({
var dialect = event.dialect, start: "[" + tag + "]",
MD = event.MD; stop: "[/" + tag + "]",
emitter: emitter
var createBBCode = function(tag, builder, hasArgs) {
return function(text, orig_match) {
var bbcodePattern = new RegExp("\\[" + tag + "=?([^\\[\\]]+)?\\]([\\s\\S]*?)\\[\\/" + tag + "\\]", "igm");
var m = bbcodePattern.exec(text);
if (m && m[0]) {
return [m[0].length, builder(m, this)];
}
};
};
var bbcodes = {'b': ['span', {'class': 'bbcode-b'}],
'i': ['span', {'class': 'bbcode-i'}],
'u': ['span', {'class': 'bbcode-u'}],
's': ['span', {'class': 'bbcode-s'}],
'spoiler': ['span', {'class': 'spoiler'}],
'li': ['li'],
'ul': ['ul'],
'ol': ['ol']};
Object.keys(bbcodes).forEach(function(tag) {
var element = bbcodes[tag];
dialect.inline["[" + tag + "]"] = createBBCode(tag, function(m, self) {
return element.concat(self.processInline(m[2]));
});
}); });
}
dialect.inline["[img]"] = createBBCode('img', function(m) { /**
return ['img', {href: m[2]}]; Creates a BBCode handler that accepts parameters. Passes them to the emitter.
});
dialect.inline["[email]"] = createBBCode('email', function(m) { @method replaceBBCodeParamsRaw
return ['a', {href: "mailto:" + m[2], 'data-bbcode': true}, m[2]]; @param {tag} tag the tag we want to match
}); @param {function} emitter the function that creates JsonML for the tag
**/
function replaceBBCodeParamsRaw(tag, emitter) {
Discourse.Dialect.inlineBetween({
start: "[" + tag + "=",
stop: "[/" + tag + "]",
rawContents: true,
emitter: function(contents) {
var regexp = /^([^\]]+)\](.*)$/,
m = regexp.exec(contents);
dialect.inline["[url]"] = createBBCode('url', function(m) { if (m) { return emitter.call(this, m[1], m[2]); }
return ['a', {href: m[2], 'data-bbcode': true}, m[2]];
});
dialect.inline["[url="] = createBBCode('url', function(m, self) {
return ['a', {href: m[1], 'data-bbcode': true}].concat(self.processInline(m[2]));
});
dialect.inline["[email="] = createBBCode('email', function(m, self) {
return ['a', {href: "mailto:" + m[1], 'data-bbcode': true}].concat(self.processInline(m[2]));
});
dialect.inline["[size="] = createBBCode('size', function(m, self) {
return ['span', {'class': "bbcode-size-" + m[1]}].concat(self.processInline(m[2]));
});
dialect.inline["[color="] = function(text, orig_match) {
var bbcodePattern = new RegExp("\\[color=?([^\\[\\]]+)?\\]([\\s\\S]*?)\\[\\/color\\]", "igm"),
m = bbcodePattern.exec(text);
if (m && m[0]) {
if (!/^(\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)$/.test(m[1])) {
return [m[0].length].concat(this.processInline(m[2]));
}
return [m[0].length, ['span', {style: "color: " + m[1]}].concat(this.processInline(m[2]))];
} }
}; });
}
/** /**
Support BBCode [code] blocks Creates a BBCode handler that accepts parameters. Passes them to the emitter.
Processes the inside recursively so it can be nested.
@method bbcodeCode @method replaceBBCodeParams
@param {Markdown.Block} block the block to examine @param {tag} tag the tag we want to match
@param {Array} next the next blocks in the sequence @param {function} emitter the function that creates JsonML for the tag
@return {Array} the JsonML containing the markup or undefined if nothing changed. **/
@namespace Discourse.Dialect function replaceBBCodeParams(tag, emitter) {
**/ replaceBBCodeParamsRaw(tag, function (param, contents) {
dialect.inline["[code]"] = function bbcodeCode(text, orig_match) { return emitter(param, this.processInline(contents));
var bbcodePattern = new RegExp("\\[code\\]([\\s\\S]*?)\\[\\/code\\]", "igm"), });
m = bbcodePattern.exec(text); }
if (m) { replaceBBCode('b', function(contents) { return ['span', {'class': 'bbcode-b'}].concat(contents); });
var contents = m[1].trim().split("\n"); replaceBBCode('i', function(contents) { return ['span', {'class': 'bbcode-i'}].concat(contents); });
replaceBBCode('u', function(contents) { return ['span', {'class': 'bbcode-u'}].concat(contents); });
replaceBBCode('s', function(contents) { return ['span', {'class': 'bbcode-s'}].concat(contents); });
var html = ['pre', "\n"]; replaceBBCode('ul', function(contents) { return ['ul'].concat(contents); });
contents.forEach(function (n) { replaceBBCode('ol', function(contents) { return ['ol'].concat(contents); });
html.push(n.trim()); replaceBBCode('li', function(contents) { return ['li'].concat(contents); });
html.push(["br"]);
html.push("\n");
});
return [m[0].length, html]; replaceBBCode('spoiler', function(contents) { return ['span', {'class': 'spoiler'}].concat(contents); });
}
};
/** Discourse.Dialect.inlineBetween({
Support BBCode [quote] blocks start: '[img]',
stop: '[/img]',
rawContents: true,
emitter: function(contents) { return ['img', {href: contents}]; }
});
@method bbcodeQuote Discourse.Dialect.inlineBetween({
@param {Markdown.Block} block the block to examine start: '[email]',
@param {Array} next the next blocks in the sequence stop: '[/email]',
@return {Array} the JsonML containing the markup or undefined if nothing changed. rawContents: true,
@namespace Discourse.Dialect emitter: function(contents) { return ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]; }
**/ });
dialect.block['quote'] = function bbcodeQuote(block, next) {
var m = new RegExp("\\[quote=?([^\\[\\]]+)?\\]([\\s\\S]*)", "igm").exec(block);
if (m) {
var paramsString = m[1].replace(/\"/g, ''),
params = {'class': 'quote'},
paramsSplit = paramsString.split(/\, */),
username = paramsSplit[0],
opts = dialect.options,
startPos = block.indexOf(m[0]),
leading,
quoteContents = [],
result = [];
if (startPos > 0) {
leading = block.slice(0, startPos);
var para = ['p'];
this.processInline(leading).forEach(function (l) {
para.push(l);
});
result.push(para);
}
paramsSplit.forEach(function(p,i) {
if (i > 0) {
var assignment = p.split(':');
if (assignment[0] && assignment[1]) {
params['data-' + assignment[0]] = assignment[1].trim();
}
}
});
var avatarImg;
if (opts.lookupAvatarByPostNumber) {
// client-side, we can retrieve the avatar from the post
var postNumber = parseInt(params['data-post'], 10);
avatarImg = opts.lookupAvatarByPostNumber(postNumber);
} else if (opts.lookupAvatar) {
// server-side, we need to lookup the avatar from the username
avatarImg = opts.lookupAvatar(username);
}
if (m[2]) { next.unshift(MD.mk_block(m[2])); }
while (next.length > 0) {
var b = next.shift(),
n = b.match(/([\s\S]*)\[\/quote\]([\s\S]*)/m);
if (n) {
if (n[2]) {
next.unshift(MD.mk_block(n[2]));
}
quoteContents.push(n[1]);
break;
} else {
quoteContents.push(b);
}
}
var contents = this.processInline(quoteContents.join(" \n \n"));
contents.unshift('blockquote');
result.push(['p', ['aside', params,
['div', {'class': 'title'},
['div', {'class': 'quote-controls'}],
avatarImg ? avatarImg : "",
I18n.t('user.said',{username: username})
],
contents
]]);
return result;
}
};
Discourse.Dialect.inlineBetween({
start: '[url]',
stop: '[/url]',
rawContents: true,
emitter: function(contents) { return ['a', {href: contents, 'data-bbcode': true}, contents]; }
}); });
Discourse.Dialect.on("parseNode", function(event) { replaceBBCodeParamsRaw("url", function(param, contents) {
return ['a', {href: param, 'data-bbcode': true}, contents];
});
var node = event.node, replaceBBCodeParamsRaw("email", function(param, contents) {
path = event.path; return ['a', {href: "mailto:" + param, 'data-bbcode': true}, contents];
});
// Make sure any quotes are followed by a <br>. The formatting looks weird otherwise. replaceBBCodeParams("size", function(param, contents) {
if (node[0] === 'aside' && node[1] && node[1]['class'] === 'quote') { return ['span', {'class': "bbcode-size-" + param}].concat(contents);
var parent = path[path.length - 1], });
location = parent.indexOf(node)+1,
trailing = parent.slice(location);
if (trailing.length) { replaceBBCodeParams("color", function(param, contents) {
parent.splice(location, 0, ['br']); // Only allow valid HTML colors.
} if (/^(\#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?)|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow)$/.test(param)) {
return ['span', {style: "color: " + param}].concat(contents);
} else {
return ['span'].concat(contents);
} }
}); });
// Handles `[code] ... [/code]` blocks
Discourse.Dialect.replaceBlock({
start: /(\[code\])([\s\S]*)/igm,
stop: '[/code]',
emitter: function(blockContents) {
return ['p', ['pre'].concat(blockContents)];
}
});

View File

@ -1,32 +1,42 @@
/** /**
Markdown.js doesn't seem to do bold and italics at the same time if you surround code with markdown-js doesn't ensure that em/strong codes are present on word boundaries.
three asterisks. This adds that support. So we create our own handlers here.
@event register
@namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("register", function(event) {
// Support for simultaneous bold and italics
Discourse.Dialect.inlineBetween({
between: '***',
wordBoundary: true,
emitter: function(contents) { return ['strong', ['em'].concat(contents)]; }
});
// Builds a common markdown replacer
var replaceMarkdown = function(match, tag) {
Discourse.Dialect.inlineBetween({
between: match,
wordBoundary: true,
emitter: function(contents) { return [tag].concat(contents) }
});
};
replaceMarkdown('**', 'strong');
replaceMarkdown('__', 'strong');
replaceMarkdown('*', 'em');
replaceMarkdown('_', 'em');
// There's a weird issue with the markdown parser where it won't process simple blockquotes
// when they are prefixed with spaces. This fixes it.
Discourse.Dialect.on("register", function(event) {
var dialect = event.dialect, var dialect = event.dialect,
MD = event.MD; MD = event.MD;
/** dialect.block["fix_simple_quotes"] = function(block, next) {
Handles simultaneous bold and italics var m = /^ +(\>[\s\S]*)/.exec(block);
if (m && m[1] && m[1].length) {
@method parseMentions next.unshift(MD.mk_block(m[1]));
@param {String} text the text match return [];
@param {Array} match the match found
@param {Array} prev the previous jsonML
@return {Array} an array containing how many chars we've replaced and the jsonML content for it.
@namespace Discourse.Dialect
**/
dialect.inline['***'] = function boldItalics(text, match, prev) {
var regExp = /^\*{3}([^\*]+)\*{3}/,
m = regExp.exec(text);
if (m) {
return [m[0].length, ['strong', ['em'].concat(this.processInline(m[1]))]];
} }
}; };
}); });

View File

@ -3,91 +3,70 @@
Discourse uses the Markdown.js as its main parser. `Discourse.Dialect` is the framework Discourse uses the Markdown.js as its main parser. `Discourse.Dialect` is the framework
for extending it with additional formatting. for extending it with additional formatting.
To extend the dialect, you can register a handler, and you will receive an `event` object
with a handle to the markdown `Dialect` from Markdown.js that we are defining. Here's
a sample dialect that replaces all occurrences of "evil trout" with a link that says
"EVIL TROUT IS AWESOME":
```javascript
Discourse.Dialect.on("register", function(event) {
var dialect = event.dialect;
// To see how this works, review one of our samples or the Markdown.js code:
dialect.inline["evil trout"] = function(text) {
return ["evil trout".length, ['a', {href: "http://eviltrout.com"}, "EVIL TROUT IS AWESOME"] ];
};
});
```
You can also manipulate the JsonML tree that is produced by the parser before it converted to HTML.
This is useful if the markup you want needs a certain structure of HTML elements. Rather than
writing regular expressions to match HTML, consider parsing the tree instead! We use this for
making sure a onebox is on one line, as an example.
This example changes the content of any `<code>` tags.
The `event.path` attribute contains the current path to the node.
```javascript
Discourse.Dialect.on("parseNode", function(event) {
var node = event.node;
if (node[0] === 'code') {
node[node.length-1] = "EVIL TROUT HACKED YOUR CODE";
}
});
```
**/ **/
var parser = window.BetterMarkdown, var parser = window.BetterMarkdown,
MD = parser.Markdown, MD = parser.Markdown,
// Our dialect
dialect = MD.dialects.Discourse = MD.subclassDialect( MD.dialects.Gruber ), dialect = MD.dialects.Discourse = MD.subclassDialect( MD.dialects.Gruber ),
initialized = false;
initialized = false, /**
Initialize our dialects for processing.
/** @method initializeDialects
Initialize our dialects for processing. **/
function initializeDialects() {
Discourse.Dialect.trigger('register', {dialect: dialect, MD: MD});
MD.buildBlockOrder(dialect.block);
MD.buildInlinePatterns(dialect.inline);
initialized = true;
}
@method initializeDialects /**
**/ Parse a JSON ML tree, using registered handlers to adjust it if necessary.
initializeDialects = function() {
Discourse.Dialect.trigger('register', {dialect: dialect, MD: MD});
MD.buildBlockOrder(dialect.block);
MD.buildInlinePatterns(dialect.inline);
initialized = true;
},
/** @method parseTree
Parse a JSON ML tree, using registered handlers to adjust it if necessary. @param {Array} tree the JsonML tree to parse
@param {Array} path the path of ancestors to the current node in the tree. Can be used for matching.
@param {Object} insideCounts counts what tags we're inside
@returns {Array} the parsed tree
**/
function parseTree(tree, path, insideCounts) {
if (tree instanceof Array) {
Discourse.Dialect.trigger('parseNode', {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}});
@method parseTree path = path || [];
@param {Array} tree the JsonML tree to parse insideCounts = insideCounts || {};
@param {Array} path the path of ancestors to the current node in the tree. Can be used for matching.
@param {Object} insideCounts counts what tags we're inside
@returns {Array} the parsed tree
**/
parseTree = function parseTree(tree, path, insideCounts) {
if (tree instanceof Array) {
Discourse.Dialect.trigger('parseNode', {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}});
path = path || []; path.push(tree);
insideCounts = insideCounts || {}; tree.slice(1).forEach(function (n) {
var tagName = n[0];
insideCounts[tagName] = (insideCounts[tagName] || 0) + 1;
parseTree(n, path, insideCounts);
insideCounts[tagName] = insideCounts[tagName] - 1;
});
path.pop();
}
return tree;
}
path.push(tree); /**
tree.slice(1).forEach(function (n) { Returns true if there's an invalid word boundary for a match.
var tagName = n[0];
insideCounts[tagName] = (insideCounts[tagName] || 0) + 1; @method invalidBoundary
parseTree(n, path, insideCounts); @param {Object} args our arguments, including whether we care about boundaries
insideCounts[tagName] = insideCounts[tagName] - 1; @param {Array} prev the previous content, if exists
}); @returns {Boolean} whether there is an invalid word boundary
path.pop(); **/
} function invalidBoundary(args, prev) {
return tree;
}; if (!args.wordBoundary && !args.spaceBoundary) { return; }
var last = prev[prev.length - 1];
if (typeof last !== "string") { return; }
if (args.wordBoundary && (last.match(/(\w|\/)$/))) { return true; }
if (args.spaceBoundary && (!last.match(/\s$/))) { return true; }
}
/** /**
An object used for rendering our dialects. An object used for rendering our dialects.
@ -110,7 +89,281 @@ Discourse.Dialect = {
dialect.options = opts; dialect.options = opts;
var tree = parser.toHTMLTree(text, 'Discourse'); var tree = parser.toHTMLTree(text, 'Discourse');
return parser.renderJsonML(parseTree(tree)); return parser.renderJsonML(parseTree(tree));
},
/**
The simplest kind of replacement possible. Replace a stirng token with JsonML.
For example to replace all occurrances of :) with a smile image:
```javascript
Discourse.Dialect.inlineReplace(':)', function (text) {
return ['img', {src: '/images/smile.png'}];
});
```
@method inlineReplace
@param {String} token The token we want to replace
@param {Function} emitter A function that emits the JsonML for the replacement.
**/
inlineReplace: function(token, emitter) {
dialect.inline[token] = function(text, match, prev) {
return [token.length, emitter.call(this, token)];
};
},
/**
Matches inline using a regular expression. The emitter function is passed
the matches from the regular expression.
For example, this auto links URLs:
```javascript
Discourse.Dialect.inlineRegexp({
matcher: /((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
spaceBoundary: true,
emitter: function(matches) {
var url = matches[1];
return ['a', {href: url}, url];
}
});
```
@method inlineRegexp
@param {Object} args Our replacement options
@param {Function} [opts.emitter] The function that will be called with the contents and regular expresison match and returns JsonML.
@param {String} [opts.start] The starting token we want to find
@param {String} [opts.matcher] The regular expression to match
@param {Boolean} [opts.wordBoundary] If true, the match must be on a word boundary
@param {Boolean} [opts.spaceBoundary] If true, the match must be on a sppace boundary
**/
inlineRegexp: function(args) {
dialect.inline[args.start] = function(text, match, prev) {
if (invalidBoundary(args, prev)) { return; }
args.matcher.lastIndex = 0;
var m = args.matcher.exec(text);
if (m) {
var result = args.emitter.call(this, m);
if (result) {
return [m[0].length, result];
}
}
};
},
/**
Handles inline replacements surrounded by tokens.
For example, to handle markdown style bold. Note we use `concat` on the array because
the contents are JsonML too since we didn't pass `rawContents` as true. This supports
recursive markup.
```javascript
Discourse.Dialect.inlineBetween({
between: '**',
wordBoundary: true.
emitter: function(contents) {
return ['strong'].concat(contents);
}
});
```
@method inlineBetween
@param {Object} args Our replacement options
@param {Function} [opts.emitter] The function that will be called with the contents and returns JsonML.
@param {String} [opts.start] The starting token we want to find
@param {String} [opts.stop] The ending token we want to find
@param {String} [opts.between] A shortcut for when the `start` and `stop` are the same.
@param {Boolean} [opts.rawContents] If true, the contents between the tokens will not be parsed.
@param {Boolean} [opts.wordBoundary] If true, the match must be on a word boundary
@param {Boolean} [opts.spaceBoundary] If true, the match must be on a sppace boundary
**/
inlineBetween: function(args) {
var start = args.start || args.between,
stop = args.stop || args.between,
startLength = start.length;
dialect.inline[start] = function(text, match, prev) {
if (invalidBoundary(args, prev)) { return; }
var endPos = text.indexOf(stop, startLength);
if (endPos === -1) { return; }
var between = text.slice(startLength, endPos);
// If rawcontents is set, don't process inline
if (!args.rawContents) {
between = this.processInline(between);
}
var contents = args.emitter.call(this, between);
if (contents) {
return [endPos+stop.length, contents];
}
};
},
/**
Replaces a block of text between a start and stop. As opposed to inline, these
might span multiple lines.
Here's an example that takes the content between `[code]` ... `[/code]` and
puts them inside a `pre` tag:
```javascript
Discourse.Dialect.replaceBlock({
start: /(\[code\])([\s\S]*)/igm,
stop: '[/code]',
emitter: function(blockContents) {
return ['p', ['pre'].concat(blockContents)];
}
});
```
@method replaceBlock
@param {Object} args Our replacement options
@param {String} [opts.start] The starting regexp we want to find
@param {String} [opts.stop] The ending token we want to find
@param {Function} [opts.emitter] The emitting function to transform the contents of the block into jsonML
**/
replaceBlock: function(args) {
dialect.block[args.start.toString()] = function(block, next) {
args.start.lastIndex = 0;
var m = (args.start).exec(block);
if (!m) { return; }
var startPos = block.indexOf(m[0]),
leading,
blockContents = [],
result = [],
lineNumber = block.lineNumber;
if (startPos > 0) {
leading = block.slice(0, startPos);
lineNumber += (leading.split("\n").length - 1);
var para = ['p'];
this.processInline(leading).forEach(function (l) {
para.push(l);
});
result.push(para);
}
if (m[2]) {
next.unshift(MD.mk_block(m[2], null, lineNumber + 1));
}
lineNumber++;
while (next.length > 0) {
var b = next.shift(),
blockLine = b.lineNumber,
diff = ((typeof blockLine === "undefined") ? lineNumber : blockLine) - lineNumber;
var endFound = b.indexOf(args.stop),
leadingContents = b.slice(0, endFound),
trailingContents = b.slice(endFound+args.stop.length);
for (var i=1; i<diff; i++) {
blockContents.push("");
}
lineNumber = blockLine + b.split("\n").length - 1;
if (endFound !== -1) {
if (trailingContents) {
next.unshift(MD.mk_block(trailingContents));
}
blockContents.push(leadingContents.replace(/\s+$/, ""));
break;
} else {
blockContents.push(b);
}
}
var test = args.emitter.call(this, blockContents, m, dialect.options);
result.push(test);
return result;
};
},
/**
After the parser has been executed, post process any text nodes in the HTML document.
This is useful if you want to apply a transformation to the text.
If you are generating HTML from the text, it is preferable to use the replacer
functions and do it in the parsing part of the pipeline. This function is best for
simple transformations or transformations that have to happen after all earlier
processing is done.
For example, to convert all text to upper case:
```javascript
Discourse.Dialect.postProcessText(function (text) {
return text.toUpperCase();
});
```
@method postProcessText
@param {Function} emitter The function to call with the text. It returns JsonML to modify the tree.
**/
postProcessText: function(emitter) {
Discourse.Dialect.on("parseNode", function(event) {
var node = event.node;
if (node.length < 2) { return; }
for (var j=1; j<node.length; j++) {
var textContent = node[j];
if (typeof textContent === "string") {
var result = emitter(textContent, event);
if (result) {
if (result instanceof Array) {
node.splice.apply(node, [j, 1].concat(result));
} else {
node[j] = result;
}
}
}
}
});
},
/**
After the parser has been executed, change the contents of a HTML tag.
Let's say you want to replace the contents of all code tags to prepend
"EVIL TROUT HACKED YOUR CODE!":
```javascript
Discourse.Dialect.postProcessTag('code', function (contents) {
return "EVIL TROUT HACKED YOUR CODE!\n\n" + contents;
});
```
@method postProcessTag
@param {String} tag The HTML tag you want to match on
@param {Function} emitter The function to call with the text. It returns JsonML to modify the tree.
**/
postProcessTag: function(tag, emitter) {
Discourse.Dialect.on('parseNode', function (event) {
var node = event.node;
if (node[0] === tag) {
node[node.length-1] = emitter(node[node.length-1]);
}
});
} }
}; };
RSVP.EventTarget.mixin(Discourse.Dialect); RSVP.EventTarget.mixin(Discourse.Dialect);

View File

@ -5,129 +5,16 @@
@event register @event register
@namespace Discourse.Dialect @namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("register", function(event) { Discourse.Dialect.replaceBlock({
var dialect = event.dialect, start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm,
MD = event.MD; stop: '```',
emitter: function(blockContents, matches) {
/** return ['p', ['pre', ['code', {'class': matches[1] || 'lang-auto'}, blockContents.join("\n") ]]];
Support for github style code blocks
@method githubCode
@param {Markdown.Block} block the block to examine
@param {Array} next the next blocks in the sequence
@return {Array} the JsonML containing the markup or undefined if nothing changed.
@namespace Discourse.Dialect
**/
dialect.block.github_code = function githubCode(block, next) {
var m = /^`{3}([^\n]+)?\n?([\s\S]*)?/gm.exec(block);
if (m) {
var startPos = block.indexOf(m[0]),
leading,
codeContents = [],
result = [],
lineNumber = block.lineNumber;
if (startPos > 0) {
leading = block.slice(0, startPos);
lineNumber += (leading.split("\n").length - 1);
var para = ['p'];
this.processInline(leading).forEach(function (l) {
para.push(l);
});
result.push(para);
}
if (m[2]) { next.unshift(MD.mk_block(m[2], null, lineNumber + 1)); }
lineNumber++;
while (next.length > 0) {
var b = next.shift(),
blockLine = b.lineNumber,
diff = ((typeof blockLine === "undefined") ? lineNumber : blockLine) - lineNumber;
var endFound = b.indexOf('```'),
leadingCode = b.slice(0, endFound),
trailingCode = b.slice(endFound+3);
for (var i=1; i<diff; i++) {
codeContents.push("");
}
lineNumber = blockLine + b.split("\n").length - 1;
if (endFound !== -1) {
if (trailingCode) {
next.unshift(MD.mk_block(trailingCode));
}
codeContents.push(leadingCode.replace(/\s+$/, ""));
break;
} else {
codeContents.push(b);
}
}
result.push(['p', ['pre', ['code', {'class': m[1] || 'lang-auto'}, codeContents.join("\n") ]]]);
return result;
}
};
});
/**
Ensure that content in a code block is fully escaped. This way it's not white listed
and we can use HTML and Javascript examples.
@event parseNode
@namespace Discourse.Dialect
**/
Discourse.Dialect.on("parseNode", function(event) {
var node = event.node;
if (node[0] === 'code') {
node[node.length-1] = Handlebars.Utils.escapeExpression(node[node.length-1]);
} }
}); });
// Ensure that content in a code block is fully escaped. This way it's not white listed
Discourse.Dialect.on("parseNode", function(event) { // and we can use HTML and Javascript examples.
Discourse.Dialect.postProcessTag('code', function (contents) {
var node = event.node, return Handlebars.Utils.escapeExpression(contents);
opts = event.dialect.options,
insideCounts = event.insideCounts,
linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
if (!linebreaks) {
// We don't add line breaks inside a pre
if (insideCounts.pre > 0) { return; }
if (node.length > 1) {
for (var j=1; j<node.length; j++) {
var textContent = node[j];
if (typeof textContent === "string") {
if (textContent === "\n") {
node[j] = ['br'];
} else {
var split = textContent.split(/\n+/);
if (split.length) {
var spliceInstructions = [j, 1];
for (var i=0; i<split.length; i++) {
if (split[i].length > 0) {
spliceInstructions.push(split[i]);
if (i !== split.length-1) { spliceInstructions.push(['br']); }
}
}
node.splice.apply(node, spliceInstructions);
}
}
}
}
}
}
}); });

View File

@ -2,47 +2,20 @@
Supports Discourse's custom @mention syntax for calling out a user in a post. Supports Discourse's custom @mention syntax for calling out a user in a post.
It will add a special class to them, and create a link if the user is found in a It will add a special class to them, and create a link if the user is found in a
local map. local map.
@event register
@namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("register", function(event) { Discourse.Dialect.inlineRegexp({
start: '@',
matcher: /^(@[A-Za-z0-9][A-Za-z0-9_]{2,14})/m,
wordBoundary: true,
var dialect = event.dialect, emitter: function(matches) {
MD = event.MD; var username = matches[1],
mentionLookup = this.dialect.options.mentionLookup || Discourse.Mention.lookupCache;
/** if (mentionLookup(username.substr(1))) {
Parses out @username mentions. return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username];
} else {
@method parseMentions return ['span', {'class': 'mention'}, username];
@param {String} text the text match
@param {Array} match the match found
@param {Array} prev the previous jsonML
@return {Array} an array containing how many chars we've replaced and the jsonML content for it.
@namespace Discourse.Dialect
**/
dialect.inline['@'] = function parseMentions(text, match, prev) {
// We only care about mentions on word boundaries
if (prev && (prev.length > 0)) {
var last = prev[prev.length - 1];
if (typeof last === "string" && (!last.match(/\W$/))) { return; }
} }
}
var pattern = /^(@[A-Za-z0-9][A-Za-z0-9_]{2,14})(?=(\W|$))/m, });
m = pattern.exec(text);
if (m) {
var username = m[1],
mentionLookup = dialect.options.mentionLookup || Discourse.Mention.lookupCache;
if (mentionLookup(username.substr(1))) {
return [username.length, ['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username]];
} else {
return [username.length, ['span', {'class': 'mention'}, username]];
}
}
};
});

View File

@ -1,42 +1,32 @@
/** /**
Support for the newline behavior in markdown that most expect. Support for the newline behavior in markdown that most expect. Look through all text nodes
in the tree, replace any new lines with `br`s.
@event parseNode
@namespace Discourse.Dialect
**/ **/
Discourse.Dialect.on("parseNode", function(event) { Discourse.Dialect.postProcessText(function (text, event) {
var node = event.node, var opts = event.dialect.options,
opts = event.dialect.options,
insideCounts = event.insideCounts, insideCounts = event.insideCounts,
linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks; linebreaks = opts.traditional_markdown_linebreaks || Discourse.SiteSettings.traditional_markdown_linebreaks;
if (!linebreaks) { if (linebreaks || (insideCounts.pre > 0)) { return; }
// We don't add line breaks inside a pre
if (insideCounts.pre > 0) { return; }
if (node.length > 1) { if (text === "\n") {
for (var j=1; j<node.length; j++) { // If the tage is just a new line, replace it with a `<br>`
var textContent = node[j]; return [['br']];
} else {
if (typeof textContent === "string") { // If the text node contains new lines, perhaps with text between them, insert the
// `<br>` tags.
if (textContent === "\n") { var split = text.split(/\n+/);
node[j] = ['br']; if (split.length) {
} else { var replacement = [];
var split = textContent.split(/\n+/); for (var i=0; i<split.length; i++) {
if (split.length) { if (split[i].length > 0) {
var spliceInstructions = [j, 1]; replacement.push(split[i]);
for (var i=0; i<split.length; i++) { if (i !== split.length-1) { replacement.push(['br']); }
if (split[i].length > 0) {
spliceInstructions.push(split[i]);
if (i !== split.length-1) { spliceInstructions.push(['br']); }
}
}
node.splice.apply(node, spliceInstructions);
}
}
} }
} }
return replacement;
} }
} }
}); });

View File

@ -0,0 +1,62 @@
/**
Support for quoting other users.
**/
Discourse.Dialect.replaceBlock({
start: new RegExp("\\[quote=?([^\\[\\]]+)?\\]([\\s\\S]*)", "igm"),
stop: '[/quote]',
emitter: function(blockContents, matches, options) {
var paramsString = matches[1].replace(/\"/g, ''),
params = {'class': 'quote'},
paramsSplit = paramsString.split(/\, */),
username = paramsSplit[0];
paramsSplit.forEach(function(p,i) {
if (i > 0) {
var assignment = p.split(':');
if (assignment[0] && assignment[1]) {
params['data-' + assignment[0]] = assignment[1].trim();
}
}
});
var avatarImg;
if (options.lookupAvatarByPostNumber) {
// client-side, we can retrieve the avatar from the post
var postNumber = parseInt(params['data-post'], 10);
avatarImg = options.lookupAvatarByPostNumber(postNumber);
} else if (options.lookupAvatar) {
// server-side, we need to lookup the avatar from the username
avatarImg = options.lookupAvatar(username);
}
var contents = this.processInline(blockContents.join(" \n \n"));
contents.unshift('blockquote');
return ['p', ['aside', params,
['div', {'class': 'title'},
['div', {'class': 'quote-controls'}],
avatarImg ? avatarImg : "",
I18n.t('user.said', {username: username})
],
contents
]];
}
});
Discourse.Dialect.on("parseNode", function(event) {
var node = event.node,
path = event.path;
// Make sure any quotes are followed by a <br>. The formatting looks weird otherwise.
if (node[0] === 'aside' && node[1] && node[1]['class'] === 'quote') {
var parent = path[path.length - 1],
location = parent.indexOf(node)+1,
trailing = parent.slice(location);
if (trailing.length) {
parent.splice(location, 0, ['br']);
}
}
});

View File

@ -78,8 +78,6 @@ DiscourseGroupedEach.prototype = {
template = this.template; template = this.template;
data.insideEach = true; data.insideEach = true;
data.insideGroup = true;
for (var i = 0; i < contentLength; i++) { for (var i = 0; i < contentLength; i++) {
template(content.objectAt(i), { data: data }); template(content.objectAt(i), { data: data });
} }
@ -124,5 +122,6 @@ Ember.Handlebars.registerHelper('groupedEach', function(path, options) {
} }
options.hash.dataSourceBinding = path; options.hash.dataSourceBinding = path;
options.data.insideGroup = true;
new DiscourseGroupedEach(this, path, options).render(); new DiscourseGroupedEach(this, path, options).render();
}); });

View File

@ -11,10 +11,15 @@ Discourse.SelectedPostsCount = Em.Mixin.create({
selectedPostsCount: function() { selectedPostsCount: function() {
if (this.get('allPostsSelected')) return this.get('posts_count') || this.get('topic.posts_count'); if (this.get('allPostsSelected')) return this.get('posts_count') || this.get('topic.posts_count');
if (!this.get('selectedPosts')) return 0; var sum = this.get('selectedPosts.length') || 0;
if (this.get('selectedReplies')) {
this.get('selectedReplies').forEach(function (p) {
sum += p.get('reply_count') || 0;
});
}
return this.get('selectedPosts.length'); return sum;
}.property('selectedPosts.length', 'allPostsSelected') }.property('selectedPosts.length', 'allPostsSelected', 'selectedReplies.length')
}); });

View File

@ -442,6 +442,7 @@ Discourse.Composer = Discourse.Model.extend({
postStream = this.get('topic.postStream'), postStream = this.get('topic.postStream'),
addedToStream = false; addedToStream = false;
// Build the post object // Build the post object
var createdPost = Discourse.Post.create({ var createdPost = Discourse.Post.create({
raw: this.get('reply'), raw: this.get('reply'),
@ -482,6 +483,8 @@ Discourse.Composer = Discourse.Model.extend({
var composer = this; var composer = this;
return Ember.Deferred.promise(function(promise) { return Ember.Deferred.promise(function(promise) {
composer.set('composeState', SAVING);
createdPost.save(function(result) { createdPost.save(function(result) {
var addedPost = false, var addedPost = false,
saving = true; saving = true;
@ -515,8 +518,16 @@ Discourse.Composer = Discourse.Model.extend({
if (postStream) { if (postStream) {
postStream.undoPost(createdPost); postStream.undoPost(createdPost);
} }
promise.reject($.parseJSON(error.responseText).errors[0]);
composer.set('composeState', OPEN); composer.set('composeState', OPEN);
// TODO extract error handling code
var parsedError;
try {
parsedError = $.parseJSON(error.responseText).errors[0];
}
catch(ex) {
parsedError = "Unknown error saving post, try again. Error: " + error.status + " " + error.statusText;
}
promise.reject(parsedError);
}); });
}); });
}, },

View File

@ -30,6 +30,7 @@ Discourse.Post = Discourse.Model.extend({
deletedViaTopic: Em.computed.and('firstPost', 'topic.deleted_at'), deletedViaTopic: Em.computed.and('firstPost', 'topic.deleted_at'),
deleted: Em.computed.or('deleted_at', 'deletedViaTopic'), deleted: Em.computed.or('deleted_at', 'deletedViaTopic'),
notDeleted: Em.computed.not('deleted'), notDeleted: Em.computed.not('deleted'),
userDeleted: Em.computed.empty('user_id'),
postDeletedBy: function() { postDeletedBy: function() {
if (this.get('firstPost')) { return this.get('topic.deleted_by'); } if (this.get('firstPost')) { return this.get('topic.deleted_by'); }
@ -224,17 +225,18 @@ Discourse.Post = Discourse.Model.extend({
}, },
/** /**
Deletes a post Changes the state of the post to be deleted. Does not call the server, that should be
done elsewhere.
@method destroy @method setDeletedState
@param {Discourse.User} deleted_by The user deleting the post @param {Discourse.User} deletedBy The user deleting the post
**/ **/
destroy: function(deleted_by) { setDeletedState: function(deletedBy) {
// Moderators can delete posts. Regular users can only trigger a deleted at message. // Moderators can delete posts. Regular users can only trigger a deleted at message.
if (deleted_by.get('staff')) { if (deletedBy.get('staff')) {
this.setProperties({ this.setProperties({
deleted_at: new Date(), deleted_at: new Date(),
deleted_by: deleted_by, deleted_by: deletedBy,
can_delete: false can_delete: false
}); });
} else { } else {
@ -247,7 +249,16 @@ Discourse.Post = Discourse.Model.extend({
user_deleted: true user_deleted: true
}); });
} }
},
/**
Deletes a post
@method destroy
@param {Discourse.User} deletedBy The user deleting the post
**/
destroy: function(deletedBy) {
this.setDeletedState(deletedBy);
return Discourse.ajax("/posts/" + (this.get('id')), { type: 'DELETE' }); return Discourse.ajax("/posts/" + (this.get('id')), { type: 'DELETE' });
}, },
@ -327,8 +338,7 @@ Discourse.Post = Discourse.Model.extend({
// Whether to show replies directly below // Whether to show replies directly below
showRepliesBelow: function() { showRepliesBelow: function() {
var reply_count, topic; var reply_count = this.get('reply_count');
reply_count = this.get('reply_count');
// We don't show replies if there aren't any // We don't show replies if there aren't any
if (reply_count === 0) return false; if (reply_count === 0) return false;
@ -340,7 +350,7 @@ Discourse.Post = Discourse.Model.extend({
if (reply_count > 1) return true; if (reply_count > 1) return true;
// If we have *exactly* one reply, we have to consider if it's directly below us // If we have *exactly* one reply, we have to consider if it's directly below us
topic = this.get('topic'); var topic = this.get('topic');
return !topic.isReplyDirectlyBelow(this); return !topic.isReplyDirectlyBelow(this);
}.property('reply_count'), }.property('reply_count'),
@ -376,11 +386,12 @@ Discourse.Post.reopenClass({
return result; return result;
}, },
deleteMany: function(posts) { deleteMany: function(selectedPosts, selectedReplies) {
return Discourse.ajax("/posts/destroy_many", { return Discourse.ajax("/posts/destroy_many", {
type: 'DELETE', type: 'DELETE',
data: { data: {
post_ids: posts.map(function(p) { return p.get('id'); }) post_ids: selectedPosts.map(function(p) { return p.get('id'); }),
reply_post_ids: selectedReplies.map(function(p) { return p.get('id'); })
} }
}); });
}, },

View File

@ -28,7 +28,11 @@ Discourse.User = Discourse.Model.extend({
searchContext: function() { searchContext: function() {
return ({ type: 'user', id: this.get('username_lower'), user: this }); return {
type: 'user',
id: this.get('username_lower'),
user: this
};
}.property('username_lower'), }.property('username_lower'),
/** /**
@ -101,7 +105,7 @@ Discourse.User = Discourse.Model.extend({
@returns Result of ajax call @returns Result of ajax call
**/ **/
changeUsername: function(newUsername) { changeUsername: function(newUsername) {
return Discourse.ajax("/users/" + (this.get('username_lower')) + "/preferences/username", { return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/username", {
type: 'PUT', type: 'PUT',
data: { new_username: newUsername } data: { new_username: newUsername }
}); });
@ -115,7 +119,7 @@ Discourse.User = Discourse.Model.extend({
@returns Result of ajax call @returns Result of ajax call
**/ **/
changeEmail: function(email) { changeEmail: function(email) {
return Discourse.ajax("/users/" + (this.get('username_lower')) + "/preferences/email", { return Discourse.ajax("/users/" + this.get('username_lower') + "/preferences/email", {
type: 'PUT', type: 'PUT',
data: { email: email } data: { email: email }
}); });
@ -173,9 +177,7 @@ Discourse.User = Discourse.Model.extend({
changePassword: function() { changePassword: function() {
return Discourse.ajax("/session/forgot_password", { return Discourse.ajax("/session/forgot_password", {
dataType: 'json', dataType: 'json',
data: { data: { login: this.get('username') },
login: this.get('username')
},
type: 'POST' type: 'POST'
}); });
}, },
@ -266,11 +268,14 @@ Discourse.User = Discourse.Model.extend({
Change avatar selection Change avatar selection
@method toggleAvatarSelection @method toggleAvatarSelection
@param {Boolean} useUploadedAvatar true if the user is using the uploaded avatar
@returns {Promise} the result of the toggle avatar selection @returns {Promise} the result of the toggle avatar selection
*/ */
toggleAvatarSelection: function() { toggleAvatarSelection: function(useUploadedAvatar) {
var data = { use_uploaded_avatar: this.get("use_uploaded_avatar") }; return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/toggle", {
return Discourse.ajax("/users/" + this.get("username") + "/preferences/avatar/toggle", { type: 'PUT', data: data }); type: 'PUT',
data: { use_uploaded_avatar: useUploadedAvatar }
});
} }
}); });

View File

@ -10,7 +10,7 @@ Discourse.Route.buildRoutes(function() {
// Topic routes // Topic routes
this.resource('topic', { path: '/t/:slug/:id' }, function() { this.resource('topic', { path: '/t/:slug/:id' }, function() {
this.route('fromParams', { path: '/' }); this.route('fromParams', { path: '/' });
this.route('fromParams', { path: '/:nearPost' }); this.route('fromParamsNear', { path: '/:nearPost' });
}); });
// Generate static page routes // Generate static page routes
@ -50,7 +50,8 @@ Discourse.Route.buildRoutes(function() {
}); });
this.resource('userPrivateMessages', { path: '/private-messages' }, function() { this.resource('userPrivateMessages', { path: '/private-messages' }, function() {
this.route('sent', {path: '/messages-sent'}); this.route('mine', {path: '/mine'});
this.route('unread', {path: '/unread'});
}); });
this.resource('preferences', { path: '/preferences' }, function() { this.resource('preferences', { path: '/preferences' }, function() {

View File

@ -18,35 +18,29 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
events: { events: {
showAvatarSelector: function() { showAvatarSelector: function() {
Discourse.Route.showModal(this, 'avatarSelector'); Discourse.Route.showModal(this, 'avatarSelector');
var user = this.modelFor("user"); // all the properties needed for displaying the avatar selector modal
console.log(user); var avatarSelector = this.modelFor('user').getProperties(
this.controllerFor("avatarSelector").setProperties(user.getProperties( 'username', 'email',
"username", 'has_uploaded_avatar', 'use_uploaded_avatar',
"email", 'gravatar_template', 'uploaded_avatar_template');
"has_uploaded_avatar", this.controllerFor('avatarSelector').setProperties(avatarSelector);
"use_uploaded_avatar",
"gravatar_template",
"uploaded_avatar_template"
));
}, },
saveAvatarSelection: function() { saveAvatarSelection: function() {
var user = this.modelFor("user"); var user = this.modelFor('user');
var avatar = this.controllerFor("avatarSelector"); var avatarSelector = this.controllerFor('avatarSelector');
// sends the information to the server if it has changed // sends the information to the server if it has changed
if (avatar.get("use_uploaded_avatar") !== user.get("use_uploaded_avatar")) { user.toggleAvatarSelection(); } if (avatarSelector.get('use_uploaded_avatar') !== user.get('use_uploaded_avatar')) {
// saves the data back user.toggleAvatarSelection(avatarSelector.get('use_uploaded_avatar'));
user.setProperties(avatar.getProperties(
"has_uploaded_avatar",
"use_uploaded_avatar",
"gravatar_template",
"uploaded_avatar_template"
));
if (avatar.get("use_uploaded_avatar")) {
user.set("avatar_template", avatar.get("uploaded_avatar_template"));
} else {
user.set("avatar_template", avatar.get("gravatar_template"));
} }
// saves the data back
user.setProperties(avatarSelector.getProperties(
'has_uploaded_avatar',
'use_uploaded_avatar',
'gravatar_template',
'uploaded_avatar_template'
));
user.set('avatar_template', avatarSelector.get('avatarTemplate'));
} }
} }
}); });

View File

@ -58,4 +58,5 @@ Discourse.TopicFromParamsRoute = Discourse.Route.extend({
}); });
Discourse.TopicFromParamsNearRoute = Discourse.TopicFromParamsRoute;

View File

@ -171,33 +171,26 @@ Discourse.UserTopicListRoute = Discourse.Route.extend({
} }
}); });
Discourse.UserPrivateMessagesIndexRoute = Discourse.UserTopicListRoute.extend({ function createPMRoute(viewName, path, type) {
userActionType: Discourse.UserAction.TYPES.messages_received, return Discourse.UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.messages_received,
model: function() { model: function() {
return Discourse.TopicList.find('topics/private-messages/' + this.modelFor('user').get('username_lower')); return Discourse.TopicList.find('topics/' + path + '/' + this.modelFor('user').get('username_lower'));
}, },
setupController: function(controller, model) { setupController: function(controller, model) {
this._super(controller, model); this._super(controller, model);
controller.set('hideCategories', true); controller.set('hideCategories', true);
this.controllerFor('userActivity').set('pmView', 'index'); this.controllerFor('userActivity').set('pmView', viewName);
} }
});
}
}); Discourse.UserPrivateMessagesIndexRoute = createPMRoute('index', 'private-messages');
Discourse.UserPrivateMessagesSentRoute = Discourse.UserTopicListRoute.extend({ Discourse.UserPrivateMessagesMineRoute = createPMRoute('mine', 'private-messages-sent');
userActionType: Discourse.UserAction.TYPES.messages_sent, Discourse.UserPrivateMessagesUnreadRoute = createPMRoute('unread', 'private-messages-unread');
model: function() {
return Discourse.TopicList.find('topics/private-messages-sent/' + this.modelFor('user').get('username_lower'));
},
setupController: function(controller, model) {
this._super(controller, model);
controller.set('hideCategories', true);
this.controllerFor('userActivity').set('pmView', 'sent');
}
});
Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({ Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({
userActionType: Discourse.UserAction.TYPES.topics, userActionType: Discourse.UserAction.TYPES.topics,
@ -205,7 +198,6 @@ Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({
model: function() { model: function() {
return Discourse.TopicList.find('topics/created-by/' + this.modelFor('user').get('username_lower')); return Discourse.TopicList.find('topics/created-by/' + this.modelFor('user').get('username_lower'));
} }
}); });
Discourse.UserActivityFavoritesRoute = Discourse.UserTopicListRoute.extend({ Discourse.UserActivityFavoritesRoute = Discourse.UserTopicListRoute.extend({

View File

@ -1,12 +1,12 @@
<div class="modal-body"> <div class="modal-body">
<div> <div>
<div> <div>
<input type="radio" id="avatar" name="avatar" value="gravatar" {{action toggleUseUploadedAvatar false}}> <input type="radio" id="avatar" name="avatar" value="gravatar" {{action useGravatar}}>
<label class="radio" for="avatar">{{avatar controller imageSize="large" template="gravatar_template"}} {{{i18n user.change_avatar.gravatar}}} {{email}}</label> <label class="radio" for="avatar">{{avatar controller imageSize="large" template="gravatar_template"}} {{{i18n user.change_avatar.gravatar}}} {{email}}</label>
<a href="//gravatar.com/emails" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn"><i class="icon-pencil"></i></a> <a href="//gravatar.com/emails" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn"><i class="icon-pencil"></i></a>
</div> </div>
<div> <div>
<input type="radio" id="uploaded_avatar" name="avatar" value="uploaded_avatar" {{action toggleUseUploadedAvatar true}}> <input type="radio" id="uploaded_avatar" name="avatar" value="uploaded_avatar" {{action useUploadedAvatar}}>
<label class="radio" for="uploaded_avatar"> <label class="radio" for="uploaded_avatar">
{{#if has_uploaded_avatar}} {{#if has_uploaded_avatar}}
{{boundAvatar controller imageSize="large" template="uploaded_avatar_template"}} {{i18n user.change_avatar.uploaded_avatar}} {{boundAvatar controller imageSize="large" template="uploaded_avatar_template"}} {{i18n user.change_avatar.uploaded_avatar}}

View File

@ -17,15 +17,25 @@
{{/if}} {{/if}}
<div class='topic-meta-data span2'> <div class='topic-meta-data span2'>
<div {{bindAttr class=":contents byTopicCreator:topic-creator"}}> {{#unless userDeleted}}
<a href='{{unbound usernameUrl}}'>{{avatar this imageSize="large"}}</a> <div {{bindAttr class=":contents byTopicCreator:topic-creator"}}>
<h3 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}'>{{breakUp username}}</a></h3> <a href='{{unbound usernameUrl}}'>{{avatar this imageSize="large"}}</a>
{{#if user_title}}<div class="user-title">{{user_title}}</div>{{/if}} <h3 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}'>{{breakUp username}}</a></h3>
</div> {{#if user_title}}<div class="user-title">{{user_title}}</div>{{/if}}
</div>
{{else}}
<div class="contents">
<i class="icon icon-trash deleted-user-avatar"></i>
<h3 class="deleted-username">{{i18n user.deleted}}</h3>
</div>
{{/unless}}
</div> </div>
<div class='topic-body span14'> <div class='topic-body span14'>
<button {{action selectPost this}} {{bindAttr class=":post-select controller.multiSelect::hidden"}}>{{view.selectText}}</button> <div {{bindAttr class=":select-posts controller.multiSelect::hidden"}}>
<button {{action toggledSelectedPostReplies this}} {{bindAttr class="view.canSelectReplies::hidden"}}>{{i18n topic.multi_select.select_replies}}</button>
<button {{action toggledSelectedPost this}} class="select-post">{{view.selectPostText}}</button>
</div>
<div {{bindAttr class="showUserReplyTab:avoid-tab view.repliesShown::bottom-round :contents :regular view.extraClass"}}> <div {{bindAttr class="showUserReplyTab:avoid-tab view.repliesShown::bottom-round :contents :regular view.extraClass"}}>
{{#unless controller.multiSelect}} {{#unless controller.multiSelect}}

View File

@ -16,10 +16,13 @@
<ul class='action-list nav-stacked side-nav'> <ul class='action-list nav-stacked side-nav'>
{{#if privateMessageView}} {{#if privateMessageView}}
<li {{bindAttr class=":noGlyph privateMessagesActive:active"}}> <li {{bindAttr class=":noGlyph privateMessagesActive:active"}}>
{{#linkTo 'userPrivateMessages.index' model}}{{i18n user.private_messages}}{{/linkTo}} {{#linkTo 'userPrivateMessages.index' model}}{{i18n user.messages.all}}{{/linkTo}}
</li> </li>
<li {{bindAttr class=":noGlyph privateMessagesSentActive:active"}}> <li {{bindAttr class=":noGlyph privateMessagesMineActive:active"}}>
{{#linkTo 'userPrivateMessages.sent' model}}{{i18n user.private_messages_sent}}{{/linkTo}} {{#linkTo 'userPrivateMessages.mine' model}}{{i18n user.messages.mine}}{{/linkTo}}
</li>
<li {{bindAttr class=":noGlyph privateMessagesUnreadActive:active"}}>
{{#linkTo 'userPrivateMessages.unread' model}}{{i18n user.messages.unread}}{{/linkTo}}
</li> </li>
{{else}} {{else}}

View File

@ -29,17 +29,20 @@ Discourse.PostView = Discourse.GroupedView.extend({
mouseUp: function(e) { mouseUp: function(e) {
if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) { if (this.get('controller.multiSelect') && (e.metaKey || e.ctrlKey)) {
this.get('controller').selectPost(this.get('post')); this.get('controller').toggledSelectedPost(this.get('post'));
} }
}, },
selected: function() { selected: function() {
var selectedPosts = this.get('controller.selectedPosts'); return this.get('controller').postSelected(this.get('post'));
if (!selectedPosts) return false;
return selectedPosts.contains(this.get('post'));
}.property('controller.selectedPostsCount'), }.property('controller.selectedPostsCount'),
selectText: function() { canSelectReplies: function() {
if (this.get('post.reply_count') === 0) { return false; }
return !this.get('selected');
}.property('post.reply_count', 'selected'),
selectPostText: function() {
return this.get('selected') ? I18n.t('topic.multi_select.selected', { count: this.get('controller.selectedPostsCount') }) : I18n.t('topic.multi_select.select'); return this.get('selected') ? I18n.t('topic.multi_select.selected', { count: this.get('controller.selectedPostsCount') }) : I18n.t('topic.multi_select.select');
}.property('selected', 'controller.selectedPostsCount'), }.property('selected', 'controller.selectedPostsCount'),

View File

@ -1,3 +1,22 @@
/*
This is a fork of markdown-js with a few changes to support discourse:
* We have replaced the strong/em handlers because we prefer them only to work on word
boundaries.
* We removed the maraku support as we don't use it.
* We don't escape the contents of HTML as we prefer to use a whitelist.
* We fixed a bug where references can be created directly following a list.
* Fix to blockquote to handle spaces in front and when nested.
* Note the name BetterMarkdown doesn't mean it's *better* than markdown-js, it refers
to it being better than our previous markdown parser!
*/
// Released under MIT license // Released under MIT license
// Copyright (c) 2009-2010 Dominic Baggott // Copyright (c) 2009-2010 Dominic Baggott
// Copyright (c) 2009-2010 Ash Berlin // Copyright (c) 2009-2010 Ash Berlin
@ -190,6 +209,35 @@ Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) {
return blocks; return blocks;
}; };
function create_attrs() {
if ( !extract_attr( this.tree ) ) {
this.tree.splice( 1, 0, {} );
}
var attrs = extract_attr( this.tree );
// make a references hash if it doesn't exist
if ( attrs.references === undefined ) {
attrs.references = {};
}
return attrs;
}
function create_reference(attrs, m) {
if ( m[2] && m[2][0] == "<" && m[2][m[2].length-1] == ">" )
m[2] = m[2].substring( 1, m[2].length - 1 );
var ref = attrs.references[ m[1].toLowerCase() ] = {
href: m[2]
};
if ( m[4] !== undefined )
ref.title = m[4];
else if ( m[5] !== undefined )
ref.title = m[5];
}
/** /**
* Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ] * Markdown#processBlock( block, next ) -> undefined | [ JsonML, ... ]
* - block (String): the block to process * - block (String): the block to process
@ -516,6 +564,7 @@ Markdown.dialects.Gruber = {
// The matcher function // The matcher function
return function( block, next ) { return function( block, next ) {
var m = block.match( is_list_re ); var m = block.match( is_list_re );
if ( !m ) return undefined; if ( !m ) return undefined;
@ -667,6 +716,7 @@ Markdown.dialects.Gruber = {
})(), })(),
blockquote: function blockquote( block, next ) { blockquote: function blockquote( block, next ) {
if ( !block.match( /^>/m ) ) if ( !block.match( /^>/m ) )
return undefined; return undefined;
@ -702,7 +752,7 @@ Markdown.dialects.Gruber = {
} }
// Strip off the leading "> " and re-process as a block. // Strip off the leading "> " and re-process as a block.
var input = block.replace( /^> ?/gm, "" ), var input = block.replace( /^> */gm, "" ),
old_tree = this.tree, old_tree = this.tree,
processedBlock = this.toTree( input, [ "blockquote" ] ), processedBlock = this.toTree( input, [ "blockquote" ] ),
attr = extract_attr( processedBlock ); attr = extract_attr( processedBlock );
@ -721,39 +771,18 @@ Markdown.dialects.Gruber = {
}, },
referenceDefn: function referenceDefn( block, next) { referenceDefn: function referenceDefn( block, next) {
var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/; var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/;
// interesting matches are [ , ref_id, url, , title, title ] // interesting matches are [ , ref_id, url, , title, title ]
if ( !block.match(re) ) if ( !block.match(re) )
return undefined; return undefined;
// make an attribute node if it doesn't exist var attrs = create_attrs.call(this);
if ( !extract_attr( this.tree ) ) {
this.tree.splice( 1, 0, {} );
}
var attrs = extract_attr( this.tree );
// make a references hash if it doesn't exist
if ( attrs.references === undefined ) {
attrs.references = {};
}
var b = this.loop_re_over_block(re, block, function( m ) { var b = this.loop_re_over_block(re, block, function( m ) {
create_reference(attrs, m);
if ( m[2] && m[2][0] == "<" && m[2][m[2].length-1] == ">" ) });
m[2] = m[2].substring( 1, m[2].length - 1 );
var ref = attrs.references[ m[1].toLowerCase() ] = {
href: m[2]
};
if ( m[4] !== undefined )
ref.title = m[4];
else if ( m[5] !== undefined )
ref.title = m[5];
} );
if ( b.length ) if ( b.length )
next.unshift( mk_block( b, block.trailing ) ); next.unshift( mk_block( b, block.trailing ) );
@ -876,6 +905,7 @@ Markdown.dialects.Gruber.inline = {
"[": function link( text ) { "[": function link( text ) {
var orig = String(text); var orig = String(text);
// Inline content is possible inside `link text` // Inline content is possible inside `link text`
var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "]" ); var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "]" );
@ -939,7 +969,6 @@ Markdown.dialects.Gruber.inline = {
m = text.match( /^\s*\[(.*?)\]/ ); m = text.match( /^\s*\[(.*?)\]/ );
if ( m ) { if ( m ) {
consumed += m[ 0 ].length; consumed += m[ 0 ].length;
// [links][] uses links as its reference // [links][] uses links as its reference
@ -953,6 +982,15 @@ Markdown.dialects.Gruber.inline = {
return [ consumed, link ]; return [ consumed, link ];
} }
m = orig.match(/^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/);
if (m) {
var attrs = create_attrs.call(this);
create_reference(attrs, m);
return [ m[0].length ]
}
// [id] // [id]
// Only if id is plain (no formatting.) // Only if id is plain (no formatting.)
if ( children.length == 1 && typeof children[0] == "string" ) { if ( children.length == 1 && typeof children[0] == "string" ) {
@ -1004,69 +1042,6 @@ Markdown.dialects.Gruber.inline = {
}; };
// Meta Helper/generator method for em and strong handling
function strong_em( tag, md ) {
var state_slot = tag + "_state",
other_slot = tag == "strong" ? "em_state" : "strong_state";
function CloseTag(len) {
this.len_after = len;
this.name = "close_" + md;
}
return function ( text, orig_match ) {
if ( this[state_slot][0] == md ) {
// Most recent em is of this type
//D:this.debug("closing", md);
this[state_slot].shift();
// "Consume" everything to go back to the recrusion in the else-block below
return[ text.length, new CloseTag(text.length-md.length) ];
}
else {
// Store a clone of the em/strong states
var other = this[other_slot].slice(),
state = this[state_slot].slice();
this[state_slot].unshift(md);
//D:this.debug_indent += " ";
// Recurse
var res = this.processInline( text.substr( md.length ) );
//D:this.debug_indent = this.debug_indent.substr(2);
var last = res[res.length - 1];
//D:this.debug("processInline from", tag + ": ", uneval( res ) );
var check = this[state_slot].shift();
if ( last instanceof CloseTag ) {
res.pop();
// We matched! Huzzah.
var consumed = text.length - last.len_after;
return [ consumed, [ tag ].concat(res) ];
}
else {
// Restore the state of the other kind. We might have mistakenly closed it.
this[other_slot] = other;
this[state_slot] = state;
// We can't reuse the processed result as it could have wrong parsing contexts in it.
return [ md.length, md ];
}
}
}; // End returned function
}
Markdown.dialects.Gruber.inline["**"] = strong_em("strong", "**");
Markdown.dialects.Gruber.inline["__"] = strong_em("strong", "__");
Markdown.dialects.Gruber.inline["*"] = strong_em("em", "*");
Markdown.dialects.Gruber.inline["_"] = strong_em("em", "_");
// Build default order from insertion order. // Build default order from insertion order.
Markdown.buildBlockOrder = function(d) { Markdown.buildBlockOrder = function(d) {
var ord = []; var ord = [];
@ -1084,7 +1059,7 @@ Markdown.buildInlinePatterns = function(d) {
for ( var i in d ) { for ( var i in d ) {
// __foo__ is reserved and not a pattern // __foo__ is reserved and not a pattern
if ( i.match( /^__.*__$/) ) continue; if ( i.match( /^__.*__$/) ) continue;
var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" ) var l = i.replace( /([\\.*+?$|()\[\]{}])/g, "\\$1" )
.replace( /\n/, "\\n" ); .replace( /\n/, "\\n" );
patterns.push( i.length == 1 ? l : "(?:" + l + ")" ); patterns.push( i.length == 1 ? l : "(?:" + l + ")" );
} }

View File

@ -355,6 +355,10 @@
font-size: 13px; font-size: 13px;
line-height: 18px; line-height: 18px;
} }
.deleted-user-avatar {
font-size: 36px;
line-height: 36px;
}
.staff a { .staff a {
@include border-radius-all(3px); @include border-radius-all(3px);
@ -496,9 +500,11 @@
} }
&.selected { &.selected {
article.boxed { article.boxed {
.post-select { .select-posts {
background-color: $blue; button.select-post {
color: $white; background-color: $blue;
color: $white;
}
} }
.topic-body { .topic-body {
.contents { .contents {
@ -515,20 +521,23 @@
font-size: 16px; font-size: 16px;
line-height: 20px; line-height: 20px;
.post-select { .select-posts {
@include border-radius-all(4px);
background-color: $light_gray;
border-top: 1px solid $white;
border-left: 1px solid $white;
border-bottom: 1px solid $gray;
border-right: 1px solid $gray;
color: $darkish_gray;
top: 4px;
position: absolute; position: absolute;
right: 5px; right: 5px;
font-size: 12px;
padding: 2px 5px;
z-index: 490; z-index: 490;
top: 4px;
button {
@include border-radius-all(4px);
background-color: $light_gray;
border-top: 1px solid $white;
border-left: 1px solid $white;
border-bottom: 1px solid $gray;
border-right: 1px solid $gray;
color: $darkish_gray;
font-size: 12px;
padding: 2px 5px;
}
} }
img { img {

View File

@ -196,6 +196,16 @@ class ApplicationController < ActionController::Base
user user
end end
def post_ids_including_replies
post_ids = params[:post_ids].map {|p| p.to_i}
if params[:reply_post_ids]
post_ids << PostReply.where(post_id: params[:reply_post_ids].map {|p| p.to_i}).pluck(:reply_id)
post_ids.flatten!
post_ids.uniq!
end
post_ids
end
private private
def preload_anonymous_data def preload_anonymous_data

View File

@ -53,6 +53,14 @@ class ListController < ApplicationController
respond(list) respond(list)
end end
def private_messages_unread
list_opts = build_topic_list_options
list = TopicQuery.new(current_user, list_opts).list_private_messages_unread(fetch_user_from_params)
list.more_topics_url = url_for(topics_private_messages_unread_path(list_opts.merge(format: 'json', page: next_page)))
respond(list)
end
def category def category
query = TopicQuery.new(current_user, page: params[:page]) query = TopicQuery.new(current_user, page: params[:page])

View File

@ -150,16 +150,16 @@ class PostsController < ApplicationController
params.require(:post_ids) params.require(:post_ids)
posts = Post.where(id: params[:post_ids]) posts = Post.where(id: post_ids_including_replies)
raise Discourse::InvalidParameters.new(:post_ids) if posts.blank? raise Discourse::InvalidParameters.new(:post_ids) if posts.blank?
# Make sure we can delete the posts # Make sure we can delete the posts
posts.each {|p| guardian.ensure_can_delete!(p) } posts.each {|p| guardian.ensure_can_delete!(p) }
Post.transaction do Post.transaction do
topic_id = posts.first.topic_id topic_id = posts.first.topic_id
posts.each {|p| p.destroy } posts.each {|p| PostDestroyer.new(current_user, p).destroy }
Topic.reset_highest(topic_id)
end end
render nothing: true render nothing: true

View File

@ -244,7 +244,7 @@ class TopicsController < ApplicationController
topic = Topic.where(id: params[:topic_id]).first topic = Topic.where(id: params[:topic_id]).first
guardian.ensure_can_move_posts!(topic) guardian.ensure_can_move_posts!(topic)
dest_topic = move_post_to_destination(topic) dest_topic = move_posts_to_destination(topic)
render_topic_changes(dest_topic) render_topic_changes(dest_topic)
end end
@ -333,12 +333,12 @@ class TopicsController < ApplicationController
private private
def move_post_to_destination(topic) def move_posts_to_destination(topic)
args = {} args = {}
args[:title] = params[:title] if params[:title].present? args[:title] = params[:title] if params[:title].present?
args[:destination_topic_id] = params[:destination_topic_id].to_i if params[:destination_topic_id].present? args[:destination_topic_id] = params[:destination_topic_id].to_i if params[:destination_topic_id].present?
topic.move_posts(current_user, params[:post_ids].map {|p| p.to_i}, args) topic.move_posts(current_user, post_ids_including_replies, args)
end end
end end

View File

@ -11,7 +11,8 @@ class Users::OmniauthCallbacksController < ApplicationController
Auth::OpenIdAuthenticator.new("yahoo", "https://me.yahoo.com", trusted: true), Auth::OpenIdAuthenticator.new("yahoo", "https://me.yahoo.com", trusted: true),
Auth::GithubAuthenticator.new, Auth::GithubAuthenticator.new,
Auth::TwitterAuthenticator.new, Auth::TwitterAuthenticator.new,
Auth::PersonaAuthenticator.new Auth::PersonaAuthenticator.new,
Auth::CasAuthenticator.new
] ]
skip_before_filter :redirect_to_login_if_required skip_before_filter :redirect_to_login_if_required
@ -37,9 +38,13 @@ class Users::OmniauthCallbacksController < ApplicationController
@data = authenticator.after_authenticate(auth) @data = authenticator.after_authenticate(auth)
@data.authenticator_name = authenticator.name @data.authenticator_name = authenticator.name
user_found(@data.user) if @data.user if @data.user
user_found(@data.user)
session[:authentication] = @data.session_data elsif SiteSetting.invite_only?
@data.requires_invite = true
else
session[:authentication] = @data.session_data
end
respond_to do |format| respond_to do |format|
format.html format.html
@ -87,7 +92,7 @@ class Users::OmniauthCallbacksController < ApplicationController
session[:authentication] = nil session[:authentication] = nil
@data.authenticated = true @data.authenticated = true
else else
if SiteSetting.invite_only? if SiteSetting.must_approve_users? && !user.approved?
@data.awaiting_approval = true @data.awaiting_approval = true
else else
@data.awaiting_activation = true @data.awaiting_activation = true

View File

@ -18,12 +18,7 @@ class CategoryFeaturedTopic < ActiveRecord::Base
CategoryFeaturedTopic.transaction do CategoryFeaturedTopic.transaction do
CategoryFeaturedTopic.delete_all(category_id: c.id) CategoryFeaturedTopic.delete_all(category_id: c.id)
# fake an admin query = TopicQuery.new(self.fake_admin, per_page: SiteSetting.category_featured_topics, except_topic_id: c.topic_id, visible: true)
admin = User.new
admin.admin = true
admin.id = -1
query = TopicQuery.new(admin, per_page: SiteSetting.category_featured_topics, except_topic_id: c.topic_id, visible: true)
results = query.list_category(c) results = query.list_category(c)
if results.present? if results.present?
results.topic_ids.each_with_index do |topic_id, idx| results.topic_ids.each_with_index do |topic_id, idx|
@ -33,6 +28,15 @@ class CategoryFeaturedTopic < ActiveRecord::Base
end end
end end
private
def self.fake_admin
# fake an admin
admin = User.new
admin.admin = true
admin.id = -1
admin
end
end end
# == Schema Information # == Schema Information

View File

@ -8,6 +8,24 @@ InviteRedeemer = Struct.new(:invite) do
invited_user invited_user
end end
# extracted from User cause it is very specific to invites
def self.create_user_for_email(email)
username = UserNameSuggester.suggest(email)
DiscourseHub.nickname_operation do
match, available, suggestion = DiscourseHub.nickname_match?(username, email)
username = suggestion unless match || available
end
user = User.new(email: email, username: username, name: username, active: true)
user.trust_level = SiteSetting.default_invitee_trust_level
user.save!
DiscourseHub.nickname_operation { DiscourseHub.register_nickname(username, email) }
user
end
private private
def invited_user def invited_user
@ -34,7 +52,7 @@ InviteRedeemer = Struct.new(:invite) do
def get_invited_user def get_invited_user
result = get_existing_user result = get_existing_user
result ||= create_new_user result ||= InviteRedeemer.create_user_for_email(invite.email)
result.send_welcome_message = false result.send_welcome_message = false
result result
end end
@ -43,9 +61,6 @@ InviteRedeemer = Struct.new(:invite) do
User.where(email: invite.email).first User.where(email: invite.email).first
end end
def create_new_user
User.create_for_email(invite.email, trust_level: SiteSetting.default_invitee_trust_level)
end
def add_to_private_topics_if_invited def add_to_private_topics_if_invited
invite.topics.private_messages.each do |t| invite.topics.private_messages.each do |t|

View File

@ -0,0 +1,18 @@
require_dependency 'enum_site_setting'
class MinTrustToCreateTopicSetting < EnumSiteSetting
def self.valid_value?(val)
valid_values.any? { |v| v.to_s == val.to_s }
end
def self.values
@values ||= valid_values.map {|x| {name: x.to_s, value: x} }
end
private
def self.valid_values
TrustLevel.levels.values.sort
end
end

View File

@ -2,3 +2,22 @@ class Oauth2UserInfo < ActiveRecord::Base
belongs_to :user belongs_to :user
end end
# == Schema Information
#
# Table name: oauth2_user_infos
#
# id :integer not null, primary key
# user_id :integer not null
# uid :string(255) not null
# provider :string(255) not null
# email :string(255)
# name :string(255)
# created_at :datetime not null
# updated_at :datetime not null
#
# Indexes
#
# index_oauth2_user_infos_on_uid_and_provider (uid,provider) UNIQUE
#

View File

@ -1,2 +1,18 @@
class PluginStoreRow < ActiveRecord::Base class PluginStoreRow < ActiveRecord::Base
end end
# == Schema Information
#
# Table name: plugin_store_rows
#
# id :integer not null, primary key
# plugin_name :string(255) not null
# key :string(255) not null
# type_name :string(255) not null
# value :text
#
# Indexes
#
# index_plugin_store_rows_on_plugin_name_and_key (plugin_name,key) UNIQUE
#

View File

@ -45,7 +45,6 @@ class Post < ActiveRecord::Base
scope :public_posts, -> { joins(:topic).where('topics.archetype <> ?', Archetype.private_message) } scope :public_posts, -> { joins(:topic).where('topics.archetype <> ?', Archetype.private_message) }
scope :private_posts, -> { joins(:topic).where('topics.archetype = ?', Archetype.private_message) } scope :private_posts, -> { joins(:topic).where('topics.archetype = ?', Archetype.private_message) }
scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) } scope :with_topic_subtype, ->(subtype) { joins(:topic).where('topics.subtype = ?', subtype) }
scope :without_nuked_users, -> { where(nuked_user: false) }
def self.hidden_reasons def self.hidden_reasons
@hidden_reasons ||= Enum.new(:flag_threshold_reached, :flag_threshold_reached_again, :new_user_spam_threshold_reached) @hidden_reasons ||= Enum.new(:flag_threshold_reached, :flag_threshold_reached_again, :new_user_spam_threshold_reached)
@ -383,7 +382,7 @@ end
# Table name: posts # Table name: posts
# #
# id :integer not null, primary key # id :integer not null, primary key
# user_id :integer not null # user_id :integer
# topic_id :integer not null # topic_id :integer not null
# post_number :integer not null # post_number :integer not null
# raw :text not null # raw :text not null
@ -419,7 +418,6 @@ end
# notify_user_count :integer default(0), not null # notify_user_count :integer default(0), not null
# like_score :integer default(0), not null # like_score :integer default(0), not null
# deleted_by_id :integer # deleted_by_id :integer
# nuked_user :boolean default(FALSE)
# #
# Indexes # Indexes
# #

View File

@ -23,3 +23,23 @@ class ScreenedEmail < ActiveRecord::Base
end end
end end
# == Schema Information
#
# Table name: screened_emails
#
# id :integer not null, primary key
# email :string(255) not null
# action_type :integer not null
# match_count :integer default(0), not null
# last_match_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# ip_address :string
#
# Indexes
#
# index_blocked_emails_on_email (email) UNIQUE
# index_blocked_emails_on_last_match_at (last_match_at)
#

View File

@ -24,3 +24,24 @@ class ScreenedUrl < ActiveRecord::Base
find_by_url(url) || create(opts.slice(:action_type, :ip_address).merge(url: url, domain: domain)) find_by_url(url) || create(opts.slice(:action_type, :ip_address).merge(url: url, domain: domain))
end end
end end
# == Schema Information
#
# Table name: screened_urls
#
# id :integer not null, primary key
# url :string(255) not null
# domain :string(255) not null
# action_type :integer not null
# match_count :integer default(0), not null
# last_match_at :datetime
# created_at :datetime not null
# updated_at :datetime not null
# ip_address :string
#
# Indexes
#
# index_screened_urls_on_last_match_at (last_match_at)
# index_screened_urls_on_url (url) UNIQUE
#

View File

@ -67,7 +67,7 @@ class SiteSetting < ActiveRecord::Base
setting(:num_flags_to_block_new_user, 3) setting(:num_flags_to_block_new_user, 3)
setting(:num_users_to_block_new_user, 3) setting(:num_users_to_block_new_user, 3)
setting(:notify_mods_when_user_blocked, true) setting(:notify_mods_when_user_blocked, false)
# used mainly for dev, force hostname for Discourse.base_url # used mainly for dev, force hostname for Discourse.base_url
# You would usually use multisite for this # You would usually use multisite for this
@ -205,6 +205,8 @@ class SiteSetting < ActiveRecord::Base
setting(:regular_requires_likes_given, 1) setting(:regular_requires_likes_given, 1)
setting(:regular_requires_topic_reply_count, 3) setting(:regular_requires_topic_reply_count, 3)
setting(:min_trust_to_create_topic, 0, enum: 'MinTrustToCreateTopicSetting')
# Reply by Email Settings # Reply by Email Settings
setting(:reply_by_email_enabled, false) setting(:reply_by_email_enabled, false)
setting(:reply_by_email_address, '') setting(:reply_by_email_address, '')

View File

@ -53,11 +53,15 @@ end
# context :string(255) # context :string(255)
# ip_address :string(255) # ip_address :string(255)
# email :string(255) # email :string(255)
# subject :text
# previous_value :text
# new_value :text
# #
# Indexes # Indexes
# #
# index_staff_action_logs_on_action_and_id (action,id) # index_staff_action_logs_on_action_and_id (action,id)
# index_staff_action_logs_on_staff_user_id_and_id (staff_user_id,id) # index_staff_action_logs_on_staff_user_id_and_id (staff_user_id,id)
# index_staff_action_logs_on_subject_and_id (subject,id)
# index_staff_action_logs_on_target_user_id_and_id (target_user_id,id) # index_staff_action_logs_on_target_user_id_and_id (target_user_id,id)
# #

View File

@ -10,6 +10,16 @@ class Topic < ActiveRecord::Base
include ActionView::Helpers::SanitizeHelper include ActionView::Helpers::SanitizeHelper
include RateLimiter::OnCreateRecord include RateLimiter::OnCreateRecord
include Trashable include Trashable
extend Forwardable
def_delegator :featured_users, :user_ids, :featured_user_ids
def_delegator :featured_users, :choose, :feature_topic_users
def_delegator :notifier, :watch!, :notify_watch!
def_delegator :notifier, :tracking!, :notify_tracking!
def_delegator :notifier, :regular!, :notify_regular!
def_delegator :notifier, :muted!, :notify_muted!
def_delegator :notifier, :toggle_mute, :toggle_mute
def self.max_sort_order def self.max_sort_order
2**31 - 1 2**31 - 1
@ -21,14 +31,6 @@ class Topic < ActiveRecord::Base
@featured_users ||= TopicFeaturedUsers.new(self) @featured_users ||= TopicFeaturedUsers.new(self)
end end
def featured_user_ids
featured_users.user_ids
end
def feature_topic_users(args={})
featured_users.choose(args)
end
def trash!(trashed_by=nil) def trash!(trashed_by=nil)
update_category_topic_count_by(-1) if deleted_at.nil? update_category_topic_count_by(-1) if deleted_at.nil?
super(trashed_by) super(trashed_by)
@ -561,34 +563,12 @@ class Topic < ActiveRecord::Base
@topic_notifier ||= TopicNotifier.new(self) @topic_notifier ||= TopicNotifier.new(self)
end end
# notification stuff
def notify_watch!(user)
notifier.watch! user
end
def notify_tracking!(user)
notifier.tracking! user
end
def notify_regular!(user)
notifier.regular! user
end
def notify_muted!(user)
notifier.muted! user
end
def muted?(user) def muted?(user)
if user && user.id if user && user.id
notifier.muted?(user.id) notifier.muted?(user.id)
end end
end end
# Enable/disable the mute on the topic
def toggle_mute(user_id)
notifier.toggle_mute user_id
end
def auto_close_days=(num_days) def auto_close_days=(num_days)
@ignore_category_auto_close = true @ignore_category_auto_close = true
set_auto_close(num_days) set_auto_close(num_days)

View File

@ -204,7 +204,7 @@ end
# #
# Indexes # Indexes
# #
# index_forum_thread_links_on_forum_thread_id (topic_id) # index_forum_thread_links_on_forum_thread_id (topic_id)
# index_forum_thread_links_on_forum_thread_id_and_post_id_and_url (topic_id,post_id,url) UNIQUE # unique_post_links (topic_id,post_id,url) UNIQUE
# #

View File

@ -53,6 +53,6 @@ end
# #
# Indexes # Indexes
# #
# index_forum_thread_link_clicks_on_forum_thread_link_id (topic_link_id) # by_link (topic_link_id)
# #

View File

@ -13,22 +13,22 @@ class User < ActiveRecord::Base
include Roleable include Roleable
has_many :posts has_many :posts
has_many :notifications has_many :notifications, dependent: :destroy
has_many :topic_users has_many :topic_users, dependent: :destroy
has_many :topics has_many :topics
has_many :user_open_ids, dependent: :destroy has_many :user_open_ids, dependent: :destroy
has_many :user_actions has_many :user_actions, dependent: :destroy
has_many :post_actions has_many :post_actions, dependent: :destroy
has_many :email_logs has_many :email_logs, dependent: :destroy
has_many :post_timings has_many :post_timings
has_many :topic_allowed_users has_many :topic_allowed_users, dependent: :destroy
has_many :topics_allowed, through: :topic_allowed_users, source: :topic has_many :topics_allowed, through: :topic_allowed_users, source: :topic
has_many :email_tokens has_many :email_tokens, dependent: :destroy
has_many :views has_many :views
has_many :user_visits has_many :user_visits, dependent: :destroy
has_many :invites has_many :invites, dependent: :destroy
has_many :topic_links has_many :topic_links, dependent: :destroy
has_many :uploads has_many :uploads, dependent: :destroy
has_one :facebook_user_info, dependent: :destroy has_one :facebook_user_info, dependent: :destroy
has_one :twitter_user_info, dependent: :destroy has_one :twitter_user_info, dependent: :destroy
@ -37,11 +37,11 @@ class User < ActiveRecord::Base
has_one :oauth2_user_info, dependent: :destroy has_one :oauth2_user_info, dependent: :destroy
belongs_to :approved_by, class_name: 'User' belongs_to :approved_by, class_name: 'User'
has_many :group_users has_many :group_users, dependent: :destroy
has_many :groups, through: :group_users has_many :groups, through: :group_users
has_many :secure_categories, through: :groups, source: :categories has_many :secure_categories, through: :groups, source: :categories
has_one :user_search_data has_one :user_search_data, dependent: :destroy
belongs_to :uploaded_avatar, class_name: 'Upload', dependent: :destroy belongs_to :uploaded_avatar, class_name: 'Upload', dependent: :destroy
@ -61,6 +61,12 @@ class User < ActiveRecord::Base
after_create :create_email_token after_create :create_email_token
before_destroy do
# These tables don't have primary keys, so destroying them with activerecord is tricky:
PostTiming.delete_all(user_id: self.id)
View.delete_all(user_id: self.id)
end
# Whether we need to be sending a system message after creation # Whether we need to be sending a system message after creation
attr_accessor :send_welcome_message attr_accessor :send_welcome_message
@ -96,23 +102,6 @@ class User < ActiveRecord::Base
user user
end end
def self.create_for_email(email, opts={})
username = UserNameSuggester.suggest(email)
discourse_hub_nickname_operation do
match, available, suggestion = DiscourseHub.nickname_match?(username, email)
username = suggestion unless match || available
end
user = User.new(email: email, username: username, name: username)
user.trust_level = opts[:trust_level] if opts[:trust_level].present?
user.save!
discourse_hub_nickname_operation { DiscourseHub.register_nickname(username, email) }
user
end
def self.suggest_name(email) def self.suggest_name(email)
return "" unless email return "" unless email
name = email.split(/[@\+]/)[0] name = email.split(/[@\+]/)[0]
@ -154,7 +143,7 @@ class User < ActiveRecord::Base
self.username = new_username self.username = new_username
if current_username.downcase != new_username.downcase && valid? if current_username.downcase != new_username.downcase && valid?
User.discourse_hub_nickname_operation { DiscourseHub.change_nickname(current_username, new_username) } DiscourseHub.nickname_operation { DiscourseHub.change_nickname(current_username, new_username) }
end end
save save
@ -612,17 +601,6 @@ class User < ActiveRecord::Base
private private
def self.discourse_hub_nickname_operation
if SiteSetting.call_discourse_hub?
begin
yield
rescue DiscourseHub::NicknameUnavailable
false
rescue => e
Rails.logger.error e.message + "\n" + e.backtrace.join("\n")
end
end
end
end end
# == Schema Information # == Schema Information
@ -647,7 +625,7 @@ end
# website :string(255) # website :string(255)
# admin :boolean default(FALSE), not null # admin :boolean default(FALSE), not null
# last_emailed_at :datetime # last_emailed_at :datetime
# email_digests :boolean default(TRUE), not null # email_digests :boolean not null
# trust_level :integer not null # trust_level :integer not null
# bio_cooked :text # bio_cooked :text
# email_private_messages :boolean default(TRUE) # email_private_messages :boolean default(TRUE)
@ -657,7 +635,7 @@ end
# approved_at :datetime # approved_at :datetime
# topics_entered :integer default(0), not null # topics_entered :integer default(0), not null
# posts_read_count :integer default(0), not null # posts_read_count :integer default(0), not null
# digest_after_days :integer default(7), not null # digest_after_days :integer
# previous_visit_at :datetime # previous_visit_at :datetime
# banned_at :datetime # banned_at :datetime
# banned_till :datetime # banned_till :datetime
@ -690,3 +668,4 @@ end
# index_users_on_username (username) UNIQUE # index_users_on_username (username) UNIQUE
# index_users_on_username_lower (username_lower) UNIQUE # index_users_on_username_lower (username_lower) UNIQUE
# #

View File

@ -196,10 +196,13 @@ ORDER BY p.created_at desc
group_ids = topic.category.groups.pluck("groups.id") group_ids = topic.category.groups.pluck("groups.id")
end end
MessageBus.publish("/users/#{action.user.username.downcase}", if action.user
action.id, MessageBus.publish("/users/#{action.user.username.downcase}",
user_ids: [user_id], action.id,
group_ids: group_ids ) user_ids: [user_id],
group_ids: group_ids )
end
action action
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique

View File

@ -8,15 +8,15 @@ class BasicPostSerializer < ApplicationSerializer
:cooked :cooked
def name def name
object.user.name object.user.try(:name)
end end
def username def username
object.user.username object.user.try(:username)
end end
def avatar_template def avatar_template
object.user.avatar_template object.user.try(:avatar_template)
end end
def cooked def cooked

View File

@ -46,11 +46,11 @@ class PostSerializer < BasicPostSerializer
def moderator? def moderator?
object.user.moderator? object.user.try(:moderator?) || false
end end
def staff? def staff?
object.user.staff? object.user.try(:staff?) || false
end end
def yours def yours
@ -70,7 +70,7 @@ class PostSerializer < BasicPostSerializer
end end
def display_username def display_username
object.user.name object.user.try(:name)
end end
def link_counts def link_counts
@ -101,11 +101,11 @@ class PostSerializer < BasicPostSerializer
end end
def user_title def user_title
object.user.title object.user.try(:title)
end end
def trust_level def trust_level
object.user.trust_level object.user.try(:trust_level)
end end
def reply_to_user def reply_to_user

View File

@ -15,15 +15,13 @@ module PostStreamSerializerMixin
@highest_number_in_posts = 0 @highest_number_in_posts = 0
if object.posts.present? if object.posts.present?
object.posts.each_with_index do |p, idx| object.posts.each_with_index do |p, idx|
if p.user @highest_number_in_posts = p.post_number if p.post_number > @highest_number_in_posts
@highest_number_in_posts = p.post_number if p.post_number > @highest_number_in_posts ps = PostSerializer.new(p, scope: scope, root: false)
ps = PostSerializer.new(p, scope: scope, root: false) ps.topic_slug = object.topic.slug
ps.topic_slug = object.topic.slug ps.topic_view = object
ps.topic_view = object p.topic = object.topic
p.topic = object.topic
@posts << ps.as_json @posts << ps.as_json
end
end end
end end
@posts @posts

View File

@ -15,12 +15,6 @@
})(); })();
</script> </script>
<%# load the selected locale before any other scripts %>
<%= javascript_include_tag "locales/#{I18n.locale}" %>
<%= javascript_include_tag "application" %>
<%- if staff? %>
<%= javascript_include_tag "admin"%>
<%- end %>
<script> <script>
Discourse.CDN = '<%= Rails.configuration.action_controller.asset_host %>'; Discourse.CDN = '<%= Rails.configuration.action_controller.asset_host %>';

View File

@ -13,6 +13,12 @@
<link rel="apple-touch-icon" type="image/png" href="<%=SiteSetting.apple_touch_icon_url%>"> <link rel="apple-touch-icon" type="image/png" href="<%=SiteSetting.apple_touch_icon_url%>">
<%= javascript_include_tag "preload_store" %> <%= javascript_include_tag "preload_store" %>
<%= javascript_include_tag "locales/#{I18n.locale}" %>
<%= javascript_include_tag "application" %>
<%- if staff? %>
<%= javascript_include_tag "admin"%>
<%- end %>
<%= render :partial => "common/special_font_face" %> <%= render :partial => "common/special_font_face" %>
<%= render :partial => "common/discourse_stylesheet" %> <%= render :partial => "common/discourse_stylesheet" %>
@ -26,24 +32,6 @@
<%=SiteCustomization.custom_header(session[:preview_style])%> <%=SiteCustomization.custom_header(session[:preview_style])%>
<section id='main'> <section id='main'>
<noscript data-path="<%= request.env['PATH_INFO'] %>">
<header class="d-header">
<div class="container">
<div class="contents">
<div class="row">
<div class="title span13">
<a href="/"><img src="<%=SiteSetting.logo_url%>" alt="<%=SiteSetting.title%>" id="site-logo"></a>
</div>
</div>
</div>
</div>
</header>
<div id="main-outlet" class="container">
<!-- preload-content: -->
<%= yield %>
<!-- :preload-content -->
</div>
</noscript>
</section> </section>
<% unless current_user %> <% unless current_user %>
@ -70,6 +58,24 @@
<%= render :partial => "common/discourse_javascript" %> <%= render :partial => "common/discourse_javascript" %>
<%= render_google_analytics_code %> <%= render_google_analytics_code %>
<noscript data-path="<%= request.env['PATH_INFO'] %>">
<header class="d-header">
<div class="container">
<div class="contents">
<div class="row">
<div class="title span13">
<a href="/"><img src="<%=SiteSetting.logo_url%>" alt="<%=SiteSetting.title%>" id="site-logo"></a>
</div>
</div>
</div>
</div>
</header>
<div id="main-outlet" class="container">
<!-- preload-content: -->
<%= yield %>
<!-- :preload-content -->
</div>
</noscript>
<!-- Discourse Version: <%= Discourse::VERSION::STRING %> --> <!-- Discourse Version: <%= Discourse::VERSION::STRING %> -->
<!-- Git Version: <%= Discourse.git_version %> --> <!-- Git Version: <%= Discourse.git_version %> -->
</body> </body>

View File

@ -66,4 +66,9 @@ Discourse::Application.configure do
# For origin pull cdns all you need to do is register an account and configure # For origin pull cdns all you need to do is register an account and configure
# config.action_controller.asset_host = "http://YOUR_CDN_HERE" # config.action_controller.asset_host = "http://YOUR_CDN_HERE"
# a comma delimited list of emails your devs have
# developers have god like rights and may impersonate anyone in the system
# normal admins may only impersonate other moderators (not admins)
config.developer_emails = []
end end

View File

@ -4,6 +4,8 @@ Discourse::Application.configure do
# Code is not reloaded between requests # Code is not reloaded between requests
config.cache_classes = true config.cache_classes = true
config.log_level = :info
# Full error reports are disabled and caching is turned on # Full error reports are disabled and caching is turned on
config.consider_all_requests_local = false config.consider_all_requests_local = false
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
@ -37,7 +39,7 @@ Discourse::Application.configure do
config.handlebars.precompile = true config.handlebars.precompile = true
# this setting enable rack_cache so it caches various requests in redis # this setting enable rack_cache so it caches various requests in redis
# config.enable_rack_cache = true config.enable_rack_cache = false
# allows users to use mini profiler # allows users to use mini profiler
config.enable_mini_profiler = false config.enable_mini_profiler = false

View File

@ -1,6 +1,7 @@
# If Mini Profiler is included via gem # If Mini Profiler is included via gem
if Rails.configuration.respond_to?(:enable_mini_profiler) && Rails.configuration.enable_mini_profiler if Rails.configuration.respond_to?(:enable_mini_profiler) && Rails.configuration.enable_mini_profiler
require 'rack-mini-profiler' require 'rack-mini-profiler'
require 'flamegraph'
# initialization is skipped so trigger it # initialization is skipped so trigger it
Rack::MiniProfilerRails.initialize!(Rails.application) Rack::MiniProfilerRails.initialize!(Rails.application)
end end
@ -41,6 +42,9 @@ if defined?(Rack::MiniProfiler)
Rack::MiniProfiler.config.backtrace_ignores << /config\/initializers\/silence_logger/ Rack::MiniProfiler.config.backtrace_ignores << /config\/initializers\/silence_logger/
Rack::MiniProfiler.config.backtrace_ignores << /config\/initializers\/quiet_logger/ Rack::MiniProfiler.config.backtrace_ignores << /config\/initializers\/quiet_logger/
# Rack::MiniProfiler.counter_method(ActiveRecord::QueryMethods, 'build_arel')
# Rack::MiniProfiler.counter_method(Array, 'uniq')
# require "#{Rails.root}/vendor/backports/notification" # require "#{Rails.root}/vendor/backports/notification"
# inst = Class.new # inst = Class.new

View File

@ -1,10 +1,13 @@
# We have had lots of config issues with SECRET_TOKEN to avoid this mess we are moving it to redis # We have had lots of config issues with SECRET_TOKEN to avoid this mess we are moving it to redis
# if you feel strongly that it does not belong there use ENV['SECRET_TOKEN'] # if you feel strongly that it does not belong there use ENV['SECRET_TOKEN']
# #
token = ENV['SECRET_TOKEN'] || $redis.get('SECRET_TOKEN') token = ENV['SECRET_TOKEN']
unless token && token.length == 128 unless token
token = SecureRandom.hex(64) token = $redis.get('SECRET_TOKEN')
$redis.set('SECRET_TOKEN',token) unless token && token.length == 128
token = SecureRandom.hex(64)
$redis.set('SECRET_TOKEN',token)
end
end end
Discourse::Application.config.secret_token = token Discourse::Application.config.secret_token = token

View File

@ -5,6 +5,11 @@ Sidekiq.configure_server do |config|
Sidetiq::Clock.start! Sidetiq::Clock.start!
end end
Sidekiq.configure_client { |config| config.redis = sidekiq_redis } Sidetiq.configure do |config|
# we only check for new jobs once every 5 seconds
# to cut down on cpu cost
config.resolution = 5
end
Sidekiq.configure_client { |config| config.redis = sidekiq_redis }
Sidekiq.logger.level = Logger::WARN Sidekiq.logger.level = Logger::WARN

View File

@ -92,6 +92,7 @@ predef:
- find - find
- sinon - sinon
- controllerFor - controllerFor
- testController
- Favcount - Favcount
browser: true # true if the standard browser globals should be predefined browser: true # true if the standard browser globals should be predefined

View File

@ -20,9 +20,6 @@ de:
mb: MB mb: MB
tb: TB tb: TB
dates: dates:
short_date_no_year: "D MMM"
short_date: "D. MMM YYYY"
long_date: "D. MMMM YYYY, H:mm"
tiny: tiny:
half_a_minute: "< 1Min" half_a_minute: "< 1Min"
less_than_x_seconds: less_than_x_seconds:
@ -43,12 +40,6 @@ de:
x_days: x_days:
one: "1T" one: "1T"
other: "%{count}T" other: "%{count}T"
about_x_months:
one: "1Mon"
other: "%{count}Mon"
x_months:
one: "1Mon"
other: "%{count}Mon"
about_x_years: about_x_years:
one: "1J" one: "1J"
other: "%{count}J" other: "%{count}J"
@ -93,6 +84,7 @@ de:
yes_value: "Ja" yes_value: "Ja"
of_value: "von" of_value: "von"
generic_error: "Entschuldigung, ein Fehler ist aufgetreten." generic_error: "Entschuldigung, ein Fehler ist aufgetreten."
generic_error_with_reason: "Ein Fehler ist aufgetreten: %{error}"
log_in: "Anmelden" log_in: "Anmelden"
age: "Alter" age: "Alter"
last_post: "Letzter Beitrag" last_post: "Letzter Beitrag"
@ -101,10 +93,20 @@ de:
show_more: "zeige mehr" show_more: "zeige mehr"
links: Links links: Links
faq: "FAQ" faq: "FAQ"
privacy_policy: "Datenschutzrichtlinie"
you: "Du" you: "Du"
or: "oder" or: "oder"
now: "gerade eben" now: "gerade eben"
read_more: 'weiterlesen' read_more: 'weiterlesen'
more: "Mehr"
less: "Weniger"
never: "nie"
daily: "täglich"
weekly: "wöchentlich"
every_two_weeks: "jede zweite Woche"
character_count:
one: "{{count}} Zeichen"
other: "{{count}} Zeichen"
in_n_seconds: in_n_seconds:
one: "in einer Sekunde" one: "in einer Sekunde"
@ -137,6 +139,10 @@ de:
saving: "Wird gespeichert..." saving: "Wird gespeichert..."
saved: "Gespeichert!" saved: "Gespeichert!"
upload: "Hochladen"
uploading: "Hochladen..."
uploaded: "Hochgeladen!"
choose_topic: choose_topic:
none_found: "Keine Themen gefunden." none_found: "Keine Themen gefunden."
title: title:
@ -175,6 +181,7 @@ de:
"13": "Eingänge" "13": "Eingänge"
user: user:
said: "{{username}} sagte:"
profile: Profil profile: Profil
title: "Benutzer" title: "Benutzer"
mute: Ignorieren mute: Ignorieren
@ -182,6 +189,7 @@ de:
download_archive: "Archiv meiner Beiträge herunterladen" download_archive: "Archiv meiner Beiträge herunterladen"
private_message: "Private Nachricht" private_message: "Private Nachricht"
private_messages: "Nachrichten" private_messages: "Nachrichten"
private_messages_sent: "Gesendete Nachrichten"
activity_stream: "Aktivität" activity_stream: "Aktivität"
preferences: "Einstellungen" preferences: "Einstellungen"
bio: "Über mich" bio: "Über mich"
@ -191,30 +199,41 @@ de:
dynamic_favicon: "Zeige eingehende Nachrichten im Favicon" dynamic_favicon: "Zeige eingehende Nachrichten im Favicon"
external_links_in_new_tab: "Öffne alle externen Links in neuen Tabs" external_links_in_new_tab: "Öffne alle externen Links in neuen Tabs"
enable_quoting: "Markierten Text bei Antwort zitieren" enable_quoting: "Markierten Text bei Antwort zitieren"
change: "ändern"
moderator: "{{user}} ist Moderator" moderator: "{{user}} ist Moderator"
admin: "{{user}} ist Administrator" admin: "{{user}} ist Administrator"
change_password: change_password:
action: "ändern"
success: "(Mail gesendet)" success: "(Mail gesendet)"
in_progress: "(sende Mail)" in_progress: "(sende Mail)"
error: "(Fehler)" error: "(Fehler)"
action: "Passwort zurücksetzten Mail senden"
change_about:
title: "Über mich ändern"
change_username: change_username:
action: "ändern"
title: "Benutzername ändern" title: "Benutzername ändern"
confirm: "Den Benutzernamen zu ändern kann Konsequenzen nach sich ziehen. Bist Du sicher, dass du fortfahren willst?" confirm: "Den Benutzernamen zu ändern kann Konsequenzen nach sich ziehen. Bist Du sicher, dass du fortfahren willst?"
taken: "Entschuldige, der Benutzername ist schon vergeben." taken: "Entschuldige, der Benutzername ist schon vergeben."
error: "Beim Ändern des Benutzernamens ist ein Fehler aufgetreten." error: "Beim Ändern des Benutzernamens ist ein Fehler aufgetreten."
invalid: "Dieser Benutzername ist ungültig, sie dürfen nur aus Zahlen und Buchstaben bestehen." invalid: "Dieser Benutzername ist ungültig, sie dürfen nur aus Zahlen und Buchstaben bestehen."
change_email: change_email:
action: 'ändern'
title: "Mailadresse ändern" title: "Mailadresse ändern"
taken: "Entschuldige, diese Mailadresse ist nicht verfügbar." taken: "Entschuldige, diese Mailadresse ist nicht verfügbar."
error: "Beim ändern der Mailadresse ist ein Fehler aufgetreten. Möglicherweise wird diese Adresse schon benutzt." error: "Beim ändern der Mailadresse ist ein Fehler aufgetreten. Möglicherweise wird diese Adresse schon benutzt."
success: "Eine Bestätigungsmail wurde an diese Adresse verschickt. Bitte folge den darin enthaltenen Anweisungen." success: "Eine Bestätigungsmail wurde an diese Adresse verschickt. Bitte folge den darin enthaltenen Anweisungen."
change_avatar:
title: "Ändere dein Avatar"
gravatar: "<a href='//gravatar.com/emails' target='_blank'>Gravatar</a>, basierend auf"
gravatar_title: "Wechsle dein Avatar auf der Gravatar Webseite"
uploaded_avatar: "Eigenes Bild"
uploaded_avatar_empty: "Eigenes Bild hinzufügen"
upload_title: "Lade dein Bild hoch"
image_is_not_a_square: "Achtung: wir haben den Bild angeschnitten, da es nicht rechteckig war."
email: email:
title: "Mail" title: "Mail"
instructions: "Deine Mailadresse wird niemals öffentlich angezeigt." instructions: "Deine Mailadresse wird niemals öffentlich angezeigt."
@ -378,6 +397,7 @@ de:
authenticating: "Authentisiere..." authenticating: "Authentisiere..."
awaiting_confirmation: 'Dein Konto ist noch nicht aktiviert. Benutze den "Passwort vergesse"-Link um eine neue Aktivierungsmail zu erhalten.' awaiting_confirmation: 'Dein Konto ist noch nicht aktiviert. Benutze den "Passwort vergesse"-Link um eine neue Aktivierungsmail zu erhalten.'
awaiting_approval: "Dein Konto wurde noch nicht von einem Moderator bewilligt. Du bekommst eine Mail, sobald das geschehen ist." awaiting_approval: "Dein Konto wurde noch nicht von einem Moderator bewilligt. Du bekommst eine Mail, sobald das geschehen ist."
requires_invite: "Entschuldige, der Zugriff auf dieses Forum ist nur mit einer Einladung erlaubt."
not_activated: "Du kannst Dich noch nicht anmelden. Wir haben Dir kürzlich eine Aktivierungsmail an <b>{{sentTo}}</b> geschickt. Bitte folge den Anweisungen darin, um dein Konto zu aktivieren." not_activated: "Du kannst Dich noch nicht anmelden. Wir haben Dir kürzlich eine Aktivierungsmail an <b>{{sentTo}}</b> geschickt. Bitte folge den Anweisungen darin, um dein Konto zu aktivieren."
resend_activation_email: "Klick hier, um ein neue Aktivierungsmail zu erhalten." resend_activation_email: "Klick hier, um ein neue Aktivierungsmail zu erhalten."
sent_activation_email_again: "Wir haben noch eine Aktivierungsmail an <b>{{currentEmail}}</b> verschickt. Es kann einige Minuten dauern, bis sie ankommt. Im Zweifel schaue auch im Spam-Ordner nach." sent_activation_email_again: "Wir haben noch eine Aktivierungsmail an <b>{{currentEmail}}</b> verschickt. Es kann einige Minuten dauern, bis sie ankommt. Im Zweifel schaue auch im Spam-Ordner nach."
@ -489,16 +509,23 @@ de:
total_flagged: "total markierte Einträge" total_flagged: "total markierte Einträge"
upload_selector: upload_selector:
title: "Bild einfügen" title: "Bild hochladen"
from_my_computer: "von meinem Gerät" title_with_attachments: "Bild oder Datei hochladen"
from_the_web: "aus dem Web" from_my_computer: "Von meinem Gerät"
from_the_web: "Aus dem Web"
add_title: "Bild hinzufügen" add_title: "Bild hinzufügen"
add_title_with_attachments: "Bild oder Datei hinzufügen"
remote_title: "Entferntes Bild" remote_title: "Entferntes Bild"
remote_title_with_attachments: "Entferntes Bild oder Datei"
remote_tip: "Gib die Adresse eines Bildes wie folgt ein: http://example.com/image.jpg" remote_tip: "Gib die Adresse eines Bildes wie folgt ein: http://example.com/image.jpg"
remote_tip_with_attachments: "Gib die Adresse eines Bildes oder Datei wie folgt ein http://example.com/file.ext (Erlaubte Dateiendungen: {{authorized_extensions}})."
local_title: "Lokales Bild" local_title: "Lokales Bild"
local_title_with_attachments: "Lokales Bild oder Datei"
local_tip: "Klicke hier, um ein Bild von deinem Gerät zu wählen." local_tip: "Klicke hier, um ein Bild von deinem Gerät zu wählen."
upload_title: "Hochladen" local_tip_with_attachments: "Klicke hier, um ein Bild oder eine Datei von deinem Gerät zu wählen (Erlaubte Dateiendungen: {{authorized_extensions}})"
uploading: "Bild wird hochgeladen" upload_title: "Bild hochladen"
upload_title_with_attachments: "Bild oder Datei hochladen"
uploading: "Hochgeladen..."
search: search:
title: "Such nach Themen, Beiträgen, Nutzern oder Kategorien" title: "Such nach Themen, Beiträgen, Nutzern oder Kategorien"
@ -745,10 +772,13 @@ de:
edit: "Editing {{link}} von {{replyAvatar}} {{username}}" edit: "Editing {{link}} von {{replyAvatar}} {{username}}"
post_number: "Beitrag {{number}}" post_number: "Beitrag {{number}}"
in_reply_to: "Antwort auf" in_reply_to: "Antwort auf"
last_edited_on: "Antwort zuletzt bearbeitet am"
reply_as_new_topic: "Mit Themenwechsel antworten" reply_as_new_topic: "Mit Themenwechsel antworten"
continue_discussion: "Fortsetzung des Gesprächs {{postLink}}:" continue_discussion: "Fortsetzung des Gesprächs {{postLink}}:"
follow_quote: "Springe zu zitiertem Beitrag" follow_quote: "Springe zu dem zitiertem Beitrag"
deleted_by_author: "(Beitrag vom Autor entfernt)" deleted_by_author:
one: "(Antwort vom Autor zurückgezogen, wird automatisch in %{count} Stunde gelöscht falls nicht gemeldet)"
other: "(Antwort vom Autor zurückgezogen, wird automatisch in %{count} Stunden gelöscht falls nicht gemeldet)"
deleted_by: "Entfernt von" deleted_by: "Entfernt von"
expand_collapse: "mehr/weniger" expand_collapse: "mehr/weniger"
@ -760,11 +790,11 @@ de:
create: "Entschuldige, es gab einen Fehler beim Anlegen des Beitrags. Bitte versuche es noch einmal." create: "Entschuldige, es gab einen Fehler beim Anlegen des Beitrags. Bitte versuche es noch einmal."
edit: "Entschuldige, es gab einen Fehler beim Bearbeiten des Beitrags. Bitte versuche es noch einmal." edit: "Entschuldige, es gab einen Fehler beim Bearbeiten des Beitrags. Bitte versuche es noch einmal."
upload: "Entschuldige, es gab einen Fehler beim Hochladen der Datei. Bitte versuche es noch einmal." upload: "Entschuldige, es gab einen Fehler beim Hochladen der Datei. Bitte versuche es noch einmal."
image_too_large: "Entschuldige, das Bild, das du hochladen wolltest, ist zu groß (Maximalgröße {{max_size_kb}}kb), bitte reduziere die Dateigröße und versuche es nochmal."
image_upload_not_allowed_for_new_user: "Entschuldige, neue Benutzer dürfen keine Bilder hochladen."
attachment_too_large: "Entschuldige, die Datei, die du hochladen wolltest, ist zu groß (Maximalgröße {{max_size_kb}}kb)." attachment_too_large: "Entschuldige, die Datei, die du hochladen wolltest, ist zu groß (Maximalgröße {{max_size_kb}}kb)."
image_too_large: "Entschuldige, das Bild, das du hochladen wolltest, ist zu groß (Maximalgröße {{max_size_kb}}kb), bitte reduziere die Dateigröße und versuche es nochmal."
too_many_uploads: "Entschuldige, du darfst immer nur eine Datei hochladen." too_many_uploads: "Entschuldige, du darfst immer nur eine Datei hochladen."
upload_not_authorized: "Entschuldige, die Datei, die du hochladen wolltest, ist nicht erlaubt (erlaubte Endungen: {{authorized_extensions}})." upload_not_authorized: "Entschuldige, die Datei, die du hochladen wolltest, ist nicht erlaubt (erlaubte Endungen: {{authorized_extensions}})."
image_upload_not_allowed_for_new_user: "Entschuldige, neue Benutzer dürfen keine Bilder hochladen."
attachment_upload_not_allowed_for_new_user: "Entschuldige, neue Benutzer dürfen keine Dateien hochladen." attachment_upload_not_allowed_for_new_user: "Entschuldige, neue Benutzer dürfen keine Dateien hochladen."
abandon: "Willst Du diesen Beitrag wirklich verwerfen?" abandon: "Willst Du diesen Beitrag wirklich verwerfen?"
@ -884,6 +914,7 @@ de:
other: "Bist Du sicher, dass Du all diesen Beiträge löschen willst?" other: "Bist Du sicher, dass Du all diesen Beiträge löschen willst?"
category: category:
can: 'kann&hellip; '
none: '(keine Kategorie)' none: '(keine Kategorie)'
edit: 'Bearbeiten' edit: 'Bearbeiten'
edit_long: "Kategorie bearbeiten" edit_long: "Kategorie bearbeiten"
@ -912,18 +943,19 @@ de:
change_in_category_topic: "Besuche die Themen dieser Kategorie um einen Eindruck für eine gute Beschreibung zu gewinnen." change_in_category_topic: "Besuche die Themen dieser Kategorie um einen Eindruck für eine gute Beschreibung zu gewinnen."
hotness: "Beliebtheit" hotness: "Beliebtheit"
already_used: 'Diese Farbe wird bereits für eine andere Kategorie verwendet' already_used: 'Diese Farbe wird bereits für eine andere Kategorie verwendet'
is_secure: "Sichere Kategorie?"
add_group: "Gruppe hinzufügen"
security: "Sicherheit" security: "Sicherheit"
allowed_groups: "Erlaubte Gruppen:"
auto_close_label: "Thema automatisch schließen nach:" auto_close_label: "Thema automatisch schließen nach:"
edit_permissions: "Berechtigung bearbeiten"
add_permission: "Berechtigung hinzufügen"
flagging: flagging:
title: 'Aus welchem Grund meldest Du diesen Beitrag?' title: 'Aus welchem Grund meldest Du diesen Beitrag?'
action: 'Beitrag melden' action: 'Beitrag melden'
take_action: "Reagieren" take_action: "Reagieren"
notify_action: 'Melden' notify_action: 'Melden'
delete_spammer: "Spammer löschen"
delete_confirm: "Du wirst <b>%{posts}</b> Beiträge und <b>%{topics}</b> Themen von diesem Benutzer löschen, das Konto entfernen und die Mail <b>%{email}</b> permanent blockieren. Bist du sicher, dass dieser Benutzer wirklich ein Spammer ist?"
yes_delete_spammer: "Ja, lösche den Spammer"
cant: "Entschuldige, Du kannst diesen Beitrag augenblicklich nicht melden." cant: "Entschuldige, Du kannst diesen Beitrag augenblicklich nicht melden."
custom_placeholder_notify_user: "Weshalb erfordert der Beitrag, dass du den Benutzer direkt und privat kontaktieren möchtest? Sei spezifisch, konstruktiv und immer freundlich." custom_placeholder_notify_user: "Weshalb erfordert der Beitrag, dass du den Benutzer direkt und privat kontaktieren möchtest? Sei spezifisch, konstruktiv und immer freundlich."
custom_placeholder_notify_moderators: "Warum soll ein Moderator sich diesen Beitrag ansehen? Bitte lass uns wissen, was genau Dich beunruhigt, und wenn möglich dafür relevante Links." custom_placeholder_notify_moderators: "Warum soll ein Moderator sich diesen Beitrag ansehen? Bitte lass uns wissen, was genau Dich beunruhigt, und wenn möglich dafür relevante Links."
@ -956,6 +988,7 @@ de:
views_long: "Dieses Thema wurde {{number}} aufgerufen" views_long: "Dieses Thema wurde {{number}} aufgerufen"
activity: "Aktivität" activity: "Aktivität"
likes: "Gefällt mir" likes: "Gefällt mir"
likes_long: "es gibt {{number}} „Gefällt mir“ in diesem Thema"
top_contributors: "Teilnehmer" top_contributors: "Teilnehmer"
category_title: "Kategorie" category_title: "Kategorie"
history: "Verlauf" history: "Verlauf"
@ -1004,6 +1037,11 @@ de:
browser_update: '<a href="http://www.discourse.org/faq/#browser">Dein Webbrowser ist leider zu alt um dieses Forum zu besuchen</a>. Bitte <a href="http://browsehappy.com">installiere einen neueren Browser</a>.' browser_update: '<a href="http://www.discourse.org/faq/#browser">Dein Webbrowser ist leider zu alt um dieses Forum zu besuchen</a>. Bitte <a href="http://browsehappy.com">installiere einen neueren Browser</a>.'
permission_types:
full: "Erstellen / Antworten / Anschauen"
create_post: "Antworten / Anschauen"
readonly: "Anschauen"
# This section is exported to the javascript for i18n in the admin section # This section is exported to the javascript for i18n in the admin section
admin_js: admin_js:
type_to_filter: "Tippe etwas ein, um zu filtern..." type_to_filter: "Tippe etwas ein, um zu filtern..."
@ -1014,6 +1052,7 @@ de:
dashboard: dashboard:
title: "Übersicht" title: "Übersicht"
last_updated: "Übersicht zuletzt aktualisiert:"
version: "Version" version: "Version"
up_to_date: "Discourse ist aktuell." up_to_date: "Discourse ist aktuell."
critical_available: "Ein kritisches Update ist verfügbar." critical_available: "Ein kritisches Update ist verfügbar."
@ -1065,6 +1104,7 @@ de:
disagree_unhide_title: "Verwerfe alle Meldungen über diesen Beitrag (blendet verstecke Beiträge ein)" disagree_unhide_title: "Verwerfe alle Meldungen über diesen Beitrag (blendet verstecke Beiträge ein)"
disagree: "Ablehnen" disagree: "Ablehnen"
disagree_title: "Meldung ablehnen, alle Meldungen über diesen Beitrag annullieren" disagree_title: "Meldung ablehnen, alle Meldungen über diesen Beitrag annullieren"
delete_spammer_title: "Lösche den Benutzer und alle seine Beiträge und Themen."
flagged_by: "Gemeldet von" flagged_by: "Gemeldet von"
error: "Etwas ist schief gelaufen" error: "Etwas ist schief gelaufen"
@ -1145,6 +1185,48 @@ de:
last_seen_user: "Letzer Benutzer:" last_seen_user: "Letzer Benutzer:"
reply_key: "Antwort-Schlüssel" reply_key: "Antwort-Schlüssel"
logs:
title: "Logs"
action: "Aktion"
created_at: "Erstellt"
last_match_at: "Letzte Übereinstimmung"
match_count: "Übereinstimmungen"
ip_address: "IP"
screened_actions:
block: "blockieren"
do_nothing: "nichts machen"
staff_actions:
title: "Mitarbeiter Aktion"
instructions: "Kilcke auf die Benutzernamen und Aktionen um die Liste zu filtern. Klicke den Avatar um die Benutzerseite zu sehen."
clear_filters: "Alles anzeigen"
staff_user: "Mitarbeiter"
target_user: "Zielnutzer"
subject: "Betreff"
when: "Wann"
context: "Kontext"
details: "Details"
previous_value: "Vorangehend"
new_value: "Neu"
diff: "Diff"
show: "Anzeigen"
modal_title: "Details"
no_previous: "Es gibt keinen vorgängigen Wert."
deleted: "Kein neuer Wert. Der Eintrag wurde gelöscht."
actions:
delete_user: "Benutzer löschen"
change_trust_level: "Vertrauensstufe ändern"
change_site_setting: "Seiten Einstellungen ändern"
change_site_customization: "Seiten Anpassungen ändern"
delete_site_customization: "Seiten Anpassungen löschen"
screened_emails:
title: "Geschützte Mails"
description: "Wen jemand ein Konto erstellt, werden die folgenden Mail überprüft und die Registration blockiert, oder eine andere Aktion ausgeführt."
email: "Mail Adresse"
screened_urls:
title: "Geschützte URLs"
description: "Die aufgelisteten URLs wurden in Beiträgen von identifizierten Spammen verwendet."
url: "URL"
impersonate: impersonate:
title: "Aus Nutzersicht betrachten" title: "Aus Nutzersicht betrachten"
username_or_email: "Benutzername oder Mailadresse des Nutzers" username_or_email: "Benutzername oder Mailadresse des Nutzers"
@ -1170,6 +1252,9 @@ de:
approved_selected: approved_selected:
one: "Benutzer zulassen" one: "Benutzer zulassen"
other: "Benutzer zulassen ({{count}})" other: "Benutzer zulassen ({{count}})"
reject_selected:
one: "Benutzer ablehnen"
other: "Lehne ({{count}}) Benutzer ab"
titles: titles:
active: 'Aktive Benutzer' active: 'Aktive Benutzer'
new: 'Neue Benutzer' new: 'Neue Benutzer'
@ -1183,12 +1268,19 @@ de:
moderators: 'Moderatoren' moderators: 'Moderatoren'
blocked: 'Gesperrte Benutzer' blocked: 'Gesperrte Benutzer'
banned: "Gebannte Benutzer" banned: "Gebannte Benutzer"
reject_successful:
one: "Erfolgreich 1 Benutzer abgelehnt."
other: "Erfolgreich %{count} Benutzer abgelehnt."
reject_failures:
one: "Konnte 1 Benutzer nicht ablehnen."
other: "Konnte %{count} Benutzer nicht ablehnen."
user: user:
ban_failed: "Beim Sperren dieses Benutzers ist etwas schief gegangen {{error}}" ban_failed: "Beim Sperren dieses Benutzers ist etwas schief gegangen {{error}}"
unban_failed: "Beim Entsperren dieses Benutzers ist etwas schief gegangen {{error}}" unban_failed: "Beim Entsperren dieses Benutzers ist etwas schief gegangen {{error}}"
ban_duration: "Wie lange soll dieser Benutzer gesperrt werden? (Tage)" ban_duration: "Wie lange soll dieser Benutzer gesperrt werden? (Tage)"
delete_all_posts: "Lösche alle Beiträge" delete_all_posts: "Lösche alle Beiträge"
delete_all_posts_confirm: "Du löschst %{posts} Beiträge und %{topics} Themen. Bist du sicher?"
ban: "Sperren" ban: "Sperren"
unban: "Entsperren" unban: "Entsperren"
banned: "Gesperrt?" banned: "Gesperrt?"
@ -1219,12 +1311,18 @@ de:
flags_received_count: Erhaltene Meldungen flags_received_count: Erhaltene Meldungen
approve: 'Genehmigen' approve: 'Genehmigen'
approved_by: "genehmigt von" approved_by: "genehmigt von"
approve_success: "Benutzer freigeschalten und Mail mit den Anweisungen zur Aktivierung gesendet." approve_success: "Benutzer freigeschalten und Mail mit den Anweisungen zur Aktivierung
approve_bulk_success: "Erfolg! Alle ausgewählten Benutzer wurden freigeschalten und benachrichtigt." gesendet."
approve_bulk_success: "Erfolg! Alle ausgewählten Benutzer wurden freigeschalten und
benachrichtigt."
time_read: "Lesezeit" time_read: "Lesezeit"
delete: Benutzer löschen delete: Benutzer löschen
delete_forbidden: "Der Benutzer kann nicht gelöscht werden, da er noch Beiträge hat. Lösche zuerst seine Beträge." delete_forbidden:
one: "Benutzer können nicht gelöscht werden, wenn sie sich vor mehr als %{count} Tag angemeldet oder noch Beiträge haben. Lösche zuerst seine Beträge."
other: "Benutzer können nicht gelöscht werden, wenn sie sich vor mehr als %{count} Tagen angemeldet oder noch Beiträge haben. Lösche zuerst seine Beträge."
delete_confirm: "Bist du SICHER das du diesen Benutzer permanent von der Seite entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden!" delete_confirm: "Bist du SICHER das du diesen Benutzer permanent von der Seite entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden!"
delete_and_block: "<b>Ja</b>, und <b>blockiere</b> Anmeldungen von dieser Mail Adresse"
delete_dont_block: "<b>Ja</b>, aber <b>erlaube</b> Anmeldungen von dieser Mail Adresse"
deleted: "Der Benutzer wurde gelöscht." deleted: "Der Benutzer wurde gelöscht."
delete_failed: "Beim Löschen des Benutzers ist ein Fehler aufgetreten. Stelle sicher, dass dieser Benutzer keine Beiträge mehr hat." delete_failed: "Beim Löschen des Benutzers ist ein Fehler aufgetreten. Stelle sicher, dass dieser Benutzer keine Beiträge mehr hat."
send_activation_email: "Aktivierungsmail senden" send_activation_email: "Aktivierungsmail senden"
@ -1239,7 +1337,7 @@ de:
deactivate_explanation: "Ein deaktivierter Benutzer muss seine E-Mail erneut bestätigen." deactivate_explanation: "Ein deaktivierter Benutzer muss seine E-Mail erneut bestätigen."
banned_explanation: "Ein gesperrter Benutzer kann sich nicht einloggen." banned_explanation: "Ein gesperrter Benutzer kann sich nicht einloggen."
block_explanation: "Ein geblockter Benutzer kann keine Themen erstellen oder Beiträge veröffentlichen." block_explanation: "Ein geblockter Benutzer kann keine Themen erstellen oder Beiträge veröffentlichen."
trust_level_change_failed: "Beim Wechsel der Vertrauensstufe ist ein Fehler aufgetreten."
site_content: site_content:
none: "Wähle einen Inhaltstyp um mit dem Bearbeiten zu beginnen." none: "Wähle einen Inhaltstyp um mit dem Bearbeiten zu beginnen."
@ -1251,3 +1349,5 @@ de:
title: 'Einstellungen' title: 'Einstellungen'
reset: 'Zurücksetzen' reset: 'Zurücksetzen'
none: "Keine" none: "Keine"

View File

@ -190,7 +190,6 @@ en:
download_archive: "download archive of my posts" download_archive: "download archive of my posts"
private_message: "Private Message" private_message: "Private Message"
private_messages: "Messages" private_messages: "Messages"
private_messages_sent: "Sent Messages"
activity_stream: "Activity" activity_stream: "Activity"
preferences: "Preferences" preferences: "Preferences"
bio: "About me" bio: "About me"
@ -203,6 +202,12 @@ en:
change: "change" change: "change"
moderator: "{{user}} is a moderator" moderator: "{{user}} is a moderator"
admin: "{{user}} is an admin" admin: "{{user}} is an admin"
deleted: "User Was Deleted"
messages:
all: "All"
mine: "Mine"
unread: "Unread"
change_password: change_password:
success: "(email sent)" success: "(email sent)"
@ -398,6 +403,7 @@ en:
authenticating: "Authenticating..." authenticating: "Authenticating..."
awaiting_confirmation: "Your account is awaiting activation, use the forgot password link to issue another activation email." awaiting_confirmation: "Your account is awaiting activation, use the forgot password link to issue another activation email."
awaiting_approval: "Your account has not been approved by a staff member yet. You will be sent an email when it is approved." awaiting_approval: "Your account has not been approved by a staff member yet. You will be sent an email when it is approved."
requires_invite: "Sorry, access to this forum is by invite only."
not_activated: "You can't log in yet. We previously sent an activation email to you at <b>{{sentTo}}</b>. Please follow the instructions in that email to activate your account." not_activated: "You can't log in yet. We previously sent an activation email to you at <b>{{sentTo}}</b>. Please follow the instructions in that email to activate your account."
resend_activation_email: "Click here to send the activation email again." resend_activation_email: "Click here to send the activation email again."
sent_activation_email_again: "We sent another activation email to you at <b>{{currentEmail}}</b>. It might take a few minutes for it to arrive; be sure to check your spam folder." sent_activation_email_again: "We sent another activation email to you at <b>{{currentEmail}}</b>. It might take a few minutes for it to arrive; be sure to check your spam folder."
@ -505,7 +511,7 @@ en:
private_message: "<i class='icon icon-envelope-alt' title='private message'></i> {{username}} {{link}}" private_message: "<i class='icon icon-envelope-alt' title='private message'></i> {{username}} {{link}}"
invited_to_private_message: "<i class='icon icon-envelope-alt' title='private message'></i> {{username}} {{link}}" invited_to_private_message: "<i class='icon icon-envelope-alt' title='private message'></i> {{username}} {{link}}"
invitee_accepted: "<i title='accepted your invitation' class='icon icon-signin'></i> {{username}} accepted your invitation" invitee_accepted: "<i title='accepted your invitation' class='icon icon-signin'></i> {{username}} accepted your invitation"
moved_post: "<i title='moved post' class='icon icon-arrow-right'></i> {{username}} moved to {{link}}" moved_post: "<i title='moved post' class='icon icon-arrow-right'></i> {{username}} moved {{link}}"
total_flagged: "total flagged posts" total_flagged: "total flagged posts"
upload_selector: upload_selector:
@ -759,6 +765,7 @@ en:
multi_select: multi_select:
select: 'select' select: 'select'
selected: 'selected ({{count}})' selected: 'selected ({{count}})'
select_replies: 'select +replies'
delete: delete selected delete: delete selected
cancel: cancel selecting cancel: cancel selecting
description: description:
@ -811,6 +818,12 @@ en:
undelete: "undelete this post" undelete: "undelete this post"
share: "share a link to this post" share: "share a link to this post"
more: "More" more: "More"
delete_replies:
confirm:
one: "Do you also want to delete the direct reply to this post?"
other: "Do you also want to delete the {{count}} direct replies to this post?"
yes_value: "Yes, delete the replies too"
no_value: "No, just this post"
actions: actions:
flag: 'Flag' flag: 'Flag'

View File

@ -220,7 +220,6 @@ ru:
download_archive: скачать архив ваших сообщений download_archive: скачать архив ваших сообщений
private_message: Личное сообщение private_message: Личное сообщение
private_messages: Личные сообщения private_messages: Личные сообщения
private_messages_sent: Отправленные сообщения
activity_stream: Активность activity_stream: Активность
preferences: Настройки preferences: Настройки
bio: Обо мне bio: Обо мне
@ -233,6 +232,10 @@ ru:
change: изменить change: изменить
moderator: '{{user}} - модератор' moderator: '{{user}} - модератор'
admin: '{{user}} - админ' admin: '{{user}} - админ'
messages:
all: Все
mine: Мои
unread: Непрочитанные
change_password: change_password:
success: (письмо отправлено) success: (письмо отправлено)
in_progress: (отправка письма) in_progress: (отправка письма)
@ -258,6 +261,7 @@ ru:
uploaded_avatar: Собственный аватар uploaded_avatar: Собственный аватар
uploaded_avatar_empty: Добавить собственный аватар uploaded_avatar_empty: Добавить собственный аватар
upload_title: Загрузка собственного аватара upload_title: Загрузка собственного аватара
image_is_not_a_square: 'Внимание: изображение было кадрировано, т.к. оно не квадратное.'
email: email:
title: Email title: Email
instructions: Ваш адрес электронной почты всегда скрыт. instructions: Ваш адрес электронной почты всегда скрыт.
@ -407,6 +411,7 @@ ru:
authenticating: Проверка... authenticating: Проверка...
awaiting_confirmation: Ваша учетная запись требует активации. Для того чтобы получить активационное письмо повторно, воспользуйтесь опцией сброса пароля. awaiting_confirmation: Ваша учетная запись требует активации. Для того чтобы получить активационное письмо повторно, воспользуйтесь опцией сброса пароля.
awaiting_approval: Ваша учетная запись еще не одобрена. Вы получите письмо, когда это случится. awaiting_approval: Ваша учетная запись еще не одобрена. Вы получите письмо, когда это случится.
requires_invite: К сожалению, доступ к форуму только по приглашениям.
not_activated: 'Прежде чем вы сможете воспользоваться новой учетной записью, вам необходимо ее активировать. Мы отправили вам на почту <b>{{sentTo}}</b> подробные инструкции, как это cделать.' not_activated: 'Прежде чем вы сможете воспользоваться новой учетной записью, вам необходимо ее активировать. Мы отправили вам на почту <b>{{sentTo}}</b> подробные инструкции, как это cделать.'
resend_activation_email: Щелкните здесь, чтобы мы повторно выслали вам письмо для активации учетной записи. resend_activation_email: Щелкните здесь, чтобы мы повторно выслали вам письмо для активации учетной записи.
sent_activation_email_again: 'По адресу <b>{{currentEmail}}</b> повторно отправлено письмо с кодом активации. Доставка сообщения может занять несколько минут. Имейте в виду, что иногда по ошибке письмо может попасть в папку Спам.' sent_activation_email_again: 'По адресу <b>{{currentEmail}}</b> повторно отправлено письмо с кодом активации. Доставка сообщения может занять несколько минут. Имейте в виду, что иногда по ошибке письмо может попасть в папку Спам.'
@ -506,7 +511,7 @@ ru:
private_message: "<i class='icon icon-envelope-alt' title='private message'></i> {{username}} {{link}}" private_message: "<i class='icon icon-envelope-alt' title='private message'></i> {{username}} {{link}}"
invited_to_private_message: "<i class='icon icon-envelope-alt' title='private message'></i> {{username}} {{link}}" invited_to_private_message: "<i class='icon icon-envelope-alt' title='private message'></i> {{username}} {{link}}"
invitee_accepted: "<i title='принятое приглашение' class='icon icon-signin'></i> {{username}} принял ваше приглашение" invitee_accepted: "<i title='принятое приглашение' class='icon icon-signin'></i> {{username}} принял ваше приглашение"
moved_post: "<i title='перенесенное сообщение' class='icon icon-arrow-right'></i> {{username}} перенес сообщение в {{link}}" moved_post: "<i title='moved post' class='icon icon-arrow-right'></i> {{username}} переместил сообщение в {{link}}"
total_flagged: всего сообщений с жалобами total_flagged: всего сообщений с жалобами
upload_selector: upload_selector:
title: Загрузить изображение title: Загрузить изображение

View File

@ -188,7 +188,6 @@ zh_CN:
download_archive: "下载我的帖子的存档" download_archive: "下载我的帖子的存档"
private_message: "私信" private_message: "私信"
private_messages: "消息" private_messages: "消息"
private_messages_sent: "已发送消息"
activity_stream: "活动" activity_stream: "活动"
preferences: "设置" preferences: "设置"
bio: "关于我" bio: "关于我"
@ -201,6 +200,11 @@ zh_CN:
change: "修改" change: "修改"
moderator: "{{user}} 是版主" moderator: "{{user}} 是版主"
admin: "{{user}} 是管理员" admin: "{{user}} 是管理员"
messages:
all: "所有"
mine: "我的"
unread: "未读"
change_password: change_password:
success: "(电子邮件已发送)" success: "(电子邮件已发送)"
@ -396,6 +400,7 @@ zh_CN:
authenticating: "验证中……" authenticating: "验证中……"
awaiting_confirmation: "你的帐号尚未激活,点击忘记密码链接来重新发送激活邮件。" awaiting_confirmation: "你的帐号尚未激活,点击忘记密码链接来重新发送激活邮件。"
awaiting_approval: "你的帐号尚未被论坛版主批准。一旦你的帐号获得批准,你会收到一封电子邮件。" awaiting_approval: "你的帐号尚未被论坛版主批准。一旦你的帐号获得批准,你会收到一封电子邮件。"
requires_invite: "抱歉,本论坛仅接受邀请注册。"
not_activated: "你还不能登录。我们之前在<b>{{sentTo}}</b>发送了一封激活邮件给你。请按照邮件中的介绍来激活你的帐号。" not_activated: "你还不能登录。我们之前在<b>{{sentTo}}</b>发送了一封激活邮件给你。请按照邮件中的介绍来激活你的帐号。"
resend_activation_email: "点击此处来重新发送激活邮件。" resend_activation_email: "点击此处来重新发送激活邮件。"
sent_activation_email_again: "我们在<b>{{currentEmail}}</b>又发送了一封激活邮件给你,邮件送达可能需要几分钟,有的电子邮箱服务商可能会认为此邮件为垃圾邮件,请检查一下你邮箱的垃圾邮件文件夹。" sent_activation_email_again: "我们在<b>{{currentEmail}}</b>又发送了一封激活邮件给你,邮件送达可能需要几分钟,有的电子邮箱服务商可能会认为此邮件为垃圾邮件,请检查一下你邮箱的垃圾邮件文件夹。"
@ -503,7 +508,7 @@ zh_CN:
private_message: "<i class='icon icon-envelope-alt' title='私信'></i> {{username}} 发送给你一条私信:{{link}}" private_message: "<i class='icon icon-envelope-alt' title='私信'></i> {{username}} 发送给你一条私信:{{link}}"
invited_to_private_message: "{{username}} 邀请你进行私下交流:{{link}}" invited_to_private_message: "{{username}} 邀请你进行私下交流:{{link}}"
invitee_accepted: "<i title='已接受你的邀请' class='icon icon-signin'></i> {{username}} 已接受你的邀请" invitee_accepted: "<i title='已接受你的邀请' class='icon icon-signin'></i> {{username}} 已接受你的邀请"
moved_post: "<i title='移动帖子' class='icon icon-arrow-right'></i> {{username}} 已将帖子移动到 {{link}}" moved_post: "<i title='移动帖子' class='icon icon-arrow-right'></i> {{username}} 移动了该帖: {{link}}"
total_flagged: "被报告帖子的总数" total_flagged: "被报告帖子的总数"
upload_selector: upload_selector:

View File

@ -5,9 +5,15 @@
# http://yamllint.com/ # http://yamllint.com/
de: de:
dates:
short_date_no_year: "D MMM"
short_date: "D. MMM YYYY"
long_date: "D. MMMM YYYY, H:mm"
time: time:
formats: formats:
short: "%d. %m. %Y" short: "%d. %m. %Y"
short_no_year: "%-d. %B"
date_only: "%-d. %b %Y"
title: "Discourse" title: "Discourse"
topics: "Themen" topics: "Themen"
@ -33,6 +39,10 @@ de:
zero: "Entschuldige, neue Benutzer können Beiträge keine Bilder hinzufügen." zero: "Entschuldige, neue Benutzer können Beiträge keine Bilder hinzufügen."
one: "Entschuldige, neue Benutzer können Beiträgen nur ein Bild hinzufügen." one: "Entschuldige, neue Benutzer können Beiträgen nur ein Bild hinzufügen."
other: "Entschuldige, neue Benutzer können Beiträge nur %{count} Bilde hinzufügen." other: "Entschuldige, neue Benutzer können Beiträge nur %{count} Bilde hinzufügen."
too_many_attachments:
zero: "Entschuldige, neue Benutzer können Beiträge keine Dateien hinzufügen."
one: "Entschuldige, neue Benutzer können Beiträgen nur eine Datei hinzufügen."
other: "Entschuldige, neue Benutzer können Beiträgen nur %{count} Dateien hinzufügen."
too_many_links: too_many_links:
zero: "Entschuldige, neue Benutzer können Beiträgen keine Links hinzufügen." zero: "Entschuldige, neue Benutzer können Beiträgen keine Links hinzufügen."
one: "Entschuldige, neue Benutzer können Beiträgen nur einen Link hinzufügen." one: "Entschuldige, neue Benutzer können Beiträgen nur einen Link hinzufügen."
@ -50,8 +60,13 @@ de:
rss_topics_in_category: "RSS-Feed von Themen in der Kategorie '%{category}'" rss_topics_in_category: "RSS-Feed von Themen in der Kategorie '%{category}'"
author_wrote: "%{author} schrieb:" author_wrote: "%{author} schrieb:"
private_message_abbrev: "PN" private_message_abbrev: "PN"
rss_description:
latest: "Neuste Themen"
hot: Angesagte Themen"
groups: groups:
errors:
can_not_modify_automatic: "Du kannst eine automatische Gruppe nicht bearbeiten"
default_names: default_names:
admins: "admins" admins: "admins"
moderators: "moderatoren" moderators: "moderatoren"
@ -70,8 +85,6 @@ de:
'new-topic': | 'new-topic': |
Willkommen auf %{site_name} &mdash; **Danke, dass Du ein neues Thema erstellst!** Willkommen auf %{site_name} &mdash; **Danke, dass Du ein neues Thema erstellst!**
Beachte dabei bitte die Folgenden Dinge:
- Ist der Titel eines adäquate Beschreibung dessen, was ein Nutzer vorzufinden erwartet, wenn er dieses Thema aufruft? - Ist der Titel eines adäquate Beschreibung dessen, was ein Nutzer vorzufinden erwartet, wenn er dieses Thema aufruft?
- Der erste Beitrag umschreibt das Thema: Worum geht es? Wer wäre interessiert daran? Warum ist es wichtig? Welche Arten von Antworten erhoffst Du dir von der Community? - Der erste Beitrag umschreibt das Thema: Worum geht es? Wer wäre interessiert daran? Warum ist es wichtig? Welche Arten von Antworten erhoffst Du dir von der Community?
@ -83,8 +96,6 @@ de:
'new-reply': | 'new-reply': |
Willkommen auf %{site_name} &mdash; **Danke für deinen Beitrag zum Thema!** Willkommen auf %{site_name} &mdash; **Danke für deinen Beitrag zum Thema!**
Beachte bitte folgende Dinge während des Schreibens:
- Fügt dein Beitrag dem Gespräch etwas Neues hinzu, und sei es auch wenig? - Fügt dein Beitrag dem Gespräch etwas Neues hinzu, und sei es auch wenig?
- Behandle deine Gesprächspartner mit demselben Respekt, den Du von ihnen erwartest. - Behandle deine Gesprächspartner mit demselben Respekt, den Du von ihnen erwartest.
@ -130,6 +141,8 @@ de:
title: "Anführer" title: "Anführer"
elder: elder:
title: "Ältester" title: "Ältester"
change_failed_explanation: "Du wolltest %{user_name} auf '%{new_trust_level}' zurückstufen. Jedoch ist seine Vertrauensstufe bereits '%{current_trust_level}'. %{user_name} verbleibt auf '%{current_trust_level}'"
rate_limiter: rate_limiter:
too_many_requests: "Du machst das zu häufig. Bitte warte %{time_left} vor dem nächsten Versuch." too_many_requests: "Du machst das zu häufig. Bitte warte %{time_left} vor dem nächsten Versuch."
@ -382,12 +395,17 @@ de:
cas_config_warning: 'Der Server erlaubt die Anmeldung mit CAS (enable_cas_logins), aber der Hostname und die Domäne sind nicht gesetzt.' cas_config_warning: 'Der Server erlaubt die Anmeldung mit CAS (enable_cas_logins), aber der Hostname und die Domäne sind nicht gesetzt.'
twitter_config_warning: 'Der Server erlaubt die Anmeldung mit Facebook Twitter (enable_twitter_logins), aber der Schlüssel und der Geheimcode sind nicht gesetzt. Besuche <a href="/admin/site_settings">die Einstellungen</a> um die fehlenden Einträge hinzuzufügen. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-twitter-logins" target="_blank">Besuche den Leitfaden um mehr zu erfahren</a>.' twitter_config_warning: 'Der Server erlaubt die Anmeldung mit Facebook Twitter (enable_twitter_logins), aber der Schlüssel und der Geheimcode sind nicht gesetzt. Besuche <a href="/admin/site_settings">die Einstellungen</a> um die fehlenden Einträge hinzuzufügen. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide#enable-twitter-logins" target="_blank">Besuche den Leitfaden um mehr zu erfahren</a>.'
github_config_warning: 'Der Server erlaubt die Anmeldung mit Facebook GitHub (enable_github_logins), aber die Kunden ID und der Geheimcode sind nicht gesetzt. Besuche <a href="/admin/site_settings">die Einstellungen</a> um die fehlenden Einträge hinzuzufügen. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide" target="_blank">Besuche den Leitfaden um mehr zu erfahren</a>.' github_config_warning: 'Der Server erlaubt die Anmeldung mit Facebook GitHub (enable_github_logins), aber die Kunden ID und der Geheimcode sind nicht gesetzt. Besuche <a href="/admin/site_settings">die Einstellungen</a> um die fehlenden Einträge hinzuzufügen. <a href="https://github.com/discourse/discourse/wiki/The-Discourse-Admin-Quick-Start-Guide" target="_blank">Besuche den Leitfaden um mehr zu erfahren</a>.'
s3_config_warning: 'Der Server wurde konfiguriert um Dateien nach s3 hochzuladen, aber mindestens der folgenden Einstellungen fehlt: s3_access_key_id, s3_secret_access_key oder s3_upload_bucket. Besuche <a href="/admin/site_settings">die Einstellungen</a> um die fehlenden Einträge hinzuzufügen. <a href="http://meta.discourse.org/t/how-to-set-up-image-uploads-to-s3/7229" target="_blank">Besuche "How to set up image uploads to S3?" um mehr zu erfahren</a>.'
image_magick_warning: 'Der Server wurde konfiguriert um Vorschaubilder von grossen Bildern zu erstellen, aber ImageMagick ist nicht installiertd. Installiere ImageMagick mit deinem bevorzugten Packetmanager oder besuche <a href="http://www.imagemagick.org/script/binary-releases.php" target="_blank">um das aktuelle Paket herunterzuladen</a>.'
failing_emails_warning: 'Es konnten insgesamt %{num_failed_jobs} Mails nicht versendet werden. Bitte überprüfe die Einstellungen in config/environments/production.rb und stelle die Richtigkeit der config.action_mailer Einstellungen. <a href="/sidekiq/retries" target="_blank">Zu den Fehlern in Sidekiq</a>.' failing_emails_warning: 'Es konnten insgesamt %{num_failed_jobs} Mails nicht versendet werden. Bitte überprüfe die Einstellungen in config/environments/production.rb und stelle die Richtigkeit der config.action_mailer Einstellungen. <a href="/sidekiq/retries" target="_blank">Zu den Fehlern in Sidekiq</a>.'
default_logo_warning: "Das Logo der Seite wurde noch nicht angepasst. Bitte bearbeite dieses in den <a href='/admin/site_settings'>Einstellungen</a> (siehe logo_url, logo_small_url und favicon_url)." default_logo_warning: "Das Logo der Seite wurde noch nicht angepasst. Bitte bearbeite dieses in den <a href='/admin/site_settings'>Einstellungen</a> (siehe logo_url, logo_small_url und favicon_url)."
contact_email_missing: "Du hast noch keine Kontaktmail für die Seite hinterlegt. Bitte hinterlege diese in den <a href='/admin/site_settings'>Einstellungen</a> (siehe contact_email)." contact_email_missing: "Du hast noch keine Kontaktmail für die Seite hinterlegt. Bitte hinterlege diese in den <a href='/admin/site_settings'>Einstellungen</a> (siehe contact_email)."
contact_email_invalid: "Die Kontaktmail der Seite ist ungültig. Bitte bearbeite diese in den <a href='/admin/site_settings'>Einstellungen</a> (siehe contact_email)." contact_email_invalid: "Die Kontaktmail der Seite ist ungültig. Bitte bearbeite diese in den <a href='/admin/site_settings'>Einstellungen</a> (siehe contact_email)."
title_nag: "Der Titel der Seite wurde noch nicht angepasst. Bitte bearbeite diesen in den <a href='/admin/site_settings'>Einstellungen</a>." title_nag: "Der Titel der Seite wurde noch nicht angepasst. Bitte bearbeite diesen in den <a href='/admin/site_settings'>Einstellungen</a>."
consumer_email_warning: "Deine Seite verwendet Gmail um Mails zu senden. <a href='http://support.google.com/a/bin/answer.py?hl=en&answer=166852' target='_blank'>Gmail hat eine Limite zum Senden von Mails</a>. Um die Mail-Zustellung zu gewährleisten, solltest du einen anderen Mail Service in Erwägung ziehen." consumer_email_warning: "Deine Seite verwendet Gmail um Mails zu senden. <a href='http://support.google.com/a/bin/answer.py?hl=en&answer=166852' target='_blank'>Gmail hat eine Limite zum Senden von Mails</a>. Um die Mail-Zustellung zu gewährleisten, solltest du einen anderen Mail Service in Erwägung ziehen."
access_password_removal: "Deine Seite hat die Einstellung access_password verwendet, welche entfernt wurde. Die Einstellungen login_required und must_approve_users wurden eingeschalten und werden sofort verwendet. Du kannst diese in <a href='/admin/site_settings'>den Einstellungen</a> wechseln. Stelle sicher, <a href='/admin/users/list/pending'>dass die Benutzer in der Warteliste</a> aktiviert werden. (Diese Meldung wird in 2 Tagen nicht mehr angezeigt.)"
system_username_warning: "Die Einstellung system_username ist leer. Bitte ändere diese in <a href='/admin/site_settings'>den Einstellungen</a>. Setzte einen Benutzernamen eines Administrators, welcher als Sender der Systemnachrichten verwendet werden soll."
notification_email_warning: "Die Einstellung notification_email ist leer. Bitte ändere diese in <a href='/admin/site_settings'>den Einstellungen</a>."
content_types: content_types:
education_new_reply: education_new_reply:
@ -405,22 +423,30 @@ de:
welcome_invite: welcome_invite:
title: "Willkommen: Eingeladener Benutzer" title: "Willkommen: Eingeladener Benutzer"
description: "Eine private Nachricht welche automatisch an alle eingeladenen Benutzer gesendet wird, wenn diese die Einladung annehmen." description: "Eine private Nachricht welche automatisch an alle eingeladenen Benutzer gesendet wird, wenn diese die Einladung annehmen."
privacy_policy:
title: "Datenschutzrichtlinie"
description: "Die Datenschutzrichtlinie deiner Seite. Leer lassen um die Vorgabe zu verwenden."
faq:
title: "FAQ"
description: "Die FAQ deiner Seite. Leer lassen um die Vorgabe zu verwenden."
login_required_welcome_message: login_required_welcome_message:
title: "Anmeldung erforderlich: Willkommensnachricht" title: "Anmeldung erforderlich: Willkommensnachricht"
description: "Willkommensnachricht welche angezeigt wird wenn der Benutzer nicht angemeldet ist und die description: "Willkommensnachricht welche angezeigt wird wenn der Benutzer nicht angemeldet ist und die
Einstellung 'login required' aktiviert ist." Einstellung 'login required' aktiviert ist."
tos_user_content_license: tos_user_content_license:
title: "Nutzungsbedingungen: Lizenz" title: "Nutzungsbedingungen: Lizenz"
description: "Der Text für die Lizenz-Sektion in den Nutzungsbedingungen." description: "Der Text für die Lizenz-Sektion in den Nutzungsbedingungen."
tos_miscellaneous: tos_miscellaneous:
title: "Nutzungsbedingungen: Verschiedenes" title: "Nutzungsbedingungen: Verschiedenes"
description: "Der Text für die Verschiedene-Sektion in den Nutzungsbedingungen." description: "Der Text für die Verschiedene-Sektion in den Nutzungsbedingungen."
login_required:
title: "Anmeldung erforderlich: Hauptseite"
description: "Der Text welcher nicht angemeldeten Benutzer angezeigt wird, wenn eine Anmeldung erforderlich ist."
site_settings: site_settings:
default_locale: "Die Standardsprache dieser Discourse-Instanz (kodiert in ISO 639-1)." default_locale: "Die Standardsprache dieser Discourse-Instanz (kodiert in ISO 639-1)."
min_post_length: "Minimale Beitragslänge in Zeichen." min_post_length: "Minimale Beitragslänge in Zeichen."
min_private_message_post_length: "Minimale Beitragslänge in Zeichen für private Nachrichten"
max_post_length: "Maximale Beitragslänge in Zeichen." max_post_length: "Maximale Beitragslänge in Zeichen."
min_topic_title_length: "Minimale Titellänge von Themen in Zeichen." min_topic_title_length: "Minimale Titellänge von Themen in Zeichen."
max_topic_title_length: "Maximale Titellänge von Themen in Zeichen." max_topic_title_length: "Maximale Titellänge von Themen in Zeichen."
@ -441,12 +467,15 @@ de:
queue_jobs: "Benutze die Sidekiq-Queue, falls falsche Queues inline sind." queue_jobs: "Benutze die Sidekiq-Queue, falls falsche Queues inline sind."
crawl_images: "Lade Bilder von Dritten herunter, um ihre Höhe und Breite zu bestimmen." crawl_images: "Lade Bilder von Dritten herunter, um ihre Höhe und Breite zu bestimmen."
ninja_edit_window: "Sekunden nach Empfang eines Beitrag, in denen Bearbeitungen nicht als neue Version gelten." ninja_edit_window: "Sekunden nach Empfang eines Beitrag, in denen Bearbeitungen nicht als neue Version gelten."
edit_history_visible_to_public: "Erlaube jedem vorherige Versionen eines beitrages zu sehen. Wenn deaktiviert, konnen nur Mitarbeiter die Bearbeitungshistorie anschauen."
delete_removed_posts_after: "Anzahl Stunden nach welchem Beiträge die von ihrem Author entfernt wurden endgültig gelöscht werden."
max_image_width: "Maximalbreite von Bildern in einem Beitrag." max_image_width: "Maximalbreite von Bildern in einem Beitrag."
max_image_height: "Maximalhöhe von Bildern in einem Beitrag."
category_featured_topics: "Zahl der angezeigten Themen je Kategorie auf der Kategorieseite /categories." category_featured_topics: "Zahl der angezeigten Themen je Kategorie auf der Kategorieseite /categories."
add_rel_nofollow_to_user_content: "Füge mit Ausnahme interner Links allen nutzergenerierten Inhalten 'rel nofollow' hinzu (inkludiert übergeordnete Domains). Die Änderung dieser Einstellung erfordert, dass Du sämtliche Markdown-Beiträge aktualisierst." add_rel_nofollow_to_user_content: "Füge mit Ausnahme interner Links allen nutzergenerierten Inhalten 'rel nofollow' hinzu (inkludiert übergeordnete Domains). Die Änderung dieser Einstellung erfordert, dass Du sämtliche Markdown-Beiträge aktualisierst."
exclude_rel_nofollow_domains: "Kommaseparierte Liste aller Domains, bei denen 'nofollow' nicht hinzugefügt wird (tld.com erlaubt auch sub.tld.com)." exclude_rel_nofollow_domains: "Kommaseparierte Liste aller Domains, bei denen 'nofollow' nicht hinzugefügt wird (tld.com erlaubt auch sub.tld.com)."
post_excerpt_maxlength: "Maximale Länge des Exzerpts eines Beitrags in Zeichen." post_excerpt_maxlength: "Maximale Länge des Zitates eines Beitrags in Zeichen."
post_onebox_maxlength: "Maximale Länge eines Onebox-Discourse-Beitrags." post_onebox_maxlength: "Maximale Länge eines Onebox-Discourse-Beitrags."
category_post_template: "Die Beitragsvorlage zur Kategoriedefinition beim erstellen einer neuen Kategorie." category_post_template: "Die Beitragsvorlage zur Kategoriedefinition beim erstellen einer neuen Kategorie."
onebox_max_chars: "Maximale Zahl der Zeichen, die eine Onebox von einer externen Webseite in einen Beitrag lädt." onebox_max_chars: "Maximale Zahl der Zeichen, die eine Onebox von einer externen Webseite in einen Beitrag lädt."
@ -457,6 +486,7 @@ de:
apple_touch_icon_url: "Icon für berührungsempfindliche Apple Geräte. Empfohlene Grösse ist 144px auf 144px." apple_touch_icon_url: "Icon für berührungsempfindliche Apple Geräte. Empfohlene Grösse ist 144px auf 144px."
notification_email: "Die Antwortadresse, die in Systemmails (zum Beispiel zur Passwortwiederherstellung, neuen Konten, etc.) eingetragen wird." notification_email: "Die Antwortadresse, die in Systemmails (zum Beispiel zur Passwortwiederherstellung, neuen Konten, etc.) eingetragen wird."
email_custom_headers: "Eine Pipe-getrennte (|) Liste von eigenen Mail Headern"
use_ssl: "Soll die Seite via SSL nutzbar sein?" use_ssl: "Soll die Seite via SSL nutzbar sein?"
best_of_score_threshold: "Der Minimalscore eines Beitrags, um zu den Top Beiträgen zu zählen." best_of_score_threshold: "Der Minimalscore eines Beitrags, um zu den Top Beiträgen zu zählen."
best_of_posts_required: "Minimale Zahl der Beiträge zu einem Thema bevor der Modus 'Top Beiträge' aktiviert wird." best_of_posts_required: "Minimale Zahl der Beiträge zu einem Thema bevor der Modus 'Top Beiträge' aktiviert wird."
@ -476,13 +506,15 @@ de:
cooldown_minutes_after_hiding_posts: "Minuten, die ein Nutzer warten muss, bevor ein Beitrag, der wegen Meldungen versteckt wurde, bearbeitet werden kann." cooldown_minutes_after_hiding_posts: "Minuten, die ein Nutzer warten muss, bevor ein Beitrag, der wegen Meldungen versteckt wurde, bearbeitet werden kann."
num_flags_to_block_new_user: "Wenn ein Beitrag eines neuen Benutzers von (n) anderen Benutzern als Werbung gemeldet wird, verstecke alle Beiträge des Benutzers und erlaube keine neue Beiträge mehr. 0 stellt diese Funktion ab." num_flags_to_block_new_user: "Wenn ein Beitrag eines neuen Benutzers von (n) anderen Benutzern als Werbung gemeldet wird, verstecke alle Beiträge des Benutzers und erlaube keine neue Beiträge mehr. 0 stellt diese Funktion ab."
num_users_to_block_new_user: "Wenn ein Beitrag eines neuen Benutzers von nderen Benutzern (n) mal als Werbung gemeldet wird, verstecke alle Beiträge des Benutzers und erlaube keine neue Beiträge mehr. 0 stellt diese Funktion ab." num_users_to_block_new_user: "Wenn ein Beitrag eines neuen Benutzers von nderen Benutzern (n) mal als Werbung gemeldet wird, verstecke alle Beiträge des Benutzers und erlaube keine neue Beiträge mehr. 0 stellt diese Funktion ab."
notify_mods_when_user_blocked: "Wenn ein Benutzer automatisch gesperrt wird, sende eine Mail an alle Moderatoren."
traditional_markdown_linebreaks: "Traditionelle Zeilenumbrüche in Markdown, anstatt zwei nachfolgende Leerzeichen als Zeilenumbruch zu verwenden." traditional_markdown_linebreaks: "Traditionelle Zeilenumbrüche in Markdown, anstatt zwei nachfolgende Leerzeichen als Zeilenumbruch zu verwenden."
post_undo_action_window_mins: "Sekunden, die ein Nutzer hat, um Aktionen auf Beiträgen rückgängig zu machen (Like, Meldung, etc.)." post_undo_action_window_mins: "Sekunden, die ein Nutzer hat, um Aktionen auf Beiträgen rückgängig zu machen (Like, Meldung, etc.)."
must_approve_users: "Administratoren müssen Nutzer freischalten, bevor sie Zugriff erlangen." must_approve_users: "Administratoren müssen Nutzer freischalten, bevor sie Zugriff erlangen."
ga_tracking_code: "Google Analytics Trackingcode, zum Beispiel: UA-12345678-9; siehe http://google.com/analytics" ga_tracking_code: "Google Analytics Trackingcode, zum Beispiel: UA-12345678-9; siehe http://google.com/analytics"
ga_domain_name: "Google Analytics Domänenname, zum Beispiel: mysite.com; siehe http://google.com/analytics" ga_domain_name: "Google Analytics Domänenname, zum Beispiel: mysite.com; siehe http://google.com/analytics"
enable_escaped_fragments: "Aktiviere Umgehungslösung um älteren Suchmaschinen-Webcrawler zu helfen die Seite zu indexieren. ACHTUNG: Nur aktivieren falls wirklich nötig."
enable_noscript_support: "Aktiviere standard Suchmaschinen-Webcrawler Unterstützung durch den noscript Tag"
top_menu: "Bestimme, welche Navigationselemente in welcher Reihenfolge auftauchen. Beispiel: latest|hot|read|favorited|unread|new|posted|categories" top_menu: "Bestimme, welche Navigationselemente in welcher Reihenfolge auftauchen. Beispiel: latest|hot|read|favorited|unread|new|posted|categories"
post_menu: "Bestimme, welche Funktionen in welcher Reihenfolge im Beitragsmenü auftauchen. Beispiel: like|edit|flag|delete|share|bookmark|reply" post_menu: "Bestimme, welche Funktionen in welcher Reihenfolge im Beitragsmenü auftauchen. Beispiel: like|edit|flag|delete|share|bookmark|reply"
share_links: "Bestimme, welche Dienste in welcher Reihenfolge im Teilen-Dialog auftauchen. Beispiel: twitter|facebook|google+|email" share_links: "Bestimme, welche Dienste in welcher Reihenfolge im Teilen-Dialog auftauchen. Beispiel: twitter|facebook|google+|email"
@ -491,11 +523,14 @@ de:
posts_per_page: "Zahl der Beiträge, die auf einer Themenseite gezeigt werden." posts_per_page: "Zahl der Beiträge, die auf einer Themenseite gezeigt werden."
system_username: "Benutzername des Autors für automatisch vom Forum versendete private Nachrichten." system_username: "Benutzername des Autors für automatisch vom Forum versendete private Nachrichten."
send_welcome_message: "Bekommen neue Nutzer eine Willkommensnachricht?" send_welcome_message: "Bekommen neue Nutzer eine Willkommensnachricht?"
suppress_reply_directly_below: "Zeige die Zahl der Antworten auf einen Beitrag nicht, falls die einzige Antwort direkt darauf folgt." suppress_reply_directly_below: "Zeige die Zahl der Antworten auf einen Beitrag nicht, falls die einzige Antwort direkt darunter folgt."
suppress_reply_directly_above: "Zeige 'In Antwort auf' nicht, falls der Beitrag direkt über der einzigen Antwort folgt."
allow_index_in_robots_txt: "Diese Seite soll durch Suchmaschinen indiziert werden (aktualisiert robots.txt)." allow_index_in_robots_txt: "Diese Seite soll durch Suchmaschinen indiziert werden (aktualisiert robots.txt)."
email_domains_blacklist: "Eine durch senkrechte Striche getrennte Liste von unerlaubten Maildomains. Beispiel: mailinator.com|trashmail.net" email_domains_blacklist: "Eine durch senkrechte Striche getrennte Liste von unerlaubten Maildomains. Beispiel: mailinator.com|trashmail.net"
email_domains_whitelist: "Eine durch senkrechte Striche getrennte Liste von erlaubte Maildomains. WARNUNG: Benutzer mit Mailadressen anderer Domains können sich nicht registrieren." email_domains_whitelist: "Eine durch senkrechte Striche getrennte Liste von erlaubte Maildomains. WARNUNG: Benutzer mit Mailadressen anderer Domains können sich nicht registrieren."
version_checks: "Erfrage Versionsupdate bei Discourse Hub und zeige Versionsbenachrichtigungen auf der Administratorkonsole /admin." version_checks: "Erfrage Versionsupdate bei Discourse Hub und zeige Versionsbenachrichtigungen auf der Administratorkonsole /admin."
new_version_emails: "Sende eine Mail an contact_email Adresse wenn eine neue Version verfügbar ist."
port: "NUR FÜR ENTWICKLER! ACHTUNG! Benutze diesen HTTP-Port anstatt den Standardport 80. Diese Feld leer lassen heißt 'keinen'. Dient hauptsächlich Entwicklungszwecken." port: "NUR FÜR ENTWICKLER! ACHTUNG! Benutze diesen HTTP-Port anstatt den Standardport 80. Diese Feld leer lassen heißt 'keinen'. Dient hauptsächlich Entwicklungszwecken."
force_hostname: "NUR FÜR ENTWICKLER! ACHTUNG! Spezifiziere einen Hostnamen in der URL. Dieses Feld leer lassen heißt 'keinen'. Dient hauptsächlich Entwicklungszwecken." force_hostname: "NUR FÜR ENTWICKLER! ACHTUNG! Spezifiziere einen Hostnamen in der URL. Dieses Feld leer lassen heißt 'keinen'. Dient hauptsächlich Entwicklungszwecken."
@ -559,6 +594,8 @@ de:
s3_secret_access_key: "Der geheime Schlüssel von Amazon S3 welcher für das Hochladen verwendet wird" s3_secret_access_key: "Der geheime Schlüssel von Amazon S3 welcher für das Hochladen verwendet wird"
s3_region: "Der Name der Amazon S3 Region welche für das Hochladen verwendet wird" s3_region: "Der Name der Amazon S3 Region welche für das Hochladen verwendet wird"
enable_flash_video_onebox: "Aktiviere das Einbinden von swf und flv Links in einer Onebox. ACHTUNG: Kann eine Sicherheitsrisiko sein"
default_invitee_trust_level: "Standardwert für die Stufe eines eingeladenen Nutzers (0-4)." default_invitee_trust_level: "Standardwert für die Stufe eines eingeladenen Nutzers (0-4)."
default_trust_level: "Standardwert für die Stufe von Nutzern (0-4)." default_trust_level: "Standardwert für die Stufe von Nutzern (0-4)."
@ -576,10 +613,14 @@ de:
newuser_max_links: "Maximale Zahl der Links, die neue Benutzer Beiträgen hinzufügen dürfen." newuser_max_links: "Maximale Zahl der Links, die neue Benutzer Beiträgen hinzufügen dürfen."
newuser_max_images: "Maximale Zahl der Bilder, die neue Benutzer Beiträgen hinzufügen dürfen." newuser_max_images: "Maximale Zahl der Bilder, die neue Benutzer Beiträgen hinzufügen dürfen."
newuser_max_attachments: "Maximale Zahl der Dateien, die neue Benutzer Beiträgen hinzufügen dürfen."
newuser_max_mentions_per_post: "Maximale Zahl der @Namens-Erwähnungen, die neue Benutzer in Beiträgen nutzen dürfen." newuser_max_mentions_per_post: "Maximale Zahl der @Namens-Erwähnungen, die neue Benutzer in Beiträgen nutzen dürfen."
max_mentions_per_post: "Maximale Zahl der @Namens-Erwähnungen, die man in einem Beitrag nutzen kann." max_mentions_per_post: "Maximale Zahl der @Namens-Erwähnungen, die man in einem Beitrag nutzen kann."
create_thumbnails: "Erstelle Vorschaubilder für Bilder in einer Lightbox"
email_time_window_mins: "Minuten Wartezeit, bevor eine Mail an Nutzer verschickt wird, um ihnen die Chance zu geben, eine Neuigkeit zuerst zu sehen." email_time_window_mins: "Minuten Wartezeit, bevor eine Mail an Nutzer verschickt wird, um ihnen die Chance zu geben, eine Neuigkeit zuerst zu sehen."
email_posts_context: "Anzahl der Antworten welche als Konext einer Notifikations-Mail hinzugefügt werden."
flush_timings_secs: "Sekunden, nach denen Zeiteinstellungen auf den Server übertragen werden." flush_timings_secs: "Sekunden, nach denen Zeiteinstellungen auf den Server übertragen werden."
max_word_length: "Maximale Wortlänge in Zeichen in Thementiteln." max_word_length: "Maximale Wortlänge in Zeichen in Thementiteln."
title_min_entropy: "Minimal nötige Entropie (einzigartige Zeichen) in einem Thementitel." title_min_entropy: "Minimal nötige Entropie (einzigartige Zeichen) in einem Thementitel."
@ -591,7 +632,9 @@ de:
min_body_similar_length: "Minimale Länge eines Beitragstextes, bevor nach ähnlichen Themen gesucht wird." min_body_similar_length: "Minimale Länge eines Beitragstextes, bevor nach ähnlichen Themen gesucht wird."
category_colors: "Eine durch senkrechte Striche getrennte Liste hexadezimaler Farbwerte, die als Kategoriefarben erlaubt sind." category_colors: "Eine durch senkrechte Striche getrennte Liste hexadezimaler Farbwerte, die als Kategoriefarben erlaubt sind."
max_image_size_kb: "Maximale Größe in Kilobytes (kB), die von Benutzern hochgeladene Bilder groß sein dürfen. Stelle sicher, dass dieser Wert auch in nginx (client_max_body_size) / apache und Proxies konfiguriert ist." max_image_size_kb: "Maximale Größe in Kilobytes (kB), die von Benutzern hochgeladene Bilder groß sein dürfen. Stelle sicher, dass dieser Wert auch in nginx (client_max_body_size) / Apache und Proxies konfiguriert ist."
max_attachment_size_kb: "Maximale Größe in Kilobytes (kB), die von Benutzern hochgeladenen Dateien groß sein dürfen. Stelle sicher, dass dieser Wert auch in nginx (client_max_body_size) / Apache und Proxies konfiguriert ist."
authorized_extensions: "Eine Pipe-getrennte (|) Liste von Dateiendungen welche hochgeladen werden dürfen."
max_similar_results: "Anzahl ähnlicher Themen, die ein Nutzer sieht, während er ein neues Thema erstellen." max_similar_results: "Anzahl ähnlicher Themen, die ein Nutzer sieht, während er ein neues Thema erstellen."
title_prettify: "Verhindert gängige Fehler im Titel, wie reine Grossschreibung, Kleinbuchstaben am Anfang, mehrere ! und ?, überflüssiger . am Ende, etc." title_prettify: "Verhindert gängige Fehler im Titel, wie reine Grossschreibung, Kleinbuchstaben am Anfang, mehrere ! und ?, überflüssiger . am Ende, etc."
@ -600,12 +643,33 @@ de:
topic_views_heat_medium: "Die Anzahl der Aufrufe bis die Popularität des Themas mittel ist." topic_views_heat_medium: "Die Anzahl der Aufrufe bis die Popularität des Themas mittel ist."
topic_views_heat_high: "Die Anzahl der Aufrufe bis die Popularität des Themas hoch ist." topic_views_heat_high: "Die Anzahl der Aufrufe bis die Popularität des Themas hoch ist."
faq_url: "URL zu einer externen FAQ welche Du gerne verwenden möchtest."
tos_url: "URL zu einer externen Dienstleistungsbedingung welche Du gerne verwenden möchtest." tos_url: "URL zu einer externen Dienstleistungsbedingung welche Du gerne verwenden möchtest."
privacy_policy_url: "URL zu einer externen Datenschutzrichtlinie welche Du gerne verwenden möchtest." privacy_policy_url: "URL zu einer externen Datenschutzrichtlinie welche Du gerne verwenden möchtest."
newuser_spam_host_threshold: "Die Anzahl welche ein Frischling Beiträge mit Links auf die gleiche Seite innerhalb ihrer `newuser_spam_host_posts` veröffentlichen , bevor der Beitrag als Spam klassifiziert wird." newuser_spam_host_threshold: "Die Anzahl welche ein Frischling Beiträge mit Links auf die gleiche Seite innerhalb ihrer `newuser_spam_host_posts` veröffentlichen , bevor der Beitrag als Spam klassifiziert wird."
staff_like_weight: "Zusätzlicher Gewichtungsfaktor wenn Mitglieder „Gefällt mir“ verteilen." staff_like_weight: "Zusätzlicher Gewichtungsfaktor wenn Mitglieder „Gefällt mir“ verteilen."
reply_by_email_enabled: "Erlaube das Antworten auf Themen via Mail"
reply_by_email_address: "Vorgabe der Antwort-Mail Adresse in der Form von: %{reply_key}@reply.myforum.com"
pop3s_polling_enabled: "Antworten via POP3S anfragen"
pop3s_polling_port: "Der Port für die POP3S Anfrage"
pop3s_polling_host: "Der Host für die POP3S Anfrage"
pop3s_polling_username: "Der Benutzername für die POP3S Anfrage"
pop3s_polling_password: "Das Passwort für die POP3S Anfrage"
minimum_topics_similar: "Wie viele Themen in der Datenbank existieren müssen, bevor ähnliche Themen angezeigt werden."
relative_date_duration: "Anzahl von Tagen nach nach welchen das Beitragsdatum relativ und nicht absolut angezeigt wird. Beispiel: relatives Datum: 7T, absolutes Datum: 20 Feb"
delete_user_max_age: "Nach wievielen Tagen ein Benutzerkonto von einem Administrator gelöscht werden kann."
delete_all_posts_max: "Die maximale Anzahl von Beiträgen welche auf einmal gelöscht werden kann. Hat ein Benutzer mehr Beiträge, so können die Beiträge nicht auf einmal und der Benutzer nicht gelöscht werden."
username_change_period: "Wie lange neu registrierte Benutzer ihren Benutzernamen ändern können."
allow_uploaded_avatars: "Erlaube das Hochladen eines eigenen Avatars"
allow_animated_avatars: "Erlaube den Benutzern animierte GIFs als Avatar zu benutzen"
default_digest_email_frequency: "Wie oft man Zusammenfassungen per Mail standardmässig erhält. Diese Einstellung kann von jedem geändert werden."
notification_types: notification_types:
mentioned: "%{display_username} hat Dich in %{link} erwähnt." mentioned: "%{display_username} hat Dich in %{link} erwähnt."
liked: "%{display_username} gefällt deinen Beitrag in %{link}." liked: "%{display_username} gefällt deinen Beitrag in %{link}."
@ -633,6 +697,9 @@ de:
moderator_post: moderator_post:
one: "Ich habe einen Beitrag in ein neues Thema verschoben: %{topic_link}" one: "Ich habe einen Beitrag in ein neues Thema verschoben: %{topic_link}"
other: "Ich habe %{count} Beiträge in ein neues Thema verschoben: %{topic_link}" other: "Ich habe %{count} Beiträge in ein neues Thema verschoben: %{topic_link}"
existing_topic_moderator_post:
one: "Ich habe den Beitrag in ein vorhandenes Thema verschoben: %{topic_link}"
other: "Ich hab %{count} Beiträge in ein vorhandenes Thema verschoben: %{topic_link}"
topic_statuses: topic_statuses:
archived_enabled: "Dieses Thema ist nun archiviert. Es ist eingefroren und kann in keiner Weise mehr verändert werden." archived_enabled: "Dieses Thema ist nun archiviert. Es ist eingefroren und kann in keiner Weise mehr verändert werden."
@ -656,6 +723,7 @@ de:
active: "Dein Konto ist nun freigeschaltet und einsatzbereit." active: "Dein Konto ist nun freigeschaltet und einsatzbereit."
activate_email: "Fast fertig! Wir haben eine Aktivierungsmail an <b>%{email}</b> verschickt. Bitte folge den Anweisungen in der Mail, um Dein Konto zu aktivieren." activate_email: "Fast fertig! Wir haben eine Aktivierungsmail an <b>%{email}</b> verschickt. Bitte folge den Anweisungen in der Mail, um Dein Konto zu aktivieren."
not_activated: "Du kannst Dich noch nicht anmelden. Wir haben Dir eine Aktivierungsmail geschickt. Bitte folge zunächst den Anweisungen aus der Mail, um Dein Konto zu aktivieren." not_activated: "Du kannst Dich noch nicht anmelden. Wir haben Dir eine Aktivierungsmail geschickt. Bitte folge zunächst den Anweisungen aus der Mail, um Dein Konto zu aktivieren."
banned: "Du kannst dich bis am %{date} nicht mehr anmelden."
errors: "%{errors}" errors: "%{errors}"
not_available: " Nicht verfügbar. Versuche %{suggestion}?" not_available: " Nicht verfügbar. Versuche %{suggestion}?"
something_already_taken: "Etwas ist schief gelaufen. Möglicherweise ist der Benutzername bereits registriert. Probiere den 'Passwort vergessen'-Link." something_already_taken: "Etwas ist schief gelaufen. Möglicherweise ist der Benutzername bereits registriert. Probiere den 'Passwort vergessen'-Link."
@ -716,6 +784,8 @@ de:
Deine Freunde von %{site_name}. Deine Freunde von %{site_name}.
:smile:
[0]: %{base_url} [0]: %{base_url}
[1]: http://www.kitterman.com/spf/validate.html [1]: http://www.kitterman.com/spf/validate.html
[2]: http://mxtoolbox.com/SuperTool.aspx [2]: http://mxtoolbox.com/SuperTool.aspx
@ -728,6 +798,17 @@ de:
<small>Am Fuß jeder Mail, die Du verschickst, sollte eine Möglichkeit zum Abbestellen gegeben werden. Hier ein Beispiel: Diese Mail wurde von Unternehmensname, Hauptstraße 55, 12345 Stadtname, Deutschland, versendet. Wenn Du zukünftig keine weiteren Mail erhalten möchtest, [klicke hier, um dich abzumelden][5].</small> <small>Am Fuß jeder Mail, die Du verschickst, sollte eine Möglichkeit zum Abbestellen gegeben werden. Hier ein Beispiel: Diese Mail wurde von Unternehmensname, Hauptstraße 55, 12345 Stadtname, Deutschland, versendet. Wenn Du zukünftig keine weiteren Mail erhalten möchtest, [klicke hier, um dich abzumelden][5].</small>
new_version_mailer:
subject_template: "[%{site_name}] neue Version verfügbar"
text_body_template: |
Eine neue Version von Discourse ist verfügbar.
**Neue Version: %{new_version}**
Deine Version: %{installed_version}
Bitte aktuallisiere die Installation so bald wie möglich um die neusten Fehlerbehebungen und Funktionen zu erhalten.
system_messages: system_messages:
post_hidden: post_hidden:
subject_template: "Beitrag wegen Meldungen aus der Community versteckt" subject_template: "Beitrag wegen Meldungen aus der Community versteckt"
@ -839,6 +920,15 @@ de:
Weitere Hilfe findest du in unserer [FAQ](%{base_url}/faq). Weitere Hilfe findest du in unserer [FAQ](%{base_url}/faq).
blocked_by_staff:
subject_template: "Konto gesperrt"
text_body_template: |
Hallo,
Dies ist eine automatische Nachricht von %{site_name} um dich zu informierenm, dass dein Konto durch einem Moderator gesperrt wurde.
Weitere Hilfe findest du in unserer [FAQ](%{base_url}/faq).
user_automatically_blocked: user_automatically_blocked:
subject_template: "Benutzer %{username} wurde automatisch gesperrt" subject_template: "Benutzer %{username} wurde automatisch gesperrt"
text_body_template: | text_body_template: |
@ -846,6 +936,13 @@ de:
Bitte [überprüfe die Beanstandungen](/admin/flags). Wenn %{username} nicht mehr gesperrt sein soll, schalte den Benutzer in der [Benuzeradministration](%{user_url}) wieder frei. Bitte [überprüfe die Beanstandungen](/admin/flags). Wenn %{username} nicht mehr gesperrt sein soll, schalte den Benutzer in der [Benuzeradministration](%{user_url}) wieder frei.
spam_post_blocked:
subject_template: "Spam wirde in einem Beitrag von %{username} entdeckt"
text_body_template: |
Dies ist eine automatische Nachricht um dich zu informieren, dass [%{username}](%{user_url}) versucht hat einen Beitrag mit Links zu erstellen, was aber basierend auf der Einstellung newuser_spam_host_threshold unterbunden wurde.
Bitte [überprüfe den Benutzer](%{user_url}).
unblocked: unblocked:
subject_template: "Benutzerkonto entsperrt" subject_template: "Benutzerkonto entsperrt"
text_body_template: | text_body_template: |
@ -855,13 +952,28 @@ de:
Du kannst nun wieder Themen erstellen und Beiträge veröffentlichen. Du kannst nun wieder Themen erstellen und Beiträge veröffentlichen.
pending_users_reminder:
subject_template:
one: "Es gibt einen nicht freigegebenen Benutzer"
other: "Es gibt %{count} nicht freigegebene Benutzer"
text_body_template: |
Es warten neuen Benutzer auf ihre Freigabe.
[Bitte bewerte diese im Administrationsbereich](/admin/users/list/pending).
unsubscribe_link: "Wenn Du diese Mails nicht mehr erhalten möchtest, verändere deine [Benutzereinstellungen](%{user_preferences_url})." unsubscribe_link: "Wenn Du diese Mails nicht mehr erhalten möchtest, verändere deine [Benutzereinstellungen](%{user_preferences_url})."
user_notifications: user_notifications:
previous_discussion: "Vorangehende Antworten"
unsubscribe: unsubscribe:
title: "Mails Abbestellen" title: "Mails Abbestellen"
description: "Nicht interessiert an diesen Mails? Kein Problem! Klicke unten um Dich abzumelden:" description: "Nicht interessiert an diesen Mails? Kein Problem! Klicke unten um Dich abzumelden:"
reply_by_email: "Um zu Antworten, antworte auf diese Email oder besuche %{base_url}%{url} in deinem Browser."
visit_link_to_respond: "Um zu Antworten, besuche %{base_url}%{url} in deinem Browser."
posted_by: "Erstellt von %{username} am %{post_date}"
user_invited_to_private_message: user_invited_to_private_message:
subject_template: "[%{site_name}] %{username} hat Dich zu einem privaten Gespräch eingeladen: '%{topic_title}'" subject_template: "[%{site_name}] %{username} hat Dich zu einem privaten Gespräch eingeladen: '%{topic_title}'"
text_body_template: | text_body_template: |
@ -872,52 +984,49 @@ de:
user_replied: user_replied:
subject_template: "[%{site_name}] %{username} hat auf deinen Beitrag '%{topic_title}' geantwortet" subject_template: "[%{site_name}] %{username} hat auf deinen Beitrag '%{topic_title}' geantwortet"
text_body_template: | text_body_template: |
%{username} hat auf deinen Beitrag '%{topic_title}' auf %{site_name} geantwortet:
---
%{message} %{message}
%{context}
--- ---
Um zu antworten, besuche den folgenden Link: %{base_url}%{url} %{respond_instructions}
user_quoted: user_quoted:
subject_template: "[%{site_name}] %{username} hat Dich in '%{topic_title}' zitiert" subject_template: "[%{site_name}] %{username} hat Dich in '%{topic_title}' zitiert"
text_body_template: | text_body_template: |
%{username} hat Dich in '%{topic_title}' auf %{site_name} zitiert:
---
%{message} %{message}
%{context}
--- ---
Um zu antworten, besuche den folgenden Link: %{base_url}%{url} %{respond_instructions}
user_mentioned: user_mentioned:
subject_template: "[%{site_name}] %{username} hat Dich in '%{topic_title}' erwähnt" subject_template: "[%{site_name}] %{username} hat Dich in '%{topic_title}' erwähnt"
text_body_template: | text_body_template: |
%{username} hat Dich in '%{topic_title}' auf %{site_name} erwähnt:
---
%{message} %{message}
%{context}
--- ---
Um zu antworten, besuche den folgenden Link: %{base_url}%{url} %{respond_instructions}
user_posted: user_posted:
subject_template: "[%{site_name}] %{subject_prefix}%{username} hat auf '%{topic_title}' geantwortet" subject_template: "[%{site_name}] %{subject_prefix}%{username} hat auf '%{topic_title}' geantwortet"
text_body_template: | text_body_template: |
%{username} hat in '%{topic_title}' auf %{site_name} geantwortet:
---
%{message} %{message}
%{context}
--- ---
Um zu antworten, besuche den folgenden Link: %{base_url}%{url} %{respond_instructions}
digest: digest:
why: "Hier eine kurze Zusammenfassung, was auf %{site_link} passiert ist, seit Du das letzte Mal am %{last_seen_at} da warst." why: "Hier eine kurze Zusammenfassung, was auf %{site_link} passiert ist, seit Du das letzte Mal am %{last_seen_at} da warst."
subject_template: "[%{site_name}] Forenaktivität für den %{date}" subject_template: "[%{site_name}] Forenaktivität für den %{date}"
new_activity: "Neues in deinen Themen und Beiträgen:" new_activity: "Neues in deinen Themen und Beiträgen:"
top_topics: "Inhalte die dich vielleicht interessieren:" top_topics: "Inhalte die dich vielleicht interessieren:"
other_new_topics: "Andere neue Themen:"
unsubscribe: "Diese Zusammenfassung wurde Dir von %{site_link} geschickt, damit Du auf dem Laufenden bleibst, und weil wir nicht eine Weile nicht begrüßen durften.\nWenn Du diese Benachrichtigungen nicht mehr erhalten möchtest, kannst Du sie in deinen Maileinstellungen abschalten: %{unsubscribe_link}." unsubscribe: "Diese Zusammenfassung wurde Dir von %{site_link} geschickt, damit Du auf dem Laufenden bleibst, und weil wir nicht eine Weile nicht begrüßen durften.\nWenn Du diese Benachrichtigungen nicht mehr erhalten möchtest, kannst Du sie in deinen Maileinstellungen abschalten: %{unsubscribe_link}."
click_here: "klicke hier" click_here: "klicke hier"
from: "%{site_name} Übersicht" from: "%{site_name} Übersicht"
@ -992,8 +1101,12 @@ de:
deleted: 'gelöscht' deleted: 'gelöscht'
upload: upload:
pasted_image_filename: "" unauthorized: "Entschuldige, die Datei die du hochladen möchtest ist nicht erlaubt (Erlaubte Dateiendungen: %{authorized_extensions})."
pasted_image_filename: "Hinzugefügtes Bild"
attachments:
too_large: "Entschuldige, die Datei die du hochladen möchtest ist zu gross (Maximale Dateigrösse ist %{max_size_kb}%kb)."
images: images:
fetch_failure: "Entschuldige, beim Laden des Bildes ist ein Fehler aufgetreten." too_large: "Entschuldige, das Bild welches du hochladen möchtest ist zu gross (Maximale Dateigrösse ist %{max_size_kb}%kb), bitte verkleinere es und versuche es nochmals."
fetch_failure: "Sorry, there has been an error while fetching the image."
unknown_image_type: "Entschuldige, aber die Datei die Du hochladen möchtest schein kein Bild zu sein." unknown_image_type: "Entschuldige, aber die Datei die Du hochladen möchtest schein kein Bild zu sein."
size_not_found: "Entschuldige, aber wir konnten die Grösse des Bildes nicht feststellen. Vielleicht ist das Bild defekt?" size_not_found: "Entschuldige, aber wir konnten die Grösse des Bildes nicht feststellen. Vielleicht ist das Bild defekt?"

View File

@ -94,13 +94,13 @@ en:
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}. For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
'new-reply': | 'new-reply': |
Welcome to %{site_name} &mdash; **thanks for contributing to the conversation!** Welcome to %{site_name} &mdash; **thanks for contributing!**
- Does your reply improve the conversation in some way? - Does your reply improve the conversation in some way?
- Be kind to your fellow community members. - Be kind to your fellow community members.
- Constructive criticism is welcome, but remember to criticize *ideas*, not people. - Constructive criticism is welcome, but criticize *ideas*, not people.
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}. For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
@ -611,6 +611,8 @@ en:
regular_requires_likes_given: "How many likes a basic user must cast before promotion to regular (2) trust level" regular_requires_likes_given: "How many likes a basic user must cast before promotion to regular (2) trust level"
regular_requires_topic_reply_count: "How many topics a basic user must reply to before promotion to regular (2) trust level" regular_requires_topic_reply_count: "How many topics a basic user must reply to before promotion to regular (2) trust level"
min_trust_to_create_topic: "The minimum trust level required to create a new topic."
newuser_max_links: "How many links a new user can add to a post" newuser_max_links: "How many links a new user can add to a post"
newuser_max_images: "How many images a new user can add to a post" newuser_max_images: "How many images a new user can add to a post"
newuser_max_attachments: "How many attachments a new user can add to a post" newuser_max_attachments: "How many attachments a new user can add to a post"
@ -937,9 +939,9 @@ en:
Please [review the flags](/admin/flags). If %{username} was incorrectly blocked from posting, click the unblock button on [the admin page for this user](%{user_url}). Please [review the flags](/admin/flags). If %{username} was incorrectly blocked from posting, click the unblock button on [the admin page for this user](%{user_url}).
spam_post_blocked: spam_post_blocked:
subject_template: "Spam was detected in a post by %{username}" subject_template: "New user %{username} is posting repeated links"
text_body_template: | text_body_template: |
This is an automated message to inform you that [%{username}](%{user_url}) tried to make a post with links, but it was stopped as spam based on the newuser_spam_host_threshold site setting. This is an automated message to inform you that the new user [%{username}](%{user_url}) tried to create multiple posts with links to the same domain, but they were blocked based on the newuser_spam_host_threshold site setting.
Please [review the user](%{user_url}). Please [review the user](%{user_url}).
@ -1027,7 +1029,7 @@ en:
new_activity: "New activity on your topics and posts:" new_activity: "New activity on your topics and posts:"
top_topics: "Recent posts the community enjoyed:" top_topics: "Recent posts the community enjoyed:"
other_new_topics: "Other New Topics:" other_new_topics: "Other New Topics:"
unsubscribe: "This summary email is sent as a courtesy notification from %{site_link} when we haven't seen you in a while.\nTo unsubscribe or change your email preferences, %{unsubscribe_link}." unsubscribe: "This summary email is sent as a courtesy notification from %{site_link} when we haven't seen you in a while. To unsubscribe or change your email preferences, %{unsubscribe_link}."
click_here: "click here" click_here: "click here"
from: "%{site_name} digest" from: "%{site_name} digest"
read_more: "Read More" read_more: "Read More"

View File

@ -47,13 +47,13 @@ id:
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}. For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
'new-reply': | 'new-reply': |
Welcome to %{site_name} &mdash; **thanks for contributing to the conversation!** Welcome to %{site_name} &mdash; **thanks for contributing!**
- Does your reply improve the conversation in some way? - Does your reply improve the conversation in some way?
- Be kind to your fellow community members. - Be kind to your fellow community members.
- Constructive criticism is welcome, but remember to criticize *ideas*, not people. - Constructive criticism is welcome, but criticize *ideas*, not people.
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}. For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
@ -725,7 +725,7 @@ id:
subject_template: "[%{site_name}] Forum Activity for %{date}" subject_template: "[%{site_name}] Forum Activity for %{date}"
new_activity: "New activity on your topics and posts:" new_activity: "New activity on your topics and posts:"
new_topics: "New topics:" new_topics: "New topics:"
unsubscribe: "This summary email is sent as a courtesy notification from %{site_link} when we haven't seen you in a while.\nTo unsubscribe or change your email preferences, %{unsubscribe_link}." unsubscribe: "This summary email is sent as a courtesy notification from %{site_link} when we haven't seen you in a while. To unsubscribe or change your email preferences, %{unsubscribe_link}."
click_here: "click here" click_here: "click here"
from: "%{site_name} digest" from: "%{site_name} digest"

View File

@ -74,7 +74,7 @@ ko:
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}. For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
'new-reply': | 'new-reply': |
Welcome to %{site_name} &mdash; **thanks for contributing to the conversation!** Welcome to %{site_name} &mdash; **thanks for contributing!**
Keep in mind as you compose your reply: Keep in mind as you compose your reply:
@ -82,7 +82,7 @@ ko:
- Be kind to your fellow community members. - Be kind to your fellow community members.
- Constructive criticism is welcome, but remember to criticize *ideas*, not people. - Constructive criticism is welcome, but criticize *ideas*, not people.
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}. For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
@ -870,7 +870,7 @@ ko:
subject_template: "[%{site_name}] Forum Activity for %{date}" subject_template: "[%{site_name}] Forum Activity for %{date}"
new_activity: "New activity on your topics and posts:" new_activity: "New activity on your topics and posts:"
new_topics: "New topics:" new_topics: "New topics:"
unsubscribe: "This summary email is sent as a courtesy notification from %{site_link} when we haven't seen you in a while.\nTo unsubscribe or change your email preferences, %{unsubscribe_link}." unsubscribe: "This summary email is sent as a courtesy notification from %{site_link} when we haven't seen you in a while. To unsubscribe or change your email preferences, %{unsubscribe_link}."
click_here: "click here" click_here: "click here"
from: "%{site_name} digest" from: "%{site_name} digest"

View File

@ -532,6 +532,7 @@ ru:
edit_history_visible_to_public: Позволить всем видеть предыдущие версии сообщения. Когда отключено, историю изменений может видеть только персонал. edit_history_visible_to_public: Позволить всем видеть предыдущие версии сообщения. Когда отключено, историю изменений может видеть только персонал.
delete_removed_posts_after: Количество часов, после которого сообщение, удаленное пользователем, удаляется. delete_removed_posts_after: Количество часов, после которого сообщение, удаленное пользователем, удаляется.
max_image_width: Максимальная ширина изображений, добавляемых в сообщение max_image_width: Максимальная ширина изображений, добавляемых в сообщение
max_image_height: Максимальная высота изображения в сообщении
category_featured_topics: Количество отображаемых тем в категориях на странице /categories category_featured_topics: Количество отображаемых тем в категориях на странице /categories
add_rel_nofollow_to_user_content: 'Добавить "rel nofollow" для всех ссылок за исключением внутренних (включая родительский домен). Изменение данной настройки потребует обновления всех сообщений (<code>rake posts:rebake</code>)' add_rel_nofollow_to_user_content: 'Добавить "rel nofollow" для всех ссылок за исключением внутренних (включая родительский домен). Изменение данной настройки потребует обновления всех сообщений (<code>rake posts:rebake</code>)'
exclude_rel_nofollow_domains: Разделенный запятыми список доменов, в которых nofollow не добавлено (tld.com автоматически позволит также и sub.tld.com) exclude_rel_nofollow_domains: Разделенный запятыми список доменов, в которых nofollow не добавлено (tld.com автоматически позволит также и sub.tld.com)
@ -941,11 +942,11 @@ ru:
Пожалуйста [проверьте жалобы](/admin/flags). Если пользователь %{username} был заблокирован неверно, нажмите кнопку разблокировки [на странице управления пользователем](%{user_url}). Пожалуйста [проверьте жалобы](/admin/flags). Если пользователь %{username} был заблокирован неверно, нажмите кнопку разблокировки [на странице управления пользователем](%{user_url}).
spam_post_blocked: spam_post_blocked:
subject_template: 'В сообщении пользователя %{username} обнаружен спам' subject_template: 'Новый пользователь %{username} отправляет одинаковые ссылки'
text_body_template: | text_body_template: |
Это автоматическое сообщение для информирования о том, что пользователь [%{username}](%{user_url}) попытался создать сообщение со ссылками, но был остановлен политикой антиспама на основе настройки сайта newuser_spam_host_threshold. Это автоматическое сообщение. Новый пользователь [%{username}](%{user_url}) попытался создать множество сообщений со ссылкой на один и тот же домен, однако был заблокирован на основании настройки newuser_spam_host_threshold.
Пожалуйста [проверьте действия пользователя](%{user_url}). Пожалуйста [проверьте блокировку](%{user_url}).
unblocked: unblocked:
subject_template: Учетная запись разблокирована subject_template: Учетная запись разблокирована
@ -954,6 +955,17 @@ ru:
Это автоматическое сообщение сайта %{site_name}. Ваш аккаунт был разблокирован. Теперь вы можете создавать новые темы и отвечать в них. Это автоматическое сообщение сайта %{site_name}. Ваш аккаунт был разблокирован. Теперь вы можете создавать новые темы и отвечать в них.
pending_users_reminder:
subject_template:
one: Один неутвержденный пользователь
other: '%{count} неутвержденных пользователей'
few: '%{count} неутвержденных пользователя'
many: '%{count} неутвержденных пользователей'
text_body_template: |
Новые пользователи ожидают утверждения.
[Пожалуйста, проверьте их список в секции администрирования](/admin/users/list/pending).
unsubscribe_link: 'Для того, чтобы отписаться от подобных сообщений, перейдите в [настройки профиля](%{user_preferences_url}).' unsubscribe_link: 'Для того, чтобы отписаться от подобных сообщений, перейдите в [настройки профиля](%{user_preferences_url}).'
user_notifications: user_notifications:
previous_discussion: Предыдущие ответы previous_discussion: Предыдущие ответы
@ -1016,9 +1028,7 @@ ru:
new_activity: 'Новая активность в ваших темах и сообщениях:' new_activity: 'Новая активность в ваших темах и сообщениях:'
top_topics: 'Последние темы, которые были оценены пользователями форума:' top_topics: 'Последние темы, которые были оценены пользователями форума:'
other_new_topics: 'Другие новые темы:' other_new_topics: 'Другие новые темы:'
unsubscribe: | unsubscribe: 'Данное сообщение отправлено как напоминание с сайта %{site_link} потому что вы давно не заходили к нам. Для того, чтобы отписаться от наших сообщений, пройдите по ссылке %{unsubscribe_link}.'
Данное сообщение отправлено как напоминание с сайта %{site_link} потому что вы давно не заходили к нам.
Для того, чтобы отписаться от наших сообщений, пройдите по ссылке %{unsubscribe_link}.
click_here: нажмите здесь click_here: нажмите здесь
from: 'Cводка новостей сайта %{site_name}' from: 'Cводка новостей сайта %{site_name}'
read_more: Читать еще read_more: Читать еще

View File

@ -783,7 +783,7 @@ sv:
subject_template: "[%{site_name}] Forum Activity for %{date}" subject_template: "[%{site_name}] Forum Activity for %{date}"
new_activity: "New activity on your topics and posts:" new_activity: "New activity on your topics and posts:"
new_topics: "New topics:" new_topics: "New topics:"
unsubscribe: "This summary email is sent as a courtesy notification from %{site_link} when we haven't seen you in a while.\nTo unsubscribe or change your email preferences, %{unsubscribe_link}." unsubscribe: "This summary email is sent as a courtesy notification from %{site_link} when we haven't seen you in a while. To unsubscribe or change your email preferences, %{unsubscribe_link}."
click_here: "click here" click_here: "click here"
from: "%{site_name} digest" from: "%{site_name} digest"

View File

@ -216,6 +216,7 @@ Discourse::Application.routes.draw do
get 'topics/created-by/:username' => 'list#topics_by', as: 'topics_by', constraints: {username: USERNAME_ROUTE_FORMAT} get 'topics/created-by/:username' => 'list#topics_by', as: 'topics_by', constraints: {username: USERNAME_ROUTE_FORMAT}
get 'topics/private-messages/:username' => 'list#private_messages', as: 'topics_private_messages', constraints: {username: USERNAME_ROUTE_FORMAT} get 'topics/private-messages/:username' => 'list#private_messages', as: 'topics_private_messages', constraints: {username: USERNAME_ROUTE_FORMAT}
get 'topics/private-messages-sent/:username' => 'list#private_messages_sent', as: 'topics_private_messages_sent', constraints: {username: USERNAME_ROUTE_FORMAT} get 'topics/private-messages-sent/:username' => 'list#private_messages_sent', as: 'topics_private_messages_sent', constraints: {username: USERNAME_ROUTE_FORMAT}
get 'topics/private-messages-unread/:username' => 'list#private_messages_unread', as: 'topics_private_messages_unread', constraints: {username: USERNAME_ROUTE_FORMAT}
# Topic routes # Topic routes
get 't/:slug/:topic_id/wordpress' => 'topics#wordpress', constraints: {topic_id: /\d+/} get 't/:slug/:topic_id/wordpress' => 'topics#wordpress', constraints: {topic_id: /\d+/}

View File

@ -0,0 +1,19 @@
class FixOptimizedImagesUrls < ActiveRecord::Migration
def up
# `AddUrlToOptimizedImages` was wrongly computing the URLs. This fixes it!
execute "UPDATE optimized_images
SET url = substring(oi.url from '^\\/uploads\\/[^/]+\\/_optimized\\/[0-9a-f]{3}\\/[0-9a-f]{3}\\/[0-9a-f]{11}')
|| '_'
|| oi.width
|| 'x'
|| oi.height
|| substring(oi.url from '\\.\\w{3,4}$')
FROM optimized_images oi
WHERE optimized_images.id = oi.id
AND oi.url ~ '^\\/uploads\\/[^/]+\\/_optimized\\/[0-9a-f]{3}\\/[0-9a-f]{3}\\/[0-9a-f]{11}\\.';"
end
def down
raise ActiveRecord::IrreversibleMigration
end
end

View File

@ -0,0 +1,12 @@
class AllowNullUserIdOnPosts < ActiveRecord::Migration
def up
change_column :posts, :user_id, :integer, null: true
execute "UPDATE posts SET user_id = NULL WHERE nuked_user = true"
remove_column :posts, :nuked_user
end
def down
add_column :posts, :nuked_user, :boolean, default: false
change_column :posts, :user_id, :integer, null: false
end
end

View File

@ -0,0 +1,9 @@
class AllowNullUserIdOnTopics < ActiveRecord::Migration
def up
change_column :topics, :user_id, :integer, null: true
end
def down
change_column :topics, :user_id, :integer, null: false
end
end

View File

@ -19,11 +19,9 @@ Note: If you are developing on a Mac, you will probably want to look at [these i
## Before you start Rails ## Before you start Rails
1. `bundle install` 1. `bundle install`
2. `bundle exec rake db:migrate` 2. `bundle exec rake db:migrate db:test:prepare db:seed_fu`
3. `bundle exec rake db:test:prepare` 4. Try running the specs: `bundle exec rake autospec`
4. `bundle exec rake db:seed_fu` 5. `bundle exec rails server`
5. Try running the specs: `bundle exec rake autospec`
6. `bundle exec rails server`
You should now be able to connect to rails on [http://localhost:3000](http://localhost:3000) - try it out! The seed data includes a pinned topic that explains how to get an admin account, so start there! Happy hacking! You should now be able to connect to rails on [http://localhost:3000](http://localhost:3000) - try it out! The seed data includes a pinned topic that explains how to get an admin account, so start there! Happy hacking!

View File

@ -48,7 +48,7 @@ If you have a mail server responsible for handling the egress of email from your
Install necessary packages: Install necessary packages:
# Run these commands as your normal login (e.g. "michael") # Run these commands as your normal login (e.g. "michael")
sudo apt-get -y install build-essential libssl-dev libyaml-dev git libtool libxslt-dev libxml2-dev libpq-dev gawk curl pngcrush python-software-properties sudo apt-get -y install build-essential libssl-dev libyaml-dev git libtool libxslt-dev libxml2-dev libpq-dev gawk curl pngcrush imagemagick python-software-properties
# If you're on Ubuntu >= 12.10, change: # If you're on Ubuntu >= 12.10, change:
# python-software-properties to software-properties-common # python-software-properties to software-properties-common
@ -187,7 +187,6 @@ Edit /var/www/discourse/config/discourse.pill
- change application name from 'discourse' if necessary - change application name from 'discourse' if necessary
- Ensure appropriate Bluepill.application line is uncommented - Ensure appropriate Bluepill.application line is uncommented
- search for "host to run on" and change to current hostname
Edit /var/www/discourse/config/environments/production.rb Edit /var/www/discourse/config/environments/production.rb
- browse througn all the settings - browse througn all the settings

View File

@ -7,3 +7,4 @@ require_dependency 'auth/open_id_authenticator'
require_dependency 'auth/github_authenticator' require_dependency 'auth/github_authenticator'
require_dependency 'auth/twitter_authenticator' require_dependency 'auth/twitter_authenticator'
require_dependency 'auth/persona_authenticator' require_dependency 'auth/persona_authenticator'
require_dependency 'auth/cas_authenticator'

View File

@ -1,7 +1,8 @@
class Auth::Result class Auth::Result
attr_accessor :user, :name, :username, :email, :user, attr_accessor :user, :name, :username, :email, :user,
:email_valid, :extra_data, :awaiting_activation, :email_valid, :extra_data, :awaiting_activation,
:awaiting_approval, :authenticated, :authenticator_name :awaiting_approval, :authenticated, :authenticator_name,
:requires_invite
def session_data def session_data
{ {
@ -15,7 +16,9 @@ class Auth::Result
end end
def to_client_hash def to_client_hash
if user if requires_invite
{ requires_invite: true }
elsif user
{ {
authenticated: !!authenticated, authenticated: !!authenticated,
awaiting_activation: !!awaiting_activation, awaiting_activation: !!awaiting_activation,

View File

@ -110,4 +110,16 @@ module DiscourseHub
def self.accepts def self.accepts
[:json, 'application/vnd.discoursehub.v1'] [:json, 'application/vnd.discoursehub.v1']
end end
def self.nickname_operation
if SiteSetting.call_discourse_hub?
begin
yield
rescue DiscourseHub::NicknameUnavailable
false
rescue => e
Rails.logger.error e.message + "\n" + e.backtrace.join("\n")
end
end
end
end end

View File

@ -2,13 +2,14 @@ require 'digest/sha1'
require 'open-uri' require 'open-uri'
class S3Store class S3Store
@fog_loaded ||= require 'fog'
def store_upload(file, upload) def store_upload(file, upload)
# <id><sha1><extension> # <id><sha1><extension>
path = "#{upload.id}#{upload.sha1}#{upload.extension}" path = "#{upload.id}#{upload.sha1}#{upload.extension}"
# if this fails, it will throw an exception # if this fails, it will throw an exception
upload(file.tempfile, path, file.content_type) upload(file.tempfile, path, upload.original_filename, file.content_type)
# returns the url of the uploaded file # returns the url of the uploaded file
"#{absolute_base_url}/#{path}" "#{absolute_base_url}/#{path}"
@ -58,9 +59,7 @@ class S3Store
end end
def remove_file(url) def remove_file(url)
return unless has_been_uploaded?(url) remove File.basename(url) if has_been_uploaded?(url)
name = File.basename(url)
remove(name)
end end
def has_been_uploaded?(url) def has_been_uploaded?(url)
@ -102,19 +101,17 @@ class S3Store
raise Discourse::SiteSettingMissing.new("s3_secret_access_key") if SiteSetting.s3_secret_access_key.blank? raise Discourse::SiteSettingMissing.new("s3_secret_access_key") if SiteSetting.s3_secret_access_key.blank?
end end
def get_or_create_directory(name) def get_or_create_directory(bucket)
check_missing_site_settings check_missing_site_settings
@fog_loaded ||= require 'fog' fog = Fog::Storage.new s3_options
fog = Fog::Storage.new generate_options directory = fog.directories.get(bucket)
directory = fog.directories.create(key: bucket) unless directory
directory = fog.directories.get(name)
directory = fog.directories.create(key: name) unless directory
directory directory
end end
def generate_options def s3_options
options = { options = {
provider: 'AWS', provider: 'AWS',
aws_access_key_id: SiteSetting.s3_access_key_id, aws_access_key_id: SiteSetting.s3_access_key_id,
@ -124,22 +121,21 @@ class S3Store
options options
end end
def upload(file, name, content_type=nil) def upload(file, unique_filename, filename=nil, content_type=nil)
args = { args = {
key: name, key: unique_filename,
public: true, public: true,
body: file, body: file
} }
args[:content_disposition] = "attachment; filename=\"#{filename}\"" if filename
args[:content_type] = content_type if content_type args[:content_type] = content_type if content_type
directory.files.create(args)
get_or_create_directory(s3_bucket).files.create(args)
end end
def remove(name) def remove(unique_filename)
directory.files.destroy(key: name) fog = Fog::Storage.new s3_options
end fog.delete_object(s3_bucket, unique_filename)
def directory
get_or_create_directory(s3_bucket)
end end
end end

View File

@ -0,0 +1,33 @@
#see: https://github.com/rails/rails/pull/12065
if rails4?
module ActiveRecord
class Result
private
def hash_rows
@hash_rows ||=
begin
# We freeze the strings to prevent them getting duped when
# used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map { |c| c.dup.freeze }
@rows.map { |row|
# In the past we used Hash[columns.zip(row)]
# though elegant, the verbose way is much more efficient
# both time and memory wise cause it avoids a big array allocation
# this method is called a lot and needs to be micro optimised
hash = {}
index = 0
length = columns.length
while index < length
hash[columns[index]] = row[index]
index += 1
end
hash
}
end
end
end
end
end

View File

@ -0,0 +1,8 @@
if rails4?
# https://github.com/rails/arel/pull/206
class Arel::Table
def hash
@name.hash
end
end
end

View File

@ -9,6 +9,7 @@ class Guardian
def secure_category_ids; []; end def secure_category_ids; []; end
def topic_create_allowed_category_ids; []; end def topic_create_allowed_category_ids; []; end
def has_trust_level?(level); false; end def has_trust_level?(level); false; end
def email; nil; end
end end
def initialize(user=nil) def initialize(user=nil)
@ -36,6 +37,13 @@ class Guardian
@user.staff? @user.staff?
end end
def is_developer?
@user &&
is_admin? &&
Rails.configuration.respond_to?(:developer_emails) &&
Rails.configuration.developer_emails.include?(@user.email)
end
# Can the user see the object? # Can the user see the object?
def can_see?(obj) def can_see?(obj)
if obj if obj
@ -89,8 +97,8 @@ class Guardian
# You must be an admin to impersonate # You must be an admin to impersonate
is_admin? && is_admin? &&
# You may not impersonate other admins # You may not impersonate other admins unless you are a dev
not(target.admin?) (!target.admin? || is_developer?)
# Additionally, you may not impersonate yourself; # Additionally, you may not impersonate yourself;
# but the two tests for different admin statuses # but the two tests for different admin statuses
@ -229,11 +237,11 @@ class Guardian
end end
def can_create_topic?(parent) def can_create_topic?(parent)
can_create_post?(parent) user && user.trust_level >= SiteSetting.min_trust_to_create_topic.to_i && can_create_post?(parent)
end end
def can_create_topic_on_category?(category) def can_create_topic_on_category?(category)
can_create_post?(nil) && ( can_create_topic?(nil) && (
!category || !category ||
Category.topic_create_allowed(self).where(:id => category.id).count == 1 Category.topic_create_allowed(self).where(:id => category.id).count == 1
) )

View File

@ -1,6 +1,6 @@
module Jobs module Jobs
class DashboardStats < Jobs::Scheduled class DashboardStats < Jobs::Scheduled
recurrence { minutely(AdminDashboardData.recalculate_interval.minutes) } recurrence { hourly.minute_of_hour(0,30) }
def execute(args) def execute(args)
stats_json = AdminDashboardData.fetch_stats.as_json stats_json = AdminDashboardData.fetch_stats.as_json
@ -13,4 +13,4 @@ module Jobs
end end
end end
end end

View File

@ -5,15 +5,14 @@ module Jobs
recurrence { daily.hour_of_day(6) } recurrence { daily.hour_of_day(6) }
def execute(args) def execute(args)
target_users.each do |u| target_user_ids.each do |user_id|
Jobs.enqueue(:user_email, type: :digest, user_id: u.id) Jobs.enqueue(:user_email, type: :digest, user_id: user_id)
end end
end end
def target_users def target_user_ids
# Users who want to receive emails and haven't been emailed in the last day # Users who want to receive emails and haven't been emailed in the last day
query = User.select(:id) query = User.where(email_digests: true, active: true)
.where(email_digests: true, active: true)
.where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)") .where("COALESCE(last_emailed_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)")
.where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)") .where("COALESCE(last_seen_at, '2010-01-01') <= CURRENT_TIMESTAMP - ('1 DAY'::INTERVAL * digest_after_days)")
@ -22,7 +21,7 @@ module Jobs
query = query.where("approved OR moderator OR admin") query = query.where("approved OR moderator OR admin")
end end
query query.pluck(:id)
end end
end end

View File

@ -2,7 +2,7 @@
module Plugin; end module Plugin; end
class Plugin::Metadata class Plugin::Metadata
FIELDS = [:name, :about, :version, :authors] FIELDS ||= [:name, :about, :version, :authors]
attr_accessor *FIELDS attr_accessor *FIELDS
def self.parse(text) def self.parse(text)

View File

@ -104,8 +104,10 @@ module PrettyText
ctx.eval("var window = {}; window.devicePixelRatio = 2;") # hack to make code think stuff is retina ctx.eval("var window = {}; window.devicePixelRatio = 2;") # hack to make code think stuff is retina
ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }"); ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }");
decorate_context(ctx)
ctx_load(ctx, ctx_load(ctx,
"app/assets/javascripts/external/markdown.js", "app/assets/javascripts/external/better_markdown.js",
"app/assets/javascripts/discourse/dialects/dialect.js", "app/assets/javascripts/discourse/dialects/dialect.js",
"app/assets/javascripts/discourse/components/utilities.js", "app/assets/javascripts/discourse/components/utilities.js",
"app/assets/javascripts/discourse/components/markdown.js") "app/assets/javascripts/discourse/components/markdown.js")
@ -145,6 +147,13 @@ module PrettyText
@ctx @ctx
end end
def self.decorate_context(context)
context.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
context.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
context.eval("Discourse.BaseUrl = 'http://#{RailsMultisite::ConnectionManagement.current_hostname}';")
context.eval("Discourse.getURL = function(url) {return '#{Discourse::base_uri}' + url};")
end
def self.markdown(text, opts=nil) def self.markdown(text, opts=nil)
# we use the exact same markdown converter as the client # we use the exact same markdown converter as the client
# TODO: use the same extensions on both client and server (in particular the template for mentions) # TODO: use the same extensions on both client and server (in particular the template for mentions)
@ -154,9 +163,7 @@ module PrettyText
@mutex.synchronize do @mutex.synchronize do
context = v8 context = v8
# we need to do this to work in a multi site environment, many sites, many settings # we need to do this to work in a multi site environment, many sites, many settings
context.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};") decorate_context(context)
context.eval("Discourse.BaseUrl = 'http://#{RailsMultisite::ConnectionManagement.current_hostname}';")
context.eval("Discourse.getURL = function(url) {return '#{Discourse::base_uri}' + url};")
context['opts'] = opts || {} context['opts'] = opts || {}
context['raw'] = text context['raw'] = text
context.eval('opts["mentionLookup"] = function(u){return helpers.is_username_valid(u);}') context.eval('opts["mentionLookup"] = function(u){return helpers.is_username_valid(u);}')
@ -175,9 +182,7 @@ module PrettyText
@mutex.synchronize do @mutex.synchronize do
v8['avatarTemplate'] = avatar_template v8['avatarTemplate'] = avatar_template
v8['size'] = size v8['size'] = size
v8.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};") decorate_context(v8)
v8.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
v8.eval("Discourse.BaseUrl = '#{RailsMultisite::ConnectionManagement.current_hostname}';")
r = v8.eval("Discourse.Utilities.avatarImg({ avatarTemplate: avatarTemplate, size: size });") r = v8.eval("Discourse.Utilities.avatarImg({ avatarTemplate: avatarTemplate, size: size });")
end end
r r

View File

@ -231,30 +231,25 @@ module SiteSettingExtension
# trivial multi db support, we can optimize this later # trivial multi db support, we can optimize this later
current[name] = current_value current[name] = current_value
clean_name = name.to_s.sub("?", "")
setter = ("#{name}=").sub("?","") eval "define_singleton_method :#{clean_name} do
eval "define_singleton_method :#{name} do
c = @@containers[provider.current_site] c = @@containers[provider.current_site]
c = c[name] if c c = c[name] if c
c c
end end
define_singleton_method :#{setter} do |val| define_singleton_method :#{clean_name}? do
#{clean_name}
end
define_singleton_method :#{clean_name}= do |val|
add_override!(:#{name}, val) add_override!(:#{name}, val)
refresh! refresh!
end end
" "
end end
def method_missing(method, *args, &block)
as_question = method.to_s.gsub(/\?$/, '')
if respond_to?(as_question)
return send(as_question, *args, &block)
end
super(method, *args, &block)
end
def enum_class(name) def enum_class(name)
enums[name] enums[name]
end end

View File

@ -3,14 +3,15 @@ require_dependency 'topic_list'
class SuggestedTopicsBuilder class SuggestedTopicsBuilder
attr_reader :excluded_topic_ids attr_reader :excluded_topic_ids
attr_reader :results
def initialize(topic) def initialize(topic)
@excluded_topic_ids = [topic.id] @excluded_topic_ids = [topic.id]
@category_id = topic.category_id
@results = [] @results = []
end end
def add_results(results)
def add_results(results, priority=:low)
# WARNING .blank? will execute an Active Record query # WARNING .blank? will execute an Active Record query
return unless results return unless results
@ -23,16 +24,46 @@ class SuggestedTopicsBuilder
unless results.empty? unless results.empty?
# Keep track of the ids we've added # Keep track of the ids we've added
@excluded_topic_ids.concat results.map {|r| r.id} @excluded_topic_ids.concat results.map {|r| r.id}
splice_results(results,priority)
end
end
def splice_results(results, priority)
if @category_id &&
priority == :high &&
non_category_index = @results.index{|r| r.category_id != @category_id}
category_results, non_category_results = results.partition{|r| r.category_id == @category_id}
@results.insert non_category_index, *category_results
@results.concat non_category_results
else
@results.concat results @results.concat results
end end
end end
def results
@results.first(SiteSetting.suggested_topics)
end
def results_left def results_left
SiteSetting.suggested_topics - @results.size SiteSetting.suggested_topics - @results.size
end end
def full? def full?
results_left == 0 results_left <= 0
end
def category_results_left
SiteSetting.suggested_topics - @results.count{|r| r.category_id == @category_id}
end
def category_full?
if @category_id
else
full?
end
end end
def size def size

View File

@ -5,6 +5,8 @@ class TextSentinel
attr_accessor :text attr_accessor :text
ENTROPY_SCALE ||= 0.7
def initialize(text, opts=nil) def initialize(text, opts=nil)
@opts = opts || {} @opts = opts || {}
@text = text.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '') @text = text.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
@ -15,14 +17,20 @@ class TextSentinel
if opts[:private_message] if opts[:private_message]
scale_entropy = SiteSetting.min_private_message_post_length.to_f / SiteSetting.min_post_length.to_f scale_entropy = SiteSetting.min_private_message_post_length.to_f / SiteSetting.min_post_length.to_f
entropy = (entropy * scale_entropy).to_i entropy = (entropy * scale_entropy).to_i
entropy = (SiteSetting.min_private_message_post_length.to_f * ENTROPY_SCALE).to_i if entropy > SiteSetting.min_private_message_post_length
else
entropy = (SiteSetting.min_post_length.to_f * ENTROPY_SCALE).to_i if entropy > SiteSetting.min_post_length
end end
TextSentinel.new(text, min_entropy: entropy) TextSentinel.new(text, min_entropy: entropy)
end end
def self.title_sentinel(text) def self.title_sentinel(text)
TextSentinel.new(text, entropy = if SiteSetting.min_topic_title_length > SiteSetting.title_min_entropy
min_entropy: SiteSetting.title_min_entropy, SiteSetting.title_min_entropy
max_word_length: SiteSetting.max_word_length) else
(SiteSetting.min_topic_title_length.to_f * ENTROPY_SCALE).to_i
end
TextSentinel.new(text, min_entropy: entropy, max_word_length: SiteSetting.max_word_length)
end end
# Entropy is a number of how many unique characters the string needs. # Entropy is a number of how many unique characters the string needs.

View File

@ -87,10 +87,10 @@ class TopicQuery
# When logged in we start with different results # When logged in we start with different results
if @user if @user
builder.add_results(unread_results(topic: topic, per_page: builder.results_left)) builder.add_results(unread_results(topic: topic, per_page: builder.results_left), :high)
builder.add_results(new_results(per_page: builder.results_left)) unless builder.full? builder.add_results(new_results(topic: topic, per_page: builder.category_results_left), :high) unless builder.category_full?
end end
builder.add_results(random_suggested(topic, builder.results_left)) unless builder.full? builder.add_results(random_suggested(topic, builder.results_left), :low) unless builder.full?
create_list(:suggested, {}, builder.results) create_list(:suggested, {}, builder.results)
end end
@ -146,6 +146,11 @@ class TopicQuery
TopicList.new(:private_messages, user, list) TopicList.new(:private_messages, user, list)
end end
def list_private_messages_unread(user)
list = private_messages_for(user)
list = TopicQuery.unread_filter(list)
TopicList.new(:private_messages, user, list)
end
def list_uncategorized def list_uncategorized
create_list(:uncategorized, unordered: true) do |list| create_list(:uncategorized, unordered: true) do |list|

View File

@ -84,12 +84,13 @@ class TopicView
def summary def summary
return nil if desired_post.blank? return nil if desired_post.blank?
# TODO, this is actually quite slow, should be cached in the post table
Summarize.new(desired_post.cooked).summary Summarize.new(desired_post.cooked).summary
end end
def image_url def image_url
return nil if desired_post.blank? return nil if desired_post.blank?
desired_post.user.small_avatar_url desired_post.user.try(:small_avatar_url)
end end
def filter_posts(opts = {}) def filter_posts(opts = {})
@ -256,7 +257,7 @@ class TopicView
def setup_filtered_posts def setup_filtered_posts
@filtered_posts = @topic.posts @filtered_posts = @topic.posts
@filtered_posts = @filtered_posts.with_deleted.without_nuked_users if @user.try(:staff?) @filtered_posts = @filtered_posts.with_deleted if @user.try(:staff?)
@filtered_posts = @filtered_posts.best_of if @filter == 'best_of' @filtered_posts = @filtered_posts.best_of if @filter == 'best_of'
@filtered_posts = @filtered_posts.where('posts.post_type <> ?', Post.types[:moderator_action]) if @best.present? @filtered_posts = @filtered_posts.where('posts.post_type <> ?', Post.types[:moderator_action]) if @best.present?
return unless @username_filters.present? return unless @username_filters.present?

Some files were not shown because too many files have changed in this diff Show More