From a546395397bedf63896d01a1284c943a62c0c55f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 14 Jun 2016 14:31:51 -0400 Subject: [PATCH 001/170] REFACTOR: Migrate markdown functionality in ES6 --- .eslintignore | 1 - .../admin/components/ace-editor.js.es6 | 3 +- .../modals/admin-badge-preview.js.es6 | 8 +- .../admin/helpers/preserve-newlines.js.es6 | 3 +- .../admin/models/staff-action-log.js.es6 | 5 +- .../admin/views/admin-backups-logs.js.es6 | 3 +- .../defer/html-sanitizer-bundle.js | 2233 ----------------- app/assets/javascripts/discourse.js | 36 +- .../discourse/components/badge-card.js.es6 | 3 +- .../components/composer-editor.js.es6 | 18 +- .../discourse/components/d-editor.js.es6 | 23 +- .../discourse/components/poster-name.js.es6 | 2 - .../discourse/components/topic-status.js.es6 | 3 +- .../discourse/components/user-stream.js.es6 | 3 +- .../controllers/avatar-selector.js.es6 | 4 +- .../discourse/controllers/composer.js.es6 | 5 +- .../controllers/create-account.js.es6 | 3 +- .../controllers/forgot-password.js.es6 | 3 +- .../controllers/full-page-search.js.es6 | 3 +- .../discourse/controllers/invite.js.es6 | 21 +- .../discourse/controllers/preferences.js.es6 | 3 +- .../discourse/controllers/quote-button.js.es6 | 12 +- .../controllers/upload-selector.js.es6 | 7 +- .../discourse/dialects/autolink_dialect.js | 25 - .../discourse/dialects/bbcode_dialect.js | 182 -- .../dialects/bold_italics_dialect.js | 78 - .../dialects/category_hashtag_dialect.js | 23 - .../discourse/dialects/censored_dialect.js | 3 - .../discourse/dialects/code_dialect.js | 81 - .../discourse/dialects/html_dialect.js | 51 - .../discourse/dialects/mention_dialect.js | 45 - .../discourse/dialects/nested_link_dialect.js | 21 - .../discourse/dialects/newline_dialect.js | 32 - .../discourse/dialects/onebox_dialect.js | 79 - .../discourse/dialects/quote_dialect.js | 61 - .../discourse/dialects/table_dialect.js | 51 - .../helpers/bound-avatar-template.js.es6 | 5 +- .../discourse/helpers/bound-avatar.js.es6 | 3 +- .../discourse/helpers/cook-text.js.es6 | 6 +- .../javascripts/discourse/helpers/i18n.js.es6 | 10 +- .../discourse/helpers/user-avatar.js.es6 | 3 +- .../discourse/helpers/user-status.js.es6 | 3 +- .../initializers/enable-emoji.js.es6 | 25 +- .../discourse/lib/autocomplete.js.es6 | 106 +- .../discourse/lib/category-hashtags.js.es6 | 3 +- .../discourse/lib/censored-words.js | 25 - .../discourse/lib/click-track.js.es6 | 3 +- .../discourse/lib/emoji/emoji.js.erb | 265 -- .../{emoji-groups.js.es6 => groups.js.es6} | 7 +- .../{emoji-toolbar.js.es6 => toolbar.js.es6} | 12 +- .../discourse/lib/load-script.js.es6 | 4 +- .../javascripts/discourse/lib/markdown.js | 268 -- .../javascripts/discourse/lib/onebox.js | 106 - .../discourse/lib/plugin-api.js.es6 | 3 +- .../javascripts/discourse/lib/text.js.es6 | 33 + .../javascripts/discourse/lib/url.js.es6 | 3 +- .../javascripts/discourse/lib/utilities.js | 327 --- .../discourse/lib/utilities.js.es6 | 302 +++ .../discourse/mixins/upload.js.es6 | 8 +- .../discourse/models/composer.js.es6 | 7 +- .../javascripts/discourse/models/post.js.es6 | 11 +- .../javascripts/discourse/models/store.js.es6 | 1 + .../models/topic-tracking-state.js.es6 | 3 +- .../javascripts/discourse/models/topic.js.es6 | 10 +- .../discourse/models/user-action.js.es6 | 5 +- .../discourse/models/user-stream.js.es6 | 3 +- .../javascripts/discourse/models/user.js.es6 | 3 +- .../discourse/routes/app-route-map.js.es6 | 4 +- .../discourse/routes/build-topic-route.js.es6 | 3 +- .../routes/discovery-categories.js.es6 | 3 +- .../discourse/routes/queued-posts.js.es6 | 6 - .../discourse/templates/application.hbs | 1 + .../discourse/views/history.js.es6 | 3 +- .../javascripts/discourse/views/topic.js.es6 | 3 +- .../discourse/widgets/emoji.js.es6 | 3 +- .../widgets/notification-item.js.es6 | 10 +- .../discourse/widgets/post-links.js.es6 | 3 +- .../javascripts/discourse/widgets/post.js.es6 | 5 +- .../discourse/widgets/topic-status.js.es6 | 3 +- app/assets/javascripts/main_include.js | 11 +- app/assets/javascripts/pretty-text-bundle.js | 13 + .../pretty-text/censored-words.js.es6 | 19 + .../javascripts/pretty-text/emoji.js.es6 | 107 + .../pretty-text/emoji/data.js.es6.erb | 28 + .../engines/discourse-markdown.js.es6} | 825 +++--- .../discourse-markdown/auto-link.js.es6 | 26 + .../engines/discourse-markdown/bbcode.js.es6 | 148 ++ .../discourse-markdown/bold-italics.js.es6 | 71 + .../category-hashtag.js.es6 | 17 + .../discourse-markdown/censored.js.es6 | 13 + .../engines/discourse-markdown/code.js.es6 | 81 + .../engines/discourse-markdown/emoji.js.es6 | 106 + .../engines/discourse-markdown/html.js.es6 | 50 + .../discourse-markdown/mentions.js.es6 | 49 + .../engines/discourse-markdown/newline.js.es6 | 27 + .../engines/discourse-markdown/onebox.js.es6 | 68 + .../engines/discourse-markdown/quote.js.es6 | 61 + .../engines/discourse-markdown/table.js.es6 | 33 + .../javascripts/pretty-text/guid.js.es6 | 14 + .../javascripts/pretty-text/oneboxer.js.es6 | 54 + .../pretty-text/pretty-text.js.es6 | 63 + .../javascripts/pretty-text/sanitizer.js.es6 | 108 + .../pretty-text/white-lister.js.es6 | 152 ++ app/assets/javascripts/pretty-text/xss.js.es6 | 3 + app/assets/javascripts/vendor.js | 2 - app/helpers/deferred_scripts_helper.rb | 16 - .../common/_discourse_javascript.html.erb | 10 - app/views/layouts/application.html.erb | 4 +- config/application.rb | 8 +- lib/cooked_post_processor.rb | 1 + lib/discourse_plugin_registry.rb | 15 - .../tilt/es6_module_transpiler_template.rb | 13 + lib/plugin/instance.rb | 39 - lib/pretty_text.rb | 291 +-- lib/pretty_text/helpers.rb | 73 + lib/pretty_text/shims.js | 51 + .../assets/javascripts/details_dialect.js | 28 - .../initializers/apply-details.js.es6 | 3 +- .../lib/discourse-markdown/details.js.es6 | 25 + plugins/discourse-details/plugin.rb | 2 - .../lib/discourse-markdown/poll.js.es6 | 344 +++ .../poll/assets/javascripts/lib/md5.js.es6 | 1 + .../poll/assets/javascripts/poll_dialect.js | 175 -- plugins/poll/plugin.rb | 2 - spec/components/cooked_post_processor_spec.rb | 3 +- .../discourse_plugin_registry_spec.rb | 16 - spec/components/plugin/instance_spec.rb | 5 +- spec/components/pretty_text_spec.rb | 17 +- spec/models/post_spec.rb | 1 + .../acceptance/category-hashtag-test.js.es6 | 3 - .../helpers/create-pretender.js.es6 | 4 + test/javascripts/helpers/create-store.js.es6 | 2 +- test/javascripts/helpers/qunit-helpers.js.es6 | 6 - test/javascripts/helpers/site-settings.js | 2 +- test/javascripts/lib/bbcode-test.js.es6 | 156 -- test/javascripts/lib/emoji-test.js.es6 | 20 +- test/javascripts/lib/onebox-test.js.es6 | 23 - ...wn-test.js.es6 => pretty-text-test.js.es6} | 291 ++- test/javascripts/lib/sanitizer-test.js.es6 | 62 + test/javascripts/lib/utilities-test.js.es6 | 69 +- .../{mdtest.js.erb => mdtest.js.es6.erb} | 51 +- test/javascripts/models/topic-test.js.es6 | 3 +- test/javascripts/test_helper.js | 10 +- vendor/assets/javascripts/better_markdown.js | 1 + vendor/assets/javascripts/ember-qunit.js | 2 +- vendor/assets/javascripts/md5.js | 180 -- 146 files changed, 3259 insertions(+), 5675 deletions(-) delete mode 100644 app/assets/javascripts/defer/html-sanitizer-bundle.js delete mode 100644 app/assets/javascripts/discourse/components/poster-name.js.es6 delete mode 100644 app/assets/javascripts/discourse/dialects/autolink_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/bbcode_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/bold_italics_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/category_hashtag_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/censored_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/code_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/html_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/mention_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/nested_link_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/newline_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/onebox_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/quote_dialect.js delete mode 100644 app/assets/javascripts/discourse/dialects/table_dialect.js delete mode 100644 app/assets/javascripts/discourse/lib/censored-words.js delete mode 100644 app/assets/javascripts/discourse/lib/emoji/emoji.js.erb rename app/assets/javascripts/discourse/lib/emoji/{emoji-groups.js.es6 => groups.js.es6} (99%) rename app/assets/javascripts/discourse/lib/emoji/{emoji-toolbar.js.es6 => toolbar.js.es6} (92%) delete mode 100644 app/assets/javascripts/discourse/lib/markdown.js delete mode 100644 app/assets/javascripts/discourse/lib/onebox.js create mode 100644 app/assets/javascripts/discourse/lib/text.js.es6 delete mode 100644 app/assets/javascripts/discourse/lib/utilities.js create mode 100644 app/assets/javascripts/discourse/lib/utilities.js.es6 create mode 100644 app/assets/javascripts/pretty-text-bundle.js create mode 100644 app/assets/javascripts/pretty-text/censored-words.js.es6 create mode 100644 app/assets/javascripts/pretty-text/emoji.js.es6 create mode 100644 app/assets/javascripts/pretty-text/emoji/data.js.es6.erb rename app/assets/javascripts/{discourse/dialects/dialect.js => pretty-text/engines/discourse-markdown.js.es6} (50%) create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/auto-link.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/bold-italics.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/category-hashtag.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/code.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/mentions.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/newline.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/onebox.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/quote.js.es6 create mode 100644 app/assets/javascripts/pretty-text/engines/discourse-markdown/table.js.es6 create mode 100644 app/assets/javascripts/pretty-text/guid.js.es6 create mode 100644 app/assets/javascripts/pretty-text/oneboxer.js.es6 create mode 100644 app/assets/javascripts/pretty-text/pretty-text.js.es6 create mode 100644 app/assets/javascripts/pretty-text/sanitizer.js.es6 create mode 100644 app/assets/javascripts/pretty-text/white-lister.js.es6 create mode 100644 app/assets/javascripts/pretty-text/xss.js.es6 delete mode 100644 app/helpers/deferred_scripts_helper.rb create mode 100644 lib/pretty_text/helpers.rb create mode 100644 lib/pretty_text/shims.js delete mode 100644 plugins/discourse-details/assets/javascripts/details_dialect.js create mode 100644 plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 create mode 100644 plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 create mode 100644 plugins/poll/assets/javascripts/lib/md5.js.es6 delete mode 100644 plugins/poll/assets/javascripts/poll_dialect.js delete mode 100644 test/javascripts/lib/bbcode-test.js.es6 delete mode 100644 test/javascripts/lib/onebox-test.js.es6 rename test/javascripts/lib/{markdown-test.js.es6 => pretty-text-test.js.es6} (67%) create mode 100644 test/javascripts/lib/sanitizer-test.js.es6 rename test/javascripts/mdtest/{mdtest.js.erb => mdtest.js.es6.erb} (55%) delete mode 100644 vendor/assets/javascripts/md5.js diff --git a/.eslintignore b/.eslintignore index a520c62c91..7e1a7cd5a7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,7 +5,6 @@ app/assets/javascripts/preload_store.js app/assets/javascripts/pagedown_custom.js app/assets/javascripts/vendor.js app/assets/javascripts/locales/i18n.js -app/assets/javascripts/defer/html-sanitizer-bundle.js app/assets/javascripts/ember-addons/ app/assets/javascripts/discourse/lib/autosize.js.es6 lib/javascripts/locale/ diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 index 755dc574b2..7e592e9d80 100644 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ b/app/assets/javascripts/admin/components/ace-editor.js.es6 @@ -1,5 +1,6 @@ /* global ace:true */ import loadScript from 'discourse/lib/load-script'; +import escapeExpression from 'discourse/lib/utilities'; export default Ember.Component.extend({ mode: 'css', @@ -16,7 +17,7 @@ export default Ember.Component.extend({ render(buffer) { buffer.push("
"); if (this.get('content')) { - buffer.push(Discourse.Utilities.escapeExpression(this.get('content'))); + buffer.push(escapeExpression(this.get('content'))); } buffer.push("
"); }, diff --git a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 index d26585f348..8f93884c60 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-badge-preview.js.es6 @@ -1,3 +1,5 @@ +import { escapeExpression } from 'discourse/lib/utilities'; + export default Ember.Controller.extend({ needs: ['modal'], @@ -22,7 +24,7 @@ export default Ember.Controller.extend({ returned = "
";
 
     _.each(raw, function(linehash) {
-      returned += Discourse.Utilities.escapeExpression(linehash["QUERY PLAN"]);
+      returned += escapeExpression(linehash["QUERY PLAN"]);
       returned += "
"; }); @@ -32,7 +34,7 @@ export default Ember.Controller.extend({ processed_sample: Ember.computed.map('model.sample', function(grant) { var i18nKey = 'admin.badges.preview.grant.with', - i18nParams = { username: Discourse.Utilities.escapeExpression(grant.username) }; + i18nParams = { username: escapeExpression(grant.username) }; if (grant.post_id) { i18nKey += "_post"; @@ -41,7 +43,7 @@ export default Ember.Controller.extend({ if (grant.granted_at) { i18nKey += "_time"; - i18nParams.time = Discourse.Utilities.escapeExpression(moment(grant.granted_at).format(I18n.t('dates.long_with_year'))); + i18nParams.time = escapeExpression(moment(grant.granted_at).format(I18n.t('dates.long_with_year'))); } return I18n.t(i18nKey, i18nParams); diff --git a/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 b/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 index 5f70a2acc8..73bac43379 100644 --- a/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 +++ b/app/assets/javascripts/admin/helpers/preserve-newlines.js.es6 @@ -1,3 +1,4 @@ import { htmlHelper } from 'discourse/lib/helpers'; +import { escapeExpression } from 'discourse/lib/utilities'; -export default htmlHelper(str => Discourse.Utilities.escapeExpression(str).replace(/\n/g, "
")); +export default htmlHelper(str => escapeExpression(str).replace(/\n/g, "
")); diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js.es6 index 4f9e5cc9ae..b6e764ce33 100644 --- a/app/assets/javascripts/admin/models/staff-action-log.js.es6 +++ b/app/assets/javascripts/admin/models/staff-action-log.js.es6 @@ -1,4 +1,5 @@ import AdminUser from 'admin/models/admin-user'; +import { escapeExpression } from 'discourse/lib/utilities'; const StaffActionLog = Discourse.Model.extend({ showFullDetails: false, @@ -19,14 +20,14 @@ const StaffActionLog = Discourse.Model.extend({ formatted += this.format('admin.logs.staff_actions.previous_value', 'previous_value'); } if (!this.get('useModalForDetails')) { - if (this.get('details')) formatted += Discourse.Utilities.escapeExpression(this.get('details')) + '
'; + if (this.get('details')) formatted += escapeExpression(this.get('details')) + '
'; } return formatted; }.property('ip_address', 'email', 'topic_id', 'post_id', 'category_id'), format: function(label, propertyName) { if (this.get(propertyName)) { - return ('' + I18n.t(label) + ': ' + Discourse.Utilities.escapeExpression(this.get(propertyName)) + '
'); + return ('' + I18n.t(label) + ': ' + escapeExpression(this.get(propertyName)) + '
'); } else { return ''; } diff --git a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 index cb838a48fc..1929abf5a2 100644 --- a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 @@ -1,5 +1,6 @@ import debounce from 'discourse/lib/debounce'; import { renderSpinner } from 'discourse/helpers/loading-spinner'; +import { escapeExpression } from 'discourse/lib/utilities'; export default Ember.View.extend({ classNames: ["admin-backups-logs"], @@ -19,7 +20,7 @@ export default Ember.View.extend({ let formattedLogs = this.get("formattedLogs"); for (let i = this.get("index"), length = logs.length; i < length; i++) { const date = logs[i].get("timestamp"), - message = Discourse.Utilities.escapeExpression(logs[i].get("message")); + message = escapeExpression(logs[i].get("message")); formattedLogs += "[" + date + "] " + message + "\n"; } // update the formatted logs & cache index diff --git a/app/assets/javascripts/defer/html-sanitizer-bundle.js b/app/assets/javascripts/defer/html-sanitizer-bundle.js deleted file mode 100644 index 7529153a85..0000000000 --- a/app/assets/javascripts/defer/html-sanitizer-bundle.js +++ /dev/null @@ -1,2233 +0,0 @@ -// Copyright (C) 2010 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview - * Implements RFC 3986 for parsing/formatting URIs. - * - * @author mikesamuel@gmail.com - * \@provides URI - * \@overrides window - */ - -var URI = (function () { - -/** - * creates a uri from the string form. The parser is relaxed, so special - * characters that aren't escaped but don't cause ambiguities will not cause - * parse failures. - * - * @return {URI|null} - */ -function parse(uriStr) { - var m = ('' + uriStr).match(URI_RE_); - if (!m) { return null; } - return new URI( - nullIfAbsent(m[1]), - nullIfAbsent(m[2]), - nullIfAbsent(m[3]), - nullIfAbsent(m[4]), - nullIfAbsent(m[5]), - nullIfAbsent(m[6]), - nullIfAbsent(m[7])); -} - - -/** - * creates a uri from the given parts. - * - * @param scheme {string} an unencoded scheme such as "http" or null - * @param credentials {string} unencoded user credentials or null - * @param domain {string} an unencoded domain name or null - * @param port {number} a port number in [1, 32768]. - * -1 indicates no port, as does null. - * @param path {string} an unencoded path - * @param query {Array.|string|null} a list of unencoded cgi - * parameters where even values are keys and odds the corresponding values - * or an unencoded query. - * @param fragment {string} an unencoded fragment without the "#" or null. - * @return {URI} - */ -function create(scheme, credentials, domain, port, path, query, fragment) { - var uri = new URI( - encodeIfExists2(scheme, URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_), - encodeIfExists2( - credentials, URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_), - encodeIfExists(domain), - port > 0 ? port.toString() : null, - encodeIfExists2(path, URI_DISALLOWED_IN_PATH_), - null, - encodeIfExists(fragment)); - if (query) { - if ('string' === typeof query) { - uri.setRawQuery(query.replace(/[^?&=0-9A-Za-z_\-~.%]/g, encodeOne)); - } else { - uri.setAllParameters(query); - } - } - return uri; -} -function encodeIfExists(unescapedPart) { - if ('string' == typeof unescapedPart) { - return encodeURIComponent(unescapedPart); - } - return null; -}; -/** - * if unescapedPart is non null, then escapes any characters in it that aren't - * valid characters in a url and also escapes any special characters that - * appear in extra. - * - * @param unescapedPart {string} - * @param extra {RegExp} a character set of characters in [\01-\177]. - * @return {string|null} null iff unescapedPart == null. - */ -function encodeIfExists2(unescapedPart, extra) { - if ('string' == typeof unescapedPart) { - return encodeURI(unescapedPart).replace(extra, encodeOne); - } - return null; -}; -/** converts a character in [\01-\177] to its url encoded equivalent. */ -function encodeOne(ch) { - var n = ch.charCodeAt(0); - return '%' + '0123456789ABCDEF'.charAt((n >> 4) & 0xf) + - '0123456789ABCDEF'.charAt(n & 0xf); -} - -/** - * {@updoc - * $ normPath('foo/./bar') - * # 'foo/bar' - * $ normPath('./foo') - * # 'foo' - * $ normPath('foo/.') - * # 'foo' - * $ normPath('foo//bar') - * # 'foo/bar' - * } - */ -function normPath(path) { - return path.replace(/(^|\/)\.(?:\/|$)/g, '$1').replace(/\/{2,}/g, '/'); -} - -var PARENT_DIRECTORY_HANDLER = new RegExp( - '' - // A path break - + '(/|^)' - // followed by a non .. path element - // (cannot be . because normPath is used prior to this RegExp) - + '(?:[^./][^/]*|\\.{2,}(?:[^./][^/]*)|\\.{3,}[^/]*)' - // followed by .. followed by a path break. - + '/\\.\\.(?:/|$)'); - -var PARENT_DIRECTORY_HANDLER_RE = new RegExp(PARENT_DIRECTORY_HANDLER); - -var EXTRA_PARENT_PATHS_RE = /^(?:\.\.\/)*(?:\.\.$)?/; - -/** - * Normalizes its input path and collapses all . and .. sequences except for - * .. sequences that would take it above the root of the current parent - * directory. - * {@updoc - * $ collapse_dots('foo/../bar') - * # 'bar' - * $ collapse_dots('foo/./bar') - * # 'foo/bar' - * $ collapse_dots('foo/../bar/./../../baz') - * # 'baz' - * $ collapse_dots('../foo') - * # '../foo' - * $ collapse_dots('../foo').replace(EXTRA_PARENT_PATHS_RE, '') - * # 'foo' - * } - */ -function collapse_dots(path) { - if (path === null) { return null; } - var p = normPath(path); - // Only /../ left to flatten - var r = PARENT_DIRECTORY_HANDLER_RE; - // We replace with $1 which matches a / before the .. because this - // guarantees that: - // (1) we have at most 1 / between the adjacent place, - // (2) always have a slash if there is a preceding path section, and - // (3) we never turn a relative path into an absolute path. - for (var q; (q = p.replace(r, '$1')) != p; p = q) {}; - return p; -} - -/** - * resolves a relative url string to a base uri. - * @return {URI} - */ -function resolve(baseUri, relativeUri) { - // there are several kinds of relative urls: - // 1. //foo - replaces everything from the domain on. foo is a domain name - // 2. foo - replaces the last part of the path, the whole query and fragment - // 3. /foo - replaces the the path, the query and fragment - // 4. ?foo - replace the query and fragment - // 5. #foo - replace the fragment only - - var absoluteUri = baseUri.clone(); - // we satisfy these conditions by looking for the first part of relativeUri - // that is not blank and applying defaults to the rest - - var overridden = relativeUri.hasScheme(); - - if (overridden) { - absoluteUri.setRawScheme(relativeUri.getRawScheme()); - } else { - overridden = relativeUri.hasCredentials(); - } - - if (overridden) { - absoluteUri.setRawCredentials(relativeUri.getRawCredentials()); - } else { - overridden = relativeUri.hasDomain(); - } - - if (overridden) { - absoluteUri.setRawDomain(relativeUri.getRawDomain()); - } else { - overridden = relativeUri.hasPort(); - } - - var rawPath = relativeUri.getRawPath(); - var simplifiedPath = collapse_dots(rawPath); - if (overridden) { - absoluteUri.setPort(relativeUri.getPort()); - simplifiedPath = simplifiedPath - && simplifiedPath.replace(EXTRA_PARENT_PATHS_RE, ''); - } else { - overridden = !!rawPath; - if (overridden) { - // resolve path properly - if (simplifiedPath.charCodeAt(0) !== 0x2f /* / */) { // path is relative - var absRawPath = collapse_dots(absoluteUri.getRawPath() || '') - .replace(EXTRA_PARENT_PATHS_RE, ''); - var slash = absRawPath.lastIndexOf('/') + 1; - simplifiedPath = collapse_dots( - (slash ? absRawPath.substring(0, slash) : '') - + collapse_dots(rawPath)) - .replace(EXTRA_PARENT_PATHS_RE, ''); - } - } else { - simplifiedPath = simplifiedPath - && simplifiedPath.replace(EXTRA_PARENT_PATHS_RE, ''); - if (simplifiedPath !== rawPath) { - absoluteUri.setRawPath(simplifiedPath); - } - } - } - - if (overridden) { - absoluteUri.setRawPath(simplifiedPath); - } else { - overridden = relativeUri.hasQuery(); - } - - if (overridden) { - absoluteUri.setRawQuery(relativeUri.getRawQuery()); - } else { - overridden = relativeUri.hasFragment(); - } - - if (overridden) { - absoluteUri.setRawFragment(relativeUri.getRawFragment()); - } - - return absoluteUri; -} - -/** - * a mutable URI. - * - * This class contains setters and getters for the parts of the URI. - * The getXYZ/setXYZ methods return the decoded part -- so - * uri.parse('/foo%20bar').getPath() will return the decoded path, - * /foo bar. - * - *

The raw versions of fields are available too. - * uri.parse('/foo%20bar').getRawPath() will return the raw path, - * /foo%20bar. Use the raw setters with care, since - * URI::toString is not guaranteed to return a valid url if a - * raw setter was used. - * - *

All setters return this and so may be chained, a la - * uri.parse('/foo').setFragment('part').toString(). - * - *

You should not use this constructor directly -- please prefer the factory - * functions {@link uri.parse}, {@link uri.create}, {@link uri.resolve} - * instead.

- * - *

The parameters are all raw (assumed to be properly escaped) parts, and - * any (but not all) may be null. Undefined is not allowed.

- * - * @constructor - */ -function URI( - rawScheme, - rawCredentials, rawDomain, port, - rawPath, rawQuery, rawFragment) { - this.scheme_ = rawScheme; - this.credentials_ = rawCredentials; - this.domain_ = rawDomain; - this.port_ = port; - this.path_ = rawPath; - this.query_ = rawQuery; - this.fragment_ = rawFragment; - /** - * @type {Array|null} - */ - this.paramCache_ = null; -} - -/** returns the string form of the url. */ -URI.prototype.toString = function () { - var out = []; - if (null !== this.scheme_) { out.push(this.scheme_, ':'); } - if (null !== this.domain_) { - out.push('//'); - if (null !== this.credentials_) { out.push(this.credentials_, '@'); } - out.push(this.domain_); - if (null !== this.port_) { out.push(':', this.port_.toString()); } - } - if (null !== this.path_) { out.push(this.path_); } - if (null !== this.query_) { out.push('?', this.query_); } - if (null !== this.fragment_) { out.push('#', this.fragment_); } - return out.join(''); -}; - -URI.prototype.clone = function () { - return new URI(this.scheme_, this.credentials_, this.domain_, this.port_, - this.path_, this.query_, this.fragment_); -}; - -URI.prototype.getScheme = function () { - // HTML5 spec does not require the scheme to be lowercased but - // all common browsers except Safari lowercase the scheme. - return this.scheme_ && decodeURIComponent(this.scheme_).toLowerCase(); -}; -URI.prototype.getRawScheme = function () { - return this.scheme_; -}; -URI.prototype.setScheme = function (newScheme) { - this.scheme_ = encodeIfExists2( - newScheme, URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_); - return this; -}; -URI.prototype.setRawScheme = function (newScheme) { - this.scheme_ = newScheme ? newScheme : null; - return this; -}; -URI.prototype.hasScheme = function () { - return null !== this.scheme_; -}; - - -URI.prototype.getCredentials = function () { - return this.credentials_ && decodeURIComponent(this.credentials_); -}; -URI.prototype.getRawCredentials = function () { - return this.credentials_; -}; -URI.prototype.setCredentials = function (newCredentials) { - this.credentials_ = encodeIfExists2( - newCredentials, URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_); - - return this; -}; -URI.prototype.setRawCredentials = function (newCredentials) { - this.credentials_ = newCredentials ? newCredentials : null; - return this; -}; -URI.prototype.hasCredentials = function () { - return null !== this.credentials_; -}; - - -URI.prototype.getDomain = function () { - return this.domain_ && decodeURIComponent(this.domain_); -}; -URI.prototype.getRawDomain = function () { - return this.domain_; -}; -URI.prototype.setDomain = function (newDomain) { - return this.setRawDomain(newDomain && encodeURIComponent(newDomain)); -}; -URI.prototype.setRawDomain = function (newDomain) { - this.domain_ = newDomain ? newDomain : null; - // Maintain the invariant that paths must start with a slash when the URI - // is not path-relative. - return this.setRawPath(this.path_); -}; -URI.prototype.hasDomain = function () { - return null !== this.domain_; -}; - - -URI.prototype.getPort = function () { - return this.port_ && decodeURIComponent(this.port_); -}; -URI.prototype.setPort = function (newPort) { - if (newPort) { - newPort = Number(newPort); - if (newPort !== (newPort & 0xffff)) { - throw new Error('Bad port number ' + newPort); - } - this.port_ = '' + newPort; - } else { - this.port_ = null; - } - return this; -}; -URI.prototype.hasPort = function () { - return null !== this.port_; -}; - - -URI.prototype.getPath = function () { - return this.path_ && decodeURIComponent(this.path_); -}; -URI.prototype.getRawPath = function () { - return this.path_; -}; -URI.prototype.setPath = function (newPath) { - return this.setRawPath(encodeIfExists2(newPath, URI_DISALLOWED_IN_PATH_)); -}; -URI.prototype.setRawPath = function (newPath) { - if (newPath) { - newPath = String(newPath); - this.path_ = - // Paths must start with '/' unless this is a path-relative URL. - (!this.domain_ || /^\//.test(newPath)) ? newPath : '/' + newPath; - } else { - this.path_ = null; - } - return this; -}; -URI.prototype.hasPath = function () { - return null !== this.path_; -}; - - -URI.prototype.getQuery = function () { - // From http://www.w3.org/Addressing/URL/4_URI_Recommentations.html - // Within the query string, the plus sign is reserved as shorthand notation - // for a space. - return this.query_ && decodeURIComponent(this.query_).replace(/\+/g, ' '); -}; -URI.prototype.getRawQuery = function () { - return this.query_; -}; -URI.prototype.setQuery = function (newQuery) { - this.paramCache_ = null; - this.query_ = encodeIfExists(newQuery); - return this; -}; -URI.prototype.setRawQuery = function (newQuery) { - this.paramCache_ = null; - this.query_ = newQuery ? newQuery : null; - return this; -}; -URI.prototype.hasQuery = function () { - return null !== this.query_; -}; - -/** - * sets the query given a list of strings of the form - * [ key0, value0, key1, value1, ... ]. - * - *

uri.setAllParameters(['a', 'b', 'c', 'd']).getQuery() - * will yield 'a=b&c=d'. - */ -URI.prototype.setAllParameters = function (params) { - if (typeof params === 'object') { - if (!(params instanceof Array) - && (params instanceof Object - || Object.prototype.toString.call(params) !== '[object Array]')) { - var newParams = []; - var i = -1; - for (var k in params) { - var v = params[k]; - if ('string' === typeof v) { - newParams[++i] = k; - newParams[++i] = v; - } - } - params = newParams; - } - } - this.paramCache_ = null; - var queryBuf = []; - var separator = ''; - for (var j = 0; j < params.length;) { - var k = params[j++]; - var v = params[j++]; - queryBuf.push(separator, encodeURIComponent(k.toString())); - separator = '&'; - if (v) { - queryBuf.push('=', encodeURIComponent(v.toString())); - } - } - this.query_ = queryBuf.join(''); - return this; -}; -URI.prototype.checkParameterCache_ = function () { - if (!this.paramCache_) { - var q = this.query_; - if (!q) { - this.paramCache_ = []; - } else { - var cgiParams = q.split(/[&\?]/); - var out = []; - var k = -1; - for (var i = 0; i < cgiParams.length; ++i) { - var m = cgiParams[i].match(/^([^=]*)(?:=(.*))?$/); - // From http://www.w3.org/Addressing/URL/4_URI_Recommentations.html - // Within the query string, the plus sign is reserved as shorthand - // notation for a space. - out[++k] = decodeURIComponent(m[1]).replace(/\+/g, ' '); - out[++k] = decodeURIComponent(m[2] || '').replace(/\+/g, ' '); - } - this.paramCache_ = out; - } - } -}; -/** - * sets the values of the named cgi parameters. - * - *

So, uri.parse('foo?a=b&c=d&e=f').setParameterValues('c', ['new']) - * yields foo?a=b&c=new&e=f.

- * - * @param key {string} - * @param values {Array.} the new values. If values is a single string - * then it will be treated as the sole value. - */ -URI.prototype.setParameterValues = function (key, values) { - // be nice and avoid subtle bugs where [] operator on string performs charAt - // on some browsers and crashes on IE - if (typeof values === 'string') { - values = [ values ]; - } - - this.checkParameterCache_(); - var newValueIndex = 0; - var pc = this.paramCache_; - var params = []; - for (var i = 0, k = 0; i < pc.length; i += 2) { - if (key === pc[i]) { - if (newValueIndex < values.length) { - params.push(key, values[newValueIndex++]); - } - } else { - params.push(pc[i], pc[i + 1]); - } - } - while (newValueIndex < values.length) { - params.push(key, values[newValueIndex++]); - } - this.setAllParameters(params); - return this; -}; -URI.prototype.removeParameter = function (key) { - return this.setParameterValues(key, []); -}; -/** - * returns the parameters specified in the query part of the uri as a list of - * keys and values like [ key0, value0, key1, value1, ... ]. - * - * @return {Array.} - */ -URI.prototype.getAllParameters = function () { - this.checkParameterCache_(); - return this.paramCache_.slice(0, this.paramCache_.length); -}; -/** - * returns the values for a given cgi parameter as a list of decoded - * query parameter values. - * @return {Array.} - */ -URI.prototype.getParameterValues = function (paramNameUnescaped) { - this.checkParameterCache_(); - var values = []; - for (var i = 0; i < this.paramCache_.length; i += 2) { - if (paramNameUnescaped === this.paramCache_[i]) { - values.push(this.paramCache_[i + 1]); - } - } - return values; -}; -/** - * returns a map of cgi parameter names to (non-empty) lists of values. - * @return {Object.>} - */ -URI.prototype.getParameterMap = function (paramNameUnescaped) { - this.checkParameterCache_(); - var paramMap = {}; - for (var i = 0; i < this.paramCache_.length; i += 2) { - var key = this.paramCache_[i++], - value = this.paramCache_[i++]; - if (!(key in paramMap)) { - paramMap[key] = [value]; - } else { - paramMap[key].push(value); - } - } - return paramMap; -}; -/** - * returns the first value for a given cgi parameter or null if the given - * parameter name does not appear in the query string. - * If the given parameter name does appear, but has no '=' following - * it, then the empty string will be returned. - * @return {string|null} - */ -URI.prototype.getParameterValue = function (paramNameUnescaped) { - this.checkParameterCache_(); - for (var i = 0; i < this.paramCache_.length; i += 2) { - if (paramNameUnescaped === this.paramCache_[i]) { - return this.paramCache_[i + 1]; - } - } - return null; -}; - -URI.prototype.getFragment = function () { - return this.fragment_ && decodeURIComponent(this.fragment_); -}; -URI.prototype.getRawFragment = function () { - return this.fragment_; -}; -URI.prototype.setFragment = function (newFragment) { - this.fragment_ = newFragment ? encodeURIComponent(newFragment) : null; - return this; -}; -URI.prototype.setRawFragment = function (newFragment) { - this.fragment_ = newFragment ? newFragment : null; - return this; -}; -URI.prototype.hasFragment = function () { - return null !== this.fragment_; -}; - -function nullIfAbsent(matchPart) { - return ('string' == typeof matchPart) && (matchPart.length > 0) - ? matchPart - : null; -} - - - - -/** - * a regular expression for breaking a URI into its component parts. - * - *

http://www.gbiv.com/protocols/uri/rfc/rfc3986.html#RFC2234 says - * As the "first-match-wins" algorithm is identical to the "greedy" - * disambiguation method used by POSIX regular expressions, it is natural and - * commonplace to use a regular expression for parsing the potential five - * components of a URI reference. - * - *

The following line is the regular expression for breaking-down a - * well-formed URI reference into its components. - * - *

- * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
- *  12            3  4          5       6  7        8 9
- * 
- * - *

The numbers in the second line above are only to assist readability; they - * indicate the reference points for each subexpression (i.e., each paired - * parenthesis). We refer to the value matched for subexpression as $. - * For example, matching the above expression to - *

- *     http://www.ics.uci.edu/pub/ietf/uri/#Related
- * 
- * results in the following subexpression matches: - *
- *    $1 = http:
- *    $2 = http
- *    $3 = //www.ics.uci.edu
- *    $4 = www.ics.uci.edu
- *    $5 = /pub/ietf/uri/
- *    $6 = 
- *    $7 = 
- *    $8 = #Related
- *    $9 = Related
- * 
- * where indicates that the component is not present, as is the - * case for the query component in the above example. Therefore, we can - * determine the value of the five components as - *
- *    scheme    = $2
- *    authority = $4
- *    path      = $5
- *    query     = $7
- *    fragment  = $9
- * 
- * - *

msamuel: I have modified the regular expression slightly to expose the - * credentials, domain, and port separately from the authority. - * The modified version yields - *

- *    $1 = http              scheme
- *    $2 =        credentials -\
- *    $3 = www.ics.uci.edu   domain       | authority
- *    $4 =        port        -/
- *    $5 = /pub/ietf/uri/    path
- *    $6 =        query without ?
- *    $7 = Related           fragment without #
- * 
- */ -var URI_RE_ = new RegExp( - "^" + - "(?:" + - "([^:/?#]+)" + // scheme - ":)?" + - "(?://" + - "(?:([^/?#]*)@)?" + // credentials - "([^/?#:@]*)" + // domain - "(?::([0-9]+))?" + // port - ")?" + - "([^?#]+)?" + // path - "(?:\\?([^#]*))?" + // query - "(?:#(.*))?" + // fragment - "$" - ); - -var URI_DISALLOWED_IN_SCHEME_OR_CREDENTIALS_ = /[#\/\?@]/g; -var URI_DISALLOWED_IN_PATH_ = /[\#\?]/g; - -URI.parse = parse; -URI.create = create; -URI.resolve = resolve; -URI.collapse_dots = collapse_dots; // Visible for testing. - -// lightweight string-based api for loadModuleMaker -URI.utils = { - mimeTypeOf: function (uri) { - var uriObj = parse(uri); - if (/\.html$/.test(uriObj.getPath())) { - return 'text/html'; - } else { - return 'application/javascript'; - } - }, - resolve: function (base, uri) { - if (base) { - return resolve(parse(base), parse(uri)).toString(); - } else { - return '' + uri; - } - } -}; - - -return URI; -})(); - -// Exports for closure compiler. -if (typeof window !== 'undefined') { - window['URI'] = URI; -} -; -// Copyright Google Inc. -// Licensed under the Apache Licence Version 2.0 -// Autogenerated at Mon Oct 21 13:30:08 EDT 2013 -// @overrides window -// @provides html4 -var html4 = {}; -html4.atype = { - 'NONE': 0, - 'URI': 1, - 'URI_FRAGMENT': 11, - 'SCRIPT': 2, - 'STYLE': 3, - 'HTML': 12, - 'ID': 4, - 'IDREF': 5, - 'IDREFS': 6, - 'GLOBAL_NAME': 7, - 'LOCAL_NAME': 8, - 'CLASSES': 9, - 'FRAME_TARGET': 10, - 'MEDIA_QUERY': 13 -}; -html4[ 'atype' ] = html4.atype; -html4.ATTRIBS = { - '*::class': 9, - '*::dir': 0, - '*::draggable': 0, - '*::hidden': 0, - '*::id': 4, - '*::inert': 0, - '*::itemprop': 0, - '*::itemref': 6, - '*::itemscope': 0, - '*::lang': 0, - '*::onblur': 2, - '*::onchange': 2, - '*::onclick': 2, - '*::ondblclick': 2, - '*::onerror': 2, - '*::onfocus': 2, - '*::onkeydown': 2, - '*::onkeypress': 2, - '*::onkeyup': 2, - '*::onload': 2, - '*::onmousedown': 2, - '*::onmousemove': 2, - '*::onmouseout': 2, - '*::onmouseover': 2, - '*::onmouseup': 2, - '*::onreset': 2, - '*::onscroll': 2, - '*::onselect': 2, - '*::onsubmit': 2, - '*::onunload': 2, - '*::spellcheck': 0, - '*::style': 3, - '*::title': 0, - '*::translate': 0, - 'a::accesskey': 0, - 'a::coords': 0, - 'a::href': 1, - 'a::hreflang': 0, - 'a::name': 7, - 'a::onblur': 2, - 'a::onfocus': 2, - 'a::shape': 0, - 'a::tabindex': 0, - 'a::target': 10, - 'a::type': 0, - 'bdo::dir': 0, - 'blockquote::cite': 1, - 'br::clear': 0, - 'caption::align': 0, - 'col::align': 0, - 'col::char': 0, - 'col::charoff': 0, - 'col::span': 0, - 'col::valign': 0, - 'col::width': 0, - 'colgroup::align': 0, - 'colgroup::char': 0, - 'colgroup::charoff': 0, - 'colgroup::span': 0, - 'colgroup::valign': 0, - 'colgroup::width': 0, - 'data::value': 0, - 'del::cite': 1, - 'del::datetime': 0, - 'details::open': 0, - 'dir::compact': 0, - 'div::align': 0, - 'dl::compact': 0, - 'h1::align': 0, - 'h2::align': 0, - 'h3::align': 0, - 'h4::align': 0, - 'h5::align': 0, - 'h6::align': 0, - 'hr::align': 0, - 'hr::noshade': 0, - 'hr::size': 0, - 'hr::width': 0, - 'iframe::align': 0, - 'iframe::frameborder': 0, - 'iframe::height': 0, - 'iframe::marginheight': 0, - 'iframe::marginwidth': 0, - 'iframe::width': 0, - 'iframe::src': 1, - 'img::alt': 0, - 'img::height': 0, - 'img::name': 7, - 'img::src': 1, - 'img::width': 0, - 'ins::cite': 1, - 'ins::datetime': 0, - 'label::accesskey': 0, - 'label::for': 5, - 'label::onblur': 2, - 'label::onfocus': 2, - 'legend::accesskey': 0, - 'legend::align': 0, - 'li::type': 0, - 'li::value': 0, - 'ol::compact': 0, - 'ol::reversed': 0, - 'ol::start': 0, - 'ol::type': 0, - 'p::align': 0, - 'pre::width': 0, - 'q::cite': 1, - 'source::type': 0, - 'ul::compact': 0, - 'ul::type': 0, -}; -html4[ 'ATTRIBS' ] = html4.ATTRIBS; -html4.eflags = { - 'OPTIONAL_ENDTAG': 1, - 'EMPTY': 2, - 'CDATA': 4, - 'RCDATA': 8, - 'UNSAFE': 16, - 'FOLDABLE': 32, - 'SCRIPT': 64, - 'STYLE': 128, - 'VIRTUALIZED': 256 -}; -html4[ 'eflags' ] = html4.eflags; -html4.ELEMENTS = { - 'a': 0, - 'abbr': 0, - 'acronym': 0, - 'address': 0, - 'article': 0, - 'aside': 0, - 'b': 0, - 'base': 274, - 'bdi': 0, - 'bdo': 0, - 'big': 0, - 'blockquote': 0, - 'body': 305, - 'br': 2, - 'caption': 0, - 'cite': 0, - 'code': 0, - 'col': 2, - 'colgroup': 1, - 'data': 0, - 'dd': 1, - 'del': 0, - 'details': 0, - 'dfn': 0, - 'dialog': 272, - 'dir': 0, - 'div': 0, - 'dl': 0, - 'dt': 1, - 'em': 0, - 'figcaption': 0, - 'figure': 0, - 'frame': 274, - 'frameset': 272, - 'h1': 0, - 'h2': 0, - 'h3': 0, - 'h4': 0, - 'h5': 0, - 'h6': 0, - 'head': 305, - 'header': 0, - 'hgroup': 0, - 'hr': 2, - 'html': 305, - 'i': 0, - 'iframe': 4, - 'img': 2, - 'ins': 0, - 'isindex': 274, - 'kbd': 0, - 'keygen': 274, - 'label': 0, - 'legend': 0, - 'li': 1, - 'link': 274, - 'nav': 0, - 'nobr': 0, - 'noembed': 276, - 'noframes': 276, - 'noscript': 276, - 'object': 272, - 'ol': 0, - 'p': 1, - 'param': 274, - 'pre': 0, - 'q': 0, - 's': 0, - 'samp': 0, - 'script': 84, - 'section': 0, - 'small': 0, - 'span': 0, - 'strike': 0, - 'strong': 0, - 'style': 148, - 'sub': 0, - 'summary': 0, - 'sup': 0, - 'table': 272, - 'tbody': 273, - 'td': 273, - 'tfoot': 1, - 'th': 273, - 'thead': 273, - 'time': 0, - 'title': 280, - 'tr': 273, - 'tt': 0, - 'u': 0, - 'ul': 0, - 'var': 0, - 'wbr': 2 -}; -html4[ 'ELEMENTS' ] = html4.ELEMENTS; -html4.ELEMENT_DOM_INTERFACES = { - 'a': 'HTMLAnchorElement', - 'abbr': 'HTMLElement', - 'acronym': 'HTMLElement', - 'address': 'HTMLElement', - 'applet': 'HTMLAppletElement', - 'area': 'HTMLAreaElement', - 'article': 'HTMLElement', - 'aside': 'HTMLElement', - 'audio': 'HTMLAudioElement', - 'b': 'HTMLElement', - 'base': 'HTMLBaseElement', - 'basefont': 'HTMLBaseFontElement', - 'bdi': 'HTMLElement', - 'bdo': 'HTMLElement', - 'big': 'HTMLElement', - 'blockquote': 'HTMLQuoteElement', - 'body': 'HTMLBodyElement', - 'br': 'HTMLBRElement', - 'caption': 'HTMLTableCaptionElement', - 'cite': 'HTMLElement', - 'code': 'HTMLElement', - 'col': 'HTMLTableColElement', - 'colgroup': 'HTMLTableColElement', - 'command': 'HTMLCommandElement', - 'data': 'HTMLElement', - 'datalist': 'HTMLDataListElement', - 'dd': 'HTMLElement', - 'del': 'HTMLModElement', - 'details': 'HTMLDetailsElement', - 'dfn': 'HTMLElement', - 'dialog': 'HTMLDialogElement', - 'dir': 'HTMLDirectoryElement', - 'div': 'HTMLDivElement', - 'dl': 'HTMLDListElement', - 'dt': 'HTMLElement', - 'em': 'HTMLElement', - 'fieldset': 'HTMLFieldSetElement', - 'figcaption': 'HTMLElement', - 'figure': 'HTMLElement', - 'footer': 'HTMLElement', - 'form': 'HTMLFormElement', - 'frame': 'HTMLFrameElement', - 'frameset': 'HTMLFrameSetElement', - 'h1': 'HTMLHeadingElement', - 'h2': 'HTMLHeadingElement', - 'h3': 'HTMLHeadingElement', - 'h4': 'HTMLHeadingElement', - 'h5': 'HTMLHeadingElement', - 'h6': 'HTMLHeadingElement', - 'head': 'HTMLHeadElement', - 'header': 'HTMLElement', - 'hgroup': 'HTMLElement', - 'hr': 'HTMLHRElement', - 'html': 'HTMLHtmlElement', - 'i': 'HTMLElement', - 'iframe': 'HTMLIFrameElement', - 'img': 'HTMLImageElement', - 'input': 'HTMLInputElement', - 'ins': 'HTMLModElement', - 'isindex': 'HTMLUnknownElement', - 'kbd': 'HTMLElement', - 'keygen': 'HTMLKeygenElement', - 'label': 'HTMLLabelElement', - 'legend': 'HTMLLegendElement', - 'li': 'HTMLLIElement', - 'link': 'HTMLLinkElement', - 'map': 'HTMLMapElement', - 'menu': 'HTMLMenuElement', - 'meta': 'HTMLMetaElement', - 'nav': 'HTMLElement', - 'nobr': 'HTMLElement', - 'noembed': 'HTMLElement', - 'noframes': 'HTMLElement', - 'noscript': 'HTMLElement', - 'object': 'HTMLObjectElement', - 'ol': 'HTMLOListElement', - 'optgroup': 'HTMLOptGroupElement', - 'option': 'HTMLOptionElement', - 'output': 'HTMLOutputElement', - 'p': 'HTMLParagraphElement', - 'param': 'HTMLParamElement', - 'pre': 'HTMLPreElement', - 'q': 'HTMLQuoteElement', - 's': 'HTMLElement', - 'samp': 'HTMLElement', - 'script': 'HTMLScriptElement', - 'section': 'HTMLElement', - 'select': 'HTMLSelectElement', - 'small': 'HTMLElement', - 'source': 'HTMLSourceElement', - 'span': 'HTMLSpanElement', - 'strike': 'HTMLElement', - 'strong': 'HTMLElement', - 'style': 'HTMLStyleElement', - 'sub': 'HTMLElement', - 'summary': 'HTMLElement', - 'sup': 'HTMLElement', - 'table': 'HTMLTableElement', - 'tbody': 'HTMLTableSectionElement', - 'td': 'HTMLTableDataCellElement', - 'tfoot': 'HTMLTableSectionElement', - 'th': 'HTMLTableHeaderCellElement', - 'thead': 'HTMLTableSectionElement', - 'time': 'HTMLTimeElement', - 'title': 'HTMLTitleElement', - 'tr': 'HTMLTableRowElement', - 'tt': 'HTMLElement', - 'u': 'HTMLElement', - 'ul': 'HTMLUListElement', - 'var': 'HTMLElement', - 'video': 'HTMLVideoElement', - 'wbr': 'HTMLElement' -}; -html4[ 'ELEMENT_DOM_INTERFACES' ] = html4.ELEMENT_DOM_INTERFACES; -html4.ueffects = { - 'NOT_LOADED': 0, - 'SAME_DOCUMENT': 1, - 'NEW_DOCUMENT': 2 -}; -html4[ 'ueffects' ] = html4.ueffects; -html4.URIEFFECTS = { - 'a::href': 2, - 'area::href': 2, - 'audio::src': 1, - 'blockquote::cite': 0, - 'command::icon': 1, - 'del::cite': 0, - 'form::action': 2, - 'iframe::src': 1, - 'img::src': 1, - 'input::src': 1, - 'ins::cite': 0, - 'q::cite': 0, - 'video::poster': 1, - 'video::src': 1 -}; -html4[ 'URIEFFECTS' ] = html4.URIEFFECTS; -html4.ltypes = { - 'UNSANDBOXED': 2, - 'SANDBOXED': 1, - 'DATA': 0 -}; -html4[ 'ltypes' ] = html4.ltypes; -html4.LOADERTYPES = { - 'a::href': 2, - 'area::href': 2, - 'audio::src': 2, - 'blockquote::cite': 2, - 'command::icon': 1, - 'del::cite': 2, - 'form::action': 2, - 'iframe::src': 2, - 'img::src': 1, - 'input::src': 1, - 'ins::cite': 2, - 'q::cite': 2, - 'video::poster': 1, - 'video::src': 2 -}; -html4[ 'LOADERTYPES' ] = html4.LOADERTYPES; -// NOTE: currently focused only on URI-type attributes -html4.REQUIREDATTRIBUTES = { - "audio" : ["src"], - "form" : ["action"], - "iframe" : ["src"], - "image" : ["src"], - "video" : ["src"] -}; -html4[ 'REQUIREDATTRIBUTES' ] = html4.REQUIREDATTRIBUTES; -// export for Closure Compiler -if (typeof window !== 'undefined') { - window['html4'] = html4; -} -; -// Copyright (C) 2006 Google Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * @fileoverview - * An HTML sanitizer that can satisfy a variety of security policies. - * - *

- * The HTML sanitizer is built around a SAX parser and HTML element and - * attributes schemas. - * - * If the cssparser is loaded, inline styles are sanitized using the - * css property and value schemas. Else they are remove during - * sanitization. - * - * If it exists, uses parseCssDeclarations, sanitizeCssProperty, cssSchema - * - * @author mikesamuel@gmail.com - * @author jasvir@gmail.com - * \@requires html4, URI - * \@overrides window - * \@provides html, html_sanitize - */ - -// The Turkish i seems to be a non-issue, but abort in case it is. -if ('I'.toLowerCase() !== 'i') { throw 'I/i problem'; } - -/** - * \@namespace - */ -var html = (function(html4) { - - // For closure compiler - var parseCssDeclarations, sanitizeCssProperty, cssSchema; - if ('undefined' !== typeof window) { - parseCssDeclarations = window['parseCssDeclarations']; - sanitizeCssProperty = window['sanitizeCssProperty']; - cssSchema = window['cssSchema']; - } - - // The keys of this object must be 'quoted' or JSCompiler will mangle them! - // This is a partial list -- lookupEntity() uses the host browser's parser - // (when available) to implement full entity lookup. - // Note that entities are in general case-sensitive; the uppercase ones are - // explicitly defined by HTML5 (presumably as compatibility). - var ENTITIES = { - 'lt': '<', - 'LT': '<', - 'gt': '>', - 'GT': '>', - 'amp': '&', - 'AMP': '&', - 'quot': '"', - 'apos': '\'', - 'nbsp': '\240' - }; - - // Patterns for types of entity/character reference names. - var decimalEscapeRe = /^#(\d+)$/; - var hexEscapeRe = /^#x([0-9A-Fa-f]+)$/; - // contains every entity per http://www.w3.org/TR/2011/WD-html5-20110113/named-character-references.html - var safeEntityNameRe = /^[A-Za-z][A-za-z0-9]+$/; - // Used as a hook to invoke the browser's entity parsing. "), "hullo"); - equal(sanitize(""), "press me!"); - equal(sanitize("draw me!"), "draw me!"); - equal(sanitize("hello"), "hello"); - equal(sanitize("highlight"), "highlight"); - - cooked("[the answer](javascript:alert(42))", "

the answer

", "it prevents XSS"); - - cooked("\n", "


", "it doesn't circumvent XSS with comments"); - - cooked("a", "

a

", "it sanitizes spans"); - cooked("a", "

a

", "it sanitizes spans"); - cooked("a", "

a

", "it sanitizes spans"); -}); - test("URLs in BBCode tags", function() { cooked("[img]http://eviltrout.com/eviltrout.png[/img][img]http://samsaffron.com/samsaffron.png[/img]", @@ -531,23 +500,6 @@ test("URLs in BBCode tags", function() { }); -test("urlAllowed", function() { - var urlAllowed = Discourse.Markdown.urlAllowed; - - var allowed = function(url, msg) { - equal(urlAllowed(url), url, msg); - }; - - allowed("/foo/bar.html", "allows relative urls"); - allowed("http://eviltrout.com/evil/trout", "allows full urls"); - allowed("https://eviltrout.com/evil/trout", "allows https urls"); - allowed("//eviltrout.com/evil/trout", "allows protocol relative urls"); - - equal(urlAllowed("http://google.com/test'onmouseover=alert('XSS!');//.swf"), - "http://google.com/test%27onmouseover=alert(%27XSS!%27);//.swf", - "escape single quotes"); -}); - test("images", function() { cooked("[![folksy logo](http://folksy.com/images/folksy-colour.png)](http://folksy.com/)", "

\"folksy

", @@ -559,7 +511,6 @@ test("images", function() { }); test("censoring", function() { - Discourse.SiteSettings.censored_words = "shucks|whiz|whizzer"; cooked("aw shucks, golly gee whiz.", "

aw ■■■■■■, golly gee ■■■■.

", "it censors words in the Site Settings"); @@ -583,3 +534,165 @@ test("code blocks/spans hoisting", function() { "

$&

", "it works even when hoisting special replacement patterns"); }); + +test('basic bbcode', function() { + cookedPara("[b]strong[/b]", "strong", "bolds text"); + cookedPara("[i]emphasis[/i]", "emphasis", "italics text"); + cookedPara("[u]underlined[/u]", "underlined", "underlines text"); + cookedPara("[s]strikethrough[/s]", "strikethrough", "strikes-through text"); + cookedPara("[img]http://eviltrout.com/eviltrout.png[/img]", "", "links images"); + cookedPara("[email]eviltrout@mailinator.com[/email]", "eviltrout@mailinator.com", "supports [email] without a title"); + cookedPara("[b]evil [i]trout[/i][/b]", + "evil trout", + "allows embedding of tags"); + cookedPara("[EMAIL]eviltrout@mailinator.com[/EMAIL]", "eviltrout@mailinator.com", "supports upper case bbcode"); + cookedPara("[b]strong [b]stronger[/b][/b]", "strong stronger", "accepts nested bbcode tags"); +}); + +test('urls', function() { + cookedPara("[url]not a url[/url]", "not a url", "supports [url] that isn't a url"); + cookedPara("[url]http://bettercallsaul.com[/url]", "http://bettercallsaul.com", "supports [url] without parameter"); + cookedPara("[url=http://example.com]example[/url]", "example", "supports [url] with given href"); + cookedPara("[url=http://www.example.com][img]http://example.com/logo.png[/img][/url]", + "", + "supports [url] with an embedded [img]"); +}); +test('invalid bbcode', function() { + const result = new PrettyText({ lookupAvatar: false }).cook("[code]I am not closed\n\nThis text exists."); + equal(result, "

[code]I am not closed

\n\n

This text exists.

", "does not raise an error with an open bbcode tag."); +}); + +test('code', function() { + cookedPara("[code]\nx++\n[/code]", "
x++
", "makes code into pre"); + cookedPara("[code]\nx++\ny++\nz++\n[/code]", "
x++\ny++\nz++
", "makes code into pre"); + cookedPara("[code]abc\n#def\n[/code]", '
abc\n#def
', 'it handles headings in a [code] block'); + cookedPara("[code]\n s[/code]", + "
   s
", + "it doesn't trim leading whitespace"); +}); + +test('lists', function() { + cookedPara("[ul][li]option one[/li][/ul]", "
  • option one
", "creates an ul"); + cookedPara("[ol][li]option one[/li][/ol]", "
  1. option one
", "creates an ol"); + cookedPara("[ul]\n[li]option one[/li]\n[li]option two[/li]\n[/ul]", "
  • option one
  • option two
", "suppresses empty lines in lists"); +}); + +test('tags with arguments', function() { + cookedPara("[url=http://bettercallsaul.com]better call![/url]", "better call!", "supports [url] with a title"); + cookedPara("[email=eviltrout@mailinator.com]evil trout[/email]", "evil trout", "supports [email] with a title"); + cookedPara("[u][i]abc[/i][/u]", "abc", "can nest tags"); + cookedPara("[b]first[/b] [b]second[/b]", "first second", "can bold two things on the same line"); +}); + + +test("quotes", function() { + const post = Post.create({ + cooked: "

lorem ipsum

", + username: "eviltrout", + post_number: 1, + topic_id: 2 + }); + + function formatQuote(val, expected, text) { + equal(Quote.build(post, val), expected, text); + }; + + formatQuote(undefined, "", "empty string for undefined content"); + formatQuote(null, "", "empty string for null content"); + formatQuote("", "", "empty string for empty string content"); + + 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"); + + formatQuote("lorem ipsum", + "[quote=\"eviltrout, post:1, topic:2, full:true\"]\nlorem ipsum\n[/quote]\n\n", + "marks quotes as full when the quote is the full message"); + + formatQuote("**lorem** ipsum", + "[quote=\"eviltrout, post:1, topic:2, full:true\"]\n**lorem** ipsum\n[/quote]\n\n", + "keeps BBCode formatting"); + + formatQuote("this is a bug", + "[quote=\"eviltrout, post:1, topic:2\"]\nthis is <not> a bug\n[/quote]\n\n", + "it escapes the contents of the quote"); + + cookedPara("[quote]test[/quote]", + "", + "it supports quotes without params"); + + cookedPara("[quote]\n*test*\n[/quote]", + "", + "it doesn't insert a new line for italics"); + + cookedPara("[quote=,script='a'>"), "
"); + equal(pt.sanitize("

hello

"), "

hello

"); + equal(pt.sanitize("<3 <3"), "<3 <3"); + equal(pt.sanitize("<_<"), "<_<"); + cooked("hello", "

hello

", "it sanitizes while cooking"); + + cooked("disney reddit", + "

disney reddit

", + "we can embed proper links"); + + cooked("
hello
", "

hello

", "it does not allow centering"); + cooked("
hello
\nafter", "

after

", "it does not allow tables"); + cooked("
a\n
\n", "
a\n\n
\n\n
", "it does not double sanitize"); + + cooked("", "", "it does not allow most iframes"); + + cooked("", + "", + "it allows iframe to google maps"); + + cooked("", + "", + "it allows iframe to OpenStreetMap"); + + equal(pt.sanitize(""), "hullo"); + equal(pt.sanitize(""), "press me!"); + equal(pt.sanitize("draw me!"), "draw me!"); + equal(pt.sanitize("hello"), "hello"); + equal(pt.sanitize("highlight"), "highlight"); + + cooked("[the answer](javascript:alert(42))", "

the answer

", "it prevents XSS"); + + cooked("\n", "


", "it doesn't circumvent XSS with comments"); + + cooked("a", "

a

", "it sanitizes spans"); + cooked("a", "

a

", "it sanitizes spans"); + cooked("a", "

a

", "it sanitizes spans"); +}); + +test("urlAllowed", function() { + const allowed = (url, msg) => equal(hrefAllowed(url), url, msg); + + allowed("/foo/bar.html", "allows relative urls"); + allowed("http://eviltrout.com/evil/trout", "allows full urls"); + allowed("https://eviltrout.com/evil/trout", "allows https urls"); + allowed("//eviltrout.com/evil/trout", "allows protocol relative urls"); + + equal(hrefAllowed("http://google.com/test'onmouseover=alert('XSS!');//.swf"), + "http://google.com/test%27onmouseover=alert(%27XSS!%27);//.swf", + "escape single quotes"); +}); + diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index c9f35a948b..75b8cc194d 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -1,16 +1,27 @@ /* global Int8Array:true */ import { blank } from 'helpers/qunit-helpers'; +import { + emailValid, + isAnImage, + avatarUrl, + allowsAttachments, + getRawSize, + avatarImg, + defaultHomepage, + validateUploadedFiles, + getUploadMarkdown, + caretRowCol, + setCaretPosition +} from 'discourse/lib/utilities'; -module("Discourse.Utilities"); - -var utils = Discourse.Utilities; +module("lib:utilities"); test("emailValid", function() { - ok(utils.emailValid('Bob@example.com'), "allows upper case in the first part of emails"); - ok(utils.emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain"); + ok(emailValid('Bob@example.com'), "allows upper case in the first part of emails"); + ok(emailValid('bob@EXAMPLE.com'), "allows upper case in the email domain"); }); -var validUpload = utils.validateUploadedFiles; +var validUpload = validateUploadedFiles; test("validateUploadedFiles", function() { not(validUpload(null), "no files are invalid"); @@ -80,8 +91,8 @@ test("allows valid uploads to go through", function() { not(bootbox.alert.calledOnce); }); -var getUploadMarkdown = function(filename) { - return utils.getUploadMarkdown({ +var testUploadMarkdown = function(filename) { + return getUploadMarkdown({ original_filename: filename, filesize: 42, width: 100, @@ -91,26 +102,26 @@ var getUploadMarkdown = function(filename) { }; test("getUploadMarkdown", function() { - ok(getUploadMarkdown("lolcat.gif") === ''); - ok(getUploadMarkdown("important.txt") === 'important.txt (42 Bytes)\n'); + ok(testUploadMarkdown("lolcat.gif") === ''); + ok(testUploadMarkdown("important.txt") === 'important.txt (42 Bytes)\n'); }); test("isAnImage", function() { _.each(["png", "jpg", "jpeg", "bmp", "gif", "tif", "tiff", "ico"], function(extension) { var image = "image." + extension; - ok(utils.isAnImage(image), image + " is recognized as an image"); - ok(utils.isAnImage("http://foo.bar/path/to/" + image), image + " is recognized as an image"); + ok(isAnImage(image), image + " is recognized as an image"); + ok(isAnImage("http://foo.bar/path/to/" + image), image + " is recognized as an image"); }); - not(utils.isAnImage("file.txt")); - not(utils.isAnImage("http://foo.bar/path/to/file.txt")); - not(utils.isAnImage("")); + not(isAnImage("file.txt")); + not(isAnImage("http://foo.bar/path/to/file.txt")); + not(isAnImage("")); }); test("avatarUrl", function() { - var rawSize = utils.getRawSize; - blank(utils.avatarUrl('', 'tiny'), "no template returns blank"); - equal(utils.avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/" + rawSize(20) + ".png", "simple avatar url"); - equal(utils.avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/" + rawSize(45) + ".png", "different size"); + var rawSize = getRawSize; + blank(avatarUrl('', 'tiny'), "no template returns blank"); + equal(avatarUrl('/fake/template/{size}.png', 'tiny'), "/fake/template/" + rawSize(20) + ".png", "simple avatar url"); + equal(avatarUrl('/fake/template/{size}.png', 'large'), "/fake/template/" + rawSize(45) + ".png", "different size"); }); var setDevicePixelRatio = function(value) { @@ -126,19 +137,19 @@ test("avatarImg", function() { setDevicePixelRatio(2); var avatarTemplate = "/path/to/avatar/{size}.png"; - equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}), + equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny'}), "", "it returns the avatar html"); - equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}), + equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', title: 'evilest trout'}), "", "it adds a title if supplied"); - equal(utils.avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}), + equal(avatarImg({avatarTemplate: avatarTemplate, size: 'tiny', extraClasses: 'evil fish'}), "", "it adds extra classes if supplied"); - blank(utils.avatarImg({avatarTemplate: "", size: 'tiny'}), + blank(avatarImg({avatarTemplate: "", size: 'tiny'}), "it doesn't render avatars for invalid avatar template"); setDevicePixelRatio(oldRatio); @@ -146,18 +157,18 @@ test("avatarImg", function() { test("allowsAttachments", function() { Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif"; - not(utils.allowsAttachments(), "no attachments allowed by default"); + not(allowsAttachments(), "no attachments allowed by default"); Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|*"; - ok(utils.allowsAttachments(), "attachments are allowed when all extensions are allowed"); + ok(allowsAttachments(), "attachments are allowed when all extensions are allowed"); Discourse.SiteSettings.authorized_extensions = "jpg|jpeg|gif|pdf"; - ok(utils.allowsAttachments(), "attachments are allowed when at least one extension is not an image extension"); + ok(allowsAttachments(), "attachments are allowed when at least one extension is not an image extension"); }); test("defaultHomepage", function() { Discourse.SiteSettings.top_menu = "latest|top|hot"; - equal(utils.defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting"); + equal(defaultHomepage(), "latest", "default homepage is the first item in the top_menu site setting"); }); test("caretRowCol", () => { @@ -167,9 +178,9 @@ test("caretRowCol", () => { document.body.appendChild(textarea); const assertResult = (setCaretPos, expectedRowNum, expectedColNum) => { - Discourse.Utilities.setCaretPosition(textarea, setCaretPos); + setCaretPosition(textarea, setCaretPos); - const result = Discourse.Utilities.caretRowCol(textarea); + const result = caretRowCol(textarea); equal(result.rowNum, expectedRowNum, "returns the right row of the caret"); equal(result.colNum, expectedColNum, "returns the right col of the caret"); }; diff --git a/test/javascripts/mdtest/mdtest.js.erb b/test/javascripts/mdtest/mdtest.js.es6.erb similarity index 55% rename from test/javascripts/mdtest/mdtest.js.erb rename to test/javascripts/mdtest/mdtest.js.es6.erb index f1c9a71381..a17d564513 100644 --- a/test/javascripts/mdtest/mdtest.js.erb +++ b/test/javascripts/mdtest/mdtest.js.es6.erb @@ -1,8 +1,9 @@ -module("MDTest", { - setup: function() { - Discourse.SiteSettings.traditional_markdown_linebreaks = false; - } -}); +import { sanitize } from 'pretty-text/sanitizer'; +import { default as PrettyText, buildOptions } from 'pretty-text/pretty-text'; +import { hashString } from 'discourse/lib/hash'; + +// Run the MDTest spec +module("MDTest"); // This is cheating, but the trivial differences between sanitization // do not affect formatting. @@ -15,44 +16,42 @@ function normalize(str) { // We use a custom sanitizer for MD test that hoists out comments. In Discourse // they are stripped, but to be compliant with the spec they should not be. -function hoistingSanitizer(result) { - var hoisted, - m = result.match(//g); +function sanitizer(result, whiteLister) { + let hoisted; + const m = result.match(//g); if (m && m.length) { hoisted = []; - for (var i=0; i result = result.replace(tuple[1], tuple[0])); } return result; } -var md = function(input, expected, text) { - var result = Discourse.Markdown.cook(input, { - sanitizerFunction: hoistingSanitizer, - traditional_markdown_linebreaks: true - }), - resultNorm = normalize(result), - expectedNorm = normalize(expected), - same = (result === expected) || (resultNorm === expectedNorm); +function md(input, expected, text) { + + const opts = buildOptions({ siteSettings: {} }); + opts.traditionalMarkdownLinebreaks = true; + opts.sanitizer = sanitizer; + + const cooker = new PrettyText(opts); + const result = cooker.cook(input); + const resultNorm = normalize(result); + const expectedNorm = normalize(expected); + const same = (result === expected) || (resultNorm === expectedNorm); if (same) { ok(same, text); } else { - console.log(resultNorm); - console.log(expectedNorm); equal(resultNorm, expectedNorm, text); } }; diff --git a/test/javascripts/models/topic-test.js.es6 b/test/javascripts/models/topic-test.js.es6 index f1d2469b43..7938b7afd9 100644 --- a/test/javascripts/models/topic-test.js.es6 +++ b/test/javascripts/models/topic-test.js.es6 @@ -1,4 +1,6 @@ import { blank, present } from 'helpers/qunit-helpers'; +import { IMAGE_VERSION as v} from 'pretty-text/emoji'; + module("model:topic"); import Topic from 'discourse/models/topic'; @@ -75,7 +77,6 @@ test("recover", function() { test('fancyTitle', function() { var topic = Topic.create({ fancy_title: ":smile: with all :) the emojis :pear::peach:" }); - const v = Discourse.Emoji.ImageVersion; equal(topic.get('fancyTitle'), `smile with all slight_smile the emojis pearpeach`, diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 7362298d15..f867246f2f 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -22,9 +22,9 @@ //= require htmlparser.js // Stuff we need to load first +//= require pretty-text-bundle //= require main_include //= require admin -//= require_tree ../../app/assets/javascripts/defer //= require sinon-1.7.1 //= require sinon-qunit-1.0.0 @@ -43,12 +43,6 @@ window.inTestEnv = true; -window.assetPath = function(url) { - if (url.indexOf('defer') === 0) { - return "/assets/" + url; - } -}; - // Stop the message bus so we don't get ajax calls window.MessageBus.stop(); @@ -137,3 +131,5 @@ Object.keys(requirejs.entries).forEach(function(entry) { require(entry, null, null, true); } }); +require('mdtest/mdtest', null, null, true); + diff --git a/vendor/assets/javascripts/better_markdown.js b/vendor/assets/javascripts/better_markdown.js index a445aacec5..836b1c7e31 100644 --- a/vendor/assets/javascripts/better_markdown.js +++ b/vendor/assets/javascripts/better_markdown.js @@ -429,6 +429,7 @@ if ( attrs && attrs.references ) refs = attrs.references; + var html = convert_tree_to_html( input, refs , options ); merge_text_nodes( html ); return html; diff --git a/vendor/assets/javascripts/ember-qunit.js b/vendor/assets/javascripts/ember-qunit.js index c3717ef55b..ce4a591162 100644 --- a/vendor/assets/javascripts/ember-qunit.js +++ b/vendor/assets/javascripts/ember-qunit.js @@ -1043,4 +1043,4 @@ window.test = emberQunit.test; window.setResolver = emberQunit.setResolver; })(); -//# sourceMappingURL=ember-qunit.map \ No newline at end of file +//# sourceMappingURL=ember-qunit.map diff --git a/vendor/assets/javascripts/md5.js b/vendor/assets/javascripts/md5.js deleted file mode 100644 index bebcdacc26..0000000000 --- a/vendor/assets/javascripts/md5.js +++ /dev/null @@ -1,180 +0,0 @@ -/*! - * Joseph Myer's md5() algorithm wrapped in a self-invoked function to prevent - * global namespace polution, modified to hash unicode characters as UTF-8. - * - * Copyright 1999-2010, Joseph Myers, Paul Johnston, Greg Holt, Will Bond - * http://www.myersdaily.org/joseph/javascript/md5-text.html - * http://pajhome.org.uk/crypt/md5 - * - * Released under the BSD license - * http://www.opensource.org/licenses/bsd-license - */ -(function() { - function md5cycle(x, k) { - var a = x[0], b = x[1], c = x[2], d = x[3]; - - a = ff(a, b, c, d, k[0], 7, -680876936); - d = ff(d, a, b, c, k[1], 12, -389564586); - c = ff(c, d, a, b, k[2], 17, 606105819); - b = ff(b, c, d, a, k[3], 22, -1044525330); - a = ff(a, b, c, d, k[4], 7, -176418897); - d = ff(d, a, b, c, k[5], 12, 1200080426); - c = ff(c, d, a, b, k[6], 17, -1473231341); - b = ff(b, c, d, a, k[7], 22, -45705983); - a = ff(a, b, c, d, k[8], 7, 1770035416); - d = ff(d, a, b, c, k[9], 12, -1958414417); - c = ff(c, d, a, b, k[10], 17, -42063); - b = ff(b, c, d, a, k[11], 22, -1990404162); - a = ff(a, b, c, d, k[12], 7, 1804603682); - d = ff(d, a, b, c, k[13], 12, -40341101); - c = ff(c, d, a, b, k[14], 17, -1502002290); - b = ff(b, c, d, a, k[15], 22, 1236535329); - - a = gg(a, b, c, d, k[1], 5, -165796510); - d = gg(d, a, b, c, k[6], 9, -1069501632); - c = gg(c, d, a, b, k[11], 14, 643717713); - b = gg(b, c, d, a, k[0], 20, -373897302); - a = gg(a, b, c, d, k[5], 5, -701558691); - d = gg(d, a, b, c, k[10], 9, 38016083); - c = gg(c, d, a, b, k[15], 14, -660478335); - b = gg(b, c, d, a, k[4], 20, -405537848); - a = gg(a, b, c, d, k[9], 5, 568446438); - d = gg(d, a, b, c, k[14], 9, -1019803690); - c = gg(c, d, a, b, k[3], 14, -187363961); - b = gg(b, c, d, a, k[8], 20, 1163531501); - a = gg(a, b, c, d, k[13], 5, -1444681467); - d = gg(d, a, b, c, k[2], 9, -51403784); - c = gg(c, d, a, b, k[7], 14, 1735328473); - b = gg(b, c, d, a, k[12], 20, -1926607734); - - a = hh(a, b, c, d, k[5], 4, -378558); - d = hh(d, a, b, c, k[8], 11, -2022574463); - c = hh(c, d, a, b, k[11], 16, 1839030562); - b = hh(b, c, d, a, k[14], 23, -35309556); - a = hh(a, b, c, d, k[1], 4, -1530992060); - d = hh(d, a, b, c, k[4], 11, 1272893353); - c = hh(c, d, a, b, k[7], 16, -155497632); - b = hh(b, c, d, a, k[10], 23, -1094730640); - a = hh(a, b, c, d, k[13], 4, 681279174); - d = hh(d, a, b, c, k[0], 11, -358537222); - c = hh(c, d, a, b, k[3], 16, -722521979); - b = hh(b, c, d, a, k[6], 23, 76029189); - a = hh(a, b, c, d, k[9], 4, -640364487); - d = hh(d, a, b, c, k[12], 11, -421815835); - c = hh(c, d, a, b, k[15], 16, 530742520); - b = hh(b, c, d, a, k[2], 23, -995338651); - - a = ii(a, b, c, d, k[0], 6, -198630844); - d = ii(d, a, b, c, k[7], 10, 1126891415); - c = ii(c, d, a, b, k[14], 15, -1416354905); - b = ii(b, c, d, a, k[5], 21, -57434055); - a = ii(a, b, c, d, k[12], 6, 1700485571); - d = ii(d, a, b, c, k[3], 10, -1894986606); - c = ii(c, d, a, b, k[10], 15, -1051523); - b = ii(b, c, d, a, k[1], 21, -2054922799); - a = ii(a, b, c, d, k[8], 6, 1873313359); - d = ii(d, a, b, c, k[15], 10, -30611744); - c = ii(c, d, a, b, k[6], 15, -1560198380); - b = ii(b, c, d, a, k[13], 21, 1309151649); - a = ii(a, b, c, d, k[4], 6, -145523070); - d = ii(d, a, b, c, k[11], 10, -1120210379); - c = ii(c, d, a, b, k[2], 15, 718787259); - b = ii(b, c, d, a, k[9], 21, -343485551); - - x[0] = add32(a, x[0]); - x[1] = add32(b, x[1]); - x[2] = add32(c, x[2]); - x[3] = add32(d, x[3]); - } - - function cmn(q, a, b, x, s, t) { - a = add32(add32(a, q), add32(x, t)); - return add32((a << s) | (a >>> (32 - s)), b); - } - - function ff(a, b, c, d, x, s, t) { - return cmn((b & c) | ((~b) & d), a, b, x, s, t); - } - - function gg(a, b, c, d, x, s, t) { - return cmn((b & d) | (c & (~d)), a, b, x, s, t); - } - - function hh(a, b, c, d, x, s, t) { - return cmn(b ^ c ^ d, a, b, x, s, t); - } - - function ii(a, b, c, d, x, s, t) { - return cmn(c ^ (b | (~d)), a, b, x, s, t); - } - - function md51(s) { - // Converts the string to UTF-8 "bytes" when necessary - if (/[\x80-\xFF]/.test(s)) { - s = unescape(encodeURI(s)); - } - txt = ''; - var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; - for (i = 64; i <= s.length; i += 64) { - md5cycle(state, md5blk(s.substring(i - 64, i))); - } - s = s.substring(i - 64); - var tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; - for (i = 0; i < s.length; i++) - tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); - tail[i >> 2] |= 0x80 << ((i % 4) << 3); - if (i > 55) { - md5cycle(state, tail); - for (i = 0; i < 16; i++) tail[i] = 0; - } - tail[14] = n * 8; - md5cycle(state, tail); - return state; - } - - function md5blk(s) { /* I figured global was faster. */ - var md5blks = [], i; /* Andy King said do it this way. */ - for (i = 0; i < 64; i += 4) { - md5blks[i >> 2] = s.charCodeAt(i) + - (s.charCodeAt(i + 1) << 8) + - (s.charCodeAt(i + 2) << 16) + - (s.charCodeAt(i + 3) << 24); - } - return md5blks; - } - - var hex_chr = '0123456789abcdef'.split(''); - - function rhex(n) { - var s = '', j = 0; - for (; j < 4; j++) - s += hex_chr[(n >> (j * 8 + 4)) & 0x0F] + - hex_chr[(n >> (j * 8)) & 0x0F]; - return s; - } - - function hex(x) { - for (var i = 0; i < x.length; i++) - x[i] = rhex(x[i]); - return x.join(''); - } - - md5 = function (s) { - return hex(md51(s)); - } - - /* this function is much faster, so if possible we use it. Some IEs are the - only ones I know of that need the idiotic second function, generated by an - if clause. */ - function add32(a, b) { - return (a + b) & 0xFFFFFFFF; - } - - if (md5('hello') != '5d41402abc4b2a76b9719d911017c592') { - function add32(x, y) { - var lsw = (x & 0xFFFF) + (y & 0xFFFF), - msw = (x >> 16) + (y >> 16) + (lsw >> 16); - return (msw << 16) | (lsw & 0xFFFF); - } - } -})(); \ No newline at end of file From 56f07529bb318aefbb582f8a317d50d07e347ee2 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 30 Jun 2016 12:26:49 -0400 Subject: [PATCH 002/170] REFACTOR: Migrate more legacy JS to ES6 --- app/assets/javascripts/discourse.js | 1 - .../components/composer-editor.js.es6 | 3 +- .../components/composer-title.js.es6 | 3 +- .../discourse/components/login-buttons.js.es6 | 6 +- .../discourse/controllers/composer.js.es6 | 3 +- .../controllers/create-account.js.es6 | 53 ++++++------- .../controllers/feature-topic.js.es6 | 5 +- .../discourse/controllers/login.js.es6 | 11 ++- .../discourse/lib/static-route-builder.js.es6 | 3 +- .../discourse/models/input-validation.js.es6 | 4 + .../discourse/models/input_validation.js | 11 --- .../discourse/models/login-method.js.es6 | 71 ++++++++++++++++++ .../discourse/models/login_method.js | 74 ------------------- .../javascripts/discourse/models/model.js.es6 | 4 +- .../discourse/models/static-page.js.es6 | 22 ++++++ .../discourse/models/static_page.js | 20 ----- .../discourse/routes/application.js.es6 | 3 +- .../routes/build-static-route.js.es6 | 3 +- lib/plugin/instance.rb | 17 ++++- .../controllers/poll-ui-builder.js.es6 | 3 +- 20 files changed, 168 insertions(+), 152 deletions(-) create mode 100644 app/assets/javascripts/discourse/models/input-validation.js.es6 delete mode 100644 app/assets/javascripts/discourse/models/input_validation.js create mode 100644 app/assets/javascripts/discourse/models/login-method.js.es6 delete mode 100644 app/assets/javascripts/discourse/models/login_method.js create mode 100644 app/assets/javascripts/discourse/models/static-page.js.es6 delete mode 100644 app/assets/javascripts/discourse/models/static_page.js diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index a97d2aeaea..926cc59fb2 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -179,7 +179,6 @@ window.Discourse = Ember.Application.extend(Discourse.Ajax, { }) }).create(); - Discourse.Markdown = { whiteListTag: Ember.K, whiteListIframe: Ember.K diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 076b05757b..362165eda2 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -4,6 +4,7 @@ import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentio import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags'; import { fetchUnseenTagHashtags, linkSeenTagHashtags } from 'discourse/lib/link-tag-hashtag'; import { load } from 'pretty-text/oneboxer'; +import InputValidation from 'discourse/models/input-validation'; import { tinyAvatar, displayErrorForUpload, @@ -121,7 +122,7 @@ export default Ember.Component.extend({ } if (reason) { - return Discourse.InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt }); + return InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt }); } }, diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6 index 2eae670557..8ede0fd3d6 100644 --- a/app/assets/javascripts/discourse/components/composer-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-title.js.es6 @@ -1,4 +1,5 @@ import { default as computed, on } from 'ember-addons/ember-computed-decorators'; +import InputValidation from 'discourse/models/input-validation'; export default Ember.Component.extend({ classNames: ['title-input'], @@ -23,7 +24,7 @@ export default Ember.Component.extend({ } if (reason) { - return Discourse.InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt }); + return InputValidation.create({ failed: true, reason, lastShownAt: lastValidatedAt }); } } }); diff --git a/app/assets/javascripts/discourse/components/login-buttons.js.es6 b/app/assets/javascripts/discourse/components/login-buttons.js.es6 index 07cd9fc57b..ccaabff239 100644 --- a/app/assets/javascripts/discourse/components/login-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/login-buttons.js.es6 @@ -1,11 +1,13 @@ +import { findAll } from 'discourse/models/login-method'; + export default Ember.Component.extend({ elementId: 'login-buttons', classNameBindings: ['hidden'], - hidden: Em.computed.equal('buttons.length', 0), + hidden: Ember.computed.equal('buttons.length', 0), buttons: function() { - return Em.get('Discourse.LoginMethod.all'); + return findAll(this.siteSettings); }.property(), actions: { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index c6fe3a7815..0903a27471 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -5,6 +5,7 @@ import Composer from 'discourse/models/composer'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { relativeAge } from 'discourse/lib/formatter'; import { escapeExpression } from 'discourse/lib/utilities'; +import InputValidation from 'discourse/models/input-validation'; function loadDraft(store, opts) { opts = opts || {}; @@ -645,7 +646,7 @@ export default Ember.Controller.extend({ @computed('model.categoryId', 'lastValidatedAt') categoryValidation(categoryId, lastValidatedAt) { if( !this.siteSettings.allow_uncategorized_topics && !categoryId) { - return Discourse.InputValidation.create({ failed: true, reason: I18n.t('composer.error.category_missing'), lastShownAt: lastValidatedAt }); + return InputValidation.create({ failed: true, reason: I18n.t('composer.error.category_missing'), lastShownAt: lastValidatedAt }); } }, diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index 8a870acfd8..985fa42dd0 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -3,6 +3,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { setting } from 'discourse/lib/computed'; import { on } from 'ember-addons/ember-computed-decorators'; import { emailValid } from 'discourse/lib/utilities'; +import InputValidation from 'discourse/models/input-validation'; export default Ember.Controller.extend(ModalFunctionality, { needs: ['login'], @@ -87,10 +88,10 @@ export default Ember.Controller.extend(ModalFunctionality, { // Validate the name. nameValidation: function() { if (Discourse.SiteSettings.full_name_required && Ember.isEmpty(this.get('accountName'))) { - return Discourse.InputValidation.create({ failed: true }); + return InputValidation.create({ failed: true }); } - return Discourse.InputValidation.create({ok: true}); + return InputValidation.create({ok: true}); }.property('accountName'), // Check the email address @@ -98,7 +99,7 @@ export default Ember.Controller.extend(ModalFunctionality, { // If blank, fail without a reason let email; if (Ember.isEmpty(this.get('accountEmail'))) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true }); } @@ -106,14 +107,14 @@ export default Ember.Controller.extend(ModalFunctionality, { email = this.get("accountEmail"); if (this.get('rejectedEmails').contains(email)) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.email.invalid') }); } if ((this.get('authOptions.email') === email) && this.get('authOptions.email_valid')) { - return Discourse.InputValidation.create({ + return InputValidation.create({ ok: true, reason: I18n.t('user.email.authenticated', { provider: this.authProviderDisplayName(this.get('authOptions.auth_provider')) @@ -122,13 +123,13 @@ export default Ember.Controller.extend(ModalFunctionality, { } if (emailValid(email)) { - return Discourse.InputValidation.create({ + return InputValidation.create({ ok: true, reason: I18n.t('user.email.ok') }); } - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.email.invalid') }); @@ -176,15 +177,15 @@ export default Ember.Controller.extend(ModalFunctionality, { if (this.usernameNeedsToBeValidatedWithEmail()) { if (this.get('emailValidation.failed')) { if (this.shouldCheckUsernameMatch()) { - return this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + return this.set('uniqueUsernameValidation', InputValidation.create({ failed: true, reason: I18n.t('user.username.enter_email') })); } else { - return this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ failed: true })); + return this.set('uniqueUsernameValidation', InputValidation.create({ failed: true })); } } else if (this.shouldCheckUsernameMatch()) { - this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + this.set('uniqueUsernameValidation', InputValidation.create({ failed: true, reason: I18n.t('user.username.checking') })); @@ -197,7 +198,7 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set('uniqueUsernameValidation', null); if (this.get('accountUsername') === this.get('prefilledUsername')) { - return Discourse.InputValidation.create({ + return InputValidation.create({ ok: true, reason: I18n.t('user.username.prefilled') }); @@ -205,14 +206,14 @@ export default Ember.Controller.extend(ModalFunctionality, { // If blank, fail without a reason if (Ember.isEmpty(this.get('accountUsername'))) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true }); } // If too short if (this.get('accountUsername').length < Discourse.SiteSettings.min_username_length) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.username.too_short') }); @@ -220,7 +221,7 @@ export default Ember.Controller.extend(ModalFunctionality, { // If too long if (this.get('accountUsername').length > this.get('maxUsernameLength')) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.username.too_long') }); @@ -228,7 +229,7 @@ export default Ember.Controller.extend(ModalFunctionality, { this.checkUsernameAvailability(); // Let's check it out asynchronously - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.username.checking') }); @@ -247,23 +248,23 @@ export default Ember.Controller.extend(ModalFunctionality, { if (result.is_developer) { _this.set('isDeveloper', true); } - return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + return _this.set('uniqueUsernameValidation', InputValidation.create({ ok: true, reason: I18n.t('user.username.available') })); } else { if (result.suggestion) { - return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + return _this.set('uniqueUsernameValidation', InputValidation.create({ failed: true, reason: I18n.t('user.username.not_available', result) })); } else if (result.errors) { - return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + return _this.set('uniqueUsernameValidation', InputValidation.create({ failed: true, reason: result.errors.join(' ') })); } else { - return _this.set('uniqueUsernameValidation', Discourse.InputValidation.create({ + return _this.set('uniqueUsernameValidation', InputValidation.create({ failed: true, reason: I18n.t('user.username.enter_email') })); @@ -287,47 +288,47 @@ export default Ember.Controller.extend(ModalFunctionality, { // Validate the password passwordValidation: function() { if (!this.get('passwordRequired')) { - return Discourse.InputValidation.create({ ok: true }); + return InputValidation.create({ ok: true }); } // If blank, fail without a reason const password = this.get("accountPassword"); if (Ember.isEmpty(this.get('accountPassword'))) { - return Discourse.InputValidation.create({ failed: true }); + return InputValidation.create({ failed: true }); } // If too short const passwordLength = this.get('isDeveloper') ? Discourse.SiteSettings.min_admin_password_length : Discourse.SiteSettings.min_password_length; if (password.length < passwordLength) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.password.too_short') }); } if (this.get('rejectedPasswords').contains(password)) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.password.common') }); } if (!Ember.isEmpty(this.get('accountUsername')) && this.get('accountPassword') === this.get('accountUsername')) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.password.same_as_username') }); } if (!Ember.isEmpty(this.get('accountEmail')) && this.get('accountPassword') === this.get('accountEmail')) { - return Discourse.InputValidation.create({ + return InputValidation.create({ failed: true, reason: I18n.t('user.password.same_as_email') }); } // Looks good! - return Discourse.InputValidation.create({ + return InputValidation.create({ ok: true, reason: I18n.t('user.password.ok') }); diff --git a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 index 34c1c9516f..f83e50c62c 100644 --- a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 @@ -1,6 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { categoryLinkHTML } from 'discourse/helpers/category-link'; import computed from 'ember-addons/ember-computed-decorators'; +import InputValidation from 'discourse/models/input-validation'; export default Ember.Controller.extend(ModalFunctionality, { needs: ["topic"], @@ -68,14 +69,14 @@ export default Ember.Controller.extend(ModalFunctionality, { @computed("pinDisabled") pinInCategoryValidation(pinDisabled) { if (pinDisabled) { - return Discourse.InputValidation.create({ failed: true, reason: I18n.t("topic.feature_topic.pin_validation") }); + return InputValidation.create({ failed: true, reason: I18n.t("topic.feature_topic.pin_validation") }); } }, @computed("pinGloballyDisabled") pinGloballyValidation(pinGloballyDisabled) { if (pinGloballyDisabled) { - return Discourse.InputValidation.create({ failed: true, reason: I18n.t("topic.feature_topic.pin_validation") }); + return InputValidation.create({ failed: true, reason: I18n.t("topic.feature_topic.pin_validation") }); } }, diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index d85d449ac7..fd884e391c 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -1,6 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import showModal from 'discourse/lib/show-modal'; import { setting } from 'discourse/lib/computed'; +import { findAll } from 'discourse/models/login-method'; // This is happening outside of the app via popup const AuthErrors = @@ -22,12 +23,10 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set('loggedIn', false); }, - /** - Determines whether at least one login button is enabled - **/ + // Determines whether at least one login button is enabled hasAtLeastOneLoginButton: function() { - return Em.get("Discourse.LoginMethod.all").length > 0; - }.property("Discourse.LoginMethod.all.[]"), + return findAll(this.siteSettings).length > 0; + }.property(), loginButtonText: function() { return this.get('loggingIn') ? I18n.t('login.logging_in') : I18n.t('login.title'); @@ -175,7 +174,7 @@ export default Ember.Controller.extend(ModalFunctionality, { authMessage: (function() { if (Ember.isEmpty(this.get('authenticate'))) return ""; - const method = Discourse.get('LoginMethod.all').findProperty("name", this.get("authenticate")); + const method = findAll(this.siteSettings).findProperty("name", this.get("authenticate")); if(method){ return method.get('message'); } diff --git a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 index 0167de4a8a..0174c16483 100644 --- a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 +++ b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 @@ -1,4 +1,5 @@ import DiscourseURL from 'discourse/lib/url'; +import StaticPage from 'discourse/models/static-page'; const configs = { "faq": "faq_url", @@ -27,7 +28,7 @@ export default function(page) { }, model() { - return Discourse.StaticPage.find(page); + return StaticPage.find(page); }, setupController(controller, model) { diff --git a/app/assets/javascripts/discourse/models/input-validation.js.es6 b/app/assets/javascripts/discourse/models/input-validation.js.es6 new file mode 100644 index 0000000000..fa32d645d7 --- /dev/null +++ b/app/assets/javascripts/discourse/models/input-validation.js.es6 @@ -0,0 +1,4 @@ +import Model from 'discourse/models/model'; + +// A trivial model we use to handle input validation +export default Model.extend(); diff --git a/app/assets/javascripts/discourse/models/input_validation.js b/app/assets/javascripts/discourse/models/input_validation.js deleted file mode 100644 index 28ed8a6ae3..0000000000 --- a/app/assets/javascripts/discourse/models/input_validation.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - A trivial model we use to handle input validation - - @class InputValidation - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.InputValidation = Discourse.Model.extend({}); - - diff --git a/app/assets/javascripts/discourse/models/login-method.js.es6 b/app/assets/javascripts/discourse/models/login-method.js.es6 new file mode 100644 index 0000000000..254c401cf9 --- /dev/null +++ b/app/assets/javascripts/discourse/models/login-method.js.es6 @@ -0,0 +1,71 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +const LoginMethod = Ember.Object.extend({ + @computed + title() { + + const titleSetting = this.get('titleSetting'); + if (!Ember.isEmpty(titleSetting)) { + const result = this.siteSettings[titleSetting]; + if (!Ember.isEmpty(result)) { return result; } + } + + return this.get("titleOverride") || I18n.t("login." + this.get("name") + ".title"); + }, + + @computed + message() { + return this.get("messageOverride") || I18n.t("login." + this.get("name") + ".message"); + } +}); + +let methods; +let preRegister; + +export function findAll(siteSettings) { + if (methods) { return methods; } + + methods = []; + + [ "google_oauth2", "facebook", "cas", "twitter", "yahoo", "instagram", "github" ].forEach(name => { + if (siteSettings["enable_" + name + "_logins"]) { + const params = { name }; + if (name === "google_oauth2") { + params.frameWidth = 850; + params.frameHeight = 500; + } else if (name === "facebook") { + params.frameHeight = 450; + } + + params.siteSettings = siteSettings; + methods.pushObject(LoginMethod.create(params)); + } + }); + + if (preRegister){ + preRegister.forEach(method => { + const enabledSetting = method.get('enabledSetting'); + if (enabledSetting) { + if (siteSettings[enabledSetting]) { + methods.pushObject(method); + } + } else { + methods.pushObject(method); + } + }); + preRegister = undefined; + } + return methods; +} + +export function register(method) { + method = LoginMethod.create(method); + if (methods) { + methods.pushObject(method); + } else { + preRegister = preRegister || []; + preRegister.push(method); + } +} + +export default LoginMethod; diff --git a/app/assets/javascripts/discourse/models/login_method.js b/app/assets/javascripts/discourse/models/login_method.js deleted file mode 100644 index dff01f0317..0000000000 --- a/app/assets/javascripts/discourse/models/login_method.js +++ /dev/null @@ -1,74 +0,0 @@ -Discourse.LoginMethod = Ember.Object.extend({ - title: function() { - var titleSetting = this.get('titleSetting'); - if (!Ember.isEmpty(titleSetting)) { - var result = Discourse.SiteSettings[titleSetting]; - if (!Ember.isEmpty(result)) { return result; } - } - - return this.get("titleOverride") || I18n.t("login." + this.get("name") + ".title"); - }.property(), - - message: function() { - return this.get("messageOverride") || I18n.t("login." + this.get("name") + ".message"); - }.property() -}); - -// Note, you can add login methods by adding to the list -// just Em.get("Discourse.LoginMethod.all") and then -// pushObject for any new methods -Discourse.LoginMethod.reopenClass({ - register: function(method) { - if (this.methods){ - this.methods.pushObject(method); - } else { - this.preRegister = this.preRegister || []; - this.preRegister.push(method); - } - }, - - all: function(){ - if (this.methods) { return this.methods; } - - var methods = this.methods = Em.A(); - - [ "google_oauth2", - "facebook", - "cas", - "twitter", - "yahoo", - "instagram", - "github" - ].forEach(function(name){ - if (Discourse.SiteSettings["enable_" + name + "_logins"]) { - - var params = {name: name}; - - if (name === "google_oauth2") { - params.frameWidth = 850; - params.frameHeight = 500; - } else if (name === "facebook") { - params.frameHeight = 450; - } - - methods.pushObject(Discourse.LoginMethod.create(params)); - } - }); - - if (this.preRegister){ - this.preRegister.forEach(function(method){ - var enabledSetting = method.get('enabledSetting'); - if (enabledSetting) { - if (Discourse.SiteSettings[enabledSetting]) { - methods.pushObject(method); - } - } else { - methods.pushObject(method); - } - }); - delete this.preRegister; - } - return methods; - }.property() -}); - diff --git a/app/assets/javascripts/discourse/models/model.js.es6 b/app/assets/javascripts/discourse/models/model.js.es6 index d7e3e650b5..ebc892dba7 100644 --- a/app/assets/javascripts/discourse/models/model.js.es6 +++ b/app/assets/javascripts/discourse/models/model.js.es6 @@ -1,8 +1,8 @@ const Model = Ember.Object.extend(); Model.reopenClass({ - extractByKey: function(collection, klass) { - var retval = {}; + extractByKey(collection, klass) { + const retval = {}; if (Ember.isEmpty(collection)) { return retval; } collection.forEach(function(item) { diff --git a/app/assets/javascripts/discourse/models/static-page.js.es6 b/app/assets/javascripts/discourse/models/static-page.js.es6 new file mode 100644 index 0000000000..a935b0edcd --- /dev/null +++ b/app/assets/javascripts/discourse/models/static-page.js.es6 @@ -0,0 +1,22 @@ +const StaticPage = Ember.Object.extend(); + +StaticPage.reopenClass({ + find(path) { + return new Ember.RSVP.Promise(resolve => { + // Models shouldn't really be doing Ajax request, but this is a huge speed boost if we + // preload content. + const $preloaded = $("noscript[data-path=\"/" + path + "\"]"); + if ($preloaded.length) { + let text = $preloaded.text(); + text = text.match(/((?:.|[\n\r])*)/)[1]; + resolve(StaticPage.create({path: path, html: text})); + } else { + Discourse.ajax(path + ".html", {dataType: 'html'}).then(function (result) { + resolve(StaticPage.create({path: path, html: result})); + }); + } + }); + } +}); + +export default StaticPage; diff --git a/app/assets/javascripts/discourse/models/static_page.js b/app/assets/javascripts/discourse/models/static_page.js deleted file mode 100644 index 708c4ae057..0000000000 --- a/app/assets/javascripts/discourse/models/static_page.js +++ /dev/null @@ -1,20 +0,0 @@ -Discourse.StaticPage = Em.Object.extend(); - -Discourse.StaticPage.reopenClass({ - find: function(path) { - return new Em.RSVP.Promise(function(resolve) { - // Models shouldn't really be doing Ajax request, but this is a huge speed boost if we - // preload content. - var $preloaded = $("noscript[data-path=\"/" + path + "\"]"); - if ($preloaded.length) { - var text = $preloaded.text(); - text = text.match(/((?:.|[\n\r])*)/)[1]; - resolve(Discourse.StaticPage.create({path: path, html: text})); - } else { - Discourse.ajax(path + ".html", {dataType: 'html'}).then(function (result) { - resolve(Discourse.StaticPage.create({path: path, html: result})); - }); - } - }); - } -}); diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index 3164199ffb..aa0a999456 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -4,6 +4,7 @@ import showModal from 'discourse/lib/show-modal'; import OpenComposer from "discourse/mixins/open-composer"; import Category from 'discourse/models/category'; import mobile from 'discourse/lib/mobile'; +import { findAll } from 'discourse/models/login-method'; function unlessReadOnly(method, message) { return function() { @@ -202,7 +203,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { }, _autoLogin(modal, modalClass, notAuto) { - const methods = Em.get('Discourse.LoginMethod.all'); + const methods = findAll(this.siteSettings); if (!this.siteSettings.enable_local_logins && methods.length === 1) { this.controllerFor('login').send('externalLogin', methods[0]); } else { diff --git a/app/assets/javascripts/discourse/routes/build-static-route.js.es6 b/app/assets/javascripts/discourse/routes/build-static-route.js.es6 index eab6d40622..e7e468a71a 100644 --- a/app/assets/javascripts/discourse/routes/build-static-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-static-route.js.es6 @@ -1,9 +1,10 @@ import DiscourseRoute from 'discourse/routes/discourse'; +import StaticPage from 'discourse/models/static-page'; export default function(pageName) { const route = { model() { - return Discourse.StaticPage.find(pageName); + return StaticPage.find(pageName); }, renderTemplate() { diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 13238b6f98..cd2e928220 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -234,7 +234,22 @@ class Plugin::Instance auth_providers.each do |auth| - js << "Discourse.LoginMethod.register(Discourse.LoginMethod.create(#{auth.to_json}));\n" + auth_json = auth.to_json + hash = Digest::SHA1.hexdigest(auth_json) + js << < Date: Thu, 30 Jun 2016 13:55:44 -0400 Subject: [PATCH 003/170] REFACTOR: Remove `Discourse.Ajax` --- .../admin/components/ip-lookup.js.es6 | 7 +- .../controllers/admin-backups-index.js.es6 | 3 +- .../controllers/admin-email-index.js.es6 | 3 +- .../admin/controllers/admin-emojis.js.es6 | 3 +- .../controllers/admin-groups-bulk.js.es6 | 3 +- .../controllers/admin-groups-type.js.es6 | 3 +- .../admin/controllers/admin-user-index.js.es6 | 5 +- .../modals/admin-edit-badge-groupings.js.es6 | 3 +- .../admin/models/admin-dashboard.js.es6 | 5 +- .../admin/models/admin-user.js.es6 | 61 ++++---- .../javascripts/admin/models/api-key.js.es6 | 9 +- .../javascripts/admin/models/backup.js.es6 | 13 +- .../admin/models/color-scheme.js.es6 | 7 +- .../javascripts/admin/models/email-log.js.es6 | 3 +- .../admin/models/email-preview.js.es6 | 3 +- .../admin/models/email-settings.js.es6 | 3 +- .../admin/models/email-template.js.es6 | 3 +- .../admin/models/flagged-post.js.es6 | 13 +- .../admin/models/incoming-email.js.es6 | 7 +- .../javascripts/admin/models/permalink.js.es6 | 7 +- .../javascripts/admin/models/report.js.es6 | 3 +- .../admin/models/screened-email.js.es6 | 5 +- .../admin/models/screened-ip-address.js.es6 | 9 +- .../admin/models/screened-url.js.es6 | 3 +- .../admin/models/site-setting.js.es6 | 5 +- .../javascripts/admin/models/site-text.js.es6 | 3 +- .../admin/models/staff-action-log.js.es6 | 3 +- .../admin/models/version-check.js.es6 | 3 +- .../admin/routes/admin-backups.js.es6 | 3 +- .../admin/routes/admin-badges-show.js.es6 | 3 +- .../admin/routes/admin-badges.js.es6 | 3 +- .../admin/routes/admin-emojis.js.es6 | 3 +- app/assets/javascripts/discourse.js | 8 +- .../adapters/post-reply-history.js.es6 | 3 +- .../discourse/adapters/post-reply.js.es6 | 3 +- .../discourse/adapters/post.js.es6 | 3 +- .../discourse/adapters/rest.js.es6 | 3 +- .../discourse/adapters/topic-list.js.es6 | 3 +- .../discourse/adapters/topic.js.es6 | 3 +- .../components/composer-editor.js.es6 | 3 +- .../controllers/create-account.js.es6 | 3 +- .../controllers/edit-topic-auto-close.js.es6 | 3 +- .../controllers/feature-topic.js.es6 | 3 +- .../controllers/forgot-password.js.es6 | 3 +- .../controllers/full-page-search.js.es6 | 5 +- .../discourse/controllers/login.js.es6 | 3 +- .../controllers/not-activated.js.es6 | 3 +- .../preferences/badge-title.js.es6 | 3 +- .../controllers/preferences/card-badge.js.es6 | 3 +- .../controllers/reorder-categories.js.es6 | 3 +- .../discourse/controllers/static.js.es6 | 3 +- .../controllers/user-notifications.js.es6 | 4 +- .../initializers/page-tracking.js.es6 | 5 +- .../javascripts/discourse/lib/ajax.js.es6 | 122 ++++++++++++++++ .../discourse/lib/click-track.js.es6 | 5 +- .../discourse/lib/export-csv.js.es6 | 3 +- .../lib/link-category-hashtags.js.es6 | 3 +- .../discourse/lib/link-mentions.js.es6 | 3 +- .../discourse/lib/link-tag-hashtag.js.es6 | 3 +- .../discourse/lib/load-script.js.es6 | 3 +- .../discourse/lib/screen-track.js.es6 | 3 +- .../javascripts/discourse/lib/search.js.es6 | 3 +- .../javascripts/discourse/mixins/ajax.js | 138 ------------------ .../discourse/models/action-summary.js.es6 | 7 +- .../javascripts/discourse/models/badge.js.es6 | 9 +- .../discourse/models/category-list.js.es6 | 6 +- .../discourse/models/category.js.es6 | 11 +- .../javascripts/discourse/models/draft.js.es6 | 7 +- .../javascripts/discourse/models/group.js.es6 | 27 ++-- .../discourse/models/invite.js.es6 | 11 +- .../discourse/models/live-post-counts.es6 | 3 +- .../discourse/models/post-stream.js.es6 | 11 +- .../javascripts/discourse/models/post.js.es6 | 31 ++-- .../discourse/models/static-page.js.es6 | 3 +- .../javascripts/discourse/models/store.js.es6 | 5 +- .../discourse/models/tag-group.js.es6 | 5 +- .../discourse/models/topic-details.js.es6 | 7 +- .../discourse/models/topic-list.js.es6 | 5 +- .../javascripts/discourse/models/topic.js.es6 | 53 +++---- .../discourse/models/user-badge.js.es6 | 9 +- .../discourse/models/user-posts-stream.js.es6 | 3 +- .../discourse/models/user-stream.js.es6 | 3 +- .../javascripts/discourse/models/user.js.es6 | 35 ++--- .../javascripts/discourse/routes/about.js.es6 | 3 +- .../discourse/routes/application.js.es6 | 5 +- .../discourse/routes/full-page-search.js.es6 | 3 +- .../discourse/routes/unknown.js.es6 | 3 +- .../widgets/notification-item.js.es6 | 3 +- .../discourse/widgets/post-cooked.js.es6 | 3 +- app/assets/javascripts/main_include.js | 2 +- .../javascripts/pretty-text/oneboxer.js.es6 | 4 +- lib/plugin/instance.rb | 1 + .../initializers/apply-details.js.es6 | 3 +- .../javascripts/components/poll-voters.js.es6 | 3 +- .../javascripts/controllers/poll.js.es6 | 5 +- .../initializers/add-poll-ui-builder.js.es6 | 2 +- .../adapters/topic-list-test.js.es6 | 13 -- .../admin/models/admin-user-test.js.es6 | 15 +- .../admin/models/api-key-test.js.es6 | 48 ------ .../admin/models/flagged-post-test.js.es6 | 21 --- .../fixtures/group-fixtures.js.es6 | 18 +-- test/javascripts/fixtures/topic.js.es6 | 4 +- test/javascripts/fixtures/user-badges.js.es6 | 57 ++++++++ .../javascripts/fixtures/user_fixtures.js.es6 | 2 +- .../helpers/create-pretender.js.es6 | 14 +- .../lib/click-track-edit-history-test.js.es6 | 3 - .../lib/click-track-profile-page-test.js.es6 | 3 - test/javascripts/lib/click-track-test.js.es6 | 3 - test/javascripts/models/badge-test.js.es6 | 12 +- test/javascripts/models/topic-test.js.es6 | 6 - .../javascripts/models/user-badge-test.js.es6 | 37 ++--- 111 files changed, 567 insertions(+), 549 deletions(-) create mode 100644 app/assets/javascripts/discourse/lib/ajax.js.es6 delete mode 100644 app/assets/javascripts/discourse/mixins/ajax.js delete mode 100644 test/javascripts/adapters/topic-list-test.js.es6 delete mode 100644 test/javascripts/admin/models/api-key-test.js.es6 delete mode 100644 test/javascripts/admin/models/flagged-post-test.js.es6 create mode 100644 test/javascripts/fixtures/user-badges.js.es6 diff --git a/app/assets/javascripts/admin/components/ip-lookup.js.es6 b/app/assets/javascripts/admin/components/ip-lookup.js.es6 index 63f61dcc98..0c9b73d54a 100644 --- a/app/assets/javascripts/admin/components/ip-lookup.js.es6 +++ b/app/assets/javascripts/admin/components/ip-lookup.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; export default Ember.Component.extend({ classNames: ["ip-lookup"], @@ -23,7 +24,7 @@ export default Ember.Component.extend({ this.set("show", true); if (!this.get("location")) { - Discourse.ajax("/admin/users/ip-info", { + ajax("/admin/users/ip-info", { data: { ip: this.get("ip") } }).then(function (location) { self.set("location", Em.Object.create(location)); @@ -39,7 +40,7 @@ export default Ember.Component.extend({ "order": "trust_level DESC" }; - Discourse.ajax("/admin/users/total-others-with-same-ip", { data }).then(function (result) { + ajax("/admin/users/total-others-with-same-ip", { data }).then(function (result) { self.set("totalOthersWithSameIP", result.total); }); @@ -67,7 +68,7 @@ export default Ember.Component.extend({ totalOthersWithSameIP: null }); - Discourse.ajax("/admin/users/delete-others-with-same-ip.json", { + ajax("/admin/users/delete-others-with-same-ip.json", { type: "DELETE", data: { "ip": self.get("ip"), diff --git a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 index 1617e6133a..98f76405a2 100644 --- a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; export default Ember.ArrayController.extend({ needs: ["adminBackups"], status: Ember.computed.alias("controllers.adminBackups"), @@ -39,7 +40,7 @@ export default Ember.ArrayController.extend({ _toggleReadOnlyMode(enable) { var site = this.site; - Discourse.ajax("/admin/backups/readonly", { + ajax("/admin/backups/readonly", { type: "PUT", data: { enable: enable } }).then(function() { diff --git a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 index b4006391b2..fce0cb891e 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; export default Ember.Controller.extend({ /** @@ -29,7 +30,7 @@ export default Ember.Controller.extend({ }); var self = this; - Discourse.ajax("/admin/email/test", { + ajax("/admin/email/test", { type: 'POST', data: { email_address: this.get('testEmailAddress') } }).then(function () { diff --git a/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 b/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 index 5277f2feb2..b111a5952b 100644 --- a/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-emojis.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; export default Ember.ArrayController.extend({ sortProperties: ["name"], @@ -15,7 +16,7 @@ export default Ember.ArrayController.extend({ I18n.t("yes_value"), function(destroy) { if (destroy) { - return Discourse.ajax("/admin/customize/emojis/" + emoji.get("name"), { type: "DELETE" }).then(function() { + return ajax("/admin/customize/emojis/" + emoji.get("name"), { type: "DELETE" }).then(function() { self.removeObject(emoji); }); } diff --git a/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 index 6628a6fa72..bf60519c5b 100644 --- a/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-groups-bulk.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import computed from 'ember-addons/ember-computed-decorators'; import { popupAjaxError } from 'discourse/lib/ajax-error'; @@ -20,7 +21,7 @@ export default Ember.Controller.extend({ .reject(x => x.length === 0); this.set('saving', true); - Discourse.ajax('/admin/groups/bulk', { + ajax('/admin/groups/bulk', { data: { users, group_id: this.get('groupId') }, method: 'PUT' }).then(() => { diff --git a/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 b/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 index eba51de4ee..9a5962cccf 100644 --- a/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-groups-type.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; export default Ember.ArrayController.extend({ sortProperties: ['name'], refreshingAutoGroups: false, @@ -9,7 +10,7 @@ export default Ember.ArrayController.extend({ refreshAutoGroups: function(){ var self = this; this.set('refreshingAutoGroups', true); - Discourse.ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(function() { + ajax('/admin/groups/refresh_automatic_groups', {type: 'POST'}).then(function() { self.transitionToRoute("adminGroupsType", "automatic").then(function() { self.set('refreshingAutoGroups', false); }); diff --git a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 index a5e34fc257..0c1cb7abb3 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; import { propertyNotEqual, setting } from 'discourse/lib/computed'; @@ -38,7 +39,7 @@ export default Ember.Controller.extend(CanCheckEmails, { saveTitle() { const self = this; - return Discourse.ajax("/users/" + this.get('model.username').toLowerCase(), { + return ajax("/users/" + this.get('model.username').toLowerCase(), { data: {title: this.get('userTitleValue')}, type: 'PUT' }).catch(function(e) { @@ -68,7 +69,7 @@ export default Ember.Controller.extend(CanCheckEmails, { savePrimaryGroup() { const self = this; - return Discourse.ajax("/admin/users/" + this.get('model.id') + "/primary_group", { + return ajax("/admin/users/" + this.get('model.id') + "/primary_group", { type: 'PUT', data: {primary_group_id: this.get('model.primary_group_id')} }).then(function () { diff --git a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 index 6c0f6b9ccd..086080c173 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-edit-badge-groupings.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; export default Ember.Controller.extend({ needs: ['modal'], @@ -57,7 +58,7 @@ export default Ember.Controller.extend({ const groupIds = items.map(function(i){return i.get("id") || -1;}); const names = items.map(function(i){return i.get("name");}); - Discourse.ajax('/admin/badges/badge_groupings',{ + ajax('/admin/badges/badge_groupings',{ data: {ids: groupIds, names: names}, method: 'POST' }).then(function(data){ diff --git a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 index 866012b952..ac44a7677f 100644 --- a/app/assets/javascripts/admin/models/admin-dashboard.js.es6 +++ b/app/assets/javascripts/admin/models/admin-dashboard.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; const AdminDashboard = Discourse.Model.extend({}); @@ -11,7 +12,7 @@ AdminDashboard.reopenClass({ @return {jqXHR} a jQuery Promise object **/ find: function() { - return Discourse.ajax("/admin/dashboard.json").then(function(json) { + return ajax("/admin/dashboard.json").then(function(json) { var model = AdminDashboard.create(json); model.set('loaded', true); return model; @@ -26,7 +27,7 @@ AdminDashboard.reopenClass({ @return {jqXHR} a jQuery Promise object **/ fetchProblems: function() { - return Discourse.ajax("/admin/dashboard/problems.json", { + return ajax("/admin/dashboard/problems.json", { type: 'GET', dataType: 'json' }).then(function(json) { diff --git a/app/assets/javascripts/admin/models/admin-user.js.es6 b/app/assets/javascripts/admin/models/admin-user.js.es6 index c97f5ea910..d804894770 100644 --- a/app/assets/javascripts/admin/models/admin-user.js.es6 +++ b/app/assets/javascripts/admin/models/admin-user.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import computed from 'ember-addons/ember-computed-decorators'; import { propertyNotEqual } from 'discourse/lib/computed'; import { popupAjaxError } from 'discourse/lib/ajax-error'; @@ -40,7 +41,7 @@ const AdminUser = Discourse.User.extend({ canResetBounceScore: Ember.computed.gt("bounce_score", 0), resetBounceScore() { - return Discourse.ajax(`/admin/users/${this.get("id")}/reset_bounce_score`, { + return ajax(`/admin/users/${this.get("id")}/reset_bounce_score`, { type: 'POST' }).then(() => this.setProperties({ "bounce_score": 0, @@ -50,7 +51,7 @@ const AdminUser = Discourse.User.extend({ generateApiKey() { const self = this; - return Discourse.ajax("/admin/users/" + this.get('id') + "/generate_api_key", { + return ajax("/admin/users/" + this.get('id') + "/generate_api_key", { type: 'POST' }).then(function (result) { const apiKey = ApiKey.create(result.api_key); @@ -60,20 +61,20 @@ const AdminUser = Discourse.User.extend({ }, groupAdded(added) { - return Discourse.ajax("/admin/users/" + this.get('id') + "/groups", { + return ajax("/admin/users/" + this.get('id') + "/groups", { type: 'POST', data: { group_id: added.id } }).then(() => this.get('groups').pushObject(added)); }, groupRemoved(groupId) { - return Discourse.ajax("/admin/users/" + this.get('id') + "/groups/" + groupId, { + return ajax("/admin/users/" + this.get('id') + "/groups/" + groupId, { type: 'DELETE' }).then(() => this.set('groups.[]', this.get('groups').rejectBy("id", groupId))); }, revokeApiKey() { - return Discourse.ajax("/admin/users/" + this.get('id') + "/revoke_api_key", { + return ajax("/admin/users/" + this.get('id') + "/revoke_api_key", { type: 'DELETE' }).then(() => this.set('api_key', null)); }, @@ -104,7 +105,7 @@ const AdminUser = Discourse.User.extend({ "label": ' ' + I18n.t("admin.user.delete_all_posts"), "class": "btn btn-danger", "callback": function() { - Discourse.ajax("/admin/users/" + user.get('id') + "/delete_all_posts", { + ajax("/admin/users/" + user.get('id') + "/delete_all_posts", { type: 'PUT' }).then(() => user.set('post_count', 0)); } @@ -114,7 +115,7 @@ const AdminUser = Discourse.User.extend({ revokeAdmin() { const self = this; - return Discourse.ajax("/admin/users/" + this.get('id') + "/revoke_admin", { + return ajax("/admin/users/" + this.get('id') + "/revoke_admin", { type: 'PUT' }).then(function() { self.setProperties({ @@ -127,7 +128,7 @@ const AdminUser = Discourse.User.extend({ grantAdmin() { const self = this; - return Discourse.ajax("/admin/users/" + this.get('id') + "/grant_admin", { + return ajax("/admin/users/" + this.get('id') + "/grant_admin", { type: 'PUT' }).then(function() { self.setProperties({ @@ -140,7 +141,7 @@ const AdminUser = Discourse.User.extend({ revokeModeration() { const self = this; - return Discourse.ajax("/admin/users/" + this.get('id') + "/revoke_moderation", { + return ajax("/admin/users/" + this.get('id') + "/revoke_moderation", { type: 'PUT' }).then(function() { self.setProperties({ @@ -153,7 +154,7 @@ const AdminUser = Discourse.User.extend({ grantModeration() { const self = this; - return Discourse.ajax("/admin/users/" + this.get('id') + "/grant_moderation", { + return ajax("/admin/users/" + this.get('id') + "/grant_moderation", { type: 'PUT' }).then(function() { self.setProperties({ @@ -165,14 +166,14 @@ const AdminUser = Discourse.User.extend({ }, refreshBrowsers() { - return Discourse.ajax("/admin/users/" + this.get('id') + "/refresh_browsers", { + return ajax("/admin/users/" + this.get('id') + "/refresh_browsers", { type: 'POST' }).finally(() => bootbox.alert(I18n.t("admin.user.refresh_browsers_message"))); }, approve() { const self = this; - return Discourse.ajax("/admin/users/" + this.get('id') + "/approve", { + return ajax("/admin/users/" + this.get('id') + "/approve", { type: 'PUT' }).then(function() { self.setProperties({ @@ -190,7 +191,7 @@ const AdminUser = Discourse.User.extend({ dirty: propertyNotEqual('originalTrustLevel', 'trustLevel.id'), saveTrustLevel() { - return Discourse.ajax("/admin/users/" + this.id + "/trust_level", { + return ajax("/admin/users/" + this.id + "/trust_level", { type: 'PUT', data: { level: this.get('trustLevel.id') } }).then(function() { @@ -210,7 +211,7 @@ const AdminUser = Discourse.User.extend({ }, lockTrustLevel(locked) { - return Discourse.ajax("/admin/users/" + this.id + "/trust_level_lock", { + return ajax("/admin/users/" + this.id + "/trust_level_lock", { type: 'PUT', data: { locked: !!locked } }).then(function() { @@ -239,14 +240,14 @@ const AdminUser = Discourse.User.extend({ }.property('suspended_till', 'suspended_at'), suspend(duration, reason) { - return Discourse.ajax("/admin/users/" + this.id + "/suspend", { + return ajax("/admin/users/" + this.id + "/suspend", { type: 'PUT', data: { duration: duration, reason: reason } }); }, unsuspend() { - return Discourse.ajax("/admin/users/" + this.id + "/unsuspend", { + return ajax("/admin/users/" + this.id + "/unsuspend", { type: 'PUT' }).then(function() { window.location.reload(); @@ -257,7 +258,7 @@ const AdminUser = Discourse.User.extend({ }, log_out() { - return Discourse.ajax("/admin/users/" + this.id + "/log_out", { + return ajax("/admin/users/" + this.id + "/log_out", { type: 'POST', data: { username_or_email: this.get('username') } }).then(function() { @@ -266,7 +267,7 @@ const AdminUser = Discourse.User.extend({ }, impersonate() { - return Discourse.ajax("/admin/impersonate", { + return ajax("/admin/impersonate", { type: 'POST', data: { username_or_email: this.get('username') } }).then(function() { @@ -281,7 +282,7 @@ const AdminUser = Discourse.User.extend({ }, activate() { - return Discourse.ajax('/admin/users/' + this.id + '/activate', { + return ajax('/admin/users/' + this.id + '/activate', { type: 'PUT' }).then(function() { window.location.reload(); @@ -292,7 +293,7 @@ const AdminUser = Discourse.User.extend({ }, deactivate() { - return Discourse.ajax('/admin/users/' + this.id + '/deactivate', { + return ajax('/admin/users/' + this.id + '/deactivate', { type: 'PUT' }).then(function() { window.location.reload(); @@ -304,7 +305,7 @@ const AdminUser = Discourse.User.extend({ unblock() { this.set('blockingUser', true); - return Discourse.ajax('/admin/users/' + this.id + '/unblock', { + return ajax('/admin/users/' + this.id + '/unblock', { type: 'PUT' }).then(function() { window.location.reload(); @@ -320,7 +321,7 @@ const AdminUser = Discourse.User.extend({ const performBlock = function() { user.set('blockingUser', true); - return Discourse.ajax('/admin/users/' + user.id + '/block', { + return ajax('/admin/users/' + user.id + '/block', { type: 'PUT' }).then(function() { window.location.reload(); @@ -345,7 +346,7 @@ const AdminUser = Discourse.User.extend({ }, sendActivationEmail() { - return Discourse.ajax('/users/action/send_activation_email', { + return ajax('/users/action/send_activation_email', { type: 'POST', data: { username: this.get('username') } }).then(function() { @@ -360,7 +361,7 @@ const AdminUser = Discourse.User.extend({ message = I18n.t("admin.user.anonymize_confirm"); const performAnonymize = function() { - return Discourse.ajax("/admin/users/" + user.get('id') + '/anonymize.json', { + return ajax("/admin/users/" + user.get('id') + '/anonymize.json', { type: 'PUT' }).then(function(data) { if (data.success) { @@ -422,7 +423,7 @@ const AdminUser = Discourse.User.extend({ if (opts && opts.deletePosts) { formData["delete_posts"] = true; } - return Discourse.ajax("/admin/users/" + user.get('id') + '.json', { + return ajax("/admin/users/" + user.get('id') + '.json', { type: 'DELETE', data: formData }).then(function(data) { @@ -481,7 +482,7 @@ const AdminUser = Discourse.User.extend({ "label": ' ' + I18n.t("flagging.yes_delete_spammer"), "class": "btn btn-danger", "callback": function() { - return Discourse.ajax("/admin/users/" + user.get('id') + '.json', { + return ajax("/admin/users/" + user.get('id') + '.json', { type: 'DELETE', data: { delete_posts: true, @@ -549,7 +550,7 @@ AdminUser.reopenClass({ }); }); - return Discourse.ajax("/admin/users/approve-bulk", { + return ajax("/admin/users/approve-bulk", { type: 'PUT', data: { users: users.map((u) => u.id) } }).finally(() => bootbox.alert(I18n.t("admin.user.approve_bulk_success"))); @@ -561,7 +562,7 @@ AdminUser.reopenClass({ user.set('selected', false); }); - return Discourse.ajax("/admin/users/reject-bulk", { + return ajax("/admin/users/reject-bulk", { type: 'DELETE', data: { users: users.map((u) => u.id), @@ -571,14 +572,14 @@ AdminUser.reopenClass({ }, find(user_id) { - return Discourse.ajax("/admin/users/" + user_id + ".json").then(result => { + return ajax("/admin/users/" + user_id + ".json").then(result => { result.loadedDetails = true; return AdminUser.create(result); }); }, findAll(query, filter) { - return Discourse.ajax("/admin/users/list/" + query + ".json", { + return ajax("/admin/users/list/" + query + ".json", { data: filter }).then(function(users) { return users.map((u) => AdminUser.create(u)); diff --git a/app/assets/javascripts/admin/models/api-key.js.es6 b/app/assets/javascripts/admin/models/api-key.js.es6 index 7ef7719543..aa05ce8341 100644 --- a/app/assets/javascripts/admin/models/api-key.js.es6 +++ b/app/assets/javascripts/admin/models/api-key.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; const ApiKey = Discourse.Model.extend({ /** @@ -8,7 +9,7 @@ const ApiKey = Discourse.Model.extend({ **/ regenerate: function() { var self = this; - return Discourse.ajax('/admin/api/key', {type: 'PUT', data: {id: this.get('id')}}).then(function (result) { + return ajax('/admin/api/key', {type: 'PUT', data: {id: this.get('id')}}).then(function (result) { self.set('key', result.api_key.key); return self; }); @@ -21,7 +22,7 @@ const ApiKey = Discourse.Model.extend({ @returns {Promise} a promise that resolves when the key has been revoked **/ revoke: function() { - return Discourse.ajax('/admin/api/key', {type: 'DELETE', data: {id: this.get('id')}}); + return ajax('/admin/api/key', {type: 'DELETE', data: {id: this.get('id')}}); } }); @@ -51,7 +52,7 @@ ApiKey.reopenClass({ @returns {Promise} a promise that resolves to the array of `ApiKey` instances **/ find: function() { - return Discourse.ajax("/admin/api").then(function(keys) { + return ajax("/admin/api").then(function(keys) { return keys.map(function (key) { return ApiKey.create(key); }); @@ -65,7 +66,7 @@ ApiKey.reopenClass({ @returns {Promise} a promise that resolves to a master `ApiKey` **/ generateMasterKey: function() { - return Discourse.ajax("/admin/api/key", {type: 'POST'}).then(function (result) { + return ajax("/admin/api/key", {type: 'POST'}).then(function (result) { return ApiKey.create(result.api_key); }); } diff --git a/app/assets/javascripts/admin/models/backup.js.es6 b/app/assets/javascripts/admin/models/backup.js.es6 index 8b4991b728..c38f74ec8d 100644 --- a/app/assets/javascripts/admin/models/backup.js.es6 +++ b/app/assets/javascripts/admin/models/backup.js.es6 @@ -1,11 +1,12 @@ +import { ajax } from 'discourse/lib/ajax'; const Backup = Discourse.Model.extend({ destroy() { - return Discourse.ajax("/admin/backups/" + this.get("filename"), { type: "DELETE" }); + return ajax("/admin/backups/" + this.get("filename"), { type: "DELETE" }); }, restore() { - return Discourse.ajax("/admin/backups/" + this.get("filename") + "/restore", { + return ajax("/admin/backups/" + this.get("filename") + "/restore", { type: "POST", data: { client_id: window.MessageBus.clientId } }); @@ -16,13 +17,13 @@ const Backup = Discourse.Model.extend({ Backup.reopenClass({ find() { - return PreloadStore.getAndRemove("backups", () => Discourse.ajax("/admin/backups.json")) + return PreloadStore.getAndRemove("backups", () => ajax("/admin/backups.json")) .then(backups => backups.map(backup => Backup.create(backup))); }, start(withUploads) { if (withUploads === undefined) { withUploads = true; } - return Discourse.ajax("/admin/backups", { + return ajax("/admin/backups", { type: "POST", data: { with_uploads: withUploads, @@ -34,14 +35,14 @@ Backup.reopenClass({ }, cancel() { - return Discourse.ajax("/admin/backups/cancel.json") + return ajax("/admin/backups/cancel.json") .then(result => { if (!result.success) { bootbox.alert(result.message); } }); }, rollback() { - return Discourse.ajax("/admin/backups/rollback.json") + return ajax("/admin/backups/rollback.json") .then(result => { if (!result.success) { bootbox.alert(result.message); diff --git a/app/assets/javascripts/admin/models/color-scheme.js.es6 b/app/assets/javascripts/admin/models/color-scheme.js.es6 index 512672230d..743c779d6c 100644 --- a/app/assets/javascripts/admin/models/color-scheme.js.es6 +++ b/app/assets/javascripts/admin/models/color-scheme.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import ColorSchemeColor from 'admin/models/color-scheme-color'; const ColorScheme = Discourse.Model.extend(Ember.Copyable, { @@ -65,7 +66,7 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, { }); } - return Discourse.ajax("/admin/color_schemes" + (this.id ? '/' + this.id : '') + '.json', { + return ajax("/admin/color_schemes" + (this.id ? '/' + this.id : '') + '.json', { data: JSON.stringify({"color_scheme": data}), type: this.id ? 'PUT' : 'POST', dataType: 'json', @@ -88,7 +89,7 @@ const ColorScheme = Discourse.Model.extend(Ember.Copyable, { destroy: function() { if (this.id) { - return Discourse.ajax("/admin/color_schemes/" + this.id, { type: 'DELETE' }); + return ajax("/admin/color_schemes/" + this.id, { type: 'DELETE' }); } } @@ -106,7 +107,7 @@ var ColorSchemes = Ember.ArrayProxy.extend({ ColorScheme.reopenClass({ findAll: function() { var colorSchemes = ColorSchemes.create({ content: [], loading: true }); - Discourse.ajax('/admin/color_schemes').then(function(all) { + ajax('/admin/color_schemes').then(function(all) { _.each(all, function(colorScheme){ colorSchemes.pushObject(ColorScheme.create({ id: colorScheme.id, diff --git a/app/assets/javascripts/admin/models/email-log.js.es6 b/app/assets/javascripts/admin/models/email-log.js.es6 index 2b19eeff4f..dd6948bd41 100644 --- a/app/assets/javascripts/admin/models/email-log.js.es6 +++ b/app/assets/javascripts/admin/models/email-log.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import AdminUser from 'admin/models/admin-user'; const EmailLog = Discourse.Model.extend({}); @@ -21,7 +22,7 @@ EmailLog.reopenClass({ const status = filter.status || "sent"; filter = _.omit(filter, "status"); - return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter }) + return ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter }) .then(logs => _.map(logs, log => EmailLog.create(log))); } }); diff --git a/app/assets/javascripts/admin/models/email-preview.js.es6 b/app/assets/javascripts/admin/models/email-preview.js.es6 index 12826f98ed..f992bf250d 100644 --- a/app/assets/javascripts/admin/models/email-preview.js.es6 +++ b/app/assets/javascripts/admin/models/email-preview.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; const EmailPreview = Discourse.Model.extend({}); EmailPreview.reopenClass({ @@ -11,7 +12,7 @@ EmailPreview.reopenClass({ username = Discourse.User.current().username; } - return Discourse.ajax("/admin/email/preview-digest.json", { + return ajax("/admin/email/preview-digest.json", { data: { last_seen_at: lastSeenAt, username: username } }).then(function (result) { return EmailPreview.create(result); diff --git a/app/assets/javascripts/admin/models/email-settings.js.es6 b/app/assets/javascripts/admin/models/email-settings.js.es6 index 1b8f791f26..ed5f000d51 100644 --- a/app/assets/javascripts/admin/models/email-settings.js.es6 +++ b/app/assets/javascripts/admin/models/email-settings.js.es6 @@ -1,8 +1,9 @@ +import { ajax } from 'discourse/lib/ajax'; const EmailSettings = Discourse.Model.extend({}); EmailSettings.reopenClass({ find: function() { - return Discourse.ajax("/admin/email.json").then(function (settings) { + return ajax("/admin/email.json").then(function (settings) { return EmailSettings.create(settings); }); } diff --git a/app/assets/javascripts/admin/models/email-template.js.es6 b/app/assets/javascripts/admin/models/email-template.js.es6 index 7e8e6579ac..81d15c06c9 100644 --- a/app/assets/javascripts/admin/models/email-template.js.es6 +++ b/app/assets/javascripts/admin/models/email-template.js.es6 @@ -1,9 +1,10 @@ +import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; const { getProperties } = Ember; export default RestModel.extend({ revert() { - return Discourse.ajax(`/admin/customize/email_templates/${this.get('id')}`, { + return ajax(`/admin/customize/email_templates/${this.get('id')}`, { method: 'DELETE' }).then(result => getProperties(result.email_template, 'subject', 'body', 'can_revert')); } diff --git a/app/assets/javascripts/admin/models/flagged-post.js.es6 b/app/assets/javascripts/admin/models/flagged-post.js.es6 index 8492c4f8b7..c7a654608f 100644 --- a/app/assets/javascripts/admin/models/flagged-post.js.es6 +++ b/app/assets/javascripts/admin/models/flagged-post.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import AdminUser from 'admin/models/admin-user'; import Topic from 'discourse/models/topic'; import Post from 'discourse/models/post'; @@ -106,22 +107,22 @@ const FlaggedPost = Post.extend({ deletePost: function() { if (this.get('post_number') === 1) { - return Discourse.ajax('/t/' + this.topic_id, { type: 'DELETE', cache: false }); + return ajax('/t/' + this.topic_id, { type: 'DELETE', cache: false }); } else { - return Discourse.ajax('/posts/' + this.id, { type: 'DELETE', cache: false }); + return ajax('/posts/' + this.id, { type: 'DELETE', cache: false }); } }, disagreeFlags: function () { - return Discourse.ajax('/admin/flags/disagree/' + this.id, { type: 'POST', cache: false }); + return ajax('/admin/flags/disagree/' + this.id, { type: 'POST', cache: false }); }, deferFlags: function (deletePost) { - return Discourse.ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } }); + return ajax('/admin/flags/defer/' + this.id, { type: 'POST', cache: false, data: { delete_post: deletePost } }); }, agreeFlags: function (actionOnPost) { - return Discourse.ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } }); + return ajax('/admin/flags/agree/' + this.id, { type: 'POST', cache: false, data: { action_on_post: actionOnPost } }); }, postHidden: Em.computed.alias('hidden'), @@ -144,7 +145,7 @@ FlaggedPost.reopenClass({ var result = Em.A(); result.set('loading', true); - return Discourse.ajax('/admin/flags/' + filter + '.json?offset=' + offset).then(function (data) { + return ajax('/admin/flags/' + filter + '.json?offset=' + offset).then(function (data) { // users var userLookup = {}; _.each(data.users, function (user) { diff --git a/app/assets/javascripts/admin/models/incoming-email.js.es6 b/app/assets/javascripts/admin/models/incoming-email.js.es6 index 82534c5c43..d0386b2bc4 100644 --- a/app/assets/javascripts/admin/models/incoming-email.js.es6 +++ b/app/assets/javascripts/admin/models/incoming-email.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import AdminUser from 'admin/models/admin-user'; const IncomingEmail = Discourse.Model.extend({}); @@ -15,7 +16,7 @@ IncomingEmail.reopenClass({ }, find(id) { - return Discourse.ajax(`/admin/email/incoming/${id}.json`); + return ajax(`/admin/email/incoming/${id}.json`); }, findAll(filter, offset) { @@ -25,12 +26,12 @@ IncomingEmail.reopenClass({ const status = filter.status || "received"; filter = _.omit(filter, "status"); - return Discourse.ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter }) + return ajax(`/admin/email/${status}.json?offset=${offset}`, { data: filter }) .then(incomings => _.map(incomings, incoming => IncomingEmail.create(incoming))); }, loadRawEmail(id) { - return Discourse.ajax(`/admin/email/incoming/${id}/raw.json`); + return ajax(`/admin/email/incoming/${id}/raw.json`); } }); diff --git a/app/assets/javascripts/admin/models/permalink.js.es6 b/app/assets/javascripts/admin/models/permalink.js.es6 index eb867adb31..966e3f10e7 100644 --- a/app/assets/javascripts/admin/models/permalink.js.es6 +++ b/app/assets/javascripts/admin/models/permalink.js.es6 @@ -1,19 +1,20 @@ +import { ajax } from 'discourse/lib/ajax'; const Permalink = Discourse.Model.extend({ save: function() { - return Discourse.ajax("/admin/permalinks.json", { + return ajax("/admin/permalinks.json", { type: 'POST', data: {url: this.get('url'), permalink_type: this.get('permalink_type'), permalink_type_value: this.get('permalink_type_value')} }); }, destroy: function() { - return Discourse.ajax("/admin/permalinks/" + this.get('id') + ".json", {type: 'DELETE'}); + return ajax("/admin/permalinks/" + this.get('id') + ".json", {type: 'DELETE'}); } }); Permalink.reopenClass({ findAll: function(filter) { - return Discourse.ajax("/admin/permalinks.json", { data: { filter: filter } }).then(function(permalinks) { + return ajax("/admin/permalinks.json", { data: { filter: filter } }).then(function(permalinks) { return permalinks.map(p => Permalink.create(p)); }); } diff --git a/app/assets/javascripts/admin/models/report.js.es6 b/app/assets/javascripts/admin/models/report.js.es6 index 1891984b47..3342323b46 100644 --- a/app/assets/javascripts/admin/models/report.js.es6 +++ b/app/assets/javascripts/admin/models/report.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import round from "discourse/lib/round"; import { fmt } from 'discourse/lib/computed'; @@ -132,7 +133,7 @@ const Report = Discourse.Model.extend({ Report.reopenClass({ find(type, startDate, endDate, categoryId, groupId) { - return Discourse.ajax("/admin/reports/" + type, { + return ajax("/admin/reports/" + type, { data: { start_date: startDate, end_date: endDate, diff --git a/app/assets/javascripts/admin/models/screened-email.js.es6 b/app/assets/javascripts/admin/models/screened-email.js.es6 index 71c74d0ad0..a07b9a14b5 100644 --- a/app/assets/javascripts/admin/models/screened-email.js.es6 +++ b/app/assets/javascripts/admin/models/screened-email.js.es6 @@ -1,16 +1,17 @@ +import { ajax } from 'discourse/lib/ajax'; const ScreenedEmail = Discourse.Model.extend({ actionName: function() { return I18n.t("admin.logs.screened_actions." + this.get('action')); }.property('action'), clearBlock: function() { - return Discourse.ajax('/admin/logs/screened_emails/' + this.get('id'), {method: 'DELETE'}); + return ajax('/admin/logs/screened_emails/' + this.get('id'), {method: 'DELETE'}); } }); ScreenedEmail.reopenClass({ findAll: function() { - return Discourse.ajax("/admin/logs/screened_emails.json").then(function(screened_emails) { + return ajax("/admin/logs/screened_emails.json").then(function(screened_emails) { return screened_emails.map(function(b) { return ScreenedEmail.create(b); }); diff --git a/app/assets/javascripts/admin/models/screened-ip-address.js.es6 b/app/assets/javascripts/admin/models/screened-ip-address.js.es6 index 635fddf026..4bde08707b 100644 --- a/app/assets/javascripts/admin/models/screened-ip-address.js.es6 +++ b/app/assets/javascripts/admin/models/screened-ip-address.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import computed from 'ember-addons/ember-computed-decorators'; const ScreenedIpAddress = Discourse.Model.extend({ @@ -14,25 +15,25 @@ const ScreenedIpAddress = Discourse.Model.extend({ }, save() { - return Discourse.ajax("/admin/logs/screened_ip_addresses" + (this.id ? '/' + this.id : '') + ".json", { + return ajax("/admin/logs/screened_ip_addresses" + (this.id ? '/' + this.id : '') + ".json", { type: this.id ? 'PUT' : 'POST', data: {ip_address: this.get('ip_address'), action_name: this.get('action_name')} }); }, destroy() { - return Discourse.ajax("/admin/logs/screened_ip_addresses/" + this.get('id') + ".json", {type: 'DELETE'}); + return ajax("/admin/logs/screened_ip_addresses/" + this.get('id') + ".json", {type: 'DELETE'}); } }); ScreenedIpAddress.reopenClass({ findAll(filter) { - return Discourse.ajax("/admin/logs/screened_ip_addresses.json", { data: { filter: filter } }) + return ajax("/admin/logs/screened_ip_addresses.json", { data: { filter: filter } }) .then(screened_ips => screened_ips.map(b => ScreenedIpAddress.create(b))); }, rollUp() { - return Discourse.ajax("/admin/logs/screened_ip_addresses/roll_up", { type: "POST" }); + return ajax("/admin/logs/screened_ip_addresses/roll_up", { type: "POST" }); } }); diff --git a/app/assets/javascripts/admin/models/screened-url.js.es6 b/app/assets/javascripts/admin/models/screened-url.js.es6 index 9b16c7faec..282bc2fde4 100644 --- a/app/assets/javascripts/admin/models/screened-url.js.es6 +++ b/app/assets/javascripts/admin/models/screened-url.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; const ScreenedUrl = Discourse.Model.extend({ actionName: function() { return I18n.t("admin.logs.screened_actions." + this.get('action')); @@ -6,7 +7,7 @@ const ScreenedUrl = Discourse.Model.extend({ ScreenedUrl.reopenClass({ findAll: function() { - return Discourse.ajax("/admin/logs/screened_urls.json").then(function(screened_urls) { + return ajax("/admin/logs/screened_urls.json").then(function(screened_urls) { return screened_urls.map(function(b) { return ScreenedUrl.create(b); }); diff --git a/app/assets/javascripts/admin/models/site-setting.js.es6 b/app/assets/javascripts/admin/models/site-setting.js.es6 index 98b3ab896e..5103762663 100644 --- a/app/assets/javascripts/admin/models/site-setting.js.es6 +++ b/app/assets/javascripts/admin/models/site-setting.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; const SiteSetting = Discourse.Model.extend({ overridden: function() { let val = this.get('value'), @@ -28,7 +29,7 @@ const SiteSetting = Discourse.Model.extend({ SiteSetting.reopenClass({ findAll() { - return Discourse.ajax("/admin/site_settings").then(function (settings) { + return ajax("/admin/site_settings").then(function (settings) { // Group the results by category const categories = {}; settings.site_settings.forEach(function(s) { @@ -47,7 +48,7 @@ SiteSetting.reopenClass({ update(key, value) { const data = {}; data[key] = value; - return Discourse.ajax("/admin/site_settings/" + key, { type: 'PUT', data }); + return ajax("/admin/site_settings/" + key, { type: 'PUT', data }); } }); diff --git a/app/assets/javascripts/admin/models/site-text.js.es6 b/app/assets/javascripts/admin/models/site-text.js.es6 index 0d18ad7ea8..8e187dede0 100644 --- a/app/assets/javascripts/admin/models/site-text.js.es6 +++ b/app/assets/javascripts/admin/models/site-text.js.es6 @@ -1,9 +1,10 @@ +import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; const { getProperties } = Ember; export default RestModel.extend({ revert() { - return Discourse.ajax(`/admin/customize/site_texts/${this.get('id')}`, { + return ajax(`/admin/customize/site_texts/${this.get('id')}`, { method: 'DELETE' }).then(result => getProperties(result.site_text, 'value', 'can_revert')); } diff --git a/app/assets/javascripts/admin/models/staff-action-log.js.es6 b/app/assets/javascripts/admin/models/staff-action-log.js.es6 index b6e764ce33..24ec4fbcc0 100644 --- a/app/assets/javascripts/admin/models/staff-action-log.js.es6 +++ b/app/assets/javascripts/admin/models/staff-action-log.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import AdminUser from 'admin/models/admin-user'; import { escapeExpression } from 'discourse/lib/utilities'; @@ -56,7 +57,7 @@ StaffActionLog.reopenClass({ }, findAll: function(filters) { - return Discourse.ajax("/admin/logs/staff_action_logs.json", { data: filters }).then(function(staff_actions) { + return ajax("/admin/logs/staff_action_logs.json", { data: filters }).then(function(staff_actions) { return staff_actions.map(function(s) { return StaffActionLog.create(s); }); diff --git a/app/assets/javascripts/admin/models/version-check.js.es6 b/app/assets/javascripts/admin/models/version-check.js.es6 index 4f9b2c4c66..fc8c1bf5e2 100644 --- a/app/assets/javascripts/admin/models/version-check.js.es6 +++ b/app/assets/javascripts/admin/models/version-check.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; const VersionCheck = Discourse.Model.extend({ noCheckPerformed: function() { @@ -33,7 +34,7 @@ const VersionCheck = Discourse.Model.extend({ VersionCheck.reopenClass({ find: function() { - return Discourse.ajax('/admin/version_check').then(function(json) { + return ajax('/admin/version_check').then(function(json) { return VersionCheck.create(json); }); } diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 index f97d86f931..1a944314ed 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import showModal from 'discourse/lib/show-modal'; import BackupStatus from 'admin/models/backup-status'; import Backup from 'admin/models/backup'; @@ -31,7 +32,7 @@ export default Discourse.Route.extend({ model() { return PreloadStore.getAndRemove("operations_status", function() { - return Discourse.ajax("/admin/backups/status.json"); + return ajax("/admin/backups/status.json"); }).then(status => { return BackupStatus.create({ isOperationRunning: status.is_operation_running, diff --git a/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 b/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 index c283ff737b..03d091d228 100644 --- a/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-badges-show.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import Badge from 'discourse/models/badge'; import showModal from 'discourse/lib/show-modal'; @@ -31,7 +32,7 @@ export default Ember.Route.extend({ preview(badge, explain) { badge.set('preview_loading', true); - Discourse.ajax('/admin/badges/preview.json', { + ajax('/admin/badges/preview.json', { method: 'post', data: { sql: badge.get('query'), diff --git a/app/assets/javascripts/admin/routes/admin-badges.js.es6 b/app/assets/javascripts/admin/routes/admin-badges.js.es6 index 5efa86491f..68da5edb6b 100644 --- a/app/assets/javascripts/admin/routes/admin-badges.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-badges.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import Badge from 'discourse/models/badge'; import BadgeGrouping from 'discourse/models/badge-grouping'; @@ -6,7 +7,7 @@ export default Discourse.Route.extend({ model: function() { var self = this; - return Discourse.ajax('/admin/badges.json').then(function(json) { + return ajax('/admin/badges.json').then(function(json) { self._json = json; return Badge.createFromJson(json); }); diff --git a/app/assets/javascripts/admin/routes/admin-emojis.js.es6 b/app/assets/javascripts/admin/routes/admin-emojis.js.es6 index a33ade0111..520f7a0d50 100644 --- a/app/assets/javascripts/admin/routes/admin-emojis.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-emojis.js.es6 @@ -1,6 +1,7 @@ +import { ajax } from 'discourse/lib/ajax'; export default Discourse.Route.extend({ model: function() { - return Discourse.ajax("/admin/customize/emojis.json").then(function(emojis) { + return ajax("/admin/customize/emojis.json").then(function(emojis) { return emojis.map(function (emoji) { return Ember.Object.create(emoji); }); }); } diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 926cc59fb2..fb983d9438 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -8,7 +8,7 @@ define('ember', ['exports'], function(__exports__) { var _pluginCallbacks = []; -window.Discourse = Ember.Application.extend(Discourse.Ajax, { +window.Discourse = Ember.Application.extend({ rootElement: '#main', _docTitle: document.title, __TAGS_INCLUDED__: true, @@ -179,6 +179,12 @@ window.Discourse = Ember.Application.extend(Discourse.Ajax, { }) }).create(); +Discourse.ajax = function() { + var ajax = require('discourse/lib/ajax').ajax; + Ember.warn("Discourse.ajax is deprecated. Import the module and use it instead"); + return ajax.apply(this, arguments); +}; + Discourse.Markdown = { whiteListTag: Ember.K, whiteListIframe: Ember.K diff --git a/app/assets/javascripts/discourse/adapters/post-reply-history.js.es6 b/app/assets/javascripts/discourse/adapters/post-reply-history.js.es6 index 335c22b6b5..549736f794 100644 --- a/app/assets/javascripts/discourse/adapters/post-reply-history.js.es6 +++ b/app/assets/javascripts/discourse/adapters/post-reply-history.js.es6 @@ -1,9 +1,10 @@ +import { ajax } from 'discourse/lib/ajax'; import RestAdapter from 'discourse/adapters/rest'; export default RestAdapter.extend({ find(store, type, findArgs) { const maxReplies = Discourse.SiteSettings.max_reply_history; - return Discourse.ajax(`/posts/${findArgs.postId}/reply-history?max_replies=${maxReplies}`).then(replies => { + return ajax(`/posts/${findArgs.postId}/reply-history?max_replies=${maxReplies}`).then(replies => { return { post_reply_histories: replies }; }); }, diff --git a/app/assets/javascripts/discourse/adapters/post-reply.js.es6 b/app/assets/javascripts/discourse/adapters/post-reply.js.es6 index f36299d002..99112ddff1 100644 --- a/app/assets/javascripts/discourse/adapters/post-reply.js.es6 +++ b/app/assets/javascripts/discourse/adapters/post-reply.js.es6 @@ -1,8 +1,9 @@ +import { ajax } from 'discourse/lib/ajax'; import RestAdapter from 'discourse/adapters/rest'; export default RestAdapter.extend({ find(store, type, findArgs) { - return Discourse.ajax(`/posts/${findArgs.postId}/replies`).then(replies => { + return ajax(`/posts/${findArgs.postId}/replies`).then(replies => { return { post_replies: replies }; }); }, diff --git a/app/assets/javascripts/discourse/adapters/post.js.es6 b/app/assets/javascripts/discourse/adapters/post.js.es6 index 1d0a375b58..3c7532d748 100644 --- a/app/assets/javascripts/discourse/adapters/post.js.es6 +++ b/app/assets/javascripts/discourse/adapters/post.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import RestAdapter from 'discourse/adapters/rest'; import { Result } from 'discourse/adapters/rest'; @@ -12,7 +13,7 @@ export default RestAdapter.extend({ createRecord(store, type, args) { const typeField = Ember.String.underscore(type); args.nested_post = true; - return Discourse.ajax(this.pathFor(store, type), { method: 'POST', data: args }).then(function (json) { + return ajax(this.pathFor(store, type), { method: 'POST', data: args }).then(function (json) { return new Result(json[typeField], json); }); } diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index 5faaf6d0bf..75366ca80e 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { hashString } from 'discourse/lib/hash'; const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host']; @@ -9,8 +10,6 @@ export function Result(payload, responseJson) { this.target = null; } -const ajax = Discourse.ajax; - // We use this to make sure 404s are caught function rethrow(error) { if (error.status === 404) { diff --git a/app/assets/javascripts/discourse/adapters/topic-list.js.es6 b/app/assets/javascripts/discourse/adapters/topic-list.js.es6 index bb7375597d..c0311c011d 100644 --- a/app/assets/javascripts/discourse/adapters/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/adapters/topic-list.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import RestAdapter from 'discourse/adapters/rest'; export function finderFor(filter, params) { @@ -19,7 +20,7 @@ export function finderFor(filter, params) { url += "?" + encoded.join('&'); } } - return Discourse.ajax(url); + return ajax(url); }; } diff --git a/app/assets/javascripts/discourse/adapters/topic.js.es6 b/app/assets/javascripts/discourse/adapters/topic.js.es6 index 290dfff4b9..c8a6120d2d 100644 --- a/app/assets/javascripts/discourse/adapters/topic.js.es6 +++ b/app/assets/javascripts/discourse/adapters/topic.js.es6 @@ -1,9 +1,10 @@ +import { ajax } from 'discourse/lib/ajax'; import RestAdapter from 'discourse/adapters/rest'; export default RestAdapter.extend({ find(store, type, findArgs) { if (findArgs.similar) { - return Discourse.ajax("/topics/similar_to", { data: findArgs.similar }); + return ajax("/topics/similar_to", { data: findArgs.similar }); } else { return this._super(store, type, findArgs); } diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 362165eda2..71c6608075 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -4,6 +4,7 @@ import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentio import { linkSeenCategoryHashtags, fetchUnseenCategoryHashtags } from 'discourse/lib/link-category-hashtags'; import { fetchUnseenTagHashtags, linkSeenTagHashtags } from 'discourse/lib/link-tag-hashtag'; import { load } from 'pretty-text/oneboxer'; +import { ajax } from 'discourse/lib/ajax'; import InputValidation from 'discourse/models/input-validation'; import { tinyAvatar, @@ -499,7 +500,7 @@ export default Ember.Component.extend({ } // Paint oneboxes - $('a.onebox', $preview).each((i, e) => load(e, refresh)); + $('a.onebox', $preview).each((i, e) => load(e, refresh, ajax)); this.trigger('previewRefreshed', $preview); this.sendAction('afterRefresh', $preview); }, diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index 985fa42dd0..9bed6142a4 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import debounce from 'discourse/lib/debounce'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { setting } from 'discourse/lib/computed'; @@ -336,7 +337,7 @@ export default Ember.Controller.extend(ModalFunctionality, { @on('init') fetchConfirmationValue() { - return Discourse.ajax('/users/hp.json').then(json => { + return ajax('/users/hp.json').then(json => { this.set('accountPasswordConfirm', json.value); this.set('accountChallenge', json.challenge.split("").reverse().join("")); }); diff --git a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 index 9cd0e00e75..f0f4301f34 100644 --- a/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-topic-auto-close.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { observes } from "ember-addons/ember-computed-decorators"; import ModalFunctionality from 'discourse/mixins/modal-functionality'; @@ -32,7 +33,7 @@ export default Ember.Controller.extend(ModalFunctionality, { setAutoClose(time) { const self = this; this.set('loading', true); - Discourse.ajax({ + ajax({ url: `/t/${this.get('model.id')}/autoclose`, type: 'PUT', dataType: 'json', diff --git a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 index f83e50c62c..bb195b636b 100644 --- a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { categoryLinkHTML } from 'discourse/helpers/category-link'; import computed from 'ember-addons/ember-computed-decorators'; @@ -91,7 +92,7 @@ export default Ember.Controller.extend(ModalFunctionality, { onShow() { this.set("loading", true); - return Discourse.ajax("/topics/feature_stats.json", { + return ajax("/topics/feature_stats.json", { data: { category_id: this.get("model.category.id") } }).then(result => { if (result) { diff --git a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 index 242bfc7319..fd98f7910f 100644 --- a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 +++ b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { escapeExpression } from 'discourse/lib/utilities'; @@ -49,7 +50,7 @@ export default Ember.Controller.extend(ModalFunctionality, { self.flash(e.responseJSON.errors[0], 'error'); }; - Discourse.ajax('/session/forgot_password', { + ajax('/session/forgot_password', { data: { login: this.get('accountEmailOrUsername').trim() }, type: 'POST' }).then(success, fail).finally(function(){ diff --git a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 index 65ff5da4c9..27ee3f0309 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { translateResults, searchContextDescription, getSearchKey, isValidSearchTerm } from "discourse/lib/search"; import showModal from 'discourse/lib/show-modal'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; @@ -157,7 +158,7 @@ export default Ember.Controller.extend({ const searchKey = getSearchKey(args); - Discourse.ajax("/search", { data: args }).then(results => { + ajax("/search", { data: args }).then(results => { const model = translateResults(results) || {}; router.transientCache('lastSearch', { searchKey, model }, 5); this.set("model", model); @@ -194,7 +195,7 @@ export default Ember.Controller.extend({ showSearchHelp() { // TODO: dupe code should be centralized - Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then((model) => { + ajax("/static/search_help.html", { dataType: 'html' }).then((model) => { showModal('searchHelp', { model }); }); }, diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index fd884e391c..4303de475a 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import showModal from 'discourse/lib/show-modal'; import { setting } from 'discourse/lib/computed'; @@ -55,7 +56,7 @@ export default Ember.Controller.extend(ModalFunctionality, { this.set('loggingIn', true); - Discourse.ajax("/session", { + ajax("/session", { data: { login: this.get('loginName'), password: this.get('loginPassword') }, type: 'POST' }).then(function (result) { diff --git a/app/assets/javascripts/discourse/controllers/not-activated.js.es6 b/app/assets/javascripts/discourse/controllers/not-activated.js.es6 index 2c2e2b0758..1c38cc58e5 100644 --- a/app/assets/javascripts/discourse/controllers/not-activated.js.es6 +++ b/app/assets/javascripts/discourse/controllers/not-activated.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; export default Ember.Controller.extend(ModalFunctionality, { @@ -9,7 +10,7 @@ export default Ember.Controller.extend(ModalFunctionality, { actions: { sendActivationEmail: function() { - Discourse.ajax('/users/action/send_activation_email', {data: {username: this.get('username')}, type: 'POST'}); + ajax('/users/action/send_activation_email', {data: {username: this.get('username')}, type: 'POST'}); this.set('emailSent', true); } } diff --git a/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 index a48dae7fc2..b71660d17f 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/badge-title.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import BadgeSelectController from "discourse/mixins/badge-select-controller"; export default Ember.ArrayController.extend(BadgeSelectController, { @@ -11,7 +12,7 @@ export default Ember.ArrayController.extend(BadgeSelectController, { this.setProperties({ saved: false, saving: true }); var self = this; - Discourse.ajax(this.get('user.path') + "/preferences/badge_title", { + ajax(this.get('user.path') + "/preferences/badge_title", { type: "PUT", data: { user_badge_id: self.get('selectedUserBadgeId') } }).then(function() { diff --git a/app/assets/javascripts/discourse/controllers/preferences/card-badge.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/card-badge.js.es6 index 4b0dcaacc5..d0c42fb558 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/card-badge.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/card-badge.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import BadgeSelectController from "discourse/mixins/badge-select-controller"; export default Ember.ArrayController.extend(BadgeSelectController, { @@ -12,7 +13,7 @@ export default Ember.ArrayController.extend(BadgeSelectController, { this.setProperties({ saved: false, saving: true }); var self = this; - Discourse.ajax(this.get('user.path') + "/preferences/card-badge", { + ajax(this.get('user.path') + "/preferences/card-badge", { type: "PUT", data: { user_badge_id: self.get('selectedUserBadgeId') } }).then(function() { diff --git a/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6 b/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6 index 62a313b9a9..18a000a388 100644 --- a/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6 +++ b/app/assets/javascripts/discourse/controllers/reorder-categories.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; const BufferedProxy = window.BufferedProxy; // import BufferedProxy from 'ember-buffered-proxy/proxy'; import { popupAjaxError } from 'discourse/lib/ajax-error'; @@ -90,7 +91,7 @@ export default Ember.Controller.extend(ModalFunctionality, Ember.Evented, { this.get('categoriesBuffered').forEach((cat) => { data[cat.get('id')] = cat.get('position'); }); - Discourse.ajax('/categories/reorder', + ajax('/categories/reorder', {type: 'POST', data: {mapping: JSON.stringify(data)}}). then(() => this.send("closeModal")). catch(popupAjaxError); diff --git a/app/assets/javascripts/discourse/controllers/static.js.es6 b/app/assets/javascripts/discourse/controllers/static.js.es6 index a1c4ab9ddd..c656b99dfd 100644 --- a/app/assets/javascripts/discourse/controllers/static.js.es6 +++ b/app/assets/javascripts/discourse/controllers/static.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend({ @@ -15,7 +16,7 @@ export default Ember.Controller.extend({ markFaqRead() { const currentUser = this.currentUser; if (currentUser) { - Discourse.ajax("/users/read-faq", { method: "POST" }).then(() => { + ajax("/users/read-faq", { method: "POST" }).then(() => { currentUser.set('read_faq', true); }); } diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 index 5039330181..2d308dffd9 100644 --- a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 @@ -1,3 +1,5 @@ +import { ajax } from 'discourse/lib/ajax'; +import { observes } from 'ember-addons/ember-computed-decorators'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; export default Ember.ArrayController.extend({ @@ -17,7 +19,7 @@ export default Ember.ArrayController.extend({ actions: { resetNew() { - Discourse.ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { + ajax('/notifications/mark-read', { method: 'PUT' }).then(() => { this.setEach('read', true); }); }, diff --git a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 index 34fed0c977..7f680580ea 100644 --- a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 +++ b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 @@ -1,5 +1,6 @@ import { cleanDOM } from 'discourse/routes/discourse'; import { startPageTracking, onPageChange } from 'discourse/lib/page-tracker'; +import { viewTrackingRequired } from 'discourse/lib/ajax'; export default { name: "page-tracking", @@ -11,9 +12,7 @@ export default { // Tell our AJAX system to track a page transition const router = container.lookup('router:main'); - router.on('willTransition', function() { - Discourse.viewTrackingRequired(); - }); + router.on('willTransition', viewTrackingRequired); router.on('didTransition', function() { Em.run.scheduleOnce('afterRender', Ember.Route, cleanDOM); diff --git a/app/assets/javascripts/discourse/lib/ajax.js.es6 b/app/assets/javascripts/discourse/lib/ajax.js.es6 new file mode 100644 index 0000000000..48f259fd4c --- /dev/null +++ b/app/assets/javascripts/discourse/lib/ajax.js.es6 @@ -0,0 +1,122 @@ +let _trackView = false; +let _transientHeader = null; + +export function setTransientHeader(key, value) { + _transientHeader = {key, value}; +} + +export function viewTrackingRequired() { + _trackView = true; +} + +/** + Our own $.ajax method. Makes sure the .then method executes in an Ember runloop + for performance reasons. Also automatically adjusts the URL to support installs + in subfolders. +**/ +export function ajax() { + let url, args; + let ajaxObj; + + if (arguments.length === 1) { + if (typeof arguments[0] === "string") { + url = arguments[0]; + args = {}; + } else { + args = arguments[0]; + url = args.url; + delete args.url; + } + } else if (arguments.length === 2) { + url = arguments[0]; + args = arguments[1]; + } + + function performAjax(resolve, reject) { + + args.headers = args.headers || {}; + + if (_transientHeader) { + args.headers[_transientHeader.key] = _transientHeader.value; + _transientHeader = null; + } + + if (_trackView && (!args.type || args.type === "GET")) { + _trackView = false; + // DON'T CHANGE: rack is prepending "HTTP_" in the header's name + args.headers['Discourse-Track-View'] = "true"; + } + + args.success = (data, textStatus, xhr) => { + if (xhr.getResponseHeader('Discourse-Readonly')) { + Ember.run(() => Discourse.Site.currentProp('isReadOnly', true)); + } + + if (args.returnXHR) { + data = { result: data, xhr: xhr }; + } + + Ember.run(null, resolve, data); + }; + + args.error = (xhr, textStatus, errorThrown) => { + // note: for bad CSRF we don't loop an extra request right away. + // this allows us to eliminate the possibility of having a loop. + if (xhr.status === 403 && xhr.responseText === "['BAD CSRF']") { + Discourse.Session.current().set('csrfToken', null); + } + + // If it's a parsererror, don't reject + if (xhr.status === 200) return args.success(xhr); + + // Fill in some extra info + xhr.jqTextStatus = textStatus; + xhr.requestedUrl = url; + + Ember.run(null, reject, { + jqXHR: xhr, + textStatus: textStatus, + errorThrown: errorThrown + }); + }; + + // We default to JSON on GET. If we don't, sometimes if the server doesn't return the proper header + // it will not be parsed as an object. + if (!args.type) args.type = 'GET'; + if (!args.dataType && args.type.toUpperCase() === 'GET') args.dataType = 'json'; + + if (args.dataType === "script") { + args.headers['Discourse-Script'] = true; + } + + if (args.type === 'GET' && args.cache !== true) { + args.cache = false; + } + + ajaxObj = $.ajax(Discourse.getURL(url), args); + }; + + let promise; + + // For cached pages we strip out CSRF tokens, need to round trip to server prior to sending the + // request (bypass for GET, not needed) + if(args.type && args.type.toUpperCase() !== 'GET' && !Discourse.Session.currentProp('csrfToken')){ + promise = new Ember.RSVP.Promise((resolve, reject) => { + ajaxObj = $.ajax(Discourse.getURL('/session/csrf'), {cache: false}) + .success(result => { + Discourse.Session.currentProp('csrfToken', result.csrf); + performAjax(resolve, reject); + }); + }); + } else { + promise = new Ember.RSVP.Promise(performAjax); + } + + promise.abort = () => { + if (ajaxObj) { + ajaxObj.abort(); + } + }; + + return promise; +} diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 5ea80bce89..f5f9119be5 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import DiscourseURL from 'discourse/lib/url'; import { wantsNewWindow } from 'discourse/lib/intercept-click'; import { selectedText } from 'discourse/lib/utilities'; @@ -64,7 +65,7 @@ export default { // if they want to open in a new tab, do an AJAX request if (wantsNewWindow(e)) { - Discourse.ajax("/clicks/track", { + ajax("/clicks/track", { data: { url: href, post_id: postId, @@ -105,7 +106,7 @@ export default { // If we're on the same site, use the router and track via AJAX if (DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) { - Discourse.ajax("/clicks/track", { + ajax("/clicks/track", { data: { url: href, post_id: postId, diff --git a/app/assets/javascripts/discourse/lib/export-csv.js.es6 b/app/assets/javascripts/discourse/lib/export-csv.js.es6 index c4b74a08fb..a88558b3d1 100644 --- a/app/assets/javascripts/discourse/lib/export-csv.js.es6 +++ b/app/assets/javascripts/discourse/lib/export-csv.js.es6 @@ -1,5 +1,6 @@ +import { ajax } from 'discourse/lib/ajax'; function exportEntityByType(type, entity, args) { - return Discourse.ajax("/export_csv/export_entity.json", { + return ajax("/export_csv/export_entity.json", { method: 'POST', data: {entity_type: type, entity, args} }); diff --git a/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 b/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 index 53b3a8c2ab..05a71cb34a 100644 --- a/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-category-hashtags.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { replaceSpan } from 'discourse/lib/category-hashtags'; const validCategoryHashtags = {}; @@ -41,7 +42,7 @@ export function linkSeenCategoryHashtags($elem) { }; export function fetchUnseenCategoryHashtags(categorySlugs) { - return Discourse.ajax("/category_hashtags/check", { data: { category_slugs: categorySlugs } }) + return ajax("/category_hashtags/check", { data: { category_slugs: categorySlugs } }) .then((response) => { response.valid.forEach((category) => { validCategoryHashtags[category.slug] = category.url; diff --git a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 index 476ae64fc4..149b151617 100644 --- a/app/assets/javascripts/discourse/lib/link-mentions.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-mentions.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; function replaceSpan($e, username, opts) { if (opts && opts.group) { var extra = "", extraClass = ""; @@ -52,7 +53,7 @@ export function linkSeenMentions($elem, siteSettings) { } export function fetchUnseenMentions($elem, usernames) { - return Discourse.ajax("/users/is_local_username", { data: { usernames } }).then(function(r) { + return ajax("/users/is_local_username", { data: { usernames } }).then(function(r) { found.push.apply(found, r.valid); foundGroups.push.apply(foundGroups, r.valid_groups); mentionableGroups.push.apply(mentionableGroups, r.mentionable_groups); diff --git a/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 b/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 index c37610ff03..e82de3eb0a 100644 --- a/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 +++ b/app/assets/javascripts/discourse/lib/link-tag-hashtag.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { replaceSpan } from 'discourse/lib/category-hashtags'; import { TAG_HASHTAG_POSTFIX } from 'discourse/lib/tag-hashtags'; @@ -42,7 +43,7 @@ export function linkSeenTagHashtags($elem) { }; export function fetchUnseenTagHashtags(tagValues) { - return Discourse.ajax("/tags/check", { data: { tag_values: tagValues } }) + return ajax("/tags/check", { data: { tag_values: tagValues } }) .then((response) => { response.valid.forEach((tag) => { validTagHashtags[tag.value] = tag.url; diff --git a/app/assets/javascripts/discourse/lib/load-script.js.es6 b/app/assets/javascripts/discourse/lib/load-script.js.es6 index b870bf1d7f..b562a11545 100644 --- a/app/assets/javascripts/discourse/lib/load-script.js.es6 +++ b/app/assets/javascripts/discourse/lib/load-script.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; const _loaded = {}; const _loading = {}; @@ -58,7 +59,7 @@ export default function loadScript(url, opts) { if (opts.scriptTag) { loadWithTag(cdnUrl, cb); } else { - Discourse.ajax({url: cdnUrl, dataType: "script", cache: true}).then(cb); + ajax({url: cdnUrl, dataType: "script", cache: true}).then(cb); } }); } diff --git a/app/assets/javascripts/discourse/lib/screen-track.js.es6 b/app/assets/javascripts/discourse/lib/screen-track.js.es6 index fa52eab6fc..af4c46a04c 100644 --- a/app/assets/javascripts/discourse/lib/screen-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/screen-track.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; // We use this class to track how long posts in a topic are on the screen. const PAUSE_UNLESS_SCROLLED = 1000 * 60 * 3; const MAX_TRACKING_TIME = 1000 * 60 * 6; @@ -107,7 +108,7 @@ export default class { if (!$.isEmptyObject(newTimings)) { if (this.currentUser) { - Discourse.ajax('/topics/timings', { + ajax('/topics/timings', { data: { timings: newTimings, topic_time: this._topicTime, diff --git a/app/assets/javascripts/discourse/lib/search.js.es6 b/app/assets/javascripts/discourse/lib/search.js.es6 index 2675f10311..b0929ca60a 100644 --- a/app/assets/javascripts/discourse/lib/search.js.es6 +++ b/app/assets/javascripts/discourse/lib/search.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; export function translateResults(results, opts) { @@ -82,7 +83,7 @@ function searchForTerm(term, opts) { }; } - var promise = Discourse.ajax('/search/query', { data: data }); + var promise = ajax('/search/query', { data: data }); promise.then(function(results){ return translateResults(results, opts); diff --git a/app/assets/javascripts/discourse/mixins/ajax.js b/app/assets/javascripts/discourse/mixins/ajax.js deleted file mode 100644 index 9c1d2d4dfe..0000000000 --- a/app/assets/javascripts/discourse/mixins/ajax.js +++ /dev/null @@ -1,138 +0,0 @@ -/** - This mixin provides an 'ajax' method that can be used to perform ajax requests that - respect Discourse paths and the run loop. -**/ -var _trackView = false; -var _transientHeader = null; - -Discourse.Ajax = Em.Mixin.create({ - - setTransientHeader: function(k, v) { - _transientHeader = {key: k, value: v}; - }, - - viewTrackingRequired: function() { - _trackView = true; - }, - - /** - Our own $.ajax method. Makes sure the .then method executes in an Ember runloop - for performance reasons. Also automatically adjusts the URL to support installs - in subfolders. - - @method ajax - **/ - ajax: function() { - var url, args; - var ajax; - - if (arguments.length === 1) { - if (typeof arguments[0] === "string") { - url = arguments[0]; - args = {}; - } else { - args = arguments[0]; - url = args.url; - delete args.url; - } - } else if (arguments.length === 2) { - url = arguments[0]; - args = arguments[1]; - } - - if (args.success || args.error) { - throw "Discourse.ajax should use promises"; - } - - var performAjax = function(resolve, reject) { - - args.headers = args.headers || {}; - - if (_transientHeader) { - args.headers[_transientHeader.key] = _transientHeader.value; - _transientHeader = null; - } - - if (_trackView && (!args.type || args.type === "GET")) { - _trackView = false; - // DON'T CHANGE: rack is prepending "HTTP_" in the header's name - args.headers['Discourse-Track-View'] = "true"; - } - - args.success = function(data, textStatus, xhr) { - if (xhr.getResponseHeader('Discourse-Readonly')) { - Ember.run(function() { - Discourse.Site.currentProp('isReadOnly', true); - }); - } - - if (args.returnXHR) { - data = { result: data, xhr: xhr }; - } - - Ember.run(null, resolve, data); - }; - - args.error = function(xhr, textStatus, errorThrown) { - // note: for bad CSRF we don't loop an extra request right away. - // this allows us to eliminate the possibility of having a loop. - if (xhr.status === 403 && xhr.responseText === "['BAD CSRF']") { - Discourse.Session.current().set('csrfToken', null); - } - - // If it's a parsererror, don't reject - if (xhr.status === 200) return args.success(xhr); - - // Fill in some extra info - xhr.jqTextStatus = textStatus; - xhr.requestedUrl = url; - - Ember.run(null, reject, { - jqXHR: xhr, - textStatus: textStatus, - errorThrown: errorThrown - }); - }; - - // We default to JSON on GET. If we don't, sometimes if the server doesn't return the proper header - // it will not be parsed as an object. - if (!args.type) args.type = 'GET'; - if (!args.dataType && args.type.toUpperCase() === 'GET') args.dataType = 'json'; - - if (args.dataType === "script") { - args.headers['Discourse-Script'] = true; - } - - if (args.type === 'GET' && args.cache !== true) { - args.cache = false; - } - - ajax = $.ajax(Discourse.getURL(url), args); - }; - - var promise; - - // For cached pages we strip out CSRF tokens, need to round trip to server prior to sending the - // request (bypass for GET, not needed) - if(args.type && args.type.toUpperCase() !== 'GET' && !Discourse.Session.currentProp('csrfToken')){ - promise = new Ember.RSVP.Promise(function(resolve, reject){ - ajax = $.ajax(Discourse.getURL('/session/csrf'), {cache: false}) - .success(function(result){ - Discourse.Session.currentProp('csrfToken', result.csrf); - performAjax(resolve, reject); - }); - }); - } else { - promise = new Ember.RSVP.Promise(performAjax); - } - - promise.abort = function(){ - if (ajax) { - ajax.abort(); - } - }; - - return promise; - } - -}); diff --git a/app/assets/javascripts/discourse/models/action-summary.js.es6 b/app/assets/javascripts/discourse/models/action-summary.js.es6 index a4f50f52de..e21a13e0e9 100644 --- a/app/assets/javascripts/discourse/models/action-summary.js.es6 +++ b/app/assets/javascripts/discourse/models/action-summary.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; import { popupAjaxError } from 'discourse/lib/ajax-error'; @@ -53,7 +54,7 @@ export default RestModel.extend({ // Create our post action const self = this; - return Discourse.ajax("/post_actions", { + return ajax("/post_actions", { type: 'POST', data: { id: this.get('flagTopic') ? this.get('flagTopic.id') : post.get('id'), @@ -82,7 +83,7 @@ export default RestModel.extend({ this.removeAction(post); // Remove our post action - return Discourse.ajax("/post_actions/" + post.get('id'), { + return ajax("/post_actions/" + post.get('id'), { type: 'DELETE', data: { post_action_type_id: this.get('id') } }).then(result => { @@ -92,7 +93,7 @@ export default RestModel.extend({ }, deferFlags(post) { - return Discourse.ajax("/post_actions/defer_flags", { + return ajax("/post_actions/defer_flags", { type: "POST", data: { post_action_type_id: this.get("id"), id: post.get('id') } }).then(() => this.set('count', 0)); diff --git a/app/assets/javascripts/discourse/models/badge.js.es6 b/app/assets/javascripts/discourse/models/badge.js.es6 index 00cf97f341..333b40c57f 100644 --- a/app/assets/javascripts/discourse/models/badge.js.es6 +++ b/app/assets/javascripts/discourse/models/badge.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import BadgeGrouping from 'discourse/models/badge-grouping'; import RestModel from 'discourse/models/rest'; @@ -53,7 +54,7 @@ const Badge = RestModel.extend({ requestType = "PUT"; } - return Discourse.ajax(url, { + return ajax(url, { type: requestType, data: data }).then(function(json) { @@ -72,7 +73,7 @@ const Badge = RestModel.extend({ **/ destroy: function() { if (this.get('newBadge')) return Ember.RSVP.resolve(); - return Discourse.ajax("/admin/badges/" + this.get('id'), { + return ajax("/admin/badges/" + this.get('id'), { type: "DELETE" }); } @@ -134,7 +135,7 @@ Badge.reopenClass({ if(opts && opts.onlyListable){ listable = "?only_listable=true"; } - return Discourse.ajax('/badges.json' + listable).then(function(badgesJson) { + return ajax('/badges.json' + listable).then(function(badgesJson) { return Badge.createFromJson(badgesJson); }); }, @@ -147,7 +148,7 @@ Badge.reopenClass({ @returns {Promise} a promise that resolves to a `Badge` **/ findById: function(id) { - return Discourse.ajax("/badges/" + id).then(function(badgeJson) { + return ajax("/badges/" + id).then(function(badgeJson) { return Badge.createFromJson(badgeJson); }); } diff --git a/app/assets/javascripts/discourse/models/category-list.js.es6 b/app/assets/javascripts/discourse/models/category-list.js.es6 index 3a0b9e47f1..b0d6c2c45a 100644 --- a/app/assets/javascripts/discourse/models/category-list.js.es6 +++ b/app/assets/javascripts/discourse/models/category-list.js.es6 @@ -1,3 +1,5 @@ +import { ajax } from 'discourse/lib/ajax'; + const CategoryList = Ember.ArrayProxy.extend({ init() { this.set('content', []); @@ -34,7 +36,7 @@ CategoryList.reopenClass({ }, listForParent(store, category) { - return Discourse.ajax(`/categories.json?parent_category_id=${category.get("id")}`).then(result => { + return ajax(`/categories.json?parent_category_id=${category.get("id")}`).then(result => { return CategoryList.create({ categories: this.categoriesFrom(store, result), parentCategory: category @@ -43,7 +45,7 @@ CategoryList.reopenClass({ }, list(store) { - const getCategories = () => Discourse.ajax("/categories.json"); + const getCategories = () => ajax("/categories.json"); return PreloadStore.getAndRemove("categories_list", getCategories).then(result => { return CategoryList.create({ categories: this.categoriesFrom(store, result), diff --git a/app/assets/javascripts/discourse/models/category.js.es6 b/app/assets/javascripts/discourse/models/category.js.es6 index 8d1eb8ba87..000aeaeccb 100644 --- a/app/assets/javascripts/discourse/models/category.js.es6 +++ b/app/assets/javascripts/discourse/models/category.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; import { on } from 'ember-addons/ember-computed-decorators'; import PermissionType from 'discourse/models/permission-type'; @@ -67,7 +68,7 @@ const Category = RestModel.extend({ url = "/categories/" + this.get('id'); } - return Discourse.ajax(url, { + return ajax(url, { data: { name: this.get('name'), slug: this.get('slug'), @@ -103,7 +104,7 @@ const Category = RestModel.extend({ }.property("permissions"), destroy: function() { - return Discourse.ajax("/categories/" + (this.get('id') || this.get('slug')), { type: 'DELETE' }); + return ajax("/categories/" + (this.get('id') || this.get('slug')), { type: 'DELETE' }); }, addPermission: function(permission){ @@ -170,7 +171,7 @@ const Category = RestModel.extend({ setNotification: function(notification_level) { var url = "/category/" + this.get('id')+"/notifications"; this.set('notification_level', notification_level); - return Discourse.ajax(url, { + return ajax(url, { data: { notification_level: notification_level }, @@ -285,11 +286,11 @@ Category.reopenClass({ }, reloadById(id) { - return Discourse.ajax(`/c/${id}/show.json`); + return ajax(`/c/${id}/show.json`); }, reloadBySlug(slug, parentSlug) { - return parentSlug ? Discourse.ajax(`/c/${parentSlug}/${slug}/find_by_slug.json`) : Discourse.ajax(`/c/${slug}/find_by_slug.json`); + return parentSlug ? ajax(`/c/${parentSlug}/${slug}/find_by_slug.json`) : ajax(`/c/${slug}/find_by_slug.json`); }, search(term, opts) { diff --git a/app/assets/javascripts/discourse/models/draft.js.es6 b/app/assets/javascripts/discourse/models/draft.js.es6 index faf16a3ae2..2959f7fce4 100644 --- a/app/assets/javascripts/discourse/models/draft.js.es6 +++ b/app/assets/javascripts/discourse/models/draft.js.es6 @@ -1,9 +1,10 @@ +import { ajax } from 'discourse/lib/ajax'; const Draft = Discourse.Model.extend(); Draft.reopenClass({ clear(key, sequence) { - return Discourse.ajax("/draft.json", { + return ajax("/draft.json", { type: 'DELETE', data: { draft_key: key, @@ -13,7 +14,7 @@ Draft.reopenClass({ }, get(key) { - return Discourse.ajax('/draft.json', { + return ajax('/draft.json', { data: { draft_key: key }, dataType: 'json' }); @@ -26,7 +27,7 @@ Draft.reopenClass({ save(key, sequence, data) { data = typeof data === "string" ? data : JSON.stringify(data); - return Discourse.ajax("/draft.json", { + return ajax("/draft.json", { type: 'POST', data: { draft_key: key, diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index c2369960d5..85c8ba5a5b 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import computed from 'ember-addons/ember-computed-decorators'; const Group = Discourse.Model.extend({ @@ -49,7 +50,7 @@ const Group = Discourse.Model.extend({ removeOwner(member) { var self = this; - return Discourse.ajax('/admin/groups/' + this.get('id') + '/owners.json', { + return ajax('/admin/groups/' + this.get('id') + '/owners.json', { type: "DELETE", data: { user_id: member.get("id") } }).then(function() { @@ -60,7 +61,7 @@ const Group = Discourse.Model.extend({ removeMember(member) { var self = this; - return Discourse.ajax('/groups/' + this.get('id') + '/members.json', { + return ajax('/groups/' + this.get('id') + '/members.json', { type: "DELETE", data: { user_id: member.get("id") } }).then(function() { @@ -71,7 +72,7 @@ const Group = Discourse.Model.extend({ addMembers(usernames) { var self = this; - return Discourse.ajax('/groups/' + this.get('id') + '/members.json', { + return ajax('/groups/' + this.get('id') + '/members.json', { type: "PUT", data: { usernames: usernames } }).then(function() { @@ -81,7 +82,7 @@ const Group = Discourse.Model.extend({ addOwners(usernames) { var self = this; - return Discourse.ajax('/admin/groups/' + this.get('id') + '/owners.json', { + return ajax('/admin/groups/' + this.get('id') + '/owners.json', { type: "PUT", data: { usernames: usernames } }).then(function() { @@ -105,18 +106,18 @@ const Group = Discourse.Model.extend({ create() { var self = this; - return Discourse.ajax("/admin/groups", { type: "POST", data: this.asJSON() }).then(function(resp) { + return ajax("/admin/groups", { type: "POST", data: this.asJSON() }).then(function(resp) { self.set('id', resp.basic_group.id); }); }, save() { - return Discourse.ajax("/admin/groups/" + this.get('id'), { type: "PUT", data: this.asJSON() }); + return ajax("/admin/groups/" + this.get('id'), { type: "PUT", data: this.asJSON() }); }, destroy() { if (!this.get('id')) { return; } - return Discourse.ajax("/admin/groups/" + this.get('id'), { type: "DELETE" }); + return ajax("/admin/groups/" + this.get('id'), { type: "DELETE" }); }, findPosts(opts) { @@ -127,7 +128,7 @@ const Group = Discourse.Model.extend({ var data = {}; if (opts.beforePostId) { data.before_post_id = opts.beforePostId; } - return Discourse.ajax(`/groups/${this.get('name')}/${type}.json`, { data: data }).then(posts => { + return ajax(`/groups/${this.get('name')}/${type}.json`, { data: data }).then(posts => { return posts.map(p => { p.user = Discourse.User.create(p.user); p.topic = Discourse.Topic.create(p.topic); @@ -138,7 +139,7 @@ const Group = Discourse.Model.extend({ setNotification(notification_level) { this.set("notification_level", notification_level); - return Discourse.ajax(`/groups/${this.get("name")}/notifications`, { + return ajax(`/groups/${this.get("name")}/notifications`, { data: { notification_level }, type: "POST" }); @@ -147,21 +148,21 @@ const Group = Discourse.Model.extend({ Group.reopenClass({ findAll(opts) { - return Discourse.ajax("/admin/groups.json", { data: opts }).then(function (groups){ + return ajax("/admin/groups.json", { data: opts }).then(function (groups){ return groups.map(g => Group.create(g)); }); }, findGroupCounts(name) { - return Discourse.ajax("/groups/" + name + "/counts.json").then(result => Em.Object.create(result.counts)); + return ajax("/groups/" + name + "/counts.json").then(result => Em.Object.create(result.counts)); }, find(name) { - return Discourse.ajax("/groups/" + name + ".json").then(result => Group.create(result.basic_group)); + return ajax("/groups/" + name + ".json").then(result => Group.create(result.basic_group)); }, loadMembers(name, offset, limit) { - return Discourse.ajax('/groups/' + name + '/members.json', { + return ajax('/groups/' + name + '/members.json', { data: { limit: limit || 50, offset: offset || 0 diff --git a/app/assets/javascripts/discourse/models/invite.js.es6 b/app/assets/javascripts/discourse/models/invite.js.es6 index 2d5197de0d..8f89cfc684 100644 --- a/app/assets/javascripts/discourse/models/invite.js.es6 +++ b/app/assets/javascripts/discourse/models/invite.js.es6 @@ -1,9 +1,10 @@ +import { ajax } from 'discourse/lib/ajax'; import { popupAjaxError } from 'discourse/lib/ajax-error'; const Invite = Discourse.Model.extend({ rescind() { - Discourse.ajax('/invites', { + ajax('/invites', { type: 'DELETE', data: { email: this.get('email') } }); @@ -12,7 +13,7 @@ const Invite = Discourse.Model.extend({ reinvite() { const self = this; - return Discourse.ajax('/invites/reinvite', { + return ajax('/invites/reinvite', { type: 'POST', data: { email: this.get('email') } }).then(function() { @@ -40,7 +41,7 @@ Invite.reopenClass({ if (!Em.isNone(search)) { data.search = search; } data.offset = offset || 0; - return Discourse.ajax("/users/" + user.get('username_lower') + "/invited.json", {data}).then(function (result) { + return ajax("/users/" + user.get('username_lower') + "/invited.json", {data}).then(function (result) { result.invites = result.invites.map(function (i) { return Invite.create(i); }); @@ -51,11 +52,11 @@ Invite.reopenClass({ findInvitedCount(user) { if (!user) { return Em.RSVP.resolve(); } - return Discourse.ajax("/users/" + user.get('username_lower') + "/invited_count.json").then(result => Em.Object.create(result.counts)); + return ajax("/users/" + user.get('username_lower') + "/invited_count.json").then(result => Em.Object.create(result.counts)); }, reinviteAll() { - return Discourse.ajax('/invites/reinvite-all', { type: 'POST' }); + return ajax('/invites/reinvite-all', { type: 'POST' }); } }); diff --git a/app/assets/javascripts/discourse/models/live-post-counts.es6 b/app/assets/javascripts/discourse/models/live-post-counts.es6 index 678aecbf59..1ecd225d54 100644 --- a/app/assets/javascripts/discourse/models/live-post-counts.es6 +++ b/app/assets/javascripts/discourse/models/live-post-counts.es6 @@ -1,8 +1,9 @@ +import { ajax } from 'discourse/lib/ajax'; const LivePostCounts = Discourse.Model.extend({}); LivePostCounts.reopenClass({ find() { - return Discourse.ajax("/about/live_post_counts.json").then(result => LivePostCounts.create(result)); + return ajax("/about/live_post_counts.json").then(result => LivePostCounts.create(result)); } }); diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 309674a9ab..a96f0e0248 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import DiscourseURL from 'discourse/lib/url'; import RestModel from 'discourse/models/rest'; import PostsWithPlaceholders from 'discourse/lib/posts-with-placeholders'; @@ -455,7 +456,7 @@ export default RestModel.extend({ const url = "/posts/" + postId; const store = this.store; - return Discourse.ajax(url).then(p => this.storePost(store.createRecord('post', p))); + return ajax(url).then(p => this.storePost(store.createRecord('post', p))); }, /** @@ -497,7 +498,7 @@ export default RestModel.extend({ // need to insert into stream const url = "/posts/" + postId; const store = this.store; - return Discourse.ajax(url).then(p => { + return ajax(url).then(p => { const post = store.createRecord('post', p); const stream = this.get("stream"); const posts = this.get("posts"); @@ -538,7 +539,7 @@ export default RestModel.extend({ const url = "/posts/" + postId; const store = this.store; - return Discourse.ajax(url).then(p => { + return ajax(url).then(p => { this.storePost(store.createRecord('post', p)); }).catch(() => { this.removePosts([existing]); @@ -555,7 +556,7 @@ export default RestModel.extend({ if (existing && existing.updated_at !== updatedAt) { const url = "/posts/" + postId; const store = this.store; - return Discourse.ajax(url).then(p => this.storePost(store.createRecord('post', p))); + return ajax(url).then(p => this.storePost(store.createRecord('post', p))); } return resolved; }, @@ -727,7 +728,7 @@ export default RestModel.extend({ const url = "/t/" + this.get('topic.id') + "/posts.json"; const data = { post_ids: postIds }; const store = this.store; - return Discourse.ajax(url, {data}).then(result => { + return ajax(url, {data}).then(result => { const posts = Ember.get(result, "post_stream.posts"); if (posts) { posts.forEach(p => this.storePost(store.createRecord('post', p))); diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index a3891e3e72..a9ca011ce7 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import ActionSummary from 'discourse/models/action-summary'; @@ -67,7 +68,7 @@ const Post = RestModel.extend({ const data = {}; data[field] = value; - return Discourse.ajax(`/posts/${this.get('id')}/${field}`, { type: 'PUT', data }).then(() => { + return ajax(`/posts/${this.get('id')}/${field}`, { type: 'PUT', data }).then(() => { this.set(field, value); this.incrementProperty("version"); }).catch(popupAjaxError); @@ -119,7 +120,7 @@ const Post = RestModel.extend({ // Expands the first post's content, if embedded and shortened. expand() { const self = this; - return Discourse.ajax("/posts/" + this.get('id') + "/expand-embed").then(function(post) { + return ajax("/posts/" + this.get('id') + "/expand-embed").then(function(post) { self.set('cooked', "
" + post.cooked + "
" ); }); }, @@ -136,7 +137,7 @@ const Post = RestModel.extend({ can_delete: false }); - return Discourse.ajax("/posts/" + (this.get('id')) + "/recover", { type: 'PUT', cache: false }).then(function(data){ + return ajax("/posts/" + (this.get('id')) + "/recover", { type: 'PUT', cache: false }).then(function(data){ post.setProperties({ cooked: data.cooked, raw: data.raw, @@ -198,7 +199,7 @@ const Post = RestModel.extend({ destroy(deletedBy) { this.setDeletedState(deletedBy); - return Discourse.ajax("/posts/" + this.get('id'), { + return ajax("/posts/" + this.get('id'), { data: { context: window.location.pathname }, type: 'DELETE' }); @@ -232,17 +233,17 @@ const Post = RestModel.extend({ }, expandHidden() { - return Discourse.ajax("/posts/" + this.get('id') + "/cooked.json").then(result => { + return ajax("/posts/" + this.get('id') + "/cooked.json").then(result => { this.setProperties({ cooked: result.cooked, cooked_hidden: false }); }); }, rebake() { - return Discourse.ajax("/posts/" + this.get("id") + "/rebake", { type: "PUT" }); + return ajax("/posts/" + this.get("id") + "/rebake", { type: "PUT" }); }, unhide() { - return Discourse.ajax("/posts/" + this.get("id") + "/unhide", { type: "PUT" }); + return ajax("/posts/" + this.get("id") + "/unhide", { type: "PUT" }); }, toggleBookmark() { @@ -277,7 +278,7 @@ const Post = RestModel.extend({ }, revertToRevision(version) { - return Discourse.ajax(`/posts/${this.get('id')}/revisions/${version}/revert`, { type: 'PUT' }); + return ajax(`/posts/${this.get('id')}/revisions/${version}/revert`, { type: 'PUT' }); } }); @@ -311,14 +312,14 @@ Post.reopenClass({ }, updateBookmark(postId, bookmarked) { - return Discourse.ajax("/posts/" + postId + "/bookmark", { + return ajax("/posts/" + postId + "/bookmark", { type: 'PUT', data: { bookmarked: bookmarked } }); }, deleteMany(selectedPosts, selectedReplies) { - return Discourse.ajax("/posts/destroy_many", { + return ajax("/posts/destroy_many", { type: 'DELETE', data: { post_ids: selectedPosts.map(function(p) { return p.get('id'); }), @@ -328,27 +329,27 @@ Post.reopenClass({ }, loadRevision(postId, version) { - return Discourse.ajax("/posts/" + postId + "/revisions/" + version + ".json") + return ajax("/posts/" + postId + "/revisions/" + version + ".json") .then(result => Ember.Object.create(result)); }, hideRevision(postId, version) { - return Discourse.ajax("/posts/" + postId + "/revisions/" + version + "/hide", { type: 'PUT' }); + return ajax("/posts/" + postId + "/revisions/" + version + "/hide", { type: 'PUT' }); }, showRevision(postId, version) { - return Discourse.ajax("/posts/" + postId + "/revisions/" + version + "/show", { type: 'PUT' }); + return ajax("/posts/" + postId + "/revisions/" + version + "/show", { type: 'PUT' }); }, loadQuote(postId) { - return Discourse.ajax("/posts/" + postId + ".json").then(result => { + return ajax("/posts/" + postId + ".json").then(result => { const post = Discourse.Post.create(result); return Quote.build(post, post.get('raw'), {raw: true, full: true}); }); }, loadRawEmail(postId) { - return Discourse.ajax(`/posts/${postId}/raw-email.json`); + return ajax(`/posts/${postId}/raw-email.json`); } }); diff --git a/app/assets/javascripts/discourse/models/static-page.js.es6 b/app/assets/javascripts/discourse/models/static-page.js.es6 index a935b0edcd..6bab9df1ce 100644 --- a/app/assets/javascripts/discourse/models/static-page.js.es6 +++ b/app/assets/javascripts/discourse/models/static-page.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; const StaticPage = Ember.Object.extend(); StaticPage.reopenClass({ @@ -11,7 +12,7 @@ StaticPage.reopenClass({ text = text.match(/((?:.|[\n\r])*)/)[1]; resolve(StaticPage.create({path: path, html: text})); } else { - Discourse.ajax(path + ".html", {dataType: 'html'}).then(function (result) { + ajax(path + ".html", {dataType: 'html'}).then(function (result) { resolve(StaticPage.create({path: path, html: result})); }); } diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index 512d41a824..1dd6ee2a1f 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; import ResultSet from 'discourse/models/result-set'; @@ -114,7 +115,7 @@ export default Ember.Object.extend({ refreshResults(resultSet, type, url) { const self = this; - return Discourse.ajax(url).then(result => { + return ajax(url).then(result => { const typeName = Ember.String.underscore(self.pluralize(type)); const content = result[typeName].map(obj => self._hydrate(type, obj, result)); resultSet.set('content', content); @@ -124,7 +125,7 @@ export default Ember.Object.extend({ appendResults(resultSet, type, url) { const self = this; - return Discourse.ajax(url).then(function(result) { + return ajax(url).then(function(result) { const typeName = Ember.String.underscore(self.pluralize(type)), totalRows = result["total_rows_" + typeName] || result.get('totalRows'), loadMoreUrl = result["load_more_" + typeName], diff --git a/app/assets/javascripts/discourse/models/tag-group.js.es6 b/app/assets/javascripts/discourse/models/tag-group.js.es6 index 67ee9b63e4..f27adad26b 100644 --- a/app/assets/javascripts/discourse/models/tag-group.js.es6 +++ b/app/assets/javascripts/discourse/models/tag-group.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; import computed from 'ember-addons/ember-computed-decorators'; @@ -17,7 +18,7 @@ const TagGroup = RestModel.extend({ this.set('savingStatus', I18n.t('saving')); this.set('saving', true); - return Discourse.ajax(url, { + return ajax(url, { data: { name: this.get('name'), tag_names: this.get('tag_names'), @@ -33,7 +34,7 @@ const TagGroup = RestModel.extend({ }, destroy() { - return Discourse.ajax("/tag_groups/" + this.get('id'), {type: "DELETE"}); + return ajax("/tag_groups/" + this.get('id'), {type: "DELETE"}); } }); diff --git a/app/assets/javascripts/discourse/models/topic-details.js.es6 b/app/assets/javascripts/discourse/models/topic-details.js.es6 index 833af1e198..25714b0c4b 100644 --- a/app/assets/javascripts/discourse/models/topic-details.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-details.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; /** A model representing a Topic's details that aren't always present, such as a list of participants. When showing topics in lists and such this information should not be required. @@ -57,7 +58,7 @@ const TopicDetails = RestModel.extend({ updateNotifications(v) { this.set('notification_level', v); this.set('notifications_reason_id', null); - return Discourse.ajax("/t/" + (this.get('topic.id')) + "/notifications", { + return ajax("/t/" + (this.get('topic.id')) + "/notifications", { type: 'POST', data: { notification_level: v } }); @@ -67,7 +68,7 @@ const TopicDetails = RestModel.extend({ const groups = this.get('allowed_groups'); const name = group.name; - return Discourse.ajax("/t/" + this.get('topic.id') + "/remove-allowed-group", { + return ajax("/t/" + this.get('topic.id') + "/remove-allowed-group", { type: 'PUT', data: { name: name } }).then(() => { @@ -79,7 +80,7 @@ const TopicDetails = RestModel.extend({ const users = this.get('allowed_users'); const username = user.get('username'); - return Discourse.ajax("/t/" + this.get('topic.id') + "/remove-allowed-user", { + return ajax("/t/" + this.get('topic.id') + "/remove-allowed-user", { type: 'PUT', data: { username: username } }).then(() => { diff --git a/app/assets/javascripts/discourse/models/topic-list.js.es6 b/app/assets/javascripts/discourse/models/topic-list.js.es6 index 15eff2635f..4e547619d1 100644 --- a/app/assets/javascripts/discourse/models/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-list.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; import Model from 'discourse/models/model'; @@ -60,7 +61,7 @@ const TopicList = RestModel.extend({ this.set('loadingMore', true); const store = this.store; - return Discourse.ajax({url: moreUrl}).then(function (result) { + return ajax({url: moreUrl}).then(function (result) { let topicsAdded = 0; if (result) { @@ -100,7 +101,7 @@ const TopicList = RestModel.extend({ const url = `${Discourse.getURL("/")}${this.get('filter')}?topic_ids=${topic_ids.join(",")}`; const store = this.store; - return Discourse.ajax({ url }).then(result => { + return ajax({ url }).then(result => { let i = 0; topicList.forEachNew(topicsFrom(result, store), function(t) { // highlight the first of the new topics so we can get a visual feedback diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 3f11614a18..5054634cac 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { flushMap } from 'discourse/models/store'; import RestModel from 'discourse/models/rest'; import { propertyEqual } from 'discourse/lib/computed'; @@ -19,7 +20,7 @@ export function loadTopicView(topic, args) { delete data.store; return PreloadStore.getAndRemove(`topic_${topicId}`, () => { - return Discourse.ajax(jsonUrl, {data}); + return ajax(jsonUrl, {data}); }).then(json => { topic.updateFromJson(json); return json; @@ -225,7 +226,7 @@ const Topic = RestModel.extend({ this.set('details.auto_close_at', null); } } - return Discourse.ajax(this.get('url') + "/status", { + return ajax(this.get('url') + "/status", { type: 'PUT', data: { status: property, @@ -237,13 +238,13 @@ const Topic = RestModel.extend({ makeBanner() { const self = this; - return Discourse.ajax('/t/' + this.get('id') + '/make-banner', { type: 'PUT' }) + return ajax('/t/' + this.get('id') + '/make-banner', { type: 'PUT' }) .then(function () { self.set('archetype', 'banner'); }); }, removeBanner() { const self = this; - return Discourse.ajax('/t/' + this.get('id') + '/remove-banner', { type: 'PUT' }) + return ajax('/t/' + this.get('id') + '/remove-banner', { type: 'PUT' }) .then(function () { self.set('archetype', 'regular'); }); }, @@ -258,7 +259,7 @@ const Topic = RestModel.extend({ const path = bookmark ? '/bookmark' : '/remove_bookmarks'; const toggleBookmarkOnServer = () => { - return Discourse.ajax(`/t/${this.get('id')}${path}`, { type: 'PUT' }).then(() => { + return ajax(`/t/${this.get('id')}${path}`, { type: 'PUT' }).then(() => { this.toggleProperty('bookmarked'); if (bookmark && firstPost) { firstPost.set('bookmarked', true); @@ -314,21 +315,21 @@ const Topic = RestModel.extend({ }, createGroupInvite(group) { - return Discourse.ajax("/t/" + this.get('id') + "/invite-group", { + return ajax("/t/" + this.get('id') + "/invite-group", { type: 'POST', data: { group } }); }, createInvite(user, group_names, custom_message) { - return Discourse.ajax("/t/" + this.get('id') + "/invite", { + return ajax("/t/" + this.get('id') + "/invite", { type: 'POST', data: { user, group_names, custom_message } }); }, generateInviteLink: function(email, groupNames, topicId) { - return Discourse.ajax('/invites/link', { + return ajax('/invites/link', { type: 'POST', data: {email: email, group_names: groupNames, topic_id: topicId} }); @@ -342,7 +343,7 @@ const Topic = RestModel.extend({ 'details.can_delete': false, 'details.can_recover': true }); - return Discourse.ajax("/t/" + this.get('id'), { + return ajax("/t/" + this.get('id'), { data: { context: window.location.pathname }, type: 'DELETE' }); @@ -356,7 +357,7 @@ const Topic = RestModel.extend({ 'details.can_delete': true, 'details.can_recover': false }); - return Discourse.ajax("/t/" + this.get('id') + "/recover", { type: 'PUT' }); + return ajax("/t/" + this.get('id') + "/recover", { type: 'PUT' }); }, // Update our attributes from a JSON result @@ -372,7 +373,7 @@ const Topic = RestModel.extend({ reload() { const self = this; - return Discourse.ajax('/t/' + this.get('id'), { type: 'GET' }).then(function(topic_json) { + return ajax('/t/' + this.get('id'), { type: 'GET' }).then(function(topic_json) { self.updateFromJson(topic_json); }); }, @@ -388,7 +389,7 @@ const Topic = RestModel.extend({ topic.set('pinned', false); topic.set('unpinned', true); - Discourse.ajax("/t/" + this.get('id') + "/clear-pin", { + ajax("/t/" + this.get('id') + "/clear-pin", { type: 'PUT' }).then(null, function() { // On error, put the pin back @@ -412,7 +413,7 @@ const Topic = RestModel.extend({ topic.set('pinned', true); topic.set('unpinned', false); - Discourse.ajax("/t/" + this.get('id') + "/re-pin", { + ajax("/t/" + this.get('id') + "/re-pin", { type: 'PUT' }).then(null, function() { // On error, put the pin back @@ -434,7 +435,7 @@ const Topic = RestModel.extend({ archiveMessage() { this.set("archiving", true); - var promise = Discourse.ajax(`/t/${this.get('id')}/archive-message`, {type: 'PUT'}); + var promise = ajax(`/t/${this.get('id')}/archive-message`, {type: 'PUT'}); promise.then((msg)=> { this.set('message_archived', true); @@ -448,7 +449,7 @@ const Topic = RestModel.extend({ moveToInbox() { this.set("archiving", true); - var promise = Discourse.ajax(`/t/${this.get('id')}/move-to-inbox`, {type: 'PUT'}); + var promise = ajax(`/t/${this.get('id')}/move-to-inbox`, {type: 'PUT'}); promise.then((msg)=> { this.set('message_archived', false); @@ -461,7 +462,7 @@ const Topic = RestModel.extend({ }, convertTopic(type) { - return Discourse.ajax(`/t/${this.get('id')}/convert-topic/${type}`, {type: 'PUT'}).then(() => { + return ajax(`/t/${this.get('id')}/convert-topic/${type}`, {type: 'PUT'}).then(() => { window.location.reload(); }).catch(popupAjaxError); } @@ -511,7 +512,7 @@ Topic.reopenClass({ } }); - return Discourse.ajax(topic.get('url'), { type: 'PUT', data: props }).then(function(result) { + return ajax(topic.get('url'), { type: 'PUT', data: props }).then(function(result) { // The title can be cleaned up server side props.title = result.basic_topic.title; props.fancy_title = result.basic_topic.fancy_title; @@ -558,11 +559,11 @@ Topic.reopenClass({ } // Check the preload store. If not, load it via JSON - return Discourse.ajax(url + ".json", {data: data}); + return ajax(url + ".json", {data: data}); }, changeOwners(topicId, opts) { - const promise = Discourse.ajax("/t/" + topicId + "/change-owner", { + const promise = ajax("/t/" + topicId + "/change-owner", { type: 'POST', data: opts }).then(function (result) { @@ -573,7 +574,7 @@ Topic.reopenClass({ }, changeTimestamp(topicId, timestamp) { - const promise = Discourse.ajax("/t/" + topicId + '/change-timestamp', { + const promise = ajax("/t/" + topicId + '/change-timestamp', { type: 'PUT', data: { timestamp: timestamp }, }).then(function(result) { @@ -584,7 +585,7 @@ Topic.reopenClass({ }, bulkOperation(topics, operation) { - return Discourse.ajax("/topics/bulk", { + return ajax("/topics/bulk", { type: 'PUT', data: { topic_ids: topics.map(function(t) { return t.get('id'); }), @@ -596,18 +597,18 @@ Topic.reopenClass({ bulkOperationByFilter(filter, operation, categoryId) { const data = { filter: filter, operation: operation }; if (categoryId) data['category_id'] = categoryId; - return Discourse.ajax("/topics/bulk", { + return ajax("/topics/bulk", { type: 'PUT', data: data }); }, resetNew() { - return Discourse.ajax("/topics/reset-new", {type: 'PUT'}); + return ajax("/topics/reset-new", {type: 'PUT'}); }, idForSlug(slug) { - return Discourse.ajax("/t/id_for/" + slug); + return ajax("/t/id_for/" + slug); } }); @@ -621,11 +622,11 @@ function moveResult(result) { } export function movePosts(topicId, data) { - return Discourse.ajax("/t/" + topicId + "/move-posts", { type: 'POST', data }).then(moveResult); + return ajax("/t/" + topicId + "/move-posts", { type: 'POST', data }).then(moveResult); } export function mergeTopic(topicId, destinationTopicId) { - return Discourse.ajax("/t/" + topicId + "/merge-topic", { + return ajax("/t/" + topicId + "/merge-topic", { type: 'POST', data: {destination_topic_id: destinationTopicId} }).then(moveResult); diff --git a/app/assets/javascripts/discourse/models/user-badge.js.es6 b/app/assets/javascripts/discourse/models/user-badge.js.es6 index bc257aafc8..e3a6872432 100644 --- a/app/assets/javascripts/discourse/models/user-badge.js.es6 +++ b/app/assets/javascripts/discourse/models/user-badge.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import Badge from 'discourse/models/badge'; const UserBadge = Discourse.Model.extend({ @@ -8,7 +9,7 @@ const UserBadge = Discourse.Model.extend({ }.property(), // avoid the extra bindings for now revoke() { - return Discourse.ajax("/user_badges/" + this.get('id'), { + return ajax("/user_badges/" + this.get('id'), { type: "DELETE" }); } @@ -89,7 +90,7 @@ UserBadge.reopenClass({ if (options && options.grouped) { url += "?grouped=true"; } - return Discourse.ajax(url).then(function(json) { + return ajax(url).then(function(json) { return UserBadge.createFromJson(json); }); }, @@ -105,7 +106,7 @@ UserBadge.reopenClass({ if (!options) { options = {}; } options.badge_id = badgeId; - return Discourse.ajax("/user_badges.json", { + return ajax("/user_badges.json", { data: options }).then(function(json) { return UserBadge.createFromJson(json); @@ -121,7 +122,7 @@ UserBadge.reopenClass({ @returns {Promise} a promise that resolves to an instance of `UserBadge`. **/ grant: function(badgeId, username, reason) { - return Discourse.ajax("/user_badges", { + return ajax("/user_badges", { type: "POST", data: { username: username, diff --git a/app/assets/javascripts/discourse/models/user-posts-stream.js.es6 b/app/assets/javascripts/discourse/models/user-posts-stream.js.es6 index 8d7a750d8b..cd552d35db 100644 --- a/app/assets/javascripts/discourse/models/user-posts-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/user-posts-stream.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { url } from 'discourse/lib/computed'; import AdminPost from 'discourse/models/admin-post'; @@ -33,7 +34,7 @@ export default Discourse.Model.extend({ this.set("loading", true); - return Discourse.ajax(this.get("url"), { cache: false }).then(function (result) { + return ajax(this.get("url"), { cache: false }).then(function (result) { if (result) { const posts = result.map(function (post) { return AdminPost.create(post); }); self.get("content").pushObjects(posts); diff --git a/app/assets/javascripts/discourse/models/user-stream.js.es6 b/app/assets/javascripts/discourse/models/user-stream.js.es6 index 56d0b269e1..49c02b4e61 100644 --- a/app/assets/javascripts/discourse/models/user-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/user-stream.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { url } from 'discourse/lib/computed'; import RestModel from 'discourse/models/rest'; import UserAction from 'discourse/models/user-action'; @@ -67,7 +68,7 @@ export default RestModel.extend({ if (this.get('loading')) { return Ember.RSVP.resolve(); } this.set('loading', true); - return Discourse.ajax(findUrl, {cache: 'false'}).then( function(result) { + return ajax(findUrl, {cache: 'false'}).then( function(result) { if (result && result.user_actions) { const copy = Em.A(); result.user_actions.forEach(function(action) { diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 965e4e636a..f35d4b11f3 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { url } from 'discourse/lib/computed'; import RestModel from 'discourse/models/rest'; import UserStream from 'discourse/models/user-stream'; @@ -39,7 +40,7 @@ const User = RestModel.extend({ staff: Em.computed.or('admin', 'moderator'), destroySession() { - return Discourse.ajax(`/session/${this.get('username')}`, { type: 'DELETE'}); + return ajax(`/session/${this.get('username')}`, { type: 'DELETE'}); }, @computed("username_lower") @@ -125,14 +126,14 @@ const User = RestModel.extend({ }, changeUsername(new_username) { - return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/username`, { + return ajax(`/users/${this.get('username_lower')}/preferences/username`, { type: 'PUT', data: { new_username } }); }, changeEmail(email) { - return Discourse.ajax(`/users/${this.get('username_lower')}/preferences/email`, { + return ajax(`/users/${this.get('username_lower')}/preferences/email`, { type: 'PUT', data: { email } }); @@ -202,7 +203,7 @@ const User = RestModel.extend({ // TODO: We can remove this when migrated fully to rest model. this.set('isSaving', true); - return Discourse.ajax(`/users/${this.get('username_lower')}`, { + return ajax(`/users/${this.get('username_lower')}`, { data: data, type: 'PUT' }).then(result => { @@ -216,7 +217,7 @@ const User = RestModel.extend({ }, changePassword() { - return Discourse.ajax("/session/forgot_password", { + return ajax("/session/forgot_password", { dataType: 'json', data: { login: this.get('username') }, type: 'POST' @@ -225,7 +226,7 @@ const User = RestModel.extend({ loadUserAction(id) { const stream = this.get('stream'); - return Discourse.ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => { + return ajax(`/user_actions/${id}.json`, { cache: 'false' }).then(result => { if (result && result.user_action) { const ua = result.user_action; @@ -278,7 +279,7 @@ const User = RestModel.extend({ const user = this; return PreloadStore.getAndRemove(`user_${user.get('username')}`, () => { - return Discourse.ajax(`/users/${user.get('username')}.json`, { data: options }); + return ajax(`/users/${user.get('username')}.json`, { data: options }); }).then(json => { if (!Em.isEmpty(json.user.stats)) { @@ -315,13 +316,13 @@ const User = RestModel.extend({ findStaffInfo() { if (!Discourse.User.currentProp("staff")) { return Ember.RSVP.resolve(null); } - return Discourse.ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => { + return ajax(`/users/${this.get("username_lower")}/staff-info.json`).then(info => { this.setProperties(info); }); }, pickAvatar(upload_id, type, avatar_template) { - return Discourse.ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, { + return ajax(`/users/${this.get("username_lower")}/preferences/avatar/pick`, { type: 'PUT', data: { upload_id, type } }).then(() => this.setProperties({ @@ -337,14 +338,14 @@ const User = RestModel.extend({ }, createInvite(email, group_names, custom_message) { - return Discourse.ajax('/invites', { + return ajax('/invites', { type: 'POST', data: { email, group_names, custom_message } }); }, generateInviteLink(email, group_names, topic_id) { - return Discourse.ajax('/invites/link', { + return ajax('/invites/link', { type: 'POST', data: { email, group_names, topic_id } }); @@ -377,7 +378,7 @@ const User = RestModel.extend({ "delete": function() { if (this.get('can_delete_account')) { - return Discourse.ajax("/users/" + this.get('username'), { + return ajax("/users/" + this.get('username'), { type: 'DELETE', data: {context: window.location.pathname} }); @@ -388,14 +389,14 @@ const User = RestModel.extend({ dismissBanner(bannerKey) { this.set("dismissed_banner_key", bannerKey); - Discourse.ajax(`/users/${this.get('username')}`, { + ajax(`/users/${this.get('username')}`, { type: 'PUT', data: { dismissed_banner_key: bannerKey } }); }, checkEmail() { - return Discourse.ajax(`/users/${this.get("username_lower")}/emails.json`, { + return ajax(`/users/${this.get("username_lower")}/emails.json`, { type: "PUT", data: { context: window.location.pathname } }).then(result => { @@ -409,7 +410,7 @@ const User = RestModel.extend({ }, summary() { - return Discourse.ajax(`/users/${this.get("username_lower")}/summary.json`) + return ajax(`/users/${this.get("username_lower")}/summary.json`) .then(json => { const summary = json["user_summary"]; const topicMap = {}; @@ -464,7 +465,7 @@ User.reopenClass(Singleton, { }, checkUsername(username, email, for_user_id) { - return Discourse.ajax('/users/check_username', { + return ajax('/users/check_username', { data: { username, email, for_user_id } }); }, @@ -495,7 +496,7 @@ User.reopenClass(Singleton, { }, createAccount(attrs) { - return Discourse.ajax("/users", { + return ajax("/users", { data: { name: attrs.accountName, email: attrs.accountEmail, diff --git a/app/assets/javascripts/discourse/routes/about.js.es6 b/app/assets/javascripts/discourse/routes/about.js.es6 index f25d64387b..1b83dfec2a 100644 --- a/app/assets/javascripts/discourse/routes/about.js.es6 +++ b/app/assets/javascripts/discourse/routes/about.js.es6 @@ -1,6 +1,7 @@ +import { ajax } from 'discourse/lib/ajax'; export default Discourse.Route.extend({ model() { - return Discourse.ajax("/about.json").then(result => result.about); + return ajax("/about.json").then(result => result.about); }, titleToken() { diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index aa0a999456..2b216ce15b 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { setting } from 'discourse/lib/computed'; import logout from 'discourse/lib/logout'; import showModal from 'discourse/lib/show-modal'; @@ -28,13 +29,13 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { actions: { showSearchHelp() { - Discourse.ajax("/static/search_help.html", { dataType: 'html' }).then(model => { + ajax("/static/search_help.html", { dataType: 'html' }).then(model => { showModal('searchHelp', { model }); }); }, toggleAnonymous() { - Discourse.ajax("/users/toggle-anon", {method: 'POST'}).then(() => { + ajax("/users/toggle-anon", {method: 'POST'}).then(() => { window.location.reload(); }); }, diff --git a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 index dc895f4307..b59580b8bf 100644 --- a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { translateResults, getSearchKey, isValidSearchTerm } from "discourse/lib/search"; import Composer from 'discourse/models/composer'; @@ -25,7 +26,7 @@ export default Discourse.Route.extend({ return PreloadStore.getAndRemove("search", function() { if (isValidSearchTerm(params.q)) { - return Discourse.ajax("/search", { data: args }); + return ajax("/search", { data: args }); } else { return null; } diff --git a/app/assets/javascripts/discourse/routes/unknown.js.es6 b/app/assets/javascripts/discourse/routes/unknown.js.es6 index db0cde0c52..7e01606f3b 100644 --- a/app/assets/javascripts/discourse/routes/unknown.js.es6 +++ b/app/assets/javascripts/discourse/routes/unknown.js.es6 @@ -1,5 +1,6 @@ +import { ajax } from 'discourse/lib/ajax'; export default Discourse.Route.extend({ model: function() { - return Discourse.ajax("/404-body", { dataType: 'html' }); + return ajax("/404-body", { dataType: 'html' }); } }); diff --git a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 index 0c80c877d2..7cb5ce028a 100644 --- a/app/assets/javascripts/discourse/widgets/notification-item.js.es6 +++ b/app/assets/javascripts/discourse/widgets/notification-item.js.es6 @@ -5,6 +5,7 @@ import DiscourseURL from 'discourse/lib/url'; import { h } from 'virtual-dom'; import { emojiUnescape } from 'discourse/lib/text'; import { postUrl, escapeExpression } from 'discourse/lib/utilities'; +import { setTransientHeader } from 'discourse/lib/ajax'; const LIKED_TYPE = 5; const INVITED_TYPE = 8; @@ -101,7 +102,7 @@ createWidget('notification-item', { click(e) { this.attrs.set('read', true); const id = this.attrs.id; - Discourse.setTransientHeader("Discourse-Clear-Notifications", id); + setTransientHeader("Discourse-Clear-Notifications", id); if (document && document.cookie) { document.cookie = `cn=${id}; expires=Fri, 31 Dec 9999 23:59:59 GMT`; } diff --git a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 index 3c0c0a1332..cf9a4a66e0 100644 --- a/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-cooked.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { isValidLink } from 'discourse/lib/click-track'; import { number } from 'discourse/lib/formatter'; @@ -130,7 +131,7 @@ export default class PostCooked { const postId = parseInt($aside.data('post'), 10); topicId = parseInt(topicId, 10); - Discourse.ajax(`/posts/by_number/${topicId}/${postId}`).then(result => { + ajax(`/posts/by_number/${topicId}/${postId}`).then(result => { const div = $("
"); div.html(result.cooked); div.highlight(originalText, {caseSensitive: true, element: 'span', className: 'highlighted'}); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 3eb0e12412..954e308ba3 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -1,4 +1,3 @@ -//= require ./discourse/mixins/ajax //= require ./discourse // Stuff we need to load first @@ -7,6 +6,7 @@ //= require ./ember-addons/macro-alias //= require ./ember-addons/ember-computed-decorators //= require ./discourse/lib/utilities +//= require ./discourse/lib/ajax //= require ./discourse/lib/text //= require ./discourse/lib/hash //= require ./discourse/lib/load-script diff --git a/app/assets/javascripts/pretty-text/oneboxer.js.es6 b/app/assets/javascripts/pretty-text/oneboxer.js.es6 index c6497f8600..31733ab0d5 100644 --- a/app/assets/javascripts/pretty-text/oneboxer.js.es6 +++ b/app/assets/javascripts/pretty-text/oneboxer.js.es6 @@ -10,7 +10,7 @@ const failedCache = {}; // Perform a lookup of a onebox based an anchor element. It will insert a loading // indicator and remove it when the loading is complete or fails. -export function load(e, refresh) { +export function load(e, refresh, ajax) { var $elem = $(e); // If the onebox has loaded, return @@ -34,7 +34,7 @@ export function load(e, refresh) { $elem.addClass('loading-onebox'); // Retrieve the onebox - return Discourse.ajax("/onebox", { + return ajax("/onebox", { dataType: 'html', data: { url, refresh }, cache: true diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index cd2e928220..c0c40f8427 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -245,6 +245,7 @@ define("discourse/initializers/login-method-#{hash}", name: "login-method-#{hash}", after: "inject-objects", initialize: function() { + if (Ember.testing) { return; } module.register(#{auth_json}); } }; diff --git a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 index b1847fbdf4..e7164334ce 100644 --- a/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 +++ b/plugins/discourse-details/assets/javascripts/initializers/apply-details.js.es6 @@ -11,7 +11,7 @@ function initializeDetails(api) { }; }); - const ComposerController = api.container.lookup("controller:composer"); + const ComposerController = api.container.lookupFactory("controller:composer"); ComposerController.reopen({ actions: { insertDetails() { @@ -27,7 +27,6 @@ function initializeDetails(api) { export default { name: "apply-details", - after: 'inject-objects', initialize() { withPluginApi('0.5', initializeDetails); diff --git a/plugins/poll/assets/javascripts/components/poll-voters.js.es6 b/plugins/poll/assets/javascripts/components/poll-voters.js.es6 index d2b8af9ae9..ac43bc53ca 100644 --- a/plugins/poll/assets/javascripts/components/poll-voters.js.es6 +++ b/plugins/poll/assets/javascripts/components/poll-voters.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; export default Ember.Component.extend({ layoutName: "components/poll-voters", tagName: 'ul', @@ -16,7 +17,7 @@ export default Ember.Component.extend({ _fetchUsers() { this.set("loading", true); - Discourse.ajax("/polls/voters.json", { + ajax("/polls/voters.json", { type: "get", data: { user_ids: this.get("voterIds") } }).then(result => { diff --git a/plugins/poll/assets/javascripts/controllers/poll.js.es6 b/plugins/poll/assets/javascripts/controllers/poll.js.es6 index acf63314c4..fe820beaa6 100644 --- a/plugins/poll/assets/javascripts/controllers/poll.js.es6 +++ b/plugins/poll/assets/javascripts/controllers/poll.js.es6 @@ -1,3 +1,4 @@ +import { ajax } from 'discourse/lib/ajax'; import { default as computed, observes } from "ember-addons/ember-computed-decorators"; export default Ember.Controller.extend({ @@ -137,7 +138,7 @@ export default Ember.Controller.extend({ this.set("loading", true); - Discourse.ajax("/polls/vote", { + ajax("/polls/vote", { type: "PUT", data: { post_id: this.get("post.id"), @@ -175,7 +176,7 @@ export default Ember.Controller.extend({ if (confirmed) { self.set("loading", true); - Discourse.ajax("/polls/toggle_status", { + ajax("/polls/toggle_status", { type: "PUT", data: { post_id: self.get("post.id"), diff --git a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 index 61fb332cb9..82b830c3e1 100644 --- a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 @@ -1,8 +1,8 @@ import { withPluginApi } from 'discourse/lib/plugin-api'; import showModal from 'discourse/lib/show-modal'; -import ComposerController from 'discourse/controllers/composer'; function initializePollUIBuilder(api) { + const ComposerController = api.container.lookupFactory("controller:composer"); ComposerController.reopen({ actions: { showPollBuilder() { diff --git a/test/javascripts/adapters/topic-list-test.js.es6 b/test/javascripts/adapters/topic-list-test.js.es6 deleted file mode 100644 index 89af1b9229..0000000000 --- a/test/javascripts/adapters/topic-list-test.js.es6 +++ /dev/null @@ -1,13 +0,0 @@ -module("adapter:topic-list"); - -import { finderFor } from 'discourse/adapters/topic-list'; - -test("finderFor", function() { - // Mocking instead of using a pretender which decodes the path and thus does - // not reflect the behavior of an actual web server. - var mock = sandbox.mock(Discourse); - mock.expects("ajax").withArgs("/search.json?q=test%25%25"); - var finderForFunction = finderFor('search', { q: "test%%" }); - finderForFunction(); - mock.verify(); -}); diff --git a/test/javascripts/admin/models/admin-user-test.js.es6 b/test/javascripts/admin/models/admin-user-test.js.es6 index 16e3cf0dde..eb3a4ed7cb 100644 --- a/test/javascripts/admin/models/admin-user-test.js.es6 +++ b/test/javascripts/admin/models/admin-user-test.js.es6 @@ -2,32 +2,23 @@ import { blank, present } from 'helpers/qunit-helpers'; import AdminUser from 'admin/models/admin-user'; import ApiKey from 'admin/models/api-key'; -module("Discourse.AdminUser"); - -asyncTestDiscourse('generate key', function() { - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({api_key: {id: 1234, key: 'asdfasdf'}})); +module("model:admin-user"); +test('generate key', function() { var adminUser = AdminUser.create({id: 333}); - blank(adminUser.get('api_key'), 'it has no api key by default'); adminUser.generateApiKey().then(function() { - start(); - ok(Discourse.ajax.calledWith("/admin/users/333/generate_api_key", { type: 'POST' }), "it POSTed to the url"); present(adminUser.get('api_key'), 'it has an api_key now'); }); }); -asyncTestDiscourse('revoke key', function() { +test('revoke key', function() { var apiKey = ApiKey.create({id: 1234, key: 'asdfasdf'}), adminUser = AdminUser.create({id: 333, api_key: apiKey}); - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve()); - equal(adminUser.get('api_key'), apiKey, 'it has the api key in the beginning'); adminUser.revokeApiKey().then(function() { - start(); - ok(Discourse.ajax.calledWith("/admin/users/333/revoke_api_key", { type: 'DELETE' }), "it DELETEd to the url"); blank(adminUser.get('api_key'), 'it cleared the api_key'); }); }); diff --git a/test/javascripts/admin/models/api-key-test.js.es6 b/test/javascripts/admin/models/api-key-test.js.es6 deleted file mode 100644 index 640845ff80..0000000000 --- a/test/javascripts/admin/models/api-key-test.js.es6 +++ /dev/null @@ -1,48 +0,0 @@ -import { present } from 'helpers/qunit-helpers'; -import ApiKey from 'admin/models/api-key'; - -module("Discourse.ApiKey"); - -test('create', function() { - var apiKey = ApiKey.create({id: 123, user: {id: 345}}); - - present(apiKey, 'it creates the api key'); - present(apiKey.get('user'), 'it creates the user inside'); -}); - - -asyncTestDiscourse('find', function() { - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve([])); - ApiKey.find().then(function() { - start(); - ok(Discourse.ajax.calledWith("/admin/api"), "it GETs the keys"); - }); -}); - -asyncTestDiscourse('generateMasterKey', function() { - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({api_key: {}})); - ApiKey.generateMasterKey().then(function() { - start(); - ok(Discourse.ajax.calledWith("/admin/api/key", {type: 'POST'}), "it POSTs to create a master key"); - }); -}); - -asyncTestDiscourse('regenerate', function() { - var apiKey = ApiKey.create({id: 3456}); - - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({api_key: {id: 3456}})); - apiKey.regenerate().then(function() { - start(); - ok(Discourse.ajax.calledWith("/admin/api/key", {type: 'PUT', data: {id: 3456}}), "it PUTs the key"); - }); -}); - -asyncTestDiscourse('revoke', function() { - var apiKey = ApiKey.create({id: 3456}); - - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve([])); - apiKey.revoke().then(function() { - start(); - ok(Discourse.ajax.calledWith("/admin/api/key", {type: 'DELETE', data: {id: 3456}}), "it DELETES the key"); - }); -}); diff --git a/test/javascripts/admin/models/flagged-post-test.js.es6 b/test/javascripts/admin/models/flagged-post-test.js.es6 deleted file mode 100644 index 3b5587e041..0000000000 --- a/test/javascripts/admin/models/flagged-post-test.js.es6 +++ /dev/null @@ -1,21 +0,0 @@ -import FlaggedPost from 'admin/models/flagged-post'; - -module("Discourse.FlaggedPost"); - -test('delete first post', function() { - sandbox.stub(Discourse, 'ajax'); - - FlaggedPost.create({ id: 1, topic_id: 2, post_number: 1 }) - .deletePost(); - - ok(Discourse.ajax.calledWith("/t/2", { type: 'DELETE', cache: false }), "it deleted the topic"); -}); - -test('delete second post', function() { - sandbox.stub(Discourse, 'ajax'); - - FlaggedPost.create({ id: 1, topic_id: 2, post_number: 2 }) - .deletePost(); - - ok(Discourse.ajax.calledWith("/posts/1", { type: 'DELETE', cache: false }), "it deleted the post"); -}); diff --git a/test/javascripts/fixtures/group-fixtures.js.es6 b/test/javascripts/fixtures/group-fixtures.js.es6 index cb8fc3b1ff..8a2d2fa252 100644 --- a/test/javascripts/fixtures/group-fixtures.js.es6 +++ b/test/javascripts/fixtures/group-fixtures.js.es6 @@ -86,7 +86,7 @@ export default { "/groups/discourse/posts.json":[ { "id":94607, - "cooked":"

Right now we have two entirely different styles for new topics and new posts within a topic... we can probably fix that pretty easily.

\n\n

\n\n

So the simple change would be:

\n\n

\n\n

but... while the dot makes the \"• new\" stand out more... it doesn't communicate any information other than \"look at me\" — can we add more context without adding more noise?

\n\n

", + "cooked":"

I don't know how to pronounce that in English, but this makes me think of the French word \"disquette\" (floppy disk)

", "created_at":"2015-01-23T15:13:01.935Z", "title":"Consistent new indicator", "url":"/t/consistent-new-indicator/24355/1", @@ -186,7 +186,7 @@ export default { }, { "id":94601, - "cooked":"

Yeah I think this category arrangement is the way to go at the very least - much easier to scan two columns...

\n\n

Also, maybe square off the bars?

\n\n

\n\n

", + "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", "created_at":"2015-01-23T14:51:55.497Z", "title":"The end of Clown Vomit, or, simplified category styles", "url":"/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/62", @@ -236,7 +236,7 @@ export default { }, { "id":94577, - "cooked":"

Yup, that's the latest version \"wink\"

\n\n

\n\n

(click to view animated version)

", + "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", "created_at":"2015-01-23T10:50:55.846Z", "title":"Quote reply insertion at cursor position", "url":"/t/quote-reply-insertion-at-cursor-position/24344/4", @@ -286,7 +286,7 @@ export default { }, { "id":94574, - "cooked":"\n\n

It used to be that but that was fixed a while ago. Are you running a recent version?

", + "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", "created_at":"2015-01-23T10:31:29.222Z", "title":"Quote reply insertion at cursor position", "url":"/t/quote-reply-insertion-at-cursor-position/24344/2", @@ -336,7 +336,7 @@ export default { }, { "id":94572, - "cooked":"\n\n

That's an Ember update that introduced this change.

", + "cooked":"

Agree that the markup isn't ideal - it's kind of hacked together at the moment; especially because we have two different styles. I think once we settle on the specifics it can be re-written entirely.

", "created_at":"2015-01-23T09:46:00.901Z", "title":"Translations frequently broken", "url":"/t/translations-frequently-broken/22546/27", @@ -386,7 +386,7 @@ export default { }, { "id":94555, - "cooked":"

I don't know how to pronounce that in English, but this makes me think of the French word \"disquette\" (floppy disk) \"smile\"

", + "cooked":"

I don't know how to pronounce that in English, but this makes me think of the French word \"disquette\" (floppy disk)

", "created_at":"2015-01-23T08:17:31.700Z", "title":"Introducing Discette - a minimal ember-cli front end to Discourse", "url":"/t/introducing-discette-a-minimal-ember-cli-front-end-to-discourse/24321/3", @@ -636,7 +636,7 @@ export default { }, { "id":94521, - "cooked":"\n\n

Yeah probably.

\n\n\n\n

Definitely a good idea. We have seen some eye melting color schemes people have picked for categories.. Much less subcategories.

\n\n\n\n

Sure try http://talk.folksy.com -- it's still too much color in boxes. Particularly anywhere a bunch of categories are displayed together, which is a lot of places considering the topic list is the main form of nav, both on the homepage default of latest and in suggested topics at the bottom of every topic...

\n\n

", + "cooked":"

@techapj fixed this for 1.2.

", "created_at":"2015-01-23T02:58:27.451Z", "title":"The end of Clown Vomit, or, simplified category styles", "url":"/t/the-end-of-clown-vomit-or-simplified-category-styles/24249/57", @@ -886,7 +886,7 @@ export default { }, { "id":94515, - "cooked":"

Liked just for the word \"Discettes\" which is adorable \"heart_eyes\"

", + "cooked":"

I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.

", "created_at":"2015-01-23T02:38:29.185Z", "title":"Introducing Discette - a minimal ember-cli front end to Discourse", "url":"/t/introducing-discette-a-minimal-ember-cli-front-end-to-discourse/24321/2", @@ -936,7 +936,7 @@ export default { }, { "id":94514, - "cooked":"\n\n

This is a good idea, are the documents public web URLs? Perhaps we could help build this onebox if so.

\n\n\n\n

Hmm. I suspect this could be done via the API. Query all new topics (assuming older topics are already synced), and for those with a certain URL within the topic (first post only? All posts?) ping those URLs.

\n\n

This could potentially be done with a webhook on save on the Discourse side.

\n\n

Let us know how we can help, very interested in public projects like this.

", + "cooked":"

I would worry about getting your expenses down to $5 per month, that seems more likely over time as hosting for Docker compliant sites gets cheaper.

", "created_at":"2015-01-23T02:37:39.518Z", "title":"How to do \"Object Oriented Discussion\" through Oneboxes?", "url":"/t/how-to-do-object-oriented-discussion-through-oneboxes/24328/2", diff --git a/test/javascripts/fixtures/topic.js.es6 b/test/javascripts/fixtures/topic.js.es6 index 644b31a262..7031b394df 100644 --- a/test/javascripts/fixtures/topic.js.es6 +++ b/test/javascripts/fixtures/topic.js.es6 @@ -1,4 +1,4 @@ /*jshint maxlen:10000000 */ -export default {"/t/280/1.json": {"post_stream":{"posts":[{"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","uploaded_avatar_id":5697,"created_at":"2013-02-05T21:29:00.280Z","cooked":"

Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?

","post_number":1,"post_type":1,"updated_at":"2013-02-05T21:29:00.280Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":25,"incoming_link_count":314,"reads":475,"score":1702.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Uwe Keim","primary_group_name":null,"version":1,"can_edit":true,"can_delete":false,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/language-mirrors/2378/2","internal":true,"reflection":true,"title":"Language mirrors","clicks":3},{"url":"https://meta.discourse.org/t/translation-workflow/6102","internal":true,"reflection":true,"title":"Translation workflow","clicks":2},{"url":"https://meta.discourse.org/t/solving-xda-developer-style-forums/4368/4","internal":true,"reflection":true,"title":"Solving XDA-Developer style forums","clicks":2},{"url":"https://meta.discourse.org/t/comrades-lets-join-our-efforts-on-ukrainian-and-russian-translations/4403/5","internal":true,"reflection":true,"title":"Comrades let's join our efforts on ukrainian and russian translations","clicks":1},{"url":"https://meta.discourse.org/t/bookmark-last-read-sometimes-doesn-t-go-to-the-end-of-a-topic/4825/9","internal":true,"reflection":true,"title":"Bookmark/last read sometimes doesn't go to the end of a topic","clicks":0},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/5","internal":true,"reflection":true,"title":"Roadplan for Discourse 2013","clicks":0}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":419,"name":"Tim Stone","username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181,"created_at":"2013-02-05T21:32:47.649Z","cooked":"

The application strings are externalized, so localization should be entirely possible with enough translation effort.

","post_number":2,"post_type":1,"updated_at":"2013-02-06T10:15:27.965Z","like_count":4,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":27,"incoming_link_count":16,"reads":460,"score":308.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Tim Stone","primary_group_name":null,"version":2,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales/en.yml","internal":false,"reflection":false,"clicks":118}],"read":true,"user_title":"Great contributor","actions_summary":[{"id":2,"count":4,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":9,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":1060,"name":"Jeff Atwood","username":"codinghorror","avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","uploaded_avatar_id":5297,"created_at":"2013-02-06T02:26:24.922Z","cooked":"

Yep, all strings are going through a lookup table.*

\n\n

master/config/locales

\n\n

So you could replace that lookup table with the \"de\" one to get German.

\n\n

* we didn't get all the strings into the lookup table for launch, but the vast, vast majority of them are and the ones that are not, we will fix as we go!

","post_number":3,"post_type":1,"updated_at":"2014-02-24T05:23:39.211Z","like_count":4,"reply_count":3,"reply_to_post_number":null,"quote_count":0,"avg_time":33,"incoming_link_count":5,"reads":449,"score":191.45,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Jeff Atwood","primary_group_name":"discourse","version":4,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales","internal":false,"reflection":false,"title":"discourse/config/locales at master · discourse/discourse · GitHub","clicks":62},{"url":"https://meta.discourse.org/t/github-onebox-rendering-issue/7616","internal":true,"reflection":true,"title":"GitHub OneBox Rendering Issue","clicks":0}],"read":true,"user_title":"co-founder","actions_summary":[{"id":2,"count":4,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":32,"hidden":false,"hidden_reason_id":null,"trust_level":3,"deleted_at":null,"user_deleted":false,"edit_reason":"","can_view_edit_history":true,"wiki":false},{"id":3623,"name":"Shade","username":"shade","avatar_template":"/user_avatar/meta.discourse.org/shade/{size}/8306.png","uploaded_avatar_id":8306,"created_at":"2013-02-07T12:55:33.129Z","cooked":"

Is it a coincidence that the strings file is 1337 lines long? \"smiley\"

","post_number":4,"post_type":1,"updated_at":"2013-02-07T12:55:33.129Z","like_count":7,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":20,"incoming_link_count":15,"reads":401,"score":291.2,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Shade","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/hi-support-chinese/4393/6","internal":true,"reflection":true,"title":"Hi, support Chinese?","clicks":0}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":7,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1808,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3651,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:02:07.869Z","cooked":"

\n\n

The problem I see here is that this file is likely two grow and change massively over the next couple months, and tracking these changes in order to keep a localized file up to date is going to be a bitch.

\n\n

I wonder where there is a tool that can compare two yml structures and point out which nodes are missing? That would help keep track of new strings.

\n\n

Re keeping track of changed strings, @codinghorror I found this very interesting: http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders if plain English placeholders were used, any change in strings would lead to a new node in the yml file, making keeping the translation up to date easier. Maybe worth thinking about in the future.

","post_number":5,"post_type":1,"updated_at":"2013-02-07T14:05:42.328Z","like_count":2,"reply_count":2,"reply_to_post_number":3,"quote_count":1,"avg_time":22,"incoming_link_count":10,"reads":386,"score":213.3,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders","internal":false,"reflection":false,"title":"internationalization - Why do people use plain english as translation placeholders? - Stack Overflow","clicks":63}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":2,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3654,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:05:39.910Z","cooked":"

Yes, I really like the concept of fuzzy matching for localization, perhaps you can chase up alex sexton he was meaning to upload a localization tool for this kind of stuff.

\n\n

Also, I am a big fan of ICU message format, but it is not the \"Rails way (tm)\".

","post_number":6,"post_type":1,"updated_at":"2013-02-07T14:05:39.910Z","like_count":1,"reply_count":1,"reply_to_post_number":5,"quote_count":0,"avg_time":17,"incoming_link_count":4,"reads":329,"score":106.65,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/SlexAxton/messageformat.js","internal":false,"reflection":false,"title":"SlexAxton/messageformat.js · GitHub","clicks":46},{"url":"https://github.com/SlexAxton","internal":false,"reflection":false,"title":"SlexAxton (Alex Sexton) · GitHub","clicks":10}],"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3655,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:08:17.493Z","cooked":"

Looks interesting, I'll take a peek.

\n\n

As said on dev, the best tool I can see in terms of giving translators a proper interface and quality control would be something like GlotPress. It's based on the PO messages format (is that somehow related to ICU?) but looks pretty great.

\n\n

\n\n

I'm not familiar with the term in this context, you mean keeping the English version in the code base (instead of a generic code like message_error_nametooshort ?)

","post_number":7,"post_type":1,"updated_at":"2013-02-07T14:12:02.965Z","like_count":1,"reply_count":1,"reply_to_post_number":6,"quote_count":1,"avg_time":16,"incoming_link_count":0,"reads":326,"score":86.0,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://translate.wordpress.org/projects/bbpress/dev","internal":false,"reflection":false,"title":"WordPress › Development < GlotPress","clicks":16}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3658,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:12:22.582Z","cooked":"

ICU Message format is basically Gettext on steroids, Gettext has been around for so many years and actually works pretty well, being super prevalent in Linux.

\n\n

Trouble is you need a fuzzy matcher for translators if you are going to store stuff like mf.compile( 'This is a message.' ) in source, one letter change and all your translators need to validate it.

","post_number":8,"post_type":1,"updated_at":"2013-02-07T14:12:22.582Z","like_count":1,"reply_count":1,"reply_to_post_number":7,"quote_count":0,"avg_time":11,"incoming_link_count":2,"reads":296,"score":89.75,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/what-i-love-about-wordpress-plugins/5697","internal":true,"reflection":true,"title":"What I love about WordPress plugins","clicks":0}],"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3660,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:14:12.666Z","cooked":"

\n\n

Yeah, that's why I've always been a friend of message_error_nametooshort placeholders, until I asked the SO question linked above. The accepted answer makes a good argument against those placeholders: you want translations to break even on small changes in the English original because the translations will probably need to reflect the change, too. Maybe that's not the case right now as new stuff is being checked in pretty much every couple of hours, but in the long run, it'll be overwhelmingly true.

","post_number":9,"post_type":1,"updated_at":"2013-02-07T14:18:09.569Z","like_count":1,"reply_count":1,"reply_to_post_number":8,"quote_count":1,"avg_time":10,"incoming_link_count":0,"reads":293,"score":79.1,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3667,"name":"Tim Stone","username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181,"created_at":"2013-02-07T14:25:16.859Z","cooked":"

Hmm...You could theoretically also build something into the development process that would monitor changes to the English locale file and make a translator-friendly list of changes between versions.

","post_number":10,"post_type":1,"updated_at":"2013-02-07T14:25:16.859Z","like_count":1,"reply_count":1,"reply_to_post_number":9,"quote_count":0,"avg_time":7,"incoming_link_count":0,"reads":275,"score":75.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Tim Stone","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"Great contributor","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":9,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3673,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:30:21.401Z","cooked":"

Yeah, totally, also we could build tools for dev that make introducing string less annoying and make it possible to garbage collect old unused strings, I hate trudging through that file.

","post_number":11,"post_type":1,"updated_at":"2013-02-07T14:30:21.401Z","like_count":1,"reply_count":1,"reply_to_post_number":10,"quote_count":0,"avg_time":7,"incoming_link_count":1,"reads":273,"score":79.95,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"co-founder","reply_to_user":{"username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3675,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:33:38.280Z","cooked":"

\n\n

As said, I'd look into whether WP's tools can't be reused for this with some tweaking. They seem to be able to scan a code base for new strings, and make them available automatically to translators.

\n\n

They're PHP based which isn't ideal, but it looks like they've done a crapload of work to take the hassle out of translations.

","post_number":12,"post_type":1,"updated_at":"2013-02-07T14:34:39.910Z","like_count":1,"reply_count":1,"reply_to_post_number":11,"quote_count":1,"avg_time":7,"incoming_link_count":2,"reads":273,"score":84.95,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3690,"name":"Valts","username":"Vilx","avatar_template":"/user_avatar/meta.discourse.org/vilx/{size}/7299.png","uploaded_avatar_id":7299,"created_at":"2013-02-07T15:05:35.867Z","cooked":"

This site looks so nice with all the little tweaks like \"10 minutes ago\" instead of simply time, etc - I wonder if there will also be support for proper pluralization in other languages? That's a pretty hard task though, I don't think I've ever seen a website that has done that. But it would be awesome.

","post_number":13,"post_type":1,"updated_at":"2013-02-07T15:05:35.867Z","like_count":3,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":7,"incoming_link_count":11,"reads":290,"score":158.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Valts","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1216,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3925,"name":"Eric Kidd","username":"emk","avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","uploaded_avatar_id":8400,"created_at":"2013-02-07T19:37:06.194Z","cooked":"

\n\n

I've had pretty decent luck using Localeapp to localize Rails applications:

\n\n

http://www.localeapp.com/

\n\n

The developer workflow took me about an hour to really get used to, and there were a few minor glitches. But the non-technical translators had very few problems. One limitation: It insists on rewriting all those yaml files full of strings.

\n\n

Anyway, it's worth a look, and it's free for open source, if I recall correctly. Certainly easier than doing a whole bunch of toolsmithing from scratch.

","post_number":14,"post_type":1,"updated_at":"2013-02-07T19:37:06.194Z","like_count":3,"reply_count":1,"reply_to_post_number":12,"quote_count":1,"avg_time":9,"incoming_link_count":0,"reads":283,"score":137.05,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Eric Kidd","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://www.localeapp.com/","internal":false,"reflection":false,"title":"Easy localization for Rails apps | Locale","clicks":69}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1860,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3938,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T19:52:13.748Z","cooked":"

\n\n

Ohhh. Looking sexy. droool

","post_number":15,"post_type":1,"updated_at":"2013-02-07T19:52:13.748Z","like_count":1,"reply_count":1,"reply_to_post_number":14,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":260,"score":72.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3982,"name":"Eric Kidd","username":"emk","avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","uploaded_avatar_id":8400,"created_at":"2013-02-07T20:52:22.454Z","cooked":"

\n\n

Yeah, it's pretty. \"smile\" But there were still some rough edges as of a few months ago.

\n\n

Whether or not those rough edges are a deal-breaker will probably depends on whether or not localization is already a source of acute pain. If you're already hurting, Localeapp is a pretty useful tool, especially when it comes to enlisting non-technical translators.

\n\n

But it does require changing how you work with text, and adding one new tool to the mix. So for projects that just don't want to know about non-English languages, it's not yet seamless the way Unicode is these days.

\n\n

(Sweet forum software, by the way. I was just testing out Egyptian hieroglyphics on the test server, because they're well off the Basic Multilingual Plane, and tend to flush Unicode bugs. Everything worked flawlessly.)

","post_number":16,"post_type":1,"updated_at":"2013-02-07T20:52:22.454Z","like_count":1,"reply_count":1,"reply_to_post_number":15,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":254,"score":71.15,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Eric Kidd","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1860,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3989,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T21:04:15.405Z","cooked":"

\n\n

Interesting, thanks for the insight. I don't think localization is seriously on their table right now, there's likely to be many other things on the table before it... but it will become an issue sooner or later.

","post_number":17,"post_type":1,"updated_at":"2013-02-07T21:04:15.405Z","like_count":1,"reply_count":2,"reply_to_post_number":16,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":255,"score":76.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3996,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T21:12:06.575Z","cooked":"

I had an idea ... what if in dev mode, you could right-click on a page and get access to all the translations on the page, make your edits and have it refreshed live.

\n\n

I think it would be awesome, very doable technically.

","post_number":18,"post_type":1,"updated_at":"2013-02-07T21:12:06.575Z","like_count":7,"reply_count":2,"reply_to_post_number":17,"quote_count":0,"avg_time":8,"incoming_link_count":0,"reads":264,"score":168.2,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":7,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":4009,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T21:18:47.422Z","cooked":"

That would be fricking cool. There'd still be some leftovers (like error messages that normally never show up, etc.) but you could corral those up on a specific page.

\n\n

It could have a dropdown giving you all the languages that you have a .yml for in the locale directory, and write the changes into the one selected. I'm sure people would love it.

","post_number":19,"post_type":1,"updated_at":"2013-02-07T21:22:10.692Z","like_count":1,"reply_count":0,"reply_to_post_number":18,"quote_count":0,"avg_time":8,"incoming_link_count":1,"reads":241,"score":68.6,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"reply_to_user":{"username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":4012,"name":"Marco Ceppi","username":"marcoceppi","avatar_template":"/user_avatar/meta.discourse.org/marcoceppi/{size}/6552.png","uploaded_avatar_id":6552,"created_at":"2013-02-07T21:22:46.376Z","cooked":"

If you use gettext format you could leverage Launchpad translations and the community behind it.

","post_number":20,"post_type":1,"updated_at":"2013-02-07T21:22:46.376Z","like_count":1,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":9,"incoming_link_count":2,"reads":244,"score":74.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Marco Ceppi","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://translations.launchpad.net/","internal":false,"reflection":false,"title":"Launchpad Translations","clicks":13}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":761,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}],"stream":[398,419,1060,3623,3651,3654,3655,3658,3660,3667,3673,3675,3690,3925,3938,3982,3989,3996,4009,4012],"gaps":{"before":{"20706":[20125]},"after":{}}},"id":280,"title":"Internationalization / localization","fancy_title":"Internationalization / localization","posts_count":103,"created_at":"2013-02-05T21:29:00.174Z","views":5211,"reply_count":67,"participant_count":40,"like_count":135,"last_posted_at":"2015-03-04T15:07:10.487Z","visible":true,"closed":false,"archived":false,"has_summary":true,"archetype":"regular","slug":"internationalization-localization","category_id":2,"word_count":6198,"deleted_at":null,"draft":null,"draft_key":"topic_280","draft_sequence":4,"posted":true,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":255,"username":"uwe_keim","uploaded_avatar_id":5697,"avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png"},"last_poster":{"id":14091,"username":"Luciano_Fantuzzi","uploaded_avatar_id":39484,"avatar_template":"/user_avatar/meta.discourse.org/luciano_fantuzzi/{size}/39484.png"},"participants":[{"id":212,"username":"alxndr","uploaded_avatar_id":5619,"avatar_template":"/user_avatar/meta.discourse.org/alxndr/{size}/5619.png","post_count":11},{"id":1,"username":"sam","uploaded_avatar_id":5243,"avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","post_count":11},{"id":7,"username":"pekka","uploaded_avatar_id":5253,"avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","post_count":8},{"id":461,"username":"kuba","uploaded_avatar_id":6049,"avatar_template":"/user_avatar/meta.discourse.org/kuba/{size}/6049.png","post_count":7},{"id":2995,"username":"tattoo","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/tattoo/{size}/3.png","post_count":6},{"id":2540,"username":"jgourdon","uploaded_avatar_id":9537,"avatar_template":"/user_avatar/meta.discourse.org/jgourdon/{size}/9537.png","post_count":5},{"id":1860,"username":"emk","uploaded_avatar_id":8400,"avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","post_count":4},{"id":1275,"username":"dacap","uploaded_avatar_id":7401,"avatar_template":"/user_avatar/meta.discourse.org/dacap/{size}/7401.png","post_count":4},{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/user_avatar/meta.discourse.org/eviltrout/{size}/5275.png","post_count":4},{"id":3704,"username":"mojzis","uploaded_avatar_id":31201,"avatar_template":"/user_avatar/meta.discourse.org/mojzis/{size}/31201.png","post_count":3},{"id":3190,"username":"gururea","uploaded_avatar_id":10663,"avatar_template":"/user_avatar/meta.discourse.org/gururea/{size}/10663.png","post_count":3},{"id":1895,"username":"maciek","uploaded_avatar_id":8463,"avatar_template":"/user_avatar/meta.discourse.org/maciek/{size}/8463.png","post_count":3},{"id":22,"username":"splattne","uploaded_avatar_id":5280,"avatar_template":"/user_avatar/meta.discourse.org/splattne/{size}/5280.png","post_count":2},{"id":1979,"username":"Superuser","uploaded_avatar_id":8604,"avatar_template":"/user_avatar/meta.discourse.org/superuser/{size}/8604.png","post_count":2},{"id":3818,"username":"Tudor","uploaded_avatar_id":11675,"avatar_template":"/user_avatar/meta.discourse.org/tudor/{size}/11675.png","post_count":2},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","post_count":2},{"id":3620,"username":"potthast","uploaded_avatar_id":11363,"avatar_template":"/user_avatar/meta.discourse.org/potthast/{size}/11363.png","post_count":2},{"id":9,"username":"tms","uploaded_avatar_id":40181,"avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","post_count":2},{"id":14091,"username":"Luciano_Fantuzzi","uploaded_avatar_id":39484,"avatar_template":"/user_avatar/meta.discourse.org/luciano_fantuzzi/{size}/39484.png","post_count":1},{"id":255,"username":"uwe_keim","uploaded_avatar_id":5697,"avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","post_count":1},{"id":9006,"username":"berk","uploaded_avatar_id":19348,"avatar_template":"/user_avatar/meta.discourse.org/berk/{size}/19348.png","post_count":1},{"id":754,"username":"danneu","uploaded_avatar_id":6540,"avatar_template":"/user_avatar/meta.discourse.org/danneu/{size}/6540.png","post_count":1},{"id":761,"username":"marcoceppi","uploaded_avatar_id":6552,"avatar_template":"/user_avatar/meta.discourse.org/marcoceppi/{size}/6552.png","post_count":1},{"id":2753,"username":"mikl","uploaded_avatar_id":9918,"avatar_template":"/user_avatar/meta.discourse.org/mikl/{size}/9918.png","post_count":1}],"suggested_topics":[{"id":27331,"title":"Polls are still very buggy","fancy_title":"Polls are still very buggy","slug":"polls-are-still-very-buggy","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":"/uploads/default/_optimized/cd1/b8c/c162528887_690x401.png","created_at":"2015-04-08T09:51:00.357Z","last_posted_at":"2015-04-08T15:59:16.258Z","bumped":true,"bumped_at":"2015-04-08T16:05:09.842Z","unseen":false,"last_read_post_number":3,"unread":0,"new_posts":1,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":11,"views":55,"category_id":1},{"id":27343,"title":"Mobile theme doesn't show last activity time for topics on category page","fancy_title":"Mobile theme doesn’t show last activity time for topics on category page","slug":"mobile-theme-doesnt-show-last-activity-time-for-topics-on-category-page","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":"/uploads/default/_optimized/13e/25c/bd30b466be_281x500.png","created_at":"2015-04-08T14:20:51.177Z","last_posted_at":"2015-04-08T15:40:30.037Z","bumped":true,"bumped_at":"2015-04-08T15:40:30.037Z","unseen":false,"last_read_post_number":2,"unread":0,"new_posts":2,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":3,"views":23,"category_id":9},{"id":27346,"title":"Reply+{messagekey}@... optionaly in header \"from\" in addition to \"reply-to\"","fancy_title":"Reply+{messagekey}@… optionaly in header “from” in addition to “reply-to”","slug":"reply-messagekey-optionaly-in-header-from-in-addition-to-reply-to","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2015-04-08T16:05:13.103Z","last_posted_at":"2015-04-08T16:05:13.415Z","bumped":true,"bumped_at":"2015-04-08T16:05:13.415Z","unseen":true,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":8,"category_id":2},{"id":19670,"title":"Parsing (Oneboxing) IMDB links","fancy_title":"Parsing (Oneboxing) IMDB links","slug":"parsing-oneboxing-imdb-links","posts_count":8,"reply_count":1,"highest_post_number":8,"image_url":null,"created_at":"2014-09-05T07:19:26.161Z","last_posted_at":"2015-04-07T09:21:21.570Z","bumped":true,"bumped_at":"2015-04-07T09:21:21.570Z","unseen":false,"last_read_post_number":8,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":4,"views":253,"category_id":2},{"id":7512,"title":"Support for Piwik Analytics as an alternative to Google Analytics","fancy_title":"Support for Piwik Analytics as an alternative to Google Analytics","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","posts_count":53,"reply_count":41,"highest_post_number":65,"image_url":"/plugins/emoji/images/smile.png","created_at":"2013-06-16T01:32:30.596Z","last_posted_at":"2015-02-22T13:46:26.845Z","bumped":true,"bumped_at":"2015-02-22T13:46:26.845Z","unseen":false,"last_read_post_number":65,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":62,"views":1877,"category_id":2},{"id":25480,"title":"CSS admin-contents reloaded","fancy_title":"CSS admin-contents reloaded","slug":"css-admin-contents-reloaded","posts_count":22,"reply_count":15,"highest_post_number":22,"image_url":null,"created_at":"2015-02-21T12:15:57.707Z","last_posted_at":"2015-03-02T23:24:18.899Z","bumped":true,"bumped_at":"2015-03-02T23:24:18.899Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":21,"views":185,"category_id":2},{"id":26576,"title":"Badge timestamp should be the time the badge was granted?","fancy_title":"Badge timestamp should be the time the badge was granted?","slug":"badge-timestamp-should-be-the-time-the-badge-was-granted","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-03-20T13:22:08.266Z","last_posted_at":"2015-03-21T00:33:52.243Z","bumped":true,"bumped_at":"2015-03-21T00:33:52.243Z","unseen":false,"last_read_post_number":1,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":1,"bookmarked":false,"liked":false,"archetype":"regular","like_count":9,"views":87,"category_id":2}],"links":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales/en.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":118,"user_id":9,"domain":"github.com"},{"url":"http://www.localeapp.com/","title":"Easy localization for Rails apps | Locale","fancy_title":null,"internal":false,"reflection":false,"clicks":69,"user_id":1860,"domain":"www.localeapp.com"},{"url":"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders","title":"internationalization - Why do people use plain english as translation placeholders? - Stack Overflow","fancy_title":null,"internal":false,"reflection":false,"clicks":63,"user_id":7,"domain":"stackoverflow.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales","title":"discourse/config/locales at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":62,"user_id":32,"domain":"github.com"},{"url":"https://github.com/SlexAxton/messageformat.js","title":"SlexAxton/messageformat.js · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":46,"user_id":1,"domain":"github.com"},{"url":"http://www.localeapp.com/projects/1537/translations?utf8=%E2%9C%93&search=source_code","title":"langforums | Locale","fancy_title":null,"internal":false,"reflection":false,"clicks":25,"user_id":1860,"domain":"www.localeapp.com"},{"url":"https://translations.launchpad.net/","title":"Launchpad Translations","fancy_title":null,"internal":false,"reflection":false,"clicks":23,"user_id":761,"domain":"translations.launchpad.net"},{"url":"https://www.transifex.com/","title":"Transifex - Continuous Localization Platform","fancy_title":null,"internal":false,"reflection":false,"clicks":22,"user_id":1979,"domain":"www.transifex.com"},{"url":"https://github.com/berk/tr8n","title":"berk/tr8n · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":22,"user_id":1,"domain":"github.com"},{"url":"http://translate.wordpress.org/projects/bbpress/dev","title":"WordPress › Development < GlotPress","fancy_title":null,"internal":false,"reflection":false,"clicks":16,"user_id":7,"domain":"translate.wordpress.org"},{"url":"http://weblate.org","title":"Weblate - web-based translation","fancy_title":null,"internal":false,"reflection":false,"clicks":15,"user_id":2316,"domain":"weblate.org"},{"url":"https://github.com/discourse/discourse/tree/master/config/locales","title":"discourse/config/locales at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":14,"user_id":19,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/pull/493","title":"Danish translation. by mikl · Pull Request #493 · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":12,"user_id":2753,"domain":"github.com"},{"url":"https://github.com/SlexAxton","title":"SlexAxton (Alex Sexton) · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":10,"user_id":1,"domain":"github.com"},{"url":"https://github.com/gururea/discourse/tree/master/config/locales","title":"discourse/config/locales at master · gururea/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":9,"user_id":3190,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.en.yml#L691","title":"discourse/config/locales/client.en.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":461,"domain":"github.com"},{"url":"https://github.com/dacap/discourse/tree/spanish","title":"dacap/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":1275,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.nl.yml","title":"discourse/config/locales/client.nl.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":461,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/commit/c5761eae8afe37e20cec0d0f9d14b85b6e585bda","title":"Support for Simplified Chinese thanks to tangramor · c5761ea · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":212,"domain":"github.com"},{"url":"http://tr8n.github.com/","title":"tr8n","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":212,"domain":"tr8n.github.com"},{"url":"http://www.getlocalization.com/","title":"Crowdsourced, Social and Collaborative App & Website Translation - Get Localization","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":22,"domain":"www.getlocalization.com"},{"url":"http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/","title":"Discourse as Your First Rails App","fancy_title":null,"internal":false,"reflection":false,"clicks":5,"user_id":1995,"domain":"blog.discourse.org"},{"url":"https://github.com/alxndr/discourse/blob/i18n-chinese/config/locales/server.zh.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":5,"user_id":212,"domain":"github.com"},{"url":"http://translate.sourceforge.net/wiki/virtaal/index","title":"Easy-to-use and powerful offline translation tool | Virtaal","fancy_title":null,"internal":false,"reflection":false,"clicks":4,"user_id":1979,"domain":"translate.sourceforge.net"},{"url":"https://poeditor.com/","title":"POEditor - online software localization tool","fancy_title":null,"internal":false,"reflection":false,"clicks":4,"user_id":1979,"domain":"poeditor.com"},{"url":"http://en.lichess.org/@/Hellball","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":1979,"domain":"en.lichess.org"},{"url":"http://en.wikipedia.org/wiki/T%E2%80%93V_distinction","title":"T–V distinction - Wikipedia, the free encyclopedia","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":3620,"domain":"en.wikipedia.org"},{"url":"http://www.slideshare.net/HeatherRivers/linguistic-potluck-crowdsourcing-localization-with-rails","title":"Linguistic Potluck: Crowdsourcing localization with Rails","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":212,"domain":"www.slideshare.net"},{"url":"https://meta.discourse.org/t/language-mirrors/2378/2","title":"Language mirrors","fancy_title":null,"internal":true,"reflection":true,"clicks":3,"user_id":32,"domain":"meta.discourse.org"},{"url":"http://www.madanalogy.com/2012/06/rails-i18n-translations-in-yaml.html","title":"Mad Analogy: Rails i18n translations in Yaml: translation tool support","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":3190,"domain":"www.madanalogy.com"},{"url":"https://github.com/tr8n","title":"Translation Exchange · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":9006,"domain":"github.com"},{"url":"http://pootle.locamotion.org/","title":"Main | Pootle Demo","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":3190,"domain":"pootle.locamotion.org"},{"url":"http://www.youtube.com/watch?v=MqqdzJ98q7s","title":"GoGaRuCo 2012 - Linguistic Potluck: Crowdsourcing Localization in Rails by Heather Rivers - YouTube","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":212,"domain":"www.youtube.com"},{"url":"https://meta.discourse.org/t/translation-workflow/6102","title":"Translation workflow","fancy_title":null,"internal":true,"reflection":true,"clicks":2,"user_id":4702,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/solving-xda-developer-style-forums/4368/4","title":"Solving XDA-Developer style forums","fancy_title":null,"internal":true,"reflection":true,"clicks":2,"user_id":639,"domain":"meta.discourse.org"},{"url":"https://tr8nhub.com","title":"TranslationExchange","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":9006,"domain":"tr8nhub.com"},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/3","title":"Roadplan for Discourse 2013","fancy_title":null,"internal":true,"reflection":true,"clicks":1,"user_id":2540,"domain":"meta.discourse.org"},{"url":"http://sugarjs.com/dates#date_locales","title":"Dates - Sugar","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":461,"domain":"sugarjs.com"},{"url":"http://blog.discourse.org/2013/03/localizing-discourse/","title":"Localizing Discourse","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"blog.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/app/assets/javascripts/locales/date_locales.js","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":461,"domain":"github.com"},{"url":"http://transifex.com/projects/p/discourse-pt-br/","title":"Discourse-Translations-Project localization","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"transifex.com"},{"url":"https://github.com/discourse/discourse/issues/279","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"github.com"},{"url":"https://meta.discourse.org/t/comrades-lets-join-our-efforts-on-ukrainian-and-russian-translations/4403/5","title":"Comrades let's join our efforts on ukrainian and russian translations","fancy_title":null,"internal":true,"reflection":true,"clicks":1,"user_id":3417,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-workflow/6102/6","title":"Translation workflow","fancy_title":null,"internal":true,"reflection":false,"clicks":0,"user_id":1995,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/bookmark-last-read-sometimes-doesn-t-go-to-the-end-of-a-topic/4825/9","title":"Bookmark/last read sometimes doesn't go to the end of a topic","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":3681,"domain":"meta.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.de.yml","title":"discourse/config/locales/client.de.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":2,"domain":"github.com"},{"url":"https://meta.discourse.org/t/what-i-love-about-wordpress-plugins/5697","title":"What I love about WordPress plugins","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":1,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/github-onebox-rendering-issue/7616","title":"GitHub OneBox Rendering Issue","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":5372,"domain":"meta.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/server.de.yml","title":"discourse/config/locales/server.de.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":2,"domain":"github.com"},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/5","title":"Roadplan for Discourse 2013","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":32,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-tools-transifex-localeapp/7763","title":"Translation Tools: Transifex? Localeapp?","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":2,"domain":"meta.discourse.org"},{"url":"http://guides.rubyonrails.org/i18n.html#the-public-i18n-api","title":"Rails Internationalization (I18n) API — Ruby on Rails Guides","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":1895,"domain":"guides.rubyonrails.org"},{"url":"https://meta.discourse.org/t/hi-support-chinese/4393/6","title":"Hi, support Chinese?","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":2014,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-tools-transifex-localeapp/7763/41","title":"Translation Tools: Transifex? Localeapp?","fancy_title":null,"internal":true,"reflection":false,"clicks":0,"user_id":6626,"domain":"meta.discourse.org"}],"notification_level":2,"notifications_reason_id":4,"can_move_posts":true,"can_edit":true,"can_delete":true,"can_recover":true,"can_remove_allowed_users":true,"can_invite_to":true,"can_create_post":true,"can_reply_as_new_topic":true,"can_flag_topic":true},"highest_post_number":10,"last_read_post_number":10,"deleted_by":null,"has_deleted":true,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":true},{"id":7,"count":0,"hidden":false,"can_act":true},{"id":8,"count":0,"hidden":false,"can_act":true}],"chunk_size":20,"bookmarked":false,"tags":null}, -"/t/28830/1.json": {"post_stream":{"posts":[{"id":118591,"name":"spends too much time on WTDWTF","username":"RaceProUK","avatar_template":"/user_avatar/meta.discourse.org/raceprouk/{size}/40071.png","uploaded_avatar_id":40071,"created_at":"2015-05-14T20:18:17.954Z","cooked":"

Normally, actions such as Liking are rate-limited, and when you hit the limit, you get a message telling you you've hit the limit. However, in 1.3.0beta9, it seems those popups are no longer appearing.

\n\n

Edit: Possibly linked to this issue?

","post_number":1,"post_type":1,"updated_at":"2015-05-14T20:21:42.825Z","like_count":6,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":14,"reads":24,"score":224.6,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"spends too much time on WTDWTF","primary_group_name":null,"version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"https://meta.discourse.org/t/post-reply-on-different-topic-no-longer-works/28825","internal":true,"reflection":false,"title":"Post reply on different topic no longer works","clicks":6}],"read":true,"user_title":"Contributor","actions_summary":[{"id":2,"count":6,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":false,"admin":false,"staff":false,"user_id":14169,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":"","can_view_edit_history":true,"wiki":false},{"id":118597,"name":"Sam","username":"Yuun","avatar_template":"/letter_avatar/yuun/{size}/3_90a587a04512ff220ac26ec1465844c5.png","uploaded_avatar_id":null,"created_at":"2015-05-14T20:35:03.793Z","cooked":"

I'm seeing this issue as well. When you hit the rate limit, any further likes look like the forum is attempting and failing to apply them - the text saying 'you liked this' comes into place before quickly being removed.

\n\n

This makes it look (to the user) like the forum software is running into errors instead of said user hitting an intentional limit, which is a bit unfortunate.

","post_number":2,"post_type":1,"updated_at":"2015-05-14T20:35:03.793Z","like_count":0,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":6,"reads":22,"score":34.2,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"Sam","primary_group_name":null,"version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":false,"admin":false,"staff":false,"user_id":14795,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":118601,"name":"Kane York","username":"riking","avatar_template":"/user_avatar/meta.discourse.org/riking/{size}/40212.png","uploaded_avatar_id":40212,"created_at":"2015-05-14T21:05:19.837Z","cooked":"

I'm going to guess that the bootbox library got broken somehow?

","post_number":3,"post_type":1,"updated_at":"2015-05-14T21:05:19.837Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":14,"score":7.2,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"Kane York","primary_group_name":null,"version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":true,"user_title":"team summer intern 2014","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":false,"admin":false,"staff":false,"user_id":6626,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":118606,"name":"Jeff Atwood","username":"codinghorror","avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","uploaded_avatar_id":5297,"created_at":"2015-05-14T21:15:41.612Z","cooked":"

Yeah maybe another Ember 1.10 regression for @eviltrout ?

","post_number":4,"post_type":1,"updated_at":"2015-05-14T21:15:41.612Z","like_count":0,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":6,"reads":12,"score":31.6,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"Jeff Atwood","primary_group_name":"discourse","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":true,"user_title":"co-founder","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":true,"admin":true,"staff":true,"user_id":32,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":118612,"name":"TDWTF member","username":"Onyx","avatar_template":"/user_avatar/meta.discourse.org/onyx/{size}/33015.png","uploaded_avatar_id":33015,"created_at":"2015-05-14T21:23:09.562Z","cooked":"\n\n

You mean the popup box library, guessing by the name? Still shows up when you want to cancel a post, so it's not all popups it seems.

","post_number":5,"post_type":1,"updated_at":"2015-05-14T21:23:09.562Z","like_count":1,"reply_count":0,"reply_to_post_number":3,"quote_count":1,"avg_time":null,"incoming_link_count":0,"reads":11,"score":16.0,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"TDWTF member","primary_group_name":null,"version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":false,"admin":false,"staff":false,"user_id":10886,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}],"stream":[118591,118597,118601,118606,118612]},"id":28830,"title":"1.3.0beta9: No rate-limit popups","fancy_title":"1.3.0beta9: No rate-limit popups","posts_count":5,"created_at":"2015-05-14T20:18:17.877Z","views":38,"reply_count":1,"participant_count":5,"like_count":7,"last_posted_at":"2015-05-14T21:23:09.562Z","visible":true,"closed":false,"archived":false,"has_summary":false,"archetype":"regular","slug":"1-3-0beta9-no-rate-limit-popups","category_id":1,"word_count":198,"deleted_at":null,"pending_posts_count":0,"draft":null,"draft_key":"topic_28830","draft_sequence":null,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":14169,"username":"RaceProUK","uploaded_avatar_id":40071,"avatar_template":"/user_avatar/meta.discourse.org/raceprouk/{size}/40071.png"},"last_poster":{"id":10886,"username":"Onyx","uploaded_avatar_id":33015,"avatar_template":"/user_avatar/meta.discourse.org/onyx/{size}/33015.png"},"participants":[{"id":14795,"username":"Yuun","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/yuun/{size}/3_90a587a04512ff220ac26ec1465844c5.png","post_count":1},{"id":10886,"username":"Onyx","uploaded_avatar_id":33015,"avatar_template":"/user_avatar/meta.discourse.org/onyx/{size}/33015.png","post_count":1},{"id":14169,"username":"RaceProUK","uploaded_avatar_id":40071,"avatar_template":"/user_avatar/meta.discourse.org/raceprouk/{size}/40071.png","post_count":1},{"id":6626,"username":"riking","uploaded_avatar_id":40212,"avatar_template":"/user_avatar/meta.discourse.org/riking/{size}/40212.png","post_count":1},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","post_count":1}],"suggested_topics":[{"id":2890,"title":"Expanded quoted text not highlighting when text is formatted","fancy_title":"Expanded quoted text not highlighting when text is formatted","slug":"expanded-quoted-text-not-highlighting-when-text-is-formatted","posts_count":8,"reply_count":5,"highest_post_number":8,"image_url":null,"created_at":"2013-02-12T12:18:02.181Z","last_posted_at":"2013-02-14T15:59:40.014Z","bumped":true,"bumped_at":"2013-02-14T15:59:40.014Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":3,"views":361,"category_id":1},{"id":14213,"title":"Plugins not being parsed in correct javascript context when loaded for jobs","fancy_title":"Plugins not being parsed in correct javascript context when loaded for jobs","slug":"plugins-not-being-parsed-in-correct-javascript-context-when-loaded-for-jobs","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/plugins/emoji/images/frowning.png","created_at":"2014-03-27T23:57:00.974Z","last_posted_at":"2015-03-20T04:56:03.982Z","bumped":true,"bumped_at":"2015-03-20T04:56:03.982Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":156,"category_id":1},{"id":22544,"title":"Like count on profile off by one","fancy_title":"Like count on profile off by one","slug":"like-count-on-profile-off-by-one","posts_count":7,"reply_count":2,"highest_post_number":7,"image_url":null,"created_at":"2014-11-26T08:15:39.802Z","last_posted_at":"2014-11-27T07:23:37.638Z","bumped":true,"bumped_at":"2014-11-27T07:23:37.638Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":18,"views":192,"category_id":1},{"id":27670,"title":"Using back still shows unread indicator on the topic","fancy_title":"Using back still shows unread indicator on the topic","slug":"using-back-still-shows-unread-indicator-on-the-topic","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-04-16T23:21:42.739Z","last_posted_at":"2015-04-17T02:43:08.447Z","bumped":true,"bumped_at":"2015-04-17T02:43:08.447Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":1,"views":85,"category_id":1},{"id":26628,"title":"Embed blacklist selector is broken","fancy_title":"Embed blacklist selector is broken","slug":"embed-blacklist-selector-is-broken","posts_count":11,"reply_count":7,"highest_post_number":11,"image_url":null,"created_at":"2015-03-22T11:21:14.825Z","last_posted_at":"2015-04-20T09:11:38.999Z","bumped":true,"bumped_at":"2015-04-20T09:11:38.999Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":1,"views":247,"category_id":1},{"id":18027,"title":"Minor: delete/undelete needs a rate limit","fancy_title":"Minor: delete/undelete needs a rate limit","slug":"minor-delete-undelete-needs-a-rate-limit","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2014-07-25T02:51:41.158Z","last_posted_at":"2014-07-25T04:01:15.343Z","bumped":true,"bumped_at":"2014-07-25T11:06:46.213Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":1,"views":165,"category_id":1},{"id":17396,"title":"Bad Reply Key when pulling Autoforwarded Emails to Discourse","fancy_title":"Bad Reply Key when pulling Autoforwarded Emails to Discourse","slug":"bad-reply-key-when-pulling-autoforwarded-emails-to-discourse","posts_count":20,"reply_count":15,"highest_post_number":20,"image_url":null,"created_at":"2014-07-09T18:34:57.114Z","last_posted_at":"2014-10-21T15:08:50.441Z","bumped":true,"bumped_at":"2014-10-21T15:08:50.441Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":7,"views":542,"category_id":1}],"links":[{"url":"https://meta.discourse.org/t/post-reply-on-different-topic-no-longer-works/28825","title":"Post reply on different topic no longer works","fancy_title":null,"internal":true,"reflection":false,"clicks":6,"user_id":14169,"domain":"meta.discourse.org"}],"notification_level":1,"can_flag_topic":false},"highest_post_number":5,"deleted_by":null,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"chunk_size":20,"bookmarked":null,"tags":null}, +export default {"/t/280/1.json": {"post_stream":{"posts":[{"id":398,"name":"Uwe Keim","username":"uwe_keim","avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","uploaded_avatar_id":5697,"created_at":"2013-02-05T21:29:00.280Z","cooked":"

Any plans to support localization of UI elements, so that I (for example) could set up a completely German speaking forum?

","post_number":1,"post_type":1,"updated_at":"2013-02-05T21:29:00.280Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":25,"incoming_link_count":314,"reads":475,"score":1702.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Uwe Keim","primary_group_name":null,"version":1,"can_edit":true,"can_delete":false,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/language-mirrors/2378/2","internal":true,"reflection":true,"title":"Language mirrors","clicks":3},{"url":"https://meta.discourse.org/t/translation-workflow/6102","internal":true,"reflection":true,"title":"Translation workflow","clicks":2},{"url":"https://meta.discourse.org/t/solving-xda-developer-style-forums/4368/4","internal":true,"reflection":true,"title":"Solving XDA-Developer style forums","clicks":2},{"url":"https://meta.discourse.org/t/comrades-lets-join-our-efforts-on-ukrainian-and-russian-translations/4403/5","internal":true,"reflection":true,"title":"Comrades let's join our efforts on ukrainian and russian translations","clicks":1},{"url":"https://meta.discourse.org/t/bookmark-last-read-sometimes-doesn-t-go-to-the-end-of-a-topic/4825/9","internal":true,"reflection":true,"title":"Bookmark/last read sometimes doesn't go to the end of a topic","clicks":0},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/5","internal":true,"reflection":true,"title":"Roadplan for Discourse 2013","clicks":0}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":255,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":419,"name":"Tim Stone","username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181,"created_at":"2013-02-05T21:32:47.649Z","cooked":"

The application strings are externalized, so localization should be entirely possible with enough translation effort.

","post_number":2,"post_type":1,"updated_at":"2013-02-06T10:15:27.965Z","like_count":4,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":27,"incoming_link_count":16,"reads":460,"score":308.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Tim Stone","primary_group_name":null,"version":2,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales/en.yml","internal":false,"reflection":false,"clicks":118}],"read":true,"user_title":"Great contributor","actions_summary":[{"id":2,"count":4,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":9,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":1060,"name":"Jeff Atwood","username":"codinghorror","avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","uploaded_avatar_id":5297,"created_at":"2013-02-06T02:26:24.922Z","cooked":"

Yep, all strings are going through a lookup table.*

\n\n

master/config/locales

\n\n

So you could replace that lookup table with the \"de\" one to get German.

\n\n

* we didn't get all the strings into the lookup table for launch, but the vast, vast majority of them are and the ones that are not, we will fix as we go!

","post_number":3,"post_type":1,"updated_at":"2014-02-24T05:23:39.211Z","like_count":4,"reply_count":3,"reply_to_post_number":null,"quote_count":0,"avg_time":33,"incoming_link_count":5,"reads":449,"score":191.45,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Jeff Atwood","primary_group_name":"discourse","version":4,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales","internal":false,"reflection":false,"title":"discourse/config/locales at master · discourse/discourse · GitHub","clicks":62},{"url":"https://meta.discourse.org/t/github-onebox-rendering-issue/7616","internal":true,"reflection":true,"title":"GitHub OneBox Rendering Issue","clicks":0}],"read":true,"user_title":"co-founder","actions_summary":[{"id":2,"count":4,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":32,"hidden":false,"hidden_reason_id":null,"trust_level":3,"deleted_at":null,"user_deleted":false,"edit_reason":"","can_view_edit_history":true,"wiki":false},{"id":3623,"name":"Shade","username":"shade","avatar_template":"/user_avatar/meta.discourse.org/shade/{size}/8306.png","uploaded_avatar_id":8306,"created_at":"2013-02-07T12:55:33.129Z","cooked":"

Is it a coincidence that the strings file is 1337 lines long? \"smiley\"

","post_number":4,"post_type":1,"updated_at":"2013-02-07T12:55:33.129Z","like_count":7,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":20,"incoming_link_count":15,"reads":401,"score":291.2,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Shade","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/hi-support-chinese/4393/6","internal":true,"reflection":true,"title":"Hi, support Chinese?","clicks":0}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":7,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1808,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3651,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:02:07.869Z","cooked":"

\n\n

The problem I see here is that this file is likely two grow and change massively over the next couple months, and tracking these changes in order to keep a localized file up to date is going to be a bitch.

\n\n

I wonder where there is a tool that can compare two yml structures and point out which nodes are missing? That would help keep track of new strings.

\n\n

Re keeping track of changed strings, @codinghorror I found this very interesting: http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders if plain English placeholders were used, any change in strings would lead to a new node in the yml file, making keeping the translation up to date easier. Maybe worth thinking about in the future.

","post_number":5,"post_type":1,"updated_at":"2013-02-07T14:05:42.328Z","like_count":2,"reply_count":2,"reply_to_post_number":3,"quote_count":1,"avg_time":22,"incoming_link_count":10,"reads":386,"score":213.3,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders","internal":false,"reflection":false,"title":"internationalization - Why do people use plain english as translation placeholders? - Stack Overflow","clicks":63}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":2,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3654,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:05:39.910Z","cooked":"

Yes, I really like the concept of fuzzy matching for localization, perhaps you can chase up alex sexton he was meaning to upload a localization tool for this kind of stuff.

\n\n

Also, I am a big fan of ICU message format, but it is not the \"Rails way (tm)\".

","post_number":6,"post_type":1,"updated_at":"2013-02-07T14:05:39.910Z","like_count":1,"reply_count":1,"reply_to_post_number":5,"quote_count":0,"avg_time":17,"incoming_link_count":4,"reads":329,"score":106.65,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://github.com/SlexAxton/messageformat.js","internal":false,"reflection":false,"title":"SlexAxton/messageformat.js · GitHub","clicks":46},{"url":"https://github.com/SlexAxton","internal":false,"reflection":false,"title":"SlexAxton (Alex Sexton) · GitHub","clicks":10}],"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3655,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:08:17.493Z","cooked":"

Looks interesting, I'll take a peek.

\n\n

As said on dev, the best tool I can see in terms of giving translators a proper interface and quality control would be something like GlotPress. It's based on the PO messages format (is that somehow related to ICU?) but looks pretty great.

\n\n

\n\n

I'm not familiar with the term in this context, you mean keeping the English version in the code base (instead of a generic code like message_error_nametooshort ?)

","post_number":7,"post_type":1,"updated_at":"2013-02-07T14:12:02.965Z","like_count":1,"reply_count":1,"reply_to_post_number":6,"quote_count":1,"avg_time":16,"incoming_link_count":0,"reads":326,"score":86.0,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://translate.wordpress.org/projects/bbpress/dev","internal":false,"reflection":false,"title":"WordPress › Development < GlotPress","clicks":16}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3658,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:12:22.582Z","cooked":"

ICU Message format is basically Gettext on steroids, Gettext has been around for so many years and actually works pretty well, being super prevalent in Linux.

\n\n

Trouble is you need a fuzzy matcher for translators if you are going to store stuff like mf.compile( 'This is a message.' ) in source, one letter change and all your translators need to validate it.

","post_number":8,"post_type":1,"updated_at":"2013-02-07T14:12:22.582Z","like_count":1,"reply_count":1,"reply_to_post_number":7,"quote_count":0,"avg_time":11,"incoming_link_count":2,"reads":296,"score":89.75,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://meta.discourse.org/t/what-i-love-about-wordpress-plugins/5697","internal":true,"reflection":true,"title":"What I love about WordPress plugins","clicks":0}],"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3660,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:14:12.666Z","cooked":"

\n\n

Yeah, that's why I've always been a friend of message_error_nametooshort placeholders, until I asked the SO question linked above. The accepted answer makes a good argument against those placeholders: you want translations to break even on small changes in the English original because the translations will probably need to reflect the change, too. Maybe that's not the case right now as new stuff is being checked in pretty much every couple of hours, but in the long run, it'll be overwhelmingly true.

","post_number":9,"post_type":1,"updated_at":"2013-02-07T14:18:09.569Z","like_count":1,"reply_count":1,"reply_to_post_number":8,"quote_count":1,"avg_time":10,"incoming_link_count":0,"reads":293,"score":79.1,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3667,"name":"Tim Stone","username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181,"created_at":"2013-02-07T14:25:16.859Z","cooked":"

Hmm...You could theoretically also build something into the development process that would monitor changes to the English locale file and make a translator-friendly list of changes between versions.

","post_number":10,"post_type":1,"updated_at":"2013-02-07T14:25:16.859Z","like_count":1,"reply_count":1,"reply_to_post_number":9,"quote_count":0,"avg_time":7,"incoming_link_count":0,"reads":275,"score":75.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Tim Stone","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"Great contributor","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":9,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3673,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T14:30:21.401Z","cooked":"

Yeah, totally, also we could build tools for dev that make introducing string less annoying and make it possible to garbage collect old unused strings, I hate trudging through that file.

","post_number":11,"post_type":1,"updated_at":"2013-02-07T14:30:21.401Z","like_count":1,"reply_count":1,"reply_to_post_number":10,"quote_count":0,"avg_time":7,"incoming_link_count":1,"reads":273,"score":79.95,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"co-founder","reply_to_user":{"username":"tms","avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","uploaded_avatar_id":40181},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3675,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T14:33:38.280Z","cooked":"

\n\n

As said, I'd look into whether WP's tools can't be reused for this with some tweaking. They seem to be able to scan a code base for new strings, and make them available automatically to translators.

\n\n

They're PHP based which isn't ideal, but it looks like they've done a crapload of work to take the hassle out of translations.

","post_number":12,"post_type":1,"updated_at":"2013-02-07T14:34:39.910Z","like_count":1,"reply_count":1,"reply_to_post_number":11,"quote_count":1,"avg_time":7,"incoming_link_count":2,"reads":273,"score":84.95,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3690,"name":"Valts","username":"Vilx","avatar_template":"/user_avatar/meta.discourse.org/vilx/{size}/7299.png","uploaded_avatar_id":7299,"created_at":"2013-02-07T15:05:35.867Z","cooked":"

This site looks so nice with all the little tweaks like \"10 minutes ago\" instead of simply time, etc - I wonder if there will also be support for proper pluralization in other languages? That's a pretty hard task though, I don't think I've ever seen a website that has done that. But it would be awesome.

","post_number":13,"post_type":1,"updated_at":"2013-02-07T15:05:35.867Z","like_count":3,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":7,"incoming_link_count":11,"reads":290,"score":158.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Valts","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1216,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3925,"name":"Eric Kidd","username":"emk","avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","uploaded_avatar_id":8400,"created_at":"2013-02-07T19:37:06.194Z","cooked":"

\n\n

I've had pretty decent luck using Localeapp to localize Rails applications:

\n\n

http://www.localeapp.com/

\n\n

The developer workflow took me about an hour to really get used to, and there were a few minor glitches. But the non-technical translators had very few problems. One limitation: It insists on rewriting all those yaml files full of strings.

\n\n

Anyway, it's worth a look, and it's free for open source, if I recall correctly. Certainly easier than doing a whole bunch of toolsmithing from scratch.

","post_number":14,"post_type":1,"updated_at":"2013-02-07T19:37:06.194Z","like_count":3,"reply_count":1,"reply_to_post_number":12,"quote_count":1,"avg_time":9,"incoming_link_count":0,"reads":283,"score":137.05,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Eric Kidd","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"http://www.localeapp.com/","internal":false,"reflection":false,"title":"Easy localization for Rails apps | Locale","clicks":69}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":3,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1860,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3938,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T19:52:13.748Z","cooked":"

\n\n

Ohhh. Looking sexy. droool

","post_number":15,"post_type":1,"updated_at":"2013-02-07T19:52:13.748Z","like_count":1,"reply_count":1,"reply_to_post_number":14,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":260,"score":72.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3982,"name":"Eric Kidd","username":"emk","avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","uploaded_avatar_id":8400,"created_at":"2013-02-07T20:52:22.454Z","cooked":"

\n\n

Yeah, it's pretty. \"smile\" But there were still some rough edges as of a few months ago.

\n\n

Whether or not those rough edges are a deal-breaker will probably depends on whether or not localization is already a source of acute pain. If you're already hurting, Localeapp is a pretty useful tool, especially when it comes to enlisting non-technical translators.

\n\n

But it does require changing how you work with text, and adding one new tool to the mix. So for projects that just don't want to know about non-English languages, it's not yet seamless the way Unicode is these days.

\n\n

(Sweet forum software, by the way. I was just testing out Egyptian hieroglyphics on the test server, because they're well off the Basic Multilingual Plane, and tend to flush Unicode bugs. Everything worked flawlessly.)

","post_number":16,"post_type":1,"updated_at":"2013-02-07T20:52:22.454Z","like_count":1,"reply_count":1,"reply_to_post_number":15,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":254,"score":71.15,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Eric Kidd","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":1860,"hidden":false,"hidden_reason_id":null,"trust_level":1,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3989,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T21:04:15.405Z","cooked":"

\n\n

Interesting, thanks for the insight. I don't think localization is seriously on their table right now, there's likely to be many other things on the table before it... but it will become an issue sooner or later.

","post_number":17,"post_type":1,"updated_at":"2013-02-07T21:04:15.405Z","like_count":1,"reply_count":2,"reply_to_post_number":16,"quote_count":1,"avg_time":7,"incoming_link_count":0,"reads":255,"score":76.35,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":3996,"name":"Sam Saffron","username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243,"created_at":"2013-02-07T21:12:06.575Z","cooked":"

I had an idea ... what if in dev mode, you could right-click on a page and get access to all the translations on the page, make your edits and have it refreshed live.

\n\n

I think it would be awesome, very doable technically.

","post_number":18,"post_type":1,"updated_at":"2013-02-07T21:12:06.575Z","like_count":7,"reply_count":2,"reply_to_post_number":17,"quote_count":0,"avg_time":8,"incoming_link_count":0,"reads":264,"score":168.2,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Sam Saffron","primary_group_name":"discourse","version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":"co-founder","reply_to_user":{"username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253},"actions_summary":[{"id":2,"count":7,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":true,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":4009,"name":"Pekka Gaiser","username":"pekka","avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","uploaded_avatar_id":5253,"created_at":"2013-02-07T21:18:47.422Z","cooked":"

That would be fricking cool. There'd still be some leftovers (like error messages that normally never show up, etc.) but you could corral those up on a specific page.

\n\n

It could have a dropdown giving you all the languages that you have a .yml for in the locale directory, and write the changes into the one selected. I'm sure people would love it.

","post_number":19,"post_type":1,"updated_at":"2013-02-07T21:22:10.692Z","like_count":1,"reply_count":0,"reply_to_post_number":18,"quote_count":0,"avg_time":8,"incoming_link_count":1,"reads":241,"score":68.6,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Pekka Gaiser","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"reply_to_user":{"username":"sam","avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","uploaded_avatar_id":5243},"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":7,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":4012,"name":"Marco Ceppi","username":"marcoceppi","avatar_template":"/user_avatar/meta.discourse.org/marcoceppi/{size}/6552.png","uploaded_avatar_id":6552,"created_at":"2013-02-07T21:22:46.376Z","cooked":"

If you use gettext format you could leverage Launchpad translations and the community behind it.

","post_number":20,"post_type":1,"updated_at":"2013-02-07T21:22:46.376Z","like_count":1,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":9,"incoming_link_count":2,"reads":244,"score":74.25,"yours":false,"topic_id":280,"topic_slug":"internationalization-localization","display_username":"Marco Ceppi","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"link_counts":[{"url":"https://translations.launchpad.net/","internal":false,"reflection":false,"title":"Launchpad Translations","clicks":13}],"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":3,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":4,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":5,"count":0,"hidden":true,"can_act":true,"can_defer_flags":false},{"id":6,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":7,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false},{"id":8,"count":0,"hidden":false,"can_act":true,"can_defer_flags":false}],"moderator":false,"admin":false,"staff":false,"user_id":761,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}],"stream":[398,419,1060,3623,3651,3654,3655,3658,3660,3667,3673,3675,3690,3925,3938,3982,3989,3996,4009,4012],"gaps":{"before":{"20706":[20125]},"after":{}}},"id":280,"title":"Internationalization / localization","fancy_title":"Internationalization / localization","posts_count":103,"created_at":"2013-02-05T21:29:00.174Z","views":5211,"reply_count":67,"participant_count":40,"like_count":135,"last_posted_at":"2015-03-04T15:07:10.487Z","visible":true,"closed":false,"archived":false,"has_summary":true,"archetype":"regular","slug":"internationalization-localization","category_id":2,"word_count":6198,"deleted_at":null,"draft":null,"draft_key":"topic_280","draft_sequence":4,"posted":true,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":255,"username":"uwe_keim","uploaded_avatar_id":5697,"avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png"},"last_poster":{"id":14091,"username":"Luciano_Fantuzzi","uploaded_avatar_id":39484,"avatar_template":"/user_avatar/meta.discourse.org/luciano_fantuzzi/{size}/39484.png"},"participants":[{"id":212,"username":"alxndr","uploaded_avatar_id":5619,"avatar_template":"/user_avatar/meta.discourse.org/alxndr/{size}/5619.png","post_count":11},{"id":1,"username":"sam","uploaded_avatar_id":5243,"avatar_template":"/user_avatar/meta.discourse.org/sam/{size}/5243.png","post_count":11},{"id":7,"username":"pekka","uploaded_avatar_id":5253,"avatar_template":"/user_avatar/meta.discourse.org/pekka/{size}/5253.png","post_count":8},{"id":461,"username":"kuba","uploaded_avatar_id":6049,"avatar_template":"/user_avatar/meta.discourse.org/kuba/{size}/6049.png","post_count":7},{"id":2995,"username":"tattoo","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/tattoo/{size}/3.png","post_count":6},{"id":2540,"username":"jgourdon","uploaded_avatar_id":9537,"avatar_template":"/user_avatar/meta.discourse.org/jgourdon/{size}/9537.png","post_count":5},{"id":1860,"username":"emk","uploaded_avatar_id":8400,"avatar_template":"/user_avatar/meta.discourse.org/emk/{size}/8400.png","post_count":4},{"id":1275,"username":"dacap","uploaded_avatar_id":7401,"avatar_template":"/user_avatar/meta.discourse.org/dacap/{size}/7401.png","post_count":4},{"id":19,"username":"eviltrout","uploaded_avatar_id":5275,"avatar_template":"/user_avatar/meta.discourse.org/eviltrout/{size}/5275.png","post_count":4},{"id":3704,"username":"mojzis","uploaded_avatar_id":31201,"avatar_template":"/user_avatar/meta.discourse.org/mojzis/{size}/31201.png","post_count":3},{"id":3190,"username":"gururea","uploaded_avatar_id":10663,"avatar_template":"/user_avatar/meta.discourse.org/gururea/{size}/10663.png","post_count":3},{"id":1895,"username":"maciek","uploaded_avatar_id":8463,"avatar_template":"/user_avatar/meta.discourse.org/maciek/{size}/8463.png","post_count":3},{"id":22,"username":"splattne","uploaded_avatar_id":5280,"avatar_template":"/user_avatar/meta.discourse.org/splattne/{size}/5280.png","post_count":2},{"id":1979,"username":"Superuser","uploaded_avatar_id":8604,"avatar_template":"/user_avatar/meta.discourse.org/superuser/{size}/8604.png","post_count":2},{"id":3818,"username":"Tudor","uploaded_avatar_id":11675,"avatar_template":"/user_avatar/meta.discourse.org/tudor/{size}/11675.png","post_count":2},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","post_count":2},{"id":3620,"username":"potthast","uploaded_avatar_id":11363,"avatar_template":"/user_avatar/meta.discourse.org/potthast/{size}/11363.png","post_count":2},{"id":9,"username":"tms","uploaded_avatar_id":40181,"avatar_template":"/user_avatar/meta.discourse.org/tms/{size}/40181.png","post_count":2},{"id":14091,"username":"Luciano_Fantuzzi","uploaded_avatar_id":39484,"avatar_template":"/user_avatar/meta.discourse.org/luciano_fantuzzi/{size}/39484.png","post_count":1},{"id":255,"username":"uwe_keim","uploaded_avatar_id":5697,"avatar_template":"/user_avatar/meta.discourse.org/uwe_keim/{size}/5697.png","post_count":1},{"id":9006,"username":"berk","uploaded_avatar_id":19348,"avatar_template":"/user_avatar/meta.discourse.org/berk/{size}/19348.png","post_count":1},{"id":754,"username":"danneu","uploaded_avatar_id":6540,"avatar_template":"/user_avatar/meta.discourse.org/danneu/{size}/6540.png","post_count":1},{"id":761,"username":"marcoceppi","uploaded_avatar_id":6552,"avatar_template":"/user_avatar/meta.discourse.org/marcoceppi/{size}/6552.png","post_count":1},{"id":2753,"username":"mikl","uploaded_avatar_id":9918,"avatar_template":"/user_avatar/meta.discourse.org/mikl/{size}/9918.png","post_count":1}],"suggested_topics":[{"id":27331,"title":"Polls are still very buggy","fancy_title":"Polls are still very buggy","slug":"polls-are-still-very-buggy","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":"/uploads/default/_optimized/cd1/b8c/c162528887_690x401.png","created_at":"2015-04-08T09:51:00.357Z","last_posted_at":"2015-04-08T15:59:16.258Z","bumped":true,"bumped_at":"2015-04-08T16:05:09.842Z","unseen":false,"last_read_post_number":3,"unread":0,"new_posts":1,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":11,"views":55,"category_id":1},{"id":27343,"title":"Mobile theme doesn't show last activity time for topics on category page","fancy_title":"Mobile theme doesn’t show last activity time for topics on category page","slug":"mobile-theme-doesnt-show-last-activity-time-for-topics-on-category-page","posts_count":4,"reply_count":2,"highest_post_number":4,"image_url":"/uploads/default/_optimized/13e/25c/bd30b466be_281x500.png","created_at":"2015-04-08T14:20:51.177Z","last_posted_at":"2015-04-08T15:40:30.037Z","bumped":true,"bumped_at":"2015-04-08T15:40:30.037Z","unseen":false,"last_read_post_number":2,"unread":0,"new_posts":2,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":3,"views":23,"category_id":9},{"id":27346,"title":"Reply+{messagekey}@... optionaly in header \"from\" in addition to \"reply-to\"","fancy_title":"Reply+{messagekey}@… optionaly in header “from” in addition to “reply-to”","slug":"reply-messagekey-optionaly-in-header-from-in-addition-to-reply-to","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2015-04-08T16:05:13.103Z","last_posted_at":"2015-04-08T16:05:13.415Z","bumped":true,"bumped_at":"2015-04-08T16:05:13.415Z","unseen":true,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":8,"category_id":2},{"id":19670,"title":"Parsing (Oneboxing) IMDB links","fancy_title":"Parsing (Oneboxing) IMDB links","slug":"parsing-oneboxing-imdb-links","posts_count":8,"reply_count":1,"highest_post_number":8,"image_url":null,"created_at":"2014-09-05T07:19:26.161Z","last_posted_at":"2015-04-07T09:21:21.570Z","bumped":true,"bumped_at":"2015-04-07T09:21:21.570Z","unseen":false,"last_read_post_number":8,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":4,"views":253,"category_id":2},{"id":7512,"title":"Support for Piwik Analytics as an alternative to Google Analytics","fancy_title":"Support for Piwik Analytics as an alternative to Google Analytics","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","posts_count":53,"reply_count":41,"highest_post_number":65,"image_url":"/plugins/emoji/images/smile.png","created_at":"2013-06-16T01:32:30.596Z","last_posted_at":"2015-02-22T13:46:26.845Z","bumped":true,"bumped_at":"2015-02-22T13:46:26.845Z","unseen":false,"last_read_post_number":65,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":62,"views":1877,"category_id":2},{"id":25480,"title":"CSS admin-contents reloaded","fancy_title":"CSS admin-contents reloaded","slug":"css-admin-contents-reloaded","posts_count":22,"reply_count":15,"highest_post_number":22,"image_url":null,"created_at":"2015-02-21T12:15:57.707Z","last_posted_at":"2015-03-02T23:24:18.899Z","bumped":true,"bumped_at":"2015-03-02T23:24:18.899Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":21,"views":185,"category_id":2},{"id":26576,"title":"Badge timestamp should be the time the badge was granted?","fancy_title":"Badge timestamp should be the time the badge was granted?","slug":"badge-timestamp-should-be-the-time-the-badge-was-granted","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-03-20T13:22:08.266Z","last_posted_at":"2015-03-21T00:33:52.243Z","bumped":true,"bumped_at":"2015-03-21T00:33:52.243Z","unseen":false,"last_read_post_number":1,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":1,"bookmarked":false,"liked":false,"archetype":"regular","like_count":9,"views":87,"category_id":2}],"links":[{"url":"https://github.com/discourse/discourse/blob/master/config/locales/en.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":118,"user_id":9,"domain":"github.com"},{"url":"http://www.localeapp.com/","title":"Easy localization for Rails apps | Locale","fancy_title":null,"internal":false,"reflection":false,"clicks":69,"user_id":1860,"domain":"www.localeapp.com"},{"url":"http://stackoverflow.com/questions/4232922/why-do-people-use-plain-english-as-translation-placeholders","title":"internationalization - Why do people use plain english as translation placeholders? - Stack Overflow","fancy_title":null,"internal":false,"reflection":false,"clicks":63,"user_id":7,"domain":"stackoverflow.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales","title":"discourse/config/locales at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":62,"user_id":32,"domain":"github.com"},{"url":"https://github.com/SlexAxton/messageformat.js","title":"SlexAxton/messageformat.js · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":46,"user_id":1,"domain":"github.com"},{"url":"http://www.localeapp.com/projects/1537/translations?utf8=%E2%9C%93&search=source_code","title":"langforums | Locale","fancy_title":null,"internal":false,"reflection":false,"clicks":25,"user_id":1860,"domain":"www.localeapp.com"},{"url":"https://translations.launchpad.net/","title":"Launchpad Translations","fancy_title":null,"internal":false,"reflection":false,"clicks":23,"user_id":761,"domain":"translations.launchpad.net"},{"url":"https://www.transifex.com/","title":"Transifex - Continuous Localization Platform","fancy_title":null,"internal":false,"reflection":false,"clicks":22,"user_id":1979,"domain":"www.transifex.com"},{"url":"https://github.com/berk/tr8n","title":"berk/tr8n · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":22,"user_id":1,"domain":"github.com"},{"url":"http://translate.wordpress.org/projects/bbpress/dev","title":"WordPress › Development < GlotPress","fancy_title":null,"internal":false,"reflection":false,"clicks":16,"user_id":7,"domain":"translate.wordpress.org"},{"url":"http://weblate.org","title":"Weblate - web-based translation","fancy_title":null,"internal":false,"reflection":false,"clicks":15,"user_id":2316,"domain":"weblate.org"},{"url":"https://github.com/discourse/discourse/tree/master/config/locales","title":"discourse/config/locales at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":14,"user_id":19,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/pull/493","title":"Danish translation. by mikl · Pull Request #493 · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":12,"user_id":2753,"domain":"github.com"},{"url":"https://github.com/SlexAxton","title":"SlexAxton (Alex Sexton) · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":10,"user_id":1,"domain":"github.com"},{"url":"https://github.com/gururea/discourse/tree/master/config/locales","title":"discourse/config/locales at master · gururea/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":9,"user_id":3190,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.en.yml#L691","title":"discourse/config/locales/client.en.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":461,"domain":"github.com"},{"url":"https://github.com/dacap/discourse/tree/spanish","title":"dacap/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":1275,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.nl.yml","title":"discourse/config/locales/client.nl.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":7,"user_id":461,"domain":"github.com"},{"url":"https://github.com/discourse/discourse/commit/c5761eae8afe37e20cec0d0f9d14b85b6e585bda","title":"Support for Simplified Chinese thanks to tangramor · c5761ea · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":212,"domain":"github.com"},{"url":"http://tr8n.github.com/","title":"tr8n","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":212,"domain":"tr8n.github.com"},{"url":"http://www.getlocalization.com/","title":"Crowdsourced, Social and Collaborative App & Website Translation - Get Localization","fancy_title":null,"internal":false,"reflection":false,"clicks":6,"user_id":22,"domain":"www.getlocalization.com"},{"url":"http://blog.discourse.org/2013/04/discourse-as-your-first-rails-app/","title":"Discourse as Your First Rails App","fancy_title":null,"internal":false,"reflection":false,"clicks":5,"user_id":1995,"domain":"blog.discourse.org"},{"url":"https://github.com/alxndr/discourse/blob/i18n-chinese/config/locales/server.zh.yml","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":5,"user_id":212,"domain":"github.com"},{"url":"http://translate.sourceforge.net/wiki/virtaal/index","title":"Easy-to-use and powerful offline translation tool | Virtaal","fancy_title":null,"internal":false,"reflection":false,"clicks":4,"user_id":1979,"domain":"translate.sourceforge.net"},{"url":"https://poeditor.com/","title":"POEditor - online software localization tool","fancy_title":null,"internal":false,"reflection":false,"clicks":4,"user_id":1979,"domain":"poeditor.com"},{"url":"http://en.lichess.org/@/Hellball","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":1979,"domain":"en.lichess.org"},{"url":"http://en.wikipedia.org/wiki/T%E2%80%93V_distinction","title":"T–V distinction - Wikipedia, the free encyclopedia","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":3620,"domain":"en.wikipedia.org"},{"url":"http://www.slideshare.net/HeatherRivers/linguistic-potluck-crowdsourcing-localization-with-rails","title":"Linguistic Potluck: Crowdsourcing localization with Rails","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":212,"domain":"www.slideshare.net"},{"url":"https://meta.discourse.org/t/language-mirrors/2378/2","title":"Language mirrors","fancy_title":null,"internal":true,"reflection":true,"clicks":3,"user_id":32,"domain":"meta.discourse.org"},{"url":"http://www.madanalogy.com/2012/06/rails-i18n-translations-in-yaml.html","title":"Mad Analogy: Rails i18n translations in Yaml: translation tool support","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":3190,"domain":"www.madanalogy.com"},{"url":"https://github.com/tr8n","title":"Translation Exchange · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":3,"user_id":9006,"domain":"github.com"},{"url":"http://pootle.locamotion.org/","title":"Main | Pootle Demo","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":3190,"domain":"pootle.locamotion.org"},{"url":"http://www.youtube.com/watch?v=MqqdzJ98q7s","title":"GoGaRuCo 2012 - Linguistic Potluck: Crowdsourcing Localization in Rails by Heather Rivers - YouTube","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":212,"domain":"www.youtube.com"},{"url":"https://meta.discourse.org/t/translation-workflow/6102","title":"Translation workflow","fancy_title":null,"internal":true,"reflection":true,"clicks":2,"user_id":4702,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/solving-xda-developer-style-forums/4368/4","title":"Solving XDA-Developer style forums","fancy_title":null,"internal":true,"reflection":true,"clicks":2,"user_id":639,"domain":"meta.discourse.org"},{"url":"https://tr8nhub.com","title":"TranslationExchange","fancy_title":null,"internal":false,"reflection":false,"clicks":2,"user_id":9006,"domain":"tr8nhub.com"},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/3","title":"Roadplan for Discourse 2013","fancy_title":null,"internal":true,"reflection":true,"clicks":1,"user_id":2540,"domain":"meta.discourse.org"},{"url":"http://sugarjs.com/dates#date_locales","title":"Dates - Sugar","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":461,"domain":"sugarjs.com"},{"url":"http://blog.discourse.org/2013/03/localizing-discourse/","title":"Localizing Discourse","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"blog.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/app/assets/javascripts/locales/date_locales.js","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":461,"domain":"github.com"},{"url":"http://transifex.com/projects/p/discourse-pt-br/","title":"Discourse-Translations-Project localization","fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"transifex.com"},{"url":"https://github.com/discourse/discourse/issues/279","title":null,"fancy_title":null,"internal":false,"reflection":false,"clicks":1,"user_id":893,"domain":"github.com"},{"url":"https://meta.discourse.org/t/comrades-lets-join-our-efforts-on-ukrainian-and-russian-translations/4403/5","title":"Comrades let's join our efforts on ukrainian and russian translations","fancy_title":null,"internal":true,"reflection":true,"clicks":1,"user_id":3417,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-workflow/6102/6","title":"Translation workflow","fancy_title":null,"internal":true,"reflection":false,"clicks":0,"user_id":1995,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/bookmark-last-read-sometimes-doesn-t-go-to-the-end-of-a-topic/4825/9","title":"Bookmark/last read sometimes doesn't go to the end of a topic","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":3681,"domain":"meta.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/client.de.yml","title":"discourse/config/locales/client.de.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":2,"domain":"github.com"},{"url":"https://meta.discourse.org/t/what-i-love-about-wordpress-plugins/5697","title":"What I love about WordPress plugins","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":1,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/github-onebox-rendering-issue/7616","title":"GitHub OneBox Rendering Issue","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":5372,"domain":"meta.discourse.org"},{"url":"https://github.com/discourse/discourse/blob/master/config/locales/server.de.yml","title":"discourse/config/locales/server.de.yml at master · discourse/discourse · GitHub","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":2,"domain":"github.com"},{"url":"https://meta.discourse.org/t/roadplan-for-discourse/2939/5","title":"Roadplan for Discourse 2013","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":32,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-tools-transifex-localeapp/7763","title":"Translation Tools: Transifex? Localeapp?","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":2,"domain":"meta.discourse.org"},{"url":"http://guides.rubyonrails.org/i18n.html#the-public-i18n-api","title":"Rails Internationalization (I18n) API — Ruby on Rails Guides","fancy_title":null,"internal":false,"reflection":false,"clicks":0,"user_id":1895,"domain":"guides.rubyonrails.org"},{"url":"https://meta.discourse.org/t/hi-support-chinese/4393/6","title":"Hi, support Chinese?","fancy_title":null,"internal":true,"reflection":true,"clicks":0,"user_id":2014,"domain":"meta.discourse.org"},{"url":"https://meta.discourse.org/t/translation-tools-transifex-localeapp/7763/41","title":"Translation Tools: Transifex? Localeapp?","fancy_title":null,"internal":true,"reflection":false,"clicks":0,"user_id":6626,"domain":"meta.discourse.org"}],"notification_level":2,"notifications_reason_id":4,"can_move_posts":true,"can_edit":true,"can_delete":true,"can_recover":true,"can_remove_allowed_users":true,"can_invite_to":true,"can_create_post":true,"can_reply_as_new_topic":true,"can_flag_topic":true},"highest_post_number":10,"last_read_post_number":10,"deleted_by":null,"has_deleted":true,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":true},{"id":7,"count":0,"hidden":false,"can_act":true},{"id":8,"count":0,"hidden":false,"can_act":true}],"chunk_size":20,"bookmarked":false,"tags":null}, +"/t/28830/1.json": {"post_stream":{"posts":[{"id":118591,"name":"spends too much time on WTDWTF","username":"RaceProUK","avatar_template":"/user_avatar/meta.discourse.org/raceprouk/{size}/40071.png","uploaded_avatar_id":40071,"created_at":"2015-05-14T20:18:17.954Z","cooked":"

Normally, actions such as Liking are rate-limited, and when you hit the limit, you get a message telling you you've hit the limit. However, in 1.3.0beta9, it seems those popups are no longer appearing.

\n\n

Edit: Possibly linked to this issue?

","post_number":1,"post_type":1,"updated_at":"2015-05-14T20:21:42.825Z","like_count":6,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":14,"reads":24,"score":224.6,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"spends too much time on WTDWTF","primary_group_name":null,"version":1,"can_edit":false,"can_delete":false,"can_recover":false,"link_counts":[{"url":"https://meta.discourse.org/t/post-reply-on-different-topic-no-longer-works/28825","internal":true,"reflection":false,"title":"Post reply on different topic no longer works","clicks":6}],"read":true,"user_title":"Contributor","actions_summary":[{"id":2,"count":6,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":false,"admin":false,"staff":false,"user_id":14169,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":"","can_view_edit_history":true,"wiki":false},{"id":118597,"name":"Sam","username":"Yuun","avatar_template":"/letter_avatar/yuun/{size}/3_90a587a04512ff220ac26ec1465844c5.png","uploaded_avatar_id":null,"created_at":"2015-05-14T20:35:03.793Z","cooked":"

I'm seeing this issue as well. When you hit the rate limit, any further likes look like the forum is attempting and failing to apply them - the text saying 'you liked this' comes into place before quickly being removed.

\n\n

This makes it look (to the user) like the forum software is running into errors instead of said user hitting an intentional limit, which is a bit unfortunate.

","post_number":2,"post_type":1,"updated_at":"2015-05-14T20:35:03.793Z","like_count":0,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":6,"reads":22,"score":34.2,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"Sam","primary_group_name":null,"version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":false,"admin":false,"staff":false,"user_id":14795,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":118601,"name":"Kane York","username":"riking","avatar_template":"/user_avatar/meta.discourse.org/riking/{size}/40212.png","uploaded_avatar_id":40212,"created_at":"2015-05-14T21:05:19.837Z","cooked":"

I'm going to guess that the bootbox library got broken somehow?

","post_number":3,"post_type":1,"updated_at":"2015-05-14T21:05:19.837Z","like_count":0,"reply_count":1,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":14,"score":7.2,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"Kane York","primary_group_name":null,"version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":true,"user_title":"team summer intern 2014","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":false,"admin":false,"staff":false,"user_id":6626,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":118606,"name":"Jeff Atwood","username":"codinghorror","avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","uploaded_avatar_id":5297,"created_at":"2015-05-14T21:15:41.612Z","cooked":"

Yeah maybe another Ember 1.10 regression for @eviltrout ?

","post_number":4,"post_type":1,"updated_at":"2015-05-14T21:15:41.612Z","like_count":0,"reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":6,"reads":12,"score":31.6,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"Jeff Atwood","primary_group_name":"discourse","version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":true,"user_title":"co-founder","actions_summary":[{"id":2,"count":0,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":true,"admin":true,"staff":true,"user_id":32,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":118612,"name":"TDWTF member","username":"Onyx","avatar_template":"/user_avatar/meta.discourse.org/onyx/{size}/33015.png","uploaded_avatar_id":33015,"created_at":"2015-05-14T21:23:09.562Z","cooked":"\n\n

You mean the popup box library, guessing by the name? Still shows up when you want to cancel a post, so it's not all popups it seems.

","post_number":5,"post_type":1,"updated_at":"2015-05-14T21:23:09.562Z","like_count":1,"reply_count":0,"reply_to_post_number":3,"quote_count":1,"avg_time":null,"incoming_link_count":0,"reads":11,"score":16.0,"yours":false,"topic_id":28830,"topic_slug":"1-3-0beta9-no-rate-limit-popups","display_username":"TDWTF member","primary_group_name":null,"version":1,"can_edit":false,"can_delete":false,"can_recover":false,"read":true,"user_title":null,"actions_summary":[{"id":2,"count":1,"hidden":false,"can_act":false},{"id":3,"count":0,"hidden":false,"can_act":false},{"id":4,"count":0,"hidden":false,"can_act":false},{"id":5,"count":0,"hidden":true,"can_act":false},{"id":6,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"moderator":false,"admin":false,"staff":false,"user_id":10886,"hidden":false,"hidden_reason_id":null,"trust_level":2,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}],"stream":[118591,118597,118601,118606,118612]},"id":28830,"title":"1.3.0beta9: No rate-limit popups","fancy_title":"1.3.0beta9: No rate-limit popups","posts_count":5,"created_at":"2015-05-14T20:18:17.877Z","views":38,"reply_count":1,"participant_count":5,"like_count":7,"last_posted_at":"2015-05-14T21:23:09.562Z","visible":true,"closed":false,"archived":false,"has_summary":false,"archetype":"regular","slug":"1-3-0beta9-no-rate-limit-popups","category_id":1,"word_count":198,"deleted_at":null,"pending_posts_count":0,"draft":null,"draft_key":"topic_28830","draft_sequence":null,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":14169,"username":"RaceProUK","uploaded_avatar_id":40071,"avatar_template":"/user_avatar/meta.discourse.org/raceprouk/{size}/40071.png"},"last_poster":{"id":10886,"username":"Onyx","uploaded_avatar_id":33015,"avatar_template":"/user_avatar/meta.discourse.org/onyx/{size}/33015.png"},"participants":[{"id":14795,"username":"Yuun","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/yuun/{size}/3_90a587a04512ff220ac26ec1465844c5.png","post_count":1},{"id":10886,"username":"Onyx","uploaded_avatar_id":33015,"avatar_template":"/user_avatar/meta.discourse.org/onyx/{size}/33015.png","post_count":1},{"id":14169,"username":"RaceProUK","uploaded_avatar_id":40071,"avatar_template":"/user_avatar/meta.discourse.org/raceprouk/{size}/40071.png","post_count":1},{"id":6626,"username":"riking","uploaded_avatar_id":40212,"avatar_template":"/user_avatar/meta.discourse.org/riking/{size}/40212.png","post_count":1},{"id":32,"username":"codinghorror","uploaded_avatar_id":5297,"avatar_template":"/user_avatar/meta.discourse.org/codinghorror/{size}/5297.png","post_count":1}],"suggested_topics":[{"id":2890,"title":"Expanded quoted text not highlighting when text is formatted","fancy_title":"Expanded quoted text not highlighting when text is formatted","slug":"expanded-quoted-text-not-highlighting-when-text-is-formatted","posts_count":8,"reply_count":5,"highest_post_number":8,"image_url":null,"created_at":"2013-02-12T12:18:02.181Z","last_posted_at":"2013-02-14T15:59:40.014Z","bumped":true,"bumped_at":"2013-02-14T15:59:40.014Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":3,"views":361,"category_id":1},{"id":14213,"title":"Plugins not being parsed in correct javascript context when loaded for jobs","fancy_title":"Plugins not being parsed in correct javascript context when loaded for jobs","slug":"plugins-not-being-parsed-in-correct-javascript-context-when-loaded-for-jobs","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":"/plugins/emoji/images/frowning.png","created_at":"2014-03-27T23:57:00.974Z","last_posted_at":"2015-03-20T04:56:03.982Z","bumped":true,"bumped_at":"2015-03-20T04:56:03.982Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":0,"views":156,"category_id":1},{"id":22544,"title":"Like count on profile off by one","fancy_title":"Like count on profile off by one","slug":"like-count-on-profile-off-by-one","posts_count":7,"reply_count":2,"highest_post_number":7,"image_url":null,"created_at":"2014-11-26T08:15:39.802Z","last_posted_at":"2014-11-27T07:23:37.638Z","bumped":true,"bumped_at":"2014-11-27T07:23:37.638Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":18,"views":192,"category_id":1},{"id":27670,"title":"Using back still shows unread indicator on the topic","fancy_title":"Using back still shows unread indicator on the topic","slug":"using-back-still-shows-unread-indicator-on-the-topic","posts_count":2,"reply_count":0,"highest_post_number":2,"image_url":null,"created_at":"2015-04-16T23:21:42.739Z","last_posted_at":"2015-04-17T02:43:08.447Z","bumped":true,"bumped_at":"2015-04-17T02:43:08.447Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":1,"views":85,"category_id":1},{"id":26628,"title":"Embed blacklist selector is broken","fancy_title":"Embed blacklist selector is broken","slug":"embed-blacklist-selector-is-broken","posts_count":11,"reply_count":7,"highest_post_number":11,"image_url":null,"created_at":"2015-03-22T11:21:14.825Z","last_posted_at":"2015-04-20T09:11:38.999Z","bumped":true,"bumped_at":"2015-04-20T09:11:38.999Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":1,"views":247,"category_id":1},{"id":18027,"title":"Minor: delete/undelete needs a rate limit","fancy_title":"Minor: delete/undelete needs a rate limit","slug":"minor-delete-undelete-needs-a-rate-limit","posts_count":4,"reply_count":1,"highest_post_number":4,"image_url":null,"created_at":"2014-07-25T02:51:41.158Z","last_posted_at":"2014-07-25T04:01:15.343Z","bumped":true,"bumped_at":"2014-07-25T11:06:46.213Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":1,"views":165,"category_id":1},{"id":17396,"title":"Bad Reply Key when pulling Autoforwarded Emails to Discourse","fancy_title":"Bad Reply Key when pulling Autoforwarded Emails to Discourse","slug":"bad-reply-key-when-pulling-autoforwarded-emails-to-discourse","posts_count":20,"reply_count":15,"highest_post_number":20,"image_url":null,"created_at":"2014-07-09T18:34:57.114Z","last_posted_at":"2014-10-21T15:08:50.441Z","bumped":true,"bumped_at":"2014-10-21T15:08:50.441Z","unseen":false,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"bookmarked":null,"liked":null,"archetype":"regular","like_count":7,"views":542,"category_id":1}],"links":[{"url":"https://meta.discourse.org/t/post-reply-on-different-topic-no-longer-works/28825","title":"Post reply on different topic no longer works","fancy_title":null,"internal":true,"reflection":false,"clicks":6,"user_id":14169,"domain":"meta.discourse.org"}],"notification_level":1,"can_flag_topic":false},"highest_post_number":5,"deleted_by":null,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":false},{"id":7,"count":0,"hidden":false,"can_act":false},{"id":8,"count":0,"hidden":false,"can_act":false}],"chunk_size":20,"bookmarked":null,"tags":null}, "/t/9/1.json": {"post_stream":{"posts":[{"id":18,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","name":"Evil Trout","uploaded_avatar_id":9,"created_at":"2015-08-13T14:49:11.840Z","cooked":"

This is the first post.

","post_number":1,"post_type":1,"updated_at":"2015-08-13T14:49:11.840Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":9,"topic_slug":"this-is-a-test-topic","display_username":"","primary_group_name":null,"version":1,"can_edit":true,"can_delete":false,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":19,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","name":"Evil Trout","uploaded_avatar_id":9,"created_at":"2015-08-13T14:49:18.231Z","cooked":"

This is the second post.

","post_number":2,"post_type":1,"updated_at":"2015-08-13T14:49:18.231Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":9,"topic_slug":"this-is-a-test-topic","display_username":"","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false},{"id":20,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","name":"Evil Trout","uploaded_avatar_id":9,"created_at":"2015-08-13T14:49:23.927Z","cooked":"

This is the third post.

","post_number":3,"post_type":1,"updated_at":"2015-08-13T14:49:23.927Z","reply_count":0,"reply_to_post_number":null,"quote_count":0,"avg_time":null,"incoming_link_count":0,"reads":1,"score":0,"yours":true,"topic_id":9,"topic_slug":"this-is-a-test-topic","display_username":"","primary_group_name":null,"version":1,"can_edit":true,"can_delete":true,"can_recover":true,"read":true,"user_title":null,"actions_summary":[{"id":3,"can_act":true},{"id":4,"can_act":true},{"id":5,"hidden":true,"can_act":true},{"id":7,"can_act":true},{"id":8,"can_act":true}],"moderator":false,"admin":true,"staff":true,"user_id":1,"hidden":false,"hidden_reason_id":null,"trust_level":4,"deleted_at":null,"user_deleted":false,"edit_reason":null,"can_view_edit_history":true,"wiki":false}],"stream":[18,19,20]},"id":9,"title":"This is a test topic!","fancy_title":"This is a test topic!","posts_count":3,"created_at":"2015-08-13T14:49:11.720Z","views":1,"reply_count":0,"participant_count":1,"like_count":0,"last_posted_at":"2015-08-13T14:49:23.927Z","visible":true,"closed":false,"archived":false,"has_summary":false,"archetype":"regular","slug":"this-is-a-test-topic","category_id":1,"word_count":15,"deleted_at":null,"pending_posts_count":0,"user_id":1,"draft":null,"draft_key":"topic_9","draft_sequence":3,"posted":true,"unpinned":null,"pinned_globally":false,"pinned":false,"pinned_at":null,"pinned_until":null,"details":{"auto_close_at":null,"auto_close_hours":null,"auto_close_based_on_last_post":false,"created_by":{"id":1,"username":"tgxworld","uploaded_avatar_id":9,"avatar_template":"/user_avatar/localhost/tgxworld/{size}/9_1.png"},"last_poster":{"id":1,"username":"tgxworld","uploaded_avatar_id":9,"avatar_template":"/user_avatar/localhost/tgxworld/{size}/9_1.png"},"participants":[{"id":1,"username":"tgxworld","uploaded_avatar_id":9,"avatar_template":"/user_avatar/localhost/tgxworld/{size}/9_1.png","post_count":3}],"suggested_topics":[{"id":8,"title":"This is a new and awesome topic!","fancy_title":"This is a new and awesome topic!","slug":"this-is-a-new-and-awesome-topic","posts_count":3,"reply_count":0,"highest_post_number":5,"image_url":null,"created_at":"2015-08-13T05:17:00.000Z","last_posted_at":"2015-08-13T10:14:34.799Z","bumped":true,"bumped_at":"2015-08-13T10:14:34.799Z","unseen":false,"last_read_post_number":5,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":2,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":2,"category_id":1},{"id":7,"title":"This is a test category!","fancy_title":"This is a test category!","slug":"this-is-a-test-category","posts_count":3,"reply_count":0,"highest_post_number":3,"image_url":null,"created_at":"2015-08-10T13:40:38.439Z","last_posted_at":"2015-08-13T01:59:44.928Z","bumped":true,"bumped_at":"2015-08-13T01:58:35.206Z","unseen":false,"last_read_post_number":3,"unread":0,"new_posts":0,"pinned":false,"unpinned":null,"visible":true,"closed":false,"archived":false,"notification_level":3,"bookmarked":false,"liked":false,"archetype":"regular","like_count":0,"views":2,"category_id":1}],"notification_level":3,"notifications_reason_id":1,"can_move_posts":true,"can_edit":true,"can_delete":true,"can_recover":true,"can_remove_allowed_users":true,"can_invite_to":true,"can_create_post":true,"can_reply_as_new_topic":true,"can_flag_topic":true},"highest_post_number":3,"last_read_post_number":3,"deleted_by":null,"has_deleted":false,"actions_summary":[{"id":4,"count":0,"hidden":false,"can_act":true},{"id":7,"count":0,"hidden":false,"can_act":true},{"id":8,"count":0,"hidden":false,"can_act":true}],"chunk_size":20,"bookmarked":false} }; diff --git a/test/javascripts/fixtures/user-badges.js.es6 b/test/javascripts/fixtures/user-badges.js.es6 new file mode 100644 index 0000000000..6b5cd069b0 --- /dev/null +++ b/test/javascripts/fixtures/user-badges.js.es6 @@ -0,0 +1,57 @@ +export default { + '/user_badges':{ + "badges":[ + { + "id":874, + "name":"Badge 2", + "description":null, + "badge_type_id":7 + } + ], + "badge_types":[ + { + "id":7, + "name":"Silver 2" + } + ], + "users":[ + { + "id":13470, + "username":"anne3", + "avatar_template":"//www.gravatar.com/avatar/a4151b1fd72089c54e2374565a87da7f.png?s={size}\u0026r=pg\u0026d=identicon" + } + ], + "user_badge":{ + "id":665, + "granted_at":"2014-03-09T20:30:01.190-04:00", + "badge_id":874, + "granted_by_id":13470 + } + }, + '/user-badges/:username':{ + "badges":[ + { + "id":880, + "name":"Badge 8", + "description":null, + "badge_type_id":13 + } + ], + "badge_types":[ + { + "id":13, + "name":"Silver 8" + } + ], + "users":[ + ], + "user_badges":[ + { + "id":668, + "granted_at":"2014-03-09T20:30:01.420-04:00", + "badge_id":880, + "granted_by_id":null + } + ] + } +}; diff --git a/test/javascripts/fixtures/user_fixtures.js.es6 b/test/javascripts/fixtures/user_fixtures.js.es6 index c123605c3a..cfdbd38ddf 100644 --- a/test/javascripts/fixtures/user_fixtures.js.es6 +++ b/test/javascripts/fixtures/user_fixtures.js.es6 @@ -1,6 +1,6 @@ /*jshint maxlen:10000000 */ export default { -"/users/eviltrout.json": {"user_badges":[{"id":5870,"granted_at":"2014-05-16T02:39:38.388Z","badge_id":4,"user_id":19,"granted_by_id":-1},{"id":40673,"granted_at":"2014-03-31T14:23:18.060Z","post_id":7241,"post_number":19,"badge_id":23,"user_id":19,"granted_by_id":-1,"topic_id":3153},{"id":5868,"granted_at":"2014-05-16T02:39:38.380Z","badge_id":3,"user_id":19,"granted_by_id":-1}],"badges":[{"id":4,"name":"Leader","description":null,"grant_count":7,"allow_title":true,"multiple_grant":false,"icon":"fa-user","image":null,"listable":true,"enabled":true,"badge_grouping_id":4,"system":true,"badge_type_id":1},{"id":23,"name":"Great Share","description":null,"grant_count":14,"allow_title":false,"multiple_grant":true,"icon":"fa-certificate","image":null,"listable":true,"enabled":true,"badge_grouping_id":2,"system":true,"badge_type_id":1},{"id":3,"name":"Regular","description":null,"grant_count":30,"allow_title":true,"multiple_grant":false,"icon":"fa-user","image":null,"listable":true,"enabled":true,"badge_grouping_id":4,"system":true,"badge_type_id":2}],"badge_types":[{"id":1,"name":"Gold","sort_order":9},{"id":2,"name":"Silver","sort_order":8},{"id":3,"name":"Bronze","sort_order":7}],"users":[{"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"},{"id":-1,"username":"system","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/system/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"}],"topics":[{"id":3153,"title":"Is it better for Discourse to use JavaScript or CoffeeScript?","fancy_title":"Is it better for Discourse to use JavaScript or CoffeeScript?","slug":"is-it-better-for-discourse-to-use-javascript-or-coffeescript","posts_count":56}],"user":{"user_option":{},"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png","name":"Robin Ward","email":"robin.ward@gmail.com","last_posted_at":"2015-05-07T15:23:35.074Z","last_seen_at":"2015-05-13T14:34:23.188Z","bio_raw":"Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.","bio_cooked":"

Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.

","created_at":"2013-02-03T15:19:22.704Z","website":"http://eviltrout.com","location":"Toronto","can_edit":false,"can_edit_username":true,"can_edit_email":true,"can_edit_name":true,"stats":[{"action_type":13,"count":342,"id":null},{"action_type":12,"count":109,"id":null},{"action_type":4,"count":27,"id":null},{"action_type":5,"count":1607,"id":null},{"action_type":6,"count":771,"id":null},{"action_type":1,"count":333,"id":null},{"action_type":2,"count":2671,"id":null},{"action_type":7,"count":949,"id":null},{"action_type":9,"count":42,"id":null},{"action_type":3,"count":8,"id":null},{"action_type":11,"count":20,"id":null}],"can_send_private_messages":true,"can_send_private_message_to_user":false,"bio_excerpt":"Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.","trust_level":4,"moderator":true,"admin":true,"title":"co-founder","badge_count":23,"notification_count":3244,"has_title_badges":true,"custom_fields":{},"user_fields":{"1":"33"},"pending_count":0,"post_count":1987,"can_be_deleted":false,"can_delete_all_posts":false,"locale":"","email_digests":true,"email_private_messages":true,"email_direct":true,"email_always":true,"digest_after_minutes":10080,"mailing_list_mode":false,"auto_track_topics_after_msecs":60000,"new_topic_duration_minutes":1440,"external_links_in_new_tab":false,"dynamic_favicon":true,"enable_quoting":true,"muted_category_ids":[],"tracked_category_ids":[],"watched_category_ids":[3],"private_messages_stats":{"all":101,"mine":13,"unread":3},"disable_jump_reply":false,"gravatar_avatar_upload_id":5275,"custom_avatar_upload_id":1573,"card_image_badge":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","card_image_badge_id":120,"muted_usernames":[],"invited_by":{"id":1,"username":"sam","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/sam/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"},"custom_groups":[{"id":44,"automatic":false,"name":"ubuntu","user_count":11,"alias_level":0,"visible":true,"automatic_membership_email_domains":null,"automatic_membership_retroactive":false,"primary_group":false,"title":null},{"id":47,"automatic":false,"name":"discourse","user_count":7,"alias_level":0,"visible":true,"automatic_membership_email_domains":null,"automatic_membership_retroactive":false,"primary_group":false,"title":null}],"featured_user_badge_ids":[5870,40673,5868],"card_badge":{"id":120,"name":"Garbage Man","description":"This Discourse developer successfully called something \"garbage!\"","grant_count":3,"allow_title":false,"multiple_grant":false,"icon":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","image":"https://meta-discourse.global.ssl.fastly.net/uploads/default/36220/15b19c80dd99d5a5.png","listable":false,"enabled":false,"badge_grouping_id":8,"system":false,"badge_type_id":3}}}, +"/users/eviltrout.json": {"user_badges":[{"id":5870,"granted_at":"2014-05-16T02:39:38.388Z","badge_id":4,"user_id":19,"granted_by_id":-1},{"id":40673,"granted_at":"2014-03-31T14:23:18.060Z","post_id":7241,"post_number":19,"badge_id":23,"user_id":19,"granted_by_id":-1,"topic_id":3153},{"id":5868,"granted_at":"2014-05-16T02:39:38.380Z","badge_id":3,"user_id":19,"granted_by_id":-1}],"badges":[{"id":4,"name":"Leader","description":null,"grant_count":7,"allow_title":true,"multiple_grant":false,"icon":"fa-user","image":null,"listable":true,"enabled":true,"badge_grouping_id":4,"system":true,"badge_type_id":1},{"id":23,"name":"Great Share","description":null,"grant_count":14,"allow_title":false,"multiple_grant":true,"icon":"fa-certificate","image":null,"listable":true,"enabled":true,"badge_grouping_id":2,"system":true,"badge_type_id":1},{"id":3,"name":"Regular","description":null,"grant_count":30,"allow_title":true,"multiple_grant":false,"icon":"fa-user","image":null,"listable":true,"enabled":true,"badge_grouping_id":4,"system":true,"badge_type_id":2}],"badge_types":[{"id":1,"name":"Gold","sort_order":9},{"id":2,"name":"Silver","sort_order":8},{"id":3,"name":"Bronze","sort_order":7}],"users":[{"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"},{"id":-1,"username":"system","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/system/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"}],"topics":[{"id":3153,"title":"Is it better for Discourse to use JavaScript or CoffeeScript?","fancy_title":"Is it better for Discourse to use JavaScript or CoffeeScript?","slug":"is-it-better-for-discourse-to-use-javascript-or-coffeescript","posts_count":56}],"user":{"user_option":{},"id":19,"username":"eviltrout","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/eviltrout/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png","name":"Robin Ward","email":"robin.ward@gmail.com","last_posted_at":"2015-05-07T15:23:35.074Z","last_seen_at":"2015-05-13T14:34:23.188Z","bio_raw":"Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.","bio_cooked":"

Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.

","created_at":"2013-02-03T15:19:22.704Z","website":"http://eviltrout.com","location":"Toronto","can_edit":false,"can_edit_username":true,"can_edit_email":true,"can_edit_name":true,"stats":[{"action_type":13,"count":342,"id":null},{"action_type":12,"count":109,"id":null},{"action_type":4,"count":27,"id":null},{"action_type":5,"count":1607,"id":null},{"action_type":6,"count":771,"id":null},{"action_type":1,"count":333,"id":null},{"action_type":2,"count":2671,"id":null},{"action_type":7,"count":949,"id":null},{"action_type":9,"count":42,"id":null},{"action_type":3,"count":8,"id":null},{"action_type":11,"count":20,"id":null}],"can_send_private_messages":true,"can_send_private_message_to_user":false,"bio_excerpt":"Co-founder of Discourse. Previously, I created Forumwarz. Follow me on Twitter.","trust_level":4,"moderator":true,"admin":true,"title":"co-founder","badge_count":23,"notification_count":3244,"has_title_badges":true,"custom_fields":{},"user_fields":{"1":"33"},"pending_count":0,"post_count":1987,"can_be_deleted":false,"can_delete_all_posts":false,"locale":"","email_digests":true,"email_private_messages":true,"email_direct":true,"email_always":true,"digest_after_minutes":10080,"mailing_list_mode":false,"auto_track_topics_after_msecs":60000,"new_topic_duration_minutes":1440,"external_links_in_new_tab":false,"dynamic_favicon":true,"enable_quoting":true,"muted_category_ids":[],"tracked_category_ids":[],"watched_category_ids":[3],"private_messages_stats":{"all":101,"mine":13,"unread":3},"disable_jump_reply":false,"gravatar_avatar_upload_id":5275,"custom_avatar_upload_id":1573,"card_image_badge":"/images/avatar.png","card_image_badge_id":120,"muted_usernames":[],"invited_by":{"id":1,"username":"sam","uploaded_avatar_id":null,"avatar_template":"/letter_avatar/sam/{size}/3_f9720745f5ce6dfc2b5641fca999d934.png"},"custom_groups":[{"id":44,"automatic":false,"name":"ubuntu","user_count":11,"alias_level":0,"visible":true,"automatic_membership_email_domains":null,"automatic_membership_retroactive":false,"primary_group":false,"title":null},{"id":47,"automatic":false,"name":"discourse","user_count":7,"alias_level":0,"visible":true,"automatic_membership_email_domains":null,"automatic_membership_retroactive":false,"primary_group":false,"title":null}],"featured_user_badge_ids":[5870,40673,5868],"card_badge":{"id":120,"name":"Garbage Man","description":"This Discourse developer successfully called something \"garbage!\"","grant_count":3,"allow_title":false,"multiple_grant":false,"icon":"/images/avatar.png","image":"/images/avatar.png","listable":false,"enabled":false,"badge_grouping_id":8,"system":false,"badge_type_id":3}}}, "/user_actions.json": {"user_actions":[{"action_type":7,"created_at":"2014-01-16T14:13:05Z","excerpt":"So again, \n\nWhat is the problem?\n\nI need to check user_trust_level , i get the 'username' from a form via ajax, i need to check what level he is on discourse \n\nAlso, if possible, i would like to get other details as well, like email address etc. \n\nI took a look at : https://github.com/discourse/dis…","avatar_template":"//www.gravatar.com/avatar/bdab7e61b3191e483492fd680f563fed.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/bdab7e61b3191e483492fd680f563fed.png?s={size}&r=pg&d=identicon","slug":"how-to-check-the-user-level-via-ajax","topic_id":11993,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":1,"reply_to_post_number":null,"username":"Abhishek_Gupta","name":"Abhishek Gupta","user_id":8021,"acting_username":"Abhishek_Gupta","acting_name":"Abhishek Gupta","acting_user_id":8021,"title":"How to check the user level via ajax?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-15T16:53:49Z","excerpt":"A good fix would be to have the ERB template do an if statement. We'd happily accept a PR that did this if you feel up to it: \n\n <% if SiteSetting.logo_url.present? %>\n display logo html\n<% else %>\n display title html\n<% end %>","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2","topic_id":10911,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"/users/activate-account pulling blank logo instead of defaulting to h2","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-15T15:21:37Z","excerpt":"A good fix would be to have the ERB template do an if statement. We'd happily accept a PR that did this if you feel up to it: \n\n <% if SiteSetting.logo_url.present? %>\n display logo html\n<% else %>\n display title html\n<% end %>","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"users-activate-account-pulling-blank-logo-instead-of-defaulting-to-h2","topic_id":10911,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"/users/activate-account pulling blank logo instead of defaulting to h2","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-15T12:22:12Z","excerpt":"OK - i see what you mean. From the piwik code I should add: \n\n_paq.push(["setDocumentTitle", document.domain + "/" + document.title]);\n\n? \n\nUnfortunately I have had to give up on Piwik for now because I have switched the forum to SSL on a free cert and have used up the free subdomain for the forum. …","avatar_template":"//localhost:3000/uploads/default/avatars/2a8/a3c/8fddcac642/{size}.jpg","acting_avatar_template":"//localhost:3000/uploads/default/avatars/2a8/a3c/8fddcac642/{size}.jpg","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":26,"reply_to_post_number":25,"username":"citkane","name":"Michael Jonker","user_id":7604,"acting_username":"citkane","acting_name":"Michael Jonker","acting_user_id":7604,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-15T11:16:36Z","excerpt":"@eviltrout recently added support for multiple API keys [wink] \n\n[]","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"allow-for-multiple-api-keys","topic_id":7444,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":null,"username":"zogstrip","name":"Régis Hanol","user_id":1995,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Allow for multiple API Keys","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-15T10:58:46Z","excerpt":"@eviltrout added a tooltip when you click on the user's avatar which allows you to show the posts made by that user \n\n[image]","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"to-group-posts-by-a-user","topic_id":7412,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":4,"reply_to_post_number":3,"username":"zogstrip","name":"Régis Hanol","user_id":1995,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"To group posts by a user","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-15T10:36:15Z","excerpt":"@eviltrout implemented per-user API key a while ago [wink] \n\n [image]\nTopics_-_Discourse_Meta-5.png884x339 29.6 KB\n","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"auth-using-rest-api","topic_id":5937,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"zogstrip","name":"Régis Hanol","user_id":1995,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Auth using REST API?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-15T09:55:17Z","excerpt":"@eviltrout has recently introduced this feature and has even blogged about it: \n\n \n \n \n \n eviltrout.com\n \n \n \n \n \n Hiding Offscreen Content in Ember.js - Evil Trout's Blog","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"infinite-scrolling-reusing-dom-nodes","topic_id":5186,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":null,"username":"zogstrip","name":"Régis Hanol","user_id":1995,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Infinite scrolling: Reusing DOM nodes","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-15T00:54:32Z","excerpt":"You can retrieve a user's JSON by making a call to /users/username.json but that assumes you know the user's username. If that's impossible, I would be happy to accept a PR that would return the current user JSON from /session/current-user or something like that. \n\nAdditionally, if you're looking to…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"watchmanmonitor","acting_name":"Watchman Monitoring","acting_user_id":8085,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T21:59:51Z","excerpt":"You can retrieve a user's JSON by making a call to /users/username.json but that assumes you know the user's username. If that's impossible, I would be happy to accept a PR that would return the current user JSON from /session/current-user or something like that. \n\nAdditionally, if you're looking to…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/9cfd2536afac32d209335b092094c12c.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"znation","acting_name":"znation","acting_user_id":8163,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T21:46:50Z","excerpt":"Okay I've fixed the https [point_right] http links on the server side and in the Javascript click tracking as @BhaelOchon pointed out. \n\nLet me know if you find anything else broken.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"broken-links-possibly-related-to-https","topic_id":11831,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Broken links, possibly related to HTTPS","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-14T21:43:28Z","excerpt":"Thanks for your help @eviltrout! I will consider making that change and sending a pull request. I may not get to it for a while. \n\nI am embedding Discourse on another site and it is mostly going well. I have indeed been using your blog for inspiration.","avatar_template":"//www.gravatar.com/avatar/9cfd2536afac32d209335b092094c12c.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/9cfd2536afac32d209335b092094c12c.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"znation","name":"znation","user_id":8163,"acting_username":"znation","acting_name":"znation","acting_user_id":8163,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T21:21:52Z","excerpt":"Okay I've fixed the https [point_right] http links on the server side and in the Javascript click tracking as @BhaelOchon pointed out. \n\nLet me know if you find anything else broken.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"broken-links-possibly-related-to-https","topic_id":11831,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Broken links, possibly related to HTTPS","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T21:03:07Z","excerpt":"Okay I've fixed the https [point_right] http links on the server side and in the Javascript click tracking as @BhaelOchon pointed out. \n\nLet me know if you find anything else broken.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"broken-links-possibly-related-to-https","topic_id":11831,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Broken links, possibly related to HTTPS","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T20:42:51Z","excerpt":"You can retrieve a user's JSON by making a call to /users/username.json but that assumes you know the user's username. If that's impossible, I would be happy to accept a PR that would return the current user JSON from /session/current-user or something like that. \n\nAdditionally, if you're looking to…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T20:29:23Z","excerpt":"You can retrieve a user's JSON by making a call to /users/username.json but that assumes you know the user's username. If that's impossible, I would be happy to accept a PR that would return the current user JSON from /session/current-user or something like that. \n\nAdditionally, if you're looking to…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"get-current-user-information-via-json","topic_id":11959,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Get current user information via JSON","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T19:20:28Z","excerpt":"Perhaps the ['trackpageView'] is not the correct API call? We can probably send more information across such as the URL.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":25,"reply_to_post_number":24,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T19:19:46Z","excerpt":"Nope but I bet you can find one!","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"how-far-to-take-user-documentation","topic_id":11943,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":4,"reply_to_post_number":3,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"How far to take user documentation?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-14T18:37:05Z","excerpt":"I'd be glad to write a pull request to take use there. Is there a specific part of their documentation you have in mind?","avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","slug":"how-far-to-take-user-documentation","topic_id":11943,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"watchmanmonitor","name":"Watchman Monitoring","user_id":8085,"acting_username":"watchmanmonitor","acting_name":"Watchman Monitoring","acting_user_id":8085,"title":"How far to take user documentation?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-14T16:04:28Z","excerpt":"Thanks @eviltrout , the code in the 'bottom of pages' now reads: \n\n<script type="text/javascript">\nDiscourse.PageTracker.current().on('change', function() {\n console.log('tracked!')\n _paq.push(['trackPageView']);\n});\n</script>\n\nThe console is logging 'tracked!' and piwik is logging for each page c…","avatar_template":"//localhost:3000/uploads/default/avatars/2a8/a3c/8fddcac642/{size}.jpg","acting_avatar_template":"//localhost:3000/uploads/default/avatars/2a8/a3c/8fddcac642/{size}.jpg","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":23,"reply_to_post_number":22,"username":"citkane","name":"Michael Jonker","user_id":7604,"acting_username":"citkane","acting_name":"Michael Jonker","acting_user_id":7604,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T15:58:27Z","excerpt":"This topic is now archived. It is frozen and cannot be changed in any way.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"regression-cannot-sort-topic-list","topic_id":11944,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":4,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Regression: Cannot sort topic list","deleted":false,"hidden":false,"moderator_action":true,"edit_reason":null},{"action_type":5,"created_at":"2014-01-14T15:26:57Z","excerpt":"I do think that leading them into the official rails documentation at that point is not a bad idea. Like "congratulations, everything is ready but now you'll need to understand the platform we built it in to be productive."","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"how-far-to-take-user-documentation","topic_id":11943,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"How far to take user documentation?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-14T08:28:00Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-14T00:21:26Z","excerpt":"In pull request 1821, @eviltrout asked: \n\n "About rails s: I wouldn't be against adding it but at what point do we stop holding their hand and expect them to know how rails works? I'm sure rails documentation could do a better job than us. Actually maybe we should just link to that? \n\nWhat point to …","avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/035d12bad251759d8fbc9fb10574d1f6.png?s={size}&r=pg&d=identicon","slug":"how-far-to-take-user-documentation","topic_id":11943,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":1,"reply_to_post_number":null,"username":"watchmanmonitor","name":"Watchman Monitoring","user_id":8085,"acting_username":"watchmanmonitor","acting_name":"Watchman Monitoring","acting_user_id":8085,"title":"How far to take user documentation?","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-13T21:58:28Z","excerpt":"It looks uneeded, but you need to review a fair amount of code to confirm it is not needed. \n\nI am going to keep it for now cause its safer under some weird edge conditions.","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"ruby-question-about-use-of-klass-self-in-the-site-customization-rb","topic_id":11889,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":2,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Ruby question about use of klass=self in the site_customization.rb","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T21:11:32Z","excerpt":"I had to fix an issue with Google analytics so I added a new API hook that can be used. \n\nIf you add the following it should work: \n\n Discourse.PageTracker.current().on('change', function() {\n _paq.push(['trackPageView']);\n});","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-13T21:10:57Z","excerpt":"Having a look, the fix is a bit scary imho, we should fix the root issue.","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"error-after-update-to-0-9-8-1","topic_id":11903,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":11,"reply_to_post_number":10,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Error after update to 0.9.8.1","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T20:50:34Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//localhost:3000/uploads/default/avatars/527/614/d16e1504d9/{size}.jpg","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"trident","acting_name":"Ben T","acting_user_id":5707,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T20:44:56Z","excerpt":"I had to fix an issue with Google analytics so I added a new API hook that can be used. \n\nIf you add the following it should work: \n\n Discourse.PageTracker.current().on('change', function() {\n _paq.push(['trackPageView']);\n});","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T20:40:21Z","excerpt":"I had to fix an issue with Google analytics so I added a new API hook that can be used. \n\nIf you add the following it should work: \n\n Discourse.PageTracker.current().on('change', function() {\n _paq.push(['trackPageView']);\n});","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T19:52:04Z","excerpt":"@Sam do you have any idea why only some people are getting this issue? I dont' mind the proposed fix but I'd prefer to know why it happens in the first place.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"error-after-update-to-0-9-8-1","topic_id":11903,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":10,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Error after update to 0.9.8.1","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T19:01:19Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T18:50:14Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T18:47:33Z","excerpt":"I am pretty sure that the denizens of SO are correct and the variable is unneeded. @sam can confirm but it seems like it was once needed for something that has since been removed and the variable declaration was left intact.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"ruby-question-about-use-of-klass-self-in-the-site-customization-rb","topic_id":11889,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Ruby question about use of klass=self in the site_customization.rb","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T18:45:41Z","excerpt":"I've just added the ability to list reply counts on your blog index and archive pages as you can see here. \n\nIt works with a similar API to embedding comments: \n\n <script type="text/javascript">\n var discourseUrl = "http://fishtank.eviltrout.com/";\n\n (function() {\n var d = document.createEleme…","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"discourse-plugin-for-static-site-generators-like-jekyll-or-octopress","topic_id":7965,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":98,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Discourse plugin for static site generators like Jekyll or Octopress","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T17:19:08Z","excerpt":"@Sam do you have any idea why only some people are getting this issue? I dont' mind the proposed fix but I'd prefer to know why it happens in the first place.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/5120fc4e345db0d1a964888272073819.png?s={size}&r=pg&d=identicon","slug":"error-after-update-to-0-9-8-1","topic_id":11903,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":10,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"riking","acting_name":"Kane York","acting_user_id":6626,"title":"Error after update to 0.9.8.1","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-13T16:41:31Z","excerpt":"I'd love to see API support. @sam and @eviltrout, I can facilitate an intro to the piwik guys if you want—I've written about them before and they're typically super-responsive. Because I know you guys are totally hunting for new stuff to do [wink]","avatar_template":"//localhost:3000/uploads/default/avatars/95a/06d/c337428568/{size}.png","acting_avatar_template":"//localhost:3000/uploads/default/avatars/95a/06d/c337428568/{size}.png","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":20,"reply_to_post_number":null,"username":"Lee_Ars","name":"Lee_Ars","user_id":4457,"acting_username":"Lee_Ars","acting_name":"Lee_Ars","acting_user_id":4457,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-13T16:15:51Z","excerpt":"The code looks okay but it's hard to debug this way. \n\nOne thing you could do is add a: console.log('tracked!') just before line 8. Then open a developer console and see if the javascript is running properly.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T15:10:41Z","excerpt":"This is really interesting. I'd like to hear your findings.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"focus-events-track-which-window-is-the-last-active-instance-of-a-forum-edit","topic_id":11872,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":9,"reply_to_post_number":8,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Focus events: Track which window is the last active instance of a forum Edit","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T15:02:45Z","excerpt":"The code looks okay but it's hard to debug this way. \n\nOne thing you could do is add a: console.log('tracked!') just before line 8. Then open a developer console and see if the javascript is running properly.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":18,"reply_to_post_number":16,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":5,"created_at":"2014-01-13T14:53:13Z","excerpt":"@Sam do you have any idea why only some people are getting this issue? I dont' mind the proposed fix but I'd prefer to know why it happens in the first place.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"error-after-update-to-0-9-8-1","topic_id":11903,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":10,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Error after update to 0.9.8.1","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-13T06:27:26Z","excerpt":"Can this be archived @eviltrout?","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"search-not-working-for-staff-users","topic_id":11371,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":13,"reply_to_post_number":null,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Search not working for Staff users","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-13T05:32:46Z","excerpt":"When you navigate to another topic using the "suggested topics" area we are not registering a page view with Google. \n\n@eviltrout perhaps we should do this from discourse location instead of application controller?","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"google-analytics-is-not-registering-page-views","topic_id":11914,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":1,"reply_to_post_number":null,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Google analytics is not registering page views","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-13T02:50:25Z","excerpt":"@eviltrout any ideas here, the code seems correct","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"support-for-piwik-analytics-as-an-alternative-to-google-analytics","topic_id":7512,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":17,"reply_to_post_number":16,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Support for Piwik Analytics as an alternative to Google Analytics","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-12T22:31:35Z","excerpt":"This is an interesting approach an an interesting feature. @eviltrout your thoughts. Essentially allows us to have notifications cross tabs.","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"focus-events-track-which-window-is-the-last-active-instance-of-a-forum-edit","topic_id":11872,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":4,"reply_to_post_number":1,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Focus events: Track which window is the last active instance of a forum Edit","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-12T18:01:04Z","excerpt":"This was the link \n\nmetric_fu \n\n[metric_fu](https://github.com/metricfu/metric_fu/blob/b1bf8feb921916fc265f041efa3157a6a6530a9b/lib/metric_fu/logging/mf_debugger.rb#L24)\n\nSeems to work fine now that @eviltrout worked so hard to get us MDTest 1.1 compliant.","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"underscores-in-linked-text-can-cause-markdown-bug","topic_id":10848,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":null,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Underscores in linked text can cause markdown bug","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-12T04:14:06Z","excerpt":"Awesome plugin, but doesn't seem to work out of the box with images \n\nhttps://github.com/discourse/discourse-spoiler-alert/issues/2","avatar_template":"//localhost:3000/uploads/default/avatars/276/f19/3826efe463/{size}.jpg","acting_avatar_template":"//localhost:3000/uploads/default/avatars/276/f19/3826efe463/{size}.jpg","slug":"brand-new-plugin-interface","topic_id":8793,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":64,"reply_to_post_number":44,"username":"xrvk","name":"Eero Heikkinen","user_id":8068,"acting_username":"xrvk","acting_name":"Eero Heikkinen","acting_user_id":8068,"title":"Brand new plugin interface","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-11T23:36:11Z","excerpt":"A few things, \n\n@eviltrout myself and many others have discourse_docker hosted on DigitalOcean, my user cpu is usually around 2% I have plenty of capacity. \n\nI know that stonehearth and other larger scale discourse work on DigitalOcean fine. Officially we strongly recommend a 2GB instance, thoug…","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon","slug":"performance-issue-on-digital-ocean-with-discourse-docker","topic_id":11895,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":2,"reply_to_post_number":null,"username":"sam","name":"Sam Saffron","user_id":1,"acting_username":"sam","acting_name":"Sam Saffron","acting_user_id":1,"title":"Performance issue on DigitalOcean with discourse_docker","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-11T00:58:23Z","excerpt":"Confirmed on try.discourse.org, this is still an issue. \n\n@eviltrout can you add that to your list -- unless you are a staff member you should not be able to delete (your own) posts from an archived topic.","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"archived-discussions-still-allow-posts-to-be-deleted","topic_id":6479,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":3,"reply_to_post_number":null,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Archived discussions still allow posts to be deleted","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-11T00:35:38Z","excerpt":"Agree, @eviltrout can you make sure the usercard is using the same logic as the user page in displaying profile info?","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"usercard-does-not-resize-for-obnoxiously-large-images","topic_id":11007,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":5,"reply_to_post_number":4,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Usercard does not resize for obnoxiously large images","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-11T00:34:06Z","excerpt":"@eviltrout can you make sure the "import post" button is suppressed on the user page when editing "about me"? \n\n(I agree it is like a "lose all my work" button on that page if you happen to press it..) \n\nThen I can archive this.","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"quote-post-button-should-be-disabled-or-raise-an-error-when-creating-a-new-topic","topic_id":834,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":5,"reply_to_post_number":4,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"\"Quote Post\" button should be disabled or raise an error when creating a new topic","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":7,"created_at":"2014-01-10T21:00:11Z","excerpt":">\n\nLooks good now. Thanks for these fixes @eviltrout, we (and markdown-js) are now MDTest 1.1 compliant!","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"text-editor-issue-with-the-code-block","topic_id":10050,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":5,"reply_to_post_number":null,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Text Editor issue with the code block","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":1,"created_at":"2014-01-10T20:07:46Z","excerpt":"We can't repro that one, also seems a bit obscure. But thank you very much for all the reports, whenever I see a bug entry from YOU I always know it is going to be a good one based on experience here and elsewhere. [trophy]","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","slug":"security-error-on-console-noticed-on-meta","topic_id":11825,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":12,"reply_to_post_number":11,"username":"codinghorror","name":"Jeff Atwood","user_id":32,"acting_username":"eviltrout","acting_name":"Robin Ward","acting_user_id":19,"title":"Security Error on console (noticed on meta)","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T19:48:08Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"codinghorror","acting_name":"Jeff Atwood","acting_user_id":32,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T19:47:17Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/42776c4982dff1fa45ee8248532f8ad0.png?s={size}&r=pg&d=identicon","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"neil","acting_name":"Neil","acting_user_id":2,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T17:39:24Z","excerpt":"We should consider doing what Google Drive does: they intercept cmd-f and pop up a box that allows you to dynamically search.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/5120fc4e345db0d1a964888272073819.png?s={size}&r=pg&d=identicon","slug":"ctrl-f-search-is-interrupted-by-quotation-popup","topic_id":7114,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":12,"reply_to_post_number":11,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"riking","acting_name":"Kane York","acting_user_id":6626,"title":"Ctrl+F search is interrupted by quotation popup","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T17:29:15Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/5120fc4e345db0d1a964888272073819.png?s={size}&r=pg&d=identicon","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"riking","acting_name":"Kane York","acting_user_id":6626,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T17:24:37Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"zogstrip","acting_name":"Régis Hanol","acting_user_id":1995,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":6,"created_at":"2014-01-10T17:02:35Z","excerpt":"Fixed [smile] \n\ntop - 12:02:00 up 12 days, 2:16, 1 user, load average: 0.28, 0.92, 0.97\nTasks: 115 total, 1 running, 114 sleeping, 0 stopped, 0 zombie\nCpu0 : 0.7%us, 0.3%sy, 0.0%ni, 99.0%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st\nCpu1 : 0.7%us, 0.3%sy, 0.0%ni, 99.0%id, 0.0%wa, 0.0%hi,…","avatar_template":"//localhost:3000/uploads/default/avatars/886/ea8/e533d87fd9/{size}.png","acting_avatar_template":"//localhost:3000/uploads/default/avatars/886/ea8/e533d87fd9/{size}.png","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":23,"reply_to_post_number":22,"username":"michaeld","name":"Michael","user_id":6548,"acting_username":"michaeld","acting_name":"Michael","acting_user_id":6548,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null},{"action_type":2,"created_at":"2014-01-10T16:58:12Z","excerpt":"Thanks for letting us know. It turns out that by using minutely(5) instead of minutely causes ice_cube to peg a core at 100% usage. I've pushed out a fix in master.","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","acting_avatar_template":"//localhost:3000/uploads/default/avatars/527/614/d16e1504d9/{size}.jpg","slug":"sidekiq-cpu-load-since-latest-release","topic_id":9515,"target_user_id":19,"target_name":"Robin Ward","target_username":"eviltrout","post_number":22,"reply_to_post_number":null,"username":"eviltrout","name":"Robin Ward","user_id":19,"acting_username":"trident","acting_name":"Ben T","acting_user_id":5707,"title":"Sidekiq CPU load since latest release","deleted":false,"hidden":false,"moderator_action":false,"edit_reason":null}]}, "/topics/created-by/eviltrout.json": {"users":[{"id":19,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon"},{"id":5460,"username":"ned","avatar_template":"//localhost:3000/uploads/default/avatars/06b/90d/3b3ea7e56b/{size}.png"},{"id":402,"username":"thebrianbarlow","avatar_template":"//www.gravatar.com/avatar/5ddf2459e8edd6cf52dfff6cb41ca70d.png?s={size}&r=pg&d=identicon"},{"id":5707,"username":"trident","avatar_template":"//localhost:3000/uploads/default/avatars/527/614/d16e1504d9/{size}.jpg"},{"id":32,"username":"codinghorror","avatar_template":"//www.gravatar.com/avatar/51d623f33f8b83095db84ff35e15dbe8.png?s={size}&r=pg&d=identicon"},{"id":1995,"username":"zogstrip","avatar_template":"//www.gravatar.com/avatar/b7797beb47cfb7aa0fe60d09604aaa09.png?s={size}&r=pg&d=identicon"},{"id":2702,"username":"ryanflorence","avatar_template":"//www.gravatar.com/avatar/749001c9fe6927c4b069a45c2a3d68f7.png?s={size}&r=pg&d=identicon"},{"id":9,"username":"tms","avatar_template":"//www.gravatar.com/avatar/3981cd271c302f5cba628c6b6d2b32ee.png?s={size}&r=pg&d=identicon"},{"id":1,"username":"sam","avatar_template":"//www.gravatar.com/avatar/3dcae8378d46c244172a115c28ca49ce.png?s={size}&r=pg&d=identicon"},{"id":2636,"username":"lonnon","avatar_template":"//www.gravatar.com/avatar/9489ef302fbff6c19bba507d09f8cd1d.png?s={size}&r=pg&d=identicon"}],"topic_list":{"can_create_topic":false,"draft":null,"draft_key":"new_topic","draft_sequence":null,"topics":[{"id":7764,"title":"New: Reply via Email Support!","fancy_title":"New: Reply via Email Support!","slug":"new-reply-via-email-support","posts_count":32,"reply_count":24,"highest_post_number":35,"image_url":"/uploads/meta_discourse/1227/8f4e5818dfaa56c7.png","created_at":"2013-06-25T11:58:39.000-04:00","last_posted_at":"2014-01-09T18:53:06.000-05:00","bumped":true,"bumped_at":"2014-01-09T17:09:40.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":2201,"like_count":46,"has_summary":false,"archetype":"regular","last_poster_username":"codinghorror","category_id":2,"posters":[{"extras":null,"description":"Original Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":5460},{"extras":null,"description":"Frequent Poster","user_id":402},{"extras":null,"description":"Frequent Poster","user_id":5707},{"extras":"latest","description":"Most Recent Poster","user_id":32}]},{"id":9318,"title":"Discourse has a new Markdown Parser!","fancy_title":"Discourse has a new Markdown Parser!","slug":"discourse-has-a-new-markdown-parser","posts_count":1,"reply_count":0,"highest_post_number":1,"image_url":null,"created_at":"2013-08-24T14:08:06.000-04:00","last_posted_at":"2013-08-24T14:08:06.000-04:00","bumped":true,"bumped_at":"2013-08-24T14:13:25.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":812,"like_count":13,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":19}]},{"id":7019,"title":"Discourse Ember Refactorings","fancy_title":"Discourse Ember Refactorings","slug":"discourse-ember-refactorings","posts_count":5,"reply_count":3,"highest_post_number":5,"image_url":null,"created_at":"2013-05-30T11:16:36.000-04:00","last_posted_at":"2013-06-02T11:22:58.000-04:00","bumped":true,"bumped_at":"2013-06-02T11:22:58.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":1075,"like_count":15,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":7,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":1995},{"extras":null,"description":"Frequent Poster","user_id":2702}]},{"id":4650,"title":"Migrating off Active Record Observers","fancy_title":"Migrating off Active Record Observers","slug":"migrating-off-active-record-observers","posts_count":8,"reply_count":7,"highest_post_number":8,"image_url":null,"created_at":"2013-03-11T11:26:13.000-04:00","last_posted_at":"2013-05-14T18:40:16.000-04:00","bumped":true,"bumped_at":"2013-05-14T18:40:16.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":377,"like_count":3,"has_summary":false,"archetype":"regular","last_poster_username":"sam","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":9},{"extras":null,"description":"Frequent Poster","user_id":1995},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":1}]},{"id":4960,"title":"Vagrant Updates!","fancy_title":"Vagrant Updates!","slug":"vagrant-updates","posts_count":5,"reply_count":3,"highest_post_number":5,"image_url":"/plugins/emoji/images/fish.png","created_at":"2013-03-20T22:29:22.000-04:00","last_posted_at":"2013-03-21T19:06:40.000-04:00","bumped":true,"bumped_at":"2013-03-21T19:06:40.000-04:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":500,"like_count":4,"has_summary":false,"archetype":"regular","last_poster_username":"zogstrip","category_id":7,"posters":[{"extras":null,"description":"Original Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":1},{"extras":null,"description":"Frequent Poster","user_id":32},{"extras":"latest","description":"Most Recent Poster","user_id":1995}]},{"id":2918,"title":"New: Updated Docs","fancy_title":"New: Updated Docs","slug":"new-updated-docs","posts_count":3,"reply_count":2,"highest_post_number":3,"image_url":null,"created_at":"2013-02-12T12:13:02.000-05:00","last_posted_at":"2013-02-15T17:57:19.000-05:00","bumped":true,"bumped_at":"2013-02-15T17:57:19.000-05:00","unseen":false,"pinned":false,"visible":true,"closed":false,"archived":false,"views":457,"like_count":10,"has_summary":false,"archetype":"regular","last_poster_username":"eviltrout","category_id":10,"posters":[{"extras":"latest","description":"Original Poster, Most Recent Poster","user_id":19},{"extras":null,"description":"Most Posts","user_id":2636}]}]}} }; diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index 770e5e0d70..2597fa6b9f 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -64,18 +64,21 @@ export default function() { return response(json); }); + this.get('/clicks/track', success); + this.put('/users/eviltrout', () => response({ user: {} })); this.get("/t/280.json", () => response(fixturesByUrl['/t/280/1.json'])); - this.get("/t/28830.json", () => response(fixturesByUrl['/t/28830/1.json'])); - this.get("/t/9.json", () => response(fixturesByUrl['/t/9/1.json'])); this.get("/t/id_for/:slug", () => { return response({id: 280, slug: "internationalization-localization", url: "/t/internationalization-localization/280"}); }); + this.delete('/t/:id', success); + this.put('/t/:id/recover', success); + this.get("/404-body", () => { return [200, {"Content-Type": "text/html"}, "
not found
"]; }); @@ -196,6 +199,9 @@ export default function() { return response(200, [ { id: 2222, post_number: 2222 } ]); }); + this.post('/user_badges', () => response(200, fixturesByUrl['/user_badges'])); + this.delete('/user_badges/:badge_id', success); + this.post('/posts', function(request) { const data = parsePostData(request.requestBody); @@ -238,6 +244,10 @@ export default function() { }); this.get('/tag_groups', () => response(200, {tag_groups: []})); + this.post('/admin/users/:user_id/generate_api_key', success); + this.delete('/admin/users/:user_id/revoke_api_key', success); + this.post('/admin/badges', success); + this.delete('/admin/badges/:id', success); }); server.prepareBody = function(body){ diff --git a/test/javascripts/lib/click-track-edit-history-test.js.es6 b/test/javascripts/lib/click-track-edit-history-test.js.es6 index f0973679be..4350ab6d22 100644 --- a/test/javascripts/lib/click-track-edit-history-test.js.es6 +++ b/test/javascripts/lib/click-track-edit-history-test.js.es6 @@ -12,7 +12,6 @@ module("lib:click-track-edit-history", { // Prevent any of these tests from navigating away win = {focus: function() { } }; redirectTo = sandbox.stub(DiscourseURL, "redirectTo"); - sandbox.stub(Discourse, "ajax"); windowOpen = sandbox.stub(window, "open").returns(win); sandbox.stub(win, "focus"); @@ -141,7 +140,6 @@ var testOpenInANewTab = function(description, clickEventModifier) { clickEventModifier(clickEvent); sandbox.stub(clickEvent, "preventDefault"); ok(track(clickEvent)); - ok(Discourse.ajax.calledOnce); ok(!clickEvent.preventDefault.calledOnce); }); }; @@ -167,7 +165,6 @@ test("tracks via AJAX if we're on the same site", function() { sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); ok(!track(generateClickEventOn('#same-site'))); - ok(Discourse.ajax.calledOnce); ok(DiscourseURL.routeTo.calledOnce); }); diff --git a/test/javascripts/lib/click-track-profile-page-test.js.es6 b/test/javascripts/lib/click-track-profile-page-test.js.es6 index cf6233fc3f..bc0b46e559 100644 --- a/test/javascripts/lib/click-track-profile-page-test.js.es6 +++ b/test/javascripts/lib/click-track-profile-page-test.js.es6 @@ -12,7 +12,6 @@ module("lib:click-track-profile-page", { // Prevent any of these tests from navigating away win = {focus: function() { } }; redirectTo = sandbox.stub(DiscourseURL, "redirectTo"); - sandbox.stub(Discourse, "ajax"); windowOpen = sandbox.stub(window, "open").returns(win); sandbox.stub(win, "focus"); @@ -141,7 +140,6 @@ var testOpenInANewTab = function(description, clickEventModifier) { clickEventModifier(clickEvent); sandbox.stub(clickEvent, "preventDefault"); ok(track(clickEvent)); - ok(Discourse.ajax.calledOnce); ok(!clickEvent.preventDefault.calledOnce); }); }; @@ -167,7 +165,6 @@ test("tracks via AJAX if we're on the same site", function() { sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); ok(!track(generateClickEventOn('#same-site'))); - ok(Discourse.ajax.calledOnce); ok(DiscourseURL.routeTo.calledOnce); }); diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6 index 806b6b5575..bebe653d6f 100644 --- a/test/javascripts/lib/click-track-test.js.es6 +++ b/test/javascripts/lib/click-track-test.js.es6 @@ -12,7 +12,6 @@ module("lib:click-track", { // Prevent any of these tests from navigating away win = {focus: function() { } }; redirectTo = sandbox.stub(DiscourseURL, "redirectTo"); - sandbox.stub(Discourse, "ajax"); windowOpen = sandbox.stub(window, "open").returns(win); sandbox.stub(win, "focus"); @@ -161,7 +160,6 @@ var testOpenInANewTab = function(description, clickEventModifier) { clickEventModifier(clickEvent); sandbox.stub(clickEvent, "preventDefault"); ok(track(clickEvent)); - ok(Discourse.ajax.calledOnce); ok(!clickEvent.preventDefault.calledOnce); }); }; @@ -187,7 +185,6 @@ test("tracks via AJAX if we're on the same site", function() { sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); ok(!track(generateClickEventOn('#same-site'))); - ok(Discourse.ajax.calledOnce); ok(DiscourseURL.routeTo.calledOnce); }); diff --git a/test/javascripts/models/badge-test.js.es6 b/test/javascripts/models/badge-test.js.es6 index 0b92f88ce4..644056d049 100644 --- a/test/javascripts/models/badge-test.js.es6 +++ b/test/javascripts/models/badge-test.js.es6 @@ -37,19 +37,15 @@ test('updateFromJson', function() { }); test('save', function() { - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({})); + expect(0); const badge = Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1}); - // TODO: clean API - badge.save(["name", "description", "badge_type_id"]); - ok(Discourse.ajax.calledOnce, "saved badge"); + return badge.save(["name", "description", "badge_type_id"]); }); test('destroy', function() { - sandbox.stub(Discourse, 'ajax'); + expect(0); const badge = Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1}); badge.destroy(); - ok(!Discourse.ajax.calledOnce, "no AJAX call for a new badge"); badge.set('id', 3); - badge.destroy(); - ok(Discourse.ajax.calledOnce, "AJAX call was made"); + return badge.destroy(); }); diff --git a/test/javascripts/models/topic-test.js.es6 b/test/javascripts/models/topic-test.js.es6 index 7938b7afd9..2c1ceac923 100644 --- a/test/javascripts/models/topic-test.js.es6 +++ b/test/javascripts/models/topic-test.js.es6 @@ -55,24 +55,18 @@ test("destroy", function() { var user = Discourse.User.create({username: 'eviltrout'}); var topic = Topic.create({id: 1234}); - sandbox.stub(Discourse, 'ajax'); - topic.destroy(user); present(topic.get('deleted_at'), 'deleted at is set'); equal(topic.get('deleted_by'), user, 'deleted by is set'); - //ok(Discourse.ajax.calledOnce, "it called delete over the wire"); }); test("recover", function() { var user = Discourse.User.create({username: 'eviltrout'}); var topic = Topic.create({id: 1234, deleted_at: new Date(), deleted_by: user}); - sandbox.stub(Discourse, 'ajax'); - topic.recover(); blank(topic.get('deleted_at'), "it clears deleted_at"); blank(topic.get('deleted_by'), "it clears deleted_by"); - //ok(Discourse.ajax.calledOnce, "it called recover over the wire"); }); test('fancyTitle', function() { diff --git a/test/javascripts/models/user-badge-test.js.es6 b/test/javascripts/models/user-badge-test.js.es6 index bf2a5bf98a..5258d98b06 100644 --- a/test/javascripts/models/user-badge-test.js.es6 +++ b/test/javascripts/models/user-badge-test.js.es6 @@ -1,12 +1,10 @@ import UserBadge from 'discourse/models/user-badge'; +import badgeFixtures from 'fixtures/user-badges'; module("model:user-badge"); -const singleBadgeJson = {"badges":[{"id":874,"name":"Badge 2","description":null,"badge_type_id":7}],"badge_types":[{"id":7,"name":"Silver 2"}],"users":[{"id":13470,"username":"anne3","avatar_template":"//www.gravatar.com/avatar/a4151b1fd72089c54e2374565a87da7f.png?s={size}\u0026r=pg\u0026d=identicon"}],"user_badge":{"id":665,"granted_at":"2014-03-09T20:30:01.190-04:00","badge_id":874,"granted_by_id":13470}}, - multipleBadgesJson = {"badges":[{"id":880,"name":"Badge 8","description":null,"badge_type_id":13}],"badge_types":[{"id":13,"name":"Silver 8"}],"users":[],"user_badges":[{"id":668,"granted_at":"2014-03-09T20:30:01.420-04:00","badge_id":880,"granted_by_id":null}]}; - test('createFromJson single', function() { - const userBadge = UserBadge.createFromJson(singleBadgeJson); + const userBadge = UserBadge.createFromJson(badgeFixtures['/user_badges']); ok(!Array.isArray(userBadge), "does not return an array"); equal(userBadge.get('badge.name'), "Badge 2", "badge reference is set"); equal(userBadge.get('badge.badge_type.name'), "Silver 2", "badge.badge_type reference is set"); @@ -14,44 +12,31 @@ test('createFromJson single', function() { }); test('createFromJson array', function() { - const userBadges = UserBadge.createFromJson(multipleBadgesJson); + const userBadges = UserBadge.createFromJson(badgeFixtures['/user-badges/:username']); ok(Array.isArray(userBadges), "returns an array"); equal(userBadges[0].get('granted_by'), null, "granted_by reference is not set when null"); }); -asyncTestDiscourse('findByUsername', function() { - expect(2); - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(multipleBadgesJson)); - UserBadge.findByUsername("anne3").then(function(badges) { +test('findByUsername', function() { + return UserBadge.findByUsername("anne3").then(function(badges) { ok(Array.isArray(badges), "returns an array"); - start(); }); - ok(Discourse.ajax.calledOnce, "makes an AJAX call"); }); -asyncTestDiscourse('findByBadgeId', function() { - expect(2); - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(multipleBadgesJson)); - UserBadge.findByBadgeId(880).then(function(badges) { +test('findByBadgeId', function() { + return UserBadge.findByBadgeId(880).then(function(badges) { ok(Array.isArray(badges), "returns an array"); - start(); }); - ok(Discourse.ajax.calledOnce, "makes an AJAX call"); }); -asyncTestDiscourse('grant', function() { - expect(2); - sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(singleBadgeJson)); - UserBadge.grant(1, "username").then(function(userBadge) { +test('grant', function() { + return UserBadge.grant(1, "username").then(function(userBadge) { ok(!Array.isArray(userBadge), "does not return an array"); - start(); }); - ok(Discourse.ajax.calledOnce, "makes an AJAX call"); }); test('revoke', function() { - sandbox.stub(Discourse, 'ajax'); + expect(0); const userBadge = UserBadge.create({id: 1}); - userBadge.revoke(); - ok(Discourse.ajax.calledOnce, "makes an AJAX call"); + return userBadge.revoke(); }); From 7ff5b228cd92e80c14a1962e5ea88e2374d69ddd Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 30 Jun 2016 17:10:08 -0400 Subject: [PATCH 004/170] REFACTOR: Raw Handlebars ported to ES6 --- .../discourse/lib/ember_compat_handlebars.js | 160 ----------------- .../javascripts/discourse/lib/helpers.js.es6 | 4 +- .../discourse/lib/raw-handlebars.js.es6 | 164 ++++++++++++++++++ app/assets/javascripts/main_include.js | 2 +- app/models/site_customization.rb | 2 +- ...compat_handlebars.rb => raw_handlebars.rb} | 37 ++-- spec/models/site_customization_spec.rb | 4 +- .../acceptance/category-hashtag-test.js.es6 | 14 +- 8 files changed, 198 insertions(+), 189 deletions(-) delete mode 100644 app/assets/javascripts/discourse/lib/ember_compat_handlebars.js create mode 100644 app/assets/javascripts/discourse/lib/raw-handlebars.js.es6 rename lib/freedom_patches/{ember_compat_handlebars.rb => raw_handlebars.rb} (54%) diff --git a/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js b/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js deleted file mode 100644 index 6539100a49..0000000000 --- a/app/assets/javascripts/discourse/lib/ember_compat_handlebars.js +++ /dev/null @@ -1,160 +0,0 @@ -// keep IIF for simpler testing - -// EmberCompatHandlebars is a mechanism for quickly rendering templates which is Ember aware -// templates are highly compatible with Ember so you don't need to worry about calling "get" -// and computed properties function, additionally it uses stringParams like Ember does - -(function(){ - - // compat with ie8 in case this gets picked up elsewhere - var objectCreate = Object.create || function(parent) { - function F() {} - F.prototype = parent; - return new F(); - }; - - - var RawHandlebars = Handlebars.create(); - - RawHandlebars.helper = function() {}; - RawHandlebars.helpers = objectCreate(Handlebars.helpers); - - RawHandlebars.helpers.get = function(context, options){ - var firstContext = options.contexts[0]; - var val = firstContext[context]; - - if (val && val.isDescriptor) { return Em.get(firstContext, context); } - val = val === undefined ? Em.get(firstContext, context): val; - return val; - }; - - // adds compatability so this works with stringParams - var stringCompatHelper = function(fn){ - - var old = RawHandlebars.helpers[fn]; - RawHandlebars.helpers[fn] = function(context,options){ - return old.apply(this, [ - RawHandlebars.helpers.get(context,options), - options - ]); - }; - }; - - // #each .. in support (as format is transformed to this) - RawHandlebars.registerHelper('each', function(localName,inKeyword,contextName,options){ - var list = Em.get(this, contextName); - var output = []; - var innerContext = Object.create(this); - for (var i=0; i { - return [ - 200, - {"Content-Type": "application/json"}, - object - ]; - }; - - } -}); +acceptance("Category hashtag", { loggedIn: true }); test("category hashtag is cooked properly", () => { visit("/t/internationalization-localization/280"); From 25d6915cac72eec02e03dd12c63b71919094475a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Sun, 3 Jul 2016 13:33:05 -0400 Subject: [PATCH 005/170] Migrate discourse.js to ES6 --- app/assets/javascripts/deprecated.js | 25 ++++ .../{discourse.js => discourse.js.es6} | 119 +++++++----------- app/assets/javascripts/ember-shim.js | 4 + app/assets/javascripts/ember_jquery.js | 6 +- app/assets/javascripts/main_include.js | 7 +- app/assets/javascripts/vendor.js | 1 - .../common/_discourse_javascript.html.erb | 1 + test/javascripts/test_helper.js | 8 +- 8 files changed, 89 insertions(+), 82 deletions(-) create mode 100644 app/assets/javascripts/deprecated.js rename app/assets/javascripts/{discourse.js => discourse.js.es6} (56%) create mode 100644 app/assets/javascripts/ember-shim.js diff --git a/app/assets/javascripts/deprecated.js b/app/assets/javascripts/deprecated.js new file mode 100644 index 0000000000..2303ec5d2c --- /dev/null +++ b/app/assets/javascripts/deprecated.js @@ -0,0 +1,25 @@ +(function() { + var Discourse = require('discourse').default; + + Discourse.Markdown = { + whiteListTag: Ember.K, + whiteListIframe: Ember.K + }; + + Discourse.Dialect = { + inlineRegexp: Ember.K, + addPreProcessor: Ember.K, + replaceBlock: Ember.K, + inlineReplace: Ember.K, + registerInline: Ember.K, + registerEmoji: Ember.K + }; + + Discourse.ajax = function() { + var ajax = require('discourse/lib/ajax').ajax; + Ember.warn("Discourse.ajax is deprecated. Import the module and use it instead"); + return ajax.apply(this, arguments); + }; + + window.Discourse = Discourse; +})(); diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js.es6 similarity index 56% rename from app/assets/javascripts/discourse.js rename to app/assets/javascripts/discourse.js.es6 index fb983d9438..23c233c7c4 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js.es6 @@ -1,19 +1,14 @@ -/*global Favcount:true*/ -var DiscourseResolver = require('discourse/ember/resolver').default; +import DiscourseResolver from 'discourse/ember/resolver'; +import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; -// Allow us to import Ember -define('ember', ['exports'], function(__exports__) { - __exports__.default = Ember; -}); +const _pluginCallbacks = []; -var _pluginCallbacks = []; - -window.Discourse = Ember.Application.extend({ +const Discourse = Ember.Application.extend({ rootElement: '#main', _docTitle: document.title, __TAGS_INCLUDED__: true, - getURL: function(url) { + getURL(url) { if (!url) return url; // if it's a non relative URL, return it. @@ -25,7 +20,7 @@ window.Discourse = Ember.Application.extend({ return Discourse.BaseUri + url; }, - getURLWithCDN: function(url) { + getURLWithCDN(url) { url = Discourse.getURL(url); // only relative urls if (Discourse.CDN && /^\/[^\/]/.test(url)) { @@ -38,78 +33,76 @@ window.Discourse = Ember.Application.extend({ Resolver: DiscourseResolver, - _titleChanged: function() { - var title = this.get('_docTitle') || Discourse.SiteSettings.title; + @observes('_docTitle', 'hasFocus', 'notifyCount') + _titleChanged() { + let title = this.get('_docTitle') || Discourse.SiteSettings.title; // if we change this we can trigger changes on document.title // only set if changed. - if($('title').text() !== title) { + if ($('title').text() !== title) { $('title').text(title); } - var notifyCount = this.get('notifyCount'); + const notifyCount = this.get('notifyCount'); if (notifyCount > 0 && !Discourse.User.currentProp('dynamic_favicon')) { - title = "(" + notifyCount + ") " + title; + title = `(${notifyCount}) ${title}`; } document.title = title; - }.observes('_docTitle', 'hasFocus', 'notifyCount'), + }, - faviconChanged: function() { - if(Discourse.User.currentProp('dynamic_favicon')) { - var url = Discourse.SiteSettings.favicon_url; + @observes('notifyCount') + faviconChanged() { + if (Discourse.User.currentProp('dynamic_favicon')) { + let url = Discourse.SiteSettings.favicon_url; if (/^http/.test(url)) { url = Discourse.getURL("/favicon/proxied?" + encodeURIComponent(url)); } - new Favcount(url).set( - this.get('notifyCount') - ); + new window.Favcount(url).set(this.get('notifyCount')); } - }.observes('notifyCount'), + }, // The classes of buttons to show on a post - postButtons: function() { + @computed + postButtons() { return Discourse.SiteSettings.post_menu.split("|").map(function(i) { return i.replace(/\+/, '').capitalize(); }); - }.property(), + }, - notifyTitle: function(count) { + notifyTitle(count) { this.set('notifyCount', count); }, - notifyBackgroundCountIncrement: function() { + notifyBackgroundCountIncrement() { if (!this.get('hasFocus')) { this.set('backgroundNotify', true); this.set('notifyCount', (this.get('notifyCount') || 0) + 1); } }, - resetBackgroundNotifyCount: function() { + @observes('hasFocus') + resetBackgroundNotifyCount() { if (this.get('hasFocus') && this.get('backgroundNotify')) { this.set('notifyCount', 0); } this.set('backgroundNotify', false); - }.observes('hasFocus'), + }, - authenticationComplete: function(options) { + authenticationComplete(options) { // TODO, how to dispatch this to the controller without the container? - var loginController = Discourse.__container__.lookup('controller:login'); + const loginController = Discourse.__container__.lookup('controller:login'); return loginController.authenticationComplete(options); }, - /** - Start up the Discourse application by running all the initializers we've defined. - - @method start - **/ - start: function() { + // Start up the Discourse application by running all the initializers we've defined. + start() { $('noscript').remove(); Object.keys(requirejs._eak_seen).forEach(function(key) { if (/\/pre\-initializers\//.test(key)) { - var module = require(key, null, null, true); + const module = require(key, null, null, true); if (!module) { throw new Error(key + ' must export an initializer.'); } Discourse.initializer(module.default); } @@ -117,11 +110,11 @@ window.Discourse = Ember.Application.extend({ Object.keys(requirejs._eak_seen).forEach(function(key) { if (/\/initializers\//.test(key)) { - var module = require(key, null, null, true); + const module = require(key, null, null, true); if (!module) { throw new Error(key + ' must export an initializer.'); } - var init = module.default; - var oldInitialize = init.initialize; + const init = module.default; + const oldInitialize = init.initialize; init.initialize = function(app) { oldInitialize.call(this, app.container, Discourse); }; @@ -131,8 +124,8 @@ window.Discourse = Ember.Application.extend({ }); // Plugins that are registered via ` <%= script 'browser-update' %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index cac200b7da..3b8b4ccc61 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -22,9 +22,9 @@ window.EmberENV['FORCE_JQUERY'] = true; - <%= script "preload_store" %> <%= script "locales/#{I18n.locale}" %> <%= script "ember_jquery" %> + <%= script "preload-store" %> <%= script "vendor" %> <%= script "pretty-text-bundle" %> <%= script "application" %> @@ -89,9 +89,12 @@ <%- if @preloaded.present? %> <%- end %> diff --git a/config/application.rb b/config/application.rb index bc909d41f9..733323342d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -70,7 +70,7 @@ module Discourse end] config.assets.precompile += ['vendor.js', 'common.css', 'desktop.css', 'mobile.css', - 'admin.js', 'admin.css', 'shiny/shiny.css', 'preload_store.js', + 'admin.js', 'admin.css', 'shiny/shiny.css', 'preload-store.js.es6', 'browser-update.js', 'embed.css', 'break_string.js', 'ember_jquery.js', 'pretty-text-bundle.js'] diff --git a/lib/freedom_patches/raw_handlebars.rb b/lib/freedom_patches/raw_handlebars.rb index 06a709f04d..28f04e3140 100644 --- a/lib/freedom_patches/raw_handlebars.rb +++ b/lib/freedom_patches/raw_handlebars.rb @@ -40,7 +40,7 @@ module Discourse module Handlebars module Helper def precompile_handlebars(string) - "Discourse.EmberCompatHandlebars.template(#{Barber::Precompiler.compile(string)});" + "require('discourse/lib/raw-handlebars').template(#{Barber::Precompiler.compile(string)});" end def compile_handlebars(string) diff --git a/test/javascripts/helpers/current_user.js b/test/javascripts/helpers/current_user.js deleted file mode 100644 index 924915e540..0000000000 --- a/test/javascripts/helpers/current_user.js +++ /dev/null @@ -1 +0,0 @@ -PreloadStore.store("currentUser", {"id":42,"username":"eviltrout","avatar_template":"//www.gravatar.com/avatar/c6e17f2ae2a215e87ff9e878a4e63cd9.png?s={size}&r=pg&d=identicon","name":"Evil Trout","unread_notifications":0,"unread_private_messages":0,"admin":false,"notification_channel_position":null,"site_flagged_posts_count":0,"moderator":false,"staff":false,"reply_count":0,"topic_count":0,"enable_quoting":true,"external_links_in_new_tab":false,"dynamic_favicon":false,"trust_level":0,"can_edit":true}); diff --git a/test/javascripts/helpers/custom-html-test.js.es6 b/test/javascripts/helpers/custom-html-test.js.es6 index 1f2526d549..4c7f765e60 100644 --- a/test/javascripts/helpers/custom-html-test.js.es6 +++ b/test/javascripts/helpers/custom-html-test.js.es6 @@ -1,4 +1,6 @@ import { blank } from 'helpers/qunit-helpers'; +import PreloadStore from 'preload-store'; + module("helper:custom-html"); import { getCustomHTML, setCustomHTML } from 'discourse/helpers/custom-html'; @@ -11,5 +13,4 @@ test("customHTML", function() { PreloadStore.store('customHTML', {cookie: 'monster'}); equal(getCustomHTML('cookie'), 'monster', 'it returns HTML fragments from the PreloadStore'); - }); diff --git a/test/javascripts/helpers/site.js b/test/javascripts/helpers/site.js.es6 similarity index 99% rename from test/javascripts/helpers/site.js rename to test/javascripts/helpers/site.js.es6 index c606a3b787..d8ab85a486 100644 --- a/test/javascripts/helpers/site.js +++ b/test/javascripts/helpers/site.js.es6 @@ -1,2 +1,4 @@ +import PreloadStore from 'preload-store'; + /*jshint maxlen:10000000 */ PreloadStore.store("site",{"default_archetype":"regular","notification_types":{"mentioned":1,"replied":2,"quoted":3,"edited":4,"liked":5,"private_message":6,"invited_to_private_message":7,"invitee_accepted":8,"posted":9,"moved_post":10},"post_types":{"regular":1,"moderator_action":2},"groups":[{"id":0,"name":"everyone"},{"id":1,"name":"admins"},{"id":2,"name":"moderators"},{"id":3,"name":"staff"},{"id":10,"name":"trust_level_0"},{"id":11,"name":"trust_level_1"},{"id":12,"name":"trust_level_2"},{"id":13,"name":"trust_level_3"},{"id":14,"name":"trust_level_4"},{"id":20,"name":"ubuntu"},{"id":21,"name":"test"}],"filters":["latest","unread","new","starred","read","posted"],"periods":["yearly","monthly","weekly","daily"],"top_menu_items":["latest","unread","new","starred","read","posted","category","categories","top"],"anonymous_top_menu_items":["latest","category","categories","top"],"uncategorized_category_id":17,"categories":[{"id":5,"name":"extensibility","color":"FE8432","text_color":"FFFFFF","slug":"extensibility","topic_count":102,"description":"Topics about extending the functionality of Discourse with plugins, themes, add-ons, or other mechanisms for extensibility. ","topic_url":"/t/category-definition-for-extensibility/28","hotness":5.0,"read_restricted":false,"permission":null},{"id":7,"name":"dev","color":"000","text_color":"FFFFFF","slug":"dev","topic_count":284,"description":"This category is for topics related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth.","topic_url":"/t/category-definition-for-dev/1026","hotness":5.0,"read_restricted":false,"permission":null},{"id":1,"name":"bug","color":"e9dd00","text_color":"000000","slug":"bug","topic_count":660,"description":"Bug reports on Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please.","topic_url":"/t/category-definition-for-bug/2","hotness":5.0,"read_restricted":false,"permission":null},{"id":8,"name":"hosting","color":"74CCED","text_color":"FFFFFF","slug":"hosting","topic_count":69,"description":"Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services.","topic_url":"/t/category-definition-for-hosting/2626","hotness":5.0,"read_restricted":false,"permission":null},{"id":6,"name":"support","color":"b99","text_color":"FFFFFF","slug":"support","topic_count":782,"description":"Support on configuring, using, and installing Discourse. Not for software development related topics, but for admins and end users configuring and using Discourse.","topic_url":"/t/category-definition-for-support/389","hotness":5.0,"read_restricted":false,"permission":null},{"id":2,"name":"feature","color":"0E76BD","text_color":"FFFFFF","slug":"feature","topic_count":727,"description":"Discussion about features or potential features of Discourse: how they work, why they work, etc.","topic_url":"/t/category-definition-for-feature/11","hotness":5.0,"read_restricted":false,"permission":null},{"id":13,"name":"blog","color":"ED207B","text_color":"FFFFFF","slug":"blog","topic_count":14,"description":"Discussion topics generated from the official Discourse Blog. These topics are linked from the bottom of each blog entry where the blog comments would normally be.","topic_url":"/t/category-definition-for-blog/5250","hotness":5.0,"read_restricted":false,"permission":null},{"id":12,"name":"discourse hub","color":"b2c79f","text_color":"FFFFFF","slug":"discourse-hub","topic_count":4,"description":"Topics about current or future Discourse Hub functionality at discourse.org including nickname registration, global user pages, and the site directory.","topic_url":"/t/category-definition-for-discourse-hub/3038","hotness":5.0,"read_restricted":false,"permission":null},{"id":11,"name":"login","color":"edb400","text_color":"FFFFFF","slug":"login","topic_count":27,"description":"Topics about logging in to Discourse, using any standard third party provider (Twitter, Facebook, Google), traditional username and password, or with a custom plugin.","topic_url":"/t/category-definition-for-login/2828","hotness":5.0,"read_restricted":false,"permission":null},{"id":3,"name":"meta","color":"aaa","text_color":"FFFFFF","slug":"meta","topic_count":79,"description":"Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site.","topic_url":"/t/category-definition-for-meta/24","hotness":5.0,"read_restricted":false,"permission":null},{"id":10,"name":"howto","color":"76923C","text_color":"FFFFFF","slug":"howto","topic_count":58,"description":"Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up. ","topic_url":"/t/category-definition-for-howto/2629","hotness":5.0,"read_restricted":false,"permission":null},{"id":14,"name":"marketplace","color":"8C6238","text_color":"FFFFFF","slug":"marketplace","topic_count":24,"description":"About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc.","topic_url":"/t/category-definition-for-marketplace/5425","hotness":5.0,"read_restricted":false,"permission":null},{"id":17,"name":"uncategorized","color":"AB9364","text_color":"FFFFFF","slug":"uncategorized","topic_count":229,"description":"","topic_url":null,"hotness":5.0,"read_restricted":false,"permission":null},{"id":9,"name":"ux","color":"5F497A","text_color":"FFFFFF","slug":"ux","topic_count":184,"description":"Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements.","topic_url":"/t/category-definition-for-ux/2628","hotness":5.0,"read_restricted":false,"permission":null},{"id":4,"name":"faq","color":"33b","text_color":"FFFFFF","slug":"faq","topic_count":49,"description":"Topics that come up very often when discussing Discourse will eventually be classified into this Frequently Asked Questions category. Should only be added to popular topics.","topic_url":"/t/category-definition-for-faq/25","hotness":5.0,"read_restricted":false,"permission":null}],"post_action_types":[{"name_key":"bookmark","name":"Bookmark","description":"Bookmark this post","long_form":"bookmarked this post","is_flag":false,"icon":null,"id":1,"is_custom_flag":false},{"name_key":"like","name":"Like","description":"Like this post","long_form":"liked this","is_flag":false,"icon":"heart","id":2,"is_custom_flag":false},{"name_key":"off_topic","name":"Off-Topic","description":"This post is radically off-topic in the current conversation, and should probably be moved to a different topic. If this is a topic, perhaps it does not belong here.","long_form":"flagged this as off-topic","is_flag":true,"icon":null,"id":3,"is_custom_flag":false},{"name_key":"inappropriate","name":"Inappropriate","description":"This post contains content that a reasonable person would consider offensive, abusive, or a violation of our community guidelines<\/a>.","long_form":"flagged this as inappropriate","is_flag":true,"icon":null,"id":4,"is_custom_flag":false},{"name_key":"vote","name":"Vote","description":"Vote for this post","long_form":"voted for this post","is_flag":false,"icon":null,"id":5,"is_custom_flag":false},{"name_key":"spam","name":"Spam","description":"This post is an advertisement. It is not useful or relevant to the current conversation, but promotional in nature.","long_form":"flagged this as spam","is_flag":true,"icon":null,"id":8,"is_custom_flag":false},{"name_key":"notify_user","name":"Notify {{username}}","description":"This post contains something I want to talk to this person directly and privately about.","long_form":"notified user","is_flag":true,"icon":null,"id":6,"is_custom_flag":true},{"name_key":"notify_moderators","name":"Notify moderators","description":"This post requires general moderator attention based on the FAQ<\/a>, TOS<\/a>, or for another reason not listed above.","long_form":"notified moderators","is_flag":true,"icon":null,"id":7,"is_custom_flag":true}],"trust_levels":[{"id":0,"name":"new user"},{"id":1,"name":"basic user"},{"id":2,"name":"member"},{"id":3,"name":"regular"},{"id":4,"name":"leader"}],"archetypes":[{"id":"regular","name":"Regular Topic","options":[]}]}); diff --git a/test/javascripts/lib/preload-store-test.js.es6 b/test/javascripts/lib/preload-store-test.js.es6 index 0b296618f9..391ac3d2f1 100644 --- a/test/javascripts/lib/preload-store-test.js.es6 +++ b/test/javascripts/lib/preload-store-test.js.es6 @@ -1,7 +1,8 @@ import { blank } from 'helpers/qunit-helpers'; +import PreloadStore from 'preload-store'; -module("Discourse.PreloadStore", { - setup: function() { +module("preload-store", { + setup() { PreloadStore.store('bane', 'evil'); } }); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 82254f17bd..2ffb2986c3 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -1,7 +1,6 @@ /*global document, sinon, QUnit, Logster */ //= require env -//= require preload_store //= require probes //= require jquery.debug //= require jquery.ui.widget @@ -14,6 +13,7 @@ //= require route-recognizer //= require pretender //= require loader +//= require preload-store //= require locales/i18n //= require locales/en @@ -41,6 +41,7 @@ // //= require jquery.magnific-popup-min.js +window.TestPreloadStore = require('preload-store').default; window.inTestEnv = true; // Stop the message bus so we don't get ajax calls @@ -75,6 +76,13 @@ function dup(obj) { return jQuery.extend(true, {}, obj); } +function resetSite() { + var createStore = require('helpers/create-store').default; + var siteAttrs = dup(fixtures['site.json'].site); + siteAttrs.store = createStore(); + Discourse.Site.resetCurrent(Discourse.Site.create(siteAttrs)); +} + QUnit.testStart(function(ctx) { server = createPretendServer(); @@ -84,14 +92,15 @@ QUnit.testStart(function(ctx) { Discourse.BaseUrl = "localhost"; Discourse.Session.resetCurrent(); Discourse.User.resetCurrent(); - Discourse.Site.resetCurrent(Discourse.Site.create(dup(fixtures['site.json'].site))); + resetSite(); _DiscourseURL.redirectedTo = null; _DiscourseURL.redirectTo = function(url) { _DiscourseURL.redirectedTo = url; }; - PreloadStore.reset(); + var ps = require('preload-store').default; + ps.reset(); window.sandbox = sinon.sandbox.create(); window.sandbox.stub(ScrollingDOMMethods, "screenNotFull"); @@ -132,4 +141,5 @@ Object.keys(requirejs.entries).forEach(function(entry) { } }); require('mdtest/mdtest', null, null, true); +resetSite(); diff --git a/vendor/assets/javascripts/xss.min.js b/vendor/assets/javascripts/xss.min.js new file mode 100644 index 0000000000..48d7880783 --- /dev/null +++ b/vendor/assets/javascripts/xss.min.js @@ -0,0 +1 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o/g;var REGEXP_QUOTE=/"/g;var REGEXP_QUOTE_2=/"/g;var REGEXP_ATTR_VALUE_1=/&#([a-zA-Z0-9]*);?/gim;var REGEXP_ATTR_VALUE_COLON=/:?/gim;var REGEXP_ATTR_VALUE_NEWLINE=/&newline;?/gim;var REGEXP_DEFAULT_ON_TAG_ATTR_3=/\/\*|\*\//gm;var REGEXP_DEFAULT_ON_TAG_ATTR_4=/((j\s*a\s*v\s*a|v\s*b|l\s*i\s*v\s*e)\s*s\s*c\s*r\s*i\s*p\s*t\s*|m\s*o\s*c\s*h\s*a)\:/gi;var REGEXP_DEFAULT_ON_TAG_ATTR_5=/^[\s"'`]*(d\s*a\s*t\s*a\s*)\:/gi;var REGEXP_DEFAULT_ON_TAG_ATTR_6=/^[\s"'`]*(d\s*a\s*t\s*a\s*)\:\s*image\//gi;var REGEXP_DEFAULT_ON_TAG_ATTR_7=/e\s*x\s*p\s*r\s*e\s*s\s*s\s*i\s*o\s*n\s*\(.*/gi;var REGEXP_DEFAULT_ON_TAG_ATTR_8=/u\s*r\s*l\s*\(.*/gi;function escapeQuote(str){return str.replace(REGEXP_QUOTE,""")}function unescapeQuote(str){return str.replace(REGEXP_QUOTE_2,'"')}function escapeHtmlEntities(str){return str.replace(REGEXP_ATTR_VALUE_1,function replaceUnicode(str,code){return code[0]==="x"||code[0]==="X"?String.fromCharCode(parseInt(code.substr(1),16)):String.fromCharCode(parseInt(code,10))})}function escapeDangerHtml5Entities(str){return str.replace(REGEXP_ATTR_VALUE_COLON,":").replace(REGEXP_ATTR_VALUE_NEWLINE," ")}function clearNonPrintableCharacter(str){var str2="";for(var i=0,len=str.length;i/g;function stripBlankChar(html){var chars=html.split("");chars=chars.filter(function(char){var c=char.charCodeAt(0);if(c===127)return false;if(c<=31){if(c===10||c===13)return true;return false}return true});return chars.join("")}exports.whiteList=getDefaultWhiteList();exports.getDefaultWhiteList=getDefaultWhiteList;exports.onTag=onTag;exports.onIgnoreTag=onIgnoreTag;exports.onTagAttr=onTagAttr;exports.onIgnoreTagAttr=onIgnoreTagAttr;exports.safeAttrValue=safeAttrValue;exports.escapeHtml=escapeHtml;exports.escapeQuote=escapeQuote;exports.unescapeQuote=unescapeQuote;exports.escapeHtmlEntities=escapeHtmlEntities;exports.escapeDangerHtml5Entities=escapeDangerHtml5Entities;exports.clearNonPrintableCharacter=clearNonPrintableCharacter;exports.friendlyAttrValue=friendlyAttrValue;exports.escapeAttrValue=escapeAttrValue;exports.onIgnoreTagStripAll=onIgnoreTagStripAll;exports.StripTagBody=StripTagBody;exports.stripCommentTag=stripCommentTag;exports.stripBlankChar=stripBlankChar;exports.cssFilter=defaultCSSFilter},{"./util":4,cssfilter:8}],2:[function(require,module,exports){var DEFAULT=require("./default");var parser=require("./parser");var FilterXSS=require("./xss");function filterXSS(html,options){var xss=new FilterXSS(options);return xss.process(html)}exports=module.exports=filterXSS;exports.FilterXSS=FilterXSS;for(var i in DEFAULT)exports[i]=DEFAULT[i];for(var i in parser)exports[i]=parser[i];if(typeof window!=="undefined"){window.filterXSS=module.exports}},{"./default":1,"./parser":3,"./xss":5}],3:[function(require,module,exports){var _=require("./util");function getTagName(html){var i=html.indexOf(" ");if(i===-1){var tagName=html.slice(1,-1)}else{var tagName=html.slice(1,i+1)}tagName=_.trim(tagName).toLowerCase();if(tagName.slice(0,1)==="/")tagName=tagName.slice(1);if(tagName.slice(-1)==="/")tagName=tagName.slice(0,-1);return tagName}function isClosing(html){return html.slice(0,2)===""){rethtml+=escapeHtml(html.slice(lastPos,tagStart));currentHtml=html.slice(tagStart,currentPos+1);currentTagName=getTagName(currentHtml);rethtml+=onTag(tagStart,rethtml.length,currentTagName,currentHtml,isClosing(currentHtml));lastPos=currentPos+1;tagStart=false;continue}if((c==='"'||c==="'")&&html.charAt(currentPos-1)==="="){quoteStart=c;continue}}else{if(c===quoteStart){quoteStart=false;continue}}}}if(lastPos0;i--){var c=str[i];if(c===" ")continue;if(c==="=")return i;return-1}}function isQuoteWrapString(text){if(text[0]==='"'&&text[text.length-1]==='"'||text[0]==="'"&&text[text.length-1]==="'"){return true}else{return false}}function stripQuoteWrap(text){if(isQuoteWrapString(text)){return text.substr(1,text.length-2)}else{return text}}exports.parseTag=parseTag;exports.parseAttr=parseAttr},{"./util":4}],4:[function(require,module,exports){module.exports={indexOf:function(arr,item){var i,j;if(Array.prototype.indexOf){return arr.indexOf(item)}for(i=0,j=arr.length;i"}var attrs=getAttrs(html);var whiteAttrList=whiteList[tag];var attrsHtml=parseAttr(attrs.html,function(name,value){var isWhiteAttr=_.indexOf(whiteAttrList,name)!==-1;var ret=onTagAttr(tag,name,value,isWhiteAttr);if(!isNull(ret))return ret;if(isWhiteAttr){value=safeAttrValue(tag,name,value,cssFilter);if(value){return name+'="'+value+'"'}else{return name}}else{var ret=onIgnoreTagAttr(tag,name,value,isWhiteAttr);if(!isNull(ret))return ret;return}});var html="<"+tag;if(attrsHtml)html+=" "+attrsHtml;if(attrs.closing)html+=" /";html+=">";return html}else{var ret=onIgnoreTag(tag,html,info);if(!isNull(ret))return ret;return escapeHtml(html)}},escapeHtml);if(stripIgnoreTagBody){retHtml=stripIgnoreTagBody.remove(retHtml)}return retHtml};module.exports=FilterXSS},{"./default":1,"./parser":3,"./util":4,cssfilter:8}],6:[function(require,module,exports){var DEFAULT=require("./default");var parseStyle=require("./parser");var _=require("./util");function isNull(obj){return obj===undefined||obj===null}function FilterCSS(options){options=options||{};options.whiteList=options.whiteList||DEFAULT.whiteList;options.onAttr=options.onAttr||DEFAULT.onAttr;options.onIgnoreAttr=options.onIgnoreAttr||DEFAULT.onIgnoreAttr;this.options=options}FilterCSS.prototype.process=function(css){css=css||"";css=css.toString();if(!css)return"";var me=this;var options=me.options;var whiteList=options.whiteList;var onAttr=options.onAttr;var onIgnoreAttr=options.onIgnoreAttr;var retCSS=parseStyle(css,function(sourcePosition,position,name,value,source){var check=whiteList[name];var isWhite=false;if(check===true)isWhite=check;else if(typeof check==="function")isWhite=check(value);else if(check instanceof RegExp)isWhite=check.test(value);if(isWhite!==true)isWhite=false;var opts={position:position,sourcePosition:sourcePosition,source:source,isWhite:isWhite};if(isWhite){var ret=onAttr(name,value,opts);if(isNull(ret)){return name+":"+value}else{return ret}}else{var ret=onIgnoreAttr(name,value,opts);if(!isNull(ret)){return ret}}});return retCSS};module.exports=FilterCSS},{"./default":7,"./parser":9,"./util":10}],7:[function(require,module,exports){function getDefaultWhiteList(){var whiteList={};whiteList["align-content"]=false;whiteList["align-items"]=false;whiteList["align-self"]=false;whiteList["alignment-adjust"]=false;whiteList["alignment-baseline"]=false;whiteList["all"]=false;whiteList["anchor-point"]=false;whiteList["animation"]=false;whiteList["animation-delay"]=false;whiteList["animation-direction"]=false;whiteList["animation-duration"]=false;whiteList["animation-fill-mode"]=false;whiteList["animation-iteration-count"]=false;whiteList["animation-name"]=false;whiteList["animation-play-state"]=false;whiteList["animation-timing-function"]=false;whiteList["azimuth"]=false;whiteList["backface-visibility"]=false;whiteList["background"]=true;whiteList["background-attachment"]=true;whiteList["background-clip"]=true;whiteList["background-color"]=true;whiteList["background-image"]=true;whiteList["background-origin"]=true;whiteList["background-position"]=true;whiteList["background-repeat"]=true;whiteList["background-size"]=true;whiteList["baseline-shift"]=false;whiteList["binding"]=false;whiteList["bleed"]=false;whiteList["bookmark-label"]=false;whiteList["bookmark-level"]=false;whiteList["bookmark-state"]=false;whiteList["border"]=true;whiteList["border-bottom"]=true;whiteList["border-bottom-color"]=true;whiteList["border-bottom-left-radius"]=true;whiteList["border-bottom-right-radius"]=true;whiteList["border-bottom-style"]=true;whiteList["border-bottom-width"]=true;whiteList["border-collapse"]=true;whiteList["border-color"]=true;whiteList["border-image"]=true;whiteList["border-image-outset"]=true;whiteList["border-image-repeat"]=true;whiteList["border-image-slice"]=true;whiteList["border-image-source"]=true;whiteList["border-image-width"]=true;whiteList["border-left"]=true;whiteList["border-left-color"]=true;whiteList["border-left-style"]=true;whiteList["border-left-width"]=true;whiteList["border-radius"]=true;whiteList["border-right"]=true;whiteList["border-right-color"]=true;whiteList["border-right-style"]=true;whiteList["border-right-width"]=true;whiteList["border-spacing"]=true;whiteList["border-style"]=true;whiteList["border-top"]=true;whiteList["border-top-color"]=true;whiteList["border-top-left-radius"]=true;whiteList["border-top-right-radius"]=true;whiteList["border-top-style"]=true;whiteList["border-top-width"]=true;whiteList["border-width"]=true;whiteList["bottom"]=false;whiteList["box-decoration-break"]=true;whiteList["box-shadow"]=true;whiteList["box-sizing"]=true;whiteList["box-snap"]=true;whiteList["box-suppress"]=true;whiteList["break-after"]=true;whiteList["break-before"]=true;whiteList["break-inside"]=true;whiteList["caption-side"]=false;whiteList["chains"]=false;whiteList["clear"]=true;whiteList["clip"]=false;whiteList["clip-path"]=false;whiteList["clip-rule"]=false;whiteList["color"]=true;whiteList["color-interpolation-filters"]=true;whiteList["column-count"]=false;whiteList["column-fill"]=false;whiteList["column-gap"]=false;whiteList["column-rule"]=false;whiteList["column-rule-color"]=false;whiteList["column-rule-style"]=false;whiteList["column-rule-width"]=false;whiteList["column-span"]=false;whiteList["column-width"]=false;whiteList["columns"]=false;whiteList["contain"]=false;whiteList["content"]=false;whiteList["counter-increment"]=false;whiteList["counter-reset"]=false;whiteList["counter-set"]=false;whiteList["crop"]=false;whiteList["cue"]=false;whiteList["cue-after"]=false;whiteList["cue-before"]=false;whiteList["cursor"]=false;whiteList["direction"]=false;whiteList["display"]=true;whiteList["display-inside"]=true;whiteList["display-list"]=true;whiteList["display-outside"]=true;whiteList["dominant-baseline"]=false;whiteList["elevation"]=false;whiteList["empty-cells"]=false;whiteList["filter"]=false;whiteList["flex"]=false;whiteList["flex-basis"]=false;whiteList["flex-direction"]=false;whiteList["flex-flow"]=false;whiteList["flex-grow"]=false;whiteList["flex-shrink"]=false;whiteList["flex-wrap"]=false;whiteList["float"]=false;whiteList["float-offset"]=false;whiteList["flood-color"]=false;whiteList["flood-opacity"]=false;whiteList["flow-from"]=false;whiteList["flow-into"]=false;whiteList["font"]=true;whiteList["font-family"]=true;whiteList["font-feature-settings"]=true;whiteList["font-kerning"]=true;whiteList["font-language-override"]=true;whiteList["font-size"]=true;whiteList["font-size-adjust"]=true;whiteList["font-stretch"]=true;whiteList["font-style"]=true;whiteList["font-synthesis"]=true;whiteList["font-variant"]=true;whiteList["font-variant-alternates"]=true;whiteList["font-variant-caps"]=true;whiteList["font-variant-east-asian"]=true;whiteList["font-variant-ligatures"]=true;whiteList["font-variant-numeric"]=true;whiteList["font-variant-position"]=true;whiteList["font-weight"]=true;whiteList["grid"]=false;whiteList["grid-area"]=false;whiteList["grid-auto-columns"]=false;whiteList["grid-auto-flow"]=false;whiteList["grid-auto-rows"]=false;whiteList["grid-column"]=false;whiteList["grid-column-end"]=false;whiteList["grid-column-start"]=false;whiteList["grid-row"]=false;whiteList["grid-row-end"]=false;whiteList["grid-row-start"]=false;whiteList["grid-template"]=false;whiteList["grid-template-areas"]=false;whiteList["grid-template-columns"]=false;whiteList["grid-template-rows"]=false;whiteList["hanging-punctuation"]=false;whiteList["height"]=true;whiteList["hyphens"]=false;whiteList["icon"]=false;whiteList["image-orientation"]=false;whiteList["image-resolution"]=false;whiteList["ime-mode"]=false;whiteList["initial-letters"]=false;whiteList["inline-box-align"]=false;whiteList["justify-content"]=false;whiteList["justify-items"]=false;whiteList["justify-self"]=false;whiteList["left"]=false;whiteList["letter-spacing"]=true;whiteList["lighting-color"]=true;whiteList["line-box-contain"]=false;whiteList["line-break"]=false;whiteList["line-grid"]=false;whiteList["line-height"]=false;whiteList["line-snap"]=false;whiteList["line-stacking"]=false;whiteList["line-stacking-ruby"]=false;whiteList["line-stacking-shift"]=false;whiteList["line-stacking-strategy"]=false;whiteList["list-style"]=true;whiteList["list-style-image"]=true;whiteList["list-style-position"]=true;whiteList["list-style-type"]=true;whiteList["margin"]=true;whiteList["margin-bottom"]=true;whiteList["margin-left"]=true;whiteList["margin-right"]=true;whiteList["margin-top"]=true;whiteList["marker-offset"]=false;whiteList["marker-side"]=false;whiteList["marks"]=false;whiteList["mask"]=false;whiteList["mask-box"]=false;whiteList["mask-box-outset"]=false;whiteList["mask-box-repeat"]=false;whiteList["mask-box-slice"]=false;whiteList["mask-box-source"]=false;whiteList["mask-box-width"]=false;whiteList["mask-clip"]=false;whiteList["mask-image"]=false;whiteList["mask-origin"]=false;whiteList["mask-position"]=false;whiteList["mask-repeat"]=false;whiteList["mask-size"]=false;whiteList["mask-source-type"]=false;whiteList["mask-type"]=false;whiteList["max-height"]=true;whiteList["max-lines"]=false;whiteList["max-width"]=true;whiteList["min-height"]=true;whiteList["min-width"]=true;whiteList["move-to"]=false;whiteList["nav-down"]=false;whiteList["nav-index"]=false;whiteList["nav-left"]=false;whiteList["nav-right"]=false;whiteList["nav-up"]=false;whiteList["object-fit"]=false;whiteList["object-position"]=false;whiteList["opacity"]=false;whiteList["order"]=false;whiteList["orphans"]=false;whiteList["outline"]=false;whiteList["outline-color"]=false;whiteList["outline-offset"]=false;whiteList["outline-style"]=false;whiteList["outline-width"]=false;whiteList["overflow"]=false;whiteList["overflow-wrap"]=false;whiteList["overflow-x"]=false;whiteList["overflow-y"]=false;whiteList["padding"]=true;whiteList["padding-bottom"]=true;whiteList["padding-left"]=true;whiteList["padding-right"]=true;whiteList["padding-top"]=true;whiteList["page"]=false;whiteList["page-break-after"]=false;whiteList["page-break-before"]=false;whiteList["page-break-inside"]=false;whiteList["page-policy"]=false;whiteList["pause"]=false;whiteList["pause-after"]=false;whiteList["pause-before"]=false;whiteList["perspective"]=false;whiteList["perspective-origin"]=false;whiteList["pitch"]=false;whiteList["pitch-range"]=false;whiteList["play-during"]=false;whiteList["position"]=false;whiteList["presentation-level"]=false;whiteList["quotes"]=false;whiteList["region-fragment"]=false;whiteList["resize"]=false;whiteList["rest"]=false;whiteList["rest-after"]=false;whiteList["rest-before"]=false;whiteList["richness"]=false;whiteList["right"]=false;whiteList["rotation"]=false;whiteList["rotation-point"]=false;whiteList["ruby-align"]=false;whiteList["ruby-merge"]=false;whiteList["ruby-position"]=false;whiteList["shape-image-threshold"]=false;whiteList["shape-outside"]=false;whiteList["shape-margin"]=false;whiteList["size"]=false;whiteList["speak"]=false;whiteList["speak-as"]=false;whiteList["speak-header"]=false;whiteList["speak-numeral"]=false;whiteList["speak-punctuation"]=false;whiteList["speech-rate"]=false;whiteList["stress"]=false;whiteList["string-set"]=false;whiteList["tab-size"]=false;whiteList["table-layout"]=false;whiteList["text-align"]=true;whiteList["text-align-last"]=true;whiteList["text-combine-upright"]=true;whiteList["text-decoration"]=true;whiteList["text-decoration-color"]=true;whiteList["text-decoration-line"]=true;whiteList["text-decoration-skip"]=true;whiteList["text-decoration-style"]=true;whiteList["text-emphasis"]=true;whiteList["text-emphasis-color"]=true;whiteList["text-emphasis-position"]=true;whiteList["text-emphasis-style"]=true;whiteList["text-height"]=true;whiteList["text-indent"]=true;whiteList["text-justify"]=true;whiteList["text-orientation"]=true;whiteList["text-overflow"]=true;whiteList["text-shadow"]=true;whiteList["text-space-collapse"]=true;whiteList["text-transform"]=true;whiteList["text-underline-position"]=true;whiteList["text-wrap"]=true;whiteList["top"]=false;whiteList["transform"]=false;whiteList["transform-origin"]=false;whiteList["transform-style"]=false;whiteList["transition"]=false;whiteList["transition-delay"]=false;whiteList["transition-duration"]=false;whiteList["transition-property"]=false;whiteList["transition-timing-function"]=false;whiteList["unicode-bidi"]=false;whiteList["vertical-align"]=false;whiteList["visibility"]=false;whiteList["voice-balance"]=false;whiteList["voice-duration"]=false;whiteList["voice-family"]=false;whiteList["voice-pitch"]=false;whiteList["voice-range"]=false;whiteList["voice-rate"]=false;whiteList["voice-stress"]=false;whiteList["voice-volume"]=false;whiteList["volume"]=false;whiteList["white-space"]=false;whiteList["widows"]=false;whiteList["width"]=true;whiteList["will-change"]=false;whiteList["word-break"]=true;whiteList["word-spacing"]=true;whiteList["word-wrap"]=true;whiteList["wrap-flow"]=false;whiteList["wrap-through"]=false;whiteList["writing-mode"]=false;whiteList["z-index"]=false;return whiteList}function onAttr(name,value,options){}function onIgnoreAttr(name,value,options){}exports.whiteList=getDefaultWhiteList();exports.getDefaultWhiteList=getDefaultWhiteList;exports.onAttr=onAttr;exports.onIgnoreAttr=onIgnoreAttr},{}],8:[function(require,module,exports){var DEFAULT=require("./default");var FilterCSS=require("./css");function filterCSS(html,options){var xss=new FilterCSS(options);return xss.process(html)}exports=module.exports=filterCSS;exports.FilterCSS=FilterCSS;for(var i in DEFAULT)exports[i]=DEFAULT[i];if(typeof window!=="undefined"){window.filterCSS=module.exports}},{"./css":6,"./default":7}],9:[function(require,module,exports){var _=require("./util");function parseStyle(css,onAttr){css=_.trimRight(css);if(css[css.length-1]!==";")css+=";";var cssLength=css.length;var isParenthesisOpen=false;var lastPos=0;var i=0;var retCSS="";function addNewAttr(){if(!isParenthesisOpen){var source=_.trim(css.slice(lastPos,i));var j=source.indexOf(":");if(j!==-1){var name=_.trim(source.slice(0,j));var value=_.trim(source.slice(j+1));if(name){var ret=onAttr(lastPos,retCSS.length,name,value,source);if(ret)retCSS+=ret+"; "}}}lastPos=i+1}for(;i Date: Tue, 5 Jul 2016 10:57:41 -0400 Subject: [PATCH 007/170] FIX: Add `kbd` support to the sanitizer --- app/assets/javascripts/pretty-text/white-lister.js.es6 | 1 + test/javascripts/lib/sanitizer-test.js.es6 | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index cac079bf6a..090e0dea25 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -149,4 +149,5 @@ whiteListFeature('default', [ 'iframe[frameborder]', 'iframe[marginheight]', 'iframe[marginwidth]', + 'kbd' ]); diff --git a/test/javascripts/lib/sanitizer-test.js.es6 b/test/javascripts/lib/sanitizer-test.js.es6 index 6b0d2fd2e2..99e9977df0 100644 --- a/test/javascripts/lib/sanitizer-test.js.es6 +++ b/test/javascripts/lib/sanitizer-test.js.es6 @@ -45,6 +45,8 @@ test("sanitize", function() { cooked("a", "

a

", "it sanitizes spans"); cooked("a", "

a

", "it sanitizes spans"); cooked("a", "

a

", "it sanitizes spans"); + + cooked("Ctrl+C", "

Ctrl+C

"); }); test("urlAllowed", function() { From 6d7e8bd68b5e6ebf75c8c7a5783395fd10f5d568 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 5 Jul 2016 11:03:10 -0400 Subject: [PATCH 008/170] FIX: Customizations were broken --- app/assets/javascripts/admin/components/ace-editor.js.es6 | 2 +- test/javascripts/components/ace-editor-test.js.es6 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/components/ace-editor.js.es6 b/app/assets/javascripts/admin/components/ace-editor.js.es6 index 7e592e9d80..f9be641640 100644 --- a/app/assets/javascripts/admin/components/ace-editor.js.es6 +++ b/app/assets/javascripts/admin/components/ace-editor.js.es6 @@ -1,6 +1,6 @@ /* global ace:true */ import loadScript from 'discourse/lib/load-script'; -import escapeExpression from 'discourse/lib/utilities'; +import { escapeExpression } from 'discourse/lib/utilities'; export default Ember.Component.extend({ mode: 'css', diff --git a/test/javascripts/components/ace-editor-test.js.es6 b/test/javascripts/components/ace-editor-test.js.es6 index 58dedbb19f..6fa9a533d4 100644 --- a/test/javascripts/components/ace-editor-test.js.es6 +++ b/test/javascripts/components/ace-editor-test.js.es6 @@ -10,7 +10,7 @@ componentTest('css editor', { }); componentTest('html editor', { - template: '{{ace-editor mode="html"}}', + template: '{{ace-editor mode="html" content="wat"}}', test(assert) { assert.ok(this.$('.ace_editor').length, 'it renders the ace editor'); } From 3d2180502222377b69be88dcc1e9e90b1e0bf1ae Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 5 Jul 2016 11:07:59 -0400 Subject: [PATCH 009/170] FIX: Backwards compatibility for plugins who sanitize --- app/assets/javascripts/discourse/lib/load-script.js.es6 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/javascripts/discourse/lib/load-script.js.es6 b/app/assets/javascripts/discourse/lib/load-script.js.es6 index b562a11545..4edc9a452a 100644 --- a/app/assets/javascripts/discourse/lib/load-script.js.es6 +++ b/app/assets/javascripts/discourse/lib/load-script.js.es6 @@ -22,6 +22,10 @@ function loadWithTag(path, cb) { } export default function loadScript(url, opts) { + + // TODO: Remove this once plugins have been updated not to use it: + if (url === "defer/html-sanitizer-bundle") { return Ember.RSVP.Promise.resolve(); } + opts = opts || {}; return new Ember.RSVP.Promise(function(resolve) { From ff4e60808a9f8295dbbdb2ab5de83558f555b51e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 5 Jul 2016 12:03:54 -0400 Subject: [PATCH 010/170] FIX: Polls were broken server side --- .../poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 index 0b086fa870..c182c2439b 100644 --- a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 +++ b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 @@ -22,7 +22,7 @@ export function setup(helper) { 'span.info-text', 'a.button.cast-votes', 'a.button.toggle-results', - 'li[data-*' + 'li[data-*]' ]); helper.replaceBlock({ From c1d4ca4031531297cfe153cc1a87177d266cde8f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 6 Jul 2016 12:27:12 -0400 Subject: [PATCH 011/170] FIX: Raw templates in customizations were broken --- app/models/site_customization.rb | 14 +++++++++----- spec/models/site_customization_spec.rb | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/models/site_customization.rb b/app/models/site_customization.rb index b85ef907e6..57bc6319d0 100644 --- a/app/models/site_customization.rb +++ b/app/models/site_customization.rb @@ -44,14 +44,18 @@ PLUGIN_API_JS name = node["name"] || node["data-template-name"] || "broken" precompiled = if name =~ /\.raw$/ - "RawHandlebars.template(#{Barber::Precompiler.compile(node.inner_html)})" + "require('discourse/lib/raw-handlebars').template(#{Barber::Precompiler.compile(node.inner_html)})" else "Ember.HTMLBars.template(#{Barber::Ember::Precompiler.compile(node.inner_html)})" end - compiled = <") + + node.replace < + (function() { + Ember.TEMPLATES[#{name.inspect}] = #{precompiled}; + })(); + +COMPILED end doc.css('script[type="text/discourse-plugin"]').each do |node| diff --git a/spec/models/site_customization_spec.rb b/spec/models/site_customization_spec.rb index 2495e1ebcc..e762c78d54 100644 --- a/spec/models/site_customization_spec.rb +++ b/spec/models/site_customization_spec.rb @@ -109,8 +109,8 @@ HTML c = SiteCustomization.create!(user_id: -1, name: "test", head_tag: with_template, body_tag: with_template) expect(c.head_tag_baked).to match(/HTMLBars/) expect(c.body_tag_baked).to match(/HTMLBars/) - expect(c.body_tag_baked).to match(/RawHandlebars/) - expect(c.head_tag_baked).to match(/RawHandlebars/) + expect(c.body_tag_baked).to match(/raw-handlebars/) + expect(c.head_tag_baked).to match(/raw-handlebars/) end it 'should create body_tag_baked on demand if needed' do From cda58511ac5e303d27f37aa665d942cc9c092f4f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 6 Jul 2016 12:39:34 -0400 Subject: [PATCH 012/170] Better deprecation messages for Pretty Text --- app/assets/javascripts/deprecated.js | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/deprecated.js b/app/assets/javascripts/deprecated.js index 2303ec5d2c..96bc35a94c 100644 --- a/app/assets/javascripts/deprecated.js +++ b/app/assets/javascripts/deprecated.js @@ -1,19 +1,19 @@ (function() { var Discourse = require('discourse').default; - Discourse.Markdown = { - whiteListTag: Ember.K, - whiteListIframe: Ember.K - }; + function deprecate(module, methods) { + const result = {}; - Discourse.Dialect = { - inlineRegexp: Ember.K, - addPreProcessor: Ember.K, - replaceBlock: Ember.K, - inlineReplace: Ember.K, - registerInline: Ember.K, - registerEmoji: Ember.K - }; + methods.forEach(m => { + result[m] = () => Ember.warn(`Discourse.${module}.${m} is deprecated. Export a setup() function instead`); + }); + + Discourse[module] = result; + } + + deprecate('Markdown', ['whiteListTag', 'whiteListIframe']); + deprecate('Dialect', ['inlineRegexp', 'inlineBetween', 'addPreProcessor', 'replaceBlock', + 'inlineReplace', 'registerInline', 'registerEmoji']); Discourse.ajax = function() { var ajax = require('discourse/lib/ajax').ajax; From 6dc4a3f2d6a95076e5fb34a3d9f5b8c7d1277ba1 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 6 Jul 2016 12:46:07 -0400 Subject: [PATCH 013/170] A constant we can use to detect if the new dialect engine is present --- app/assets/javascripts/deprecated.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/deprecated.js b/app/assets/javascripts/deprecated.js index 96bc35a94c..7f8b9c7087 100644 --- a/app/assets/javascripts/deprecated.js +++ b/app/assets/javascripts/deprecated.js @@ -15,6 +15,8 @@ deprecate('Dialect', ['inlineRegexp', 'inlineBetween', 'addPreProcessor', 'replaceBlock', 'inlineReplace', 'registerInline', 'registerEmoji']); + Discourse.dialect_deprecated = true; + Discourse.ajax = function() { var ajax = require('discourse/lib/ajax').ajax; Ember.warn("Discourse.ajax is deprecated. Import the module and use it instead"); From a2b9b01d0f81fca5788321fbac8c52da57f83da8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 6 Jul 2016 19:12:21 +0200 Subject: [PATCH 014/170] FIX: details plugin wasn't working properly --- .../engines/discourse-markdown/html.js.es6 | 4 ++-- .../javascripts/lib/discourse-markdown/details.js.es6 | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 index c3ff79aae4..64a237ca46 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/html.js.es6 @@ -1,5 +1,5 @@ -const BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'div', - 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', +const BLOCK_TAGS = ['address', 'article', 'aside', 'audio', 'blockquote', 'canvas', 'dd', 'details', + 'div', 'dl', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'iframe', 'noscript', 'ol', 'output', 'p', 'pre', 'section', 'table', 'tfoot', 'ul', 'video']; diff --git a/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 b/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 index 8416c8ec91..af0cfca5db 100644 --- a/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 +++ b/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 @@ -12,7 +12,8 @@ function replaceDetails(text) { // add new lines to make sure we *always* have a

element after and around // otherwise we can't hide the content since we can't target text nodes via CSS - return text.replace(/<\/summary>/ig, "\n\n").replace(/<\/details>/ig, "\n\n\n\n"); + return text.replace(/<\/summary>/ig, "\n\n") + .replace(/<\/details>/ig, "\n\n\n\n"); } registerOption((siteSettings, opts) => { @@ -20,6 +21,12 @@ registerOption((siteSettings, opts) => { }); export function setup(helper) { - helper.whiteList('details.elided'); + helper.whiteList([ + 'summary', + 'summary[title]', + 'details', + 'details.elided' + ]); + helper.addPreProcessor(text => replaceDetails(text)); } From d9d758aeeb59fe65bd8ef98f4c38cb4032ee2c03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 6 Jul 2016 19:46:47 +0200 Subject: [PATCH 015/170] add pretty-text tests for discourse-details plugin --- .../spec/components/pretty_text_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 plugins/discourse-details/spec/components/pretty_text_spec.rb diff --git a/plugins/discourse-details/spec/components/pretty_text_spec.rb b/plugins/discourse-details/spec/components/pretty_text_spec.rb new file mode 100644 index 0000000000..b9b91ea771 --- /dev/null +++ b/plugins/discourse-details/spec/components/pretty_text_spec.rb @@ -0,0 +1,12 @@ +require 'rails_helper' +require 'pretty_text' + +describe PrettyText do + + it "supports details tag" do + cooked_html = "

foo\n\n

bar

\n\n

" + expect(PrettyText.cook("
foobar
")).to match_html(cooked_html) + expect(PrettyText.cook("[details=foo]bar[/details]")).to match_html(cooked_html) + end + +end From 748ce74653bceba9db83abe50c18af1ee4b50624 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 6 Jul 2016 16:30:41 -0400 Subject: [PATCH 016/170] FIX: Deprecations have to be ES5 --- app/assets/javascripts/deprecated.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/deprecated.js b/app/assets/javascripts/deprecated.js index 7f8b9c7087..50e0fef681 100644 --- a/app/assets/javascripts/deprecated.js +++ b/app/assets/javascripts/deprecated.js @@ -4,8 +4,10 @@ function deprecate(module, methods) { const result = {}; - methods.forEach(m => { - result[m] = () => Ember.warn(`Discourse.${module}.${m} is deprecated. Export a setup() function instead`); + methods.forEach(function(m) { + result[m] = function() { + Ember.warn("Discourse." + module + "." + m + " is deprecated. Export a setup() function instead"); + }; }); Discourse[module] = result; @@ -13,7 +15,7 @@ deprecate('Markdown', ['whiteListTag', 'whiteListIframe']); deprecate('Dialect', ['inlineRegexp', 'inlineBetween', 'addPreProcessor', 'replaceBlock', - 'inlineReplace', 'registerInline', 'registerEmoji']); + 'inlineReplace', 'registerInline', 'registerEmoji']); Discourse.dialect_deprecated = true; From 529fea3c42fe8da75ed6603444fedb49e76b3da8 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 7 Jul 2016 13:52:56 -0400 Subject: [PATCH 017/170] FIX: Duplicate variable --- .../javascripts/discourse/controllers/user-notifications.js.es6 | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 index 2d308dffd9..d6ed2f360a 100644 --- a/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-notifications.js.es6 @@ -1,5 +1,4 @@ import { ajax } from 'discourse/lib/ajax'; -import { observes } from 'ember-addons/ember-computed-decorators'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; export default Ember.ArrayController.extend({ From e5293f2c9a3fa1a7ee71f1191d62e50d21ad76bb Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 7 Jul 2016 16:27:18 -0400 Subject: [PATCH 018/170] FIX: Force HTML to recompile --- app/models/site_customization.rb | 21 ++++++++++++++++--- ...compiler_version_to_site_customizations.rb | 5 +++++ 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20160707195549_add_compiler_version_to_site_customizations.rb diff --git a/app/models/site_customization.rb b/app/models/site_customization.rb index 57bc6319d0..5978892f40 100644 --- a/app/models/site_customization.rb +++ b/app/models/site_customization.rb @@ -4,6 +4,9 @@ require_dependency 'distributed_cache' class SiteCustomization < ActiveRecord::Base ENABLED_KEY = '7e202ef2-56d7-47d5-98d8-a9c8d15e57dd' + + COMPILER_VERSION = 1 + @cache = DistributedCache.new('site_customization') def self.css_fields @@ -131,7 +134,7 @@ COMPILED end def self.enabled_stylesheet_contents(target=:desktop) - @cache["enabled_stylesheet_#{target}"] ||= where(enabled: true) + @cache["enabled_stylesheet_#{target}:#{COMPILER_VERSION}"] ||= where(enabled: true) .order(:name) .pluck(baked_for_target(target)) .compact @@ -167,7 +170,7 @@ COMPILED def self.lookup_field(key, target, field) return if key.blank? - cache_key = key + target.to_s + field.to_s; + cache_key = "#{key}:#{target}:#{field}:#{COMPILER_VERSION}" lookup = @cache[cache_key] return lookup.html_safe if lookup @@ -203,7 +206,19 @@ COMPILED end def ensure_baked!(field) - unless self.send("#{field}_baked") + + # If the version number changes, clear out all the baked fields + if compiler_version != COMPILER_VERSION + updates = { compiler_version: COMPILER_VERSION } + SiteCustomization.html_fields.each do |f| + updates["#{f}_baked".to_sym] = nil + end + + update_columns(updates) + end + + baked = send("#{field}_baked") + if baked.blank? if val = self.send(field) val = process_html(val) rescue "" self.update_columns("#{field}_baked" => val) diff --git a/db/migrate/20160707195549_add_compiler_version_to_site_customizations.rb b/db/migrate/20160707195549_add_compiler_version_to_site_customizations.rb new file mode 100644 index 0000000000..6abab5f72f --- /dev/null +++ b/db/migrate/20160707195549_add_compiler_version_to_site_customizations.rb @@ -0,0 +1,5 @@ +class AddCompilerVersionToSiteCustomizations < ActiveRecord::Migration + def change + add_column :site_customizations, :compiler_version, :integer, default: 0, null: false + end +end From 4d65370797d87d90741f2f3774b7c6dccdb6a7ae Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 11 Jul 2016 10:28:31 -0400 Subject: [PATCH 019/170] FIX: `` was no longer whitelisted --- app/assets/javascripts/pretty-text/white-lister.js.es6 | 3 ++- test/javascripts/lib/sanitizer-test.js.es6 | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index 090e0dea25..fc6376c3a3 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -149,5 +149,6 @@ whiteListFeature('default', [ 'iframe[frameborder]', 'iframe[marginheight]', 'iframe[marginwidth]', - 'kbd' + 'kbd', + 'strike' ]); diff --git a/test/javascripts/lib/sanitizer-test.js.es6 b/test/javascripts/lib/sanitizer-test.js.es6 index 99e9977df0..7da137725a 100644 --- a/test/javascripts/lib/sanitizer-test.js.es6 +++ b/test/javascripts/lib/sanitizer-test.js.es6 @@ -47,6 +47,7 @@ test("sanitize", function() { cooked("a", "

a

", "it sanitizes spans"); cooked("Ctrl+C", "

Ctrl+C

"); + cooked("it has been 1 day 0 days since our last test failure", "

it has been 1 day 0 days since our last test failure

"); }); test("urlAllowed", function() { From c145e747b62f81922c253d1363c7edb3c66130ca Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Mon, 11 Jul 2016 23:59:15 +0200 Subject: [PATCH 020/170] A few small changes to the phpBB3 importer (#4321) * Reconnect to phpBB3 database on connection loss * Map geek smiley to :nerd: emoji in phpBB3 importer * Import PMs to yourself from phpBB3 * Allow empty table prefix in phpBB3 importer --- .../phpbb3/database/database.rb | 5 +- .../phpbb3/database/database_3_0.rb | 88 +++++++++---------- .../phpbb3/database/database_3_1.rb | 8 +- .../phpbb3/importers/message_importer.rb | 5 +- script/import_scripts/phpbb3/settings.yml | 4 +- .../phpbb3/support/smiley_processor.rb | 3 +- 6 files changed, 57 insertions(+), 56 deletions(-) diff --git a/script/import_scripts/phpbb3/database/database.rb b/script/import_scripts/phpbb3/database/database.rb index b63d035633..fa88352349 100644 --- a/script/import_scripts/phpbb3/database/database.rb +++ b/script/import_scripts/phpbb3/database/database.rb @@ -37,7 +37,8 @@ module ImportScripts::PhpBB3 port: @database_settings.port, username: @database_settings.username, password: @database_settings.password, - database: @database_settings.schema + database: @database_settings.schema, + reconnect: true ) end @@ -46,7 +47,7 @@ module ImportScripts::PhpBB3 @database_client.query(<<-SQL, cache_rows: false, symbolize_keys: true).first[:config_value] SELECT config_value - FROM #{table_prefix}_config + FROM #{table_prefix}config WHERE config_name = 'version' SQL end diff --git a/script/import_scripts/phpbb3/database/database_3_0.rb b/script/import_scripts/phpbb3/database/database_3_0.rb index 94ec4ca50f..c596b37338 100644 --- a/script/import_scripts/phpbb3/database/database_3_0.rb +++ b/script/import_scripts/phpbb3/database/database_3_0.rb @@ -6,8 +6,8 @@ module ImportScripts::PhpBB3 def count_users count(<<-SQL) SELECT COUNT(*) AS count - FROM #{@table_prefix}_users u - JOIN #{@table_prefix}_groups g ON g.group_id = u.group_id + FROM #{@table_prefix}users u + JOIN #{@table_prefix}groups g ON g.group_id = u.group_id WHERE u.user_type != #{Constants::USER_TYPE_IGNORE} SQL end @@ -17,9 +17,9 @@ module ImportScripts::PhpBB3 SELECT u.user_id, u.user_email, u.username, u.user_password, u.user_regdate, u.user_lastvisit, u.user_ip, u.user_type, u.user_inactive_reason, g.group_name, b.ban_start, b.ban_end, b.ban_reason, u.user_posts, u.user_website, u.user_from, u.user_birthday, u.user_avatar_type, u.user_avatar - FROM #{@table_prefix}_users u - JOIN #{@table_prefix}_groups g ON (g.group_id = u.group_id) - LEFT OUTER JOIN #{@table_prefix}_banlist b ON ( + FROM #{@table_prefix}users u + JOIN #{@table_prefix}groups g ON (g.group_id = u.group_id) + LEFT OUTER JOIN #{@table_prefix}banlist b ON ( u.user_id = b.ban_userid AND b.ban_exclude = 0 AND (b.ban_end = 0 OR b.ban_end >= UNIX_TIMESTAMP()) ) @@ -32,7 +32,7 @@ module ImportScripts::PhpBB3 def count_anonymous_users count(<<-SQL) SELECT COUNT(DISTINCT post_username) AS count - FROM #{@table_prefix}_posts + FROM #{@table_prefix}posts WHERE post_username <> '' SQL end @@ -42,7 +42,7 @@ module ImportScripts::PhpBB3 query(<<-SQL, :post_username) SELECT post_username, MIN(post_time) AS first_post_time - FROM #{@table_prefix}_posts + FROM #{@table_prefix}posts WHERE post_username > '#{last_username}' GROUP BY post_username ORDER BY post_username @@ -53,10 +53,10 @@ module ImportScripts::PhpBB3 def fetch_categories query(<<-SQL) SELECT f.forum_id, f.parent_id, f.forum_name, f.forum_desc, x.first_post_time - FROM #{@table_prefix}_forums f + FROM #{@table_prefix}forums f LEFT OUTER JOIN ( SELECT MIN(topic_time) AS first_post_time, forum_id - FROM #{@table_prefix}_topics + FROM #{@table_prefix}topics GROUP BY forum_id ) x ON (f.forum_id = x.forum_id) WHERE f.forum_type != #{Constants::FORUM_TYPE_LINK} @@ -67,7 +67,7 @@ module ImportScripts::PhpBB3 def count_posts count(<<-SQL) SELECT COUNT(*) AS count - FROM #{@table_prefix}_posts + FROM #{@table_prefix}posts SQL end @@ -77,8 +77,8 @@ module ImportScripts::PhpBB3 p.post_text, p.post_time, p.post_username, t.topic_status, t.topic_type, t.poll_title, CASE WHEN t.poll_length > 0 THEN t.poll_start + t.poll_length ELSE NULL END AS poll_end, t.poll_max_options, p.post_attachment - FROM #{@table_prefix}_posts p - JOIN #{@table_prefix}_topics t ON (p.topic_id = t.topic_id) + FROM #{@table_prefix}posts p + JOIN #{@table_prefix}topics t ON (p.topic_id = t.topic_id) WHERE p.post_id > #{last_post_id} ORDER BY p.post_id LIMIT #{@batch_size} @@ -88,7 +88,7 @@ module ImportScripts::PhpBB3 def get_first_post_id(topic_id) query(<<-SQL).try(:first).try(:[], :topic_first_post_id) SELECT topic_first_post_id - FROM #{@table_prefix}_topics + FROM #{@table_prefix}topics WHERE topic_id = #{topic_id} SQL end @@ -98,12 +98,12 @@ module ImportScripts::PhpBB3 SELECT o.poll_option_id, o.poll_option_text, o.poll_option_total AS total_votes, o.poll_option_total - ( SELECT COUNT(DISTINCT v.vote_user_id) - FROM #{@table_prefix}_poll_votes v - JOIN #{@table_prefix}_users u ON (v.vote_user_id = u.user_id) - JOIN #{@table_prefix}_topics t ON (v.topic_id = t.topic_id) + FROM #{@table_prefix}poll_votes v + JOIN #{@table_prefix}users u ON (v.vote_user_id = u.user_id) + JOIN #{@table_prefix}topics t ON (v.topic_id = t.topic_id) WHERE v.poll_option_id = o.poll_option_id AND v.topic_id = o.topic_id ) AS anonymous_votes - FROM #{@table_prefix}_poll_options o + FROM #{@table_prefix}poll_options o WHERE o.topic_id = #{topic_id} ORDER BY o.poll_option_id SQL @@ -113,10 +113,10 @@ module ImportScripts::PhpBB3 # this query ignores invalid votes that belong to non-existent users or topics query(<<-SQL) SELECT u.user_id, v.poll_option_id - FROM #{@table_prefix}_poll_votes v - JOIN #{@table_prefix}_poll_options o ON (v.poll_option_id = o.poll_option_id AND v.topic_id = o.topic_id) - JOIN #{@table_prefix}_users u ON (v.vote_user_id = u.user_id) - JOIN #{@table_prefix}_topics t ON (v.topic_id = t.topic_id) + FROM #{@table_prefix}poll_votes v + JOIN #{@table_prefix}poll_options o ON (v.poll_option_id = o.poll_option_id AND v.topic_id = o.topic_id) + JOIN #{@table_prefix}users u ON (v.vote_user_id = u.user_id) + JOIN #{@table_prefix}topics t ON (v.topic_id = t.topic_id) WHERE v.topic_id = #{topic_id} SQL end @@ -127,19 +127,19 @@ module ImportScripts::PhpBB3 SELECT MAX(x.total_voters) AS total_voters, MAX(x.total_voters) - ( SELECT COUNT(DISTINCT v.vote_user_id) - FROM #{@table_prefix}_poll_votes v - JOIN #{@table_prefix}_poll_options o ON (v.poll_option_id = o.poll_option_id AND v.topic_id = o.topic_id) - JOIN #{@table_prefix}_users u ON (v.vote_user_id = u.user_id) - JOIN #{@table_prefix}_topics t ON (v.topic_id = t.topic_id) + FROM #{@table_prefix}poll_votes v + JOIN #{@table_prefix}poll_options o ON (v.poll_option_id = o.poll_option_id AND v.topic_id = o.topic_id) + JOIN #{@table_prefix}users u ON (v.vote_user_id = u.user_id) + JOIN #{@table_prefix}topics t ON (v.topic_id = t.topic_id) WHERE v.topic_id = #{topic_id} ) AS anonymous_voters FROM ( SELECT COUNT(DISTINCT vote_user_id) AS total_voters - FROM #{@table_prefix}_poll_votes + FROM #{@table_prefix}poll_votes WHERE topic_id = #{topic_id} UNION SELECT MAX(poll_option_total) AS total_voters - FROM #{@table_prefix}_poll_options + FROM #{@table_prefix}poll_options WHERE topic_id = #{topic_id} ) x SQL @@ -148,14 +148,14 @@ module ImportScripts::PhpBB3 def get_max_attachment_size query(<<-SQL).first[:filesize] SELECT IFNULL(MAX(filesize), 0) AS filesize - FROM #{@table_prefix}_attachments + FROM #{@table_prefix}attachments SQL end def fetch_attachments(topic_id, post_id) query(<<-SQL) SELECT physical_filename, real_filename - FROM #{@table_prefix}_attachments + FROM #{@table_prefix}attachments WHERE topic_id = #{topic_id} AND post_msg_id = #{post_id} ORDER BY filetime DESC, post_msg_id SQL @@ -164,10 +164,10 @@ module ImportScripts::PhpBB3 def count_messages count(<<-SQL) SELECT COUNT(*) AS count - FROM #{@table_prefix}_privmsgs m + FROM #{@table_prefix}privmsgs m WHERE NOT EXISTS ( -- ignore duplicate messages SELECT 1 - FROM #{@table_prefix}_privmsgs x + FROM #{@table_prefix}privmsgs x WHERE x.msg_id < m.msg_id AND x.root_level = m.root_level AND x.author_id = m.author_id AND x.to_address = m.to_address AND x.message_time = m.message_time ) @@ -179,15 +179,15 @@ module ImportScripts::PhpBB3 SELECT m.msg_id, m.root_level AS root_msg_id, m.author_id, m.message_time, m.message_subject, m.message_text, m.to_address, r.author_id AS root_author_id, r.to_address AS root_to_address, ( SELECT COUNT(*) - FROM #{@table_prefix}_attachments a + FROM #{@table_prefix}attachments a WHERE a.topic_id = 0 AND m.msg_id = a.post_msg_id ) AS attachment_count - FROM #{@table_prefix}_privmsgs m - LEFT OUTER JOIN #{@table_prefix}_privmsgs r ON (m.root_level = r.msg_id) + FROM #{@table_prefix}privmsgs m + LEFT OUTER JOIN #{@table_prefix}privmsgs r ON (m.root_level = r.msg_id) WHERE m.msg_id > #{last_msg_id} AND NOT EXISTS ( -- ignore duplicate messages SELECT 1 - FROM #{@table_prefix}_privmsgs x + FROM #{@table_prefix}privmsgs x WHERE x.msg_id < m.msg_id AND x.root_level = m.root_level AND x.author_id = m.author_id AND x.to_address = m.to_address AND x.message_time = m.message_time ) @@ -199,15 +199,15 @@ module ImportScripts::PhpBB3 def count_bookmarks count(<<-SQL) SELECT COUNT(*) AS count - FROM #{@table_prefix}_bookmarks + FROM #{@table_prefix}bookmarks SQL end def fetch_bookmarks(last_user_id, last_topic_id) query(<<-SQL, :user_id, :topic_first_post_id) SELECT b.user_id, t.topic_first_post_id - FROM #{@table_prefix}_bookmarks b - JOIN #{@table_prefix}_topics t ON (b.topic_id = t.topic_id) + FROM #{@table_prefix}bookmarks b + JOIN #{@table_prefix}topics t ON (b.topic_id = t.topic_id) WHERE b.user_id > #{last_user_id} AND b.topic_id > #{last_topic_id} ORDER BY b.user_id, b.topic_id LIMIT #{@batch_size} @@ -217,12 +217,12 @@ module ImportScripts::PhpBB3 def get_config_values query(<<-SQL).first SELECT - (SELECT config_value FROM #{@table_prefix}_config WHERE config_name = 'version') AS phpbb_version, - (SELECT config_value FROM #{@table_prefix}_config WHERE config_name = 'avatar_gallery_path') AS avatar_gallery_path, - (SELECT config_value FROM #{@table_prefix}_config WHERE config_name = 'avatar_path') AS avatar_path, - (SELECT config_value FROM #{@table_prefix}_config WHERE config_name = 'avatar_salt') AS avatar_salt, - (SELECT config_value FROM #{@table_prefix}_config WHERE config_name = 'smilies_path') AS smilies_path, - (SELECT config_value FROM #{@table_prefix}_config WHERE config_name = 'upload_path') AS attachment_path + (SELECT config_value FROM #{@table_prefix}config WHERE config_name = 'version') AS phpbb_version, + (SELECT config_value FROM #{@table_prefix}config WHERE config_name = 'avatar_gallery_path') AS avatar_gallery_path, + (SELECT config_value FROM #{@table_prefix}config WHERE config_name = 'avatar_path') AS avatar_path, + (SELECT config_value FROM #{@table_prefix}config WHERE config_name = 'avatar_salt') AS avatar_salt, + (SELECT config_value FROM #{@table_prefix}config WHERE config_name = 'smilies_path') AS smilies_path, + (SELECT config_value FROM #{@table_prefix}config WHERE config_name = 'upload_path') AS attachment_path SQL end end diff --git a/script/import_scripts/phpbb3/database/database_3_1.rb b/script/import_scripts/phpbb3/database/database_3_1.rb index 2d18880ab5..0bd264856e 100644 --- a/script/import_scripts/phpbb3/database/database_3_1.rb +++ b/script/import_scripts/phpbb3/database/database_3_1.rb @@ -13,10 +13,10 @@ module ImportScripts::PhpBB3 u.user_type, u.user_inactive_reason, g.group_name, b.ban_start, b.ban_end, b.ban_reason, u.user_posts, f.pf_phpbb_website AS user_website, f.pf_phpbb_location AS user_from, u.user_birthday, u.user_avatar_type, u.user_avatar - FROM #{@table_prefix}_users u - LEFT OUTER JOIN #{@table_prefix}_profile_fields_data f ON (u.user_id = f.user_id) - JOIN #{@table_prefix}_groups g ON (g.group_id = u.group_id) - LEFT OUTER JOIN #{@table_prefix}_banlist b ON ( + FROM #{@table_prefix}users u + LEFT OUTER JOIN #{@table_prefix}profile_fields_data f ON (u.user_id = f.user_id) + JOIN #{@table_prefix}groups g ON (g.group_id = u.group_id) + LEFT OUTER JOIN #{@table_prefix}banlist b ON ( u.user_id = b.ban_userid AND b.ban_exclude = 0 AND (b.ban_end = 0 OR b.ban_end >= UNIX_TIMESTAMP()) ) diff --git a/script/import_scripts/phpbb3/importers/message_importer.rb b/script/import_scripts/phpbb3/importers/message_importer.rb index c164806b65..1289122cb9 100644 --- a/script/import_scripts/phpbb3/importers/message_importer.rb +++ b/script/import_scripts/phpbb3/importers/message_importer.rb @@ -56,7 +56,7 @@ module ImportScripts::PhpBB3 mapped[:target_usernames] = get_recipient_usernames(row) mapped[:custom_fields] = {import_user_ids: current_user_ids.join(',')} - if mapped[:target_usernames].empty? # pm with yourself? + if mapped[:target_usernames].empty? puts "Private message without recipients. Skipping #{row[:msg_id]}: #{row[:message_subject][0..40]}" return nil end @@ -80,11 +80,10 @@ module ImportScripts::PhpBB3 end def get_recipient_usernames(row) - author_id = row[:author_id].to_s import_user_ids = get_recipient_user_ids(row[:to_address]) import_user_ids.map! do |import_user_id| - import_user_id.to_s == author_id ? nil : @lookup.find_user_by_import_id(import_user_id).try(:username) + @lookup.find_user_by_import_id(import_user_id).try(:username) end.compact end diff --git a/script/import_scripts/phpbb3/settings.yml b/script/import_scripts/phpbb3/settings.yml index 8377860e8d..eff7ac3a6d 100644 --- a/script/import_scripts/phpbb3/settings.yml +++ b/script/import_scripts/phpbb3/settings.yml @@ -1,13 +1,13 @@ # This is an example settings file for the phpBB3 importer. database: - type: MySQL # currently only MySQL is supported - more to come soon + type: MySQL # currently only MySQL is supported host: localhost port: 3306 username: root password: schema: phpbb - table_prefix: phpbb # Usually all table names start with phpbb. Change this, if your forum is using a different prefix. + table_prefix: phpbb_ # Usually all table names start with phpbb_. Change this, if your forum is using a different prefix. batch_size: 1000 # Don't change this unless you know what you're doing. The default (1000) should work just fine. import: diff --git a/script/import_scripts/phpbb3/support/smiley_processor.rb b/script/import_scripts/phpbb3/support/smiley_processor.rb index 342cadfaab..9b4d643d0d 100644 --- a/script/import_scripts/phpbb3/support/smiley_processor.rb +++ b/script/import_scripts/phpbb3/support/smiley_processor.rb @@ -47,7 +47,8 @@ module ImportScripts::PhpBB3 [':?:'] => ':question:', [':idea:'] => ':bulb:', [':arrow:'] => ':arrow_right:', - [':|', ':-|'] => ':neutral_face:' + [':|', ':-|'] => ':neutral_face:', + [':geek:'] => ':nerd:' }.each do |smilies, emoji| smilies.each { |smiley| @smiley_map[smiley] = emoji } end From dbc25a9d649a222421fe0ae0bc0a84a5df68269a Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Tue, 12 Jul 2016 13:51:44 +1000 Subject: [PATCH 021/170] FEATURE: flags for suppressing pinned expansion To suppress pinned excerpt expansion on mobile set "show_pinned_excerpt_mobile" to false To suppress pinned excerpt expansion on desktop set "show_pinned_excerpt_desktop" to false --- .../discourse/components/topic-list-item.js.es6 | 10 ++++++++++ .../discourse/templates/mobile/discovery/topics.hbs | 5 ++++- .../templates/mobile/list/topic_list_item.raw.hbs | 4 +++- config/locales/server.en.yml | 2 ++ config/site_settings.yml | 6 ++++++ 5 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 index 943a7b6622..2f5026e108 100644 --- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 @@ -55,6 +55,16 @@ export default Ember.Component.extend(StringBuffer, { return false; } + if (this.site.mobileView) { + if (!this.siteSettings.show_pinned_excerpt_mobile) { + return false; + } + } else { + if (!this.siteSettings.show_pinned_excerpt_desktop) { + return false; + } + } + if (this.get('expandGloballyPinned') && this.get('topic.pinned_globally')) { return true; } diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs index df2e91044e..6d61e9c4e7 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/topics.hbs @@ -17,7 +17,10 @@ showPosters=true currentUser=currentUser hideCategory=model.hideCategory - topics=model.topics}} + topics=model.topics + expandGloballyPinned=expandGloballyPinned + expandAllPinned=expandAllPinned + }} {{/if}} {{/discovery-topics-list}} diff --git a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs index 1047de9675..e630889d49 100644 --- a/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/list/topic_list_item.raw.hbs @@ -1,5 +1,5 @@ - {{~#unless topic.hasExcerpt}} + {{~#unless expandPinned}}
@@ -13,7 +13,9 @@ {{#if topic.unseen}} {{/if}} + {{~#if expandPinned}} {{raw "list/topic-excerpt" topic=topic}} + {{/if~}}
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 75f8a4404d..80aea6e724 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -852,6 +852,8 @@ en: exclude_rel_nofollow_domains: "A list of domains where nofollow should not be added to links. tld.com will automatically allow sub.tld.com as well. As a minimum, you should add the top-level domain of this site to help web crawlers find all content. If other parts of your website are at other domains, add those too." post_excerpt_maxlength: "Maximum length of a post excerpt / summary." + show_pinned_excerpt_mobile: "Show excerpt on pinned topics in mobile view." + show_pinned_excerpt_desktop: "Show excerpt on pinned topics in desktop view." post_onebox_maxlength: "Maximum length of a oneboxed Discourse post in characters." onebox_domains_whitelist: "A list of domains to allow oneboxing for; these domains should support OpenGraph or oEmbed. Test them at http://iframely.com/debug" diff --git a/config/site_settings.yml b/config/site_settings.yml index 5e726a2fd9..9fe29a028c 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -474,6 +474,12 @@ posting: client: true default: 0 post_excerpt_maxlength: 300 + show_pinned_excerpt_mobile: + client: true + default: true + show_pinned_excerpt_desktop: + client: true + default: true display_name_on_posts: client: true default: false From 86b3de510bbf7fdd22da7f2b38b8a1d330506ec8 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Tue, 12 Jul 2016 17:03:42 +1000 Subject: [PATCH 022/170] UX: staged posts show up with opacity 0.4 till on server --- app/assets/javascripts/discourse/widgets/post.js.es6 | 1 + app/assets/stylesheets/common/base/topic-post.scss | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index 366ff29453..d48e054a73 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -401,6 +401,7 @@ export default createWidget('post', { if (attrs.cloaked) { return 'cloaked-post'; } const classNames = ['topic-post', 'clearfix']; + if (attrs.id === -1) { classNames.push('staged'); } if (attrs.selected) { classNames.push('selected'); } if (attrs.topicOwner) { classNames.push('topic-owner'); } if (attrs.hidden) { classNames.push('post-hidden'); } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 9c1eeb1b8d..6028d278ab 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -107,6 +107,10 @@ aside.quote { opacity: 0.5; } +.topic-post.staged { + opacity: 0.4; +} + .quote-controls { float: right; From f369d492b33566ce890703e4562168f343a634c5 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Tue, 12 Jul 2016 21:11:33 +1000 Subject: [PATCH 023/170] FEATURE: stop linking to last post in crawler view This only makes stuff harder for google AND does not even function correctly --- app/views/list/list.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/list/list.erb b/app/views/list/list.erb index b01f7a6555..a88fb49974 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -37,7 +37,7 @@ <% if (!@category || @category.has_children?) && t.category %> [<%= t.category.name %>] <% end %> - '>(<%= t.posts_count %>) + '>(<%= t.posts_count %>)
<% end %> From 166d753bd38417a67cd38ef0a51f68a99510341b Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 12 Jul 2016 17:53:26 +0530 Subject: [PATCH 024/170] FIX: delete PostgreSQL dump before gzipping archive (#4323) --- lib/backup_restore/backuper.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/backup_restore/backuper.rb b/lib/backup_restore/backuper.rb index 14e0b75747..b8047abab2 100644 --- a/lib/backup_restore/backuper.rb +++ b/lib/backup_restore/backuper.rb @@ -257,6 +257,8 @@ module BackupRestore end end + remove_tmp_directory + log "Gzipping archive, this may take a while..." `gzip -5 #{tar_filename}` end @@ -284,7 +286,6 @@ module BackupRestore def clean_up log "Cleaning stuff up..." remove_tar_leftovers - remove_tmp_directory unpause_sidekiq disable_readonly_mode if Discourse.readonly_mode? mark_backup_as_not_running From 59159291667d6f2f7655b5eff96b6b14734cbcad Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Tue, 12 Jul 2016 12:08:55 -0300 Subject: [PATCH 025/170] FIX: Unicode aware text sentinel (#4301) * FIX: Handle unicode text on Text Sentinel Uses active_support to properly handle unicode text * Adds test cases to unicode Text Sentinel --- lib/text_sentinel.rb | 8 ++++++-- spec/components/text_sentinel_spec.rb | 6 +++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/text_sentinel.rb b/lib/text_sentinel.rb index 8e01a58881..4e9bf8840c 100644 --- a/lib/text_sentinel.rb +++ b/lib/text_sentinel.rb @@ -1,3 +1,7 @@ +# Whe use ActiveSupport mb_chars from here to properly support non ascii downcase +# TODO remove when ruby 2.4 lands +require 'active_support/core_ext/string/multibyte' + # # Given a string, tell us whether or not is acceptable. # @@ -72,8 +76,8 @@ class TextSentinel def seems_quiet? - # We don't allow all upper case content in english - SiteSetting.allow_uppercase_posts || not((@text =~ /[A-Z]+/) && !(@text =~ /[^[:ascii:]]/) && (@text == @text.upcase)) + # We don't allow all upper case content + SiteSetting.allow_uppercase_posts || @text == @text.mb_chars.downcase.to_s || @text != @text.mb_chars.upcase.to_s end end diff --git a/spec/components/text_sentinel_spec.rb b/spec/components/text_sentinel_spec.rb index ea11c1c145..f75a176459 100644 --- a/spec/components/text_sentinel_spec.rb +++ b/spec/components/text_sentinel_spec.rb @@ -49,7 +49,7 @@ describe TextSentinel do [ 'evil trout is evil', "去年十社會警告", "P.S. Пробирочка очень толковая и весьма умная, так что не обнимайтесь.", - "LOOK: 去年十社會警告" + "Look: 去年十社會警告" ].each do |valid_body| it "handles a valid body in a private message" do expect(TextSentinel.body_sentinel(valid_body, private_message: true)).to be_valid @@ -74,6 +74,10 @@ describe TextSentinel do expect(TextSentinel.new(valid_string.upcase)).not_to be_valid end + it "doesn't allow all caps foreign topics" do + expect(TextSentinel.new('É COM VOCÊ LOMBARDIAM. MA VEJAM SÓ, VEJAM SÓ. VALENDO UM MILHÃO DE REAISAMMM. MA VALE DÉRREAISAM?')).not_to be_valid + end + it "allows all caps topics when loud posts are allowed" do SiteSetting.stubs(:allow_uppercase_posts).returns(true) expect(TextSentinel.new(valid_string.upcase)).to be_valid From c2b769bd9524fcf97ace18e448c38bdf12fcc280 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 12 Jul 2016 12:30:39 -0400 Subject: [PATCH 026/170] Provide hints about which files can be restored --- script/discourse | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/script/discourse b/script/discourse index af03de89b3..b6c14276bf 100755 --- a/script/discourse +++ b/script/discourse @@ -56,7 +56,18 @@ class DiscourseCLI < Thor end desc "restore", "Restore a Discourse backup" - def restore(filename) + def restore(filename=nil) + + if !filename + puts "You must provide a filename to restore. Did you mean one of the following?\n\n" + + Dir["public/backups/default/*"].each do |f| + puts "script/discourse restore #{File.basename(f)}" + end + + return + end + load_rails require "backup_restore/backup_restore" require "backup_restore/restorer" From 0c3b04917698d0692670a5bbad63db17ea6c6ab3 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 12 Jul 2016 13:33:13 -0400 Subject: [PATCH 027/170] FIX: Autolinking in email formatter was broken --- lib/email_cook.rb | 2 +- spec/components/email_cook_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 spec/components/email_cook_spec.rb diff --git a/lib/email_cook.rb b/lib/email_cook.rb index 48aea34454..03f40ea2a1 100644 --- a/lib/email_cook.rb +++ b/lib/email_cook.rb @@ -3,7 +3,7 @@ class EmailCook def self.url_regexp - /[^\>]*((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»\s]))/ + /^((?:https?:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.])(?:[^\s()<>]+|\([^\s()<>]+\))+(?:\([^\s()<>]+\)|[^`!()\[\]{};:'".,<>?«»“”‘’\s]))/ end def initialize(raw) diff --git a/spec/components/email_cook_spec.rb b/spec/components/email_cook_spec.rb new file mode 100644 index 0000000000..a440fbf792 --- /dev/null +++ b/spec/components/email_cook_spec.rb @@ -0,0 +1,13 @@ +require 'rails_helper' +require 'email_cook' + +describe EmailCook do + + it 'adds linebreaks' do + expect(EmailCook.new("hello\nworld\n").cook).to eq("hello\n
world\n
") + end + + it 'autolinks' do + expect(EmailCook.new("https://www.eviltrout.com").cook).to eq("https://www.eviltrout.com
") + end +end From bb901297310b7527e38d6c5f90c1fd01860a901b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 12 Jul 2016 13:49:03 -0400 Subject: [PATCH 028/170] Improvements to email cook text rendering --- lib/email_cook.rb | 6 +++++- spec/components/email_cook_spec.rb | 22 +++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/email_cook.rb b/lib/email_cook.rb index 03f40ea2a1..8c91ac27cb 100644 --- a/lib/email_cook.rb +++ b/lib/email_cook.rb @@ -26,11 +26,15 @@ class EmailCook in_quote = false else + sz = l.size + l.scan(EmailCook.url_regexp).each do |m| url = m[0] l.gsub!(url, "#{url}") end - result << l << "
" + + result << l + result << "
" if sz < 60 end end diff --git a/spec/components/email_cook_spec.rb b/spec/components/email_cook_spec.rb index a440fbf792..783f7ef575 100644 --- a/spec/components/email_cook_spec.rb +++ b/spec/components/email_cook_spec.rb @@ -3,10 +3,30 @@ require 'email_cook' describe EmailCook do - it 'adds linebreaks' do + it 'adds linebreaks to short lines' do expect(EmailCook.new("hello\nworld\n").cook).to eq("hello\n
world\n
") end + it "doesn't add linebreaks to long lines" do + long = < +
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc convallis volutpat +risus. Nulla ac faucibus quam, quis cursus lorem. Sed rutrum eget nunc sed accumsan. +Vestibulum feugiat mi vitae turpis tempor dignissim. +
+LONG_COOKED + expect(EmailCook.new(long).cook).to eq(long_cooked.strip) + end + it 'autolinks' do expect(EmailCook.new("https://www.eviltrout.com").cook).to eq("https://www.eviltrout.com
") end From b00fd79989e4d58fed04355210597b8eb95c2369 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 12 Jul 2016 15:37:19 -0400 Subject: [PATCH 029/170] FIX: Even better email rendering fixes --- lib/email_cook.rb | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/email_cook.rb b/lib/email_cook.rb index 8c91ac27cb..f2cbaa33cc 100644 --- a/lib/email_cook.rb +++ b/lib/email_cook.rb @@ -13,7 +13,9 @@ class EmailCook def cook result = "" + in_text = false in_quote = false + quote_buffer = "" @raw.each_line do |l| @@ -34,7 +36,16 @@ class EmailCook end result << l - result << "
" if sz < 60 + + if sz < 60 + result << "
" + if in_text + result << "
" + end + in_text = false + else + in_text = true + end end end @@ -42,7 +53,7 @@ class EmailCook result << "
#{quote_buffer}
" end - result.gsub!(/(
){3,10}/, '

') + result.gsub!(/(
\n*){3,10}/, '

') result end From 91e4af0d3dff8df6cdeac1a4e5a3b2b9a6b2db03 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 12 Jul 2016 16:26:21 -0400 Subject: [PATCH 030/170] FIX: restore of a backup from an older Discourse version can create new tables in the wrong schema, leading to UndefinedTable errors --- lib/backup_restore/restorer.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/backup_restore/restorer.rb b/lib/backup_restore/restorer.rb index 3b843998ab..2ecccc1e14 100644 --- a/lib/backup_restore/restorer.rb +++ b/lib/backup_restore/restorer.rb @@ -256,6 +256,7 @@ module BackupRestore log "Migrating the database..." Discourse::Application.load_tasks ENV["VERSION"] = @current_version.to_s + User.exec_sql("SET search_path = public, pg_catalog;") Rake::Task["db:migrate"].invoke end From 15a46d419f71330f9db90ea1ef00b2a2775ec5ad Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 12 Jul 2016 16:26:44 -0700 Subject: [PATCH 031/170] tweak in-reply-to email CSS --- lib/email/styles.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/email/styles.rb b/lib/email/styles.rb index c915864571..9186bb3f13 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -82,7 +82,7 @@ module Email end def format_notification - style('.previous-discussion', 'font-size: 17px; color: #444;') + style('.previous-discussion', 'font-size: 17px; color: #444; margin-bottom:10px;') style('.notification-date', "text-align:right;color:#999999;padding-right:5px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;font-size:11px") style('.username', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;color:#3b5998;text-decoration:none;font-weight:bold") style('.user-title', "font-size:13px;font-family:'lucida grande',tahoma,verdana,arial,sans-serif;text-decoration:none;margin-left:7px;color: #999;") From 973a7c9d3a3d6ded8a9aa633e1e8222378c8be2e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 13 Jul 2016 11:58:31 +0800 Subject: [PATCH 032/170] FIX: Redeeming an invitation fails if inviter has been destroyed. --- app/models/invite_redeemer.rb | 6 ++++-- spec/models/invite_redeemer_spec.rb | 31 ++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb index dae43a92bc..e799caf45a 100644 --- a/app/models/invite_redeemer.rb +++ b/app/models/invite_redeemer.rb @@ -112,8 +112,10 @@ InviteRedeemer = Struct.new(:invite, :username, :name) do end def notify_invitee - invite.invited_by.notifications.create(notification_type: Notification.types[:invitee_accepted], - data: {display_username: invited_user.username}.to_json) + if inviter = invite.invited_by + inviter.notifications.create(notification_type: Notification.types[:invitee_accepted], + data: {display_username: invited_user.username}.to_json) + end end def delete_duplicate_invites diff --git a/spec/models/invite_redeemer_spec.rb b/spec/models/invite_redeemer_spec.rb index a61819fd49..4b5d1bcc44 100644 --- a/spec/models/invite_redeemer_spec.rb +++ b/spec/models/invite_redeemer_spec.rb @@ -2,8 +2,9 @@ require 'rails_helper' describe InviteRedeemer do - describe '#create_for_email' do + describe '#create_user_from_invite' do let(:user) { InviteRedeemer.create_user_from_invite(Fabricate(:invite, email: 'walter.white@email.com'), 'walter', 'Walter White') } + it "should be created correctly" do expect(user.username).to eq('walter') expect(user.name).to eq('Walter White') @@ -11,4 +12,32 @@ describe InviteRedeemer do expect(user.email).to eq('walter.white@email.com') end end + + describe "#redeem" do + let(:invite) { Fabricate(:invite) } + let(:name) { 'john snow' } + let(:username) { 'kingofthenorth' } + let(:invite_redeemer) { InviteRedeemer.new(invite, username, name) } + + it "should redeem the invite" do + inviter = invite.invited_by + user = invite_redeemer.redeem + + expect(user.name).to eq(name) + expect(user.username).to eq(username) + expect(user.invited_by).to eq(inviter) + expect(inviter.notifications.count).to eq(1) + end + + it "should not blow up if invited_by user has been removed" do + invite.invited_by.destroy! + invite.reload + + user = invite_redeemer.redeem + + expect(user.name).to eq(name) + expect(user.username).to eq(username) + expect(user.invited_by).to eq(nil) + end + end end From 467b35df14b89fff37b457eeaf7360d3b795fc17 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Wed, 13 Jul 2016 02:08:01 -0700 Subject: [PATCH 033/170] shorten copy for Watching First Post --- config/locales/client.en.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index cae4000993..b6e1d6dd04 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -409,7 +409,7 @@ en: title: "Watching" description: "You will be notified of every new post in every message, and a count of new replies will be shown." watching_first_post: - title: "Watching First Post Only" + title: "Watching First Post" description: "You will only be notified of the first post in each new topic in this group." tracking: title: "Tracking" @@ -550,7 +550,7 @@ en: watched_categories_instructions: "You will automatically watch all topics in these categories. You will be notified of all new posts and topics, and a count of new posts will also appear next to the topic." tracked_categories: "Tracked" tracked_categories_instructions: "You will automatically track all new topics in these categories. A count of new posts will appear next to the topic." - watched_first_post_categories: "Watching First Post Only" + watched_first_post_categories: "Watching First Post" watched_first_post_categories_instructions: "You will be notified of the first post in each new topic in these categories." muted_categories: "Muted" muted_categories_instructions: "You will not be notified of anything about new topics in these categories, and they will not appear in latest." @@ -1820,7 +1820,7 @@ en: title: "Watching" description: "You will automatically watch all new topics in these categories. You will be notified of every new post in every topic, and a count of new replies will be shown." watching_first_post: - title: "Watching First Post Only" + title: "Watching First Post" description: "You will only be notified of the first post in each new topic in these categories." tracking: title: "Tracking" @@ -2144,7 +2144,7 @@ en: title: "Watching" description: "You will automatically watch all new topics in this tag. You will be notified of all new posts and topics, plus the count of unread and new posts will also appear next to the topic." watching_first_post: - title: "Watching First Post Only" + title: "Watching First Post" description: "You will only be notified of the first post in each new topic in this tag." tracking: title: "Tracking" From c3cab989981676bd990a0dfcbb84ec9948b699d2 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 7 Jul 2016 15:52:56 +0800 Subject: [PATCH 034/170] FEATURE: Admins should be able to create polls even when plugin is disabled. --- app/assets/javascripts/discourse/lib/text.js.es6 | 1 + .../javascripts/pretty-text/pretty-text.js.es6 | 15 ++++++++++++++- app/models/post.rb | 10 +++++++--- lib/pretty_text.rb | 5 ++++- lib/pretty_text/helpers.rb | 6 ++++++ lib/pretty_text/shims.js | 4 ++++ .../initializers/add-poll-ui-builder.js.es6 | 4 ++++ .../lib/discourse-markdown/poll.js.es6 | 8 +++++--- plugins/poll/lib/polls_validator.rb | 2 +- plugins/poll/plugin.rb | 8 ++++---- 10 files changed, 50 insertions(+), 13 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/text.js.es6 b/app/assets/javascripts/discourse/lib/text.js.es6 index fba637345b..01c15add3c 100644 --- a/app/assets/javascripts/discourse/lib/text.js.es6 +++ b/app/assets/javascripts/discourse/lib/text.js.es6 @@ -7,6 +7,7 @@ export function cook(text) { const opts = { getURL: Discourse.getURLWithCDN, + currentUser: Discourse.__container__.lookup('current-user:main'), siteSettings }; diff --git a/app/assets/javascripts/pretty-text/pretty-text.js.es6 b/app/assets/javascripts/pretty-text/pretty-text.js.es6 index e254d7dbc4..a70da34ccf 100644 --- a/app/assets/javascripts/pretty-text/pretty-text.js.es6 +++ b/app/assets/javascripts/pretty-text/pretty-text.js.es6 @@ -12,7 +12,17 @@ export function registerOption(fn) { export function buildOptions(state) { setup(); - const { siteSettings, getURL, lookupAvatar, getTopicInfo, topicId, categoryHashtagLookup } = state; + const { + siteSettings, + getURL, + lookupAvatar, + getTopicInfo, + topicId, + categoryHashtagLookup, + userId, + getCurrentUser, + currentUser + } = state; const features = { 'bold-italics': true, @@ -34,6 +44,9 @@ export function buildOptions(state) { getTopicInfo, topicId, categoryHashtagLookup, + userId, + getCurrentUser, + currentUser, mentionLookup: state.mentionLookup, }; diff --git a/app/models/post.rb b/app/models/post.rb index 1946d4fa71..8bb5ef151e 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -197,12 +197,16 @@ class Post < ActiveRecord::Base if cook_method == Post.cook_methods[:email] cooked = EmailCook.new(raw).cook else - cooked = if !self.user || SiteSetting.tl3_links_no_follow || !self.user.has_trust_level?(TrustLevel[3]) + cloned = args.dup + cloned[1] ||= {} + + post_user = self.user + cloned[1][:user_id] = post_user.id if post_user + + cooked = if !post_user || SiteSetting.tl3_links_no_follow || !post_user.has_trust_level?(TrustLevel[3]) post_analyzer.cook(*args) else # At trust level 3, we don't apply nofollow to links - cloned = args.dup - cloned[1] ||= {} cloned[1][:omit_nofollow] = true post_analyzer.cook(*cloned) end diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 6259f4107a..85ba6df90b 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -114,7 +114,7 @@ module PrettyText end end - def self.markdown(text, opts=nil) + def self.markdown(text, opts={}) # 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) baked = nil @@ -143,7 +143,10 @@ module PrettyText context.eval("__optInput.topicId = #{opts[:topicId].to_i};") end + context.eval("__optInput.userId = #{opts[:user_id].to_i};") if opts[:user_id] + context.eval("__optInput.getURL = __getURL;") + context.eval("__optInput.getCurrentUser = __getCurrentUser;") context.eval("__optInput.lookupAvatar = __lookupAvatar;") context.eval("__optInput.getTopicInfo = __getTopicInfo;") context.eval("__optInput.categoryHashtagLookup = __categoryLookup;") diff --git a/lib/pretty_text/helpers.rb b/lib/pretty_text/helpers.rb index 81f419977d..fa61e9efc2 100644 --- a/lib/pretty_text/helpers.rb +++ b/lib/pretty_text/helpers.rb @@ -68,6 +68,12 @@ module PrettyText nil end end + + def get_current_user(user_id) + user = User.find_by(id: user_id) + staff = user ? user.staff? : false + { staff: staff } + end end end diff --git a/lib/pretty_text/shims.js b/lib/pretty_text/shims.js index 040d56b0ac..c156fdb0cd 100644 --- a/lib/pretty_text/shims.js +++ b/lib/pretty_text/shims.js @@ -46,6 +46,10 @@ function __lookupAvatar(p) { return __utils.avatarImg({size: "tiny", avatarTemplate: __helpers.avatar_template(p) }, __getURL); } +function __getCurrentUser(userId) { + return __helpers.get_current_user(userId); +} + I18n = { t: function(a,b) { return __helpers.t(a,b); } }; diff --git a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 index 82b830c3e1..66fbb76404 100644 --- a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 +++ b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 @@ -2,6 +2,10 @@ import { withPluginApi } from 'discourse/lib/plugin-api'; import showModal from 'discourse/lib/show-modal'; function initializePollUIBuilder(api) { + const siteSettings = api.container.lookup('site-settings:main'); + + if (!siteSettings.poll_enabled && (api.getCurrentUser() && !api.getCurrentUser().staff)) return; + const ComposerController = api.container.lookupFactory("controller:composer"); ComposerController.reopen({ actions: { diff --git a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 index c182c2439b..927aa5cc52 100644 --- a/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 +++ b/plugins/poll/assets/javascripts/lib/discourse-markdown/poll.js.es6 @@ -7,7 +7,9 @@ const WHITELISTED_ATTRIBUTES = ["type", "name", "min", "max", "step", "order", " const ATTRIBUTES_REGEX = new RegExp("(" + WHITELISTED_ATTRIBUTES.join("|") + ")=['\"]?[^\\s\\]]+['\"]?", "g"); registerOption((siteSettings, opts) => { - opts.features.poll = !!siteSettings.poll_enabled; + const currentUser = (opts.getCurrentUser && opts.getCurrentUser(opts.userId)) || opts.currentUser; + + opts.features.poll = !!siteSettings.poll_enabled || currentUser.staff; opts.pollMaximumOptions = siteSettings.poll_maximum_options; }); @@ -179,11 +181,11 @@ export function setup(helper) { /*! * Joseph Myer's md5() algorithm wrapped in a self-invoked function to prevent * global namespace polution, modified to hash unicode characters as UTF-8. - * + * * Copyright 1999-2010, Joseph Myers, Paul Johnston, Greg Holt, Will Bond * http://www.myersdaily.org/joseph/javascript/md5-text.html * http://pajhome.org.uk/crypt/md5 - * + * * Released under the BSD license * http://www.opensource.org/licenses/bsd-license */ diff --git a/plugins/poll/lib/polls_validator.rb b/plugins/poll/lib/polls_validator.rb index c02920bf38..2321f7f0af 100644 --- a/plugins/poll/lib/polls_validator.rb +++ b/plugins/poll/lib/polls_validator.rb @@ -7,7 +7,7 @@ module DiscoursePoll def validate_polls polls = {} - extracted_polls = DiscoursePoll::Poll::extract(@post.raw, @post.topic_id) + extracted_polls = DiscoursePoll::Poll::extract(@post.raw, @post.topic_id, @post.user_id) extracted_polls.each do |poll| # polls should have a unique name diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb index c752e0dfcd..91f070a1b2 100644 --- a/plugins/poll/plugin.rb +++ b/plugins/poll/plugin.rb @@ -4,8 +4,6 @@ # authors: Vikhyat Korrapati (vikhyat), Régis Hanol (zogstrip) # url: https://github.com/discourse/discourse/tree/master/plugins/poll -enabled_site_setting :poll_enabled - register_asset "stylesheets/common/poll.scss" register_asset "stylesheets/common/poll-ui-builder.scss" register_asset "stylesheets/desktop/poll.scss", :desktop @@ -145,10 +143,10 @@ after_initialize do end end - def extract(raw, topic_id) + def extract(raw, topic_id, user_id = nil) # TODO: we should fix the callback mess so that the cooked version is available # in the validators instead of cooking twice - cooked = PrettyText.cook(raw, topic_id: topic_id) + cooked = PrettyText.cook(raw, topic_id: topic_id, user_id: user_id) parsed = Nokogiri::HTML(cooked) extracted_polls = [] @@ -252,6 +250,8 @@ after_initialize do end validate(:post, :validate_polls) do + return if !SiteSetting.poll_enabled? && (self.user && !self.user.staff?) + # only care when raw has changed! return unless self.raw_changed? From 41cbdb5dfa07084ed80747227bd5f13e3c45c4d9 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 13 Jul 2016 19:14:40 +0800 Subject: [PATCH 035/170] Fix the build. --- spec/components/email_cook_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/components/email_cook_spec.rb b/spec/components/email_cook_spec.rb index 783f7ef575..3f46781e2d 100644 --- a/spec/components/email_cook_spec.rb +++ b/spec/components/email_cook_spec.rb @@ -22,7 +22,7 @@ Hello,
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc convallis volutpat risus. Nulla ac faucibus quam, quis cursus lorem. Sed rutrum eget nunc sed accumsan. Vestibulum feugiat mi vitae turpis tempor dignissim. -
+

LONG_COOKED expect(EmailCook.new(long).cook).to eq(long_cooked.strip) end From 5fed886c8f177ca991b2a8bcef9abf5395127d62 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 13 Jul 2016 23:34:21 +0800 Subject: [PATCH 036/170] FIX: Update post replies when we move posts. (#4324) --- app/controllers/application_controller.rb | 4 +- app/models/post_mover.rb | 6 +++ app/models/post_reply.rb | 12 +++++ config/locales/server.en.yml | 4 ++ spec/models/post_mover_spec.rb | 58 +++++++++++++++++++++++ spec/models/post_reply_spec.rb | 19 ++++++++ 6 files changed, 100 insertions(+), 3 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4f60aef2df..6c17642f39 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -315,9 +315,7 @@ class ApplicationController < ActionController::Base 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! + post_ids |= PostReply.where(post_id: params[:reply_post_ids].map {|p| p.to_i}).pluck(:reply_id) end post_ids end diff --git a/app/models/post_mover.rb b/app/models/post_mover.rb index ec21ce7352..845226959b 100644 --- a/app/models/post_mover.rb +++ b/app/models/post_mover.rb @@ -76,6 +76,12 @@ class PostMover posts.each do |post| post.is_first_post? ? create_first_post(post) : move(post) end + + PostReply.where("reply_id in (:post_ids) OR post_id in (:post_ids)", post_ids: post_ids).each do |post_reply| + if post_reply.reply.topic_id != post_reply.post.topic_id + PostReply.delete_all(reply_id: post_reply.reply.id, post_id: post_reply.post.id) + end + end end def create_first_post(post) diff --git a/app/models/post_reply.rb b/app/models/post_reply.rb index 81c39269d1..c763751bab 100644 --- a/app/models/post_reply.rb +++ b/app/models/post_reply.rb @@ -3,6 +3,18 @@ class PostReply < ActiveRecord::Base belongs_to :reply, class_name: 'Post' validates_uniqueness_of :reply_id, scope: :post_id + validate :ensure_same_topic + + private + + def ensure_same_topic + if post.topic_id != reply.topic_id + self.errors.add( + :base, + I18n.t("activerecord.errors.models.post_reply.base.different_topic") + ) + end + end end # == Schema Information diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 80aea6e724..754ceccc5c 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -346,6 +346,10 @@ en: attributes: hex: invalid: "is not a valid color" + post_reply: + base: + different_topic: "Post and reply must belong to the same topic." + user_profile: no_info_me: "
the About Me field of your profile is currently blank, would you like to fill it out?
" diff --git a/spec/models/post_mover_spec.rb b/spec/models/post_mover_spec.rb index c90dcae0cb..03450fe051 100644 --- a/spec/models/post_mover_spec.rb +++ b/spec/models/post_mover_spec.rb @@ -29,6 +29,8 @@ describe PostMover do let!(:p4) { Fabricate(:post, topic: topic, reply_to_post_number: p2.post_number, user: user)} before do + p1.replies << p3 + p2.replies << p4 # add a like to a post, enable observers so we get user actions ActiveRecord::Base.observers.enable :all @like = PostAction.act(another_user, p4, PostActionType.types[:like]) @@ -72,6 +74,62 @@ describe PostMover do TopicLink.extract_from(p2) end + context "post replies" do + describe "when a post with replies is moved" do + it "should update post replies correctly" do + topic.move_posts( + user, + [p2.id], + title: 'GOT is a very addictive showw', category_id: category.id + ) + + expect(p2.reload.replies).to eq([]) + end + end + + describe "when replies of a post have been moved" do + it "should update post replies correctly" do + p5 = Fabricate( + :post, + topic: topic, + reply_to_post_number: p2.post_number, + user: another_user + ) + + p2.replies << p5 + + topic.move_posts( + user, + [p4.id], + title: 'GOT is a very addictive showw', category_id: category.id + ) + + expect(p2.reload.replies).to eq([p5]) + end + end + + describe "when only one reply is left behind" do + it "should update post replies correctly" do + p5 = Fabricate( + :post, + topic: topic, + reply_to_post_number: p2.post_number, + user: another_user + ) + + p2.replies << p5 + + topic.move_posts( + user, + [p2.id, p4.id], + title: 'GOT is a very addictive showw', category_id: category.id + ) + + expect(p2.reload.replies).to eq([p4]) + end + end + end + context "to a new topic" do it "works correctly" do diff --git a/spec/models/post_reply_spec.rb b/spec/models/post_reply_spec.rb index e3392e338e..d67ef33cae 100644 --- a/spec/models/post_reply_spec.rb +++ b/spec/models/post_reply_spec.rb @@ -1,8 +1,27 @@ require 'rails_helper' describe PostReply do + let(:topic) { Fabricate(:topic) } + let(:post) { Fabricate(:post, topic: topic) } + let(:other_post) { Fabricate(:post, topic: topic) } it { is_expected.to belong_to :post } it { is_expected.to belong_to :reply } + context "validation" do + it "should ensure that the posts belong in the same topic" do + expect(PostReply.new(post: post, reply: other_post)).to be_valid + + other_topic = Fabricate(:topic) + other_post.update_attributes!(topic_id: other_topic.id) + other_post.reload + + post_reply = PostReply.new(post: post, reply: other_post) + expect(post_reply).to_not be_valid + + expect(post_reply.errors[:base]).to include( + I18n.t("activerecord.errors.models.post_reply.base.different_topic") + ) + end + end end From 1f5b593800c581ac66ba127cd2f36168d7ecdf97 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 13 Jul 2016 11:54:48 -0400 Subject: [PATCH 037/170] DOCS: Instructions for creating an admin user --- docs/VAGRANT.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/VAGRANT.md b/docs/VAGRANT.md index aa52836105..71bd278d43 100644 --- a/docs/VAGRANT.md +++ b/docs/VAGRANT.md @@ -94,6 +94,29 @@ In a few seconds, rails will start serving pages. To access them, open a web bro You can now edit files on your local file system, using your favorite text editor or IDE. When you reload your web browser, it should have the latest changes. +### Creating an Admin User + +You'll want an admin account to be able to do anything fun on your new Discourse environment. The easiest way to do this is to sign up for an account in the web browser with a username and password. + +Once you have done that, you'll notice **no mail is delivered** to confirm it. This is because in the development environment emails are disabled by default. + +An easy way to approve your account and give it admin access is to enter a rails console and update the data. Run the following commands after `vagrant ssh`: + +```bash +cd /vagrant +bundle exec rails console +``` + +Once the console opens, enter the following commands. Make sure to replace `eviltrout` with the username you signed up with. + +```ruby +user = User.find_by_username('eviltrout') +user.update_columns(admin: true) +EmailToken.confirm(user.email_tokens.pluck(:token).last) +``` + +Your admin account should be approved. Log in in your browser and you're good to go! + ### Tests If you're actively working on Discourse, we recommend that you run rake autospec, which will run the specs. It’s very, very smart. It’ll abort very long test runs. So if it starts running all of the specs and then you just start editing a spec file and save it, it knows that it’s time to interrupt the spec suite, run this one spec for you, then it’ll keep running these specs until they pass as well. If you fail a spec by saving it and then go and start editing around the project to try and fix that spec, it’ll detect that and run that one failing spec, not a hundred of them. From 24b83d1c578339c5bfd1c7c593c959b1182dff21 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 13 Jul 2016 12:04:30 -0400 Subject: [PATCH 038/170] DOCS: Much better way to create an admin account. Thanks @techapj --- docs/VAGRANT.md | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/docs/VAGRANT.md b/docs/VAGRANT.md index 71bd278d43..f1393aa96e 100644 --- a/docs/VAGRANT.md +++ b/docs/VAGRANT.md @@ -96,23 +96,11 @@ You can now edit files on your local file system, using your favorite text edito ### Creating an Admin User -You'll want an admin account to be able to do anything fun on your new Discourse environment. The easiest way to do this is to sign up for an account in the web browser with a username and password. - -Once you have done that, you'll notice **no mail is delivered** to confirm it. This is because in the development environment emails are disabled by default. - -An easy way to approve your account and give it admin access is to enter a rails console and update the data. Run the following commands after `vagrant ssh`: +You'll want an admin account to be able to do anything fun on your new Discourse environment. Enter your vagrant image by using `vagrant ssh` then +run the following command and follow the instructions: ```bash -cd /vagrant -bundle exec rails console -``` - -Once the console opens, enter the following commands. Make sure to replace `eviltrout` with the username you signed up with. - -```ruby -user = User.find_by_username('eviltrout') -user.update_columns(admin: true) -EmailToken.confirm(user.email_tokens.pluck(:token).last) +rake admin:create ``` Your admin account should be approved. Log in in your browser and you're good to go! From c7bbc1cebf5f0e774c45a21e8b00390aca307937 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 13 Jul 2016 21:54:49 +0530 Subject: [PATCH 039/170] update onebox gem --- Gemfile.lock | 2 +- app/assets/stylesheets/common/base/onebox.scss | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 29e84d147c..e931b62998 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -218,7 +218,7 @@ GEM omniauth-twitter (1.2.1) json (~> 1.3) omniauth-oauth (~> 1.1) - onebox (1.5.42) + onebox (1.5.43) htmlentities (~> 4.3.4) moneta (~> 0.8) multi_json (~> 1.11) diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index a31a4b4aa6..aff56f5e47 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -330,9 +330,15 @@ aside.onebox.twitterstatus .onebox-body { } // resize stackexchange onebox image -aside.onebox.stackexchange .onebox-body img { - max-height: 60%; - max-width: 10%; +aside.onebox.stackexchange .onebox-body { + img { + max-height: 60%; + max-width: 10%; + } + + .tags { + color: gray; + } } .onebox-metadata { From 5e8cfe8cef64999dc19d23545af0cbceb41b1fda Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 13 Jul 2016 13:55:42 -0400 Subject: [PATCH 040/170] tag group input needs to be bigger --- app/assets/stylesheets/common/base/tagging.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/tagging.scss b/app/assets/stylesheets/common/base/tagging.scss index 8882ae1bb2..21a438dc33 100644 --- a/app/assets/stylesheets/common/base/tagging.scss +++ b/app/assets/stylesheets/common/base/tagging.scss @@ -232,9 +232,9 @@ header .discourse-tag {color: $tag-color !important; } } } .group-tags-list .tag-chooser { - height: 150px !important; + height: 250px !important; .select2-choices { - height: 150px !important; // to fight with select2.scss's important + height: 250px !important; // to fight with select2.scss's important } } .btn {margin-left: 10px;} From 29c9979b9b761b4524a46173b563eb0f575cb52d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 13 Jul 2016 14:05:35 -0400 Subject: [PATCH 041/170] Deprecate the BBCode module too for plugins that reach in there. --- app/assets/javascripts/deprecated.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/deprecated.js b/app/assets/javascripts/deprecated.js index 50e0fef681..5f8a06d597 100644 --- a/app/assets/javascripts/deprecated.js +++ b/app/assets/javascripts/deprecated.js @@ -17,6 +17,8 @@ deprecate('Dialect', ['inlineRegexp', 'inlineBetween', 'addPreProcessor', 'replaceBlock', 'inlineReplace', 'registerInline', 'registerEmoji']); + deprecate('BBCode', ['replaceBBCode', 'register', 'rawBBCode', 'replaceBBCodeParamsRaw']); + Discourse.dialect_deprecated = true; Discourse.ajax = function() { From 00e45c0d3c3f5b2913e32dc05bfbea366fb894ff Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 13 Jul 2016 15:36:34 -0400 Subject: [PATCH 042/170] FIX: Safari strict mode errors --- .../discourse/lib/raw-handlebars.js.es6 | 91 +++++++++---------- .../engines/discourse-markdown.js.es6 | 2 +- .../engines/discourse-markdown/bbcode.js.es6 | 64 +++++++------ 3 files changed, 84 insertions(+), 73 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/raw-handlebars.js.es6 b/app/assets/javascripts/discourse/lib/raw-handlebars.js.es6 index e33c912009..423d8e8835 100644 --- a/app/assets/javascripts/discourse/lib/raw-handlebars.js.es6 +++ b/app/assets/javascripts/discourse/lib/raw-handlebars.js.es6 @@ -14,7 +14,7 @@ const RawHandlebars = Handlebars.create(); RawHandlebars.helper = function() {}; RawHandlebars.helpers = objectCreate(Handlebars.helpers); -RawHandlebars.helpers.get = function(context, options){ +RawHandlebars.helpers['get'] = function(context, options) { var firstContext = options.contexts[0]; var val = firstContext[context]; @@ -38,7 +38,7 @@ function stringCompatHelper(fn) { RawHandlebars.registerHelper('each', function(localName,inKeyword,contextName,options){ var list = Em.get(this, contextName); var output = []; - var innerContext = Object.create(this); + var innerContext = objectCreate(this); for (var i=0; i ['span', {'class': 'bbcode-b'}].concat(contents)); replaceBBCode('i', contents => ['span', {'class': 'bbcode-i'}].concat(contents)); From b8261a662bd185b1e51883a21c50054ca2e6c21e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 13 Jul 2016 16:11:48 -0400 Subject: [PATCH 043/170] FIX: `siteSettings` weren't getting applied to plugin auth --- lib/plugin/instance.rb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index c0c40f8427..7085122c9a 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -244,9 +244,12 @@ define("discourse/initializers/login-method-#{hash}", __exports__["default"] = { name: "login-method-#{hash}", after: "inject-objects", - initialize: function() { + initialize: function(container) { if (Ember.testing) { return; } - module.register(#{auth_json}); + + var authOpts = #{auth_json}; + authOpts.siteSettings = container.lookup('site-settings:main'); + module.register(authOpts); } }; }); From 926c021125fbd6e847a12c7d3acc0d738b2e80ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 13 Jul 2016 22:32:46 +0200 Subject: [PATCH 044/170] set 'List-Unsubscribe' email header to new unsubscribe url instead of user preferences url --- lib/email/message_builder.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/email/message_builder.rb b/lib/email/message_builder.rb index 79fe8a46b4..4be0c89a4c 100644 --- a/lib/email/message_builder.rb +++ b/lib/email/message_builder.rb @@ -132,10 +132,11 @@ module Email def header_args result = {} if @opts[:add_unsubscribe_link] - result['List-Unsubscribe'] = "<#{template_args[:user_preferences_url]}>" + unsubscribe_url = @template_args[:unsubscribe_url].presence || @template_args[:user_preferences_url] + result['List-Unsubscribe'] = "<#{unsubscribe_url}>" end - result['X-Discourse-Post-Id'] = @opts[:post_id].to_s if @opts[:post_id] + result['X-Discourse-Post-Id'] = @opts[:post_id].to_s if @opts[:post_id] result['X-Discourse-Topic-Id'] = @opts[:topic_id].to_s if @opts[:topic_id] if allow_reply_by_email? From 7b6d946613e2ceded861b00ccbbd0d4d6ce3ca3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 13 Jul 2016 22:43:25 +0200 Subject: [PATCH 045/170] FIX: searching received emails for TO was broken --- app/controllers/admin/email_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/admin/email_controller.rb b/app/controllers/admin/email_controller.rb index 832efd85e4..287b0ffdac 100644 --- a/app/controllers/admin/email_controller.rb +++ b/app/controllers/admin/email_controller.rb @@ -94,7 +94,7 @@ class Admin::EmailController < Admin::AdminController .limit(50) incoming_emails = incoming_emails.where("from_address ILIKE ?", "%#{params[:from]}%") if params[:from].present? - incoming_emails = incoming_emails.where("to_addresses ILIKE ? OR cc_addresses ILIKE ?", "%#{params[:to]}%") if params[:to].present? + incoming_emails = incoming_emails.where("to_addresses ILIKE :to OR cc_addresses ILIKE :to", to: "%#{params[:to]}%") if params[:to].present? incoming_emails = incoming_emails.where("subject ILIKE ?", "%#{params[:subject]}%") if params[:subject].present? incoming_emails = incoming_emails.where("error ILIKE ?", "%#{params[:error]}%") if params[:error].present? From 2e71e6fc6f72442a6b93200debfaa7f9c18be9bc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 13 Jul 2016 17:07:20 -0400 Subject: [PATCH 046/170] Try warming up pretty text before we fork --- config/unicorn.conf.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/unicorn.conf.rb b/config/unicorn.conf.rb index 1e3752218d..dbc741a2dc 100644 --- a/config/unicorn.conf.rb +++ b/config/unicorn.conf.rb @@ -48,6 +48,8 @@ before_fork do |server, worker| table.classify.constantize.first rescue nil end + PrettyText.cook "

initialize pretty text

" + # router warm up Rails.application.routes.recognize_path('abc') rescue nil From f15290096923ff1b2fdc9eb48937107b85cdb144 Mon Sep 17 00:00:00 2001 From: Loic Dachary Date: Thu, 14 Jul 2016 09:10:31 +0200 Subject: [PATCH 047/170] FIX: poll builder should ignore empty lines Although pollOptionsCount skips empty lines, pollOutput inserts empty lines. Skip them instead. Signed-off-by: Loic Dachary --- .../assets/javascripts/controllers/poll-ui-builder.js.es6 | 4 +++- .../test/javascripts/controllers/poll-ui-builder-test.js.es6 | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 index 2a1bd3b864..2f96ef3d2c 100644 --- a/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 +++ b/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 @@ -111,7 +111,9 @@ export default Ember.Controller.extend({ output += `${pollHeader}\n`; if (pollOptions.length > 0 && !isNumber) { - output += `${pollOptions.split("\n").map(option => `* ${option}`).join("\n")}\n`; + pollOptions.split("\n").forEach(option => { + if (option.length !== 0) output += `* ${option}\n`; + }); } output += '[/poll]'; diff --git a/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 b/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 index 02c4ad8d03..fe263538c5 100644 --- a/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 +++ b/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 @@ -210,7 +210,7 @@ test("multiple pollOutput", function() { isMultiple: true, pollType: controller.get("multiplePollType"), pollMin: 1, - pollOptions: "1\n2" + pollOptions: "\n\n1\n\n2" }); equal(controller.get("pollOutput"), "[poll type=multiple min=1 max=2]\n* 1\n* 2\n[/poll]", "it should return the right output"); From 1386f9c8c9f32e1294f80b7f4b81eaf3996de9dd Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 14 Jul 2016 03:40:50 -0700 Subject: [PATCH 048/170] make the activate account button a btn-primary --- app/views/users/activate_account.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/users/activate_account.html.erb b/app/views/users/activate_account.html.erb index 68688245f6..6723d66ed5 100644 --- a/app/views/users/activate_account.html.erb +++ b/app/views/users/activate_account.html.erb @@ -1,7 +1,7 @@

<%= t 'activation.welcome_to', site_name: SiteSetting.title %>


- + <%= form_tag(perform_activate_account_path, method: :put, id: 'activate-account-form') do %> <%= hidden_field_tag 'password_confirmation' %> From 3dcd6edb46ac7f814e73b89083186a7d79823ddd Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 14 Jul 2016 22:20:43 +1000 Subject: [PATCH 049/170] FEATURE: stage post in stream on edit --- app/assets/javascripts/discourse/lib/transform-post.js.es6 | 1 + app/assets/javascripts/discourse/models/composer.js.es6 | 4 +++- app/assets/javascripts/discourse/widgets/post.js.es6 | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index 03ff394ec3..18da1e5993 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -113,6 +113,7 @@ export default function transformPost(currentUser, site, post, prevPost, nextPos postAtts.actionCodeWho = post.action_code_who; postAtts.userCustomFields = post.user_custom_fields; postAtts.topicUrl = topic.get('url'); + postAtts.isSaving = post.isSaving; const showPMMap = topic.archetype === 'private_message' && post.post_number === 1; if (showPMMap) { diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 837d97d265..9ecc0beefe 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -539,7 +539,7 @@ const Composer = RestModel.extend({ cooked: this.getCookedHtml() }; - this.set('composeState', CLOSED); + this.set('composeState', SAVING); var rollback = throwAjaxError(function(){ post.set('cooked', oldCooked); @@ -547,6 +547,8 @@ const Composer = RestModel.extend({ }); return promise.then(function() { + // rest model only sets props after it is saved + post.set("cooked", props.cooked); return post.save(props).then(function(result) { self.clearState(); return result; diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index d48e054a73..a1e86d3bf1 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -401,7 +401,7 @@ export default createWidget('post', { if (attrs.cloaked) { return 'cloaked-post'; } const classNames = ['topic-post', 'clearfix']; - if (attrs.id === -1) { classNames.push('staged'); } + if (attrs.id === -1 || attrs.isSaving) { classNames.push('staged'); } if (attrs.selected) { classNames.push('selected'); } if (attrs.topicOwner) { classNames.push('topic-owner'); } if (attrs.hidden) { classNames.push('post-hidden'); } From fa8ba3b4080efe48f0b3b3d69292e3237d1e2822 Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 14 Jul 2016 22:30:51 +1000 Subject: [PATCH 050/170] UX: don't expand pinned on mobile categories page --- .../templates/mobile/discovery/categories.hbs | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs index b6b384eaf8..3250bbdaa2 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs @@ -25,17 +25,6 @@ {{/if}} {{{format-age t.last_posted_at}}} - {{#if t.hasExcerpt}} -
- {{{t.excerpt}}} - {{#if t.excerptTruncated}} - {{#unless t.canClearPin}}{{i18n 'read_more'}}{{/unless}} - {{/if}} - {{#if t.canClearPin}} - {{i18n 'topic.clear_pin.title'}} - {{/if}} -
- {{/if}}
{{raw "list/post-count-or-badges" topic=t postBadgesEnabled="true"}} From bea06afd3d26b385e18d55092026c10afb950cfd Mon Sep 17 00:00:00 2001 From: Sam Saffron Date: Thu, 14 Jul 2016 22:38:16 +1000 Subject: [PATCH 051/170] UX: suppress description excerpt on mobile --- app/assets/stylesheets/mobile/topic-list.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 5ca8cb7b5a..660446f6f5 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -419,3 +419,7 @@ td .main-link { width: 120%; } } + +.category-list-item .category-description { + display: none; +} From 529528f122495dba88f14ecbab959ad1660cbb15 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 14 Jul 2016 18:56:54 +0530 Subject: [PATCH 052/170] add Drupal JSON import script --- script/import_scripts/drupal_json.rb | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 script/import_scripts/drupal_json.rb diff --git a/script/import_scripts/drupal_json.rb b/script/import_scripts/drupal_json.rb new file mode 100644 index 0000000000..ac40525caf --- /dev/null +++ b/script/import_scripts/drupal_json.rb @@ -0,0 +1,45 @@ +require File.expand_path(File.dirname(__FILE__) + "/base.rb") + +# Edit the constants and initialize method for your import data. + +class ImportScripts::DrupalJson < ImportScripts::Base + + JSON_FILES_DIR = "/Users/techapj/Documents" + + def initialize + super + @users_json = load_json("formatted_users.json") + end + + def execute + puts "", "Importing from Drupal..." + + import_users + + puts "", "Done" + end + + def load_json(arg) + filename = File.join(JSON_FILES_DIR, arg) + raise RuntimeError.new("File #{filename} not found!") if !File.exists?(filename) + JSON.parse(File.read(filename)).reverse + end + + def import_users + puts '', "Importing users" + + create_users(@users_json) do |u| + { + id: u["uid"], + name: u["name"], + email: u["mail"], + created_at: Time.zone.at(u["created"].to_i) + } + end + EmailToken.delete_all + end +end + +if __FILE__==$0 + ImportScripts::DrupalJson.new.perform +end From 1c4bc154c9a4953a2c3df6ac9c2414dea4639c3e Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 14 Jul 2016 19:07:25 +0530 Subject: [PATCH 053/170] add SimplePress import script --- script/import_scripts/simplepress.rb | 192 +++++++++++++++++++++++++++ 1 file changed, 192 insertions(+) create mode 100644 script/import_scripts/simplepress.rb diff --git a/script/import_scripts/simplepress.rb b/script/import_scripts/simplepress.rb new file mode 100644 index 0000000000..2f4e20ab75 --- /dev/null +++ b/script/import_scripts/simplepress.rb @@ -0,0 +1,192 @@ +require 'mysql2' +require File.expand_path(File.dirname(__FILE__) + "/base.rb") + +class ImportScripts::SimplePress < ImportScripts::Base + + SIMPLE_PRESS_DB ||= ENV['SIMPLEPRESS_DB'] || "simplepress" + TABLE_PREFIX = "wp_sf" + BATCH_SIZE ||= 1000 + + def initialize + super + + @client = Mysql2::Client.new( + host: "localhost", + username: "root", + database: SIMPLE_PRESS_DB, + ) + + SiteSetting.max_username_length = 50 + end + + def execute + import_users + import_categories + import_topics_and_posts + end + + def import_users + puts "", "importing users..." + + last_user_id = -1 + total_users = mysql_query("SELECT COUNT(*) count FROM wp_users WHERE user_email LIKE '%@%'").first["count"] + + batches(BATCH_SIZE) do |offset| + users = mysql_query(<<-SQL + SELECT ID id, user_nicename, display_name, user_email, user_registered, user_url + FROM wp_users + WHERE user_email LIKE '%@%' + AND id > #{last_user_id} + ORDER BY id + LIMIT #{BATCH_SIZE} + SQL + ).to_a + + break if users.empty? + + last_user_id = users[-1]["id"] + user_ids = users.map { |u| u["id"].to_i } + + next if all_records_exist?(:users, user_ids) + + user_ids_sql = user_ids.join(",") + + users_description = {} + mysql_query(<<-SQL + SELECT user_id, meta_value description + FROM wp_usermeta + WHERE user_id IN (#{user_ids_sql}) + AND meta_key = 'description' + SQL + ).each { |um| users_description[um["user_id"]] = um["description"] } + + create_users(users, total: total_users, offset: offset) do |u| + { + id: u["id"].to_i, + username: u["user_nicename"], + email: u["user_email"].downcase, + name: u["display_name"], + created_at: u["user_registered"], + website: u["user_url"], + bio_raw: users_description[u["id"]] + } + end + end + end + + def import_categories + puts "", "importing categories..." + + categories = mysql_query(<<-SQL + SELECT forum_id, forum_name, forum_seq, forum_desc, parent + FROM #{TABLE_PREFIX}forums + ORDER BY forum_id + SQL + ) + + create_categories(categories) do |c| + category = { id: c['forum_id'], name: CGI.unescapeHTML(c['forum_name']), description: CGI.unescapeHTML(c['forum_desc']), position: c['forum_seq'] } + if (parent_id = c['parent'].to_i) > 0 + category[:parent_category_id] = category_id_from_imported_category_id(parent_id) + end + category + end + end + + def import_topics_and_posts + puts "", "creating topics and posts" + + total_count = mysql_query("SELECT count(*) count from #{TABLE_PREFIX}posts").first["count"] + + topic_first_post_id = {} + + batches(BATCH_SIZE) do |offset| + results = mysql_query(" + SELECT p.post_id id, + p.topic_id topic_id, + t.forum_id category_id, + t.topic_name title, + p.post_index post_index, + p.user_id user_id, + p.post_content raw, + p.post_date post_time + FROM #{TABLE_PREFIX}posts p, + #{TABLE_PREFIX}topics t + WHERE p.topic_id = t.topic_id + ORDER BY p.post_date + LIMIT #{BATCH_SIZE} + OFFSET #{offset}; + ") + + break if results.size < 1 + + next if all_records_exist? :posts, results.map {|m| m['id'].to_i} + + create_posts(results, total: total_count, offset: offset) do |m| + skip = false + mapped = {} + + mapped[:id] = m['id'] + mapped[:user_id] = user_id_from_imported_user_id(m['user_id']) || -1 + mapped[:raw] = process_simplepress_post(m['raw'], m['id']) + mapped[:created_at] = Time.zone.at(m['post_time']) + + if m['post_index'] == 1 + mapped[:category] = category_id_from_imported_category_id(m['category_id']) + mapped[:title] = CGI.unescapeHTML(m['title']) + topic_first_post_id[m['topic_id']] = m['id'] + else + parent = topic_lookup_from_imported_post_id(topic_first_post_id[m['topic_id']]) + if parent + mapped[:topic_id] = parent[:topic_id] + else + puts "Parent post #{first_post_id} doesn't exist. Skipping #{m["id"]}: #{m["title"][0..40]}" + skip = true + end + end + + skip ? nil : mapped + end + end + end + + def process_simplepress_post(raw, import_id) + s = raw.dup + + # convert the quote line + s.gsub!(/\[quote='([^']+)'.*?pid='(\d+).*?\]/) { + "[quote=\"#{convert_username($1, import_id)}, " + post_id_to_post_num_and_topic($2, import_id) + '"]' + } + + # :) is encoded as :) + s.gsub!(/(?:.*)/, '\1') + + # Some links look like this: http://www.onegameamonth.com + s.gsub!(/(.+)<\/a>/, '[\2](\1)') + + # Many phpbb bbcode tags have a hash attached to them. Examples: + # [url=https://google.com:1qh1i7ky]click here[/url:1qh1i7ky] + # [quote="cybereality":b0wtlzex]Some text.[/quote:b0wtlzex] + s.gsub!(/:(?:\w{8})\]/, ']') + + # Remove mybb video tags. + s.gsub!(/(^\[video=.*?\])|(\[\/video\]$)/, '') + + s = CGI.unescapeHTML(s) + + # phpBB shortens link text like this, which breaks our markdown processing: + # [http://answers.yahoo.com/question/index ... 223AAkkPli](http://answers.yahoo.com/question/index?qid=20070920134223AAkkPli) + # + # Work around it for now: + s.gsub!(/\[http(s)?:\/\/(www\.)?/, '[') + + s + end + + def mysql_query(sql) + @client.query(sql, cache_rows: false) + end + +end + +ImportScripts::SimplePress.new.perform From e5bbfe1f1bcabc4e5e4daf53379df1c46c4899b8 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 14 Jul 2016 10:22:12 -0400 Subject: [PATCH 054/170] Lint inner function declarations to prevent Safari breakage --- .eslintrc | 1 + .../javascripts/discourse/views/reorder-categories.js.es6 | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintrc b/.eslintrc index b529fa2068..4148a259ba 100644 --- a/.eslintrc +++ b/.eslintrc @@ -76,6 +76,7 @@ "no-eval": 2, "no-extend-native": 2, "no-extra-parens": 0, + "no-inner-declarations": 2, "no-irregular-whitespace": 2, "no-iterator": 2, "no-loop-func": 2, diff --git a/app/assets/javascripts/discourse/views/reorder-categories.js.es6 b/app/assets/javascripts/discourse/views/reorder-categories.js.es6 index 570c9ec963..86e0093cad 100644 --- a/app/assets/javascripts/discourse/views/reorder-categories.js.es6 +++ b/app/assets/javascripts/discourse/views/reorder-categories.js.es6 @@ -54,7 +54,7 @@ export default ModalBodyView.extend({ const startTime = performance.now(); const duration = 100; - function doScroll(timestamp) { + const doScroll = function(timestamp) { let progress = (timestamp - startTime) / duration; if (progress > 1) { progress = 1; @@ -73,7 +73,7 @@ export default ModalBodyView.extend({ const iprogress = 1 - progress; scrollParent.scrollTop(goal * progress + current * iprogress); - } + }; window.requestAnimationFrame(doScroll); } } From f8a12d494090e288929df395455ed382b279d0b5 Mon Sep 17 00:00:00 2001 From: Hu Ming Date: Thu, 14 Jul 2016 22:56:09 +0800 Subject: [PATCH 055/170] Add support for AWS cn (#4327) --- app/models/s3_region_site_setting.rb | 3 ++- config/locales/client.en.yml | 1 + config/locales/client.zh_CN.yml | 1 + lib/file_store/s3_store.rb | 2 ++ spec/components/file_store/s3_store_spec.rb | 4 ++++ spec/models/s3_region_site_setting_spec.rb | 2 +- 6 files changed, 11 insertions(+), 2 deletions(-) diff --git a/app/models/s3_region_site_setting.rb b/app/models/s3_region_site_setting.rb index 24710f89af..06bbe705e0 100644 --- a/app/models/s3_region_site_setting.rb +++ b/app/models/s3_region_site_setting.rb @@ -20,7 +20,8 @@ class S3RegionSiteSetting < EnumSiteSetting 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2', - 'sa-east-1' + 'sa-east-1', + 'cn-north-1' ] end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index b6e1d6dd04..89663506b1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -186,6 +186,7 @@ en: ap_northeast_1: "Asia Pacific (Tokyo)" ap_northeast_2: "Asia Pacific (Seoul)" sa_east_1: "South America (Sao Paulo)" + cn_north_1: "China (Beijing)" edit: 'edit the title and category of this topic' not_implemented: "That feature hasn't been implemented yet, sorry!" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 26cc84e919..bc8c110c81 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -132,6 +132,7 @@ zh_CN: ap_northeast_1: "亚洲太平洋(Tokyo)" ap_northeast_2: "亚洲太平洋(Seoul)" sa_east_1: "南美(Sao Paulo)" + cn_north_1: "中国 (北京)" edit: '编辑本主题的标题和分类' not_implemented: "非常抱歉,此功能暂时尚未实现!" no_value: "否" diff --git a/lib/file_store/s3_store.rb b/lib/file_store/s3_store.rb index 8b7bb1ae3e..0a50ce4459 100644 --- a/lib/file_store/s3_store.rb +++ b/lib/file_store/s3_store.rb @@ -62,6 +62,8 @@ module FileStore # cf. http://docs.aws.amazon.com/general/latest/gr/rande.html#s3_region @absolute_base_url ||= if SiteSetting.s3_region == "us-east-1" "//#{s3_bucket}.s3.amazonaws.com" + elsif SiteSetting.s3_region == 'cn-north-1' + "//#{s3_bucket}.s3.cn-north-1.amazonaws.com.cn" else "//#{s3_bucket}.s3-#{SiteSetting.s3_region}.amazonaws.com" end diff --git a/spec/components/file_store/s3_store_spec.rb b/spec/components/file_store/s3_store_spec.rb index 8b504a72b5..2976a0cbe0 100644 --- a/spec/components/file_store/s3_store_spec.rb +++ b/spec/components/file_store/s3_store_spec.rb @@ -81,6 +81,10 @@ describe FileStore::S3Store do SiteSetting.stubs(:s3_region).returns("us-east-2") expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq("//s3_upload_bucket.s3-us-east-2.amazonaws.com") + + SiteSetting.stubs(:s3_region).returns("cn-north-1") + expect(FileStore::S3Store.new(s3_helper).absolute_base_url).to eq("//s3_upload_bucket.s3.cn-north-1.amazonaws.com.cn") + end end diff --git a/spec/models/s3_region_site_setting_spec.rb b/spec/models/s3_region_site_setting_spec.rb index ffba50b7f7..d2d5dccffc 100644 --- a/spec/models/s3_region_site_setting_spec.rb +++ b/spec/models/s3_region_site_setting_spec.rb @@ -14,7 +14,7 @@ describe S3RegionSiteSetting do describe 'values' do it 'returns all the S3 regions' do - expect(S3RegionSiteSetting.values.map {|x| x[:value]}.sort).to eq(['us-east-1', 'us-west-1', 'us-west-2', 'us-gov-west-1', 'eu-west-1', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2', 'sa-east-1'].sort) + expect(S3RegionSiteSetting.values.map {|x| x[:value]}.sort).to eq(['us-east-1', 'us-west-1', 'us-west-2', 'us-gov-west-1', 'eu-west-1', 'eu-central-1', 'ap-southeast-1', 'ap-southeast-2', 'ap-northeast-1', 'ap-northeast-2', 'sa-east-1', 'cn-north-1'].sort) end end From ba637e40b6ed4e3175a86a27d27e01f0bd08a3c1 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Thu, 14 Jul 2016 13:52:37 -0400 Subject: [PATCH 056/170] FEATURE: Google Tag Manager Universal Analytics support --- .../discourse/initializers/page-tracking.js.es6 | 13 +++++++++++++ app/helpers/application_helper.rb | 12 ++++++++++-- app/helpers/common_helper.rb | 6 ++++++ app/views/common/_google_tag_manager.html.erb | 11 +++++++++++ app/views/layouts/application.html.erb | 2 ++ config/locales/server.en.yml | 2 ++ config/site_settings.yml | 7 +++++++ 7 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 app/views/common/_google_tag_manager.html.erb diff --git a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 index 7f680580ea..860ddaf0f8 100644 --- a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 +++ b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 @@ -50,5 +50,18 @@ export default { window.ga('send', 'pageview', {page: url, title: title}); }); } + + // And Google Tag Manager too + if (typeof window.dataLayer !== 'undefined') { + onPageChange((url, title) => { + window.dataLayer.push({ + 'event': 'virtualPageView', + 'page': { + 'title': title, + 'url': url + } + }); + }); + } } }; diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 06d782214f..c8321c795c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -15,8 +15,8 @@ module ApplicationHelper include ConfigurableUrls include GlobalPath - def ga_universal_json - cookie_domain = SiteSetting.ga_universal_domain_name.gsub(/^http(s)?:\/\//, '') + def google_universal_analytics_json(ua_domain_name) + cookie_domain = ua_domain_name.gsub(/^http(s)?:\/\//, '') result = {cookieDomain: cookie_domain} if current_user.present? result[:userId] = current_user.id @@ -24,6 +24,14 @@ module ApplicationHelper result.to_json.html_safe end + def ga_universal_json + google_universal_analytics_json(SiteSetting.ga_universal_domain_name) + end + + def google_tag_manager_json + google_universal_analytics_json(SiteSetting.gtm_ua_domain_name) + end + def shared_session_key if SiteSetting.long_polling_base_url != '/'.freeze && current_user sk = "shared_session_key" diff --git a/app/helpers/common_helper.rb b/app/helpers/common_helper.rb index 10992b1ae6..e610736903 100644 --- a/app/helpers/common_helper.rb +++ b/app/helpers/common_helper.rb @@ -10,4 +10,10 @@ module CommonHelper render partial: "common/google_analytics" end end + + def render_google_tag_manager_code + if Rails.env.production? && SiteSetting.gtm_container_id.present? + render partial: "common/google_tag_manager" + end + end end diff --git a/app/views/common/_google_tag_manager.html.erb b/app/views/common/_google_tag_manager.html.erb new file mode 100644 index 0000000000..dca75352a0 --- /dev/null +++ b/app/views/common/_google_tag_manager.html.erb @@ -0,0 +1,11 @@ + + + + diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 3b8b4ccc61..af5e7df438 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -44,6 +44,8 @@ + <%= render_google_tag_manager_code %> +
-

Blockquotes

+

Blockquotes

Markdown uses email-style > characters for blockquoting. If you're familiar with quoting passages of text in an email message, then you @@ -296,7 +296,7 @@ and code blocks:

example, with BBEdit, you can make a selection and choose Increase Quote Level from the Text menu.

-

Lists

+

Lists

Markdown supports ordered (numbered) and unordered (bulleted) lists.

@@ -472,7 +472,7 @@ line. To avoid this, you can backslash-escape the period:

1986\. What a great season.
 
-

Code Blocks

+

Code Blocks

Pre-formatted code blocks are used for writing about programming or markup source code. Rather than forming normal paragraphs, the lines @@ -541,7 +541,7 @@ ampersands and angle brackets. For example, this:

asterisks are just literal asterisks within a code block. This means it's also easy to use Markdown to write about Markdown's own syntax.

-

Horizontal Rules

+

Horizontal Rules

You can produce a horizontal rule tag (<hr />) by placing three or more hyphens, asterisks, or underscores on a line by themselves. If you @@ -563,9 +563,9 @@ _ _ _


-

Span Elements

+

Span Elements

-

Links

+

Markdown supports two style of links: inline and reference.

@@ -727,7 +727,7 @@ allowing you to move the markup-related metadata out of the paragraph, you can add links without interrupting the narrative flow of your prose.

-

Emphasis

+

Emphasis

Markdown treats asterisks (*) and underscores (_) as indicators of emphasis. Text wrapped with one * or _ will be wrapped with an @@ -772,7 +772,7 @@ escape it:

\*this text is surrounded by literal asterisks\*
 
-

Code

+

Code

To indicate a span of code, wrap it with backtick quotes (`). Unlike a pre-formatted code block, a code span indicates code within a @@ -836,7 +836,7 @@ tags. Markdown will turn this:

equivalent of <code>&amp;mdash;</code>.</p> -

Images

+

Images

Admittedly, it's fairly difficult to devise a "natural" syntax for placing images into a plain text document format.

@@ -879,9 +879,9 @@ use regular HTML <img> tags.


-

Miscellaneous

+

Miscellaneous

-

Automatic Links

+

Markdown supports a shortcut style for creating "automatic" links for URLs and email addresses: simply surround the URL or email address with angle brackets. What this means is that if you want to show the actual text of a URL or email address, and also have it be a clickable link, you can do this:

@@ -916,7 +916,7 @@ most, address-harvesting bots, but it definitely won't fool all of them. It's better than nothing, but an address published in this way will probably eventually start receiving spam.)

-

Backslash Escapes

+

Backslash Escapes

Markdown allows you to use backslash escapes to generate literal characters which would otherwise have special meaning in Markdown's From f5c8d05f1ee69fc7dd40f40e7e54191691e3219a Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 20 Jul 2016 09:30:08 +0800 Subject: [PATCH 113/170] Should be `api_username`. --- .../discourse-nginx-performance-report/script/nginx_analyze.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/discourse-nginx-performance-report/script/nginx_analyze.rb b/plugins/discourse-nginx-performance-report/script/nginx_analyze.rb index a21edf3264..262ad047ea 100644 --- a/plugins/discourse-nginx-performance-report/script/nginx_analyze.rb +++ b/plugins/discourse-nginx-performance-report/script/nginx_analyze.rb @@ -49,7 +49,7 @@ class LogAnalyzer private def self.sanitize_url(url) - url.gsub(/(api_key|api_user)=(\w+)/, '\1=[FILTERED]') + url.gsub(/(api_key|api_username)=(\w+)/, '\1=[FILTERED]') end end From 1b986f2266ba4a6d35f9b7ee4a688e8ad9080d03 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 20 Jul 2016 14:10:42 +0800 Subject: [PATCH 114/170] Fix the build. --- plugins/discourse-details/spec/components/pretty_text_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/discourse-details/spec/components/pretty_text_spec.rb b/plugins/discourse-details/spec/components/pretty_text_spec.rb index b9b91ea771..5c2ad81e10 100644 --- a/plugins/discourse-details/spec/components/pretty_text_spec.rb +++ b/plugins/discourse-details/spec/components/pretty_text_spec.rb @@ -4,7 +4,7 @@ require 'pretty_text' describe PrettyText do it "supports details tag" do - cooked_html = "

foo\n\n

bar

\n\n

" + cooked_html = "
foo\n\n

bar

\n\n
" expect(PrettyText.cook("
foobar
")).to match_html(cooked_html) expect(PrettyText.cook("[details=foo]bar[/details]")).to match_html(cooked_html) end From fe080f5c571f4ff222d83f2d4db2828291057f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 20 Jul 2016 15:59:25 +0200 Subject: [PATCH 115/170] FIX: allows plugin-outlets to use to block syntax --- .../discourse/helpers/plugin-outlet.js.es6 | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 index cd7363d9fb..3b47ac8cf4 100644 --- a/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 +++ b/app/assets/javascripts/discourse/helpers/plugin-outlet.js.es6 @@ -160,7 +160,9 @@ registerHelper('plugin-outlet', function(params, hash, options, env) { const newHash = $.extend({}, viewInjections(env.data.view.container)); if (hash.tagName) { newHash.tagName = hash.tagName; } - delete options.fn; // we don't need the default template since we have a connector + // we don't need the default template since we have a connector + delete options.fn; + delete options.template; env.helpers.view.helperFunction.call(this, [viewClass], newHash, options, env); const cvs = env.data.view._childViews; @@ -172,6 +174,13 @@ registerHelper('plugin-outlet', function(params, hash, options, env) { }); } } + } else if (options.isBlock) { + const virtualView = Ember.View.extend({ + isVirtual: true, + tagName: hash.tagName || '', + template: options.template + }); + env.helpers.view.helperFunction.call(this, [virtualView], hash, options, env); } }); From af53d37e4710f994a9e27120bd4bb87bf24811a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 20 Jul 2016 16:00:30 +0200 Subject: [PATCH 116/170] FEATURE: add plugin-outlets from groups & categories incoming emails --- .../admin/routes/admin-group.js.es6 | 15 ++++------ .../javascripts/admin/templates/group.hbs | 4 +-- .../javascripts/discourse/models/group.js.es6 | 2 +- .../components/edit-category-settings.hbs | 28 ++++++++++--------- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/app/assets/javascripts/admin/routes/admin-group.js.es6 b/app/assets/javascripts/admin/routes/admin-group.js.es6 index 1555c4200a..298bffaa43 100644 --- a/app/assets/javascripts/admin/routes/admin-group.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-group.js.es6 @@ -2,26 +2,23 @@ import Group from 'discourse/models/group'; export default Discourse.Route.extend({ - model: function(params) { - var groups = this.modelFor('adminGroupsType'); + model(params) { if (params.name === 'new') { - return Group.create({ - automatic: false, - visible: true - }); + return Group.create({ automatic: false, visible: true }); } - var group = groups.findProperty('name', params.name); + const group = this.modelFor('adminGroupsType') + .findProperty('name', params.name); if (!group) { return this.transitionTo('adminGroups.index'); } return group; }, - setupController: function(controller, model) { + setupController(controller, model) { controller.set("model", model); controller.set("model.usernames", null); - controller.set("savingStatus", ''); + controller.set("savingStatus", ""); model.findMembers(); } diff --git a/app/assets/javascripts/admin/templates/group.hbs b/app/assets/javascripts/admin/templates/group.hbs index 49db722d6f..2c2a1929a3 100644 --- a/app/assets/javascripts/admin/templates/group.hbs +++ b/app/assets/javascripts/admin/templates/group.hbs @@ -94,10 +94,10 @@ {{#if siteSettings.email_in}} -
+ {{#plugin-outlet "group-email-in"}} {{text-field name="incoming_email" value=model.incoming_email placeholderKey="admin.groups.incoming_email_placeholder"}} -
+ {{/plugin-outlet}} {{/if}} {{/unless}} diff --git a/app/assets/javascripts/discourse/models/group.js.es6 b/app/assets/javascripts/discourse/models/group.js.es6 index 85c8ba5a5b..b6890e8263 100644 --- a/app/assets/javascripts/discourse/models/group.js.es6 +++ b/app/assets/javascripts/discourse/models/group.js.es6 @@ -1,5 +1,5 @@ import { ajax } from 'discourse/lib/ajax'; -import computed from 'ember-addons/ember-computed-decorators'; +import computed from "ember-addons/ember-computed-decorators"; const Group = Discourse.Model.extend({ limit: 50, diff --git a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs index 01aeb2d15b..03b2bc4dee 100644 --- a/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs +++ b/app/assets/javascripts/discourse/templates/components/edit-category-settings.hbs @@ -20,20 +20,22 @@ {{#if emailInEnabled}} -
- -
+ {{#plugin-outlet "category-email-in"}} +
+ +
-
- -
+
+ +
+ {{/plugin-outlet}} {{/if}} {{#if showPositionInput}} From e7e6840803cb4c673cb21d9ae0e6833d545b6b32 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 10:42:18 -0400 Subject: [PATCH 117/170] FIX: I shouldn't commit while tired :) --- app/assets/javascripts/discourse/lib/url.js.es6 | 1 - test/javascripts/lib/sanitizer-test.js.es6 | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 495b2856b6..dbe9874958 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -31,7 +31,6 @@ const DiscourseURL = Ember.Object.extend({ if (opts.anchor) { elementId = opts.anchor; holder = $(elementId); - console.log(holder.length); } if (!holder || holder.length === 0) { diff --git a/test/javascripts/lib/sanitizer-test.js.es6 b/test/javascripts/lib/sanitizer-test.js.es6 index 49581fadc2..6e83c4d373 100644 --- a/test/javascripts/lib/sanitizer-test.js.es6 +++ b/test/javascripts/lib/sanitizer-test.js.es6 @@ -53,12 +53,12 @@ test("sanitize", function() { test("ids on headings", () => { const pt = new PrettyText(buildOptions({ siteSettings: {} })); equal(pt.sanitize("

Test Heading

"), "

Test Heading

"); - equal(pt.sanitize(`

Test Heading

`), `

Test Heading

`); - equal(pt.sanitize(`

Test Heading

`), `

Test Heading

`); + equal(pt.sanitize(`

Test Heading

`), `

Test Heading

`); + equal(pt.sanitize(`

Test Heading

`), `

Test Heading

`); equal(pt.sanitize(`

Test Heading

`), `

Test Heading

`); - equal(pt.sanitize(`

Test Heading

`), `

Test Heading

`); - equal(pt.sanitize(`
Test Heading
`), `
Test Heading
`); - equal(pt.sanitize(`
Test Heading
`), `
Test Heading
`); + equal(pt.sanitize(`

Test Heading

`), `

Test Heading

`); + equal(pt.sanitize(`
Test Heading
`), `
Test Heading
`); + equal(pt.sanitize(`
Test Heading
`), `
Test Heading
`); }); test("urlAllowed", () => { From 7dd44700755efcf4f5ebb274e40a860fe96a7c5c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 12:39:12 -0400 Subject: [PATCH 118/170] FIX: Allow `div align` --- app/assets/javascripts/pretty-text/white-lister.js.es6 | 1 + test/javascripts/lib/sanitizer-test.js.es6 | 2 ++ 2 files changed, 3 insertions(+) diff --git a/app/assets/javascripts/pretty-text/white-lister.js.es6 b/app/assets/javascripts/pretty-text/white-lister.js.es6 index 63bc3b742a..1dc3885a6a 100644 --- a/app/assets/javascripts/pretty-text/white-lister.js.es6 +++ b/app/assets/javascripts/pretty-text/white-lister.js.es6 @@ -107,6 +107,7 @@ whiteListFeature('default', [ 'div', 'div.title', 'div.quote-controls', + 'div[align]', 'i', 'b', 'ul', diff --git a/test/javascripts/lib/sanitizer-test.js.es6 b/test/javascripts/lib/sanitizer-test.js.es6 index 6e83c4d373..9e74166d58 100644 --- a/test/javascripts/lib/sanitizer-test.js.es6 +++ b/test/javascripts/lib/sanitizer-test.js.es6 @@ -48,6 +48,8 @@ test("sanitize", function() { cooked("Ctrl+C", "

Ctrl+C

"); cooked("it has been 1 day 0 days since our last test failure", "

it has been 1 day 0 days since our last test failure

"); + + cooked(`
hello
`, `
hello
`); }); test("ids on headings", () => { From e341596536622a4cdc10767d1f32617ad2d93758 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 13:26:23 -0400 Subject: [PATCH 119/170] FIX: Suppory open `details` elements --- .../assets/javascripts/lib/discourse-markdown/details.js.es6 | 1 + .../test/javascripts/lib/details-cooked-test.js.es6 | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 b/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 index af0cfca5db..26e9efd5c7 100644 --- a/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 +++ b/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 @@ -25,6 +25,7 @@ export function setup(helper) { 'summary', 'summary[title]', 'details', + 'details[open=open]', 'details.elided' ]); diff --git a/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 b/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 index a7ef8ad671..39d4688242 100644 --- a/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 +++ b/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 @@ -24,5 +24,8 @@ test("details", () => { cooked(`
Infocoucou
`, `
Info\n\n

coucou

\n\n
`, "manual HTML for details with a space"); -}); + cooked(`
Infocoucou
`, + `
Info\n\n

coucou

\n\n
`, + "open attribute"); +}); From 16383a1749edc5a6d449884b0620b6b1082e0b64 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 13:30:36 -0400 Subject: [PATCH 120/170] FIX: Also support just `open` --- app/assets/javascripts/pretty-text/sanitizer.js.es6 | 6 +++++- .../javascripts/lib/discourse-markdown/details.js.es6 | 2 +- .../test/javascripts/lib/details-cooked-test.js.es6 | 4 ++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/pretty-text/sanitizer.js.es6 b/app/assets/javascripts/pretty-text/sanitizer.js.es6 index 9881eceef5..a643169c39 100644 --- a/app/assets/javascripts/pretty-text/sanitizer.js.es6 +++ b/app/assets/javascripts/pretty-text/sanitizer.js.es6 @@ -3,7 +3,11 @@ import xss from 'pretty-text/xss'; const _validIframes = []; function attr(name, value) { - return `${name}="${xss.escapeAttrValue(value)}"`; + if (value) { + return `${name}="${xss.escapeAttrValue(value)}"`; + } + + return name; } const ESCAPE_REPLACEMENTS = { diff --git a/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 b/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 index 26e9efd5c7..b710abf7e6 100644 --- a/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 +++ b/plugins/discourse-details/assets/javascripts/lib/discourse-markdown/details.js.es6 @@ -25,7 +25,7 @@ export function setup(helper) { 'summary', 'summary[title]', 'details', - 'details[open=open]', + 'details[open]', 'details.elided' ]); diff --git a/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 b/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 index 39d4688242..f020474e6c 100644 --- a/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 +++ b/plugins/discourse-details/test/javascripts/lib/details-cooked-test.js.es6 @@ -28,4 +28,8 @@ test("details", () => { cooked(`
Infocoucou
`, `
Info\n\n

coucou

\n\n
`, "open attribute"); + + cooked(`
Infocoucou
`, + `
Info\n\n

coucou

\n\n
`, + "open attribute"); }); From b9177af1eb21d6132e131388f55115252e296620 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 13:44:12 -0400 Subject: [PATCH 121/170] FIX: Protocol-less links that begin with `a` shouldn't error --- .../pretty-text/engines/discourse-markdown/bbcode.js.es6 | 8 +++++--- test/javascripts/lib/pretty-text-test.js.es6 | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 index d50880c293..bc15c40fb2 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/bbcode.js.es6 @@ -114,10 +114,12 @@ export function setup(helper) { replaceBBCode('url', contents => { if (!Array.isArray(contents)) { return; } - if (contents.length === 1 && contents[0][0] === 'a') { + + const first = contents[0]; + if (contents.length === 1 && Array.isArray(first) && first[0] === 'a') { // single-line bbcode links shouldn't be oneboxed, so we mark this as a bbcode link. - if (typeof contents[0][1] !== 'object') { contents[0].splice(1, 0, {}); } - contents[0][1]['data-bbcode'] = true; + if (typeof first[1] !== 'object') { first.splice(1, 0, {}); } + first[1]['data-bbcode'] = true; } return ['concat'].concat(contents); }); diff --git a/test/javascripts/lib/pretty-text-test.js.es6 b/test/javascripts/lib/pretty-text-test.js.es6 index e4ef370a01..af6486b33a 100644 --- a/test/javascripts/lib/pretty-text-test.js.es6 +++ b/test/javascripts/lib/pretty-text-test.js.es6 @@ -551,6 +551,7 @@ test('basic bbcode', function() { test('urls', function() { cookedPara("[url]not a url[/url]", "not a url", "supports [url] that isn't a url"); + cookedPara("[url]abc.com[/url]", "abc.com", "no error when a url has no protocol and begins with a"); cookedPara("[url]http://bettercallsaul.com[/url]", "http://bettercallsaul.com", "supports [url] without parameter"); cookedPara("[url=http://example.com]example[/url]", "example", "supports [url] with given href"); cookedPara("[url=http://www.example.com][img]http://example.com/logo.png[/img][/url]", From e09a304122576248edd964ec5ba6243002410dd0 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 14:39:26 -0400 Subject: [PATCH 122/170] FIX: Jumping within a topic should respect anchors --- .../javascripts/discourse/lib/url.js.es6 | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index dbe9874958..5bb54f9358 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -5,10 +5,11 @@ import { defaultHomepage } from 'discourse/lib/utilities'; let _jumpScheduled = false; const rewrites = []; +const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/; + const DiscourseURL = Ember.Object.extend({ // Used for matching a topic - TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/, isJumpScheduled: function() { return _jumpScheduled; @@ -207,11 +208,11 @@ const DiscourseURL = Ember.Object.extend({ same topic, use replaceState and instruct our controller to load more posts. **/ navigatedToPost(oldPath, path, routeOpts) { - const newMatches = this.TOPIC_REGEXP.exec(path); + const newMatches = TOPIC_REGEXP.exec(path); const newTopicId = newMatches ? newMatches[2] : null; if (newTopicId) { - const oldMatches = this.TOPIC_REGEXP.exec(oldPath); + const oldMatches = TOPIC_REGEXP.exec(oldPath); const oldTopicId = oldMatches ? oldMatches[2] : null; // If the topic_id is the same @@ -237,7 +238,16 @@ const DiscourseURL = Ember.Object.extend({ this.appEvents.trigger('post:highlight', closest); }).then(() => { - DiscourseURL.jumpToPost(closest, {skipIfOnScreen: routeOpts.skipIfOnScreen}); + const jumpOpts = { + skipIfOnScreen: routeOpts.skipIfOnScreen + }; + + const m = /#.+$/.exec(path); + if (m) { + jumpOpts.anchor = m[0]; + } + + this.jumpToPost(closest, jumpOpts); }); // Abort routing, we have replaced our state. From 078f6c3fb5f36f3c445cb729288a13cfa34d2f9a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 15:13:56 -0400 Subject: [PATCH 123/170] FIX: Consistency with HTML anchors --- .../javascripts/discourse/lib/lock-on.js.es6 | 3 + .../discourse/lib/offset-calculator.js.es6 | 2 + .../discourse/lib/static-route-builder.js.es6 | 5 +- .../javascripts/discourse/lib/url.js.es6 | 74 +++++++------------ 4 files changed, 33 insertions(+), 51 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/lock-on.js.es6 b/app/assets/javascripts/discourse/lib/lock-on.js.es6 index 0770083c72..d6b9594584 100644 --- a/app/assets/javascripts/discourse/lib/lock-on.js.es6 +++ b/app/assets/javascripts/discourse/lib/lock-on.js.es6 @@ -42,6 +42,9 @@ export default class LockOn { clearLock(interval) { $('body,html').off(SCROLL_EVENTS); clearInterval(interval); + if (this.options.finished) { + this.options.finished(); + } } lock() { diff --git a/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 b/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 index e9413eb771..92dd68dea5 100644 --- a/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 +++ b/app/assets/javascripts/discourse/lib/offset-calculator.js.es6 @@ -19,6 +19,8 @@ export default function offsetCalculator(y) { const ideal = headerHeight + ((expectedOffset < 0) ? 0 : expectedOffset); const $container = $('.posts-wrapper'); + if ($container.length === 0) { return expectedOffset; } + const topPos = $container.offset().top; const scrollTop = y || $(window).scrollTop(); diff --git a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 index 0174c16483..7a1baf2e27 100644 --- a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 +++ b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 @@ -1,5 +1,5 @@ -import DiscourseURL from 'discourse/lib/url'; import StaticPage from 'discourse/models/static-page'; +import { default as DiscourseURL, jumpToElement } from 'discourse/lib/url'; const configs = { "faq": "faq_url", @@ -23,8 +23,7 @@ export default function(page) { activate() { this._super(); - // Scroll to an element if exists - DiscourseURL.scrollToId(document.location.hash); + jumpToElement(document.location.hash.substr(1)); }, model() { diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 5bb54f9358..4b72cc26eb 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -2,16 +2,28 @@ import offsetCalculator from 'discourse/lib/offset-calculator'; import LockOn from 'discourse/lib/lock-on'; import { defaultHomepage } from 'discourse/lib/utilities'; -let _jumpScheduled = false; const rewrites = []; - const TOPIC_REGEXP = /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/; +let _jumpScheduled = false; +export function jumpToElement(elementId) { + if (_jumpScheduled || Ember.isEmpty(elementId)) { return; } + + const selector = `#${elementId}, a[name=${elementId}]`; + _jumpScheduled = true; + Ember.run.schedule('afterRender', function() { + const lockon = new LockOn(selector, { + finished() { + _jumpScheduled = false; + } + }); + lockon.lock(); + }); +} + const DiscourseURL = Ember.Object.extend({ - // Used for matching a topic - - isJumpScheduled: function() { + isJumpScheduled() { return _jumpScheduled; }, @@ -56,13 +68,8 @@ const DiscourseURL = Ember.Object.extend({ }); }, - /** - Browser aware replaceState. Will only be invoked if the browser supports it. - - @method replaceState - @param {String} path The path we are replacing our history state with. - **/ - replaceState: function(path) { + // Browser aware replaceState. Will only be invoked if the browser supports it. + replaceState(path) { if (window.history && window.history.pushState && window.history.replaceState && @@ -81,23 +88,6 @@ const DiscourseURL = Ember.Object.extend({ } }, - // Scroll to the same page, different anchor - scrollToId(id) { - if (Em.isEmpty(id)) { return; } - - _jumpScheduled = true; - Em.run.schedule('afterRender', function() { - let $elem = $(id); - if ($elem.length === 0) { - $elem = $("[name='" + id.replace('#', '') + "']"); - } - if ($elem.length > 0) { - $('html,body').scrollTop($elem.offset().top - $('header').height() - 15); - _jumpScheduled = false; - } - }); - }, - routeToTag(a) { if (a && a.host !== document.location.host) { document.location = a.href; @@ -131,10 +121,10 @@ const DiscourseURL = Ember.Object.extend({ } // Scroll to the same page, different anchor - if (path.indexOf('#') === 0) { - this.scrollToId(path); - this.replaceState(path); - return; + const m = /#(.+)$/.exec(path); + if (m) { + jumpToElement(m[1]); + return this.replaceState(path); } const oldPath = window.location.pathname; @@ -299,7 +289,7 @@ const DiscourseURL = Ember.Object.extend({ // Get a controller. Note that currently it uses `__container__` which is not // advised but there is no other way to access the router. - controllerFor: function(name) { + controllerFor(name) { return Discourse.__container__.lookup('controller:' + name); }, @@ -307,7 +297,7 @@ const DiscourseURL = Ember.Object.extend({ Be wary of looking up the router. In this case, we have links in our HTML, say form compiled markdown posts, that need to be routed. **/ - handleURL: function(path, opts) { + handleURL(path, opts) { opts = opts || {}; const router = this.get('router'); @@ -328,19 +318,7 @@ const DiscourseURL = Ember.Object.extend({ const transition = router.handleURL(path); transition._discourse_intercepted = true; - transition.promise.then(function() { - if (elementId) { - - _jumpScheduled = true; - Em.run.next('afterRender', function() { - const offset = $('#' + elementId).offset(); - if (offset && offset.top) { - $('html, body').scrollTop(offset.top - $('header').height() - 10); - _jumpScheduled = false; - } - }); - } - }); + transition.promise.then(() => jumpToElement(elementId)); } }).create(); From cc976e30468d0fff0e7d6218702ca5302a3e0c32 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 15:37:32 -0400 Subject: [PATCH 124/170] FIX: Don't lose focus when refreshing user results --- app/assets/javascripts/discourse/templates/users.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/users.hbs b/app/assets/javascripts/discourse/templates/users.hbs index bed82ddb6c..82b7ce0c70 100644 --- a/app/assets/javascripts/discourse/templates/users.hbs +++ b/app/assets/javascripts/discourse/templates/users.hbs @@ -4,7 +4,7 @@
{{period-chooser period=period}} - {{text-field value=nameInput placeholderKey="directory.filter_name" class="filter-name"}} + {{text-field value=nameInput placeholderKey="directory.filter_name" class="filter-name no-blur"}}
{{#conditional-loading-spinner condition=model.loading}} From 8e87a727ef2ba46c332695aaf87e0f0d747f7364 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 16:08:58 -0400 Subject: [PATCH 125/170] FIX: Add topic entrance to mobile categories page when clicking count --- .../components/mobile-category-topic.js.es6 | 7 +++++ .../components/topic-list-item.js.es6 | 30 +++++++++++-------- .../components/mobile-category-topic.hbs | 11 +++++++ .../templates/mobile/discovery/categories.hbs | 16 ++-------- 4 files changed, 38 insertions(+), 26 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/mobile-category-topic.hbs diff --git a/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 b/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 new file mode 100644 index 0000000000..833c4a1dfb --- /dev/null +++ b/app/assets/javascripts/discourse/components/mobile-category-topic.js.es6 @@ -0,0 +1,7 @@ +import { showEntrance } from 'discourse/components/topic-list-item'; + +export default Ember.Component.extend({ + tagName: 'tr', + classNameBindings: [':category-topic-link', 'topic.archived'], + click: showEntrance +}); diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 index 2f5026e108..28c00c4345 100644 --- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 @@ -1,5 +1,20 @@ import StringBuffer from 'discourse/mixins/string-buffer'; +export function showEntrance(e) { + let target = $(e.target); + + if (target.hasClass('posts-map') || target.parents('.posts-map').length > 0) { + if (target.prop('tagName') !== 'A') { + target = target.find('a'); + if (target.length===0) { + target = target.end(); + } + } + this.container.lookup('controller:application').send("showTopicEntrance", {topic: this.get('topic'), position: target.offset()}); + return false; + } +} + export default Ember.Component.extend(StringBuffer, { rerenderTriggers: ['bulkSelectEnabled', 'topic.pinned'], tagName: 'tr', @@ -77,19 +92,10 @@ export default Ember.Component.extend(StringBuffer, { }.property(), click(e) { - let target = $(e.target); - - if (target.hasClass('posts-map') || target.parents('.posts-map').length > 0) { - if (target.prop('tagName') !== 'A') { - target = target.find('a'); - if (target.length===0) { - target = target.end(); - } - } - this.container.lookup('controller:application').send("showTopicEntrance", {topic: this.get('topic'), position: target.offset()}); - return false; - } + const result = showEntrance.call(this, e); + if (result === false) { return result; } + const target = $(e.target); if (target.hasClass('bulk-select')) { const selected = this.get('selected'); const topic = this.get('topic'); diff --git a/app/assets/javascripts/discourse/templates/components/mobile-category-topic.hbs b/app/assets/javascripts/discourse/templates/components/mobile-category-topic.hbs new file mode 100644 index 0000000000..c3827841dc --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/mobile-category-topic.hbs @@ -0,0 +1,11 @@ + +
+ {{topic-status topic=topic}} + {{topic-link topic}} + {{#if topic.unseen}} + + {{/if}} + {{{format-age topic.last_posted_at}}} +
+ +{{raw "list/post-count-or-badges" topic=topic postBadgesEnabled="true"}} diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs index 3250bbdaa2..34a6e99a27 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs @@ -16,21 +16,9 @@ {{/if}} {{#each c.topics as |t|}} - - -
- {{topic-status topic=t}} - {{topic-link t}} - {{#if t.unseen}} - - {{/if}} - {{{format-age t.last_posted_at}}} -
- - {{raw "list/post-count-or-badges" topic=t postBadgesEnabled="true"}} - - + {{mobile-category-topic topic=t}} {{/each}} + {{#if c.subcategories}} From 7c092b0fe051a1240cdbf53450f2fe9b0f9190f3 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 20 Jul 2016 16:21:43 -0400 Subject: [PATCH 126/170] FEATURE: add filter to show topics that have not been tagged --- .../discourse/components/tag-drop.js.es6 | 14 +++++++ .../discourse/routes/tags-show.js.es6 | 27 +++++++++----- .../templates/components/tag-drop.hbs | 2 + app/controllers/tags_controller.rb | 9 ++++- config/locales/client.en.yml | 3 ++ lib/topic_query.rb | 4 ++ spec/components/topic_query_spec.rb | 37 +++++++++++-------- 7 files changed, 70 insertions(+), 26 deletions(-) diff --git a/app/assets/javascripts/discourse/components/tag-drop.js.es6 b/app/assets/javascripts/discourse/components/tag-drop.js.es6 index df07fcd0c3..75329a5cd0 100644 --- a/app/assets/javascripts/discourse/components/tag-drop.js.es6 +++ b/app/assets/javascripts/discourse/components/tag-drop.js.es6 @@ -45,6 +45,20 @@ export default Ember.Component.extend({ return I18n.t("tagging.selector_all_tags"); }.property('tag'), + @computed('firstCategory', 'secondCategory') + noTagsUrl() { + var url = '/tags'; + if (this.get('currentCategory')) { + url += this.get('currentCategory.url'); + } + return url + '/none'; + }, + + @computed('tag') + noTagsLabel() { + return I18n.t("tagging.selector_no_tags"); + }, + dropdownButtonClass: function() { var result = 'badge-category category-dropdown-button'; if (Em.isNone(this.get('tag'))) { diff --git a/app/assets/javascripts/discourse/routes/tags-show.js.es6 b/app/assets/javascripts/discourse/routes/tags-show.js.es6 index ea8762745b..a42e7f7e4d 100644 --- a/app/assets/javascripts/discourse/routes/tags-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/tags-show.js.es6 @@ -11,7 +11,7 @@ export default Discourse.Route.extend({ }, model(params) { - var tag = this.store.createRecord("tag", { id: Handlebars.Utils.escapeExpression(params.tag_id) }), + var tag = (params.tag_id === 'none' ? null : this.store.createRecord("tag", { id: Handlebars.Utils.escapeExpression(params.tag_id) })), f = ''; if (params.category) { @@ -25,8 +25,8 @@ export default Discourse.Route.extend({ if (params.category) { this.set('categorySlug', params.category); } if (params.parent_category) { this.set('parentCategorySlug', params.parent_category); } - if (this.get("currentUser")) { - // If logged in, we should get the tag"s user settings + if (tag && this.get("currentUser")) { + // If logged in, we should get the tag's user settings return this.store.find("tagNotification", tag.get("id")).then(tn => { this.set("tagNotification", tn); return tag; @@ -45,18 +45,19 @@ export default Discourse.Route.extend({ const categorySlug = this.get('categorySlug'); const parentCategorySlug = this.get('parentCategorySlug'); const filter = this.get('navMode'); + const tag_id = (tag ? tag.id : 'none'); if (categorySlug) { var category = Discourse.Category.findBySlug(categorySlug, parentCategorySlug); if (parentCategorySlug) { - params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tag.id}/l/${filter}`; + params.filter = `tags/c/${parentCategorySlug}/${categorySlug}/${tag_id}/l/${filter}`; } else { - params.filter = `tags/c/${categorySlug}/${tag.id}/l/${filter}`; + params.filter = `tags/c/${categorySlug}/${tag_id}/l/${filter}`; } this.set('category', category); } else { - params.filter = `tags/${tag.id}/l/${filter}`; + params.filter = `tags/${tag_id}/l/${filter}`; this.set('category', null); } @@ -74,10 +75,18 @@ export default Discourse.Route.extend({ const filterText = I18n.t('filters.' + this.get('navMode').replace('/', '.') + '.title'), controller = this.controllerFor('tags.show'); - if (this.get('category')) { - return I18n.t('tagging.filters.with_category', { filter: filterText, tag: controller.get('model.id'), category: this.get('category.name')}); + if (controller.get('model.id')) { + if (this.get('category')) { + return I18n.t('tagging.filters.with_category', { filter: filterText, tag: controller.get('model.id'), category: this.get('category.name')}); + } else { + return I18n.t('tagging.filters.without_category', { filter: filterText, tag: controller.get('model.id')}); + } } else { - return I18n.t('tagging.filters.without_category', { filter: filterText, tag: controller.get('model.id')}); + if (this.get('category')) { + return I18n.t('tagging.filters.untagged_with_category', { filter: filterText, category: this.get('category.name')}); + } else { + return I18n.t('tagging.filters.untagged_without_category', { filter: filterText}); + } } }, diff --git a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs index 990a0776f8..5d9a658192 100644 --- a/app/assets/javascripts/discourse/templates/components/tag-drop.hbs +++ b/app/assets/javascripts/discourse/templates/components/tag-drop.hbs @@ -2,6 +2,7 @@ {{#if tagId}} {{tagId}} {{else}} + {{allTagsLabel}} {{/if}} @@ -9,6 +10,7 @@
+ {{#if renderTags}} {{#each tags as |t|}}
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index a476c204db..e0b945931d 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -57,7 +57,7 @@ class TagsController < ::ApplicationController @list.more_topics_url = list_by_tag_path(tag_id: @tag_id, page: page + 1) @rss = "tag" - if @list.topics.size == 0 && !Tag.where(name: @tag_id).exists? + if @list.topics.size == 0 && params[:tag_id] != 'none' && !Tag.where(name: @tag_id).exists? raise Discourse::NotFound else respond_with_list(@list) @@ -203,7 +203,6 @@ class TagsController < ::ApplicationController topic_ids: param_to_integer_list(:topic_ids), exclude_category_ids: params[:exclude_category_ids], category: params[:category], - tags: [params[:tag_id]], order: params[:order], ascending: params[:ascending], min_posts: params[:min_posts], @@ -217,6 +216,12 @@ class TagsController < ::ApplicationController options[:no_subcategories] = true if params[:no_subcategories] == 'true' options[:slow_platform] = true if slow_platform? + if params[:tag_id] == 'none' + options[:no_tags] = true + else + options[:tags] = [params[:tag_id]] + end + options end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3cb67e0c85..56876ff59e 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2123,6 +2123,7 @@ en: tagging: all_tags: "All Tags" selector_all_tags: "all tags" + selector_no_tags: "no tags" changed: "tags changed:" tags: "Tags" choose_for_topic: "choose optional tags for this topic" @@ -2139,6 +2140,8 @@ en: filters: without_category: "%{filter} %{tag} topics" with_category: "%{filter} %{tag} topics in %{category}" + untagged_without_category: "%{filter} untagged topics" + untagged_with_category: "%{filter} untagged topics in %{category}" notifications: watching: diff --git a/lib/topic_query.rb b/lib/topic_query.rb index ed4aecf818..49cef6da06 100644 --- a/lib/topic_query.rb +++ b/lib/topic_query.rb @@ -20,6 +20,7 @@ class TopicQuery visible category tags + no_tags order ascending no_subcategories @@ -465,6 +466,9 @@ class TopicQuery else result = result.where("tags.name in (?)", @options[:tags]) end + elsif @options[:no_tags] + # the following will do: ("topics"."id" NOT IN (SELECT DISTINCT "topic_tags"."topic_id" FROM "topic_tags")) + result = result.where.not(:id => TopicTag.select(:topic_id).uniq) end end diff --git a/spec/components/topic_query_spec.rb b/spec/components/topic_query_spec.rb index 0b9b19f854..1c6e63fcef 100644 --- a/spec/components/topic_query_spec.rb +++ b/spec/components/topic_query_spec.rb @@ -122,25 +122,32 @@ describe TopicQuery do SiteSetting.tagging_enabled = true end - it "returns topics with the tag when filtered to it" do - tagged_topic1 = Fabricate(:topic, {tags: [tag]}) - tagged_topic2 = Fabricate(:topic, {tags: [other_tag]}) - tagged_topic3 = Fabricate(:topic, {tags: [tag, other_tag]}) - no_tags_topic = Fabricate(:topic) + context "no category filter" do + # create some topics before each test: + let!(:tagged_topic1) { Fabricate(:topic, {tags: [tag]}) } + let!(:tagged_topic2) { Fabricate(:topic, {tags: [other_tag]}) } + let!(:tagged_topic3) { Fabricate(:topic, {tags: [tag, other_tag]}) } + let!(:no_tags_topic) { Fabricate(:topic) } - expect(TopicQuery.new(moderator, tags: [tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort) - expect(TopicQuery.new(moderator, tags: [tag.id]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort) + it "returns topics with the tag when filtered to it" do + expect(TopicQuery.new(moderator, tags: [tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort) + expect(TopicQuery.new(moderator, tags: [tag.id]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic3.id].sort) - two_tag_topic = TopicQuery.new(moderator, tags: [tag.name]).list_latest.topics.find { |t| t.id == tagged_topic3.id } - expect(two_tag_topic.tags.size).to eq(2) + two_tag_topic = TopicQuery.new(moderator, tags: [tag.name]).list_latest.topics.find { |t| t.id == tagged_topic3.id } + expect(two_tag_topic.tags.size).to eq(2) - # topics with ANY of the given tags: - expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort) - expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort) + # topics with ANY of the given tags: + expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort) + expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics.map(&:id).sort).to eq([tagged_topic1.id, tagged_topic2.id, tagged_topic3.id].sort) - # TODO: topics with ALL of the given tags: - # expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics.map(&:id)).to eq([tagged_topic3.id].sort) - # expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics.map(&:id)).to eq([tagged_topic3.id].sort) + # TODO: topics with ALL of the given tags: + # expect(TopicQuery.new(moderator, tags: [tag.name, other_tag.name]).list_latest.topics.map(&:id)).to eq([tagged_topic3.id].sort) + # expect(TopicQuery.new(moderator, tags: [tag.id, other_tag.id]).list_latest.topics.map(&:id)).to eq([tagged_topic3.id].sort) + end + + it "can return topics with no tags" do + expect(TopicQuery.new(moderator, no_tags: true).list_latest.topics.map(&:id)).to eq([no_tags_topic.id]) + end end context "and categories too" do From 87b52e4ceaad2c611f8c09969fadf48c9fc049c1 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 20 Jul 2016 16:45:48 -0400 Subject: [PATCH 127/170] FIX: Support emoji in "Popular Links" --- app/assets/javascripts/discourse/widgets/emoji.js.es6 | 8 +++++++- .../javascripts/discourse/widgets/post-links.js.es6 | 6 ++---- app/assets/javascripts/discourse/widgets/topic-map.js.es6 | 4 ++-- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/emoji.js.es6 b/app/assets/javascripts/discourse/widgets/emoji.js.es6 index 2eb66d9368..5550af6a2e 100644 --- a/app/assets/javascripts/discourse/widgets/emoji.js.es6 +++ b/app/assets/javascripts/discourse/widgets/emoji.js.es6 @@ -1,5 +1,11 @@ import { createWidget } from 'discourse/widgets/widget'; -import { emojiUrlFor } from 'discourse/lib/text'; +import { emojiUrlFor, emojiUnescape } from 'discourse/lib/text'; +import RawHtml from 'discourse/widgets/raw-html'; + +export function replaceEmoji(str) { + const escaped = emojiUnescape(Handlebars.Utils.escapeExpression(str)); + return [new RawHtml({ html: `${escaped}` })]; +} export default createWidget('emoji', { tagName: 'img.emoji', diff --git a/app/assets/javascripts/discourse/widgets/post-links.js.es6 b/app/assets/javascripts/discourse/widgets/post-links.js.es6 index c607a84cb3..3d5b8f871f 100644 --- a/app/assets/javascripts/discourse/widgets/post-links.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-links.js.es6 @@ -1,8 +1,7 @@ import { iconNode } from 'discourse/helpers/fa-icon'; import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; -import RawHtml from 'discourse/widgets/raw-html'; -import { emojiUnescape } from 'discourse/lib/text'; +import { replaceEmoji } from 'discourse/widgets/emoji'; export default createWidget('post-links', { tagName: 'div.post-links-container', @@ -13,8 +12,7 @@ export default createWidget('post-links', { }, linkHtml(link) { - const escapedTitle = emojiUnescape(Handlebars.Utils.escapeExpression(link.title)); - const linkBody = [new RawHtml({ html: `${escapedTitle}` })]; + const linkBody = replaceEmoji(link.title); if (link.clicks) { linkBody.push(h('span.badge.badge-notification.clicks', link.clicks.toString())); } diff --git a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 index c219f2a085..675e8b9d3a 100644 --- a/app/assets/javascripts/discourse/widgets/topic-map.js.es6 +++ b/app/assets/javascripts/discourse/widgets/topic-map.js.es6 @@ -2,6 +2,7 @@ import { createWidget } from 'discourse/widgets/widget'; import { h } from 'virtual-dom'; import { avatarImg, avatarFor } from 'discourse/widgets/post'; import { dateNode, numberNode } from 'discourse/helpers/node'; +import { replaceEmoji } from 'discourse/widgets/emoji'; const LINKS_SHOWN = 5; @@ -115,8 +116,7 @@ createWidget('topic-map-link', { }, html(attrs) { - if (attrs.title) { return attrs.title; } - return attrs.url; + return attrs.title ? replaceEmoji(attrs.title) : attrs.url; } }); From 33a628b0b05478b255586b01b7a95f3d5b13eea7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 21 Jul 2016 06:05:06 +0800 Subject: [PATCH 128/170] UX: Vote now button to show up as primary once options have been selected. --- plugins/poll/assets/javascripts/controllers/poll.js.es6 | 5 +++++ plugins/poll/assets/javascripts/discourse/templates/poll.hbs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/plugins/poll/assets/javascripts/controllers/poll.js.es6 b/plugins/poll/assets/javascripts/controllers/poll.js.es6 index fe820beaa6..c015c393f6 100644 --- a/plugins/poll/assets/javascripts/controllers/poll.js.es6 +++ b/plugins/poll/assets/javascripts/controllers/poll.js.es6 @@ -107,6 +107,11 @@ export default Ember.Controller.extend({ castVotesDisabled: Em.computed.not("canCastVotes"), + @computed("castVotesDisabled") + castVotesButtonClass(castVotesDisabled) { + return `cast-votes ${castVotesDisabled ? '' : 'btn-primary'}`; + }, + @computed("loading", "post.user_id", "post.topic.archived") canToggleStatus(loading, userId, topicArchived) { return this.currentUser && diff --git a/plugins/poll/assets/javascripts/discourse/templates/poll.hbs b/plugins/poll/assets/javascripts/discourse/templates/poll.hbs index dc8c415913..6860374758 100644 --- a/plugins/poll/assets/javascripts/discourse/templates/poll.hbs +++ b/plugins/poll/assets/javascripts/discourse/templates/poll.hbs @@ -41,7 +41,7 @@
{{#if isMultiple}} {{#unless hideResultsDisabled}} - {{d-button class="cast-votes" title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}} + {{d-button class=castVotesButtonClass title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}} {{/unless}} {{/if}} From d7ffbf9c9704604755aeb00b940531378a640aa3 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 21 Jul 2016 00:49:29 -0700 Subject: [PATCH 129/170] copyedit to reflect improved watch/track --- config/locales/client.en.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 56876ff59e..01459719db 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -544,13 +544,13 @@ en: watched_tags: "Watched" watched_tags_instructions: "You will automatically watch all topics with these tags. You will be notified of all new posts and topics, and a count of new posts will also appear next to the topic." tracked_tags: "Tracked" - tracked_tags_instructions: "You will automatically track all new topics with these tags. A count of new posts will appear next to the topic." + tracked_tags_instructions: "You will automatically track all topics with these tags. A count of new posts will appear next to the topic." muted_tags: "Muted" muted_tags_instructions: "You will not be notified of anything about new topics with these tags, and they will not appear in latest." watched_categories: "Watched" watched_categories_instructions: "You will automatically watch all topics in these categories. You will be notified of all new posts and topics, and a count of new posts will also appear next to the topic." tracked_categories: "Tracked" - tracked_categories_instructions: "You will automatically track all new topics in these categories. A count of new posts will appear next to the topic." + tracked_categories_instructions: "You will automatically track all topics in these categories. A count of new posts will appear next to the topic." watched_first_post_categories: "Watching First Post" watched_first_post_categories_instructions: "You will be notified of the first post in each new topic in these categories." muted_categories: "Muted" @@ -1819,13 +1819,13 @@ en: notifications: watching: title: "Watching" - description: "You will automatically watch all new topics in these categories. You will be notified of every new post in every topic, and a count of new replies will be shown." + description: "You will automatically watch all topics in these categories. You will be notified of every new post in every topic, and a count of new replies will be shown." watching_first_post: title: "Watching First Post" description: "You will only be notified of the first post in each new topic in these categories." tracking: title: "Tracking" - description: "You will automatically track all new topics in these categories. You will be notified if someone mentions your @name or replies to you, and a count of new replies will be shown." + description: "You will automatically track all topics in these categories. You will be notified if someone mentions your @name or replies to you, and a count of new replies will be shown." regular: title: "Normal" description: "You will be notified if someone mentions your @name or replies to you." @@ -2146,13 +2146,13 @@ en: notifications: watching: title: "Watching" - description: "You will automatically watch all new topics in this tag. You will be notified of all new posts and topics, plus the count of unread and new posts will also appear next to the topic." + description: "You will automatically watch all topics in this tag. You will be notified of all new posts and topics, plus the count of unread and new posts will also appear next to the topic." watching_first_post: title: "Watching First Post" description: "You will only be notified of the first post in each new topic in this tag." tracking: title: "Tracking" - description: "You will automatically track all new topics in this tag. A count of unread and new posts will appear next to the topic." + description: "You will automatically track all topics in this tag. A count of unread and new posts will appear next to the topic." regular: title: "Regular" description: "You will be notified if someone mentions your @name or replies to your post." From c11f7bee99ae899e5dc1988c684f2e2c36256349 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 21 Jul 2016 14:10:57 -0400 Subject: [PATCH 130/170] FIX: Registering emoji via plugin.rb was broken --- lib/plugin/instance.rb | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 7085122c9a..54a1405cd5 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -264,6 +264,29 @@ JS end end + if emojis.present? + emoji_registrations = "" + emojis.each do |name, url| + emoji_registrations << "emoji.registerEmoji(#{name.inspect}, #{url.inspect});\n" + end + + js << <<~JS + define("discourse/initializers/custom-emoji", + ["pretty-text/emoji", "exports"], + function(emoji, __exports__) { + "use strict"; + + __exports__["default"] = { + name: "custom-emoji", + after: "inject-objects", + initialize: function(container) { + #{emoji_registrations} + } + }; + }); + JS + end + # Generate an IIFE for the JS js = "(function(){#{js}})();" if js.present? From c279889191570cf7d5cf4fe1920f3b10b32deba0 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 21 Jul 2016 15:05:10 -0400 Subject: [PATCH 131/170] FIX: Watching First Post in groups was working incorrectly --- app/services/post_alerter.rb | 2 +- spec/services/post_alerter_spec.rb | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/services/post_alerter.rb b/app/services/post_alerter.rb index 073c2ba06b..86ecfbc2f1 100644 --- a/app/services/post_alerter.rb +++ b/app/services/post_alerter.rb @@ -120,7 +120,7 @@ class PostAlerter .where(notification_level: TagUser.notification_levels[:watching_first_post]) .pluck(:user_id) - group_ids = post.user.groups.pluck(:id) + group_ids = topic.allowed_groups.pluck(:group_id) group_watchers = GroupUser.where(group_id: group_ids, notification_level: GroupUser.notification_levels[:watching_first_post]) .pluck(:user_id) diff --git a/spec/services/post_alerter_spec.rb b/spec/services/post_alerter_spec.rb index 4f7e200975..e0288c4a6e 100644 --- a/spec/services/post_alerter_spec.rb +++ b/spec/services/post_alerter_spec.rb @@ -363,6 +363,7 @@ describe PostAlerter do it "notifies the user who is following the first post group" do GroupUser.create(group_id: group.id, user_id: user.id) GroupUser.create(group_id: group.id, user_id: post.user.id) + topic.topic_allowed_groups.create(group_id: group.id) level = GroupUser.notification_levels[:watching_first_post] GroupUser.where(user_id: user.id, group_id: group.id).update_all(notification_level: level) From 440558517f247539f465e3c4985770a8e1337b7e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 21 Jul 2016 15:22:57 -0400 Subject: [PATCH 132/170] Revert "Let's avoid Ruby 2.3 syntax for now" According to @tgxworld we only support 2.3 now so let's put this back! This reverts commit ede19943b334658c77661102fbefefaa96f1928e. --- .../app/jobs/scheduled/daily_performance_report.rb | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/discourse-nginx-performance-report/app/jobs/scheduled/daily_performance_report.rb b/plugins/discourse-nginx-performance-report/app/jobs/scheduled/daily_performance_report.rb index c16a35197e..78b25666e4 100644 --- a/plugins/discourse-nginx-performance-report/app/jobs/scheduled/daily_performance_report.rb +++ b/plugins/discourse-nginx-performance-report/app/jobs/scheduled/daily_performance_report.rb @@ -10,14 +10,14 @@ module Jobs report_data = if result.strip.empty? - < Date: Fri, 22 Jul 2016 09:48:26 +1000 Subject: [PATCH 133/170] PERF: make score calculator cheaper when site has long topics --- app/jobs/scheduled/periodical_updates.rb | 12 +++- lib/score_calculator.rb | 79 +++++++++++------------- spec/components/score_calculator_spec.rb | 13 ++++ 3 files changed, 61 insertions(+), 43 deletions(-) diff --git a/app/jobs/scheduled/periodical_updates.rb b/app/jobs/scheduled/periodical_updates.rb index b051b3f85d..2d0485ba52 100644 --- a/app/jobs/scheduled/periodical_updates.rb +++ b/app/jobs/scheduled/periodical_updates.rb @@ -7,12 +7,22 @@ module Jobs class PeriodicalUpdates < Jobs::Scheduled every 15.minutes + def self.should_update_long_topics? + @call_count ||= 0 + @call_count += 1 + + # once every 6 hours + (@call_count % 24) == 1 + end + def execute(args) # Feature topics in categories CategoryFeaturedTopic.feature_topics # Update the scores of posts - ScoreCalculator.new.calculate(1.day.ago) + args = {min_topic_age: 1.day.ago} + args[:max_topic_length] = 500 unless self.class.should_update_long_topics? + ScoreCalculator.new.calculate(args) # Automatically close stuff that we missed Topic.auto_close diff --git a/lib/score_calculator.rb b/lib/score_calculator.rb index 681877bdae..8901c12e5a 100644 --- a/lib/score_calculator.rb +++ b/lib/score_calculator.rb @@ -16,48 +16,44 @@ class ScoreCalculator end # Calculate the score for all posts based on the weightings - def calculate(min_topic_age=nil) - - update_posts_score(min_topic_age) - - update_posts_rank(min_topic_age) - - update_topics_rank(min_topic_age) - - update_topics_percent_rank(min_topic_age) - + def calculate(opts=nil) + update_posts_score(opts) + update_posts_rank(opts) + update_topics_rank(opts) + update_topics_percent_rank(opts) end private - def update_posts_score(min_topic_age) + def update_posts_score(opts) limit = 20000 components = [] - @weightings.each_key { |k| components << "COALESCE(#{k}, 0) * :#{k}" } + @weightings.each_key { |k| components << "COALESCE(posts.#{k}, 0) * :#{k}" } components = components.join(" + ") builder = SqlBuilder.new < #{components}", @weightings) + builder.where("posts.score IS NULL OR posts.score <> #{components}", @weightings) - filter_topics(builder, min_topic_age) + filter_topics(builder, opts) while builder.exec.cmd_tuples == limit end end - def update_posts_rank(min_topic_age) + def update_posts_rank(opts) limit = 20000 builder = SqlBuilder.new < posts.percent_rank") - filter_topics(builder, min_topic_age) + filter_topics(builder, opts) while builder.exec.cmd_tuples == limit end end - def update_topics_rank(min_topic_age) - builder = SqlBuilder.new("UPDATE topics AS t - SET has_summary = (t.like_count >= :likes_required AND - t.posts_count >= :posts_required AND + def update_topics_rank(opts) + builder = SqlBuilder.new("UPDATE topics AS topics + SET has_summary = (topics.like_count >= :likes_required AND + topics.posts_count >= :posts_required AND x.max_score >= :score_required), score = x.avg_score FROM (SELECT p.topic_id, @@ -99,12 +96,12 @@ SQL GROUP BY p.topic_id) AS x /*where*/") - builder.where("x.topic_id = t.id AND + builder.where("x.topic_id = topics.id AND ( - (t.score <> x.avg_score OR t.score IS NULL) OR - (t.has_summary IS NULL OR t.has_summary <> ( - t.like_count >= :likes_required AND - t.posts_count >= :posts_required AND + (topics.score <> x.avg_score OR topics.score IS NULL) OR + (topics.has_summary IS NULL OR topics.has_summary <> ( + topics.like_count >= :likes_required AND + topics.posts_count >= :posts_required AND x.max_score >= :score_required )) ) @@ -113,15 +110,13 @@ SQL posts_required: SiteSetting.summary_posts_required, score_required: SiteSetting.summary_score_threshold) - if min_topic_age - builder.where("t.bumped_at > :bumped_at ", - bumped_at: min_topic_age) - end + + filter_topics(builder, opts) builder.exec end - def update_topics_percent_rank(min_topic_age) + def update_topics_percent_rank(opts) builder = SqlBuilder.new("UPDATE topics SET percent_rank = x.percent_rank FROM (SELECT id, percent_rank() @@ -131,22 +126,22 @@ SQL builder.where("x.id = topics.id AND (topics.percent_rank <> x.percent_rank OR topics.percent_rank IS NULL)") - - if min_topic_age - builder.where("topics.bumped_at > :bumped_at ", - bumped_at: min_topic_age) - end - + filter_topics(builder, opts) builder.exec end - def filter_topics(builder, min_topic_age) - if min_topic_age - builder.where('posts.topic_id IN - (SELECT id FROM topics WHERE bumped_at > :bumped_at)', - bumped_at: min_topic_age) + def filter_topics(builder, opts) + return builder unless opts + + if min_topic_age = opts[:min_topic_age] + builder.where("topics.bumped_at > :bumped_at ", + bumped_at: min_topic_age) + end + if max_topic_length = opts[:max_topic_length] + builder.where("topics.posts_count < :max_topic_length", + max_topic_length: max_topic_length) end builder diff --git a/spec/components/score_calculator_spec.rb b/spec/components/score_calculator_spec.rb index bcf41ebaa9..d747b14334 100644 --- a/spec/components/score_calculator_spec.rb +++ b/spec/components/score_calculator_spec.rb @@ -49,6 +49,19 @@ describe ScoreCalculator do expect(topic.has_summary).to eq(false) end + it "respects the min_topic_age" do + topic.update_columns(has_summary: true, bumped_at: 1.month.ago) + ScoreCalculator.new(reads: 3).calculate(min_topic_age: 20.days.ago) + expect(topic.has_summary).to eq(true) + end + + it "respects the max_topic_length" do + Fabricate(:post, topic_id: topic.id) + topic.update_columns(has_summary: true) + ScoreCalculator.new(reads: 3).calculate(max_topic_length: 1) + expect(topic.has_summary).to eq(true) + end + it "won't update the site settings when the site settings don't match" do SiteSetting.expects(:summary_likes_required).returns(0) SiteSetting.expects(:summary_posts_required).returns(1) From 2a257190e7ecb277e5ad91cedc7f57ea53712891 Mon Sep 17 00:00:00 2001 From: Ryan Mulligan Date: Fri, 22 Jul 2016 01:08:41 -0700 Subject: [PATCH 134/170] FEATURE: make discourse remap optionally do regex_replace (#4116) This adds a --regex option to discourse remap to use the regexp_replace feature in PostgreSQL. Example usage: discourse remap --regex "\[\/?color(=[^\]]*)*]" "" removes all the "color" bbcodes. Also, this commit fixes the --global option, which did not work because of how Thor processes the options. --- script/discourse | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/script/discourse b/script/discourse index b6c14276bf..794588e401 100755 --- a/script/discourse +++ b/script/discourse @@ -6,27 +6,31 @@ class DiscourseCLI < Thor class_option :verbose, default: false, aliases: :v desc "remap", "Remap a string sequence accross all tables" - def remap(from, to, global=nil) + option :global, :type => :boolean + option :regex, :type => :boolean + def remap(from, to) load_rails - global = global == "--global" - - puts "Rewriting all occurences of #{from} to #{to}" + if options[:regex] + puts "Rewriting all occurences of #{from} to #{to} using regexp_replace" + else + puts "Rewriting all occurences of #{from} to #{to}" + end puts "THIS TASK WILL REWRITE DATA, ARE YOU SURE (type YES)" - puts "WILL RUN ON ALL #{RailsMultisite::ConnectionManagement.all_dbs.length} DBS" if global + puts "WILL RUN ON ALL #{RailsMultisite::ConnectionManagement.all_dbs.length} DBS" if options[:global] text = STDIN.gets if text.strip != "YES" puts "aborting." exit end - if global + if options[:global] RailsMultisite::ConnectionManagement.each_connection do |db| puts "","Remapping tables on #{db}...","" - do_remap(from, to) + do_remap(from, to, options[:regex]) end else - do_remap(from, to) + do_remap(from, to, options[:regex]) end end @@ -196,7 +200,7 @@ class DiscourseCLI < Thor require File.expand_path(File.dirname(__FILE__) + "/../lib/import_export/import_export") end - def do_remap(from, to) + def do_remap(from, to, regex=false) sql = "SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' and (data_type like 'char%' or data_type like 'text%') and is_updatable = 'YES'" @@ -210,10 +214,17 @@ WHERE table_schema='public' and (data_type like 'char%' or data_type like 'text% column_name = result["column_name"] puts "Remapping #{table_name} #{column_name}" begin - result = cnn.async_exec("UPDATE #{table_name} - SET #{column_name} = replace(#{column_name}, $1, $2) - WHERE NOT #{column_name} IS NULL - AND #{column_name} <> replace(#{column_name}, $1, $2)", [from, to]) + result = if regex + cnn.async_exec("UPDATE #{table_name} + SET #{column_name} = regexp_replace(#{column_name}, $1, $2, 'g') + WHERE NOT #{column_name} IS NULL + AND #{column_name} <> regexp_replace(#{column_name}, $1, $2, 'g')", [from, to]) + else + cnn.async_exec("UPDATE #{table_name} + SET #{column_name} = replace(#{column_name}, $1, $2) + WHERE NOT #{column_name} IS NULL + AND #{column_name} <> replace(#{column_name}, $1, $2)", [from, to]) + end puts "#{result.cmd_tuples} rows affected!" rescue => ex puts "Error: #{ex}" From af266acac150b16f23a9e9886311d6c9150bc936 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 22 Jul 2016 12:59:43 -0400 Subject: [PATCH 135/170] FIX: Plugin Custom emoji weren't working correctly on the server side --- app/models/emoji.rb | 33 ++++++++++++++++++++-------- lib/plugin/instance.rb | 49 ++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 40 deletions(-) diff --git a/app/models/emoji.rb b/app/models/emoji.rb index e7bab29854..51eae0c706 100644 --- a/app/models/emoji.rb +++ b/app/models/emoji.rb @@ -23,19 +23,19 @@ class Emoji end def self.all - Discourse.cache.fetch("all_emojis:#{EMOJI_VERSION}") { standard | custom } + Discourse.cache.fetch(cache_key("all_emojis")) { standard | custom } end def self.standard - Discourse.cache.fetch("standard_emojis:#{EMOJI_VERSION}") { load_standard } + Discourse.cache.fetch(cache_key("standard_emojis")) { load_standard } end def self.aliases - Discourse.cache.fetch("aliases_emojis:#{EMOJI_VERSION}") { load_aliases } + Discourse.cache.fetch(cache_key("aliases_emojis")) { load_aliases } end def self.custom - Discourse.cache.fetch("custom_emojis:#{EMOJI_VERSION}") { load_custom } + Discourse.cache.fetch(cache_key("custom_emojis")) { load_custom } end def self.exists?(name) @@ -78,11 +78,15 @@ class Emoji Emoji[name] end + def self.cache_key(name) + "#{name}:#{EMOJI_VERSION}:#{Plugin::CustomEmoji.cache_key}" + end + def self.clear_cache - Discourse.cache.delete("custom_emojis:#{EMOJI_VERSION}") - Discourse.cache.delete("standard_emojis:#{EMOJI_VERSION}") - Discourse.cache.delete("aliases_emojis:#{EMOJI_VERSION}") - Discourse.cache.delete("all_emojis:#{EMOJI_VERSION}") + Discourse.cache.delete(cache_key("custom_emojis")) + Discourse.cache.delete(cache_key("standard_emojis")) + Discourse.cache.delete(cache_key("aliases_emojis")) + Discourse.cache.delete(cache_key("all_emojis")) end def self.db_file @@ -117,9 +121,20 @@ class Emoji end def self.load_custom + result = [] + Dir.glob(File.join(Emoji.base_directory, "*.{png,gif}")) .sort - .map { |emoji| Emoji.create_from_path(emoji) } + .each { |emoji| result << Emoji.create_from_path(emoji) } + + Plugin::CustomEmoji.emojis.each do |name, url| + result << Emoji.new.tap do |e| + e.name = name + e.url = url + end + end + + result end def self.base_directory diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 54a1405cd5..a9de8e4698 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -3,6 +3,21 @@ require 'fileutils' require_dependency 'plugin/metadata' require_dependency 'plugin/auth_provider' +class Plugin::CustomEmoji + def self.cache_key + @@cache_key ||= "plugin-emoji" + end + + def self.emojis + @@emojis ||= {} + end + + def self.register(name, url) + @@cache_key = Digest::SHA1.hexdigest(cache_key + name)[0..10] + emojis[name] = url + end +end + class Plugin::Instance attr_accessor :path, :metadata @@ -17,13 +32,8 @@ class Plugin::Instance } end - # Memoized hash readers - [:seed_data, :emojis].each do |att| - class_eval %Q{ - def #{att} - @#{att} ||= HashWithIndifferentAccess.new({}) - end - } + def seed_data + @seed_data ||= HashWithIndifferentAccess.new({}) end def self.find_all(parent_path) @@ -225,7 +235,7 @@ class Plugin::Instance end def register_emoji(name, url) - emojis[name] = url + Plugin::CustomEmoji.register(name, url) end def automatic_assets @@ -264,29 +274,6 @@ JS end end - if emojis.present? - emoji_registrations = "" - emojis.each do |name, url| - emoji_registrations << "emoji.registerEmoji(#{name.inspect}, #{url.inspect});\n" - end - - js << <<~JS - define("discourse/initializers/custom-emoji", - ["pretty-text/emoji", "exports"], - function(emoji, __exports__) { - "use strict"; - - __exports__["default"] = { - name: "custom-emoji", - after: "inject-objects", - initialize: function(container) { - #{emoji_registrations} - } - }; - }); - JS - end - # Generate an IIFE for the JS js = "(function(){#{js}})();" if js.present? From c28dd826fec5ba6963cd0dc64e1efb745079a5dd Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 22 Jul 2016 13:56:17 -0400 Subject: [PATCH 136/170] UX: Focus on usernames if creating a PM from your user page --- .../discourse/components/composer-title.js.es6 | 8 ++++---- .../components/composer-user-selector.js.es6 | 8 ++++++++ .../discourse/controllers/composer.js.es6 | 15 +++++++++++++++ .../javascripts/discourse/templates/composer.hbs | 8 ++++---- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/components/composer-title.js.es6 b/app/assets/javascripts/discourse/components/composer-title.js.es6 index 8ede0fd3d6..3d806ca0f7 100644 --- a/app/assets/javascripts/discourse/components/composer-title.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-title.js.es6 @@ -1,12 +1,12 @@ -import { default as computed, on } from 'ember-addons/ember-computed-decorators'; +import computed from 'ember-addons/ember-computed-decorators'; import InputValidation from 'discourse/models/input-validation'; export default Ember.Component.extend({ classNames: ['title-input'], - @on('didInsertElement') - _focusOnTitle() { - if (!this.capabilities.isIOS) { + didInsertElement() { + this._super(); + if (this.get('focusTarget') === 'title') { this.$('input').putCursorAtEnd(); } }, diff --git a/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 b/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 index d977d6e309..4d440091f5 100644 --- a/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-user-selector.js.es6 @@ -5,6 +5,14 @@ export default Ember.Component.extend({ shouldHide: false, defaultUsernameCount: 0, + didInsertElement() { + this._super(); + + if (this.get('focusTarget') === 'usernames') { + this.$('input').putCursorAtEnd(); + } + }, + @observes('usernames') _checkWidth() { let width = 0; diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 0903a27471..f896464a18 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -64,6 +64,21 @@ export default Ember.Controller.extend({ topic: null, linkLookup: null, + @computed('model.replyingToTopic', 'model.creatingPrivateMessage', 'model.targetUsernames') + focusTarget(replyingToTopic, creatingPM, usernames) { + if (this.capabilities.isIOS) { return "none"; } + + if (creatingPM && usernames === this.currentUser.get('username')) { + return 'usernames'; + } + + if (replyingToTopic) { + return 'reply'; + } + + return 'title'; + }, + showToolbar: Em.computed({ get(){ const keyValueStore = this.container.lookup('key-value-store:main'); diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 0680e931a1..acb26b1854 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -49,9 +49,9 @@
{{#if model.creatingPrivateMessage}} {{composer-user-selector topicId=topicModel.id - usernames=model.targetUsernames - hasGroups=model.hasTargetGroups - }} + usernames=model.targetUsernames + hasGroups=model.hasTargetGroups + focusTarget=focusTarget}} {{#if showWarning}}