mirror of
https://github.com/codeninjasllc/discourse.git
synced 2025-03-23 13:26:22 -04:00
Merge branch 'master' into mobile
This commit is contained in:
commit
45d7765936
133 changed files with 2458 additions and 1264 deletions
GemfileGemfile.lockGemfile_rails4.lock
app
assets
javascripts
discourse
components
controllers
avatar_selector_controller.jsedit_topic_auto_close_controller.jsflag_controller.jslogin_controller.jsmerge_topic_controller.jssplit_topic_controller.jstopic_controller.jsuser_activity_controller.js
dialects
autolink_dialect.jsbbcode_dialect.jsbold_italics_dialect.jsdialect.jsgithub_code_dialect.jsmention_dialect.jsnewline_dialect.jsquote_dialect.js
helpers
mixins
models
routes
templates
views
external
stylesheets/common
controllers
models
category_featured_topic.rbinvite_redeemer.rbmin_trust_to_create_topic_setting.rboauth2_user_info.rbplugin_store_row.rbpost.rbscreened_email.rbscreened_url.rbsite_setting.rbstaff_action_log.rbtopic.rbtopic_link.rbtopic_link_click.rbuser.rbuser_action.rb
serializers
views
config
environments
initializers
jshint.ymllocales
client.de.ymlclient.en.ymlclient.ru.ymlclient.zh_CN.ymlserver.de.ymlserver.en.ymlserver.id.ymlserver.ko.ymlserver.ru.ymlserver.sv.yml
routes.rbdb/migrate
20130828192526_fix_optimized_images_urls.rb20130903154323_allow_null_user_id_on_posts.rb20130904181208_allow_null_user_id_on_topics.rb
docs
lib
auth.rb
auth
discourse_hub.rbfile_store
freedom_patches
guardian.rbjobs
plugin
pretty_text.rbsite_setting_extension.rbsuggested_topics_builder.rbtext_sentinel.rbtopic_query.rbtopic_view.rb
6
Gemfile
6
Gemfile
|
@ -187,7 +187,10 @@ gem 'lru_redux'
|
|||
# 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
|
||||
# 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
|
||||
# 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-cors', require: false
|
||||
gem 'unicorn', require: false
|
||||
gem 'puma', require: false
|
||||
|
||||
# perftools only works on 1.9 atm
|
||||
group :profile do
|
||||
|
|
19
Gemfile.lock
19
Gemfile.lock
|
@ -180,9 +180,14 @@ GEM
|
|||
fast_blank (0.0.1)
|
||||
rake
|
||||
rake-compiler
|
||||
fast_stack (0.0.5)
|
||||
rake
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
fastimage (1.3.0)
|
||||
ffi (1.8.1)
|
||||
flamegraph (0.0.2)
|
||||
fast_stack
|
||||
fog (1.14.0)
|
||||
builder
|
||||
excon (~> 0.25.0)
|
||||
|
@ -219,7 +224,7 @@ GEM
|
|||
librarian (0.1.0)
|
||||
highline
|
||||
thor (~> 0.15)
|
||||
libv8 (3.11.8.17)
|
||||
libv8 (3.16.14.3)
|
||||
listen (0.7.3)
|
||||
lru_redux (0.0.6)
|
||||
mail (2.4.4)
|
||||
|
@ -287,6 +292,8 @@ GEM
|
|||
pry (~> 0.9.10)
|
||||
pry-rails (0.2.2)
|
||||
pry (>= 0.9.10)
|
||||
puma (2.5.1)
|
||||
rack (>= 1.1, < 2.0)
|
||||
qunit-rails (0.0.3)
|
||||
railties (>= 3.2.3)
|
||||
rack (1.4.5)
|
||||
|
@ -294,7 +301,7 @@ GEM
|
|||
rack (>= 0.4)
|
||||
rack-cors (0.2.7)
|
||||
rack
|
||||
rack-mini-profiler (0.1.29)
|
||||
rack-mini-profiler (0.1.31)
|
||||
rack (>= 1.1.3)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
|
@ -423,8 +430,8 @@ GEM
|
|||
activemodel (~> 3.0)
|
||||
railties (~> 3.0)
|
||||
temple (0.6.4)
|
||||
therubyracer (0.11.4)
|
||||
libv8 (~> 3.11.8.12)
|
||||
therubyracer (0.12.0)
|
||||
libv8 (~> 3.16.14.0)
|
||||
ref
|
||||
thin (1.5.1)
|
||||
daemons (>= 1.0.9)
|
||||
|
@ -472,6 +479,7 @@ DEPENDENCIES
|
|||
fast_xor!
|
||||
fast_xs
|
||||
fastimage
|
||||
flamegraph
|
||||
fog
|
||||
handlebars-source (= 1.0.12)
|
||||
highline
|
||||
|
@ -501,10 +509,11 @@ DEPENDENCIES
|
|||
pg
|
||||
pry-nav
|
||||
pry-rails
|
||||
puma
|
||||
qunit-rails
|
||||
rack-cache
|
||||
rack-cors
|
||||
rack-mini-profiler (= 0.1.29)
|
||||
rack-mini-profiler
|
||||
rails (= 3.2.12)
|
||||
rails_multisite!
|
||||
rake
|
||||
|
|
|
@ -33,7 +33,7 @@ GIT
|
|||
|
||||
GIT
|
||||
remote: git://github.com/rails/rails.git
|
||||
revision: e36692a7466011ab51393ac8ca6dfffcb9d79ec0
|
||||
revision: 025b63db308fbbf942a3bc2673d4aadab968c524
|
||||
branch: 4-0-stable
|
||||
specs:
|
||||
actionmailer (4.0.0)
|
||||
|
@ -216,9 +216,14 @@ GEM
|
|||
fast_blank (0.0.1)
|
||||
rake
|
||||
rake-compiler
|
||||
fast_stack (0.0.5)
|
||||
rake
|
||||
rake-compiler
|
||||
fast_xs (0.8.0)
|
||||
fastimage (1.5.0)
|
||||
ffi (1.9.0)
|
||||
flamegraph (0.0.2)
|
||||
fast_stack
|
||||
fog (1.14.0)
|
||||
builder
|
||||
excon (~> 0.25.0)
|
||||
|
@ -256,7 +261,7 @@ GEM
|
|||
librarian (0.1.0)
|
||||
highline
|
||||
thor (~> 0.15)
|
||||
libv8 (3.11.8.17)
|
||||
libv8 (3.16.14.3)
|
||||
listen (1.2.2)
|
||||
rb-fsevent (>= 0.9.3)
|
||||
rb-inotify (>= 0.9)
|
||||
|
@ -267,7 +272,7 @@ GEM
|
|||
treetop (~> 1.4.8)
|
||||
metaclass (0.0.1)
|
||||
method_source (0.8.1)
|
||||
mime-types (1.24)
|
||||
mime-types (1.25)
|
||||
mini_portile (0.5.1)
|
||||
minitest (4.7.5)
|
||||
mocha (0.14.0)
|
||||
|
@ -327,6 +332,8 @@ GEM
|
|||
pry (~> 0.9.10)
|
||||
pry-rails (0.3.1)
|
||||
pry (>= 0.9.10)
|
||||
puma (2.5.1)
|
||||
rack (>= 1.1, < 2.0)
|
||||
qunit-rails (0.0.3)
|
||||
railties (>= 3.2.3)
|
||||
rack (1.5.2)
|
||||
|
@ -334,7 +341,7 @@ GEM
|
|||
rack (>= 0.4)
|
||||
rack-cors (0.2.8)
|
||||
rack
|
||||
rack-mini-profiler (0.1.29)
|
||||
rack-mini-profiler (0.1.31)
|
||||
rack (>= 1.1.3)
|
||||
rack-openid (1.3.1)
|
||||
rack (>= 1.1.0)
|
||||
|
@ -432,8 +439,8 @@ GEM
|
|||
activesupport (>= 3.0)
|
||||
sprockets (~> 2.8)
|
||||
temple (0.6.5)
|
||||
therubyracer (0.11.4)
|
||||
libv8 (~> 3.11.8.12)
|
||||
therubyracer (0.12.0)
|
||||
libv8 (~> 3.16.14.0)
|
||||
ref
|
||||
thin (1.5.1)
|
||||
daemons (>= 1.0.9)
|
||||
|
@ -482,6 +489,7 @@ DEPENDENCIES
|
|||
fast_xor!
|
||||
fast_xs
|
||||
fastimage
|
||||
flamegraph
|
||||
fog
|
||||
handlebars-source (= 1.0.12)
|
||||
highline
|
||||
|
@ -511,10 +519,11 @@ DEPENDENCIES
|
|||
pg
|
||||
pry-nav
|
||||
pry-rails
|
||||
puma
|
||||
qunit-rails
|
||||
rack-cache
|
||||
rack-cors
|
||||
rack-mini-profiler (= 0.1.29)
|
||||
rack-mini-profiler
|
||||
rails!
|
||||
rails-observers
|
||||
rails_multisite!
|
||||
|
|
|
@ -3,6 +3,49 @@
|
|||
|
||||
@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) {
|
||||
|
||||
var autocompletePlugin = this;
|
||||
|
@ -338,11 +381,15 @@ $.fn.autocomplete = function(options) {
|
|||
}
|
||||
term = me.val().substring(completeStart + (options.key ? 1 : 0), caretPosition);
|
||||
if (e.which >= 48 && e.which <= 90) {
|
||||
term += String.fromCharCode(e.which);
|
||||
term += mapKeyPressToActualCharacter(e.shiftKey, e.which);
|
||||
} else if (e.which === 187) {
|
||||
term += "+";
|
||||
} else if (e.which === 189) {
|
||||
term += (e.shiftKey) ? "_" : "-";
|
||||
} else if (e.which === 220) {
|
||||
term += (e.shiftKey) ? "|" : "]";
|
||||
} else if (e.which === 222) {
|
||||
term += (e.shiftKey) ? "\"" : "'";
|
||||
} else {
|
||||
if (e.which !== 8) {
|
||||
term += ",";
|
||||
|
|
|
@ -13,13 +13,29 @@ Discourse.Formatter = (function(){
|
|||
|
||||
var firstPart = string.substr(0, maxLength);
|
||||
|
||||
var betterSplit = firstPart.substr(1).search(/[^a-z]/);
|
||||
if (betterSplit >= 0) {
|
||||
var offset = 1;
|
||||
if(string[betterSplit+1] === "_") {
|
||||
offset = 2;
|
||||
// work backward to split stuff like ABPoop to AB Poop
|
||||
var i;
|
||||
for(i=firstPart.length-1;i>0;i--){
|
||||
if(firstPart[i].match(/[A-Z]/)){
|
||||
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 {
|
||||
return firstPart + " " + string.substr(maxLength);
|
||||
}
|
||||
|
|
|
@ -23,6 +23,10 @@ Discourse.Quote = {
|
|||
sansQuotes = contents.replace(this.REGEXP, '').trim();
|
||||
if (sansQuotes.length === 0) return "";
|
||||
|
||||
// Escape the content of the quote
|
||||
sansQuotes = sansQuotes.replace(/</g, "<")
|
||||
.replace(/>/g, ">");
|
||||
|
||||
result = "[quote=\"" + post.get('username') + ", post:" + post.get('post_number') + ", topic:" + post.get('topic_id');
|
||||
|
||||
/* Strip the HTML from cooked */
|
||||
|
|
|
@ -8,7 +8,15 @@
|
|||
@module Discourse
|
||||
**/
|
||||
Discourse.AvatarSelectorController = Discourse.Controller.extend(Discourse.ModalFunctionality, {
|
||||
toggleUseUploadedAvatar: function(toggle) {
|
||||
this.set("use_uploaded_avatar", toggle);
|
||||
}
|
||||
useUploadedAvatar: function() {
|
||||
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")
|
||||
});
|
||||
|
|
|
@ -13,7 +13,7 @@ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Disco
|
|||
if( this.get('details.auto_close_at') ) {
|
||||
var closeTime = new Date( this.get('details.auto_close_at') );
|
||||
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 {
|
||||
this.set('details.auto_close_days', '');
|
||||
|
|
|
@ -58,9 +58,11 @@ Discourse.FlagController = Discourse.ObjectController.extend(Discourse.ModalFunc
|
|||
|
||||
if (opts) params = $.extend(params, opts);
|
||||
|
||||
$('#discourse-modal').modal('hide');
|
||||
postAction.act(params).then(function() {
|
||||
flagController.send('closeModal');
|
||||
}, function(errors) {
|
||||
$('#discourse-modal').modal('show');
|
||||
flagController.displayErrors(errors);
|
||||
});
|
||||
},
|
||||
|
|
|
@ -95,6 +95,11 @@ Discourse.LoginController = Discourse.Controller.extend(Discourse.ModalFunctiona
|
|||
},
|
||||
|
||||
authenticationComplete: function(options) {
|
||||
if (options.requires_invite) {
|
||||
this.flash(I18n.t('login.requires_invite'), 'success');
|
||||
this.set('authenticate', null);
|
||||
return;
|
||||
}
|
||||
if (options.awaiting_approval) {
|
||||
this.flash(I18n.t('login.awaiting_approval'), 'success');
|
||||
this.set('authenticate', null);
|
||||
|
|
|
@ -12,6 +12,7 @@ Discourse.MergeTopicController = Discourse.ObjectController.extend(Discourse.Sel
|
|||
|
||||
topicController: Em.computed.alias('controllers.topic'),
|
||||
selectedPosts: Em.computed.alias('topicController.selectedPosts'),
|
||||
selectedReplies: Em.computed.alias('topicController.selectedReplies'),
|
||||
allPostsSelected: Em.computed.alias('topicController.allPostsSelected'),
|
||||
|
||||
buttonDisabled: function() {
|
||||
|
@ -31,10 +32,13 @@ Discourse.MergeTopicController = Discourse.ObjectController.extend(Discourse.Sel
|
|||
if (this.get('allPostsSelected')) {
|
||||
promise = Discourse.Topic.mergeTopic(this.get('id'), this.get('selectedTopicId'));
|
||||
} 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'), {
|
||||
destination_topic_id: this.get('selectedTopicId'),
|
||||
post_ids: postIds
|
||||
post_ids: postIds,
|
||||
reply_post_ids: replyPostIds
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ Discourse.SplitTopicController = Discourse.ObjectController.extend(Discourse.Sel
|
|||
|
||||
topicController: Em.computed.alias('controllers.topic'),
|
||||
selectedPosts: Em.computed.alias('topicController.selectedPosts'),
|
||||
selectedReplies: Em.computed.alias('topicController.selectedReplies'),
|
||||
|
||||
buttonDisabled: function() {
|
||||
if (this.get('saving')) return true;
|
||||
|
@ -30,21 +31,23 @@ Discourse.SplitTopicController = Discourse.ObjectController.extend(Discourse.Sel
|
|||
movePostsToNewTopic: function() {
|
||||
this.set('saving', true);
|
||||
|
||||
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); });
|
||||
var splitTopicController = this;
|
||||
var postIds = this.get('selectedPosts').map(function(p) { return p.get('id'); }),
|
||||
replyPostIds = this.get('selectedReplies').map(function(p) { return p.get('id'); }),
|
||||
self = this;
|
||||
|
||||
Discourse.Topic.movePosts(this.get('id'), {
|
||||
title: this.get('topicName'),
|
||||
post_ids: postIds
|
||||
post_ids: postIds,
|
||||
reply_post_ids: replyPostIds
|
||||
}).then(function(result) {
|
||||
// Posts moved
|
||||
splitTopicController.send('closeModal');
|
||||
splitTopicController.get('topicController').toggleMultiSelect();
|
||||
self.send('closeModal');
|
||||
self.get('topicController').toggleMultiSelect();
|
||||
Em.run.next(function() { Discourse.URL.routeTo(result.url); });
|
||||
}, function() {
|
||||
// Error moving posts
|
||||
splitTopicController.flash(I18n.t('topic.split_topic.error'));
|
||||
splitTopicController.set('saving', false);
|
||||
self.flash(I18n.t('topic.split_topic.error'));
|
||||
self.set('saving', false);
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -11,8 +11,15 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
summaryCollapsed: true,
|
||||
needs: ['header', 'modal', 'composer', 'quoteButton'],
|
||||
allPostsSelected: false,
|
||||
selectedPosts: new Em.Set(),
|
||||
editingTopic: false,
|
||||
selectedPosts: null,
|
||||
selectedReplies: null,
|
||||
|
||||
init: function() {
|
||||
this._super();
|
||||
this.set('selectedPosts', new Em.Set());
|
||||
this.set('selectedReplies', new Em.Set());
|
||||
},
|
||||
|
||||
jumpTopDisabled: function() {
|
||||
return (this.get('progressPosition') === 1);
|
||||
|
@ -82,18 +89,48 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
return false;
|
||||
}.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');
|
||||
if (selectedPosts.contains(post)) {
|
||||
selectedPosts.removeObject(post);
|
||||
this.set('allPostsSelected', false);
|
||||
if (this.postSelected(post)) {
|
||||
this.deselectPost(post);
|
||||
return false;
|
||||
} else {
|
||||
selectedPosts.addObject(post);
|
||||
|
||||
// If the user manually selects all posts, all posts are selected
|
||||
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() {
|
||||
this.get('selectedPosts').clear();
|
||||
this.get('selectedReplies').clear();
|
||||
this.set('allPostsSelected', false);
|
||||
},
|
||||
|
||||
|
@ -177,19 +215,28 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
|
|||
},
|
||||
|
||||
deleteSelected: function() {
|
||||
var topicController = this;
|
||||
var self = this;
|
||||
bootbox.confirm(I18n.t("post.delete.confirm", { count: this.get('selectedPostsCount')}), function(result) {
|
||||
if (result) {
|
||||
|
||||
// If all posts are selected, it's the same thing as deleting the topic
|
||||
if (topicController.get('allPostsSelected')) {
|
||||
return topicController.deleteTopic();
|
||||
if (self.get('allPostsSelected')) {
|
||||
return self.deleteTopic();
|
||||
}
|
||||
|
||||
var selectedPosts = topicController.get('selectedPosts');
|
||||
Discourse.Post.deleteMany(selectedPosts);
|
||||
topicController.get('model.postStream').removePosts(selectedPosts);
|
||||
topicController.toggleMultiSelect();
|
||||
var selectedPosts = self.get('selectedPosts'),
|
||||
selectedReplies = self.get('selectedReplies'),
|
||||
postStream = self.get('postStream'),
|
||||
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) {
|
||||
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) {
|
||||
|
|
|
@ -24,5 +24,6 @@ Discourse.UserActivityController = Discourse.ObjectController.extend({
|
|||
},
|
||||
|
||||
privateMessagesActive: Em.computed.equal('pmView', 'index'),
|
||||
privateMessagesSentActive: Em.computed.equal('pmView', 'sent')
|
||||
privateMessagesMineActive: Em.computed.equal('pmView', 'mine'),
|
||||
privateMessagesUnreadActive: Em.computed.equal('pmView', 'unread')
|
||||
});
|
||||
|
|
|
@ -1,45 +1,19 @@
|
|||
/**
|
||||
This addition handles auto linking of text. When included, it will parse out links and create
|
||||
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,
|
||||
MD = event.MD;
|
||||
emitter: function(matches) {
|
||||
var url = matches[1],
|
||||
displayUrl = url;
|
||||
|
||||
/**
|
||||
Parses out links from HTML.
|
||||
if (url.match(/^www/)) { url = "http://" + url; }
|
||||
return ['a', {href: url}, displayUrl];
|
||||
}
|
||||
};
|
||||
|
||||
@method autoLink
|
||||
@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['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]];
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
Discourse.Dialect.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs));
|
||||
Discourse.Dialect.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs));
|
||||
|
|
|
@ -1,205 +1,114 @@
|
|||
/**
|
||||
Regsiter all functionality for supporting BBCode in Discourse.
|
||||
Create a simple BBCode tag handler
|
||||
|
||||
@event register
|
||||
@namespace Discourse.Dialect
|
||||
@method replaceBBCode
|
||||
@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) {
|
||||
|
||||
var dialect = event.dialect,
|
||||
MD = event.MD;
|
||||
|
||||
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]));
|
||||
});
|
||||
function replaceBBCode(tag, emitter) {
|
||||
Discourse.Dialect.inlineBetween({
|
||||
start: "[" + tag + "]",
|
||||
stop: "[/" + tag + "]",
|
||||
emitter: emitter
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
return ['a', {href: "mailto:" + m[2], 'data-bbcode': true}, m[2]];
|
||||
});
|
||||
@method replaceBBCodeParamsRaw
|
||||
@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) {
|
||||
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]))];
|
||||
if (m) { return emitter.call(this, m[1], 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
|
||||
@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.inline["[code]"] = function bbcodeCode(text, orig_match) {
|
||||
var bbcodePattern = new RegExp("\\[code\\]([\\s\\S]*?)\\[\\/code\\]", "igm"),
|
||||
m = bbcodePattern.exec(text);
|
||||
@method replaceBBCodeParams
|
||||
@param {tag} tag the tag we want to match
|
||||
@param {function} emitter the function that creates JsonML for the tag
|
||||
**/
|
||||
function replaceBBCodeParams(tag, emitter) {
|
||||
replaceBBCodeParamsRaw(tag, function (param, contents) {
|
||||
return emitter(param, this.processInline(contents));
|
||||
});
|
||||
}
|
||||
|
||||
if (m) {
|
||||
var contents = m[1].trim().split("\n");
|
||||
replaceBBCode('b', function(contents) { return ['span', {'class': 'bbcode-b'}].concat(contents); });
|
||||
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"];
|
||||
contents.forEach(function (n) {
|
||||
html.push(n.trim());
|
||||
html.push(["br"]);
|
||||
html.push("\n");
|
||||
});
|
||||
replaceBBCode('ul', function(contents) { return ['ul'].concat(contents); });
|
||||
replaceBBCode('ol', function(contents) { return ['ol'].concat(contents); });
|
||||
replaceBBCode('li', function(contents) { return ['li'].concat(contents); });
|
||||
|
||||
return [m[0].length, html];
|
||||
}
|
||||
};
|
||||
replaceBBCode('spoiler', function(contents) { return ['span', {'class': 'spoiler'}].concat(contents); });
|
||||
|
||||
/**
|
||||
Support BBCode [quote] blocks
|
||||
Discourse.Dialect.inlineBetween({
|
||||
start: '[img]',
|
||||
stop: '[/img]',
|
||||
rawContents: true,
|
||||
emitter: function(contents) { return ['img', {href: contents}]; }
|
||||
});
|
||||
|
||||
@method bbcodeQuote
|
||||
@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['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: '[email]',
|
||||
stop: '[/email]',
|
||||
rawContents: true,
|
||||
emitter: function(contents) { return ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]; }
|
||||
});
|
||||
|
||||
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,
|
||||
path = event.path;
|
||||
replaceBBCodeParamsRaw("email", function(param, contents) {
|
||||
return ['a', {href: "mailto:" + param, 'data-bbcode': true}, contents];
|
||||
});
|
||||
|
||||
// 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);
|
||||
replaceBBCodeParams("size", function(param, contents) {
|
||||
return ['span', {'class': "bbcode-size-" + param}].concat(contents);
|
||||
});
|
||||
|
||||
if (trailing.length) {
|
||||
parent.splice(location, 0, ['br']);
|
||||
}
|
||||
replaceBBCodeParams("color", function(param, contents) {
|
||||
// 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)];
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,32 +1,42 @@
|
|||
/**
|
||||
Markdown.js doesn't seem to do bold and italics at the same time if you surround code with
|
||||
three asterisks. This adds that support.
|
||||
|
||||
@event register
|
||||
@namespace Discourse.Dialect
|
||||
markdown-js doesn't ensure that em/strong codes are present on word boundaries.
|
||||
So we create our own handlers here.
|
||||
**/
|
||||
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,
|
||||
MD = event.MD;
|
||||
|
||||
/**
|
||||
Handles simultaneous bold and italics
|
||||
|
||||
@method parseMentions
|
||||
@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 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]))]];
|
||||
dialect.block["fix_simple_quotes"] = function(block, next) {
|
||||
var m = /^ +(\>[\s\S]*)/.exec(block);
|
||||
if (m && m[1] && m[1].length) {
|
||||
next.unshift(MD.mk_block(m[1]));
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
});
|
|
@ -3,91 +3,70 @@
|
|||
Discourse uses the Markdown.js as its main parser. `Discourse.Dialect` is the framework
|
||||
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,
|
||||
MD = parser.Markdown,
|
||||
|
||||
// Our dialect
|
||||
dialect = MD.dialects.Discourse = MD.subclassDialect( MD.dialects.Gruber ),
|
||||
initialized = false;
|
||||
|
||||
initialized = false,
|
||||
/**
|
||||
Initialize our dialects for processing.
|
||||
|
||||
/**
|
||||
Initialize our dialects for processing.
|
||||
@method initializeDialects
|
||||
**/
|
||||
function initializeDialects() {
|
||||
Discourse.Dialect.trigger('register', {dialect: dialect, MD: MD});
|
||||
MD.buildBlockOrder(dialect.block);
|
||||
MD.buildInlinePatterns(dialect.inline);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
@method initializeDialects
|
||||
**/
|
||||
initializeDialects = function() {
|
||||
Discourse.Dialect.trigger('register', {dialect: dialect, MD: MD});
|
||||
MD.buildBlockOrder(dialect.block);
|
||||
MD.buildInlinePatterns(dialect.inline);
|
||||
initialized = true;
|
||||
},
|
||||
/**
|
||||
Parse a JSON ML tree, using registered handlers to adjust it if necessary.
|
||||
|
||||
/**
|
||||
Parse a JSON ML tree, using registered handlers to adjust it if necessary.
|
||||
@method parseTree
|
||||
@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
|
||||
@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
|
||||
**/
|
||||
parseTree = function parseTree(tree, path, insideCounts) {
|
||||
if (tree instanceof Array) {
|
||||
Discourse.Dialect.trigger('parseNode', {node: tree, path: path, dialect: dialect, insideCounts: insideCounts || {}});
|
||||
path = path || [];
|
||||
insideCounts = insideCounts || {};
|
||||
|
||||
path = path || [];
|
||||
insideCounts = insideCounts || {};
|
||||
path.push(tree);
|
||||
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) {
|
||||
var tagName = n[0];
|
||||
insideCounts[tagName] = (insideCounts[tagName] || 0) + 1;
|
||||
parseTree(n, path, insideCounts);
|
||||
insideCounts[tagName] = insideCounts[tagName] - 1;
|
||||
});
|
||||
path.pop();
|
||||
}
|
||||
return tree;
|
||||
};
|
||||
/**
|
||||
Returns true if there's an invalid word boundary for a match.
|
||||
|
||||
@method invalidBoundary
|
||||
@param {Object} args our arguments, including whether we care about boundaries
|
||||
@param {Array} prev the previous content, if exists
|
||||
@returns {Boolean} whether there is an invalid word boundary
|
||||
**/
|
||||
function invalidBoundary(args, prev) {
|
||||
|
||||
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.
|
||||
|
@ -110,7 +89,281 @@ Discourse.Dialect = {
|
|||
dialect.options = opts;
|
||||
var tree = parser.toHTMLTree(text, 'Discourse');
|
||||
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);
|
||||
|
||||
|
||||
|
|
|
@ -5,129 +5,16 @@
|
|||
@event register
|
||||
@namespace Discourse.Dialect
|
||||
**/
|
||||
Discourse.Dialect.on("register", function(event) {
|
||||
var dialect = event.dialect,
|
||||
MD = event.MD;
|
||||
|
||||
/**
|
||||
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]);
|
||||
Discourse.Dialect.replaceBlock({
|
||||
start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm,
|
||||
stop: '```',
|
||||
emitter: function(blockContents, matches) {
|
||||
return ['p', ['pre', ['code', {'class': matches[1] || 'lang-auto'}, blockContents.join("\n") ]]];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Discourse.Dialect.on("parseNode", function(event) {
|
||||
|
||||
var node = event.node,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
Discourse.Dialect.postProcessTag('code', function (contents) {
|
||||
return Handlebars.Utils.escapeExpression(contents);
|
||||
});
|
||||
|
|
|
@ -2,47 +2,20 @@
|
|||
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
|
||||
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,
|
||||
MD = event.MD;
|
||||
emitter: function(matches) {
|
||||
var username = matches[1],
|
||||
mentionLookup = this.dialect.options.mentionLookup || Discourse.Mention.lookupCache;
|
||||
|
||||
/**
|
||||
Parses out @username mentions.
|
||||
|
||||
@method parseMentions
|
||||
@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; }
|
||||
if (mentionLookup(username.substr(1))) {
|
||||
return ['a', {'class': 'mention', href: Discourse.getURL("/users/") + username.substr(1).toLowerCase()}, username];
|
||||
} else {
|
||||
return ['span', {'class': 'mention'}, username];
|
||||
}
|
||||
|
||||
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]];
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
});
|
||||
}
|
||||
});
|
|
@ -1,42 +1,32 @@
|
|||
/**
|
||||
Support for the newline behavior in markdown that most expect.
|
||||
|
||||
@event parseNode
|
||||
@namespace Discourse.Dialect
|
||||
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.
|
||||
**/
|
||||
Discourse.Dialect.on("parseNode", function(event) {
|
||||
var node = event.node,
|
||||
opts = event.dialect.options,
|
||||
Discourse.Dialect.postProcessText(function (text, event) {
|
||||
var 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 (linebreaks || (insideCounts.pre > 0)) { return; }
|
||||
|
||||
if (node.length > 1) {
|
||||
for (var j=1; j<node.length; j++) {
|
||||
var textContent = node[j];
|
||||
if (text === "\n") {
|
||||
// If the tage is just a new line, replace it with a `<br>`
|
||||
return [['br']];
|
||||
} else {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
// If the text node contains new lines, perhaps with text between them, insert the
|
||||
// `<br>` tags.
|
||||
var split = text.split(/\n+/);
|
||||
if (split.length) {
|
||||
var replacement = [];
|
||||
for (var i=0; i<split.length; i++) {
|
||||
if (split[i].length > 0) {
|
||||
replacement.push(split[i]);
|
||||
if (i !== split.length-1) { replacement.push(['br']); }
|
||||
}
|
||||
}
|
||||
return replacement;
|
||||
}
|
||||
}
|
||||
|
||||
});
|
62
app/assets/javascripts/discourse/dialects/quote_dialect.js
Normal file
62
app/assets/javascripts/discourse/dialects/quote_dialect.js
Normal 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']);
|
||||
}
|
||||
}
|
||||
|
||||
});
|
|
@ -78,8 +78,6 @@ DiscourseGroupedEach.prototype = {
|
|||
template = this.template;
|
||||
|
||||
data.insideEach = true;
|
||||
data.insideGroup = true;
|
||||
|
||||
for (var i = 0; i < contentLength; i++) {
|
||||
template(content.objectAt(i), { data: data });
|
||||
}
|
||||
|
@ -124,5 +122,6 @@ Ember.Handlebars.registerHelper('groupedEach', function(path, options) {
|
|||
}
|
||||
|
||||
options.hash.dataSourceBinding = path;
|
||||
options.data.insideGroup = true;
|
||||
new DiscourseGroupedEach(this, path, options).render();
|
||||
});
|
|
@ -11,10 +11,15 @@ Discourse.SelectedPostsCount = Em.Mixin.create({
|
|||
selectedPostsCount: function() {
|
||||
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');
|
||||
}.property('selectedPosts.length', 'allPostsSelected')
|
||||
return sum;
|
||||
}.property('selectedPosts.length', 'allPostsSelected', 'selectedReplies.length')
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -442,6 +442,7 @@ Discourse.Composer = Discourse.Model.extend({
|
|||
postStream = this.get('topic.postStream'),
|
||||
addedToStream = false;
|
||||
|
||||
|
||||
// Build the post object
|
||||
var createdPost = Discourse.Post.create({
|
||||
raw: this.get('reply'),
|
||||
|
@ -482,6 +483,8 @@ Discourse.Composer = Discourse.Model.extend({
|
|||
|
||||
var composer = this;
|
||||
return Ember.Deferred.promise(function(promise) {
|
||||
|
||||
composer.set('composeState', SAVING);
|
||||
createdPost.save(function(result) {
|
||||
var addedPost = false,
|
||||
saving = true;
|
||||
|
@ -515,8 +518,16 @@ Discourse.Composer = Discourse.Model.extend({
|
|||
if (postStream) {
|
||||
postStream.undoPost(createdPost);
|
||||
}
|
||||
promise.reject($.parseJSON(error.responseText).errors[0]);
|
||||
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);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -30,6 +30,7 @@ Discourse.Post = Discourse.Model.extend({
|
|||
deletedViaTopic: Em.computed.and('firstPost', 'topic.deleted_at'),
|
||||
deleted: Em.computed.or('deleted_at', 'deletedViaTopic'),
|
||||
notDeleted: Em.computed.not('deleted'),
|
||||
userDeleted: Em.computed.empty('user_id'),
|
||||
|
||||
postDeletedBy: function() {
|
||||
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
|
||||
@param {Discourse.User} deleted_by The user deleting the post
|
||||
@method setDeletedState
|
||||
@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.
|
||||
if (deleted_by.get('staff')) {
|
||||
if (deletedBy.get('staff')) {
|
||||
this.setProperties({
|
||||
deleted_at: new Date(),
|
||||
deleted_by: deleted_by,
|
||||
deleted_by: deletedBy,
|
||||
can_delete: false
|
||||
});
|
||||
} else {
|
||||
|
@ -247,7 +249,16 @@ Discourse.Post = Discourse.Model.extend({
|
|||
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' });
|
||||
},
|
||||
|
||||
|
@ -327,8 +338,7 @@ Discourse.Post = Discourse.Model.extend({
|
|||
|
||||
// Whether to show replies directly below
|
||||
showRepliesBelow: function() {
|
||||
var reply_count, topic;
|
||||
reply_count = this.get('reply_count');
|
||||
var reply_count = this.get('reply_count');
|
||||
|
||||
// We don't show replies if there aren't any
|
||||
if (reply_count === 0) return false;
|
||||
|
@ -340,7 +350,7 @@ Discourse.Post = Discourse.Model.extend({
|
|||
if (reply_count > 1) return true;
|
||||
|
||||
// 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);
|
||||
|
||||
}.property('reply_count'),
|
||||
|
@ -376,11 +386,12 @@ Discourse.Post.reopenClass({
|
|||
return result;
|
||||
},
|
||||
|
||||
deleteMany: function(posts) {
|
||||
deleteMany: function(selectedPosts, selectedReplies) {
|
||||
return Discourse.ajax("/posts/destroy_many", {
|
||||
type: 'DELETE',
|
||||
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'); })
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -28,7 +28,11 @@ Discourse.User = Discourse.Model.extend({
|
|||
|
||||
|
||||
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'),
|
||||
|
||||
/**
|
||||
|
@ -101,7 +105,7 @@ Discourse.User = Discourse.Model.extend({
|
|||
@returns Result of ajax call
|
||||
**/
|
||||
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',
|
||||
data: { new_username: newUsername }
|
||||
});
|
||||
|
@ -115,7 +119,7 @@ Discourse.User = Discourse.Model.extend({
|
|||
@returns Result of ajax call
|
||||
**/
|
||||
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',
|
||||
data: { email: email }
|
||||
});
|
||||
|
@ -173,9 +177,7 @@ Discourse.User = Discourse.Model.extend({
|
|||
changePassword: function() {
|
||||
return Discourse.ajax("/session/forgot_password", {
|
||||
dataType: 'json',
|
||||
data: {
|
||||
login: this.get('username')
|
||||
},
|
||||
data: { login: this.get('username') },
|
||||
type: 'POST'
|
||||
});
|
||||
},
|
||||
|
@ -266,11 +268,14 @@ Discourse.User = Discourse.Model.extend({
|
|||
Change avatar selection
|
||||
|
||||
@method toggleAvatarSelection
|
||||
@param {Boolean} useUploadedAvatar true if the user is using the uploaded avatar
|
||||
@returns {Promise} the result of the toggle avatar selection
|
||||
*/
|
||||
toggleAvatarSelection: function() {
|
||||
var data = { use_uploaded_avatar: this.get("use_uploaded_avatar") };
|
||||
return Discourse.ajax("/users/" + this.get("username") + "/preferences/avatar/toggle", { type: 'PUT', data: data });
|
||||
toggleAvatarSelection: function(useUploadedAvatar) {
|
||||
return Discourse.ajax("/users/" + this.get("username_lower") + "/preferences/avatar/toggle", {
|
||||
type: 'PUT',
|
||||
data: { use_uploaded_avatar: useUploadedAvatar }
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
|
|
|
@ -10,7 +10,7 @@ Discourse.Route.buildRoutes(function() {
|
|||
// Topic routes
|
||||
this.resource('topic', { path: '/t/:slug/:id' }, function() {
|
||||
this.route('fromParams', { path: '/' });
|
||||
this.route('fromParams', { path: '/:nearPost' });
|
||||
this.route('fromParamsNear', { path: '/:nearPost' });
|
||||
});
|
||||
|
||||
// Generate static page routes
|
||||
|
@ -50,7 +50,8 @@ Discourse.Route.buildRoutes(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() {
|
||||
|
|
|
@ -18,35 +18,29 @@ Discourse.PreferencesRoute = Discourse.RestrictedUserRoute.extend({
|
|||
events: {
|
||||
showAvatarSelector: function() {
|
||||
Discourse.Route.showModal(this, 'avatarSelector');
|
||||
var user = this.modelFor("user");
|
||||
console.log(user);
|
||||
this.controllerFor("avatarSelector").setProperties(user.getProperties(
|
||||
"username",
|
||||
"email",
|
||||
"has_uploaded_avatar",
|
||||
"use_uploaded_avatar",
|
||||
"gravatar_template",
|
||||
"uploaded_avatar_template"
|
||||
));
|
||||
// all the properties needed for displaying the avatar selector modal
|
||||
var avatarSelector = this.modelFor('user').getProperties(
|
||||
'username', 'email',
|
||||
'has_uploaded_avatar', 'use_uploaded_avatar',
|
||||
'gravatar_template', 'uploaded_avatar_template');
|
||||
this.controllerFor('avatarSelector').setProperties(avatarSelector);
|
||||
},
|
||||
|
||||
saveAvatarSelection: function() {
|
||||
var user = this.modelFor("user");
|
||||
var avatar = this.controllerFor("avatarSelector");
|
||||
var user = this.modelFor('user');
|
||||
var avatarSelector = this.controllerFor('avatarSelector');
|
||||
// sends the information to the server if it has changed
|
||||
if (avatar.get("use_uploaded_avatar") !== user.get("use_uploaded_avatar")) { user.toggleAvatarSelection(); }
|
||||
// saves the data back
|
||||
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"));
|
||||
if (avatarSelector.get('use_uploaded_avatar') !== user.get('use_uploaded_avatar')) {
|
||||
user.toggleAvatarSelection(avatarSelector.get('use_uploaded_avatar'));
|
||||
}
|
||||
// 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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -58,4 +58,5 @@ Discourse.TopicFromParamsRoute = Discourse.Route.extend({
|
|||
|
||||
});
|
||||
|
||||
Discourse.TopicFromParamsNearRoute = Discourse.TopicFromParamsRoute;
|
||||
|
||||
|
|
|
@ -171,33 +171,26 @@ Discourse.UserTopicListRoute = Discourse.Route.extend({
|
|||
}
|
||||
});
|
||||
|
||||
Discourse.UserPrivateMessagesIndexRoute = Discourse.UserTopicListRoute.extend({
|
||||
userActionType: Discourse.UserAction.TYPES.messages_received,
|
||||
function createPMRoute(viewName, path, type) {
|
||||
return Discourse.UserTopicListRoute.extend({
|
||||
userActionType: Discourse.UserAction.TYPES.messages_received,
|
||||
|
||||
model: function() {
|
||||
return Discourse.TopicList.find('topics/private-messages/' + this.modelFor('user').get('username_lower'));
|
||||
},
|
||||
model: function() {
|
||||
return Discourse.TopicList.find('topics/' + path + '/' + this.modelFor('user').get('username_lower'));
|
||||
},
|
||||
|
||||
setupController: function(controller, model) {
|
||||
this._super(controller, model);
|
||||
controller.set('hideCategories', true);
|
||||
this.controllerFor('userActivity').set('pmView', 'index');
|
||||
}
|
||||
setupController: function(controller, model) {
|
||||
this._super(controller, model);
|
||||
controller.set('hideCategories', true);
|
||||
this.controllerFor('userActivity').set('pmView', viewName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
});
|
||||
Discourse.UserPrivateMessagesSentRoute = Discourse.UserTopicListRoute.extend({
|
||||
userActionType: Discourse.UserAction.TYPES.messages_sent,
|
||||
Discourse.UserPrivateMessagesIndexRoute = createPMRoute('index', 'private-messages');
|
||||
Discourse.UserPrivateMessagesMineRoute = createPMRoute('mine', 'private-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({
|
||||
userActionType: Discourse.UserAction.TYPES.topics,
|
||||
|
@ -205,7 +198,6 @@ Discourse.UserActivityTopicsRoute = Discourse.UserTopicListRoute.extend({
|
|||
model: function() {
|
||||
return Discourse.TopicList.find('topics/created-by/' + this.modelFor('user').get('username_lower'));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Discourse.UserActivityFavoritesRoute = Discourse.UserTopicListRoute.extend({
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<div class="modal-body">
|
||||
<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>
|
||||
<a href="//gravatar.com/emails" target="_blank" title="{{i18n user.change_avatar.gravatar_title}}" class="btn"><i class="icon-pencil"></i></a>
|
||||
</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">
|
||||
{{#if has_uploaded_avatar}}
|
||||
{{boundAvatar controller imageSize="large" template="uploaded_avatar_template"}} {{i18n user.change_avatar.uploaded_avatar}}
|
||||
|
|
|
@ -17,15 +17,25 @@
|
|||
{{/if}}
|
||||
|
||||
<div class='topic-meta-data span2'>
|
||||
<div {{bindAttr class=":contents byTopicCreator:topic-creator"}}>
|
||||
<a href='{{unbound usernameUrl}}'>{{avatar this imageSize="large"}}</a>
|
||||
<h3 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}'>{{breakUp username}}</a></h3>
|
||||
{{#if user_title}}<div class="user-title">{{user_title}}</div>{{/if}}
|
||||
</div>
|
||||
{{#unless userDeleted}}
|
||||
<div {{bindAttr class=":contents byTopicCreator:topic-creator"}}>
|
||||
<a href='{{unbound usernameUrl}}'>{{avatar this imageSize="large"}}</a>
|
||||
<h3 {{bindAttr class="staff new_user"}}><a href='{{unbound usernameUrl}}'>{{breakUp username}}</a></h3>
|
||||
{{#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 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"}}>
|
||||
{{#unless controller.multiSelect}}
|
||||
|
|
|
@ -16,10 +16,13 @@
|
|||
<ul class='action-list nav-stacked side-nav'>
|
||||
{{#if privateMessageView}}
|
||||
<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 {{bindAttr class=":noGlyph privateMessagesSentActive:active"}}>
|
||||
{{#linkTo 'userPrivateMessages.sent' model}}{{i18n user.private_messages_sent}}{{/linkTo}}
|
||||
<li {{bindAttr class=":noGlyph privateMessagesMineActive:active"}}>
|
||||
{{#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>
|
||||
|
||||
{{else}}
|
||||
|
|
|
@ -29,17 +29,20 @@ Discourse.PostView = Discourse.GroupedView.extend({
|
|||
|
||||
mouseUp: function(e) {
|
||||
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() {
|
||||
var selectedPosts = this.get('controller.selectedPosts');
|
||||
if (!selectedPosts) return false;
|
||||
return selectedPosts.contains(this.get('post'));
|
||||
return this.get('controller').postSelected(this.get('post'));
|
||||
}.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');
|
||||
}.property('selected', 'controller.selectedPostsCount'),
|
||||
|
||||
|
|
|
@ -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
|
||||
// Copyright (c) 2009-2010 Dominic Baggott
|
||||
// Copyright (c) 2009-2010 Ash Berlin
|
||||
|
@ -190,6 +209,35 @@ Markdown.prototype.split_blocks = function splitBlocks( input, startLine ) {
|
|||
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, ... ]
|
||||
* - block (String): the block to process
|
||||
|
@ -516,6 +564,7 @@ Markdown.dialects.Gruber = {
|
|||
|
||||
// The matcher function
|
||||
return function( block, next ) {
|
||||
|
||||
var m = block.match( is_list_re );
|
||||
if ( !m ) return undefined;
|
||||
|
||||
|
@ -667,6 +716,7 @@ Markdown.dialects.Gruber = {
|
|||
})(),
|
||||
|
||||
blockquote: function blockquote( block, next ) {
|
||||
|
||||
if ( !block.match( /^>/m ) )
|
||||
return undefined;
|
||||
|
||||
|
@ -702,7 +752,7 @@ Markdown.dialects.Gruber = {
|
|||
}
|
||||
|
||||
// Strip off the leading "> " and re-process as a block.
|
||||
var input = block.replace( /^> ?/gm, "" ),
|
||||
var input = block.replace( /^> */gm, "" ),
|
||||
old_tree = this.tree,
|
||||
processedBlock = this.toTree( input, [ "blockquote" ] ),
|
||||
attr = extract_attr( processedBlock );
|
||||
|
@ -721,39 +771,18 @@ Markdown.dialects.Gruber = {
|
|||
},
|
||||
|
||||
referenceDefn: function referenceDefn( block, next) {
|
||||
|
||||
var re = /^\s*\[(.*?)\]:\s*(\S+)(?:\s+(?:(['"])(.*?)\3|\((.*?)\)))?\n?/;
|
||||
// interesting matches are [ , ref_id, url, , title, title ]
|
||||
|
||||
if ( !block.match(re) )
|
||||
return undefined;
|
||||
|
||||
// make an attribute node if it doesn't exist
|
||||
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 attrs = create_attrs.call(this);
|
||||
|
||||
var b = this.loop_re_over_block(re, block, function( 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];
|
||||
|
||||
} );
|
||||
create_reference(attrs, m);
|
||||
});
|
||||
|
||||
if ( b.length )
|
||||
next.unshift( mk_block( b, block.trailing ) );
|
||||
|
@ -876,6 +905,7 @@ Markdown.dialects.Gruber.inline = {
|
|||
"[": function link( text ) {
|
||||
|
||||
var orig = String(text);
|
||||
|
||||
// Inline content is possible inside `link text`
|
||||
var res = Markdown.DialectHelpers.inline_until_char.call( this, text.substr(1), "]" );
|
||||
|
||||
|
@ -939,7 +969,6 @@ Markdown.dialects.Gruber.inline = {
|
|||
m = text.match( /^\s*\[(.*?)\]/ );
|
||||
|
||||
if ( m ) {
|
||||
|
||||
consumed += m[ 0 ].length;
|
||||
|
||||
// [links][] uses links as its reference
|
||||
|
@ -953,6 +982,15 @@ Markdown.dialects.Gruber.inline = {
|
|||
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]
|
||||
// Only if id is plain (no formatting.)
|
||||
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.
|
||||
Markdown.buildBlockOrder = function(d) {
|
||||
var ord = [];
|
||||
|
@ -1084,7 +1059,7 @@ Markdown.buildInlinePatterns = function(d) {
|
|||
for ( var i in d ) {
|
||||
// __foo__ is reserved and not a pattern
|
||||
if ( i.match( /^__.*__$/) ) continue;
|
||||
var l = i.replace( /([\\.*+?|()\[\]{}])/g, "\\$1" )
|
||||
var l = i.replace( /([\\.*+?$|()\[\]{}])/g, "\\$1" )
|
||||
.replace( /\n/, "\\n" );
|
||||
patterns.push( i.length == 1 ? l : "(?:" + l + ")" );
|
||||
}
|
|
@ -355,6 +355,10 @@
|
|||
font-size: 13px;
|
||||
line-height: 18px;
|
||||
}
|
||||
.deleted-user-avatar {
|
||||
font-size: 36px;
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
.staff a {
|
||||
@include border-radius-all(3px);
|
||||
|
@ -496,9 +500,11 @@
|
|||
}
|
||||
&.selected {
|
||||
article.boxed {
|
||||
.post-select {
|
||||
background-color: $blue;
|
||||
color: $white;
|
||||
.select-posts {
|
||||
button.select-post {
|
||||
background-color: $blue;
|
||||
color: $white;
|
||||
}
|
||||
}
|
||||
.topic-body {
|
||||
.contents {
|
||||
|
@ -515,20 +521,23 @@
|
|||
font-size: 16px;
|
||||
line-height: 20px;
|
||||
|
||||
.post-select {
|
||||
@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;
|
||||
.select-posts {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
font-size: 12px;
|
||||
padding: 2px 5px;
|
||||
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 {
|
||||
|
|
|
@ -196,6 +196,16 @@ class ApplicationController < ActionController::Base
|
|||
user
|
||||
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
|
||||
|
||||
def preload_anonymous_data
|
||||
|
|
|
@ -53,6 +53,14 @@ class ListController < ApplicationController
|
|||
respond(list)
|
||||
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
|
||||
query = TopicQuery.new(current_user, page: params[:page])
|
||||
|
||||
|
|
|
@ -150,16 +150,16 @@ class PostsController < ApplicationController
|
|||
|
||||
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?
|
||||
|
||||
# Make sure we can delete the posts
|
||||
|
||||
posts.each {|p| guardian.ensure_can_delete!(p) }
|
||||
|
||||
Post.transaction do
|
||||
topic_id = posts.first.topic_id
|
||||
posts.each {|p| p.destroy }
|
||||
Topic.reset_highest(topic_id)
|
||||
posts.each {|p| PostDestroyer.new(current_user, p).destroy }
|
||||
end
|
||||
|
||||
render nothing: true
|
||||
|
|
|
@ -244,7 +244,7 @@ class TopicsController < ApplicationController
|
|||
topic = Topic.where(id: params[:topic_id]).first
|
||||
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)
|
||||
end
|
||||
|
||||
|
@ -333,12 +333,12 @@ class TopicsController < ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def move_post_to_destination(topic)
|
||||
def move_posts_to_destination(topic)
|
||||
args = {}
|
||||
args[:title] = params[:title] if params[:title].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
|
||||
|
|
|
@ -11,7 +11,8 @@ class Users::OmniauthCallbacksController < ApplicationController
|
|||
Auth::OpenIdAuthenticator.new("yahoo", "https://me.yahoo.com", trusted: true),
|
||||
Auth::GithubAuthenticator.new,
|
||||
Auth::TwitterAuthenticator.new,
|
||||
Auth::PersonaAuthenticator.new
|
||||
Auth::PersonaAuthenticator.new,
|
||||
Auth::CasAuthenticator.new
|
||||
]
|
||||
|
||||
skip_before_filter :redirect_to_login_if_required
|
||||
|
@ -37,9 +38,13 @@ class Users::OmniauthCallbacksController < ApplicationController
|
|||
@data = authenticator.after_authenticate(auth)
|
||||
@data.authenticator_name = authenticator.name
|
||||
|
||||
user_found(@data.user) if @data.user
|
||||
|
||||
session[:authentication] = @data.session_data
|
||||
if @data.user
|
||||
user_found(@data.user)
|
||||
elsif SiteSetting.invite_only?
|
||||
@data.requires_invite = true
|
||||
else
|
||||
session[:authentication] = @data.session_data
|
||||
end
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
@ -87,7 +92,7 @@ class Users::OmniauthCallbacksController < ApplicationController
|
|||
session[:authentication] = nil
|
||||
@data.authenticated = true
|
||||
else
|
||||
if SiteSetting.invite_only?
|
||||
if SiteSetting.must_approve_users? && !user.approved?
|
||||
@data.awaiting_approval = true
|
||||
else
|
||||
@data.awaiting_activation = true
|
||||
|
|
|
@ -18,12 +18,7 @@ class CategoryFeaturedTopic < ActiveRecord::Base
|
|||
CategoryFeaturedTopic.transaction do
|
||||
CategoryFeaturedTopic.delete_all(category_id: c.id)
|
||||
|
||||
# fake an admin
|
||||
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)
|
||||
query = TopicQuery.new(self.fake_admin, per_page: SiteSetting.category_featured_topics, except_topic_id: c.topic_id, visible: true)
|
||||
results = query.list_category(c)
|
||||
if results.present?
|
||||
results.topic_ids.each_with_index do |topic_id, idx|
|
||||
|
@ -33,6 +28,15 @@ class CategoryFeaturedTopic < ActiveRecord::Base
|
|||
end
|
||||
end
|
||||
|
||||
private
|
||||
def self.fake_admin
|
||||
# fake an admin
|
||||
admin = User.new
|
||||
admin.admin = true
|
||||
admin.id = -1
|
||||
admin
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
# == Schema Information
|
||||
|
|
|
@ -8,6 +8,24 @@ InviteRedeemer = Struct.new(:invite) do
|
|||
invited_user
|
||||
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
|
||||
|
||||
def invited_user
|
||||
|
@ -34,7 +52,7 @@ InviteRedeemer = Struct.new(:invite) do
|
|||
|
||||
def get_invited_user
|
||||
result = get_existing_user
|
||||
result ||= create_new_user
|
||||
result ||= InviteRedeemer.create_user_for_email(invite.email)
|
||||
result.send_welcome_message = false
|
||||
result
|
||||
end
|
||||
|
@ -43,9 +61,6 @@ InviteRedeemer = Struct.new(:invite) do
|
|||
User.where(email: invite.email).first
|
||||
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
|
||||
invite.topics.private_messages.each do |t|
|
||||
|
|
18
app/models/min_trust_to_create_topic_setting.rb
Normal file
18
app/models/min_trust_to_create_topic_setting.rb
Normal 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
|
|
@ -2,3 +2,22 @@ class Oauth2UserInfo < ActiveRecord::Base
|
|||
belongs_to :user
|
||||
|
||||
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
|
||||
#
|
||||
|
||||
|
|
|
@ -1,2 +1,18 @@
|
|||
class PluginStoreRow < ActiveRecord::Base
|
||||
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
|
||||
#
|
||||
|
||||
|
|
|
@ -45,7 +45,6 @@ class Post < ActiveRecord::Base
|
|||
scope :public_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 :without_nuked_users, -> { where(nuked_user: false) }
|
||||
|
||||
def self.hidden_reasons
|
||||
@hidden_reasons ||= Enum.new(:flag_threshold_reached, :flag_threshold_reached_again, :new_user_spam_threshold_reached)
|
||||
|
@ -383,7 +382,7 @@ end
|
|||
# Table name: posts
|
||||
#
|
||||
# id :integer not null, primary key
|
||||
# user_id :integer not null
|
||||
# user_id :integer
|
||||
# topic_id :integer not null
|
||||
# post_number :integer not null
|
||||
# raw :text not null
|
||||
|
@ -419,7 +418,6 @@ end
|
|||
# notify_user_count :integer default(0), not null
|
||||
# like_score :integer default(0), not null
|
||||
# deleted_by_id :integer
|
||||
# nuked_user :boolean default(FALSE)
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
|
|
|
@ -23,3 +23,23 @@ class ScreenedEmail < ActiveRecord::Base
|
|||
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)
|
||||
#
|
||||
|
||||
|
|
|
@ -24,3 +24,24 @@ class ScreenedUrl < ActiveRecord::Base
|
|||
find_by_url(url) || create(opts.slice(:action_type, :ip_address).merge(url: url, domain: domain))
|
||||
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
|
||||
#
|
||||
|
||||
|
|
|
@ -67,7 +67,7 @@ class SiteSetting < ActiveRecord::Base
|
|||
|
||||
setting(:num_flags_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
|
||||
# You would usually use multisite for this
|
||||
|
@ -205,6 +205,8 @@ class SiteSetting < ActiveRecord::Base
|
|||
setting(:regular_requires_likes_given, 1)
|
||||
setting(:regular_requires_topic_reply_count, 3)
|
||||
|
||||
setting(:min_trust_to_create_topic, 0, enum: 'MinTrustToCreateTopicSetting')
|
||||
|
||||
# Reply by Email Settings
|
||||
setting(:reply_by_email_enabled, false)
|
||||
setting(:reply_by_email_address, '')
|
||||
|
|
|
@ -53,11 +53,15 @@ end
|
|||
# context :string(255)
|
||||
# ip_address :string(255)
|
||||
# email :string(255)
|
||||
# subject :text
|
||||
# previous_value :text
|
||||
# new_value :text
|
||||
#
|
||||
# Indexes
|
||||
#
|
||||
# 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_subject_and_id (subject,id)
|
||||
# index_staff_action_logs_on_target_user_id_and_id (target_user_id,id)
|
||||
#
|
||||
|
||||
|
|
|
@ -10,6 +10,16 @@ class Topic < ActiveRecord::Base
|
|||
include ActionView::Helpers::SanitizeHelper
|
||||
include RateLimiter::OnCreateRecord
|
||||
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
|
||||
2**31 - 1
|
||||
|
@ -21,14 +31,6 @@ class Topic < ActiveRecord::Base
|
|||
@featured_users ||= TopicFeaturedUsers.new(self)
|
||||
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)
|
||||
update_category_topic_count_by(-1) if deleted_at.nil?
|
||||
super(trashed_by)
|
||||
|
@ -561,34 +563,12 @@ class Topic < ActiveRecord::Base
|
|||
@topic_notifier ||= TopicNotifier.new(self)
|
||||
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)
|
||||
if user && user.id
|
||||
notifier.muted?(user.id)
|
||||
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)
|
||||
@ignore_category_auto_close = true
|
||||
set_auto_close(num_days)
|
||||
|
|
|
@ -204,7 +204,7 @@ end
|
|||
#
|
||||
# Indexes
|
||||
#
|
||||
# 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
|
||||
# index_forum_thread_links_on_forum_thread_id (topic_id)
|
||||
# unique_post_links (topic_id,post_id,url) UNIQUE
|
||||
#
|
||||
|
||||
|
|
|
@ -53,6 +53,6 @@ end
|
|||
#
|
||||
# Indexes
|
||||
#
|
||||
# index_forum_thread_link_clicks_on_forum_thread_link_id (topic_link_id)
|
||||
# by_link (topic_link_id)
|
||||
#
|
||||
|
||||
|
|
|
@ -13,22 +13,22 @@ class User < ActiveRecord::Base
|
|||
include Roleable
|
||||
|
||||
has_many :posts
|
||||
has_many :notifications
|
||||
has_many :topic_users
|
||||
has_many :notifications, dependent: :destroy
|
||||
has_many :topic_users, dependent: :destroy
|
||||
has_many :topics
|
||||
has_many :user_open_ids, dependent: :destroy
|
||||
has_many :user_actions
|
||||
has_many :post_actions
|
||||
has_many :email_logs
|
||||
has_many :user_actions, dependent: :destroy
|
||||
has_many :post_actions, dependent: :destroy
|
||||
has_many :email_logs, dependent: :destroy
|
||||
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 :email_tokens
|
||||
has_many :email_tokens, dependent: :destroy
|
||||
has_many :views
|
||||
has_many :user_visits
|
||||
has_many :invites
|
||||
has_many :topic_links
|
||||
has_many :uploads
|
||||
has_many :user_visits, dependent: :destroy
|
||||
has_many :invites, dependent: :destroy
|
||||
has_many :topic_links, dependent: :destroy
|
||||
has_many :uploads, dependent: :destroy
|
||||
|
||||
has_one :facebook_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
|
||||
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 :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
|
||||
|
||||
|
@ -61,6 +61,12 @@ class User < ActiveRecord::Base
|
|||
|
||||
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
|
||||
attr_accessor :send_welcome_message
|
||||
|
||||
|
@ -96,23 +102,6 @@ class User < ActiveRecord::Base
|
|||
user
|
||||
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)
|
||||
return "" unless email
|
||||
name = email.split(/[@\+]/)[0]
|
||||
|
@ -154,7 +143,7 @@ class User < ActiveRecord::Base
|
|||
self.username = new_username
|
||||
|
||||
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
|
||||
|
||||
save
|
||||
|
@ -612,17 +601,6 @@ class User < ActiveRecord::Base
|
|||
|
||||
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
|
||||
|
||||
# == Schema Information
|
||||
|
@ -647,7 +625,7 @@ end
|
|||
# website :string(255)
|
||||
# admin :boolean default(FALSE), not null
|
||||
# last_emailed_at :datetime
|
||||
# email_digests :boolean default(TRUE), not null
|
||||
# email_digests :boolean not null
|
||||
# trust_level :integer not null
|
||||
# bio_cooked :text
|
||||
# email_private_messages :boolean default(TRUE)
|
||||
|
@ -657,7 +635,7 @@ end
|
|||
# approved_at :datetime
|
||||
# topics_entered :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
|
||||
# banned_at :datetime
|
||||
# banned_till :datetime
|
||||
|
@ -690,3 +668,4 @@ end
|
|||
# index_users_on_username (username) UNIQUE
|
||||
# index_users_on_username_lower (username_lower) UNIQUE
|
||||
#
|
||||
|
||||
|
|
|
@ -196,10 +196,13 @@ ORDER BY p.created_at desc
|
|||
group_ids = topic.category.groups.pluck("groups.id")
|
||||
end
|
||||
|
||||
MessageBus.publish("/users/#{action.user.username.downcase}",
|
||||
action.id,
|
||||
user_ids: [user_id],
|
||||
group_ids: group_ids )
|
||||
if action.user
|
||||
MessageBus.publish("/users/#{action.user.username.downcase}",
|
||||
action.id,
|
||||
user_ids: [user_id],
|
||||
group_ids: group_ids )
|
||||
end
|
||||
|
||||
action
|
||||
|
||||
rescue ActiveRecord::RecordNotUnique
|
||||
|
|
|
@ -8,15 +8,15 @@ class BasicPostSerializer < ApplicationSerializer
|
|||
:cooked
|
||||
|
||||
def name
|
||||
object.user.name
|
||||
object.user.try(:name)
|
||||
end
|
||||
|
||||
def username
|
||||
object.user.username
|
||||
object.user.try(:username)
|
||||
end
|
||||
|
||||
def avatar_template
|
||||
object.user.avatar_template
|
||||
object.user.try(:avatar_template)
|
||||
end
|
||||
|
||||
def cooked
|
||||
|
|
|
@ -46,11 +46,11 @@ class PostSerializer < BasicPostSerializer
|
|||
|
||||
|
||||
def moderator?
|
||||
object.user.moderator?
|
||||
object.user.try(:moderator?) || false
|
||||
end
|
||||
|
||||
def staff?
|
||||
object.user.staff?
|
||||
object.user.try(:staff?) || false
|
||||
end
|
||||
|
||||
def yours
|
||||
|
@ -70,7 +70,7 @@ class PostSerializer < BasicPostSerializer
|
|||
end
|
||||
|
||||
def display_username
|
||||
object.user.name
|
||||
object.user.try(:name)
|
||||
end
|
||||
|
||||
def link_counts
|
||||
|
@ -101,11 +101,11 @@ class PostSerializer < BasicPostSerializer
|
|||
end
|
||||
|
||||
def user_title
|
||||
object.user.title
|
||||
object.user.try(:title)
|
||||
end
|
||||
|
||||
def trust_level
|
||||
object.user.trust_level
|
||||
object.user.try(:trust_level)
|
||||
end
|
||||
|
||||
def reply_to_user
|
||||
|
|
|
@ -15,15 +15,13 @@ module PostStreamSerializerMixin
|
|||
@highest_number_in_posts = 0
|
||||
if object.posts.present?
|
||||
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
|
||||
ps = PostSerializer.new(p, scope: scope, root: false)
|
||||
ps.topic_slug = object.topic.slug
|
||||
ps.topic_view = object
|
||||
p.topic = object.topic
|
||||
@highest_number_in_posts = p.post_number if p.post_number > @highest_number_in_posts
|
||||
ps = PostSerializer.new(p, scope: scope, root: false)
|
||||
ps.topic_slug = object.topic.slug
|
||||
ps.topic_view = object
|
||||
p.topic = object.topic
|
||||
|
||||
@posts << ps.as_json
|
||||
end
|
||||
@posts << ps.as_json
|
||||
end
|
||||
end
|
||||
@posts
|
||||
|
|
|
@ -15,12 +15,6 @@
|
|||
})();
|
||||
</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>
|
||||
Discourse.CDN = '<%= Rails.configuration.action_controller.asset_host %>';
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
<link rel="apple-touch-icon" type="image/png" href="<%=SiteSetting.apple_touch_icon_url%>">
|
||||
<%= 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/discourse_stylesheet" %>
|
||||
|
||||
|
@ -26,24 +32,6 @@
|
|||
|
||||
<%=SiteCustomization.custom_header(session[:preview_style])%>
|
||||
<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>
|
||||
|
||||
<% unless current_user %>
|
||||
|
@ -70,6 +58,24 @@
|
|||
<%= render :partial => "common/discourse_javascript" %>
|
||||
<%= 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 %> -->
|
||||
<!-- Git Version: <%= Discourse.git_version %> -->
|
||||
</body>
|
||||
|
|
|
@ -66,4 +66,9 @@ Discourse::Application.configure do
|
|||
# For origin pull cdns all you need to do is register an account and configure
|
||||
# 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
|
||||
|
|
|
@ -4,6 +4,8 @@ Discourse::Application.configure do
|
|||
# Code is not reloaded between requests
|
||||
config.cache_classes = true
|
||||
|
||||
config.log_level = :info
|
||||
|
||||
# Full error reports are disabled and caching is turned on
|
||||
config.consider_all_requests_local = false
|
||||
config.action_controller.perform_caching = true
|
||||
|
@ -37,7 +39,7 @@ Discourse::Application.configure do
|
|||
config.handlebars.precompile = true
|
||||
|
||||
# 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
|
||||
config.enable_mini_profiler = false
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# If Mini Profiler is included via gem
|
||||
if Rails.configuration.respond_to?(:enable_mini_profiler) && Rails.configuration.enable_mini_profiler
|
||||
require 'rack-mini-profiler'
|
||||
require 'flamegraph'
|
||||
# initialization is skipped so trigger it
|
||||
Rack::MiniProfilerRails.initialize!(Rails.application)
|
||||
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\/quiet_logger/
|
||||
|
||||
|
||||
# Rack::MiniProfiler.counter_method(ActiveRecord::QueryMethods, 'build_arel')
|
||||
# Rack::MiniProfiler.counter_method(Array, 'uniq')
|
||||
# require "#{Rails.root}/vendor/backports/notification"
|
||||
|
||||
# inst = Class.new
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
# 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']
|
||||
#
|
||||
token = ENV['SECRET_TOKEN'] || $redis.get('SECRET_TOKEN')
|
||||
unless token && token.length == 128
|
||||
token = SecureRandom.hex(64)
|
||||
$redis.set('SECRET_TOKEN',token)
|
||||
token = ENV['SECRET_TOKEN']
|
||||
unless token
|
||||
token = $redis.get('SECRET_TOKEN')
|
||||
unless token && token.length == 128
|
||||
token = SecureRandom.hex(64)
|
||||
$redis.set('SECRET_TOKEN',token)
|
||||
end
|
||||
end
|
||||
|
||||
Discourse::Application.config.secret_token = token
|
||||
|
|
|
@ -5,6 +5,11 @@ Sidekiq.configure_server do |config|
|
|||
Sidetiq::Clock.start!
|
||||
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
|
||||
|
|
|
@ -92,6 +92,7 @@ predef:
|
|||
- find
|
||||
- sinon
|
||||
- controllerFor
|
||||
- testController
|
||||
- Favcount
|
||||
|
||||
browser: true # true if the standard browser globals should be predefined
|
||||
|
|
|
@ -20,9 +20,6 @@ de:
|
|||
mb: MB
|
||||
tb: TB
|
||||
dates:
|
||||
short_date_no_year: "D MMM"
|
||||
short_date: "D. MMM YYYY"
|
||||
long_date: "D. MMMM YYYY, H:mm"
|
||||
tiny:
|
||||
half_a_minute: "< 1Min"
|
||||
less_than_x_seconds:
|
||||
|
@ -43,12 +40,6 @@ de:
|
|||
x_days:
|
||||
one: "1T"
|
||||
other: "%{count}T"
|
||||
about_x_months:
|
||||
one: "1Mon"
|
||||
other: "%{count}Mon"
|
||||
x_months:
|
||||
one: "1Mon"
|
||||
other: "%{count}Mon"
|
||||
about_x_years:
|
||||
one: "1J"
|
||||
other: "%{count}J"
|
||||
|
@ -93,6 +84,7 @@ de:
|
|||
yes_value: "Ja"
|
||||
of_value: "von"
|
||||
generic_error: "Entschuldigung, ein Fehler ist aufgetreten."
|
||||
generic_error_with_reason: "Ein Fehler ist aufgetreten: %{error}"
|
||||
log_in: "Anmelden"
|
||||
age: "Alter"
|
||||
last_post: "Letzter Beitrag"
|
||||
|
@ -101,10 +93,20 @@ de:
|
|||
show_more: "zeige mehr"
|
||||
links: Links
|
||||
faq: "FAQ"
|
||||
privacy_policy: "Datenschutzrichtlinie"
|
||||
you: "Du"
|
||||
or: "oder"
|
||||
now: "gerade eben"
|
||||
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:
|
||||
one: "in einer Sekunde"
|
||||
|
@ -137,6 +139,10 @@ de:
|
|||
saving: "Wird gespeichert..."
|
||||
saved: "Gespeichert!"
|
||||
|
||||
upload: "Hochladen"
|
||||
uploading: "Hochladen..."
|
||||
uploaded: "Hochgeladen!"
|
||||
|
||||
choose_topic:
|
||||
none_found: "Keine Themen gefunden."
|
||||
title:
|
||||
|
@ -175,6 +181,7 @@ de:
|
|||
"13": "Eingänge"
|
||||
|
||||
user:
|
||||
said: "{{username}} sagte:"
|
||||
profile: Profil
|
||||
title: "Benutzer"
|
||||
mute: Ignorieren
|
||||
|
@ -182,6 +189,7 @@ de:
|
|||
download_archive: "Archiv meiner Beiträge herunterladen"
|
||||
private_message: "Private Nachricht"
|
||||
private_messages: "Nachrichten"
|
||||
private_messages_sent: "Gesendete Nachrichten"
|
||||
activity_stream: "Aktivität"
|
||||
preferences: "Einstellungen"
|
||||
bio: "Über mich"
|
||||
|
@ -191,30 +199,41 @@ de:
|
|||
dynamic_favicon: "Zeige eingehende Nachrichten im Favicon"
|
||||
external_links_in_new_tab: "Öffne alle externen Links in neuen Tabs"
|
||||
enable_quoting: "Markierten Text bei Antwort zitieren"
|
||||
|
||||
change: "ändern"
|
||||
moderator: "{{user}} ist Moderator"
|
||||
admin: "{{user}} ist Administrator"
|
||||
|
||||
change_password:
|
||||
action: "ändern"
|
||||
success: "(Mail gesendet)"
|
||||
in_progress: "(sende Mail)"
|
||||
error: "(Fehler)"
|
||||
action: "Passwort zurücksetzten Mail senden"
|
||||
|
||||
change_about:
|
||||
title: "Über mich ändern"
|
||||
|
||||
change_username:
|
||||
action: "ändern"
|
||||
title: "Benutzername ändern"
|
||||
confirm: "Den Benutzernamen zu ändern kann Konsequenzen nach sich ziehen. Bist Du sicher, dass du fortfahren willst?"
|
||||
taken: "Entschuldige, der Benutzername ist schon vergeben."
|
||||
error: "Beim Ändern des Benutzernamens ist ein Fehler aufgetreten."
|
||||
invalid: "Dieser Benutzername ist ungültig, sie dürfen nur aus Zahlen und Buchstaben bestehen."
|
||||
|
||||
change_email:
|
||||
action: 'ändern'
|
||||
title: "Mailadresse ändern"
|
||||
taken: "Entschuldige, diese Mailadresse ist nicht verfügbar."
|
||||
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."
|
||||
|
||||
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:
|
||||
title: "Mail"
|
||||
instructions: "Deine Mailadresse wird niemals öffentlich angezeigt."
|
||||
|
@ -378,6 +397,7 @@ de:
|
|||
authenticating: "Authentisiere..."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
|
@ -489,16 +509,23 @@ de:
|
|||
total_flagged: "total markierte Einträge"
|
||||
|
||||
upload_selector:
|
||||
title: "Bild einfügen"
|
||||
from_my_computer: "von meinem Gerät"
|
||||
from_the_web: "aus dem Web"
|
||||
title: "Bild hochladen"
|
||||
title_with_attachments: "Bild oder Datei hochladen"
|
||||
from_my_computer: "Von meinem Gerät"
|
||||
from_the_web: "Aus dem Web"
|
||||
add_title: "Bild hinzufügen"
|
||||
add_title_with_attachments: "Bild oder Datei hinzufügen"
|
||||
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_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_with_attachments: "Lokales Bild oder Datei"
|
||||
local_tip: "Klicke hier, um ein Bild von deinem Gerät zu wählen."
|
||||
upload_title: "Hochladen"
|
||||
uploading: "Bild wird hochgeladen"
|
||||
local_tip_with_attachments: "Klicke hier, um ein Bild oder eine Datei von deinem Gerät zu wählen (Erlaubte Dateiendungen: {{authorized_extensions}})"
|
||||
upload_title: "Bild hochladen"
|
||||
upload_title_with_attachments: "Bild oder Datei hochladen"
|
||||
uploading: "Hochgeladen..."
|
||||
|
||||
search:
|
||||
title: "Such nach Themen, Beiträgen, Nutzern oder Kategorien"
|
||||
|
@ -745,10 +772,13 @@ de:
|
|||
edit: "Editing {{link}} von {{replyAvatar}} {{username}}"
|
||||
post_number: "Beitrag {{number}}"
|
||||
in_reply_to: "Antwort auf"
|
||||
last_edited_on: "Antwort zuletzt bearbeitet am"
|
||||
reply_as_new_topic: "Mit Themenwechsel antworten"
|
||||
continue_discussion: "Fortsetzung des Gesprächs {{postLink}}:"
|
||||
follow_quote: "Springe zu zitiertem Beitrag"
|
||||
deleted_by_author: "(Beitrag vom Autor entfernt)"
|
||||
follow_quote: "Springe zu dem zitiertem Beitrag"
|
||||
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"
|
||||
expand_collapse: "mehr/weniger"
|
||||
|
||||
|
@ -760,11 +790,11 @@ de:
|
|||
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."
|
||||
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)."
|
||||
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."
|
||||
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."
|
||||
|
||||
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?"
|
||||
|
||||
category:
|
||||
can: 'kann… '
|
||||
none: '(keine Kategorie)'
|
||||
edit: '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."
|
||||
hotness: "Beliebtheit"
|
||||
already_used: 'Diese Farbe wird bereits für eine andere Kategorie verwendet'
|
||||
is_secure: "Sichere Kategorie?"
|
||||
add_group: "Gruppe hinzufügen"
|
||||
security: "Sicherheit"
|
||||
allowed_groups: "Erlaubte Gruppen:"
|
||||
auto_close_label: "Thema automatisch schließen nach:"
|
||||
|
||||
edit_permissions: "Berechtigung bearbeiten"
|
||||
add_permission: "Berechtigung hinzufügen"
|
||||
|
||||
flagging:
|
||||
title: 'Aus welchem Grund meldest Du diesen Beitrag?'
|
||||
action: 'Beitrag melden'
|
||||
take_action: "Reagieren"
|
||||
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."
|
||||
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."
|
||||
|
@ -956,6 +988,7 @@ de:
|
|||
views_long: "Dieses Thema wurde {{number}} aufgerufen"
|
||||
activity: "Aktivität"
|
||||
likes: "Gefällt mir"
|
||||
likes_long: "es gibt {{number}} „Gefällt mir“ in diesem Thema"
|
||||
top_contributors: "Teilnehmer"
|
||||
category_title: "Kategorie"
|
||||
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>.'
|
||||
|
||||
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
|
||||
admin_js:
|
||||
type_to_filter: "Tippe etwas ein, um zu filtern..."
|
||||
|
@ -1014,6 +1052,7 @@ de:
|
|||
|
||||
dashboard:
|
||||
title: "Übersicht"
|
||||
last_updated: "Übersicht zuletzt aktualisiert:"
|
||||
version: "Version"
|
||||
up_to_date: "Discourse ist aktuell."
|
||||
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: "Ablehnen"
|
||||
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"
|
||||
error: "Etwas ist schief gelaufen"
|
||||
|
@ -1145,6 +1185,48 @@ de:
|
|||
last_seen_user: "Letzer Benutzer:"
|
||||
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:
|
||||
title: "Aus Nutzersicht betrachten"
|
||||
username_or_email: "Benutzername oder Mailadresse des Nutzers"
|
||||
|
@ -1170,6 +1252,9 @@ de:
|
|||
approved_selected:
|
||||
one: "Benutzer zulassen"
|
||||
other: "Benutzer zulassen ({{count}})"
|
||||
reject_selected:
|
||||
one: "Benutzer ablehnen"
|
||||
other: "Lehne ({{count}}) Benutzer ab"
|
||||
titles:
|
||||
active: 'Aktive Benutzer'
|
||||
new: 'Neue Benutzer'
|
||||
|
@ -1183,12 +1268,19 @@ de:
|
|||
moderators: 'Moderatoren'
|
||||
blocked: 'Gesperrte 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:
|
||||
ban_failed: "Beim Sperren 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)"
|
||||
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"
|
||||
unban: "Entsperren"
|
||||
banned: "Gesperrt?"
|
||||
|
@ -1219,12 +1311,18 @@ de:
|
|||
flags_received_count: Erhaltene Meldungen
|
||||
approve: 'Genehmigen'
|
||||
approved_by: "genehmigt von"
|
||||
approve_success: "Benutzer freigeschalten und Mail mit den Anweisungen zur Aktivierung gesendet."
|
||||
approve_bulk_success: "Erfolg! Alle ausgewählten Benutzer wurden freigeschalten und benachrichtigt."
|
||||
approve_success: "Benutzer freigeschalten und Mail mit den Anweisungen zur Aktivierung
|
||||
gesendet."
|
||||
approve_bulk_success: "Erfolg! Alle ausgewählten Benutzer wurden freigeschalten und
|
||||
benachrichtigt."
|
||||
time_read: "Lesezeit"
|
||||
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_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."
|
||||
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"
|
||||
|
@ -1239,7 +1337,7 @@ de:
|
|||
deactivate_explanation: "Ein deaktivierter Benutzer muss seine E-Mail erneut bestätigen."
|
||||
banned_explanation: "Ein gesperrter Benutzer kann sich nicht einloggen."
|
||||
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:
|
||||
none: "Wähle einen Inhaltstyp um mit dem Bearbeiten zu beginnen."
|
||||
|
@ -1251,3 +1349,5 @@ de:
|
|||
title: 'Einstellungen'
|
||||
reset: 'Zurücksetzen'
|
||||
none: "Keine"
|
||||
|
||||
|
||||
|
|
|
@ -190,7 +190,6 @@ en:
|
|||
download_archive: "download archive of my posts"
|
||||
private_message: "Private Message"
|
||||
private_messages: "Messages"
|
||||
private_messages_sent: "Sent Messages"
|
||||
activity_stream: "Activity"
|
||||
preferences: "Preferences"
|
||||
bio: "About me"
|
||||
|
@ -203,6 +202,12 @@ en:
|
|||
change: "change"
|
||||
moderator: "{{user}} is a moderator"
|
||||
admin: "{{user}} is an admin"
|
||||
deleted: "User Was Deleted"
|
||||
|
||||
messages:
|
||||
all: "All"
|
||||
mine: "Mine"
|
||||
unread: "Unread"
|
||||
|
||||
change_password:
|
||||
success: "(email sent)"
|
||||
|
@ -398,6 +403,7 @@ en:
|
|||
authenticating: "Authenticating..."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
|
@ -505,7 +511,7 @@ en:
|
|||
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"
|
||||
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"
|
||||
|
||||
upload_selector:
|
||||
|
@ -759,6 +765,7 @@ en:
|
|||
multi_select:
|
||||
select: 'select'
|
||||
selected: 'selected ({{count}})'
|
||||
select_replies: 'select +replies'
|
||||
delete: delete selected
|
||||
cancel: cancel selecting
|
||||
description:
|
||||
|
@ -811,6 +818,12 @@ en:
|
|||
undelete: "undelete this post"
|
||||
share: "share a link to this post"
|
||||
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:
|
||||
flag: 'Flag'
|
||||
|
|
|
@ -220,7 +220,6 @@ ru:
|
|||
download_archive: скачать архив ваших сообщений
|
||||
private_message: Личное сообщение
|
||||
private_messages: Личные сообщения
|
||||
private_messages_sent: Отправленные сообщения
|
||||
activity_stream: Активность
|
||||
preferences: Настройки
|
||||
bio: Обо мне
|
||||
|
@ -233,6 +232,10 @@ ru:
|
|||
change: изменить
|
||||
moderator: '{{user}} - модератор'
|
||||
admin: '{{user}} - админ'
|
||||
messages:
|
||||
all: Все
|
||||
mine: Мои
|
||||
unread: Непрочитанные
|
||||
change_password:
|
||||
success: (письмо отправлено)
|
||||
in_progress: (отправка письма)
|
||||
|
@ -258,6 +261,7 @@ ru:
|
|||
uploaded_avatar: Собственный аватар
|
||||
uploaded_avatar_empty: Добавить собственный аватар
|
||||
upload_title: Загрузка собственного аватара
|
||||
image_is_not_a_square: 'Внимание: изображение было кадрировано, т.к. оно не квадратное.'
|
||||
email:
|
||||
title: Email
|
||||
instructions: Ваш адрес электронной почты всегда скрыт.
|
||||
|
@ -407,6 +411,7 @@ ru:
|
|||
authenticating: Проверка...
|
||||
awaiting_confirmation: Ваша учетная запись требует активации. Для того чтобы получить активационное письмо повторно, воспользуйтесь опцией сброса пароля.
|
||||
awaiting_approval: Ваша учетная запись еще не одобрена. Вы получите письмо, когда это случится.
|
||||
requires_invite: К сожалению, доступ к форуму только по приглашениям.
|
||||
not_activated: 'Прежде чем вы сможете воспользоваться новой учетной записью, вам необходимо ее активировать. Мы отправили вам на почту <b>{{sentTo}}</b> подробные инструкции, как это cделать.'
|
||||
resend_activation_email: Щелкните здесь, чтобы мы повторно выслали вам письмо для активации учетной записи.
|
||||
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}}"
|
||||
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}} принял ваше приглашение"
|
||||
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: всего сообщений с жалобами
|
||||
upload_selector:
|
||||
title: Загрузить изображение
|
||||
|
|
|
@ -188,7 +188,6 @@ zh_CN:
|
|||
download_archive: "下载我的帖子的存档"
|
||||
private_message: "私信"
|
||||
private_messages: "消息"
|
||||
private_messages_sent: "已发送消息"
|
||||
activity_stream: "活动"
|
||||
preferences: "设置"
|
||||
bio: "关于我"
|
||||
|
@ -201,6 +200,11 @@ zh_CN:
|
|||
change: "修改"
|
||||
moderator: "{{user}} 是版主"
|
||||
admin: "{{user}} 是管理员"
|
||||
|
||||
messages:
|
||||
all: "所有"
|
||||
mine: "我的"
|
||||
unread: "未读"
|
||||
|
||||
change_password:
|
||||
success: "(电子邮件已发送)"
|
||||
|
@ -396,6 +400,7 @@ zh_CN:
|
|||
authenticating: "验证中……"
|
||||
awaiting_confirmation: "你的帐号尚未激活,点击忘记密码链接来重新发送激活邮件。"
|
||||
awaiting_approval: "你的帐号尚未被论坛版主批准。一旦你的帐号获得批准,你会收到一封电子邮件。"
|
||||
requires_invite: "抱歉,本论坛仅接受邀请注册。"
|
||||
not_activated: "你还不能登录。我们之前在<b>{{sentTo}}</b>发送了一封激活邮件给你。请按照邮件中的介绍来激活你的帐号。"
|
||||
resend_activation_email: "点击此处来重新发送激活邮件。"
|
||||
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}}"
|
||||
invited_to_private_message: "{{username}} 邀请你进行私下交流:{{link}}"
|
||||
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: "被报告帖子的总数"
|
||||
|
||||
upload_selector:
|
||||
|
|
|
@ -5,9 +5,15 @@
|
|||
# http://yamllint.com/
|
||||
|
||||
de:
|
||||
dates:
|
||||
short_date_no_year: "D MMM"
|
||||
short_date: "D. MMM YYYY"
|
||||
long_date: "D. MMMM YYYY, H:mm"
|
||||
time:
|
||||
formats:
|
||||
short: "%d. %m. %Y"
|
||||
short_no_year: "%-d. %B"
|
||||
date_only: "%-d. %b %Y"
|
||||
|
||||
title: "Discourse"
|
||||
topics: "Themen"
|
||||
|
@ -33,6 +39,10 @@ de:
|
|||
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."
|
||||
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:
|
||||
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."
|
||||
|
@ -50,8 +60,13 @@ de:
|
|||
rss_topics_in_category: "RSS-Feed von Themen in der Kategorie '%{category}'"
|
||||
author_wrote: "%{author} schrieb:"
|
||||
private_message_abbrev: "PN"
|
||||
rss_description:
|
||||
latest: "Neuste Themen"
|
||||
hot: Angesagte Themen"
|
||||
|
||||
groups:
|
||||
errors:
|
||||
can_not_modify_automatic: "Du kannst eine automatische Gruppe nicht bearbeiten"
|
||||
default_names:
|
||||
admins: "admins"
|
||||
moderators: "moderatoren"
|
||||
|
@ -70,8 +85,6 @@ de:
|
|||
'new-topic': |
|
||||
Willkommen auf %{site_name} — **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?
|
||||
|
||||
- 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': |
|
||||
Willkommen auf %{site_name} — **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?
|
||||
|
||||
- Behandle deine Gesprächspartner mit demselben Respekt, den Du von ihnen erwartest.
|
||||
|
@ -130,6 +141,8 @@ de:
|
|||
title: "Anführer"
|
||||
elder:
|
||||
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:
|
||||
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.'
|
||||
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>.'
|
||||
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>.'
|
||||
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_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>."
|
||||
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:
|
||||
education_new_reply:
|
||||
|
@ -405,22 +423,30 @@ de:
|
|||
welcome_invite:
|
||||
title: "Willkommen: Eingeladener Benutzer"
|
||||
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:
|
||||
title: "Anmeldung erforderlich: Willkommensnachricht"
|
||||
description: "Willkommensnachricht welche angezeigt wird wenn der Benutzer nicht angemeldet ist und die
|
||||
Einstellung 'login required' aktiviert ist."
|
||||
|
||||
tos_user_content_license:
|
||||
title: "Nutzungsbedingungen: Lizenz"
|
||||
description: "Der Text für die Lizenz-Sektion in den Nutzungsbedingungen."
|
||||
tos_miscellaneous:
|
||||
title: "Nutzungsbedingungen: Verschiedenes"
|
||||
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:
|
||||
default_locale: "Die Standardsprache dieser Discourse-Instanz (kodiert in ISO 639-1)."
|
||||
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."
|
||||
min_topic_title_length: "Minimale 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."
|
||||
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."
|
||||
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_height: "Maximalhöhe von Bildern in einem Beitrag."
|
||||
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."
|
||||
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."
|
||||
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."
|
||||
|
@ -457,6 +486,7 @@ de:
|
|||
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."
|
||||
email_custom_headers: "Eine Pipe-getrennte (|) Liste von eigenen Mail Headern"
|
||||
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_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."
|
||||
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."
|
||||
|
||||
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."
|
||||
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."
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
|
@ -491,11 +523,14 @@ de:
|
|||
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."
|
||||
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)."
|
||||
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."
|
||||
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."
|
||||
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_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_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_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."
|
||||
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_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."
|
||||
max_word_length: "Maximale Wortlänge in Zeichen in Thementiteln."
|
||||
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."
|
||||
|
||||
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."
|
||||
|
||||
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_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."
|
||||
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."
|
||||
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:
|
||||
mentioned: "%{display_username} hat Dich in %{link} erwähnt."
|
||||
liked: "%{display_username} gefällt deinen Beitrag in %{link}."
|
||||
|
@ -633,6 +697,9 @@ de:
|
|||
moderator_post:
|
||||
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}"
|
||||
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:
|
||||
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."
|
||||
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."
|
||||
banned: "Du kannst dich bis am %{date} nicht mehr anmelden."
|
||||
errors: "%{errors}"
|
||||
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."
|
||||
|
@ -716,6 +784,8 @@ de:
|
|||
|
||||
Deine Freunde von %{site_name}.
|
||||
|
||||
:smile:
|
||||
|
||||
[0]: %{base_url}
|
||||
[1]: http://www.kitterman.com/spf/validate.html
|
||||
[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>
|
||||
|
||||
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:
|
||||
post_hidden:
|
||||
subject_template: "Beitrag wegen Meldungen aus der Community versteckt"
|
||||
|
@ -839,6 +920,15 @@ de:
|
|||
|
||||
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:
|
||||
subject_template: "Benutzer %{username} wurde automatisch gesperrt"
|
||||
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.
|
||||
|
||||
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:
|
||||
subject_template: "Benutzerkonto entsperrt"
|
||||
text_body_template: |
|
||||
|
@ -855,13 +952,28 @@ de:
|
|||
|
||||
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})."
|
||||
|
||||
user_notifications:
|
||||
previous_discussion: "Vorangehende Antworten"
|
||||
unsubscribe:
|
||||
title: "Mails Abbestellen"
|
||||
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:
|
||||
subject_template: "[%{site_name}] %{username} hat Dich zu einem privaten Gespräch eingeladen: '%{topic_title}'"
|
||||
text_body_template: |
|
||||
|
@ -872,52 +984,49 @@ de:
|
|||
user_replied:
|
||||
subject_template: "[%{site_name}] %{username} hat auf deinen Beitrag '%{topic_title}' geantwortet"
|
||||
text_body_template: |
|
||||
%{username} hat auf deinen Beitrag '%{topic_title}' auf %{site_name} geantwortet:
|
||||
|
||||
---
|
||||
%{message}
|
||||
|
||||
%{context}
|
||||
|
||||
---
|
||||
Um zu antworten, besuche den folgenden Link: %{base_url}%{url}
|
||||
%{respond_instructions}
|
||||
|
||||
user_quoted:
|
||||
subject_template: "[%{site_name}] %{username} hat Dich in '%{topic_title}' zitiert"
|
||||
text_body_template: |
|
||||
%{username} hat Dich in '%{topic_title}' auf %{site_name} zitiert:
|
||||
|
||||
---
|
||||
%{message}
|
||||
|
||||
%{context}
|
||||
|
||||
---
|
||||
Um zu antworten, besuche den folgenden Link: %{base_url}%{url}
|
||||
%{respond_instructions}
|
||||
|
||||
user_mentioned:
|
||||
subject_template: "[%{site_name}] %{username} hat Dich in '%{topic_title}' erwähnt"
|
||||
text_body_template: |
|
||||
%{username} hat Dich in '%{topic_title}' auf %{site_name} erwähnt:
|
||||
|
||||
---
|
||||
%{message}
|
||||
|
||||
%{context}
|
||||
|
||||
---
|
||||
Um zu antworten, besuche den folgenden Link: %{base_url}%{url}
|
||||
%{respond_instructions}
|
||||
|
||||
user_posted:
|
||||
subject_template: "[%{site_name}] %{subject_prefix}%{username} hat auf '%{topic_title}' geantwortet"
|
||||
text_body_template: |
|
||||
%{username} hat in '%{topic_title}' auf %{site_name} geantwortet:
|
||||
|
||||
---
|
||||
%{message}
|
||||
|
||||
%{context}
|
||||
|
||||
---
|
||||
Um zu antworten, besuche den folgenden Link: %{base_url}%{url}
|
||||
%{respond_instructions}
|
||||
|
||||
digest:
|
||||
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}"
|
||||
new_activity: "Neues in deinen Themen und Beiträgen:"
|
||||
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}."
|
||||
click_here: "klicke hier"
|
||||
from: "%{site_name} Übersicht"
|
||||
|
@ -992,8 +1101,12 @@ de:
|
|||
deleted: 'gelöscht'
|
||||
|
||||
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:
|
||||
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."
|
||||
size_not_found: "Entschuldige, aber wir konnten die Grösse des Bildes nicht feststellen. Vielleicht ist das Bild defekt?"
|
||||
|
|
|
@ -94,13 +94,13 @@ en:
|
|||
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
|
||||
|
||||
'new-reply': |
|
||||
Welcome to %{site_name} — **thanks for contributing to the conversation!**
|
||||
Welcome to %{site_name} — **thanks for contributing!**
|
||||
|
||||
- Does your reply improve the conversation in some way?
|
||||
|
||||
- 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}.
|
||||
|
||||
|
@ -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_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_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"
|
||||
|
@ -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}).
|
||||
|
||||
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: |
|
||||
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}).
|
||||
|
||||
|
@ -1027,7 +1029,7 @@ en:
|
|||
new_activity: "New activity on your topics and posts:"
|
||||
top_topics: "Recent posts the community enjoyed:"
|
||||
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"
|
||||
from: "%{site_name} digest"
|
||||
read_more: "Read More"
|
||||
|
|
|
@ -47,13 +47,13 @@ id:
|
|||
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
|
||||
|
||||
'new-reply': |
|
||||
Welcome to %{site_name} — **thanks for contributing to the conversation!**
|
||||
Welcome to %{site_name} — **thanks for contributing!**
|
||||
|
||||
- Does your reply improve the conversation in some way?
|
||||
|
||||
- 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}.
|
||||
|
||||
|
@ -725,7 +725,7 @@ id:
|
|||
subject_template: "[%{site_name}] Forum Activity for %{date}"
|
||||
new_activity: "New activity on your topics and posts:"
|
||||
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"
|
||||
from: "%{site_name} digest"
|
||||
|
||||
|
|
|
@ -74,7 +74,7 @@ ko:
|
|||
For more guidance, [see our FAQ](/faq). This panel will only appear for your first %{education_posts_text}.
|
||||
|
||||
'new-reply': |
|
||||
Welcome to %{site_name} — **thanks for contributing to the conversation!**
|
||||
Welcome to %{site_name} — **thanks for contributing!**
|
||||
|
||||
Keep in mind as you compose your reply:
|
||||
|
||||
|
@ -82,7 +82,7 @@ ko:
|
|||
|
||||
- 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}.
|
||||
|
||||
|
@ -870,7 +870,7 @@ ko:
|
|||
subject_template: "[%{site_name}] Forum Activity for %{date}"
|
||||
new_activity: "New activity on your topics and posts:"
|
||||
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"
|
||||
from: "%{site_name} digest"
|
||||
|
||||
|
|
|
@ -532,6 +532,7 @@ ru:
|
|||
edit_history_visible_to_public: Позволить всем видеть предыдущие версии сообщения. Когда отключено, историю изменений может видеть только персонал.
|
||||
delete_removed_posts_after: Количество часов, после которого сообщение, удаленное пользователем, удаляется.
|
||||
max_image_width: Максимальная ширина изображений, добавляемых в сообщение
|
||||
max_image_height: Максимальная высота изображения в сообщении
|
||||
category_featured_topics: Количество отображаемых тем в категориях на странице /categories
|
||||
add_rel_nofollow_to_user_content: 'Добавить "rel nofollow" для всех ссылок за исключением внутренних (включая родительский домен). Изменение данной настройки потребует обновления всех сообщений (<code>rake posts:rebake</code>)'
|
||||
exclude_rel_nofollow_domains: Разделенный запятыми список доменов, в которых nofollow не добавлено (tld.com автоматически позволит также и sub.tld.com)
|
||||
|
@ -941,11 +942,11 @@ ru:
|
|||
Пожалуйста [проверьте жалобы](/admin/flags). Если пользователь %{username} был заблокирован неверно, нажмите кнопку разблокировки [на странице управления пользователем](%{user_url}).
|
||||
|
||||
spam_post_blocked:
|
||||
subject_template: 'В сообщении пользователя %{username} обнаружен спам'
|
||||
subject_template: 'Новый пользователь %{username} отправляет одинаковые ссылки'
|
||||
text_body_template: |
|
||||
Это автоматическое сообщение для информирования о том, что пользователь [%{username}](%{user_url}) попытался создать сообщение со ссылками, но был остановлен политикой антиспама на основе настройки сайта newuser_spam_host_threshold.
|
||||
Это автоматическое сообщение. Новый пользователь [%{username}](%{user_url}) попытался создать множество сообщений со ссылкой на один и тот же домен, однако был заблокирован на основании настройки newuser_spam_host_threshold.
|
||||
|
||||
Пожалуйста [проверьте действия пользователя](%{user_url}).
|
||||
Пожалуйста [проверьте блокировку](%{user_url}).
|
||||
|
||||
unblocked:
|
||||
subject_template: Учетная запись разблокирована
|
||||
|
@ -954,6 +955,17 @@ ru:
|
|||
|
||||
Это автоматическое сообщение сайта %{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}).'
|
||||
user_notifications:
|
||||
previous_discussion: Предыдущие ответы
|
||||
|
@ -1016,9 +1028,7 @@ ru:
|
|||
new_activity: 'Новая активность в ваших темах и сообщениях:'
|
||||
top_topics: 'Последние темы, которые были оценены пользователями форума:'
|
||||
other_new_topics: 'Другие новые темы:'
|
||||
unsubscribe: |
|
||||
Данное сообщение отправлено как напоминание с сайта %{site_link} потому что вы давно не заходили к нам.
|
||||
Для того, чтобы отписаться от наших сообщений, пройдите по ссылке %{unsubscribe_link}.
|
||||
unsubscribe: 'Данное сообщение отправлено как напоминание с сайта %{site_link} потому что вы давно не заходили к нам. Для того, чтобы отписаться от наших сообщений, пройдите по ссылке %{unsubscribe_link}.'
|
||||
click_here: нажмите здесь
|
||||
from: 'Cводка новостей сайта %{site_name}'
|
||||
read_more: Читать еще
|
||||
|
|
|
@ -783,7 +783,7 @@ sv:
|
|||
subject_template: "[%{site_name}] Forum Activity for %{date}"
|
||||
new_activity: "New activity on your topics and posts:"
|
||||
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"
|
||||
from: "%{site_name} digest"
|
||||
|
||||
|
|
|
@ -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/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-unread/:username' => 'list#private_messages_unread', as: 'topics_private_messages_unread', constraints: {username: USERNAME_ROUTE_FORMAT}
|
||||
|
||||
# Topic routes
|
||||
get 't/:slug/:topic_id/wordpress' => 'topics#wordpress', constraints: {topic_id: /\d+/}
|
||||
|
|
19
db/migrate/20130828192526_fix_optimized_images_urls.rb
Normal file
19
db/migrate/20130828192526_fix_optimized_images_urls.rb
Normal 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
|
12
db/migrate/20130903154323_allow_null_user_id_on_posts.rb
Normal file
12
db/migrate/20130903154323_allow_null_user_id_on_posts.rb
Normal 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
|
|
@ -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
|
|
@ -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
|
||||
|
||||
1. `bundle install`
|
||||
2. `bundle exec rake db:migrate`
|
||||
3. `bundle exec rake db:test:prepare`
|
||||
4. `bundle exec rake db:seed_fu`
|
||||
5. Try running the specs: `bundle exec rake autospec`
|
||||
6. `bundle exec rails server`
|
||||
2. `bundle exec rake db:migrate db:test:prepare db:seed_fu`
|
||||
4. Try running the specs: `bundle exec rake autospec`
|
||||
5. `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!
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ If you have a mail server responsible for handling the egress of email from your
|
|||
Install necessary packages:
|
||||
|
||||
# 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:
|
||||
# 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
|
||||
- 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
|
||||
- browse througn all the settings
|
||||
|
|
|
@ -7,3 +7,4 @@ require_dependency 'auth/open_id_authenticator'
|
|||
require_dependency 'auth/github_authenticator'
|
||||
require_dependency 'auth/twitter_authenticator'
|
||||
require_dependency 'auth/persona_authenticator'
|
||||
require_dependency 'auth/cas_authenticator'
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
class Auth::Result
|
||||
attr_accessor :user, :name, :username, :email, :user,
|
||||
:email_valid, :extra_data, :awaiting_activation,
|
||||
:awaiting_approval, :authenticated, :authenticator_name
|
||||
:awaiting_approval, :authenticated, :authenticator_name,
|
||||
:requires_invite
|
||||
|
||||
def session_data
|
||||
{
|
||||
|
@ -15,7 +16,9 @@ class Auth::Result
|
|||
end
|
||||
|
||||
def to_client_hash
|
||||
if user
|
||||
if requires_invite
|
||||
{ requires_invite: true }
|
||||
elsif user
|
||||
{
|
||||
authenticated: !!authenticated,
|
||||
awaiting_activation: !!awaiting_activation,
|
||||
|
|
|
@ -110,4 +110,16 @@ module DiscourseHub
|
|||
def self.accepts
|
||||
[:json, 'application/vnd.discoursehub.v1']
|
||||
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
|
||||
|
|
|
@ -2,13 +2,14 @@ require 'digest/sha1'
|
|||
require 'open-uri'
|
||||
|
||||
class S3Store
|
||||
@fog_loaded ||= require 'fog'
|
||||
|
||||
def store_upload(file, upload)
|
||||
# <id><sha1><extension>
|
||||
path = "#{upload.id}#{upload.sha1}#{upload.extension}"
|
||||
|
||||
# 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
|
||||
"#{absolute_base_url}/#{path}"
|
||||
|
@ -58,9 +59,7 @@ class S3Store
|
|||
end
|
||||
|
||||
def remove_file(url)
|
||||
return unless has_been_uploaded?(url)
|
||||
name = File.basename(url)
|
||||
remove(name)
|
||||
remove File.basename(url) if has_been_uploaded?(url)
|
||||
end
|
||||
|
||||
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?
|
||||
end
|
||||
|
||||
def get_or_create_directory(name)
|
||||
def get_or_create_directory(bucket)
|
||||
check_missing_site_settings
|
||||
|
||||
@fog_loaded ||= require 'fog'
|
||||
fog = Fog::Storage.new s3_options
|
||||
|
||||
fog = Fog::Storage.new generate_options
|
||||
|
||||
directory = fog.directories.get(name)
|
||||
directory = fog.directories.create(key: name) unless directory
|
||||
directory = fog.directories.get(bucket)
|
||||
directory = fog.directories.create(key: bucket) unless directory
|
||||
directory
|
||||
end
|
||||
|
||||
def generate_options
|
||||
def s3_options
|
||||
options = {
|
||||
provider: 'AWS',
|
||||
aws_access_key_id: SiteSetting.s3_access_key_id,
|
||||
|
@ -124,22 +121,21 @@ class S3Store
|
|||
options
|
||||
end
|
||||
|
||||
def upload(file, name, content_type=nil)
|
||||
def upload(file, unique_filename, filename=nil, content_type=nil)
|
||||
args = {
|
||||
key: name,
|
||||
key: unique_filename,
|
||||
public: true,
|
||||
body: file,
|
||||
body: file
|
||||
}
|
||||
args[:content_disposition] = "attachment; filename=\"#{filename}\"" if filename
|
||||
args[:content_type] = content_type if content_type
|
||||
directory.files.create(args)
|
||||
|
||||
get_or_create_directory(s3_bucket).files.create(args)
|
||||
end
|
||||
|
||||
def remove(name)
|
||||
directory.files.destroy(key: name)
|
||||
end
|
||||
|
||||
def directory
|
||||
get_or_create_directory(s3_bucket)
|
||||
def remove(unique_filename)
|
||||
fog = Fog::Storage.new s3_options
|
||||
fog.delete_object(s3_bucket, unique_filename)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
33
lib/freedom_patches/ar_result.rb
Normal file
33
lib/freedom_patches/ar_result.rb
Normal 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
|
8
lib/freedom_patches/arel_patch.rb
Normal file
8
lib/freedom_patches/arel_patch.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
if rails4?
|
||||
# https://github.com/rails/arel/pull/206
|
||||
class Arel::Table
|
||||
def hash
|
||||
@name.hash
|
||||
end
|
||||
end
|
||||
end
|
|
@ -9,6 +9,7 @@ class Guardian
|
|||
def secure_category_ids; []; end
|
||||
def topic_create_allowed_category_ids; []; end
|
||||
def has_trust_level?(level); false; end
|
||||
def email; nil; end
|
||||
end
|
||||
|
||||
def initialize(user=nil)
|
||||
|
@ -36,6 +37,13 @@ class Guardian
|
|||
@user.staff?
|
||||
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?
|
||||
def can_see?(obj)
|
||||
if obj
|
||||
|
@ -89,8 +97,8 @@ class Guardian
|
|||
# You must be an admin to impersonate
|
||||
is_admin? &&
|
||||
|
||||
# You may not impersonate other admins
|
||||
not(target.admin?)
|
||||
# You may not impersonate other admins unless you are a dev
|
||||
(!target.admin? || is_developer?)
|
||||
|
||||
# Additionally, you may not impersonate yourself;
|
||||
# but the two tests for different admin statuses
|
||||
|
@ -229,11 +237,11 @@ class Guardian
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def can_create_topic_on_category?(category)
|
||||
can_create_post?(nil) && (
|
||||
can_create_topic?(nil) && (
|
||||
!category ||
|
||||
Category.topic_create_allowed(self).where(:id => category.id).count == 1
|
||||
)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module Jobs
|
||||
class DashboardStats < Jobs::Scheduled
|
||||
recurrence { minutely(AdminDashboardData.recalculate_interval.minutes) }
|
||||
recurrence { hourly.minute_of_hour(0,30) }
|
||||
|
||||
def execute(args)
|
||||
stats_json = AdminDashboardData.fetch_stats.as_json
|
||||
|
@ -13,4 +13,4 @@ module Jobs
|
|||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,15 +5,14 @@ module Jobs
|
|||
recurrence { daily.hour_of_day(6) }
|
||||
|
||||
def execute(args)
|
||||
target_users.each do |u|
|
||||
Jobs.enqueue(:user_email, type: :digest, user_id: u.id)
|
||||
target_user_ids.each do |user_id|
|
||||
Jobs.enqueue(:user_email, type: :digest, user_id: user_id)
|
||||
end
|
||||
end
|
||||
|
||||
def target_users
|
||||
def target_user_ids
|
||||
# Users who want to receive emails and haven't been emailed in the last day
|
||||
query = User.select(:id)
|
||||
.where(email_digests: true, active: true)
|
||||
query = User.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_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")
|
||||
end
|
||||
|
||||
query
|
||||
query.pluck(:id)
|
||||
end
|
||||
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
module Plugin; end
|
||||
|
||||
class Plugin::Metadata
|
||||
FIELDS = [:name, :about, :version, :authors]
|
||||
FIELDS ||= [:name, :about, :version, :authors]
|
||||
attr_accessor *FIELDS
|
||||
|
||||
def self.parse(text)
|
||||
|
|
|
@ -104,8 +104,10 @@ module PrettyText
|
|||
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); }");
|
||||
|
||||
decorate_context(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/components/utilities.js",
|
||||
"app/assets/javascripts/discourse/components/markdown.js")
|
||||
|
@ -145,6 +147,13 @@ module PrettyText
|
|||
@ctx
|
||||
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)
|
||||
# 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)
|
||||
|
@ -154,9 +163,7 @@ module PrettyText
|
|||
@mutex.synchronize do
|
||||
context = v8
|
||||
# we need to do this to work in a multi site environment, many sites, many settings
|
||||
context.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
|
||||
context.eval("Discourse.BaseUrl = 'http://#{RailsMultisite::ConnectionManagement.current_hostname}';")
|
||||
context.eval("Discourse.getURL = function(url) {return '#{Discourse::base_uri}' + url};")
|
||||
decorate_context(context)
|
||||
context['opts'] = opts || {}
|
||||
context['raw'] = text
|
||||
context.eval('opts["mentionLookup"] = function(u){return helpers.is_username_valid(u);}')
|
||||
|
@ -175,9 +182,7 @@ module PrettyText
|
|||
@mutex.synchronize do
|
||||
v8['avatarTemplate'] = avatar_template
|
||||
v8['size'] = size
|
||||
v8.eval("Discourse.SiteSettings = #{SiteSetting.client_settings_json};")
|
||||
v8.eval("Discourse.CDN = '#{Rails.configuration.action_controller.asset_host}';")
|
||||
v8.eval("Discourse.BaseUrl = '#{RailsMultisite::ConnectionManagement.current_hostname}';")
|
||||
decorate_context(v8)
|
||||
r = v8.eval("Discourse.Utilities.avatarImg({ avatarTemplate: avatarTemplate, size: size });")
|
||||
end
|
||||
r
|
||||
|
|
|
@ -231,30 +231,25 @@ module SiteSettingExtension
|
|||
|
||||
# trivial multi db support, we can optimize this later
|
||||
current[name] = current_value
|
||||
clean_name = name.to_s.sub("?", "")
|
||||
|
||||
setter = ("#{name}=").sub("?","")
|
||||
|
||||
eval "define_singleton_method :#{name} do
|
||||
eval "define_singleton_method :#{clean_name} do
|
||||
c = @@containers[provider.current_site]
|
||||
c = c[name] if c
|
||||
c
|
||||
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)
|
||||
refresh!
|
||||
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)
|
||||
enums[name]
|
||||
end
|
||||
|
|
|
@ -3,14 +3,15 @@ require_dependency 'topic_list'
|
|||
class SuggestedTopicsBuilder
|
||||
|
||||
attr_reader :excluded_topic_ids
|
||||
attr_reader :results
|
||||
|
||||
def initialize(topic)
|
||||
@excluded_topic_ids = [topic.id]
|
||||
@category_id = topic.category_id
|
||||
@results = []
|
||||
end
|
||||
|
||||
def add_results(results)
|
||||
|
||||
def add_results(results, priority=:low)
|
||||
|
||||
# WARNING .blank? will execute an Active Record query
|
||||
return unless results
|
||||
|
@ -23,16 +24,46 @@ class SuggestedTopicsBuilder
|
|||
unless results.empty?
|
||||
# Keep track of the ids we've added
|
||||
@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
|
||||
end
|
||||
end
|
||||
|
||||
def results
|
||||
@results.first(SiteSetting.suggested_topics)
|
||||
end
|
||||
|
||||
def results_left
|
||||
SiteSetting.suggested_topics - @results.size
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
def size
|
||||
|
|
|
@ -5,6 +5,8 @@ class TextSentinel
|
|||
|
||||
attr_accessor :text
|
||||
|
||||
ENTROPY_SCALE ||= 0.7
|
||||
|
||||
def initialize(text, opts=nil)
|
||||
@opts = opts || {}
|
||||
@text = text.to_s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
|
||||
|
@ -15,14 +17,20 @@ class TextSentinel
|
|||
if opts[:private_message]
|
||||
scale_entropy = SiteSetting.min_private_message_post_length.to_f / SiteSetting.min_post_length.to_f
|
||||
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
|
||||
TextSentinel.new(text, min_entropy: entropy)
|
||||
end
|
||||
|
||||
def self.title_sentinel(text)
|
||||
TextSentinel.new(text,
|
||||
min_entropy: SiteSetting.title_min_entropy,
|
||||
max_word_length: SiteSetting.max_word_length)
|
||||
entropy = if SiteSetting.min_topic_title_length > SiteSetting.title_min_entropy
|
||||
SiteSetting.title_min_entropy
|
||||
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
|
||||
|
||||
# Entropy is a number of how many unique characters the string needs.
|
||||
|
|
|
@ -87,10 +87,10 @@ class TopicQuery
|
|||
|
||||
# When logged in we start with different results
|
||||
if @user
|
||||
builder.add_results(unread_results(topic: topic, per_page: builder.results_left))
|
||||
builder.add_results(new_results(per_page: builder.results_left)) unless builder.full?
|
||||
builder.add_results(unread_results(topic: topic, per_page: builder.results_left), :high)
|
||||
builder.add_results(new_results(topic: topic, per_page: builder.category_results_left), :high) unless builder.category_full?
|
||||
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)
|
||||
end
|
||||
|
@ -146,6 +146,11 @@ class TopicQuery
|
|||
TopicList.new(:private_messages, user, list)
|
||||
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
|
||||
create_list(:uncategorized, unordered: true) do |list|
|
||||
|
|
|
@ -84,12 +84,13 @@ class TopicView
|
|||
|
||||
def summary
|
||||
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
|
||||
end
|
||||
|
||||
def image_url
|
||||
return nil if desired_post.blank?
|
||||
desired_post.user.small_avatar_url
|
||||
desired_post.user.try(:small_avatar_url)
|
||||
end
|
||||
|
||||
def filter_posts(opts = {})
|
||||
|
@ -256,7 +257,7 @@ class TopicView
|
|||
|
||||
def setup_filtered_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.where('posts.post_type <> ?', Post.types[:moderator_action]) if @best.present?
|
||||
return unless @username_filters.present?
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue