From 73489b652e2aea5dfb6c391168e13bc7dcdb395c Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Tue, 27 Aug 2013 12:24:17 -0400
Subject: [PATCH 01/84] FIX: Allow intra-word underscores.

---
 .../dialects/bold_italics_dialect.js          | 41 ++++++----
 .../{markdown.js => better_markdown.js}       | 78 ++++---------------
 lib/pretty_text.rb                            |  2 +-
 test/javascripts/components/markdown_test.js  |  6 ++
 4 files changed, 48 insertions(+), 79 deletions(-)
 rename app/assets/javascripts/external/{markdown.js => better_markdown.js} (95%)

diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
index 704b6a164..b4fadffb7 100644
--- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
@@ -10,23 +10,34 @@ 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);
+  var inlineBuilder = function(symbol, tag, surround) {
+    return function(text, match, prev) {
+      if (prev && (prev.length > 0)) {
+        var last = prev[prev.length - 1];
+        if (typeof last === "string" && (!last.match(/\W$/))) { return; }
+      }
 
-    if (m) {
-      return [m[0].length, ['strong', ['em'].concat(this.processInline(m[1]))]];
-    }
+      var regExp = new RegExp("^\\" + symbol + "([^\\" + symbol + "]+)" + "\\" + symbol, "igm"),
+          m = regExp.exec(text);
+
+      if (m) {
+
+        var contents = [tag].concat(this.processInline(m[1]));
+        if (surround) {
+          contents = [surround, contents];
+        }
+
+        return [m[0].length, contents];
+      }
+    };
   };
 
+  dialect.inline['***'] = inlineBuilder('**', 'em', 'strong');
+  dialect.inline['**'] = inlineBuilder('**', 'strong');
+  dialect.inline['*'] = inlineBuilder('*', 'em');
+  dialect.inline['_'] = inlineBuilder('_', 'em');
+
+
+
 });
diff --git a/app/assets/javascripts/external/markdown.js b/app/assets/javascripts/external/better_markdown.js
similarity index 95%
rename from app/assets/javascripts/external/markdown.js
rename to app/assets/javascripts/external/better_markdown.js
index e11d71e6c..29e708fb8 100644
--- a/app/assets/javascripts/external/markdown.js
+++ b/app/assets/javascripts/external/better_markdown.js
@@ -1,3 +1,18 @@
+/*
+  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.
+
+  * 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
@@ -1004,69 +1019,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 = [];
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index 48fe7fdf2..eecb5ca7e 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -105,7 +105,7 @@ module PrettyText
     ctx.eval("var I18n = {}; I18n.t = function(a,b){ return helpers.t(a,b); }");
 
     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")
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index f4714baee..2370f0b0a 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -17,7 +17,13 @@ var cookedOptions = function(input, opts, expected, text) {
 
 test("basic cooking", function() {
   cooked("hello", "<p>hello</p>", "surrounds text with paragraphs");
+  cooked("**evil**", "<p><strong>evil</strong></p>", "it bolds text.");
+  cooked("*trout*", "<p><em>trout</em></p>", "it italicizes text.");
+  cooked("_trout_", "<p><em>trout</em></p>", "it italicizes text.");
   cooked("***hello***", "<p><strong><em>hello</em></strong></p>", "it can do bold and italics at once.");
+  cooked("word_with_underscores", "<p>word_with_underscores</p>", "it doesn't do intraword italics");
+  cooked("hello \\*evil\\*", "<p>hello *evil*</p>", "it supports escaping of asterisks");
+  cooked("hello \\_evil\\_", "<p>hello _evil_</p>", "it supports escaping of italics");
 });
 
 test("Traditional Line Breaks", function() {

From 96772af35b4640f4592d6c837f839c5809cb7555 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Tue, 27 Aug 2013 23:01:35 +0200
Subject: [PATCH 02/84] FIX: avatar thumbnail won't save after upload

---
 .../controllers/avatar_selector_controller.js | 14 +++++--
 .../javascripts/discourse/models/user.js      | 23 ++++++----
 .../discourse/routes/preferences_routes.js    | 42 ++++++++-----------
 .../modal/avatar_selector.js.handlebars       |  4 +-
 .../avatar_selector_controller_test.js        | 28 +++++++++++++
 5 files changed, 73 insertions(+), 38 deletions(-)
 create mode 100644 test/javascripts/controllers/avatar_selector_controller_test.js

diff --git a/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js b/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js
index 861853ca4..91ce9648b 100644
--- a/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js
+++ b/app/assets/javascripts/discourse/controllers/avatar_selector_controller.js
@@ -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")
 });
diff --git a/app/assets/javascripts/discourse/models/user.js b/app/assets/javascripts/discourse/models/user.js
index 4abb65886..c367dd875 100644
--- a/app/assets/javascripts/discourse/models/user.js
+++ b/app/assets/javascripts/discourse/models/user.js
@@ -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 }
+    });
   }
 
 });
diff --git a/app/assets/javascripts/discourse/routes/preferences_routes.js b/app/assets/javascripts/discourse/routes/preferences_routes.js
index e623cf3c4..e337a43a8 100644
--- a/app/assets/javascripts/discourse/routes/preferences_routes.js
+++ b/app/assets/javascripts/discourse/routes/preferences_routes.js
@@ -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'));
     }
   }
 });
diff --git a/app/assets/javascripts/discourse/templates/modal/avatar_selector.js.handlebars b/app/assets/javascripts/discourse/templates/modal/avatar_selector.js.handlebars
index 4548c091b..63fcf8e34 100644
--- a/app/assets/javascripts/discourse/templates/modal/avatar_selector.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/modal/avatar_selector.js.handlebars
@@ -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}}
diff --git a/test/javascripts/controllers/avatar_selector_controller_test.js b/test/javascripts/controllers/avatar_selector_controller_test.js
new file mode 100644
index 000000000..7fd84a30f
--- /dev/null
+++ b/test/javascripts/controllers/avatar_selector_controller_test.js
@@ -0,0 +1,28 @@
+var avatarSelector = Em.Object.create({
+  use_uploaded_avatar: false,
+  gravatar_template: "//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon",
+  uploaded_avatar_template: "//cdn.discourse.org/uploads/meta_discourse/avatars/093/607/185cff113e/{size}.jpg"
+});
+
+module("Discourse.AvatarSelectorController");
+
+test("avatarTemplate", function() {
+  var avatarSelectorController = controllerFor("avatarSelector");
+  avatarSelectorController.setProperties(avatarSelector);
+
+  equal(avatarSelectorController.get("avatarTemplate"),
+        avatarSelector.get("gravatar_template"),
+        "we are using gravatar by default");
+
+  avatarSelectorController.useUploadedAvatar();
+
+  equal(avatarSelectorController.get("avatarTemplate"),
+        avatarSelector.get("uploaded_avatar_template"),
+        "calling useUploadedAvatar switches to using the uploaded avatar");
+
+  avatarSelectorController.useGravatar();
+
+  equal(avatarSelectorController.get("avatarTemplate"),
+        avatarSelector.get("gravatar_template"),
+       "calling useGravatar switches to using gravatar");
+});

From 8f94760cd40b232b328555151a9b40a6abfa45a2 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Tue, 27 Aug 2013 12:52:00 -0400
Subject: [PATCH 03/84] Better API for adding on to our Dialect

---
 .../discourse/dialects/autolink_dialect.js    |  50 ++----
 .../discourse/dialects/bbcode_dialect.js      | 166 +++++++++++-------
 .../dialects/bold_italics_dialect.js          |  60 +++----
 .../javascripts/discourse/dialects/dialect.js | 134 ++++++++++----
 .../discourse/dialects/mention_dialect.js     |  53 ++----
 .../discourse/dialects/newline_dialect.js     |  39 ++--
 test/javascripts/components/bbcode_test.js    |   5 +-
 7 files changed, 265 insertions(+), 242 deletions(-)

diff --git a/app/assets/javascripts/discourse/dialects/autolink_dialect.js b/app/assets/javascripts/discourse/dialects/autolink_dialect.js
index 4846abd68..cd00e3036 100644
--- a/app/assets/javascripts/discourse/dialects/autolink_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/autolink_dialect.js
@@ -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: /(^|\s)((?: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[2],
+        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]];
-    }
-
-  };
-
-
-});
\ No newline at end of file
+Discourse.Dialect.inlineRegexp(_.merge({start: 'http'}, urlReplacerArgs));
+Discourse.Dialect.inlineRegexp(_.merge({start: 'www'}, urlReplacerArgs));
diff --git a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
index 8f2c9fb30..7295d8de5 100644
--- a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
@@ -1,76 +1,112 @@
 /**
-  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
 **/
+function replaceBBCode(tag, emitter) {
+  Discourse.Dialect.inlineReplace({
+    start: "[" + tag + "]",
+    stop: "[/" + tag + "]",
+    emitter: emitter
+  });
+}
+
+/**
+  Creates a BBCode handler that accepts parameters. Passes them to the emitter.
+
+  @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.inlineReplace({
+    start: "[" + tag + "=",
+    stop: "[/" + tag + "]",
+    rawContents: true,
+    emitter: function(contents) {
+      var regexp = /^([^\]]+)\](.*)$/,
+          m = regexp.exec(contents);
+
+      if (m) { return emitter.call(this, m[1], m[2]); }
+    }
+  });
+}
+
+/**
+  Creates a BBCode handler that accepts parameters. Passes them to the emitter.
+  Processes the inside recursively so it can be nested.
+
+  @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));
+  });
+}
+
+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); });
+
+replaceBBCode('ul', function(contents) { return ['ul'].concat(contents); });
+replaceBBCode('ol', function(contents) { return ['ol'].concat(contents); });
+replaceBBCode('li', function(contents) { return ['li'].concat(contents); });
+
+replaceBBCode('spoiler', function(contents) { return ['span', {'class': 'spoiler'}].concat(contents); });
+
+Discourse.Dialect.inlineReplace({
+  start: '[img]',
+  stop: '[/img]',
+  rawContents: true,
+  emitter: function(contents) { return ['img', {href: contents}]; }
+});
+
+Discourse.Dialect.inlineReplace({
+  start: '[email]',
+  stop: '[/email]',
+  rawContents: true,
+  emitter: function(contents) { return ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]; }
+});
+
+Discourse.Dialect.inlineReplace({
+  start: '[url]',
+  stop: '[/url]',
+  rawContents: true,
+  emitter: function(contents) { return ['a', {href: contents, 'data-bbcode': true}, contents]; }
+});
+
+
+replaceBBCodeParamsRaw("url", function(param, contents) {
+  return ['a', {href: param, 'data-bbcode': true}, contents];
+});
+
+replaceBBCodeParamsRaw("email", function(param, contents) {
+  return ['a', {href: "mailto:" + param, 'data-bbcode': true}, contents];
+});
+
+replaceBBCodeParams("size", function(param, contents) {
+  return ['span', {'class': "bbcode-size-" + param}].concat(contents);
+});
+
+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);
+  }
+});
+
 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]));
-    });
-  });
-
-  dialect.inline["[img]"] = createBBCode('img', function(m) {
-    return ['img', {href: m[2]}];
-  });
-
-  dialect.inline["[email]"] = createBBCode('email', function(m) {
-    return ['a', {href: "mailto:" + m[2], 'data-bbcode': true}, m[2]];
-  });
-
-  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]))];
-    }
-  };
-
   /**
     Support BBCode [code] blocks
 
diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
index b4fadffb7..890b380d8 100644
--- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
@@ -1,43 +1,25 @@
 /**
-  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) {
-
-  var dialect = event.dialect,
-      MD = event.MD;
-
-
-  var inlineBuilder = function(symbol, tag, surround) {
-    return function(text, match, prev) {
-      if (prev && (prev.length > 0)) {
-        var last = prev[prev.length - 1];
-        if (typeof last === "string" && (!last.match(/\W$/))) { return; }
-      }
-
-      var regExp = new RegExp("^\\" + symbol + "([^\\" + symbol + "]+)" + "\\" + symbol, "igm"),
-          m = regExp.exec(text);
-
-      if (m) {
-
-        var contents = [tag].concat(this.processInline(m[1]));
-        if (surround) {
-          contents = [surround, contents];
-        }
-
-        return [m[0].length, contents];
-      }
-    };
-  };
-
-  dialect.inline['***'] = inlineBuilder('**', 'em', 'strong');
-  dialect.inline['**'] = inlineBuilder('**', 'strong');
-  dialect.inline['*'] = inlineBuilder('*', 'em');
-  dialect.inline['_'] = inlineBuilder('_', 'em');
-
-
 
+// Support for simultaneous bold and italics
+Discourse.Dialect.inlineReplace({
+  between: '***',
+  wordBoundary: true,
+  emitter: function(contents) { return ['strong', ['em'].concat(contents)]; }
 });
+
+// Builds a common markdown replacer
+var replaceMarkdown = function(match, tag) {
+  Discourse.Dialect.inlineReplace({
+    between: match,
+    wordBoundary: true,
+    emitter: function(contents) { return [tag].concat(contents) }
+  });
+};
+
+replaceMarkdown('**', 'strong');
+replaceMarkdown('*', 'em');
+replaceMarkdown('_', 'em');
+
diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index 6d2d50e53..d5befd231 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -43,51 +43,67 @@
 **/
 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 +126,51 @@ Discourse.Dialect = {
     dialect.options = opts;
     var tree = parser.toHTMLTree(text, 'Discourse');
     return parser.renderJsonML(parseTree(tree));
+  },
+
+  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];
+        }
+      }
+    };
+  },
+
+  inlineReplace: 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 + startLength + 1, contents];
+      }
+    };
+
   }
+
 };
 
 RSVP.EventTarget.mixin(Discourse.Dialect);
+
+
diff --git a/app/assets/javascripts/discourse/dialects/mention_dialect.js b/app/assets/javascripts/discourse/dialects/mention_dialect.js
index 914e52dd7..66dc10fb5 100644
--- a/app/assets/javascripts/discourse/dialects/mention_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/mention_dialect.js
@@ -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]];
-      }
-    }
-
-  };
-
-});
+  }
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/dialects/newline_dialect.js b/app/assets/javascripts/discourse/dialects/newline_dialect.js
index 5859a3ed5..3653bdcdb 100644
--- a/app/assets/javascripts/discourse/dialects/newline_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/newline_dialect.js
@@ -10,33 +10,28 @@ Discourse.Dialect.on("parseNode", function(event) {
       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) || (node.length < 1)) { return; }
 
-    if (node.length > 1) {
-      for (var j=1; j<node.length; j++) {
-        var textContent = node[j];
+  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);
+    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);
         }
       }
     }
   }
-});
\ No newline at end of file
+
+});
diff --git a/test/javascripts/components/bbcode_test.js b/test/javascripts/components/bbcode_test.js
index 08a93df02..71416e267 100644
--- a/test/javascripts/components/bbcode_test.js
+++ b/test/javascripts/components/bbcode_test.js
@@ -17,6 +17,9 @@ test('basic bbcode', function() {
   format("[img]http://eviltrout.com/eviltrout.png[/img]", "<img src=\"http://eviltrout.com/eviltrout.png\"/>", "links images");
   format("[url]http://bettercallsaul.com[/url]", "<a href=\"http://bettercallsaul.com\">http://bettercallsaul.com</a>", "supports [url] without a title");
   format("[email]eviltrout@mailinator.com[/email]", "<a href=\"mailto:eviltrout@mailinator.com\">eviltrout@mailinator.com</a>", "supports [email] without a title");
+  format("[b]evil [i]trout[/i][/b]",
+         "<span class=\"bbcode-b\">evil <span class=\"bbcode-i\">trout</span></span>",
+         "allows embedding of tags");
 });
 
 test('lists', function() {
@@ -28,7 +31,7 @@ test('color', function() {
   format("[color=#00f]blue[/color]", "<span style=\"color: #00f\">blue</span>", "supports [color=] with a short hex value");
   format("[color=#ffff00]yellow[/color]", "<span style=\"color: #ffff00\">yellow</span>", "supports [color=] with a long hex value");
   format("[color=red]red[/color]", "<span style=\"color: red\">red</span>", "supports [color=] with an html color");
-  format("[color=javascript:alert('wat')]noop[/color]", "noop", "it performs a noop on invalid input");
+  format("[color=javascript:alert('wat')]noop[/color]", "<span>noop</span>", "it performs a noop on invalid input");
 });
 
 test('tags with arguments', function() {

From dfa5a8a83f25c9008c8971c925676dd16352d37b Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 28 Aug 2013 10:42:58 +1000
Subject: [PATCH 04/84] annotate models

---
 app/models/oauth2_user_info.rb | 19 +++++++++++++++++++
 app/models/plugin_store_row.rb | 16 ++++++++++++++++
 app/models/screened_email.rb   | 20 ++++++++++++++++++++
 app/models/screened_url.rb     | 21 +++++++++++++++++++++
 app/models/staff_action_log.rb |  4 ++++
 app/models/topic_link.rb       |  4 ++--
 app/models/topic_link_click.rb |  2 +-
 app/models/user.rb             |  5 +++--
 8 files changed, 86 insertions(+), 5 deletions(-)

diff --git a/app/models/oauth2_user_info.rb b/app/models/oauth2_user_info.rb
index 048f05b78..ca39927a2 100644
--- a/app/models/oauth2_user_info.rb
+++ b/app/models/oauth2_user_info.rb
@@ -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
+#
+
diff --git a/app/models/plugin_store_row.rb b/app/models/plugin_store_row.rb
index a18cd005b..4928f4adb 100644
--- a/app/models/plugin_store_row.rb
+++ b/app/models/plugin_store_row.rb
@@ -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
+#
+
diff --git a/app/models/screened_email.rb b/app/models/screened_email.rb
index c93d6a75a..0541cb3dd 100644
--- a/app/models/screened_email.rb
+++ b/app/models/screened_email.rb
@@ -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)
+#
+
diff --git a/app/models/screened_url.rb b/app/models/screened_url.rb
index abccdd7ac..8603b397c 100644
--- a/app/models/screened_url.rb
+++ b/app/models/screened_url.rb
@@ -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
+#
+
diff --git a/app/models/staff_action_log.rb b/app/models/staff_action_log.rb
index bee6d61e0..a7eeb6091 100644
--- a/app/models/staff_action_log.rb
+++ b/app/models/staff_action_log.rb
@@ -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)
 #
 
diff --git a/app/models/topic_link.rb b/app/models/topic_link.rb
index 3279af882..054ae273d 100644
--- a/app/models/topic_link.rb
+++ b/app/models/topic_link.rb
@@ -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
 #
 
diff --git a/app/models/topic_link_click.rb b/app/models/topic_link_click.rb
index 68a03d883..05f53b5a9 100644
--- a/app/models/topic_link_click.rb
+++ b/app/models/topic_link_click.rb
@@ -53,6 +53,6 @@ end
 #
 # Indexes
 #
-#  index_forum_thread_link_clicks_on_forum_thread_link_id  (topic_link_id)
+#  by_link  (topic_link_id)
 #
 
diff --git a/app/models/user.rb b/app/models/user.rb
index e45632c06..58c1411b9 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -647,7 +647,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 +657,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 +690,4 @@ end
 #  index_users_on_username        (username) UNIQUE
 #  index_users_on_username_lower  (username_lower) UNIQUE
 #
+

From b6028c39d7a28757e2a6ba09277306421ba6ea17 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 28 Aug 2013 10:43:26 +1000
Subject: [PATCH 05/84] user simulator was bust

---
 script/user_simulator.rb | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/script/user_simulator.rb b/script/user_simulator.rb
index b03d77e21..5e7eab7ae 100644
--- a/script/user_simulator.rb
+++ b/script/user_simulator.rb
@@ -50,11 +50,11 @@ puts "Simulating activity for user id #{user.id}: #{user.name}"
 while true
   puts "Creating a random topic"
 
-  category = Category.where(secure: false).order('random()').first
+  category = Category.where(read_restricted: false).order('random()').first
   PostCreator.create(user, raw: sentence, title: sentence[0..50].strip, category:  category.name)
 
   puts "creating random reply"
   PostCreator.create(user, raw: sentence, topic_id: last_topics.sample)
 
-  sleep 10
+  sleep 2
 end

From c0f610daf6663a29814775c4973f4274e4d974ee Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 28 Aug 2013 10:44:13 +1000
Subject: [PATCH 06/84] digest interfaces returning empty objects (except for
 id) interface needed correction

---
 lib/jobs/enqueue_digest_emails.rb             | 11 +++++-----
 .../jobs/enqueue_digest_emails_spec.rb        | 20 +++++++++----------
 2 files changed, 15 insertions(+), 16 deletions(-)

diff --git a/lib/jobs/enqueue_digest_emails.rb b/lib/jobs/enqueue_digest_emails.rb
index a19e35119..651156ff6 100644
--- a/lib/jobs/enqueue_digest_emails.rb
+++ b/lib/jobs/enqueue_digest_emails.rb
@@ -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
diff --git a/spec/components/jobs/enqueue_digest_emails_spec.rb b/spec/components/jobs/enqueue_digest_emails_spec.rb
index 15900f04a..4825ad1ac 100644
--- a/spec/components/jobs/enqueue_digest_emails_spec.rb
+++ b/spec/components/jobs/enqueue_digest_emails_spec.rb
@@ -10,33 +10,33 @@ describe Jobs::EnqueueDigestEmails do
       let!(:user_no_digests) { Fabricate(:active_user, email_digests: false, last_emailed_at: 8.days.ago, last_seen_at: 10.days.ago) }
 
       it "doesn't return users with email disabled" do
-        Jobs::EnqueueDigestEmails.new.target_users.include?(user_no_digests).should be_false
+        Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_no_digests.id).should be_false
       end
     end
 
     context 'unapproved users' do
       Given!(:unapproved_user) { Fabricate(:active_user, approved: false, last_emailed_at: 8.days.ago, last_seen_at: 10.days.ago) }
       When { SiteSetting.stubs(:must_approve_users?).returns(true) }
-      Then { expect(Jobs::EnqueueDigestEmails.new.target_users.include?(unapproved_user)).to eq(false) }
+      Then { expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq(false) }
 
       # As a moderator
       And { unapproved_user.update_column(:moderator, true) }
-      And { expect(Jobs::EnqueueDigestEmails.new.target_users.include?(unapproved_user)).to eq(true) }
+      And { expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq(true) }
 
       # As an admin
       And { unapproved_user.update_attributes(admin: true, moderator: false) }
-      And { expect(Jobs::EnqueueDigestEmails.new.target_users.include?(unapproved_user)).to eq(true) }
+      And { expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq(true) }
 
       # As an approved user
       And { unapproved_user.update_attributes(admin: false, moderator: false, approved: true ) }
-      And { expect(Jobs::EnqueueDigestEmails.new.target_users.include?(unapproved_user)).to eq(true) }
+      And { expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(unapproved_user.id)).to eq(true) }
     end
 
     context 'recently emailed' do
       let!(:user_emailed_recently) { Fabricate(:active_user, last_emailed_at: 6.days.ago) }
 
       it "doesn't return users who have been emailed recently" do
-        Jobs::EnqueueDigestEmails.new.target_users.include?(user_emailed_recently).should be_false
+        Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_emailed_recently.id).should be_false
       end
     end
 
@@ -44,7 +44,7 @@ describe Jobs::EnqueueDigestEmails do
       let!(:inactive_user) { Fabricate(:user) }
 
       it "doesn't return users who have been emailed recently" do
-        Jobs::EnqueueDigestEmails.new.target_users.include?(inactive_user).should be_false
+        Jobs::EnqueueDigestEmails.new.target_user_ids.include?(inactive_user.id).should be_false
       end
     end
 
@@ -53,7 +53,7 @@ describe Jobs::EnqueueDigestEmails do
       let!(:user_visited_today) { Fabricate(:active_user, last_seen_at: 6.days.ago) }
 
       it "doesn't return users who have been emailed recently" do
-        Jobs::EnqueueDigestEmails.new.target_users.include?(user_visited_today).should be_false
+        Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_visited_today.id).should be_false
       end
     end
 
@@ -62,7 +62,7 @@ describe Jobs::EnqueueDigestEmails do
       let!(:user) { Fabricate(:active_user) }
 
       it "returns the user" do
-        Jobs::EnqueueDigestEmails.new.target_users.should == [user]
+        Jobs::EnqueueDigestEmails.new.target_user_ids.should == [user.id]
       end
     end
 
@@ -73,7 +73,7 @@ describe Jobs::EnqueueDigestEmails do
     let(:user) { Fabricate(:user) }
 
     before do
-      Jobs::EnqueueDigestEmails.any_instance.expects(:target_users).returns([user])
+      Jobs::EnqueueDigestEmails.any_instance.expects(:target_user_ids).returns([user.id])
     end
 
     it "enqueues the digest email job" do

From 28466eb5b29c70437d08f5beb4a9f400b5ef40c2 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 28 Aug 2013 10:51:49 +1000
Subject: [PATCH 07/84] group the "suggested topics" by category correctly.

in the past new topics were not prioritizing current category and
new topics in a category were not being inserted before other unread topics
in other categories
---
 lib/suggested_topics_builder.rb               | 37 +++++++++++++++++--
 lib/topic_query.rb                            |  6 +--
 .../suggested_topics_builder_spec.rb          | 34 +++++++++++++++--
 3 files changed, 68 insertions(+), 9 deletions(-)

diff --git a/lib/suggested_topics_builder.rb b/lib/suggested_topics_builder.rb
index 35ca744c0..26a83dcff 100644
--- a/lib/suggested_topics_builder.rb
+++ b/lib/suggested_topics_builder.rb
@@ -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
diff --git a/lib/topic_query.rb b/lib/topic_query.rb
index 6bd63830a..766fb8178 100644
--- a/lib/topic_query.rb
+++ b/lib/topic_query.rb
@@ -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
diff --git a/spec/components/suggested_topics_builder_spec.rb b/spec/components/suggested_topics_builder_spec.rb
index ed35220ab..71187f709 100644
--- a/spec/components/suggested_topics_builder_spec.rb
+++ b/spec/components/suggested_topics_builder_spec.rb
@@ -3,14 +3,42 @@ require 'suggested_topics_builder'
 
 describe SuggestedTopicsBuilder do
 
-  let!(:topic) { Fabricate(:topic) }
-
-  let!(:builder) { SuggestedTopicsBuilder.new(topic) }
+  let(:topic) { Fabricate(:topic) }
+  let(:builder) { SuggestedTopicsBuilder.new(topic) }
 
   before do
     SiteSetting.stubs(:suggested_topics).returns(5)
   end
 
+  context "splicing category results" do
+
+    def fake_topic(topic_id, category_id)
+      build(:topic, id: topic_id, category_id: category_id)
+    end
+
+    let(:builder) do
+      SuggestedTopicsBuilder.new(fake_topic(1,1))
+    end
+
+    it "prioritizes category correctly" do
+      builder.splice_results([fake_topic(2,2)], :high)
+      builder.splice_results([fake_topic(3,1)], :high)
+      builder.splice_results([fake_topic(4,1)], :high)
+
+      builder.results.map(&:id).should == [3,4,2]
+
+      # we have 2 items in category 1
+      builder.category_results_left.should == 3
+    end
+
+    it "inserts using default approach for non high priority" do
+      builder.splice_results([fake_topic(2,2)], :high)
+      builder.splice_results([fake_topic(3,1)], :low)
+
+      builder.results.map(&:id).should == [2,3]
+    end
+  end
+
   it "has the correct defaults" do
     builder.excluded_topic_ids.include?(topic.id).should be_true
     builder.results_left.should == 5

From 61281a3c81343e49d43933d784f3e48017a2d286 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 28 Aug 2013 17:18:31 +1000
Subject: [PATCH 08/84] invite only forums had very wonky logic, invited users
 were not being activated, invite_only forums were still registering users

---
 .../discourse/controllers/login_controller.js |  5 ++++
 .../users/omniauth_callbacks_controller.rb    | 12 +++++---
 app/models/invite_redeemer.rb                 | 23 +++++++++++---
 app/models/user.rb                            | 30 +------------------
 config/locales/client.en.yml                  |  1 +
 lib/auth/result.rb                            |  7 +++--
 lib/discourse_hub.rb                          | 12 ++++++++
 spec/models/invite_redeemer_spec.rb           | 14 +++++++++
 spec/models/user_spec.rb                      |  9 ------
 9 files changed, 65 insertions(+), 48 deletions(-)
 create mode 100644 spec/models/invite_redeemer_spec.rb

diff --git a/app/assets/javascripts/discourse/controllers/login_controller.js b/app/assets/javascripts/discourse/controllers/login_controller.js
index 3cdbf674f..58664f6ff 100644
--- a/app/assets/javascripts/discourse/controllers/login_controller.js
+++ b/app/assets/javascripts/discourse/controllers/login_controller.js
@@ -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);
diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
index 5118378ac..42fc78f3b 100644
--- a/app/controllers/users/omniauth_callbacks_controller.rb
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -37,9 +37,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 +91,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
diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb
index 91fb3aefa..45623474b 100644
--- a/app/models/invite_redeemer.rb
+++ b/app/models/invite_redeemer.rb
@@ -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|
diff --git a/app/models/user.rb b/app/models/user.rb
index 58c1411b9..fb2870798 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -96,23 +96,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 +137,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 +595,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
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 07a961884..204b4d13f 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -396,6 +396,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."
diff --git a/lib/auth/result.rb b/lib/auth/result.rb
index d76f3e045..26e067dce 100644
--- a/lib/auth/result.rb
+++ b/lib/auth/result.rb
@@ -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,
diff --git a/lib/discourse_hub.rb b/lib/discourse_hub.rb
index 60ec2fea4..eb0e85ddc 100644
--- a/lib/discourse_hub.rb
+++ b/lib/discourse_hub.rb
@@ -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
diff --git a/spec/models/invite_redeemer_spec.rb b/spec/models/invite_redeemer_spec.rb
new file mode 100644
index 000000000..4d1d0bb96
--- /dev/null
+++ b/spec/models/invite_redeemer_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe InviteRedeemer do
+
+  describe '#create_for_email' do
+    let(:user) { InviteRedeemer.create_user_for_email('walter.white@email.com') }
+    it "should be created correctly" do
+      user.username.should == 'walter_white'
+      user.name.should == 'walter_white'
+      user.should be_active
+      user.email.should == 'walter.white@email.com'
+    end
+  end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ec844be63..757cd636e 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -731,15 +731,6 @@ describe User do
     end
   end
 
-  describe '#create_for_email' do
-    let(:subject) { User.create_for_email('walter.white@email.com') }
-    it { should be_present }
-    its(:username) { should == 'walter_white' }
-    its(:name) { should == 'walter_white'}
-    it { should_not be_active }
-    its(:email) { should == 'walter.white@email.com' }
-  end
-
   describe 'email_confirmed?' do
     let(:user) { Fabricate(:user) }
 

From 3ed037940f464bc1ab32c0751dfe7df476ac31cb Mon Sep 17 00:00:00 2001
From: Einar Jonsson <einar@codelette.com>
Date: Wed, 28 Aug 2013 11:25:57 +0000
Subject: [PATCH 09/84] Replaced delegator methods with Forwardable
 def_delegator in Topic

---
 app/models/topic.rb | 40 ++++++++++------------------------------
 1 file changed, 10 insertions(+), 30 deletions(-)

diff --git a/app/models/topic.rb b/app/models/topic.rb
index 5fe6a17bd..f297edbe5 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -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!, :notifiy_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)

From ee96fabcba3c215fc11840130f9e74f4de1fe041 Mon Sep 17 00:00:00 2001
From: Emili Parreno <emili@eparreno.com>
Date: Wed, 28 Aug 2013 14:32:51 +0200
Subject: [PATCH 10/84] Allow CAS authentication

---
 app/controllers/users/omniauth_callbacks_controller.rb | 3 ++-
 lib/auth.rb                                            | 1 +
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb
index 42fc78f3b..2be789910 100644
--- a/app/controllers/users/omniauth_callbacks_controller.rb
+++ b/app/controllers/users/omniauth_callbacks_controller.rb
@@ -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
diff --git a/lib/auth.rb b/lib/auth.rb
index ead065a5a..910a5863a 100644
--- a/lib/auth.rb
+++ b/lib/auth.rb
@@ -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'

From f611a5d898ff5338f9291bb784e18ac3a9656a1a Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Wed, 28 Aug 2013 11:04:28 -0400
Subject: [PATCH 11/84] If min entropy setting is greater than min post/body
 length setting, then use a sensible min entropy value instead

---
 lib/text_sentinel.rb                  | 14 +++++++++++---
 spec/components/text_sentinel_spec.rb | 28 ++++++++++++++++++++++++++-
 2 files changed, 38 insertions(+), 4 deletions(-)

diff --git a/lib/text_sentinel.rb b/lib/text_sentinel.rb
index 5d7d8ac79..b16e76132 100644
--- a/lib/text_sentinel.rb
+++ b/lib/text_sentinel.rb
@@ -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.
diff --git a/spec/components/text_sentinel_spec.rb b/spec/components/text_sentinel_spec.rb
index 15275e25a..8c3ca4bc4 100644
--- a/spec/components/text_sentinel_spec.rb
+++ b/spec/components/text_sentinel_spec.rb
@@ -90,7 +90,7 @@ describe TextSentinel do
       TextSentinel.new("jfewjfoejwfojeojfoejofjeo3" * 5, max_word_length: 30).should_not be_valid
     end
 
-    it "doesn't except junk symbols as a string" do
+    it "doesn't accept junk symbols as a string" do
       TextSentinel.new("[[[").should_not be_valid
       TextSentinel.new("<<<").should_not be_valid
       TextSentinel.new("{{$!").should_not be_valid
@@ -98,4 +98,30 @@ describe TextSentinel do
 
   end
 
+  context 'title_sentinel' do
+
+    it "uses a sensible min entropy value when min title length is less than title_min_entropy" do
+      SiteSetting.stubs(:min_topic_title_length).returns(3)
+      SiteSetting.stubs(:title_min_entropy).returns(10)
+      TextSentinel.title_sentinel('Hey').should be_valid
+    end
+
+  end
+
+  context 'body_sentinel' do
+
+    it "uses a sensible min entropy value when min body length is less than min entropy" do
+      SiteSetting.stubs(:min_post_length).returns(3)
+      SiteSetting.stubs(:body_min_entropy).returns(7)
+      TextSentinel.body_sentinel('Yup').should be_valid
+    end
+
+    it "uses a sensible min entropy value when min pm body length is less than min entropy" do
+      SiteSetting.stubs(:min_post_length).returns(5)
+      SiteSetting.stubs(:min_private_message_post_length).returns(3)
+      SiteSetting.stubs(:body_min_entropy).returns(7)
+      TextSentinel.body_sentinel('Lol', private_message: true).should be_valid
+    end
+  end
+
 end

From f7ad80ff7b239d68760a38c9e8a08d2c7b48968a Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 28 Aug 2013 11:14:06 -0400
Subject: [PATCH 12/84] A couple of important markdown fixes.

---
 .../javascripts/discourse/dialects/bold_italics_dialect.js      | 1 +
 app/assets/javascripts/discourse/dialects/dialect.js            | 2 +-
 test/javascripts/components/markdown_test.js                    | 2 ++
 3 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
index 890b380d8..eb90914ae 100644
--- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
@@ -20,6 +20,7 @@ var replaceMarkdown = function(match, tag) {
 };
 
 replaceMarkdown('**', 'strong');
+replaceMarkdown('__', 'strong');
 replaceMarkdown('*', 'em');
 replaceMarkdown('_', 'em');
 
diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index d5befd231..e4202f462 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -163,7 +163,7 @@ Discourse.Dialect = {
 
       var contents = args.emitter.call(this, between);
       if (contents) {
-        return [endPos + startLength + 1, contents];
+        return [endPos+stop.length, contents];
       }
     };
 
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index 2370f0b0a..3450e457e 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -18,12 +18,14 @@ var cookedOptions = function(input, opts, expected, text) {
 test("basic cooking", function() {
   cooked("hello", "<p>hello</p>", "surrounds text with paragraphs");
   cooked("**evil**", "<p><strong>evil</strong></p>", "it bolds text.");
+  cooked("__bold__", "<p><strong>bold</strong></p>", "it bolds text.");
   cooked("*trout*", "<p><em>trout</em></p>", "it italicizes text.");
   cooked("_trout_", "<p><em>trout</em></p>", "it italicizes text.");
   cooked("***hello***", "<p><strong><em>hello</em></strong></p>", "it can do bold and italics at once.");
   cooked("word_with_underscores", "<p>word_with_underscores</p>", "it doesn't do intraword italics");
   cooked("hello \\*evil\\*", "<p>hello *evil*</p>", "it supports escaping of asterisks");
   cooked("hello \\_evil\\_", "<p>hello _evil_</p>", "it supports escaping of italics");
+  cooked("brussel sproutes are *awful*.", "<p>brussel sproutes are <em>awful</em>.</p>", "it doesn't swallow periods.");
 });
 
 test("Traditional Line Breaks", function() {

From 550ef104c6f4b9d656c030dcf6e10d1d31ae68bb Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 28 Aug 2013 13:06:41 -0400
Subject: [PATCH 13/84] FIX: Markdown references within a list were not working
 properly.

---
 .../javascripts/external/better_markdown.js   | 73 ++++++++++++-------
 test/javascripts/components/markdown_test.js  |  4 +
 2 files changed, 51 insertions(+), 26 deletions(-)

diff --git a/app/assets/javascripts/external/better_markdown.js b/app/assets/javascripts/external/better_markdown.js
index 29e708fb8..dd46e8782 100644
--- a/app/assets/javascripts/external/better_markdown.js
+++ b/app/assets/javascripts/external/better_markdown.js
@@ -8,6 +8,8 @@
 
   * 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.
+
   * Note the name BetterMarkdown doesn't mean it's *better* than markdown-js, it refers
     to it being better than our previous markdown parser!
 
@@ -205,6 +207,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
@@ -531,6 +562,7 @@ Markdown.dialects.Gruber = {
 
       // The matcher function
       return function( block, next ) {
+
         var m = block.match( is_list_re );
         if ( !m ) return undefined;
 
@@ -682,6 +714,7 @@ Markdown.dialects.Gruber = {
     })(),
 
     blockquote: function blockquote( block, next ) {
+
       if ( !block.match( /^>/m ) )
         return undefined;
 
@@ -736,39 +769,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 ) );
@@ -891,6 +903,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), "]" );
 
@@ -954,7 +967,6 @@ Markdown.dialects.Gruber.inline = {
       m = text.match( /^\s*\[(.*?)\]/ );
 
       if ( m ) {
-
         consumed += m[ 0 ].length;
 
         // [links][] uses links as its reference
@@ -968,6 +980,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" ) {
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index 3450e457e..b4a22bf1b 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -103,6 +103,10 @@ test("Links", function() {
          "<a href=\"http://www.imdb.com/name/nm2225369\">http://www.imdb.com/name/nm2225369</a></p>",
          'allows multiple links on one line');
 
+  cooked("* [Evil Trout][1]\n  [1]: http://eviltrout.com",
+         "<ul><li><a href=\"http://eviltrout.com\">Evil Trout</a><br></li></ul>",
+         "allows markdown link references in a list");
+
 });
 
 test("Quotes", function() {

From af18cc87fde760f3c9d1cf82c8c63003e5838915 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 28 Aug 2013 13:55:08 -0400
Subject: [PATCH 14/84] Better documentation for new Dialect helpers. Migrated
 emoticon support to new helper format.

---
 .../discourse/dialects/autolink_dialect.js    |  4 +-
 .../dialects/bold_italics_dialect.js          |  1 -
 .../javascripts/discourse/dialects/dialect.js | 54 +++++++++++++++++++
 .../assets/javascripts/discourse_emoji.js     | 20 +++----
 4 files changed, 64 insertions(+), 15 deletions(-)

diff --git a/app/assets/javascripts/discourse/dialects/autolink_dialect.js b/app/assets/javascripts/discourse/dialects/autolink_dialect.js
index cd00e3036..8917939d1 100644
--- a/app/assets/javascripts/discourse/dialects/autolink_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/autolink_dialect.js
@@ -3,11 +3,11 @@
   a hrefs for them.
 **/
 var urlReplacerArgs = {
-  matcher: /(^|\s)((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
+  matcher: /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/gm,
   spaceBoundary: true,
 
   emitter: function(matches) {
-    var url = matches[2],
+    var url = matches[1],
         displayUrl = url;
 
     if (url.match(/^www/)) { url = "http://" + url; }
diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
index eb90914ae..08992e079 100644
--- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
@@ -23,4 +23,3 @@ replaceMarkdown('**', 'strong');
 replaceMarkdown('__', 'strong');
 replaceMarkdown('*', 'em');
 replaceMarkdown('_', 'em');
-
diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index e4202f462..820a680f6 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -128,6 +128,32 @@ Discourse.Dialect = {
     return parser.renderJsonML(parseTree(tree));
   },
 
+  /**
+    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 inlineReplace
+    @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; }
@@ -143,6 +169,34 @@ Discourse.Dialect = {
     };
   },
 
+  /**
+    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.inlineReplace({
+        between: '**',
+        wordBoundary: true.
+        emitter: function(contents) {
+          return ['strong'].concat(contents);
+        }
+      });
+    ```
+
+    @method inlineReplace
+    @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
+  **/
   inlineReplace: function(args) {
     var start = args.start || args.between,
         stop = args.stop || args.between,
diff --git a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
index 3bd1e0613..2d2950152 100644
--- a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
+++ b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
@@ -2,21 +2,17 @@
 
   var emoji = ["+1", "-1", "100", "1234", "8ball", "a", "ab", "abc", "abcd", "accept", "aerial_tramway", "airplane", "alarm_clock", "alien", "ambulance", "anchor", "angel", "anger", "angry", "anguished", "ant", "apple", "aquarius", "aries", "arrow_backward", "arrow_double_down", "arrow_double_up", "arrow_down", "arrow_down_small", "arrow_forward", "arrow_heading_down", "arrow_heading_up", "arrow_left", "arrow_lower_left", "arrow_lower_right", "arrow_right", "arrow_right_hook", "arrow_up", "arrow_up_down", "arrow_up_small", "arrow_upper_left", "arrow_upper_right", "arrows_clockwise", "arrows_counterclockwise", "art", "articulated_lorry", "astonished", "athletic_shoe", "atm", "b", "baby", "baby_bottle", "baby_chick", "baby_symbol", "back", "baggage_claim", "balloon", "ballot_box_with_check", "bamboo", "banana", "bangbang", "bank", "bar_chart", "barber", "baseball", "basketball", "bath", "bathtub", "battery", "bear", "bee", "beer", "beers", "beetle", "beginner", "bell", "bento", "bicyclist", "bike", "bikini", "bird", "birthday", "black_circle", "black_joker", "black_large_square", "black_medium_small_square", "black_medium_square", "black_nib", "black_small_square", "black_square_button", "blossom", "blowfish", "blue_book", "blue_car", "blue_heart", "blush", "boar", "boat", "bomb", "book", "bookmark", "bookmark_tabs", "books", "boom", "boot", "bouquet", "bow", "bowling", "bowtie", "boy", "bread", "bride_with_veil", "bridge_at_night", "briefcase", "broken_heart", "bug", "bulb", "bullettrain_front", "bullettrain_side", "bus", "busstop", "bust_in_silhouette", "busts_in_silhouette", "cactus", "cake", "calendar", "calling", "camel", "camera", "cancer", "candy", "capital_abcd", "capricorn", "car", "card_index", "carousel_horse", "cat", "cat2", "cd", "chart", "chart_with_downwards_trend", "chart_with_upwards_trend", "checkered_flag", "cherries", "cherry_blossom", "chestnut", "chicken", "children_crossing", "chocolate_bar", "christmas_tree", "church", "cinema", "circus_tent", "city_sunrise", "city_sunset", "cl", "clap", "clapper", "clipboard", "clock1", "clock10", "clock1030", "clock11", "clock1130", "clock12", "clock1230", "clock130", "clock2", "clock230", "clock3", "clock330", "clock4", "clock430", "clock5", "clock530", "clock6", "clock630", "clock7", "clock730", "clock8", "clock830", "clock9", "clock930", "closed_book", "closed_lock_with_key", "closed_umbrella", "cloud", "clubs", "cn", "cocktail", "coffee", "cold_sweat", "collision", "computer", "confetti_ball", "confounded", "confused", "congratulations", "construction", "construction_worker", "convenience_store", "cookie", "cool", "cop", "copyright", "corn", "couple", "couple_with_heart", "couplekiss", "cow", "cow2", "credit_card", "crescent_moon", "crocodile", "crossed_flags", "crown", "cry", "crying_cat_face", "crystal_ball", "cupid", "curly_loop", "currency_exchange", "curry", "custard", "customs", "cyclone", "dancer", "dancers", "dango", "dart", "dash", "date", "de", "deciduous_tree", "department_store", "diamond_shape_with_a_dot_inside", "diamonds", "disappointed", "disappointed_relieved", "dizzy", "dizzy_face", "do_not_litter", "dog", "dog2", "dollar", "dolls", "dolphin", "door", "doughnut", "dragon", "dragon_face", "dress", "dromedary_camel", "droplet", "dvd", "e-mail", "ear", "ear_of_rice", "earth_africa", "earth_americas", "earth_asia", "egg", "eggplant", "eight", "eight_pointed_black_star", "eight_spoked_asterisk", "electric_plug", "elephant", "email", "end", "envelope", "envelope_with_arrow", "es", "euro", "european_castle", "european_post_office", "evergreen_tree", "exclamation", "expressionless", "eyeglasses", "eyes", "facepunch", "factory", "fallen_leaf", "family", "fast_forward", "fax", "fearful", "feelsgood", "feet", "ferris_wheel", "file_folder", "finnadie", "fire", "fire_engine", "fireworks", "first_quarter_moon", "first_quarter_moon_with_face", "fish", "fish_cake", "fishing_pole_and_fish", "fist", "five", "flags", "flashlight", "flipper", "floppy_disk", "flower_playing_cards", "flushed", "foggy", "football", "footprints", "fork_and_knife", "fountain", "four", "four_leaf_clover", "fr", "free", "fried_shrimp", "fries", "frog", "frowning", "fu", "fuelpump", "full_moon", "full_moon_with_face", "game_die", "gb", "gem", "gemini", "ghost", "gift", "gift_heart", "girl", "globe_with_meridians", "goat", "goberserk", "godmode", "golf", "grapes", "green_apple", "green_book", "green_heart", "grey_exclamation", "grey_question", "grimacing", "grin", "grinning", "guardsman", "guitar", "gun", "haircut", "hamburger", "hammer", "hamster", "hand", "handbag", "hankey", "hash", "hatched_chick", "hatching_chick", "headphones", "hear_no_evil", "heart", "heart_decoration", "heart_eyes", "heart_eyes_cat", "heartbeat", "heartpulse", "hearts", "heavy_check_mark", "heavy_division_sign", "heavy_dollar_sign", "heavy_exclamation_mark", "heavy_minus_sign", "heavy_multiplication_x", "heavy_plus_sign", "helicopter", "herb", "hibiscus", "high_brightness", "high_heel", "hocho", "honey_pot", "honeybee", "horse", "horse_racing", "hospital", "hotel", "hotsprings", "hourglass", "hourglass_flowing_sand", "house", "house_with_garden", "hurtrealbad", "hushed", "ice_cream", "icecream", "id", "ideograph_advantage", "imp", "inbox_tray", "incoming_envelope", "information_desk_person", "information_source", "innocent", "interrobang", "iphone", "it", "izakaya_lantern", "jack_o_lantern", "japan", "japanese_castle", "japanese_goblin", "japanese_ogre", "jeans", "joy", "joy_cat", "jp", "key", "keycap_ten", "kimono", "kiss", "kissing", "kissing_cat", "kissing_closed_eyes", "kissing_heart", "kissing_smiling_eyes", "koala", "koko", "kr", "lantern", "large_blue_circle", "large_blue_diamond", "large_orange_diamond", "last_quarter_moon", "last_quarter_moon_with_face", "laughing", "leaves", "ledger", "left_luggage", "left_right_arrow", "leftwards_arrow_with_hook", "lemon", "leo", "leopard", "libra", "light_rail", "link", "lips", "lipstick", "lock", "lock_with_ink_pen", "lollipop", "loop", "loudspeaker", "love_hotel", "love_letter", "low_brightness", "m", "mag", "mag_right", "mahjong", "mailbox", "mailbox_closed", "mailbox_with_mail", "mailbox_with_no_mail", "man", "man_with_gua_pi_mao", "man_with_turban", "mans_shoe", "maple_leaf", "mask", "massage", "meat_on_bone", "mega", "melon", "memo", "mens", "metal", "metro", "microphone", "microscope", "milky_way", "minibus", "minidisc", "mobile_phone_off", "money_with_wings", "moneybag", "monkey", "monkey_face", "monorail", "moon", "mortar_board", "mount_fuji", "mountain_bicyclist", "mountain_cableway", "mountain_railway", "mouse", "mouse2", "movie_camera", "moyai", "muscle", "mushroom", "musical_keyboard", "musical_note", "musical_score", "mute", "nail_care", "name_badge", "neckbeard", "necktie", "negative_squared_cross_mark", "neutral_face", "new", "new_moon", "new_moon_with_face", "newspaper", "ng", "nine", "no_bell", "no_bicycles", "no_entry", "no_entry_sign", "no_good", "no_mobile_phones", "no_mouth", "no_pedestrians", "no_smoking", "non-potable_water", "nose", "notebook", "notebook_with_decorative_cover", "notes", "nut_and_bolt", "o", "o2", "ocean", "octocat", "octopus", "oden", "office", "ok", "ok_hand", "ok_woman", "older_man", "older_woman", "on", "oncoming_automobile", "oncoming_bus", "oncoming_police_car", "oncoming_taxi", "one", "open_book", "open_file_folder", "open_hands", "open_mouth", "ophiuchus", "orange_book", "outbox_tray", "ox", "package", "page_facing_up", "page_with_curl", "pager", "palm_tree", "panda_face", "paperclip", "parking", "part_alternation_mark", "partly_sunny", "passport_control", "paw_prints", "peach", "pear", "pencil", "pencil2", "penguin", "pensive", "performing_arts", "persevere", "person_frowning", "person_with_blond_hair", "person_with_pouting_face", "phone", "pig", "pig2", "pig_nose", "pill", "pineapple", "pisces", "pizza", "point_down", "point_left", "point_right", "point_up", "point_up_2", "police_car", "poodle", "poop", "post_office", "postal_horn", "postbox", "potable_water", "pouch", "poultry_leg", "pound", "pouting_cat", "pray", "princess", "punch", "purple_heart", "purse", "pushpin", "put_litter_in_its_place", "question", "rabbit", "rabbit2", "racehorse", "radio", "radio_button", "rage", "rage1", "rage2", "rage3", "rage4", "railway_car", "rainbow", "raised_hand", "raised_hands", "raising_hand", "ram", "ramen", "rat", "recycle", "red_car", "red_circle", "registered", "relaxed", "relieved", "repeat", "repeat_one", "restroom", "revolving_hearts", "rewind", "ribbon", "rice", "rice_ball", "rice_cracker", "rice_scene", "ring", "rocket", "roller_coaster", "rooster", "rose", "rotating_light", "round_pushpin", "rowboat", "ru", "rugby_football", "runner", "running", "running_shirt_with_sash", "sa", "sagittarius", "sailboat", "sake", "sandal", "santa", "satellite", "satisfied", "saxophone", "school", "school_satchel", "scissors", "scorpius", "scream", "scream_cat", "scroll", "seat", "secret", "see_no_evil", "seedling", "seven", "shaved_ice", "sheep", "shell", "ship", "shipit", "shirt", "shit", "shoe", "shower", "signal_strength", "six", "six_pointed_star", "ski", "skull", "sleeping", "sleepy", "slot_machine", "small_blue_diamond", "small_orange_diamond", "small_red_triangle", "small_red_triangle_down", "smile", "smile_cat", "smiley", "smiley_cat", "smiling_imp", "smirk", "smirk_cat", "smoking", "snail", "snake", "snowboarder", "snowflake", "snowman", "sob", "soccer", "soon", "sos", "sound", "space_invader", "spades", "spaghetti", "sparkle", "sparkler", "sparkles", "sparkling_heart", "speak_no_evil", "speaker", "speech_balloon", "speedboat", "squirrel", "star", "star2", "stars", "station", "statue_of_liberty", "steam_locomotive", "stew", "straight_ruler", "strawberry", "stuck_out_tongue", "stuck_out_tongue_closed_eyes", "stuck_out_tongue_winking_eye", "sun_with_face", "sunflower", "sunglasses", "sunny", "sunrise", "sunrise_over_mountains", "surfer", "sushi", "suspect", "suspension_railway", "sweat", "sweat_drops", "sweat_smile", "sweet_potato", "swimmer", "symbols", "syringe", "tada", "tanabata_tree", "tangerine", "taurus", "taxi", "tea", "telephone", "telephone_receiver", "telescope", "tennis", "tent", "thought_balloon", "three", "thumbsdown", "thumbsup", "ticket", "tiger", "tiger2", "tired_face", "tm", "toilet", "tokyo_tower", "tomato", "tongue", "top", "tophat", "tractor", "traffic_light", "train", "train2", "tram", "triangular_flag_on_post", "triangular_ruler", "trident", "triumph", "trolleybus", "trollface", "trophy", "tropical_drink", "tropical_fish", "truck", "trumpet", "tshirt", "tulip", "turtle", "tv", "twisted_rightwards_arrows", "two", "two_hearts", "two_men_holding_hands", "two_women_holding_hands", "u5272", "u5408", "u55b6", "u6307", "u6708", "u6709", "u6e80", "u7121", "u7533", "u7981", "u7a7a", "uk", "umbrella", "unamused", "underage", "unlock", "up", "us", "v", "vertical_traffic_light", "vhs", "vibration_mode", "video_camera", "video_game", "violin", "virgo", "volcano", "vs", "walking", "waning_crescent_moon", "waning_gibbous_moon", "warning", "watch", "water_buffalo", "watermelon", "wave", "wavy_dash", "waxing_crescent_moon", "waxing_gibbous_moon", "wc", "weary", "wedding", "whale", "whale2", "wheelchair", "white_check_mark", "white_circle", "white_flower", "white_large_square", "white_medium_small_square", "white_medium_square", "white_small_square", "white_square_button", "wind_chime", "wine_glass", "wink", "wolf", "woman", "womans_clothes", "womans_hat", "womens", "worried", "wrench", "x", "yellow_heart", "yen", "yum", "zap", "zero", "zzz"]
 
-  Discourse.Dialect.on("register", function(event) {
-    var dialect = event.dialect,
-        MD = event.MD;
-
-    dialect.inline[":"] = function(text, orig_match) {
-      var m = /^\:([a-z\_\+\-0-9]+)\:/.exec(text);
-
-      if (m && (emoji.indexOf(m[1]) !== -1)) {
-        var url = Discourse.getURL('/assets/emoji/' + m[1] + '.png');
-        return [m[0].length, ['img', {href: url, title: ':' + m[1] + ':', 'class': 'emoji', alt: m[1]}] ];
+  Discourse.Dialect.inlineReplace({
+    between: ':',
+    rawContents: true,
+    emitter: function(contents) {
+      if (emoji.indexOf(contents) !== -1) {
+        var url = Discourse.getURL('/assets/emoji/' + contents + '.png');
+        return ['img', {href: url, title: ':' + contents + ':', 'class': 'emoji', alt: contents}];
       }
-    };
+    }
   });
 
-
   if (Discourse && Discourse.ComposerView) {
     Discourse.ComposerView.on("initWmdEditor", function(event){
 

From eeef24b9da0c4ea3c9be4345d03a293363850694 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 28 Aug 2013 15:27:03 -0400
Subject: [PATCH 15/84] Allow basic emoticons to work too.

---
 .../discourse/components/autocomplete.js      | 49 ++++++++++++++++++-
 .../discourse/dialects/bbcode_dialect.js      | 10 ++--
 .../dialects/bold_italics_dialect.js          |  4 +-
 .../javascripts/discourse/dialects/dialect.js | 27 ++++++++--
 .../javascripts/external/better_markdown.js   |  2 +-
 .../assets/javascripts/discourse_emoji.js     | 48 +++++++++++++-----
 6 files changed, 114 insertions(+), 26 deletions(-)

diff --git a/app/assets/javascripts/discourse/components/autocomplete.js b/app/assets/javascripts/discourse/components/autocomplete.js
index 6e282f36a..718c8841b 100644
--- a/app/assets/javascripts/discourse/components/autocomplete.js
+++ b/app/assets/javascripts/discourse/components/autocomplete.js
@@ -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 += ",";
diff --git a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
index 7295d8de5..8ea9825b4 100644
--- a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
@@ -6,7 +6,7 @@
   @param {function} emitter the function that creates JsonML for the tag
 **/
 function replaceBBCode(tag, emitter) {
-  Discourse.Dialect.inlineReplace({
+  Discourse.Dialect.inlineBetween({
     start: "[" + tag + "]",
     stop: "[/" + tag + "]",
     emitter: emitter
@@ -21,7 +21,7 @@ function replaceBBCode(tag, emitter) {
   @param {function} emitter the function that creates JsonML for the tag
 **/
 function replaceBBCodeParamsRaw(tag, emitter) {
-  Discourse.Dialect.inlineReplace({
+  Discourse.Dialect.inlineBetween({
     start: "[" + tag + "=",
     stop: "[/" + tag + "]",
     rawContents: true,
@@ -59,21 +59,21 @@ replaceBBCode('li', function(contents) { return ['li'].concat(contents); });
 
 replaceBBCode('spoiler', function(contents) { return ['span', {'class': 'spoiler'}].concat(contents); });
 
-Discourse.Dialect.inlineReplace({
+Discourse.Dialect.inlineBetween({
   start: '[img]',
   stop: '[/img]',
   rawContents: true,
   emitter: function(contents) { return ['img', {href: contents}]; }
 });
 
-Discourse.Dialect.inlineReplace({
+Discourse.Dialect.inlineBetween({
   start: '[email]',
   stop: '[/email]',
   rawContents: true,
   emitter: function(contents) { return ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]; }
 });
 
-Discourse.Dialect.inlineReplace({
+Discourse.Dialect.inlineBetween({
   start: '[url]',
   stop: '[/url]',
   rawContents: true,
diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
index 08992e079..b77b2e304 100644
--- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
@@ -4,7 +4,7 @@
 **/
 
 // Support for simultaneous bold and italics
-Discourse.Dialect.inlineReplace({
+Discourse.Dialect.inlineBetween({
   between: '***',
   wordBoundary: true,
   emitter: function(contents) { return ['strong', ['em'].concat(contents)]; }
@@ -12,7 +12,7 @@ Discourse.Dialect.inlineReplace({
 
 // Builds a common markdown replacer
 var replaceMarkdown = function(match, tag) {
-  Discourse.Dialect.inlineReplace({
+  Discourse.Dialect.inlineBetween({
     between: match,
     wordBoundary: true,
     emitter: function(contents) { return [tag].concat(contents) }
diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index 820a680f6..eb26d0cad 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -128,6 +128,25 @@ Discourse.Dialect = {
     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(':)', ['img', {src: '/images/smile.gif'}])
+    ```
+
+    @method inlineReplace
+    @param {String} token The token we want to replace
+    @param {Array} jsonml The JsonML to replace it with.
+  **/
+  inlineReplace: function(token, jsonml) {
+    dialect.inline[token] = function(text, match, prev) {
+      return [token.length, jsonml];
+    };
+  },
+
   /**
     Matches inline using a regular expression. The emitter function is passed
     the matches from the regular expression.
@@ -146,7 +165,7 @@ Discourse.Dialect = {
       });
     ```
 
-    @method inlineReplace
+    @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
@@ -178,7 +197,7 @@ Discourse.Dialect = {
 
     ```javascript
 
-      Discourse.Dialect.inlineReplace({
+      Discourse.Dialect.inlineBetween({
         between: '**',
         wordBoundary: true.
         emitter: function(contents) {
@@ -187,7 +206,7 @@ Discourse.Dialect = {
       });
     ```
 
-    @method inlineReplace
+    @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
@@ -197,7 +216,7 @@ Discourse.Dialect = {
       @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
   **/
-  inlineReplace: function(args) {
+  inlineBetween: function(args) {
     var start = args.start || args.between,
         stop = args.stop || args.between,
         startLength = start.length;
diff --git a/app/assets/javascripts/external/better_markdown.js b/app/assets/javascripts/external/better_markdown.js
index dd46e8782..4d5da8935 100644
--- a/app/assets/javascripts/external/better_markdown.js
+++ b/app/assets/javascripts/external/better_markdown.js
@@ -1057,7 +1057,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 + ")" );
   }
diff --git a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
index 2d2950152..b524de74c 100644
--- a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
+++ b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
@@ -2,15 +2,36 @@
 
   var emoji = ["+1", "-1", "100", "1234", "8ball", "a", "ab", "abc", "abcd", "accept", "aerial_tramway", "airplane", "alarm_clock", "alien", "ambulance", "anchor", "angel", "anger", "angry", "anguished", "ant", "apple", "aquarius", "aries", "arrow_backward", "arrow_double_down", "arrow_double_up", "arrow_down", "arrow_down_small", "arrow_forward", "arrow_heading_down", "arrow_heading_up", "arrow_left", "arrow_lower_left", "arrow_lower_right", "arrow_right", "arrow_right_hook", "arrow_up", "arrow_up_down", "arrow_up_small", "arrow_upper_left", "arrow_upper_right", "arrows_clockwise", "arrows_counterclockwise", "art", "articulated_lorry", "astonished", "athletic_shoe", "atm", "b", "baby", "baby_bottle", "baby_chick", "baby_symbol", "back", "baggage_claim", "balloon", "ballot_box_with_check", "bamboo", "banana", "bangbang", "bank", "bar_chart", "barber", "baseball", "basketball", "bath", "bathtub", "battery", "bear", "bee", "beer", "beers", "beetle", "beginner", "bell", "bento", "bicyclist", "bike", "bikini", "bird", "birthday", "black_circle", "black_joker", "black_large_square", "black_medium_small_square", "black_medium_square", "black_nib", "black_small_square", "black_square_button", "blossom", "blowfish", "blue_book", "blue_car", "blue_heart", "blush", "boar", "boat", "bomb", "book", "bookmark", "bookmark_tabs", "books", "boom", "boot", "bouquet", "bow", "bowling", "bowtie", "boy", "bread", "bride_with_veil", "bridge_at_night", "briefcase", "broken_heart", "bug", "bulb", "bullettrain_front", "bullettrain_side", "bus", "busstop", "bust_in_silhouette", "busts_in_silhouette", "cactus", "cake", "calendar", "calling", "camel", "camera", "cancer", "candy", "capital_abcd", "capricorn", "car", "card_index", "carousel_horse", "cat", "cat2", "cd", "chart", "chart_with_downwards_trend", "chart_with_upwards_trend", "checkered_flag", "cherries", "cherry_blossom", "chestnut", "chicken", "children_crossing", "chocolate_bar", "christmas_tree", "church", "cinema", "circus_tent", "city_sunrise", "city_sunset", "cl", "clap", "clapper", "clipboard", "clock1", "clock10", "clock1030", "clock11", "clock1130", "clock12", "clock1230", "clock130", "clock2", "clock230", "clock3", "clock330", "clock4", "clock430", "clock5", "clock530", "clock6", "clock630", "clock7", "clock730", "clock8", "clock830", "clock9", "clock930", "closed_book", "closed_lock_with_key", "closed_umbrella", "cloud", "clubs", "cn", "cocktail", "coffee", "cold_sweat", "collision", "computer", "confetti_ball", "confounded", "confused", "congratulations", "construction", "construction_worker", "convenience_store", "cookie", "cool", "cop", "copyright", "corn", "couple", "couple_with_heart", "couplekiss", "cow", "cow2", "credit_card", "crescent_moon", "crocodile", "crossed_flags", "crown", "cry", "crying_cat_face", "crystal_ball", "cupid", "curly_loop", "currency_exchange", "curry", "custard", "customs", "cyclone", "dancer", "dancers", "dango", "dart", "dash", "date", "de", "deciduous_tree", "department_store", "diamond_shape_with_a_dot_inside", "diamonds", "disappointed", "disappointed_relieved", "dizzy", "dizzy_face", "do_not_litter", "dog", "dog2", "dollar", "dolls", "dolphin", "door", "doughnut", "dragon", "dragon_face", "dress", "dromedary_camel", "droplet", "dvd", "e-mail", "ear", "ear_of_rice", "earth_africa", "earth_americas", "earth_asia", "egg", "eggplant", "eight", "eight_pointed_black_star", "eight_spoked_asterisk", "electric_plug", "elephant", "email", "end", "envelope", "envelope_with_arrow", "es", "euro", "european_castle", "european_post_office", "evergreen_tree", "exclamation", "expressionless", "eyeglasses", "eyes", "facepunch", "factory", "fallen_leaf", "family", "fast_forward", "fax", "fearful", "feelsgood", "feet", "ferris_wheel", "file_folder", "finnadie", "fire", "fire_engine", "fireworks", "first_quarter_moon", "first_quarter_moon_with_face", "fish", "fish_cake", "fishing_pole_and_fish", "fist", "five", "flags", "flashlight", "flipper", "floppy_disk", "flower_playing_cards", "flushed", "foggy", "football", "footprints", "fork_and_knife", "fountain", "four", "four_leaf_clover", "fr", "free", "fried_shrimp", "fries", "frog", "frowning", "fu", "fuelpump", "full_moon", "full_moon_with_face", "game_die", "gb", "gem", "gemini", "ghost", "gift", "gift_heart", "girl", "globe_with_meridians", "goat", "goberserk", "godmode", "golf", "grapes", "green_apple", "green_book", "green_heart", "grey_exclamation", "grey_question", "grimacing", "grin", "grinning", "guardsman", "guitar", "gun", "haircut", "hamburger", "hammer", "hamster", "hand", "handbag", "hankey", "hash", "hatched_chick", "hatching_chick", "headphones", "hear_no_evil", "heart", "heart_decoration", "heart_eyes", "heart_eyes_cat", "heartbeat", "heartpulse", "hearts", "heavy_check_mark", "heavy_division_sign", "heavy_dollar_sign", "heavy_exclamation_mark", "heavy_minus_sign", "heavy_multiplication_x", "heavy_plus_sign", "helicopter", "herb", "hibiscus", "high_brightness", "high_heel", "hocho", "honey_pot", "honeybee", "horse", "horse_racing", "hospital", "hotel", "hotsprings", "hourglass", "hourglass_flowing_sand", "house", "house_with_garden", "hurtrealbad", "hushed", "ice_cream", "icecream", "id", "ideograph_advantage", "imp", "inbox_tray", "incoming_envelope", "information_desk_person", "information_source", "innocent", "interrobang", "iphone", "it", "izakaya_lantern", "jack_o_lantern", "japan", "japanese_castle", "japanese_goblin", "japanese_ogre", "jeans", "joy", "joy_cat", "jp", "key", "keycap_ten", "kimono", "kiss", "kissing", "kissing_cat", "kissing_closed_eyes", "kissing_heart", "kissing_smiling_eyes", "koala", "koko", "kr", "lantern", "large_blue_circle", "large_blue_diamond", "large_orange_diamond", "last_quarter_moon", "last_quarter_moon_with_face", "laughing", "leaves", "ledger", "left_luggage", "left_right_arrow", "leftwards_arrow_with_hook", "lemon", "leo", "leopard", "libra", "light_rail", "link", "lips", "lipstick", "lock", "lock_with_ink_pen", "lollipop", "loop", "loudspeaker", "love_hotel", "love_letter", "low_brightness", "m", "mag", "mag_right", "mahjong", "mailbox", "mailbox_closed", "mailbox_with_mail", "mailbox_with_no_mail", "man", "man_with_gua_pi_mao", "man_with_turban", "mans_shoe", "maple_leaf", "mask", "massage", "meat_on_bone", "mega", "melon", "memo", "mens", "metal", "metro", "microphone", "microscope", "milky_way", "minibus", "minidisc", "mobile_phone_off", "money_with_wings", "moneybag", "monkey", "monkey_face", "monorail", "moon", "mortar_board", "mount_fuji", "mountain_bicyclist", "mountain_cableway", "mountain_railway", "mouse", "mouse2", "movie_camera", "moyai", "muscle", "mushroom", "musical_keyboard", "musical_note", "musical_score", "mute", "nail_care", "name_badge", "neckbeard", "necktie", "negative_squared_cross_mark", "neutral_face", "new", "new_moon", "new_moon_with_face", "newspaper", "ng", "nine", "no_bell", "no_bicycles", "no_entry", "no_entry_sign", "no_good", "no_mobile_phones", "no_mouth", "no_pedestrians", "no_smoking", "non-potable_water", "nose", "notebook", "notebook_with_decorative_cover", "notes", "nut_and_bolt", "o", "o2", "ocean", "octocat", "octopus", "oden", "office", "ok", "ok_hand", "ok_woman", "older_man", "older_woman", "on", "oncoming_automobile", "oncoming_bus", "oncoming_police_car", "oncoming_taxi", "one", "open_book", "open_file_folder", "open_hands", "open_mouth", "ophiuchus", "orange_book", "outbox_tray", "ox", "package", "page_facing_up", "page_with_curl", "pager", "palm_tree", "panda_face", "paperclip", "parking", "part_alternation_mark", "partly_sunny", "passport_control", "paw_prints", "peach", "pear", "pencil", "pencil2", "penguin", "pensive", "performing_arts", "persevere", "person_frowning", "person_with_blond_hair", "person_with_pouting_face", "phone", "pig", "pig2", "pig_nose", "pill", "pineapple", "pisces", "pizza", "point_down", "point_left", "point_right", "point_up", "point_up_2", "police_car", "poodle", "poop", "post_office", "postal_horn", "postbox", "potable_water", "pouch", "poultry_leg", "pound", "pouting_cat", "pray", "princess", "punch", "purple_heart", "purse", "pushpin", "put_litter_in_its_place", "question", "rabbit", "rabbit2", "racehorse", "radio", "radio_button", "rage", "rage1", "rage2", "rage3", "rage4", "railway_car", "rainbow", "raised_hand", "raised_hands", "raising_hand", "ram", "ramen", "rat", "recycle", "red_car", "red_circle", "registered", "relaxed", "relieved", "repeat", "repeat_one", "restroom", "revolving_hearts", "rewind", "ribbon", "rice", "rice_ball", "rice_cracker", "rice_scene", "ring", "rocket", "roller_coaster", "rooster", "rose", "rotating_light", "round_pushpin", "rowboat", "ru", "rugby_football", "runner", "running", "running_shirt_with_sash", "sa", "sagittarius", "sailboat", "sake", "sandal", "santa", "satellite", "satisfied", "saxophone", "school", "school_satchel", "scissors", "scorpius", "scream", "scream_cat", "scroll", "seat", "secret", "see_no_evil", "seedling", "seven", "shaved_ice", "sheep", "shell", "ship", "shipit", "shirt", "shit", "shoe", "shower", "signal_strength", "six", "six_pointed_star", "ski", "skull", "sleeping", "sleepy", "slot_machine", "small_blue_diamond", "small_orange_diamond", "small_red_triangle", "small_red_triangle_down", "smile", "smile_cat", "smiley", "smiley_cat", "smiling_imp", "smirk", "smirk_cat", "smoking", "snail", "snake", "snowboarder", "snowflake", "snowman", "sob", "soccer", "soon", "sos", "sound", "space_invader", "spades", "spaghetti", "sparkle", "sparkler", "sparkles", "sparkling_heart", "speak_no_evil", "speaker", "speech_balloon", "speedboat", "squirrel", "star", "star2", "stars", "station", "statue_of_liberty", "steam_locomotive", "stew", "straight_ruler", "strawberry", "stuck_out_tongue", "stuck_out_tongue_closed_eyes", "stuck_out_tongue_winking_eye", "sun_with_face", "sunflower", "sunglasses", "sunny", "sunrise", "sunrise_over_mountains", "surfer", "sushi", "suspect", "suspension_railway", "sweat", "sweat_drops", "sweat_smile", "sweet_potato", "swimmer", "symbols", "syringe", "tada", "tanabata_tree", "tangerine", "taurus", "taxi", "tea", "telephone", "telephone_receiver", "telescope", "tennis", "tent", "thought_balloon", "three", "thumbsdown", "thumbsup", "ticket", "tiger", "tiger2", "tired_face", "tm", "toilet", "tokyo_tower", "tomato", "tongue", "top", "tophat", "tractor", "traffic_light", "train", "train2", "tram", "triangular_flag_on_post", "triangular_ruler", "trident", "triumph", "trolleybus", "trollface", "trophy", "tropical_drink", "tropical_fish", "truck", "trumpet", "tshirt", "tulip", "turtle", "tv", "twisted_rightwards_arrows", "two", "two_hearts", "two_men_holding_hands", "two_women_holding_hands", "u5272", "u5408", "u55b6", "u6307", "u6708", "u6709", "u6e80", "u7121", "u7533", "u7981", "u7a7a", "uk", "umbrella", "unamused", "underage", "unlock", "up", "us", "v", "vertical_traffic_light", "vhs", "vibration_mode", "video_camera", "video_game", "violin", "virgo", "volcano", "vs", "walking", "waning_crescent_moon", "waning_gibbous_moon", "warning", "watch", "water_buffalo", "watermelon", "wave", "wavy_dash", "waxing_crescent_moon", "waxing_gibbous_moon", "wc", "weary", "wedding", "whale", "whale2", "wheelchair", "white_check_mark", "white_circle", "white_flower", "white_large_square", "white_medium_small_square", "white_medium_square", "white_small_square", "white_square_button", "wind_chime", "wine_glass", "wink", "wolf", "woman", "womans_clothes", "womans_hat", "womens", "worried", "wrench", "x", "yellow_heart", "yen", "yum", "zap", "zero", "zzz"]
 
-  Discourse.Dialect.inlineReplace({
+  function imageFor(code) {
+    if (emoji.indexOf(code) !== -1) {
+      var url = Discourse.getURL('/assets/emoji/' + code + '.png');
+      return ['img', {href: url, title: ':' + code + ':', 'class': 'emoji', alt: code}];
+    }
+  }
+
+  // Also support default emotions
+  var translations = {
+    ':)'   : 'smile',
+    ':('   : 'frowning',
+    ';)'   : 'wink',
+    ':\'(' : 'cry',
+    ':P'   : 'stuck_out_tongue',
+    ':O'   : 'open_mouth',
+    ':D'   : 'grin',
+    ':|'   : 'expressionless',
+    ";P"   : 'stuck_out_tongue_winking_eye',
+    ';)'   : 'wink',
+    ":$"   : 'blush'
+  };
+
+  Object.keys(translations).forEach(function (code) {
+    Discourse.Dialect.inlineReplace(code, imageFor(translations[code]));
+  });
+
+  Discourse.Dialect.inlineBetween({
     between: ':',
     rawContents: true,
-    emitter: function(contents) {
-      if (emoji.indexOf(contents) !== -1) {
-        var url = Discourse.getURL('/assets/emoji/' + contents + '.png');
-        return ['img', {href: url, title: ':' + contents + ':', 'class': 'emoji', alt: contents}];
-      }
-    }
+    emitter: imageFor
   });
 
   if (Discourse && Discourse.ComposerView) {
@@ -36,12 +57,15 @@
         transformComplete: function(v){ return v + ":"; },
         dataSource: function(term){
 
+          var full = ":" + term;
           term = term.toLowerCase();
 
           if (term === "") {
-            return Ember.Deferred.promise(function (promise) {
-              promise.resolve(["smile", "smiley", "wink", "sunny", "blush"]);
-            });
+            return Ember.RSVP.resolve(["smile", "smiley", "wink", "sunny", "blush"]);
+          }
+
+          if (translations[full]) {
+            return Ember.RSVP.resolve([translations[full]]);
           }
 
           var options = [];
@@ -62,9 +86,7 @@
             }
           }
 
-          return Ember.Deferred.promise(function (promise) {
-            promise.resolve(options);
-          });
+          return Ember.RSVP.resolve(options);
         }
       });
     });

From 027af77741e9fdda01185670b79d62eb193cc622 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Wed, 28 Aug 2013 21:29:45 +0200
Subject: [PATCH 16/84] FIX: 404 on old thumbnails

---
 ...0130828192526_fix_optimized_images_urls.rb | 19 +++++++++++++++++++
 1 file changed, 19 insertions(+)
 create mode 100644 db/migrate/20130828192526_fix_optimized_images_urls.rb

diff --git a/db/migrate/20130828192526_fix_optimized_images_urls.rb b/db/migrate/20130828192526_fix_optimized_images_urls.rb
new file mode 100644
index 000000000..4e19bb907
--- /dev/null
+++ b/db/migrate/20130828192526_fix_optimized_images_urls.rb
@@ -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

From c2088f5b5ddba038c66976830fa3a490a44a6fbe Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Wed, 28 Aug 2013 15:37:20 -0400
Subject: [PATCH 17/84] Use moment.js in auto close js

---
 .../discourse/controllers/edit_topic_auto_close_controller.js  | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
index da86cfcdf..76e7253a7 100644
--- a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
+++ b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
@@ -11,9 +11,10 @@ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Disco
 
   setDays: function() {
     if( this.get('details.auto_close_at') ) {
+      console.log( 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', '');

From 2b2c83ffb7ce58adae9c6ba965a1bb86bb88a63f Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Wed, 28 Aug 2013 15:53:42 -0400
Subject: [PATCH 18/84] remove console.log

---
 .../discourse/controllers/edit_topic_auto_close_controller.js    | 1 -
 1 file changed, 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
index 76e7253a7..6301c2def 100644
--- a/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
+++ b/app/assets/javascripts/discourse/controllers/edit_topic_auto_close_controller.js
@@ -11,7 +11,6 @@ Discourse.EditTopicAutoCloseController = Discourse.ObjectController.extend(Disco
 
   setDays: function() {
     if( this.get('details.auto_close_at') ) {
-      console.log( 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', Math.round(moment(closeTime).diff(new Date(), 'days', true)));

From e15982a4760702286b012a7f8e92f431dfe60c8b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Wed, 28 Aug 2013 22:06:09 +0200
Subject: [PATCH 19/84] FIX: convert error in test

---
 spec/controllers/users_controller_spec.rb | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index c1013f085..0233062bb 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -979,14 +979,11 @@ describe UsersController do
         user.uploaded_avatar.id.should == upload.id
         # automatically set "use_uploaded_avatar"
         user.use_uploaded_avatar.should == true
-      end
-
-      it 'returns the url, width and height of the uploaded image' do
-        xhr :post, :upload_avatar, username: user.username, file: avatar
+        # returns the url, width and height of the uploaded image
         json = JSON.parse(response.body)
-        json['url'].should_not be_nil
-        json['width'].should == 244
-        json['height'].should == 66
+        json['url'].should == "/uploads/default/1/1234567890123456.jpg"
+        json['width'].should == 100
+        json['height'].should == 200
       end
 
     end

From 6029a77efb3a24e3b07573a37f73ec98a4a3c011 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 28 Aug 2013 16:15:11 -0400
Subject: [PATCH 20/84] A nicer API for dealing with text replacements in
 Discourse's parser pipeline

---
 .../javascripts/discourse/dialects/dialect.js | 105 ++++++++++++------
 .../discourse/dialects/github_code_dialect.js |  56 +---------
 .../discourse/dialects/newline_dialect.js     |  45 ++++----
 3 files changed, 92 insertions(+), 114 deletions(-)

diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index eb26d0cad..9ee2d24a4 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -3,43 +3,6 @@
   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,
@@ -239,7 +202,75 @@ Discourse.Dialect = {
         return [endPos+stop.length, contents];
       }
     };
+  },
 
+  /**
+    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]);
+      }
+    });
   }
 
 };
diff --git a/app/assets/javascripts/discourse/dialects/github_code_dialect.js b/app/assets/javascripts/discourse/dialects/github_code_dialect.js
index 2c82cd65d..c7b0b3786 100644
--- a/app/assets/javascripts/discourse/dialects/github_code_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/github_code_dialect.js
@@ -78,56 +78,8 @@ Discourse.Dialect.on("register", function(event) {
 
 });
 
-/**
-  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.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);
 });
diff --git a/app/assets/javascripts/discourse/dialects/newline_dialect.js b/app/assets/javascripts/discourse/dialects/newline_dialect.js
index 3653bdcdb..1d51d4b9c 100644
--- a/app/assets/javascripts/discourse/dialects/newline_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/newline_dialect.js
@@ -1,37 +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 || (insideCounts.pre > 0) || (node.length < 1)) { return; }
+  if (linebreaks || (insideCounts.pre > 0)) { return; }
 
-  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;
     }
   }
 
-});
+});
\ No newline at end of file

From 86d2477f978735b63eaa7ca2f0c7c8e3b8770c12 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Wed, 28 Aug 2013 23:21:46 +0200
Subject: [PATCH 21/84] properly escape backslashes

---
 db/migrate/20130828192526_fix_optimized_images_urls.rb | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/db/migrate/20130828192526_fix_optimized_images_urls.rb b/db/migrate/20130828192526_fix_optimized_images_urls.rb
index 4e19bb907..c605b7b74 100644
--- a/db/migrate/20130828192526_fix_optimized_images_urls.rb
+++ b/db/migrate/20130828192526_fix_optimized_images_urls.rb
@@ -2,15 +2,15 @@ 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}')
+             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}$')
+                    || 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}\.';"
+               AND oi.url ~ '^\\/uploads\\/[^/]+\\/_optimized\\/[0-9a-f]{3}\\/[0-9a-f]{3}\\/[0-9a-f]{11}\\.';"
   end
 
   def down

From 522c60c81a126d2be3e3648387d747209eace817 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 28 Aug 2013 18:28:25 -0400
Subject: [PATCH 22/84] FIX: Error loading up V8 due to Discourse not being
 present

---
 .../vendor/assets/javascripts/discourse_emoji.js            | 6 ++++--
 1 file changed, 4 insertions(+), 2 deletions(-)

diff --git a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
index b524de74c..2f5aced8c 100644
--- a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
+++ b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
@@ -24,8 +24,10 @@
     ":$"   : 'blush'
   };
 
-  Object.keys(translations).forEach(function (code) {
-    Discourse.Dialect.inlineReplace(code, imageFor(translations[code]));
+  Discourse.Dialect.on('register', function() {
+    Object.keys(translations).forEach(function (code) {
+      Discourse.Dialect.inlineReplace(code, imageFor(translations[code]));
+    });
   });
 
   Discourse.Dialect.inlineBetween({

From eec77e7bea4c73dcefc918803c7c16b51aec2383 Mon Sep 17 00:00:00 2001
From: Jeff Atwood <jatwood@discourse.org>
Date: Wed, 28 Aug 2013 16:31:23 -0700
Subject: [PATCH 23/84] "moved" makes more sense in the notify

---
 config/locales/client.en.yml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 204b4d13f..fb6642845 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -504,7 +504,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:

From 2b1a140c9be6546d70cd44a6049d58a13c86c836 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 29 Aug 2013 15:26:27 +1000
Subject: [PATCH 24/84] add a soft puma dependency so people can run Discourse
 on puma

---
 Gemfile             | 1 +
 Gemfile.lock        | 3 +++
 Gemfile_rails4.lock | 3 +++
 3 files changed, 7 insertions(+)

diff --git a/Gemfile b/Gemfile
index 3aed53724..85b02277d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -196,6 +196,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
diff --git a/Gemfile.lock b/Gemfile.lock
index a01605169..e0988b534 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -287,6 +287,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)
@@ -501,6 +503,7 @@ DEPENDENCIES
   pg
   pry-nav
   pry-rails
+  puma
   qunit-rails
   rack-cache
   rack-cors
diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index ebb7d5eaa..7f387e83f 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -327,6 +327,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)
@@ -511,6 +513,7 @@ DEPENDENCIES
   pg
   pry-nav
   pry-rails
+  puma
   qunit-rails
   rack-cache
   rack-cors

From aaf41d227f77b8e52ab682ca8f1dfe56b68d7fce Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 29 Aug 2013 15:27:01 +1000
Subject: [PATCH 25/84] fix secret_token init to always allow an override even
 if its too short

---
 config/initializers/secret_token.rb | 11 +++++++----
 1 file changed, 7 insertions(+), 4 deletions(-)

diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb
index 8ff0b8bed..4c858affb 100644
--- a/config/initializers/secret_token.rb
+++ b/config/initializers/secret_token.rb
@@ -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

From 47c6ba2f51a54928f66540ce6e9045c7500b0567 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 29 Aug 2013 15:27:33 +1000
Subject: [PATCH 26/84] mucking with my measure script

---
 script/measure.rb | 113 ++++++++++++++++++++++++++++++----------------
 1 file changed, 75 insertions(+), 38 deletions(-)

diff --git a/script/measure.rb b/script/measure.rb
index e4fa161d0..4b303172e 100644
--- a/script/measure.rb
+++ b/script/measure.rb
@@ -60,12 +60,49 @@ def profile(name, &block)
 end
 
 
-# User.limit(10).to_a
+
+def stuff
+  u = User.first
+  r = TopicQuery.new(u, {}).list_latest
+  r.topics.to_a
+end
+
+stuff
+profile_allocations "stuff" do
+  stuff
+end
+
+
+# Benchmark.bmbm do |x|
+# 
+#   x.report("find") do
+#     100.times{stuff}
+#   end
+# 
+# end
+# 
+#   x.report("grab 10 users id") do
+#     100.times{User.limit(10).select(:id).to_a}
+#   end
+# 
+#   x.report("grab 10 users") do
+#     100.times{User.limit(10).to_a}
+#   end
+# 
+# profile("topic query") do
+# r = TopicQuery.new(u, {}).list_latest
+# r.topics.to_a
+# end
+
+# 
 # RubyProf.start
-# User.limit(10).to_a
+# 
+# r = TopicQuery.new(u, {}).list_latest
+# r.topics.to_a
+# 
 # result = RubyProf.stop
-# # printer = RubyProf::GraphPrinter.new(result)
-# printer = RubyProf::FlatPrinter.new(result)
+# printer = RubyProf::GraphPrinter.new(result)
+# # printer = RubyProf::FlatPrinter.new(result)
 # printer.print(STDOUT, :min_percent => 2)
 # 
 # exit
@@ -81,40 +118,40 @@ end
 # User.limit(10).to_a
 # exit
 #
-User.select('id, 2 bob').first
-Benchmark.bmbm do |x|
-
-  x.report("find") do
-    100.times{User.find(1)}
-  end
-
-  x.report("grab 10 users created_at") do
-    100.times{User.limit(10).select(:created_at).to_a}
-  end
-
-  x.report("grab 10 users id") do
-    100.times{User.limit(10).select(:id).to_a}
-  end
-
-  x.report("grab 10 users") do
-    100.times{User.limit(10).to_a}
-  end
-
-
-  x.report("pg direct grab 10 users") do
-    100.times do
-      r = ActiveRecord::Base.connection.raw_connection.async_exec("select * from users limit 10")
-      r.fields.each_with_index do |f,i|
-        r.ftype(i)
-      end
-      r.each_row do |x|
-        x
-      end
-    end
-  end
-
-end
-
+# User.select('id, 2 bob').first
+# Benchmark.bmbm do |x|
+# 
+#   x.report("find") do
+#     100.times{User.find(1)}
+#   end
+# 
+#   x.report("grab 10 users created_at") do
+#     100.times{User.limit(10).select(:created_at).to_a}
+#   end
+# 
+#   x.report("grab 10 users id") do
+#     100.times{User.limit(10).select(:id).to_a}
+#   end
+# 
+#   x.report("grab 10 users") do
+#     100.times{User.limit(10).to_a}
+#   end
+# 
+# 
+#   x.report("pg direct grab 10 users") do
+#     100.times do
+#       r = ActiveRecord::Base.connection.raw_connection.async_exec("select * from users limit 10")
+#       r.fields.each_with_index do |f,i|
+#         r.ftype(i)
+#       end
+#       r.each_row do |x|
+#         x
+#       end
+#     end
+#   end
+# 
+# end
+# 
 
 # profile("find") do
 #   User.find(1)

From 2c61828668efe44a443316c4c1a842b1bcb5ed91 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 29 Aug 2013 17:06:27 +1000
Subject: [PATCH 27/84] saving dialog somehow vanished during refactoring
 better error handling for posts that fail to save

---
 app/assets/javascripts/discourse/models/composer.js | 13 ++++++++++++-
 1 file changed, 12 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js
index 4b6046ba6..bb33e6456 100644
--- a/app/assets/javascripts/discourse/models/composer.js
+++ b/app/assets/javascripts/discourse/models/composer.js
@@ -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);
       });
     });
   },

From b41fa4988a50100d36457dff259d0f68455174ca Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 29 Aug 2013 21:23:00 +1000
Subject: [PATCH 28/84] update rails 4 ensure you clear the tmp/cache folder on
 each bench (so results are correct, there is an incompat upgrade) add facter
 gem which will be used to collect hardware data from the bencher

---
 Gemfile_rails4.lock            |  2 +-
 config/environments/profile.rb |  2 +-
 script/bench.rb                | 17 +++++++++++++++--
 3 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index 7f387e83f..de3fc0bac 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -33,7 +33,7 @@ GIT
 
 GIT
   remote: git://github.com/rails/rails.git
-  revision: e36692a7466011ab51393ac8ca6dfffcb9d79ec0
+  revision: 78624ffedbf98c49743a60e00aa1351c9a48f54b
   branch: 4-0-stable
   specs:
     actionmailer (4.0.0)
diff --git a/config/environments/profile.rb b/config/environments/profile.rb
index d2aa9c330..2e51ad468 100644
--- a/config/environments/profile.rb
+++ b/config/environments/profile.rb
@@ -37,7 +37,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
diff --git a/script/bench.rb b/script/bench.rb
index 37dacb551..7c7cd5831 100644
--- a/script/bench.rb
+++ b/script/bench.rb
@@ -2,12 +2,21 @@ require "socket"
 require "csv"
 require "yaml"
 
-@timings = {}
-
 def run(command)
   system(command, out: $stdout, err: :out)
 end
 
+begin
+  require 'facter'
+rescue LoadError
+  run "gem install facter"
+  puts "just installed the facter gem, please re-run script"
+  exit
+end
+
+@timings = {}
+
+
 def measure(name)
   start = Time.now
   yield
@@ -98,6 +107,8 @@ def bench(path)
 end
 
 begin
+  # critical cause cache may be incompatible or something
+  `rm -fr tmp/cache`
   pid = spawn("bundle exec thin start -p #{@port}")
 
   while port_available? @port
@@ -119,6 +130,8 @@ begin
     "rails4?" => ENV["RAILS4"] == "1"
   }.to_yaml)
 
+  # TODO include Facter.to_hash ... for all facts
+
 ensure
   Process.kill "KILL", pid
 end

From cc5b958d9e8bdc58c881cb012d760c0cdb25508f Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 29 Aug 2013 21:34:32 +1000
Subject: [PATCH 29/84] update with some facts

---
 script/bench.rb | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/script/bench.rb b/script/bench.rb
index 7c7cd5831..59df8c5fc 100644
--- a/script/bench.rb
+++ b/script/bench.rb
@@ -122,13 +122,21 @@ begin
 
   puts "Your Results: (note for timings- percentile is first, duration is second in millisecs)"
 
+  facts = Facter.to_hash
+
+  facts.delete_if{|k,v|
+    !["operatingsystem","architecture","kernelversion",
+    "memorysize", "physicalprocessorcount", "processor0",
+    "virtual"].include?(k)
+  }
+
   puts({
     "home_page" => home_page,
     "topic_page" => topic_page,
     "timings" => @timings,
     "ruby-version" => "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}",
     "rails4?" => ENV["RAILS4"] == "1"
-  }.to_yaml)
+  }.merge(facts).to_yaml)
 
   # TODO include Facter.to_hash ... for all facts
 

From 192c9c5dc223c5b52b721df0a204a0350cd99347 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Thu, 29 Aug 2013 14:54:09 +0200
Subject: [PATCH 30/84] Add imagemagick dependency

---
 docs/INSTALL-ubuntu.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/docs/INSTALL-ubuntu.md b/docs/INSTALL-ubuntu.md
index d202d6248..8ff5bbea6 100644
--- a/docs/INSTALL-ubuntu.md
+++ b/docs/INSTALL-ubuntu.md
@@ -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

From d68721240042448b78ea621364a27a9408e6edfe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Thu, 29 Aug 2013 15:29:40 +0200
Subject: [PATCH 31/84] clockwork is not used anymore

---
 docs/INSTALL-ubuntu.md | 1 -
 1 file changed, 1 deletion(-)

diff --git a/docs/INSTALL-ubuntu.md b/docs/INSTALL-ubuntu.md
index d202d6248..58935d4a3 100644
--- a/docs/INSTALL-ubuntu.md
+++ b/docs/INSTALL-ubuntu.md
@@ -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

From 1dedfb22518db503ae9d61621d87c21b1f31d3d5 Mon Sep 17 00:00:00 2001
From: Emili Parreno <emili@eparreno.com>
Date: Thu, 29 Aug 2013 16:15:02 +0200
Subject: [PATCH 32/84] fix user_name_suggester spec

---
 spec/components/user_name_suggester_spec.rb | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/spec/components/user_name_suggester_spec.rb b/spec/components/user_name_suggester_spec.rb
index 0922a0b0f..a70be9afd 100644
--- a/spec/components/user_name_suggester_spec.rb
+++ b/spec/components/user_name_suggester_spec.rb
@@ -10,6 +10,9 @@ describe UserNameSuggester do
   end
 
   describe '.suggest' do
+    before do
+      User.stubs(:username_length).returns(3..15)
+    end
 
     it "doesn't raise an error on nil username" do
       UserNameSuggester.suggest(nil).should be_nil
@@ -67,4 +70,4 @@ describe UserNameSuggester do
     end
   end
 
-end
\ No newline at end of file
+end

From f74e68eece143a5787af0f4e40c86589aa22e032 Mon Sep 17 00:00:00 2001
From: Michael Kessler <michi@netzpiraten.ch>
Date: Thu, 29 Aug 2013 16:24:27 +0200
Subject: [PATCH 33/84] Update German client-side translations.

---
 config/locales/client.de.yml | 160 ++++++++++++++++++++++++++++-------
 1 file changed, 130 insertions(+), 30 deletions(-)

diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml
index 372cb2401..96e28f16c 100644
--- a/config/locales/client.de.yml
+++ b/config/locales/client.de.yml
@@ -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&hellip; '
       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"
+
+

From bbd79aafd10fa7fa9ed1d0798b1b59465674bb17 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Thu, 29 Aug 2013 11:45:27 -0400
Subject: [PATCH 34/84] Revert "Replaced delegator methods with Forwardable
 def_delegator in Topic"

This reverts commit 3ed037940f464bc1ab32c0751dfe7df476ac31cb.
---
 app/models/topic.rb | 40 ++++++++++++++++++++++++++++++----------
 1 file changed, 30 insertions(+), 10 deletions(-)

diff --git a/app/models/topic.rb b/app/models/topic.rb
index f297edbe5..5fe6a17bd 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -10,16 +10,6 @@ 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!, :notifiy_regular!
-  def_delegator :notifier, :muted!, :notify_muted!
-  def_delegator :notifier, :toggle_mute, :toggle_mute
 
   def self.max_sort_order
     2**31 - 1
@@ -31,6 +21,14 @@ 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)
@@ -563,12 +561,34 @@ 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)

From 3cec95a2c35acb2f460dcb37dc2ffb7a9f042c07 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Thu, 29 Aug 2013 11:38:51 -0400
Subject: [PATCH 35/84] Better API for parsing out blocks in the parser.

---
 .../discourse/dialects/bbcode_dialect.js      | 145 +++++-------------
 .../javascripts/discourse/dialects/dialect.js |  58 +++++++
 .../discourse/dialects/github_code_dialect.js |  77 +---------
 spec/components/pretty_text_spec.rb           |   2 +-
 test/javascripts/components/bbcode_test.js    |   4 +-
 test/javascripts/components/markdown_test.js  |   2 +-
 6 files changed, 107 insertions(+), 181 deletions(-)

diff --git a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
index 8ea9825b4..90c528bf8 100644
--- a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
@@ -102,128 +102,61 @@ replaceBBCodeParams("color", function(param, contents) {
   }
 });
 
-Discourse.Dialect.on("register", function(event) {
+// Handles `[code] ... [/code]` blocks
+Discourse.Dialect.replaceBlock({
+  start: /(\[code\])([\s\S]*)/igm,
+  stop: '[/code]',
 
-  var dialect = event.dialect,
-      MD = event.MD;
+  emitter: function(blockContents) {
+    return ['p', ['pre'].concat(blockContents)];
+  }
+});
 
-  /**
-    Support BBCode [code] blocks
+// Support BBCode [quote] blocks
+Discourse.Dialect.replaceBlock({
+  start: new RegExp("\\[quote=?([^\\[\\]]+)?\\]([\\s\\S]*)", "igm"),
+  stop: '[/quote]',
+  emitter: function(blockContents, matches, options) {
 
-    @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);
+    var paramsString = matches[1].replace(/\"/g, ''),
+        params = {'class': 'quote'},
+        paramsSplit = paramsString.split(/\, */),
+        username = paramsSplit[0];
 
-    if (m) {
-      var contents = m[1].trim().split("\n");
+    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 html = ['pre', "\n"];
-      contents.forEach(function (n) {
-        html.push(n.trim());
-        html.push(["br"]);
-        html.push("\n");
-      });
-
-      return [m[0].length, html];
+    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);
     }
-  };
 
-  /**
-    Support BBCode [quote] blocks
+    var contents = this.processInline(blockContents.join("  \n  \n"));
+    contents.unshift('blockquote');
 
-    @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,
+    return ['p', ['aside', params,
                    ['div', {'class': 'title'},
                      ['div', {'class': 'quote-controls'}],
                      avatarImg ? avatarImg : "",
-                     I18n.t('user.said',{username: username})
+                     I18n.t('user.said', {username: username})
                    ],
                    contents
-                ]]);
-      return result;
-    }
-  };
-
+                ]];
+  }
 });
 
-
 Discourse.Dialect.on("parseNode", function(event) {
-
   var node = event.node,
       path = event.path;
 
diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index 9ee2d24a4..ba208fac9 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -204,6 +204,64 @@ Discourse.Dialect = {
     };
   },
 
+  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.
diff --git a/app/assets/javascripts/discourse/dialects/github_code_dialect.js b/app/assets/javascripts/discourse/dialects/github_code_dialect.js
index c7b0b3786..1d70d4551 100644
--- a/app/assets/javascripts/discourse/dialects/github_code_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/github_code_dialect.js
@@ -5,77 +5,12 @@
   @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;
-    }
-  };
-
+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") ]]];
+  }
 });
 
 // Ensure that content in a code block is fully escaped. This way it's not white listed
diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb
index 82ee603d1..169f6cee4 100644
--- a/spec/components/pretty_text_spec.rb
+++ b/spec/components/pretty_text_spec.rb
@@ -14,7 +14,7 @@ describe PrettyText do
       end
 
       it "produces a quote even with new lines in it" do
-        PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]").should match_html "<p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">EvilTrout said:</div>\n<blockquote>ddd<br>\n</blockquote></aside></p>"
+        PrettyText.cook("[quote=\"EvilTrout, post:123, topic:456, full:true\"]ddd\n[/quote]").should match_html "<p><aside class=\"quote\" data-post=\"123\" data-topic=\"456\" data-full=\"true\"><div class=\"title\">\n<div class=\"quote-controls\"></div>\n<img width=\"20\" height=\"20\" src=\"http://test.localhost/uploads/default/avatars/42d/57c/46ce7ee487/40.png\" class=\"avatar\">EvilTrout said:</div>\n<blockquote>ddd</blockquote></aside></p>"
       end
 
       it "should produce a quote" do
diff --git a/test/javascripts/components/bbcode_test.js b/test/javascripts/components/bbcode_test.js
index 71416e267..a72e86559 100644
--- a/test/javascripts/components/bbcode_test.js
+++ b/test/javascripts/components/bbcode_test.js
@@ -11,8 +11,8 @@ test('basic bbcode', function() {
   format("[i]emphasis[/i]", "<span class=\"bbcode-i\">emphasis</span>", "italics text");
   format("[u]underlined[/u]", "<span class=\"bbcode-u\">underlined</span>", "underlines text");
   format("[s]strikethrough[/s]", "<span class=\"bbcode-s\">strikethrough</span>", "strikes-through text");
-  format("[code]\nx++\n[/code]", "<pre>\nx++<br/>\n</pre>", "makes code into pre");
-  format("[code]\nx++\ny++\nz++\n[/code]", "<pre>\nx++<br/>\ny++<br/>\nz++<br/>\n</pre>", "makes code into pre");
+  format("[code]\nx++\n[/code]", "<pre>\nx++</pre>", "makes code into pre");
+  format("[code]\nx++\ny++\nz++\n[/code]", "<pre>\nx++\ny++\nz++</pre>", "makes code into pre");
   format("[spoiler]it's a sled[/spoiler]", "<span class=\"spoiler\">it's a sled</span>", "supports spoiler tags");
   format("[img]http://eviltrout.com/eviltrout.png[/img]", "<img src=\"http://eviltrout.com/eviltrout.png\"/>", "links images");
   format("[url]http://bettercallsaul.com[/url]", "<a href=\"http://bettercallsaul.com\">http://bettercallsaul.com</a>", "supports [url] without a title");
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index b4a22bf1b..118164da5 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -114,7 +114,7 @@ test("Quotes", function() {
   cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n[/quote]",
                 { topicId: 2 },
                 "<p><aside class=\"quote\" data-post=\"1\"><div class=\"title\"><div class=\"quote-controls\"></div>eviltrout said:</div><blockquote>" +
-                "a quote<br/><br/>second line<br/></blockquote></aside></p>",
+                "a quote<br/><br/>second line</blockquote></aside></p>",
                 "works with multiple lines");
 
   cookedOptions("1[quote=\"bob, post:1\"]my quote[/quote]2",

From 53cc1a4730e4d93f2a86a8e2d494ee93fa674462 Mon Sep 17 00:00:00 2001
From: Michael Kessler <michi@netzpiraten.ch>
Date: Thu, 29 Aug 2013 18:10:20 +0200
Subject: [PATCH 36/84] Update German server-side translations.

---
 config/locales/server.de.yml | 169 +++++++++++++++++++++++++++++------
 1 file changed, 141 insertions(+), 28 deletions(-)

diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml
index 2058fc286..a2f903e88 100644
--- a/config/locales/server.de.yml
+++ b/config/locales/server.de.yml
@@ -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} &mdash; **Danke, dass Du ein neues Thema erstellst!**
 
-      Beachte dabei bitte die Folgenden Dinge:
-
       - Ist der Titel eines adäquate Beschreibung dessen, was ein Nutzer vorzufinden erwartet, wenn er dieses Thema aufruft?
 
       - 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} &mdash; **Danke für deinen Beitrag zum Thema!**
 
-      Beachte bitte folgende Dinge während des Schreibens:
-
       - Fügt dein Beitrag dem Gespräch etwas Neues hinzu, und sei es auch wenig?
 
       - 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?"

From 425b59d381b38e01920b92923d726e0f4917a322 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Thu, 29 Aug 2013 12:26:05 -0400
Subject: [PATCH 37/84] FIX: Don't name two routes the same. It breaks in Ember
 RC8.

---
 app/assets/javascripts/discourse/routes/application_routes.js   | 2 +-
 .../javascripts/discourse/routes/topic_from_params_route.js     | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
index 5eadf489a..663314d3f 100644
--- a/app/assets/javascripts/discourse/routes/application_routes.js
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -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
diff --git a/app/assets/javascripts/discourse/routes/topic_from_params_route.js b/app/assets/javascripts/discourse/routes/topic_from_params_route.js
index 368e5444f..0b8a1455e 100644
--- a/app/assets/javascripts/discourse/routes/topic_from_params_route.js
+++ b/app/assets/javascripts/discourse/routes/topic_from_params_route.js
@@ -58,4 +58,5 @@ Discourse.TopicFromParamsRoute = Discourse.Route.extend({
 
 });
 
+Discourse.TopicFromParamsNearRoute = Discourse.TopicFromParamsRoute;
 

From eb5830f3b03c323e8a5debe2a712ce2bf5bdc8a8 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Thu, 29 Aug 2013 13:11:12 -0400
Subject: [PATCH 38/84] FIX: Make `getURL` available to plugins while they are
 starting up in a similar load order to the client app.

---
 .../javascripts/discourse/dialects/dialect.js   |  8 ++++----
 lib/pretty_text.rb                              | 17 +++++++++++------
 .../assets/javascripts/discourse_emoji.js       |  8 +++++---
 3 files changed, 20 insertions(+), 13 deletions(-)

diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index ba208fac9..5202c4910 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -96,17 +96,17 @@ Discourse.Dialect = {
 
     For example to replace all occurrances of :) with a smile image:
 
-     ```javascript
+    ```javascript
       Discourse.Dialect.inlineReplace(':)', ['img', {src: '/images/smile.gif'}])
     ```
 
     @method inlineReplace
     @param {String} token The token we want to replace
-    @param {Array} jsonml The JsonML to replace it with.
+    @param {Function} emitter A function that emits the JsonML for the replacement.
   **/
-  inlineReplace: function(token, jsonml) {
+  inlineReplace: function(token, emitter) {
     dialect.inline[token] = function(text, match, prev) {
-      return [token.length, jsonml];
+      return [token.length, emitter.call(this, token)];
     };
   },
 
diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb
index eecb5ca7e..8a9603ae2 100644
--- a/lib/pretty_text.rb
+++ b/lib/pretty_text.rb
@@ -104,6 +104,8 @@ 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/better_markdown.js",
               "app/assets/javascripts/discourse/dialects/dialect.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
diff --git a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
index 2f5aced8c..de90a3d8c 100644
--- a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
+++ b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
@@ -24,9 +24,11 @@
     ":$"   : 'blush'
   };
 
-  Discourse.Dialect.on('register', function() {
-    Object.keys(translations).forEach(function (code) {
-      Discourse.Dialect.inlineReplace(code, imageFor(translations[code]));
+  Object.keys(translations).forEach(function (code) {
+
+    var replacement = translations[code];
+    Discourse.Dialect.inlineReplace(code, function (code) {
+      return imageFor(replacement);
     });
   });
 

From 45b9f8048a122132827757a03c893d56f0a3fe4a Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Thu, 29 Aug 2013 13:59:41 -0400
Subject: [PATCH 39/84] Documentation update to dialect

---
 .../javascripts/discourse/dialects/dialect.js | 30 ++++++++++++++++++-
 1 file changed, 29 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index 5202c4910..bac59993b 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -97,7 +97,10 @@ Discourse.Dialect = {
     For example to replace all occurrances of :) with a smile image:
 
     ```javascript
-      Discourse.Dialect.inlineReplace(':)', ['img', {src: '/images/smile.gif'}])
+      Discourse.Dialect.inlineReplace(':)', function (text) {
+        return ['img', {src: '/images/smile.png'}];
+      });
+
     ```
 
     @method inlineReplace
@@ -204,6 +207,31 @@ Discourse.Dialect = {
     };
   },
 
+  /**
+    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;

From 1204eb62c31f75dbaf976757da8536e35aeecfcb Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Thu, 29 Aug 2013 14:15:05 -0400
Subject: [PATCH 40/84] FIX: Escape contents when using the quote tool.

---
 app/assets/javascripts/discourse/components/quote.js | 4 ++++
 test/javascripts/components/bbcode_test.js           | 4 +++-
 2 files changed, 7 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/components/quote.js b/app/assets/javascripts/discourse/components/quote.js
index 496a2dc99..54692464c 100644
--- a/app/assets/javascripts/discourse/components/quote.js
+++ b/app/assets/javascripts/discourse/components/quote.js
@@ -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, "&lt;")
+                           .replace(/>/g, "&gt;");
+
     result = "[quote=\"" + post.get('username') + ", post:" + post.get('post_number') + ", topic:" + post.get('topic_id');
 
     /* Strip the HTML from cooked */
diff --git a/test/javascripts/components/bbcode_test.js b/test/javascripts/components/bbcode_test.js
index a72e86559..60112f708 100644
--- a/test/javascripts/components/bbcode_test.js
+++ b/test/javascripts/components/bbcode_test.js
@@ -62,7 +62,6 @@ test("quotes", function() {
 
   formatQuote("lorem", "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n", "correctly formats quotes");
 
-
   formatQuote("  lorem \t  ",
               "[quote=\"eviltrout, post:1, topic:2\"]\nlorem\n[/quote]\n\n",
               "trims white spaces before & after the quoted contents");
@@ -75,6 +74,9 @@ test("quotes", function() {
               "[quote=\"eviltrout, post:1, topic:2, full:true\"]\n**lorem** ipsum\n[/quote]\n\n",
                "keeps BBCode formatting");
 
+  formatQuote("this is <not> a bug",
+              "[quote=\"eviltrout, post:1, topic:2\"]\nthis is &lt;not&gt; a bug\n[/quote]\n\n",
+              "it escapes the contents of the quote");
 });
 
 test("quote formatting", function() {

From c99cf64d70a19e39382b9e634372afb155636d08 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Thu, 29 Aug 2013 14:42:31 -0400
Subject: [PATCH 41/84] FIX: Quoting within code blocks.

---
 .../discourse/dialects/bbcode_dialect.js      | 60 ------------------
 .../javascripts/discourse/dialects/dialect.js |  5 +-
 .../discourse/dialects/github_code_dialect.js |  2 +-
 .../discourse/dialects/quote_dialect.js       | 62 +++++++++++++++++++
 test/javascripts/components/markdown_test.js  |  6 ++
 5 files changed, 73 insertions(+), 62 deletions(-)
 create mode 100644 app/assets/javascripts/discourse/dialects/quote_dialect.js

diff --git a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
index 90c528bf8..9216deada 100644
--- a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js
@@ -112,63 +112,3 @@ Discourse.Dialect.replaceBlock({
   }
 });
 
-// Support BBCode [quote] blocks
-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']);
-    }
-  }
-
-});
diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index bac59993b..302597b66 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -255,7 +255,10 @@ Discourse.Dialect = {
 
         result.push(para);
       }
-      if (m[2]) { next.unshift(MD.mk_block(m[2], null, lineNumber + 1)); }
+
+      if (m[2]) {
+        next.unshift(MD.mk_block(m[2], null, lineNumber + 1));
+      }
 
       lineNumber++;
       while (next.length > 0) {
diff --git a/app/assets/javascripts/discourse/dialects/github_code_dialect.js b/app/assets/javascripts/discourse/dialects/github_code_dialect.js
index 1d70d4551..f24efb0dd 100644
--- a/app/assets/javascripts/discourse/dialects/github_code_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/github_code_dialect.js
@@ -6,7 +6,7 @@
   @namespace Discourse.Dialect
 **/
 Discourse.Dialect.replaceBlock({
-  start: /^`{3}([^\n]+)?\n?([\s\S]*)?/gm,
+  start: /^`{3}([^\n\[\]]+)?\n?([\s\S]*)?/gm,
   stop: '```',
   emitter: function(blockContents, matches) {
     return ['p', ['pre', ['code', {'class': matches[1] || 'lang-auto'}, blockContents.join("\n") ]]];
diff --git a/app/assets/javascripts/discourse/dialects/quote_dialect.js b/app/assets/javascripts/discourse/dialects/quote_dialect.js
new file mode 100644
index 000000000..7bece862c
--- /dev/null
+++ b/app/assets/javascripts/discourse/dialects/quote_dialect.js
@@ -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']);
+    }
+  }
+
+});
\ No newline at end of file
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index 118164da5..19ecf7f36 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -247,6 +247,12 @@ test("Code Blocks", function() {
   cooked("```ruby\nhello `eviltrout`\n```",
          "<p><pre><code class=\"ruby\">hello &#x60;eviltrout&#x60;</code></pre></p>",
          "it allows code with backticks in it");
+
+
+  cooked("```[quote=\"sam, post:1, topic:9441, full:true\"]This is `<not>` a bug.[/quote]```",
+         "<p><pre><code class=\"lang-auto\">[quote=&quot;sam, post:1, topic:9441, full:true&quot;]This is &#x60;&lt;not&gt;&#x60; a bug.[/quote]</code></pre></p>",
+         "it allows code with backticks in it");
+
 });
 
 test("SanitizeHTML", function() {

From 380a6c9e9d3422a01061cc01e4c6f91f43b92b8d Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Thu, 29 Aug 2013 15:18:27 -0400
Subject: [PATCH 42/84] FIX: Blockquotes prefixed by spaces

---
 .../discourse/dialects/bold_italics_dialect.js  | 17 +++++++++++++++++
 .../javascripts/external/better_markdown.js     |  4 +++-
 test/javascripts/components/markdown_test.js    | 11 +++++++++++
 3 files changed, 31 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
index b77b2e304..7cea70c84 100644
--- a/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
+++ b/app/assets/javascripts/discourse/dialects/bold_italics_dialect.js
@@ -23,3 +23,20 @@ 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;
+
+  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 [];
+    }
+  };
+
+});
\ No newline at end of file
diff --git a/app/assets/javascripts/external/better_markdown.js b/app/assets/javascripts/external/better_markdown.js
index 4d5da8935..df9905106 100644
--- a/app/assets/javascripts/external/better_markdown.js
+++ b/app/assets/javascripts/external/better_markdown.js
@@ -10,6 +10,8 @@
 
   * 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!
 
@@ -750,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 );
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index 19ecf7f36..ef2d5e9db 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -109,6 +109,17 @@ test("Links", function() {
 
 });
 
+test("simple quotes", function() {
+  cooked("> nice!", "<blockquote><p>nice!</p></blockquote>", "it supports simple quotes");
+  cooked(" > nice!", "<blockquote><p>nice!</p></blockquote>", "it allows quotes with preceeding spaces");
+  cooked("> level 1\n> > level 2",
+         "<blockquote><p>level 1</p><blockquote><p>level 2</p></blockquote></blockquote>",
+         "it allows nesting of blockquotes");
+  cooked("> level 1\n>  > level 2",
+         "<blockquote><p>level 1</p><blockquote><p>level 2</p></blockquote></blockquote>",
+         "it allows nesting of blockquotes with spaces");
+});
+
 test("Quotes", function() {
 
   cookedOptions("[quote=\"eviltrout, post: 1\"]\na quote\n\nsecond line\n[/quote]",

From 33800969a8f8b54019303abd2e120d24e89b052a Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Fri, 30 Aug 2013 16:44:03 +1000
Subject: [PATCH 43/84] upgraded the ruby racer and mini profiler

---
 Gemfile             |  3 ++-
 Gemfile.lock        | 10 +++++-----
 Gemfile_rails4.lock | 10 +++++-----
 3 files changed, 12 insertions(+), 11 deletions(-)

diff --git a/Gemfile b/Gemfile
index 85b02277d..98640deb4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -187,7 +187,8 @@ 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
 
 # used for caching, optional
 # redis-rack-cache is missing a sane expiry policy, it hogs redis
diff --git a/Gemfile.lock b/Gemfile.lock
index e0988b534..d5bca75c7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -219,7 +219,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)
@@ -296,7 +296,7 @@ GEM
       rack (>= 0.4)
     rack-cors (0.2.7)
       rack
-    rack-mini-profiler (0.1.29)
+    rack-mini-profiler (0.1.30)
       rack (>= 1.1.3)
     rack-openid (1.3.1)
       rack (>= 1.1.0)
@@ -425,8 +425,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)
@@ -507,7 +507,7 @@ DEPENDENCIES
   qunit-rails
   rack-cache
   rack-cors
-  rack-mini-profiler (= 0.1.29)
+  rack-mini-profiler (= 0.1.30)
   rails (= 3.2.12)
   rails_multisite!
   rake
diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index de3fc0bac..b2bdbd1a6 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -256,7 +256,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)
@@ -336,7 +336,7 @@ GEM
       rack (>= 0.4)
     rack-cors (0.2.8)
       rack
-    rack-mini-profiler (0.1.29)
+    rack-mini-profiler (0.1.30)
       rack (>= 1.1.3)
     rack-openid (1.3.1)
       rack (>= 1.1.0)
@@ -434,8 +434,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)
@@ -517,7 +517,7 @@ DEPENDENCIES
   qunit-rails
   rack-cache
   rack-cors
-  rack-mini-profiler (= 0.1.29)
+  rack-mini-profiler (= 0.1.30)
   rails!
   rails-observers
   rails_multisite!

From b730b27c4fd815edac9a4365ddb9f4a6396b283c Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Fri, 30 Aug 2013 16:44:17 +1000
Subject: [PATCH 44/84] some soample counter methods

---
 config/initializers/06-mini_profiler.rb | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb
index a7a5dbec5..8e24637a5 100644
--- a/config/initializers/06-mini_profiler.rb
+++ b/config/initializers/06-mini_profiler.rb
@@ -41,6 +41,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

From 23d8f5fec7b6feab4599268cd2a221591f87659f Mon Sep 17 00:00:00 2001
From: Einar Jonsson <einar@codelette.com>
Date: Fri, 30 Aug 2013 09:12:44 +0000
Subject: [PATCH 45/84] Replaced delegator methods with Forwardable
 def_delegator in Topic

---
 app/models/topic.rb | 40 ++++++++++------------------------------
 1 file changed, 10 insertions(+), 30 deletions(-)

diff --git a/app/models/topic.rb b/app/models/topic.rb
index 5fe6a17bd..8087c8492 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -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)

From 432f9bf5cfc5065dc090c16780e1c055dc890f9b Mon Sep 17 00:00:00 2001
From: Gaurish Sharma <contact@gaurishsharma.com>
Date: Fri, 30 Aug 2013 18:26:59 +0530
Subject: [PATCH 46/84] Rake Commands can be chained

We can run all three setup related commands in one go, making it easier for people to copy-paste. Also it would be bit faster, I would imagine.
---
 docs/DEVELOPER-ADVANCED.md | 8 +++-----
 1 file changed, 3 insertions(+), 5 deletions(-)

diff --git a/docs/DEVELOPER-ADVANCED.md b/docs/DEVELOPER-ADVANCED.md
index 9f6be8df6..a75068dfe 100644
--- a/docs/DEVELOPER-ADVANCED.md
+++ b/docs/DEVELOPER-ADVANCED.md
@@ -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!
 

From df5cbf76e002c46e4b66c2e000d30eab4b448441 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Fri, 30 Aug 2013 10:43:54 -0400
Subject: [PATCH 47/84] Move `insideGroup` into the helper, not the class for
 grouped each

---
 app/assets/javascripts/discourse/helpers/grouped_each.js | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/app/assets/javascripts/discourse/helpers/grouped_each.js b/app/assets/javascripts/discourse/helpers/grouped_each.js
index f0632acae..218232aca 100644
--- a/app/assets/javascripts/discourse/helpers/grouped_each.js
+++ b/app/assets/javascripts/discourse/helpers/grouped_each.js
@@ -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();
 });
\ No newline at end of file

From 63f2187d72356dedc26d5e1d727580f2ba87b132 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Fri, 30 Aug 2013 10:56:41 -0400
Subject: [PATCH 48/84] FIX: Don't do intraword italics when prefixed by a
 forward slash

---
 app/assets/javascripts/discourse/dialects/dialect.js | 2 +-
 test/javascripts/components/markdown_test.js         | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/assets/javascripts/discourse/dialects/dialect.js b/app/assets/javascripts/discourse/dialects/dialect.js
index 302597b66..58bb55f7a 100644
--- a/app/assets/javascripts/discourse/dialects/dialect.js
+++ b/app/assets/javascripts/discourse/dialects/dialect.js
@@ -64,7 +64,7 @@ function invalidBoundary(args, prev) {
   var last = prev[prev.length - 1];
   if (typeof last !== "string") { return; }
 
-  if (args.wordBoundary && (!last.match(/\W$/))) { return true; }
+  if (args.wordBoundary && (last.match(/(\w|\/)$/))) { return true; }
   if (args.spaceBoundary && (!last.match(/\s$/))) { return true; }
 }
 
diff --git a/test/javascripts/components/markdown_test.js b/test/javascripts/components/markdown_test.js
index ef2d5e9db..9f53d0705 100644
--- a/test/javascripts/components/markdown_test.js
+++ b/test/javascripts/components/markdown_test.js
@@ -23,6 +23,7 @@ test("basic cooking", function() {
   cooked("_trout_", "<p><em>trout</em></p>", "it italicizes text.");
   cooked("***hello***", "<p><strong><em>hello</em></strong></p>", "it can do bold and italics at once.");
   cooked("word_with_underscores", "<p>word_with_underscores</p>", "it doesn't do intraword italics");
+  cooked("common/_special_font_face.html.erb", "<p>common/_special_font_face.html.erb</p>", "it doesn't intraword with a slash");
   cooked("hello \\*evil\\*", "<p>hello *evil*</p>", "it supports escaping of asterisks");
   cooked("hello \\_evil\\_", "<p>hello _evil_</p>", "it supports escaping of italics");
   cooked("brussel sproutes are *awful*.", "<p>brussel sproutes are <em>awful</em>.</p>", "it doesn't swallow periods.");

From 1a38f66c7e20e7736a06fdb2ffa5d13580eb7844 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Fri, 30 Aug 2013 17:27:24 +0200
Subject: [PATCH 49/84] removed warning about already existing constants

---
 lib/plugin/metadata.rb | 2 +-
 lib/text_sentinel.rb   | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/lib/plugin/metadata.rb b/lib/plugin/metadata.rb
index 0d5aa308b..65686ede4 100644
--- a/lib/plugin/metadata.rb
+++ b/lib/plugin/metadata.rb
@@ -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)
diff --git a/lib/text_sentinel.rb b/lib/text_sentinel.rb
index b16e76132..e7a6b3b33 100644
--- a/lib/text_sentinel.rb
+++ b/lib/text_sentinel.rb
@@ -5,7 +5,7 @@ class TextSentinel
 
   attr_accessor :text
 
-  ENTROPY_SCALE = 0.7
+  ENTROPY_SCALE ||= 0.7
 
   def initialize(text, opts=nil)
     @opts = opts || {}

From 9d6cb6aae4b0682d9598d3a91277534b54dcfce2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9gis=20Hanol?= <regis@hanol.fr>
Date: Fri, 30 Aug 2013 17:27:42 +0200
Subject: [PATCH 50/84] added content-disposition header for uploads on S3

---
 lib/file_store/s3_store.rb | 38 +++++++++++++++++---------------------
 1 file changed, 17 insertions(+), 21 deletions(-)

diff --git a/lib/file_store/s3_store.rb b/lib/file_store/s3_store.rb
index 02f9a29c7..4644625ce 100644
--- a/lib/file_store/s3_store.rb
+++ b/lib/file_store/s3_store.rb
@@ -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

From 46efbac40ee3623449a008c40c8093968c40cc08 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Fri, 30 Aug 2013 12:32:05 -0400
Subject: [PATCH 51/84] Add "Unread" filter to messages tab. Rename the other
 two views.

---
 .../controllers/user_activity_controller.js   |  3 +-
 .../discourse/routes/application_routes.js    |  3 +-
 .../discourse/routes/user_routes.js           | 40 ++++++++-----------
 .../templates/user/activity.js.handlebars     |  9 +++--
 app/controllers/list_controller.rb            |  8 ++++
 config/locales/client.en.yml                  |  6 ++-
 config/routes.rb                              |  1 +
 lib/topic_query.rb                            |  5 +++
 8 files changed, 45 insertions(+), 30 deletions(-)

diff --git a/app/assets/javascripts/discourse/controllers/user_activity_controller.js b/app/assets/javascripts/discourse/controllers/user_activity_controller.js
index 038d786bc..e765f09fe 100644
--- a/app/assets/javascripts/discourse/controllers/user_activity_controller.js
+++ b/app/assets/javascripts/discourse/controllers/user_activity_controller.js
@@ -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')
 });
diff --git a/app/assets/javascripts/discourse/routes/application_routes.js b/app/assets/javascripts/discourse/routes/application_routes.js
index 663314d3f..5772dc6a3 100644
--- a/app/assets/javascripts/discourse/routes/application_routes.js
+++ b/app/assets/javascripts/discourse/routes/application_routes.js
@@ -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() {
diff --git a/app/assets/javascripts/discourse/routes/user_routes.js b/app/assets/javascripts/discourse/routes/user_routes.js
index 9ff40c4b1..ab8d8699b 100644
--- a/app/assets/javascripts/discourse/routes/user_routes.js
+++ b/app/assets/javascripts/discourse/routes/user_routes.js
@@ -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({
diff --git a/app/assets/javascripts/discourse/templates/user/activity.js.handlebars b/app/assets/javascripts/discourse/templates/user/activity.js.handlebars
index bdf292ff0..03cd3c61d 100644
--- a/app/assets/javascripts/discourse/templates/user/activity.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/user/activity.js.handlebars
@@ -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}}
diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb
index 9dfada7b4..a46976c50 100644
--- a/app/controllers/list_controller.rb
+++ b/app/controllers/list_controller.rb
@@ -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])
 
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index fb6642845..d3b74cd09 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -188,7 +188,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"
@@ -202,6 +201,11 @@ en:
       moderator: "{{user}} is a moderator"
       admin: "{{user}} is an admin"
 
+      messages:
+        all: "All"
+        mine: "Mine"
+        unread: "Unread"
+
       change_password:
         success: "(email sent)"
         in_progress: "(sending email)"
diff --git a/config/routes.rb b/config/routes.rb
index e4c81d8cf..40283267c 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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+/}
diff --git a/lib/topic_query.rb b/lib/topic_query.rb
index 766fb8178..a39e4293c 100644
--- a/lib/topic_query.rb
+++ b/lib/topic_query.rb
@@ -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|

From e06356ebbe597f38673235a86728a9181af23ca8 Mon Sep 17 00:00:00 2001
From: Einar Jonsson <einar@codelette.com>
Date: Fri, 30 Aug 2013 17:39:31 +0000
Subject: [PATCH 52/84] Extracted a fake_admin method

---
 app/models/category_featured_topic.rb | 16 ++++++++++------
 1 file changed, 10 insertions(+), 6 deletions(-)

diff --git a/app/models/category_featured_topic.rb b/app/models/category_featured_topic.rb
index 5bbdc3aca..3329cd4b2 100644
--- a/app/models/category_featured_topic.rb
+++ b/app/models/category_featured_topic.rb
@@ -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

From d875a9549da2750101ef3adc22f3ab92fe33d22f Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Fri, 30 Aug 2013 13:41:43 -0400
Subject: [PATCH 53/84] Version bump to v0.9.6.2

---
 lib/version.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/version.rb b/lib/version.rb
index c54f87566..4b1ff0085 100644
--- a/lib/version.rb
+++ b/lib/version.rb
@@ -5,7 +5,7 @@ module Discourse
       MAJOR = 0
       MINOR = 9
       TINY  = 6
-      PRE   = 1
+      PRE   = 2
 
       STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
     end

From 96313ce742a8695ee62ee8137333775ee788c1b8 Mon Sep 17 00:00:00 2001
From: Jeff Atwood <jatwood@discourse.org>
Date: Fri, 30 Aug 2013 21:08:58 -0700
Subject: [PATCH 54/84] better copy for repeated URL blocking

---
 config/locales/server.en.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 98f2bdb8b..772cf368e 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -937,9 +937,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}).
 

From c4ecfad78a58aaef9d1caaad824eaeec00cb4af8 Mon Sep 17 00:00:00 2001
From: Kane York <rikingcoding@gmail.com>
Date: Fri, 30 Aug 2013 22:50:31 -0700
Subject: [PATCH 55/84] Add "nose" faces to the translations table

as per http://meta.discourse.org/t/use-more-standard-smiley-codes-instead-of-smile/1822/44
---
 .../vendor/assets/javascripts/discourse_emoji.js     | 12 ++++++++++++
 1 file changed, 12 insertions(+)

diff --git a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
index de90a3d8c..b85c81157 100644
--- a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
+++ b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
@@ -12,16 +12,28 @@
   // Also support default emotions
   var translations = {
     ':)'   : 'smile',
+    ':-)'   : 'smile',
     ':('   : 'frowning',
+    ':-('   : 'frowning',
     ';)'   : 'wink',
+    ';-)'   : 'wink',
     ':\'(' : 'cry',
+    ':\'-(' : 'cry',
+    ':-\'(' : 'cry',
     ':P'   : 'stuck_out_tongue',
+    ':-P'   : 'stuck_out_tongue',
     ':O'   : 'open_mouth',
+    ':-O'   : 'open_mouth',
     ':D'   : 'grin',
+    ':-D'   : 'grin',
     ':|'   : 'expressionless',
+    ':-|'   : 'expressionless',
     ";P"   : 'stuck_out_tongue_winking_eye',
+    ";-P"   : 'stuck_out_tongue_winking_eye',
     ';)'   : 'wink',
+    ';-)'   : 'wink',
     ":$"   : 'blush'
+    ":-$"   : 'blush'
   };
 
   Object.keys(translations).forEach(function (code) {

From 0089b10e25a4888c141dac9bf3e8497aa1a6789a Mon Sep 17 00:00:00 2001
From: Jeff Atwood <jatwood@discourse.org>
Date: Fri, 30 Aug 2013 23:08:29 -0700
Subject: [PATCH 56/84] remove footer newline from digest emails, is ugly

---
 config/locales/server.en.yml | 2 +-
 config/locales/server.id.yml | 2 +-
 config/locales/server.ko.yml | 2 +-
 config/locales/server.sv.yml | 2 +-
 4 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 772cf368e..69274dc08 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -1027,7 +1027,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"
diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml
index 217ab1fe6..3988ece34 100644
--- a/config/locales/server.id.yml
+++ b/config/locales/server.id.yml
@@ -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"
 
diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml
index 165d5133f..6025c3a21 100644
--- a/config/locales/server.ko.yml
+++ b/config/locales/server.ko.yml
@@ -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"
 
diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml
index 7aac74e9b..5e74f96fe 100644
--- a/config/locales/server.sv.yml
+++ b/config/locales/server.sv.yml
@@ -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"
 

From ee79a28569ec7443f9adba254354b1ef3cd051bc Mon Sep 17 00:00:00 2001
From: ofGEEK <admin@ofgeek.com>
Date: Mon, 2 Sep 2013 10:09:11 +0800
Subject: [PATCH 57/84] Update client.zh_CN.yml

---
 config/locales/client.zh_CN.yml | 9 +++++++--
 1 file changed, 7 insertions(+), 2 deletions(-)

diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml
index c93720873..e6c9de24c 100644
--- a/config/locales/client.zh_CN.yml
+++ b/config/locales/client.zh_CN.yml
@@ -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:

From d3c5afbb80ecc44a89dce3ebc868889216677acc Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Mon, 2 Sep 2013 17:14:41 +1000
Subject: [PATCH 58/84] reduce sidetiq frequency remove minutely() schedule
 that was very inefficient

---
 config/initializers/sidekiq.rb | 7 ++++++-
 lib/jobs/dashboard_stats.rb    | 4 ++--
 2 files changed, 8 insertions(+), 3 deletions(-)

diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 5a7d2c0e5..3dfd5587a 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -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
diff --git a/lib/jobs/dashboard_stats.rb b/lib/jobs/dashboard_stats.rb
index 285f01f7c..ffb0c3155 100644
--- a/lib/jobs/dashboard_stats.rb
+++ b/lib/jobs/dashboard_stats.rb
@@ -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
\ No newline at end of file
+end

From 529fe3245300e50b822a0425c8695b202012bd87 Mon Sep 17 00:00:00 2001
From: Anton Batenev <antonbatenev@yandex.ru>
Date: Mon, 2 Sep 2013 18:48:47 +0400
Subject: [PATCH 59/84] Russian translation (actual version)

---
 config/locales/client.ru.yml |  9 +++++++--
 config/locales/server.ru.yml | 22 ++++++++++++++++------
 2 files changed, 23 insertions(+), 8 deletions(-)

diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml
index d59566975..790226f05 100644
--- a/config/locales/client.ru.yml
+++ b/config/locales/client.ru.yml
@@ -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: Загрузить изображение
diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml
index b01188a9f..b014bc2d7 100644
--- a/config/locales/server.ru.yml
+++ b/config/locales/server.ru.yml
@@ -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: Читать еще

From f13a408c2a5bb9addde7f3366a92621df7c78bb7 Mon Sep 17 00:00:00 2001
From: Jeff Atwood <jatwood@discourse.org>
Date: Mon, 2 Sep 2013 22:41:19 -0700
Subject: [PATCH 60/84] Omit Needless Words on new user JIT popups

---
 config/locales/server.en.yml | 4 ++--
 config/locales/server.id.yml | 4 ++--
 config/locales/server.ko.yml | 4 ++--
 3 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 69274dc08..3c407dc85 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -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} &mdash; **thanks for contributing to the conversation!**
+      Welcome to %{site_name} &mdash; **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}.
 
diff --git a/config/locales/server.id.yml b/config/locales/server.id.yml
index 3988ece34..6f0a013b5 100644
--- a/config/locales/server.id.yml
+++ b/config/locales/server.id.yml
@@ -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} &mdash; **thanks for contributing to the conversation!**
+      Welcome to %{site_name} &mdash; **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}.
 
diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml
index 6025c3a21..84c01a165 100644
--- a/config/locales/server.ko.yml
+++ b/config/locales/server.ko.yml
@@ -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} &mdash; **thanks for contributing to the conversation!**
+      Welcome to %{site_name} &mdash; **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}.
 

From 46d5314ec482f76cf75e29bc22325bc0b4564aec Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Tue, 3 Sep 2013 17:39:56 +1000
Subject: [PATCH 61/84] Improve Rails4 performance by 20%, before the median on
 the page is 53ms, after it is 44ms

---
 lib/site_setting_extension.rb | 21 +++++++++------------
 1 file changed, 9 insertions(+), 12 deletions(-)

diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index 2dc91d2d8..d365fd3a6 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -231,30 +231,27 @@ 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
+      c = @@containers[provider.current_site]
+      c = c[name] if c
+      c
+    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

From 51eb764345302224e43737876b67012b62284981 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Tue, 3 Sep 2013 17:46:39 +1000
Subject: [PATCH 62/84] mini profiler update to use latest flame graph engine

---
 Gemfile                                 | 4 +++-
 Gemfile.lock                            | 8 +++++++-
 Gemfile_rails4.lock                     | 8 +++++++-
 config/initializers/06-mini_profiler.rb | 1 +
 4 files changed, 18 insertions(+), 3 deletions(-)

diff --git a/Gemfile b/Gemfile
index 98640deb4..76f6bf403 100644
--- a/Gemfile
+++ b/Gemfile
@@ -188,7 +188,9 @@ gem 'lru_redux'
 #  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.30', require: false
+# 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
diff --git a/Gemfile.lock b/Gemfile.lock
index d5bca75c7..693cb70d7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -180,9 +180,14 @@ GEM
     fast_blank (0.0.1)
       rake
       rake-compiler
+    fast_stack (0.0.3)
+      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)
@@ -474,6 +479,7 @@ DEPENDENCIES
   fast_xor!
   fast_xs
   fastimage
+  flamegraph
   fog
   handlebars-source (= 1.0.12)
   highline
@@ -507,7 +513,7 @@ DEPENDENCIES
   qunit-rails
   rack-cache
   rack-cors
-  rack-mini-profiler (= 0.1.30)
+  rack-mini-profiler
   rails (= 3.2.12)
   rails_multisite!
   rake
diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index b2bdbd1a6..a603a9234 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -216,9 +216,14 @@ GEM
     fast_blank (0.0.1)
       rake
       rake-compiler
+    fast_stack (0.0.3)
+      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)
@@ -484,6 +489,7 @@ DEPENDENCIES
   fast_xor!
   fast_xs
   fastimage
+  flamegraph
   fog
   handlebars-source (= 1.0.12)
   highline
@@ -517,7 +523,7 @@ DEPENDENCIES
   qunit-rails
   rack-cache
   rack-cors
-  rack-mini-profiler (= 0.1.30)
+  rack-mini-profiler
   rails!
   rails-observers
   rails_multisite!
diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb
index 8e24637a5..35765a144 100644
--- a/config/initializers/06-mini_profiler.rb
+++ b/config/initializers/06-mini_profiler.rb
@@ -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

From 1b9079ed20387396a9e1b1d735ca86a1178e4bc0 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Tue, 3 Sep 2013 17:58:30 +1000
Subject: [PATCH 63/84] centralize logic so mockers stop mocking me

---
 lib/site_setting_extension.rb | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb
index d365fd3a6..f9f181f7a 100644
--- a/lib/site_setting_extension.rb
+++ b/lib/site_setting_extension.rb
@@ -240,9 +240,7 @@ module SiteSettingExtension
     end
 
     define_singleton_method :#{clean_name}? do
-      c = @@containers[provider.current_site]
-      c = c[name] if c
-      c
+      #{clean_name}
     end
 
     define_singleton_method :#{clean_name}= do |val|

From ba6576efd12a1ed53a2f0a218ec316b5d83bf717 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Tue, 3 Sep 2013 18:32:41 +1000
Subject: [PATCH 64/84] rails 4 update

---
 Gemfile_rails4.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index a603a9234..4c59d6afb 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -33,7 +33,7 @@ GIT
 
 GIT
   remote: git://github.com/rails/rails.git
-  revision: 78624ffedbf98c49743a60e00aa1351c9a48f54b
+  revision: 8487692903c05e7ba94b7b02c19e96589be091d4
   branch: 4-0-stable
   specs:
     actionmailer (4.0.0)
@@ -272,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)

From 5dd199df261bc5c5cdcfac2a84a2d60e1d85f27d Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Tue, 3 Sep 2013 18:58:56 +1000
Subject: [PATCH 65/84] bench fix and new arel patches for rails 4

---
 lib/freedom_patches/arel_patch.rb | 8 ++++++++
 script/bench.rb                   | 2 ++
 2 files changed, 10 insertions(+)
 create mode 100644 lib/freedom_patches/arel_patch.rb

diff --git a/lib/freedom_patches/arel_patch.rb b/lib/freedom_patches/arel_patch.rb
new file mode 100644
index 000000000..3d03f6494
--- /dev/null
+++ b/lib/freedom_patches/arel_patch.rb
@@ -0,0 +1,8 @@
+if rails4?
+  # https://github.com/rails/arel/pull/206
+  class Arel::Table
+    def hash
+      @name.hash
+    end
+  end
+end
diff --git a/script/bench.rb b/script/bench.rb
index 59df8c5fc..71797eba4 100644
--- a/script/bench.rb
+++ b/script/bench.rb
@@ -117,6 +117,8 @@ begin
 
   puts "Starting benchmark..."
 
+  # asset precompilation is a dog, wget to force it
+  run "wget http://127.0.0.1:#{@port}/ -o tmp/test.html"
   home_page = bench("/")
   topic_page = bench("/t/oh-how-i-wish-i-could-shut-up-like-a-tunnel-for-so/69")
 

From 7b62f9ccd7f4e730c51db005e39410d96736356c Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Tue, 3 Sep 2013 19:11:55 +1000
Subject: [PATCH 66/84] more fixes for rails 4

---
 lib/freedom_patches/ar_result.rb | 33 ++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)
 create mode 100644 lib/freedom_patches/ar_result.rb

diff --git a/lib/freedom_patches/ar_result.rb b/lib/freedom_patches/ar_result.rb
new file mode 100644
index 000000000..99e38fe8d
--- /dev/null
+++ b/lib/freedom_patches/ar_result.rb
@@ -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

From 438dd9759d788bc7f3de12b3d12e58567fb960b6 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Tue, 3 Sep 2013 19:25:46 +1000
Subject: [PATCH 67/84] latest mini profiler

---
 Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 693cb70d7..cdb582a6b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -301,7 +301,7 @@ GEM
       rack (>= 0.4)
     rack-cors (0.2.7)
       rack
-    rack-mini-profiler (0.1.30)
+    rack-mini-profiler (0.1.31)
       rack (>= 1.1.3)
     rack-openid (1.3.1)
       rack (>= 1.1.0)

From 9edef9b0174caaf3818175f4323a902aae65ea56 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Tue, 3 Sep 2013 19:29:17 +1000
Subject: [PATCH 68/84] rails 4 mini profiler update

---
 Gemfile_rails4.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index 4c59d6afb..28f24d51c 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -341,7 +341,7 @@ GEM
       rack (>= 0.4)
     rack-cors (0.2.8)
       rack
-    rack-mini-profiler (0.1.30)
+    rack-mini-profiler (0.1.31)
       rack (>= 1.1.3)
     rack-openid (1.3.1)
       rack (>= 1.1.0)

From 212f1363ae4e3756a193e5fe8f8b6e5f88ab3c70 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 4 Sep 2013 06:45:17 +1000
Subject: [PATCH 69/84] update fast_stack, to fix gem install issue

---
 Gemfile.lock        | 2 +-
 Gemfile_rails4.lock | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index cdb582a6b..4f726ccea 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -180,7 +180,7 @@ GEM
     fast_blank (0.0.1)
       rake
       rake-compiler
-    fast_stack (0.0.3)
+    fast_stack (0.0.4)
       rake
       rake-compiler
     fast_xs (0.8.0)
diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index 28f24d51c..349d219be 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -216,7 +216,7 @@ GEM
     fast_blank (0.0.1)
       rake
       rake-compiler
-    fast_stack (0.0.3)
+    fast_stack (0.0.4)
       rake
       rake-compiler
     fast_xs (0.8.0)

From b47eedba0030e123eb1c40d4eaf4074f399ed298 Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Tue, 3 Sep 2013 19:12:22 -0400
Subject: [PATCH 70/84] Add min_trust_to_create_topic setting to require a
 certain trust level before users can start new topics

---
 .../min_trust_to_create_topic_setting.rb       | 18 ++++++++++++++++++
 app/models/site_setting.rb                     |  2 ++
 config/locales/server.en.yml                   |  2 ++
 lib/guardian.rb                                |  4 ++--
 spec/components/guardian_spec.rb               | 15 +++++++++++++++
 5 files changed, 39 insertions(+), 2 deletions(-)
 create mode 100644 app/models/min_trust_to_create_topic_setting.rb

diff --git a/app/models/min_trust_to_create_topic_setting.rb b/app/models/min_trust_to_create_topic_setting.rb
new file mode 100644
index 000000000..a38031d1b
--- /dev/null
+++ b/app/models/min_trust_to_create_topic_setting.rb
@@ -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
diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 53004c947..88c6e2f75 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -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, '')
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 3c407dc85..0b061deb2 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -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"
diff --git a/lib/guardian.rb b/lib/guardian.rb
index cea13bc93..d5bd9e6a9 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -229,11 +229,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
     )
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index 84afc362a..a176a0713 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -283,6 +283,21 @@ describe Guardian do
         category.save
         Guardian.new(user).can_create?(Topic,category).should be_false
       end
+
+      it "is true for new users by default" do
+        Guardian.new(user).can_create?(Topic,Fabricate(:category)).should be_true
+      end
+
+      it "is false if user has not met minimum trust level" do
+        SiteSetting.stubs(:min_trust_to_create_topic).returns(1)
+        Guardian.new(build(:user, trust_level: 0)).can_create?(Topic,Fabricate(:category)).should be_false
+      end
+
+      it "is true if user has met or exceeded the minimum trust level" do
+        SiteSetting.stubs(:min_trust_to_create_topic).returns(1)
+        Guardian.new(build(:user, trust_level: 1)).can_create?(Topic,Fabricate(:category)).should be_true
+        Guardian.new(build(:user, trust_level: 2)).can_create?(Topic,Fabricate(:category)).should be_true
+      end
     end
 
     describe 'a Post' do

From 22f2a0c1f409119a3e3fc6fa4c7bc4e795fb40c8 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 4 Sep 2013 09:39:16 +1000
Subject: [PATCH 71/84] update rails 4, use log level info in profile for
 better accuracy

---
 Gemfile_rails4.lock            | 2 +-
 config/environments/profile.rb | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index 349d219be..513364a77 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -33,7 +33,7 @@ GIT
 
 GIT
   remote: git://github.com/rails/rails.git
-  revision: 8487692903c05e7ba94b7b02c19e96589be091d4
+  revision: 025b63db308fbbf942a3bc2673d4aadab968c524
   branch: 4-0-stable
   specs:
     actionmailer (4.0.0)
diff --git a/config/environments/profile.rb b/config/environments/profile.rb
index 2e51ad468..31a652e0a 100644
--- a/config/environments/profile.rb
+++ b/config/environments/profile.rb
@@ -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

From 9977f3098caefc4cf56a33753a4a36eb8fe8777c Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 4 Sep 2013 10:40:53 +1000
Subject: [PATCH 72/84] fast stack needs upgrading, had a bug

---
 Gemfile.lock        | 2 +-
 Gemfile_rails4.lock | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/Gemfile.lock b/Gemfile.lock
index 4f726ccea..79a1ee0bb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -180,7 +180,7 @@ GEM
     fast_blank (0.0.1)
       rake
       rake-compiler
-    fast_stack (0.0.4)
+    fast_stack (0.0.5)
       rake
       rake-compiler
     fast_xs (0.8.0)
diff --git a/Gemfile_rails4.lock b/Gemfile_rails4.lock
index 513364a77..28d1aa381 100644
--- a/Gemfile_rails4.lock
+++ b/Gemfile_rails4.lock
@@ -216,7 +216,7 @@ GEM
     fast_blank (0.0.1)
       rake
       rake-compiler
-    fast_stack (0.0.4)
+    fast_stack (0.0.5)
       rake
       rake-compiler
     fast_xs (0.8.0)

From 61d3e43744ca7fe4f87f65a2576e58578cdd10b7 Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Wed, 4 Sep 2013 15:02:04 +1000
Subject: [PATCH 73/84] better split logic for names starting with capitols eg:
 ABBob should split to AB Bob

---
 .../discourse/components/formatter.js         | 28 +++++++++++++++----
 test/javascripts/components/formatter_test.js |  1 +
 2 files changed, 23 insertions(+), 6 deletions(-)

diff --git a/app/assets/javascripts/discourse/components/formatter.js b/app/assets/javascripts/discourse/components/formatter.js
index 8a21385a4..85f0de23a 100644
--- a/app/assets/javascripts/discourse/components/formatter.js
+++ b/app/assets/javascripts/discourse/components/formatter.js
@@ -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);
     }
diff --git a/test/javascripts/components/formatter_test.js b/test/javascripts/components/formatter_test.js
index 1ff9678b6..0767ca379 100644
--- a/test/javascripts/components/formatter_test.js
+++ b/test/javascripts/components/formatter_test.js
@@ -199,5 +199,6 @@ test("breakUp", function(){
   equal(b("HeMans"), "He Mans");
   equal(b("he_man"), "he_ man");
   equal(b("he11111"), "he 11111");
+  equal(b("HRCBob"), "HRC Bob");
 
 });

From 808225f43ad405fe629d1041077caae6d99748b0 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 4 Sep 2013 10:57:31 -0400
Subject: [PATCH 74/84] FIX: missing comma

---
 .../vendor/assets/javascripts/discourse_emoji.js                | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
index b85c81157..b6d07a32e 100644
--- a/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
+++ b/vendor/gems/discourse_emoji/vendor/assets/javascripts/discourse_emoji.js
@@ -32,7 +32,7 @@
     ";-P"   : 'stuck_out_tongue_winking_eye',
     ';)'   : 'wink',
     ';-)'   : 'wink',
-    ":$"   : 'blush'
+    ":$"   : 'blush',
     ":-$"   : 'blush'
   };
 

From 117fc8db58db6d9ebb9534ac6a41b1b336efffef Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Tue, 3 Sep 2013 17:19:29 -0400
Subject: [PATCH 75/84] Change the way nuked users' posts are handled. Allow
 null in the user_id column of posts. Show these posts in the posts stream.

---
 .../javascripts/discourse/models/post.js      |  1 +
 .../discourse/templates/post.js.handlebars    | 17 +++++++---
 .../application/topic-post.css.scss           |  4 +++
 app/models/post.rb                            |  4 +--
 app/models/user.rb                            | 32 +++++++++++--------
 app/serializers/basic_post_serializer.rb      |  6 ++--
 app/serializers/post_serializer.rb            | 10 +++---
 .../post_stream_serializer_mixin.rb           | 14 ++++----
 config/locales/client.en.yml                  |  1 +
 ...30903154323_allow_null_user_id_on_posts.rb | 12 +++++++
 lib/topic_view.rb                             |  4 +--
 lib/user_destroyer.rb                         |  2 +-
 lib/validators/post_validator.rb              |  5 ++-
 spec/components/topic_view_spec.rb            | 19 ++++++++++-
 spec/components/user_destroyer_spec.rb        | 17 +++++-----
 spec/serializers/post_serializer_spec.rb      | 25 +++++++++++++++
 16 files changed, 123 insertions(+), 50 deletions(-)
 create mode 100644 db/migrate/20130903154323_allow_null_user_id_on_posts.rb
 create mode 100644 spec/serializers/post_serializer_spec.rb

diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js
index 1305a3320..ab1e088d2 100644
--- a/app/assets/javascripts/discourse/models/post.js
+++ b/app/assets/javascripts/discourse/models/post.js
@@ -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'); }
diff --git a/app/assets/javascripts/discourse/templates/post.js.handlebars b/app/assets/javascripts/discourse/templates/post.js.handlebars
index 23924a535..7817cd39a 100644
--- a/app/assets/javascripts/discourse/templates/post.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/post.js.handlebars
@@ -17,11 +17,18 @@
     {{/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'>
diff --git a/app/assets/stylesheets/application/topic-post.css.scss b/app/assets/stylesheets/application/topic-post.css.scss
index 806e10ae3..901f23ae0 100644
--- a/app/assets/stylesheets/application/topic-post.css.scss
+++ b/app/assets/stylesheets/application/topic-post.css.scss
@@ -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);
diff --git a/app/models/post.rb b/app/models/post.rb
index 7c76e80c5..c30d4df92 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -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
 #
diff --git a/app/models/user.rb b/app/models/user.rb
index fb2870798..f4b8d9a32 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
 
diff --git a/app/serializers/basic_post_serializer.rb b/app/serializers/basic_post_serializer.rb
index 1fedd6f6f..f200193aa 100644
--- a/app/serializers/basic_post_serializer.rb
+++ b/app/serializers/basic_post_serializer.rb
@@ -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
diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb
index 09f4d9bec..39031cc7a 100644
--- a/app/serializers/post_serializer.rb
+++ b/app/serializers/post_serializer.rb
@@ -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
diff --git a/app/serializers/post_stream_serializer_mixin.rb b/app/serializers/post_stream_serializer_mixin.rb
index 64a717adf..fad70bbeb 100644
--- a/app/serializers/post_stream_serializer_mixin.rb
+++ b/app/serializers/post_stream_serializer_mixin.rb
@@ -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
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index d3b74cd09..dc26b2cd6 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -200,6 +200,7 @@ en:
       change: "change"
       moderator: "{{user}} is a moderator"
       admin: "{{user}} is an admin"
+      deleted: "User Was Deleted"
 
       messages:
         all: "All"
diff --git a/db/migrate/20130903154323_allow_null_user_id_on_posts.rb b/db/migrate/20130903154323_allow_null_user_id_on_posts.rb
new file mode 100644
index 000000000..8a9141268
--- /dev/null
+++ b/db/migrate/20130903154323_allow_null_user_id_on_posts.rb
@@ -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
diff --git a/lib/topic_view.rb b/lib/topic_view.rb
index da30b59d4..bdece17c8 100644
--- a/lib/topic_view.rb
+++ b/lib/topic_view.rb
@@ -89,7 +89,7 @@ class TopicView
 
   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 +256,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?
diff --git a/lib/user_destroyer.rb b/lib/user_destroyer.rb
index 4cd10232c..6e21d67e3 100644
--- a/lib/user_destroyer.rb
+++ b/lib/user_destroyer.rb
@@ -34,7 +34,7 @@ class UserDestroyer
             b = ScreenedEmail.block(u.email, ip_address: u.ip_address)
             b.record_match! if b
           end
-          Post.with_deleted.where(user_id: user.id).update_all("nuked_user = true")
+          Post.with_deleted.where(user_id: user.id).update_all("user_id = NULL")
           StaffActionLogger.new(@staff).log_user_deletion(user, opts.slice(:context))
           DiscourseHub.unregister_nickname(user.username) if SiteSetting.call_discourse_hub?
           MessageBus.publish "/file-change", ["refresh"], user_ids: [user.id]
diff --git a/lib/validators/post_validator.rb b/lib/validators/post_validator.rb
index f8e14a6df..e9ae7704c 100644
--- a/lib/validators/post_validator.rb
+++ b/lib/validators/post_validator.rb
@@ -13,9 +13,12 @@ class Validators::PostValidator < ActiveModel::Validator
   end
 
   def presence(post)
-    [:raw,:user_id,:topic_id].each do |attr_name|
+    [:raw,:topic_id].each do |attr_name|
        post.errors.add(attr_name, :blank, options) if post.send(attr_name).blank?
     end
+    if post.new_record? and post.user_id.nil?
+      post.errors.add(:user_id, :blank, options)
+    end
   end
 
   def stripped_length(post)
diff --git a/spec/components/topic_view_spec.rb b/spec/components/topic_view_spec.rb
index 6ee0831c5..3b21443d0 100644
--- a/spec/components/topic_view_spec.rb
+++ b/spec/components/topic_view_spec.rb
@@ -216,6 +216,7 @@ describe TopicView do
     # Create the posts in a different order than the sort_order
     let!(:p5) { Fabricate(:post, topic: topic, user: coding_horror)}
     let!(:p2) { Fabricate(:post, topic: topic, user: coding_horror)}
+    let!(:p6) { Fabricate(:post, topic: topic, user: Fabricate(:user), deleted_at: Time.now)}
     let!(:p4) { Fabricate(:post, topic: topic, user: coding_horror, deleted_at: Time.now)}
     let!(:p1) { Fabricate(:post, topic: topic, user: first_poster)}
     let!(:p3) { Fabricate(:post, topic: topic, user: first_poster)}
@@ -224,10 +225,12 @@ describe TopicView do
       SiteSetting.stubs(:posts_per_page).returns(3)
 
       # Update them to the sort order we're checking for
-      [p1, p2, p3, p4, p5].each_with_index do |p, idx|
+      [p1, p2, p3, p4, p5, p6].each_with_index do |p, idx|
         p.sort_order = idx + 1
         p.save
       end
+      p6.user_id = nil # user got nuked
+      p6.save!
     end
 
     describe '#filter_posts_paged' do
@@ -236,6 +239,7 @@ describe TopicView do
       it 'returns correct posts for all pages' do
         topic_view.filter_posts_paged(1).should == [p1, p2]
         topic_view.filter_posts_paged(2).should == [p3, p5]
+        topic_view.filter_posts_paged(3).should == []
         topic_view.filter_posts_paged(100).should == []
       end
     end
@@ -271,6 +275,13 @@ describe TopicView do
         near_view.posts.should == [p2, p3, p4]
       end
 
+      it "returns deleted posts by nuked users to an admin" do
+        coding_horror.admin = true
+        near_view = topic_view_near(p5)
+        near_view.desired_post.should == p5
+        near_view.posts.should == [p4, p5, p6]
+      end
+
       context "when 'posts per page' exceeds the number of posts" do
         before { SiteSetting.stubs(:posts_per_page).returns(100) }
 
@@ -278,6 +289,12 @@ describe TopicView do
           near_view = topic_view_near(p5)
           near_view.posts.should == [p1, p2, p3, p5]
         end
+
+        it 'returns deleted posts to admins' do
+          coding_horror.admin = true
+          near_view = topic_view_near(p5)
+          near_view.posts.should == [p1, p2, p3, p4, p5, p6]
+        end
       end
     end
   end
diff --git a/spec/components/user_destroyer_spec.rb b/spec/components/user_destroyer_spec.rb
index 01bb4634b..907dc7d2e 100644
--- a/spec/components/user_destroyer_spec.rb
+++ b/spec/components/user_destroyer_spec.rb
@@ -121,25 +121,26 @@ describe UserDestroyer do
         it "deletes the posts" do
           destroy
           post.reload.deleted_at.should_not be_nil
-          post.nuked_user.should be_true
+          post.user_id.should be_nil
         end
       end
     end
 
+    context 'user has deleted posts' do
+      let!(:deleted_post) { Fabricate(:post, user: @user, deleted_at: 1.hour.ago) }
+      it "should mark the user's deleted posts as belonging to a nuked user" do
+        expect { UserDestroyer.new(@admin).destroy(@user) }.to change { User.count }.by(-1)
+        deleted_post.reload.user_id.should be_nil
+      end
+    end
+
     context 'user has no posts' do
       context 'and destroy succeeds' do
-
         let(:destroy_opts) { {} }
         subject(:destroy) { UserDestroyer.new(@admin).destroy(@user) }
 
         include_examples "successfully destroy a user"
         include_examples "email block list"
-
-        it "should mark the user's deleted posts as belonging to a nuked user" do
-          post = Fabricate(:post, user: @user, deleted_at: 1.hour.ago)
-          expect { destroy }.to change { User.count }.by(-1)
-          post.reload.nuked_user.should be_true
-        end
       end
 
       context 'and destroy fails' do
diff --git a/spec/serializers/post_serializer_spec.rb b/spec/serializers/post_serializer_spec.rb
new file mode 100644
index 000000000..d60261cdc
--- /dev/null
+++ b/spec/serializers/post_serializer_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe PostSerializer do
+
+  context "a post by a nuked user" do
+    let!(:post) { Fabricate(:post, user: Fabricate(:user), deleted_at: Time.zone.now) }
+
+    before do
+      post.user_id = nil
+      post.save!
+    end
+
+    subject { PostSerializer.new(post, scope: Guardian.new(Fabricate(:admin)), root: false).as_json }
+
+    it "serializes correctly" do
+      [:name, :username, :display_username, :avatar_template].each do |attr|
+        subject[attr].should be_nil
+      end
+      [:moderator?, :staff?, :yours, :user_title, :trust_level].each do |attr|
+        subject[attr].should be_false
+      end
+    end
+  end
+
+end

From d76486a48bc9cd50f719fc66d019218a55ea5890 Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Wed, 4 Sep 2013 15:35:10 -0400
Subject: [PATCH 76/84] Topic can have null user_id when user was nuked

---
 app/models/user_action.rb                     | 11 +++++++----
 ...0904181208_allow_null_user_id_on_topics.rb |  9 +++++++++
 lib/user_destroyer.rb                         |  3 +++
 spec/components/user_destroyer_spec.rb        | 19 ++++++++++++++++++-
 4 files changed, 37 insertions(+), 5 deletions(-)
 create mode 100644 db/migrate/20130904181208_allow_null_user_id_on_topics.rb

diff --git a/app/models/user_action.rb b/app/models/user_action.rb
index bb44c60c9..06a43114c 100644
--- a/app/models/user_action.rb
+++ b/app/models/user_action.rb
@@ -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
diff --git a/db/migrate/20130904181208_allow_null_user_id_on_topics.rb b/db/migrate/20130904181208_allow_null_user_id_on_topics.rb
new file mode 100644
index 000000000..48e9b157d
--- /dev/null
+++ b/db/migrate/20130904181208_allow_null_user_id_on_topics.rb
@@ -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
diff --git a/lib/user_destroyer.rb b/lib/user_destroyer.rb
index 6e21d67e3..b41417bbf 100644
--- a/lib/user_destroyer.rb
+++ b/lib/user_destroyer.rb
@@ -25,6 +25,9 @@ class UserDestroyer
             end
           end
           PostDestroyer.new(@staff, post).destroy
+          if post.topic and post.post_number == 1
+            Topic.unscoped.where(id: post.topic.id).update_all(user_id: nil)
+          end
         end
         raise PostsExistError if user.reload.post_count != 0
       end
diff --git a/spec/components/user_destroyer_spec.rb b/spec/components/user_destroyer_spec.rb
index 907dc7d2e..bde129ee0 100644
--- a/spec/components/user_destroyer_spec.rb
+++ b/spec/components/user_destroyer_spec.rb
@@ -87,7 +87,10 @@ describe UserDestroyer do
     end
 
     context 'user has posts' do
-      let!(:post) { Fabricate(:post, user: @user) }
+      let!(:topic_starter) { Fabricate(:user) }
+      let!(:topic) { Fabricate(:topic, user: topic_starter) }
+      let!(:first_post) { Fabricate(:post, user: topic_starter, topic: topic) }
+      let!(:post) { Fabricate(:post, user: @user, topic: topic) }
 
       context "delete_posts is false" do
         subject(:destroy) { UserDestroyer.new(@admin).destroy(@user) }
@@ -123,6 +126,20 @@ describe UserDestroyer do
           post.reload.deleted_at.should_not be_nil
           post.user_id.should be_nil
         end
+
+        it "does not delete topics started by others in which the user has replies" do
+          destroy
+          topic.reload.deleted_at.should be_nil
+          topic.user_id.should_not be_nil
+        end
+
+        it "deletes topics started by the deleted user" do
+          spammer_topic = Fabricate(:topic, user: @user)
+          spammer_post = Fabricate(:post, user: @user, topic: spammer_topic)
+          destroy
+          spammer_topic.reload.deleted_at.should_not be_nil
+          spammer_topic.user_id.should be_nil
+        end
       end
     end
 

From 4933e9d6abffe020401b71455a4724d58e58ce5c Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 5 Sep 2013 09:33:30 +1000
Subject: [PATCH 77/84] todo added to code

---
 lib/topic_view.rb | 1 +
 1 file changed, 1 insertion(+)

diff --git a/lib/topic_view.rb b/lib/topic_view.rb
index bdece17c8..cf088d39d 100644
--- a/lib/topic_view.rb
+++ b/lib/topic_view.rb
@@ -84,6 +84,7 @@ 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
 

From 5b08f73561b105df6046d4af33dc6a91ce8b52cf Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 5 Sep 2013 10:27:34 +1000
Subject: [PATCH 78/84] give god rights of impersonation to developers, must be
 edited into the production.rb config file

---
 config/environments/production.rb.sample |  5 +++++
 lib/guardian.rb                          | 12 ++++++++++--
 spec/components/guardian_spec.rb         |  3 +++
 3 files changed, 18 insertions(+), 2 deletions(-)

diff --git a/config/environments/production.rb.sample b/config/environments/production.rb.sample
index e4dabbd35..c12b534dd 100644
--- a/config/environments/production.rb.sample
+++ b/config/environments/production.rb.sample
@@ -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
diff --git a/lib/guardian.rb b/lib/guardian.rb
index d5bd9e6a9..9b87b82fb 100644
--- a/lib/guardian.rb
+++ b/lib/guardian.rb
@@ -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
diff --git a/spec/components/guardian_spec.rb b/spec/components/guardian_spec.rb
index a176a0713..ad8d1d0ec 100644
--- a/spec/components/guardian_spec.rb
+++ b/spec/components/guardian_spec.rb
@@ -175,6 +175,9 @@ describe Guardian do
       Guardian.new(admin).can_impersonate?(another_admin).should be_false
       Guardian.new(admin).can_impersonate?(user).should be_true
       Guardian.new(admin).can_impersonate?(moderator).should be_true
+
+      Rails.configuration.stubs(:developer_emails).returns([admin.email])
+      Guardian.new(admin).can_impersonate?(another_admin).should be_true
     end
   end
 

From c9321cae7fe1b3d005ebe4bfd80fd3b4b42b601b Mon Sep 17 00:00:00 2001
From: Sam <sam.saffron@gmail.com>
Date: Thu, 5 Sep 2013 10:41:11 +1000
Subject: [PATCH 79/84] Our JS includes belong at the top of the file, that way
 we can start downloading and parsing them earlier and not wait for the rest
 of the page

Our noscript stuff belongs at the bottom, we should not hold off any js work
while noscript is downloading
---
 .../common/_discourse_javascript.html.erb     |  6 ---
 app/views/layouts/application.html.erb        | 42 +++++++++++--------
 2 files changed, 24 insertions(+), 24 deletions(-)

diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb
index e4b4f4d87..d9f58b76d 100644
--- a/app/views/common/_discourse_javascript.html.erb
+++ b/app/views/common/_discourse_javascript.html.erb
@@ -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  %>';
diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb
index 49bde9e01..5f640e2ed 100644
--- a/app/views/layouts/application.html.erb
+++ b/app/views/layouts/application.html.erb
@@ -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>

From 9fdabd3b8aa1e05e96b6ed34288939d03afd389b Mon Sep 17 00:00:00 2001
From: Emili Parreno <emili@eparreno.com>
Date: Thu, 5 Sep 2013 13:22:15 +0200
Subject: [PATCH 80/84] refactor spec_helper

---
 spec/spec_helper.rb                | 72 +-----------------------------
 spec/support/diagnostics_helper.rb | 23 ++++++++++
 spec/support/helpers.rb            | 45 +++++++++++++++++++
 3 files changed, 69 insertions(+), 71 deletions(-)
 create mode 100644 spec/support/diagnostics_helper.rb
 create mode 100644 spec/support/helpers.rb

diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d48bf2643..abe733cdf 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -11,29 +11,6 @@ require 'spork'
 require 'fakeweb'
 FakeWeb.allow_net_connect = false
 
-module Helpers
-
-  def self.next_seq
-    @next_seq = (@next_seq || 0) + 1
-  end
-
-  def log_in(fabricator=nil)
-    user = Fabricate(fabricator || :user)
-    log_in_user(user)
-    user
-  end
-
-  def log_in_user(user)
-    session[:current_user_id] = user.id
-  end
-
-  def fixture_file(filename)
-    return '' if filename == ''
-    file_path = File.expand_path(File.dirname(__FILE__) + '/fixtures/' + filename)
-    File.read(file_path)
-  end
-end
-
 Spork.prefork do
   # Loading more in this block will cause your tests to run faster. However,
   # if you change any configuration or code from libraries loaded here, you'll
@@ -53,15 +30,14 @@ Spork.prefork do
   # in spec/support/ and its subdirectories.
   Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}
 
-
   # let's not run seed_fu every test
   SeedFu.quiet = true if SeedFu.respond_to? :quiet
   SeedFu.seed
 
   RSpec.configure do |config|
-
     config.fail_fast = ENV['RSPEC_FAIL_FAST'] == "1"
     config.include Helpers
+    config.include MessageBus
     config.mock_framework = :mocha
     config.order = 'random'
 
@@ -124,50 +100,6 @@ Spork.each_run do
   $redis.client.reconnect
   Rails.cache.reconnect
   MessageBus.after_fork
-
-end
-
-def build(*args)
-  Fabricate.build(*args)
-end
-
-def create_topic(args={})
-  args[:title] ||= "This is my title #{Helpers.next_seq}"
-  user = args.delete(:user) || Fabricate(:user)
-  guardian = Guardian.new(user)
-  TopicCreator.create(user, guardian, args)
-end
-
-def create_post(args={})
-  args[:title] ||= "This is my title #{Helpers.next_seq}"
-  args[:raw] ||= "This is the raw body of my post, it is cool #{Helpers.next_seq}"
-  args[:topic_id] = args[:topic].id if args[:topic]
-  user = args.delete(:user) || Fabricate(:user)
-  PostCreator.create(user, args)
-end
-
-module MessageBus::DiagnosticsHelper
-  def publish(channel, data, opts = nil)
-    id = super(channel, data, opts)
-    if @tracking
-      m = MessageBus::Message.new(-1, id, channel, data)
-      m.user_ids = opts[:user_ids] if opts
-      m.group_ids = opts[:group_ids] if opts
-      @tracking << m
-    end
-    id
-  end
-
-  def track_publish
-    @tracking = tracking =  []
-    yield
-    @tracking = nil
-    tracking
-  end
-end
-
-module MessageBus
-  extend MessageBus::DiagnosticsHelper
 end
 
 # --- Instructions ---
@@ -198,5 +130,3 @@ end
 #
 # These instructions should self-destruct in 10 seconds.  If they don't, feel
 # free to delete them.
-
-
diff --git a/spec/support/diagnostics_helper.rb b/spec/support/diagnostics_helper.rb
new file mode 100644
index 000000000..5d4c9eed4
--- /dev/null
+++ b/spec/support/diagnostics_helper.rb
@@ -0,0 +1,23 @@
+module MessageBus::DiagnosticsHelper
+  def publish(channel, data, opts = nil)
+    id = super(channel, data, opts)
+    if @tracking
+      m = MessageBus::Message.new(-1, id, channel, data)
+      m.user_ids = opts[:user_ids] if opts
+      m.group_ids = opts[:group_ids] if opts
+      @tracking << m
+    end
+    id
+  end
+
+  def track_publish
+    @tracking = tracking =  []
+    yield
+    @tracking = nil
+    tracking
+  end
+end
+
+module MessageBus
+  extend MessageBus::DiagnosticsHelper
+end
diff --git a/spec/support/helpers.rb b/spec/support/helpers.rb
new file mode 100644
index 000000000..4e4c3f623
--- /dev/null
+++ b/spec/support/helpers.rb
@@ -0,0 +1,45 @@
+module Helpers
+  def self.next_seq
+    @next_seq = (@next_seq || 0) + 1
+  end
+
+  def log_in(fabricator=nil)
+    user = Fabricate(fabricator || :user)
+    log_in_user(user)
+    user
+  end
+
+  def log_in_user(user)
+    session[:current_user_id] = user.id
+  end
+
+  def fixture_file(filename)
+    return '' if filename.blank?
+    file_path = File.expand_path(Rails.root + 'spec/fixtures/' + filename)
+    File.read(file_path)
+  end
+
+  def build(*args)
+    Fabricate.build(*args)
+  end
+
+  def create_topic(args={})
+    args[:title] ||= "This is my title #{Helpers.next_seq}"
+    user = args.delete(:user) || Fabricate(:user)
+    guardian = Guardian.new(user)
+    TopicCreator.create(user, guardian, args)
+  end
+
+  def create_post(args={})
+    args[:title] ||= "This is my title #{Helpers.next_seq}"
+    args[:raw] ||= "This is the raw body of my post, it is cool #{Helpers.next_seq}"
+    args[:topic_id] = args[:topic].id if args[:topic]
+    user = args.delete(:user) || Fabricate(:user)
+    PostCreator.create(user, args)
+  end
+
+  def generate_username(length=10)
+    range = [*'a'..'z']
+    Array.new(length){range.sample}.join
+  end
+end

From f157ec1f91f9f28f0b8bb23367bd358a09b06df9 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 4 Sep 2013 11:53:00 -0400
Subject: [PATCH 81/84] Select +Replies for bulk operations

---
 .../controllers/merge_topic_controller.js     |   8 +-
 .../controllers/split_topic_controller.js     |  17 +--
 .../discourse/controllers/topic_controller.js |  74 +++++++++--
 .../discourse/mixins/selected_posts_count.js  |  11 +-
 .../javascripts/discourse/models/post.js      |  10 +-
 .../discourse/templates/post.js.handlebars    |   5 +-
 .../javascripts/discourse/views/post_view.js  |  13 +-
 .../application/topic-post.css.scss           |  33 ++---
 app/controllers/application_controller.rb     |  10 ++
 app/controllers/posts_controller.rb           |   3 +-
 app/controllers/topics_controller.rb          |   6 +-
 config/jshint.yml                             |   1 +
 config/locales/client.en.yml                  |   1 +
 spec/controllers/posts_controller_spec.rb     |  17 ++-
 spec/controllers/topics_controller_spec.rb    |  21 ++++
 .../avatar_selector_controller_test.js        |   2 +-
 .../controllers/flag_controller_test.js       |   6 +-
 .../controllers/topic_controller_test.js      | 116 +++++++++++++++---
 test/javascripts/helpers/qunit_helpers.js     |   4 +
 test/javascripts/jshint_all.js.erb            |   1 +
 20 files changed, 282 insertions(+), 77 deletions(-)

diff --git a/app/assets/javascripts/discourse/controllers/merge_topic_controller.js b/app/assets/javascripts/discourse/controllers/merge_topic_controller.js
index 9d79f00c3..672654877 100644
--- a/app/assets/javascripts/discourse/controllers/merge_topic_controller.js
+++ b/app/assets/javascripts/discourse/controllers/merge_topic_controller.js
@@ -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
       });
     }
 
diff --git a/app/assets/javascripts/discourse/controllers/split_topic_controller.js b/app/assets/javascripts/discourse/controllers/split_topic_controller.js
index b07b007b0..7d2dd5993 100644
--- a/app/assets/javascripts/discourse/controllers/split_topic_controller.js
+++ b/app/assets/javascripts/discourse/controllers/split_topic_controller.js
@@ -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;
   }
diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js
index 6dc6c23c6..a1c969011 100644
--- a/app/assets/javascripts/discourse/controllers/topic_controller.js
+++ b/app/assets/javascripts/discourse/controllers/topic_controller.js
@@ -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,49 @@ 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 +146,7 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
 
   deselectAll: function() {
     this.get('selectedPosts').clear();
+    this.get('selectedReplies').clear();
     this.set('allPostsSelected', false);
   },
 
@@ -177,19 +216,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();
       }
     });
   },
diff --git a/app/assets/javascripts/discourse/mixins/selected_posts_count.js b/app/assets/javascripts/discourse/mixins/selected_posts_count.js
index e9e6b5496..e30cfdf40 100644
--- a/app/assets/javascripts/discourse/mixins/selected_posts_count.js
+++ b/app/assets/javascripts/discourse/mixins/selected_posts_count.js
@@ -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')
 
 });
 
diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js
index ab1e088d2..7e6f56560 100644
--- a/app/assets/javascripts/discourse/models/post.js
+++ b/app/assets/javascripts/discourse/models/post.js
@@ -328,8 +328,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;
@@ -341,7 +340,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'),
@@ -377,11 +376,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'); })
       }
     });
   },
diff --git a/app/assets/javascripts/discourse/templates/post.js.handlebars b/app/assets/javascripts/discourse/templates/post.js.handlebars
index 7817cd39a..7077f869d 100644
--- a/app/assets/javascripts/discourse/templates/post.js.handlebars
+++ b/app/assets/javascripts/discourse/templates/post.js.handlebars
@@ -32,7 +32,10 @@
     </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}}
diff --git a/app/assets/javascripts/discourse/views/post_view.js b/app/assets/javascripts/discourse/views/post_view.js
index 272266e7f..466c388ee 100644
--- a/app/assets/javascripts/discourse/views/post_view.js
+++ b/app/assets/javascripts/discourse/views/post_view.js
@@ -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'),
 
diff --git a/app/assets/stylesheets/application/topic-post.css.scss b/app/assets/stylesheets/application/topic-post.css.scss
index 901f23ae0..53530595c 100644
--- a/app/assets/stylesheets/application/topic-post.css.scss
+++ b/app/assets/stylesheets/application/topic-post.css.scss
@@ -500,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 {
@@ -519,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 {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index a9b6abbc9..da27cd283 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -191,6 +191,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
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 6a990875e..c3748b7a5 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -150,10 +150,11 @@ 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
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index c2acad8b7..2005e42f1 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -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
diff --git a/config/jshint.yml b/config/jshint.yml
index 7da52be34..588a527d8 100644
--- a/config/jshint.yml
+++ b/config/jshint.yml
@@ -92,6 +92,7 @@ predef:
   - find
   - sinon
   - controllerFor
+  - testController
   - Favcount
 
 browser:  true      # true if the standard browser globals should be predefined
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index dc26b2cd6..1f7bdadb1 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -763,6 +763,7 @@ en:
       multi_select:
         select: 'select'
         selected: 'selected ({{count}})'
+        select_replies: 'select +replies'
         delete: delete selected
         cancel: cancel selecting
         description:
diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb
index 578dafd89..70e5d401d 100644
--- a/spec/controllers/posts_controller_spec.rb
+++ b/spec/controllers/posts_controller_spec.rb
@@ -163,10 +163,10 @@ describe PostsController do
 
       let!(:poster) { log_in(:moderator) }
       let!(:post1) { Fabricate(:post, user: poster, post_number: 2) }
-      let!(:post2) { Fabricate(:post, topic_id: post1.topic_id, user: poster, post_number: 3) }
+      let!(:post2) { Fabricate(:post, topic_id: post1.topic_id, user: poster, post_number: 3, reply_to_post_number: post1.post_number) }
 
       it "raises invalid parameters no post_ids" do
-	lambda { xhr :delete, :destroy_many }.should raise_error(ActionController::ParameterMissing)
+        lambda { xhr :delete, :destroy_many }.should raise_error(ActionController::ParameterMissing)
       end
 
       it "raises invalid parameters with missing ids" do
@@ -189,6 +189,19 @@ describe PostsController do
         xhr :delete, :destroy_many, post_ids: [post1.id, post2.id]
       end
 
+      describe "can delete replies" do
+
+        before do
+          PostReply.create(post_id: post1.id, reply_id: post2.id)
+        end
+
+        it "deletes the post and the reply to it" do
+          Post.any_instance.expects(:destroy).twice
+          xhr :delete, :destroy_many, post_ids: [post1.id], reply_post_ids: [post1.id]
+        end
+
+      end
+
     end
 
   end
diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb
index 787acb756..fb2c61ee3 100644
--- a/spec/controllers/topics_controller_spec.rb
+++ b/spec/controllers/topics_controller_spec.rb
@@ -93,6 +93,27 @@ describe TopicsController do
       end
     end
 
+    describe "moving replied posts" do
+      let!(:user) { log_in(:moderator) }
+      let!(:p1) { Fabricate(:post, user: user) }
+      let!(:topic) { p1.topic }
+      let!(:p2) { Fabricate(:post, topic: topic, user: user, reply_to_post_number: p1.post_number ) }
+
+      context 'success' do
+
+        before do
+          PostReply.create(post_id: p1.id, reply_id: p2.id)
+        end
+
+        it "moves the child posts too" do
+          Topic.any_instance.expects(:move_posts).with(user, [p1.id, p2.id], title: 'blah').returns(topic)
+          xhr :post, :move_posts, topic_id: topic.id, title: 'blah', post_ids: [p1.id], reply_post_ids: [p1.id]
+        end
+      end
+
+    end
+
+
     describe 'moving to an existing topic' do
       let!(:user) { log_in(:moderator) }
       let(:p1) { Fabricate(:post, user: user) }
diff --git a/test/javascripts/controllers/avatar_selector_controller_test.js b/test/javascripts/controllers/avatar_selector_controller_test.js
index 7fd84a30f..9bcc6ad17 100644
--- a/test/javascripts/controllers/avatar_selector_controller_test.js
+++ b/test/javascripts/controllers/avatar_selector_controller_test.js
@@ -7,7 +7,7 @@ var avatarSelector = Em.Object.create({
 module("Discourse.AvatarSelectorController");
 
 test("avatarTemplate", function() {
-  var avatarSelectorController = controllerFor("avatarSelector");
+  var avatarSelectorController = testController(Discourse.AvatarSelectorController);
   avatarSelectorController.setProperties(avatarSelector);
 
   equal(avatarSelectorController.get("avatarTemplate"),
diff --git a/test/javascripts/controllers/flag_controller_test.js b/test/javascripts/controllers/flag_controller_test.js
index a8bbc23dd..57840c567 100644
--- a/test/javascripts/controllers/flag_controller_test.js
+++ b/test/javascripts/controllers/flag_controller_test.js
@@ -16,7 +16,7 @@ var buildAdminUser = function(args) {
 module("Discourse.FlagController canDeleteSpammer");
 
 test("canDeleteSpammer not staff", function(){
-  var flagController = controllerFor('flag', buildPost());
+  var flagController = testController(Discourse.FlagController, buildPost());
   this.stub(Discourse.User, 'currentProp').withArgs('staff').returns(false);
   flagController.set('selected', Discourse.PostActionType.create({name_key: 'spam'}));
   equal(flagController.get('canDeleteSpammer'), false, 'false if current user is not staff');
@@ -29,7 +29,7 @@ var canDeleteSpammer = function(test, postActionType, expected, testName) {
 
 test("canDeleteSpammer spam not selected", function(){
   this.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true);
-  this.flagController = controllerFor('flag', buildPost());
+  this.flagController = testController(Discourse.FlagController, buildPost());
   this.flagController.set('userDetails', buildAdminUser({can_delete_all_posts: true, can_be_deleted: true}));
   canDeleteSpammer(this, 'off_topic', false, 'false if current user is staff, but selected is off_topic');
   canDeleteSpammer(this, 'inappropriate', false, 'false if current user is staff, but selected is inappropriate');
@@ -39,7 +39,7 @@ test("canDeleteSpammer spam not selected", function(){
 
 test("canDeleteSpammer spam selected", function(){
   this.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true);
-  this.flagController = controllerFor('flag', buildPost());
+  this.flagController = testController(Discourse.FlagController, buildPost());
 
   this.flagController.set('userDetails', buildAdminUser({can_delete_all_posts: true, can_be_deleted: true}));
   canDeleteSpammer(this, 'spam', true, 'true if current user is staff, selected is spam, posts and user can be deleted');
diff --git a/test/javascripts/controllers/topic_controller_test.js b/test/javascripts/controllers/topic_controller_test.js
index 950b861f1..f45338c24 100644
--- a/test/javascripts/controllers/topic_controller_test.js
+++ b/test/javascripts/controllers/topic_controller_test.js
@@ -1,22 +1,19 @@
+module("Discourse.TopicController");
 
-var topic = Discourse.Topic.create({
-  title: "Qunit Test Topic",
-  participants: [
-    {id: 1234,
-     post_count: 4,
-     username: "eviltrout"}
-  ]
-});
-
-
-module("Discourse.TopicController", {
-  setup: function() {
-    this.topicController = controllerFor('topic', topic);
-  }
-});
+var buildTopic = function() {
+  return Discourse.Topic.create({
+    title: "Qunit Test Topic",
+    participants: [
+      {id: 1234,
+       post_count: 4,
+       username: "eviltrout"}
+    ]
+  });
+};
 
 test("editingMode", function() {
-  var topicController = this.topicController;
+  var topic = buildTopic(),
+      topicController = testController(Discourse.TopicController, topic);
 
   ok(!topicController.get('editingTopic'), "we are not editing by default");
 
@@ -32,4 +29,89 @@ test("editingMode", function() {
 
   topicController.cancelEditingTopic();
   ok(!topicController.get('editingTopic'), "cancelling edit mode reverts the property value");
-});
\ No newline at end of file
+});
+
+test("toggledSelectedPost", function() {
+  var tc = testController(Discourse.TopicController, buildTopic()),
+      post = Discourse.Post.create({id: 123, post_number: 2}),
+      postStream = tc.get('postStream');
+
+  postStream.appendPost(post);
+  postStream.appendPost(Discourse.Post.create({id: 124, post_number: 3}));
+
+  blank(tc.get('selectedPosts'), "there are no selected posts by default");
+  equal(tc.get('selectedPostsCount'), 0, "there is a selected post count of 0");
+  ok(!tc.postSelected(post), "the post is not selected by default");
+
+  tc.toggledSelectedPost(post);
+  present(tc.get('selectedPosts'), "there is a selectedPosts collection");
+  equal(tc.get('selectedPostsCount'), 1, "there is a selected post now");
+  ok(tc.postSelected(post), "the post is now selected");
+
+  tc.toggledSelectedPost(post);
+  ok(!tc.postSelected(post), "the post is no longer selected");
+
+});
+
+test("selectAll", function() {
+  var tc = testController(Discourse.TopicController, buildTopic()),
+      post = Discourse.Post.create({id: 123, post_number: 2}),
+      postStream = tc.get('postStream');
+
+  postStream.appendPost(post);
+
+  ok(!tc.postSelected(post), "the post is not selected by default");
+  tc.selectAll();
+  ok(tc.postSelected(post), "the post is now selected");
+  ok(tc.get('allPostsSelected'), "all posts are selected");
+  tc.deselectAll();
+  ok(!tc.postSelected(post), "the post is deselected again");
+  ok(!tc.get('allPostsSelected'), "all posts are not selected");
+
+});
+
+test("Automating setting of allPostsSelected", function() {
+  var topic = buildTopic(),
+      tc = testController(Discourse.TopicController, topic),
+      post = Discourse.Post.create({id: 123, post_number: 2}),
+      postStream = tc.get('postStream');
+
+  topic.set('posts_count', 1);
+  postStream.appendPost(post);
+  ok(!tc.get('allPostsSelected'), "all posts are not selected by default");
+
+  tc.toggledSelectedPost(post);
+  ok(tc.get('allPostsSelected'), "all posts are selected if we select the only post");
+
+  tc.toggledSelectedPost(post);
+  ok(!tc.get('allPostsSelected'), "the posts are no longer automatically selected");
+});
+
+test("Select Replies when present", function() {
+  var topic = buildTopic(),
+      tc = testController(Discourse.TopicController, topic),
+      p1 = Discourse.Post.create({id: 1, post_number: 1, reply_count: 1}),
+      p2 = Discourse.Post.create({id: 2, post_number: 2}),
+      p3 = Discourse.Post.create({id: 2, post_number: 3, reply_to_post_number: 1}),
+      postStream = tc.get('postStream');
+
+  ok(!tc.postSelected(p3), "replies are not selected by default");
+  tc.toggledSelectedPostReplies(p1);
+  ok(tc.postSelected(p1), "it selects the post");
+  ok(!tc.postSelected(p2), "it doesn't select a post that's not a reply");
+  ok(tc.postSelected(p3), "it selects a post that is a reply");
+  equal(tc.get('selectedPostsCount'), 2, "it has a selected posts count of two");
+
+  // If we deselected the post whose replies are selected...
+  tc.toggledSelectedPost(p1);
+  ok(!tc.postSelected(p1), "it deselects the post");
+  ok(!tc.postSelected(p3), "it deselects the replies too");
+
+  // If we deselect a reply, it should deselect the parent's replies selected attribute. Weird but what else would make sense?
+  tc.toggledSelectedPostReplies(p1);
+  tc.toggledSelectedPost(p3);
+  ok(tc.postSelected(p1), "the post stays selected");
+  ok(!tc.postSelected(p3), "it deselects the replies too");
+
+});
+
diff --git a/test/javascripts/helpers/qunit_helpers.js b/test/javascripts/helpers/qunit_helpers.js
index 3ad3732e6..5254c24ae 100644
--- a/test/javascripts/helpers/qunit_helpers.js
+++ b/test/javascripts/helpers/qunit_helpers.js
@@ -14,6 +14,10 @@ function integration(name) {
   });
 }
 
+function testController(klass, model) {
+  return klass.create({model: model, container: Discourse.__container__});
+}
+
 function controllerFor(controller, model) {
   var controller = Discourse.__container__.lookup('controller:' + controller);
   if (model) { controller.set('model', model ); }
diff --git a/test/javascripts/jshint_all.js.erb b/test/javascripts/jshint_all.js.erb
index 0515fa670..34b7eea68 100644
--- a/test/javascripts/jshint_all.js.erb
+++ b/test/javascripts/jshint_all.js.erb
@@ -122,6 +122,7 @@ var jsHintOpts = {
     "console",
     "alert",
     "controllerFor",
+    "testController",
     "containsInstance",
     "deepEqual",
     "notEqual",

From 71c1b8b9b92b19413ffd48e9e2acf40662ef6d41 Mon Sep 17 00:00:00 2001
From: Robin Ward <robin.ward@gmail.com>
Date: Wed, 4 Sep 2013 20:50:58 -0400
Subject: [PATCH 82/84] When deleting a post as staff, ask if you want to
 delete direct replies too

---
 .../discourse/controllers/topic_controller.js | 29 +++++++++++++++++--
 .../javascripts/discourse/models/post.js      | 22 ++++++++++----
 app/controllers/posts_controller.rb           |  3 +-
 config/locales/client.en.yml                  |  6 ++++
 spec/controllers/posts_controller_spec.rb     |  6 ++--
 5 files changed, 53 insertions(+), 13 deletions(-)

diff --git a/app/assets/javascripts/discourse/controllers/topic_controller.js b/app/assets/javascripts/discourse/controllers/topic_controller.js
index a1c969011..549682818 100644
--- a/app/assets/javascripts/discourse/controllers/topic_controller.js
+++ b/app/assets/javascripts/discourse/controllers/topic_controller.js
@@ -104,7 +104,6 @@ Discourse.TopicController = Discourse.ObjectController.extend(Discourse.Selected
   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;
@@ -458,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) {
diff --git a/app/assets/javascripts/discourse/models/post.js b/app/assets/javascripts/discourse/models/post.js
index 7e6f56560..1927a769e 100644
--- a/app/assets/javascripts/discourse/models/post.js
+++ b/app/assets/javascripts/discourse/models/post.js
@@ -225,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 {
@@ -248,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' });
   },
 
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index c3748b7a5..78de8e35a 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -159,8 +159,7 @@ class PostsController < ApplicationController
 
     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
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index 1f7bdadb1..cf82f4fdb 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -816,6 +816,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'
diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb
index 70e5d401d..6a63fe5d6 100644
--- a/spec/controllers/posts_controller_spec.rb
+++ b/spec/controllers/posts_controller_spec.rb
@@ -180,12 +180,12 @@ describe PostsController do
       end
 
       it "deletes the post" do
-        Post.any_instance.expects(:destroy).twice
+        PostDestroyer.any_instance.expects(:destroy).twice
         xhr :delete, :destroy_many, post_ids: [post1.id, post2.id]
       end
 
       it "updates the highest read data for the forum" do
-        Topic.expects(:reset_highest)
+        Topic.expects(:reset_highest).twice
         xhr :delete, :destroy_many, post_ids: [post1.id, post2.id]
       end
 
@@ -196,7 +196,7 @@ describe PostsController do
         end
 
         it "deletes the post and the reply to it" do
-          Post.any_instance.expects(:destroy).twice
+          PostDestroyer.any_instance.expects(:destroy).twice
           xhr :delete, :destroy_many, post_ids: [post1.id], reply_post_ids: [post1.id]
         end
 

From cde631cbd1768620dd44373d662d822900a0f494 Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Thu, 5 Sep 2013 11:27:15 -0400
Subject: [PATCH 83/84] Change default of notify_mods_when_user_blocked to
 false

---
 app/models/site_setting.rb                | 2 +-
 spec/services/spam_rules_enforcer_spec.rb | 1 +
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb
index 88c6e2f75..e7f1ae0c9 100644
--- a/app/models/site_setting.rb
+++ b/app/models/site_setting.rb
@@ -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
diff --git a/spec/services/spam_rules_enforcer_spec.rb b/spec/services/spam_rules_enforcer_spec.rb
index 25f711ce3..071571c33 100644
--- a/spec/services/spam_rules_enforcer_spec.rb
+++ b/spec/services/spam_rules_enforcer_spec.rb
@@ -122,6 +122,7 @@ describe SpamRulesEnforcer do
       end
 
       it 'sends private message to moderators' do
+        SiteSetting.stubs(:notify_mods_when_user_blocked).returns(true)
         moderator = Fabricate(:moderator)
         GroupMessage.expects(:create).with do |group, msg_type, params|
           group == Group[:moderators].name and msg_type == :user_automatically_blocked and params[:user].id == user.id

From c2f37d36ab719fa97fb6e5cfc5156a98f42a852d Mon Sep 17 00:00:00 2001
From: Neil Lalonde <neillalonde@gmail.com>
Date: Thu, 5 Sep 2013 15:14:07 -0400
Subject: [PATCH 84/84] Hide flag modal while flag is submitted

---
 app/assets/javascripts/discourse/controllers/flag_controller.js | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/app/assets/javascripts/discourse/controllers/flag_controller.js b/app/assets/javascripts/discourse/controllers/flag_controller.js
index 1284e8479..a573161bb 100644
--- a/app/assets/javascripts/discourse/controllers/flag_controller.js
+++ b/app/assets/javascripts/discourse/controllers/flag_controller.js
@@ -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);
     });
   },