From 4ee1bc63204ed9bcd7dccbc893f80fef76e26b47 Mon Sep 17 00:00:00 2001 From: 5minpause Date: Tue, 12 May 2015 10:16:21 +0200 Subject: [PATCH 001/237] Changes RSS item creation to prevent encoding errors SimpleRss is unreliable with parsing RSS feeds that contain German Umlauts. For example this feed http://www.lauffeuer-lb.de/api/v2/articles.xml can't be parsed by SimpleRss. Discourse's logs are full of ``` Job exception: Wrapped Encoding::CompatibilityError: incompatible character encodings: ASCII-8BIT and UTF-8 Job exception: incompatible character encodings: ASCII-8BIT and UTF-8 ``` The embedding fails because the feed can't be parsed. This change forces the encoding (using #scrub) which prevents the numerous encoding errors. --- app/jobs/scheduled/poll_feed.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/jobs/scheduled/poll_feed.rb b/app/jobs/scheduled/poll_feed.rb index 8c0c8eb018..920c2161a9 100644 --- a/app/jobs/scheduled/poll_feed.rb +++ b/app/jobs/scheduled/poll_feed.rb @@ -86,11 +86,15 @@ module Jobs end def content - @article_rss_item.content || @article_rss_item.description + if @article_rss_item.content + @article_rss_item.content.scrub + else + @article_rss_item.description.scrub + end end def title - @article_rss_item.title + @article_rss_item.title.scrub end def user From 690ad2ae5d4f904efe88815a95b7c3cc829d3cfa Mon Sep 17 00:00:00 2001 From: Marlon Andrade Date: Tue, 11 Aug 2015 14:13:07 -0300 Subject: [PATCH 002/237] Adjust Brewfile to follow homebrew-bundle syntax According to https://meta.discourse.org/t/brew-bundle-is-dead/23962 the official replacement for `brew bundle` is homebrew-bundle (https://github.com/Homebrew/homebrew-bundle) and its syntax changed a little bit. --- Brewfile | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/Brewfile b/Brewfile index 2f0e8f764e..ee7378b71c 100644 --- a/Brewfile +++ b/Brewfile @@ -1,22 +1,19 @@ # Install development dependencies on Mac OS X using Homebrew (http://mxcl.github.com/homebrew) -# ensure that Homebrew's sources are up to date -update - # add this repo to Homebrew's sources -tap homebrew/dupes +tap 'homebrew/dupes' # install the gcc compiler required for ruby -install apple-gcc42 +brew 'apple-gcc42' # you probably already have git installed; ensure that it is the latest version -install git +brew 'git' # install the PostgreSQL database -install postgresql +brew 'postgresql' # install the Redis datastore -install redis +brew 'redis' # install headless Javascript testing library -install phantomjs +brew 'phantomjs' From a45d6936f2d9c0cf06211df741dc3fd1f69d4d1f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Aug 2015 16:25:49 +0800 Subject: [PATCH 003/237] FIX: Allow user to abandon reply when clicking edit. --- app/assets/javascripts/discourse/controllers/composer.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index f724a6cbce..47d80d447e 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -396,7 +396,8 @@ export default Ember.ObjectController.extend(Presence, { // If we're already open, we don't have to do anything if (composerModel.get('composeState') === Discourse.Composer.OPEN && - composerModel.get('draftKey') === opts.draftKey) { + composerModel.get('draftKey') === opts.draftKey && + composerModel.get('action') === opts.action ) { return resolve(); } From 73bb60ee747b1ce3a7df4f69776dbce8c8037cee Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Aug 2015 16:26:58 +0800 Subject: [PATCH 004/237] FIX: Allow user to abandon draft reply when clicking edit. --- app/assets/javascripts/discourse/controllers/composer.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 47d80d447e..dbfb181829 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -405,7 +405,7 @@ export default Ember.ObjectController.extend(Presence, { if (composerModel.get('composeState') === Discourse.Composer.DRAFT && composerModel.get('draftKey') === opts.draftKey) { composerModel.set('composeState', Discourse.Composer.OPEN); - return resolve(); + if (composerModel.get('action') === opts.action) return resolve(); } // If it's a different draft, cancel it and try opening again. From 01354b5c30e61e1e1b661a182715981a55dd0ca7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Aug 2015 17:56:57 +0800 Subject: [PATCH 005/237] Remove unused code. --- app/assets/javascripts/discourse/models/composer.js.es6 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index e58f62362e..7f23460eb8 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -369,9 +369,7 @@ const Composer = RestModel.extend({ const composer = this; if (!replyBlank && - (opts.action !== this.get('action') || ((opts.reply || opts.action === this.EDIT) && this.get('reply') !== this.get('originalText'))) && - !opts.tested) { - opts.tested = true; + (opts.action !== this.get('action') || ((opts.reply || opts.action === this.EDIT) && this.get('reply') !== this.get('originalText')))) { return; } From 3ef66b1dca388e0919028654db3a63c6ca30ae64 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Aug 2015 18:26:06 +0800 Subject: [PATCH 006/237] Use existing function. --- app/assets/javascripts/discourse/models/composer.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 7f23460eb8..d0bc215627 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -369,7 +369,7 @@ const Composer = RestModel.extend({ const composer = this; if (!replyBlank && - (opts.action !== this.get('action') || ((opts.reply || opts.action === this.EDIT) && this.get('reply') !== this.get('originalText')))) { + (opts.action !== this.get('action') || ((opts.reply || opts.action === this.EDIT) && this.get('replyDirty')))) { return; } From 9fbab34e5738709c348c371334a6ac6278146daf Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 13 Aug 2015 20:17:01 +0800 Subject: [PATCH 007/237] FIX: Clear edit post when clicking reply. --- app/assets/javascripts/discourse/models/composer.js.es6 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index d0bc215627..e3f7f2ebf5 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -369,10 +369,11 @@ const Composer = RestModel.extend({ const composer = this; if (!replyBlank && - (opts.action !== this.get('action') || ((opts.reply || opts.action === this.EDIT) && this.get('replyDirty')))) { + ((opts.reply || opts.action === this.EDIT) && this.get('replyDirty'))) { return; } + if (opts.action === REPLY && this.get('action') === EDIT) this.set('reply', ''); if (!opts.draftKey) throw 'draft key is required'; if (opts.draftSequence === null) throw 'draft sequence is required'; From e2e3e7c0e04dff1df034d18f1366a0524de0c7f8 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 10 Aug 2015 17:11:27 -0400 Subject: [PATCH 008/237] Add ES6 support to more files --- .jshintrc | 1 - .../admin/controllers/admin-email-sent.js.es6 | 3 +- .../controllers/admin-email-skipped.js.es6 | 3 +- .../admin-logs-screened-ip-addresses.js.es6 | 3 +- .../admin/controllers/admin-permalinks.js.es6 | 4 +- .../controllers/admin-site-settings.js.es6 | 3 +- .../controllers/admin-users-list-show.js.es6 | 3 +- .../admin/views/admin-backups-logs.js.es6 | 3 +- .../admin/views/admin-backups.js.es6 | 4 +- .../components/edit-category-general.js.es6 | 3 +- .../components/header-extra-info.js.es6 | 4 +- .../discourse/components/home-logo.js.es6 | 3 +- .../components/navigation-bar.js.es6 | 6 +- .../discourse/controllers/change-owner.js.es6 | 3 +- .../discourse/controllers/composer.js.es6 | 15 +- .../controllers/create-account.js.es6 | 5 +- .../discourse/controllers/discovery.js.es6 | 4 +- .../controllers/edit-category.js.es6 | 5 +- .../discourse/controllers/invite.js.es6 | 3 +- .../discourse/controllers/merge-topic.js.es6 | 3 +- .../controllers/preferences/username.js.es6 | 3 +- .../discourse/controllers/quote-button.js.es6 | 3 +- .../discourse/controllers/search.js.es6 | 3 +- .../discourse/controllers/split-topic.js.es6 | 3 +- .../controllers/topic-entrance.js.es6 | 6 +- .../controllers/topic-progress.js.es6 | 4 +- .../discourse/controllers/topic.js.es6 | 5 +- .../discourse/controllers/user-card.js.es6 | 3 +- .../controllers/user-invited-show.js.es6 | 27 ++-- .../discourse/controllers/users.js.es6 | 4 +- .../discourse/helpers/custom-html.js.es6 | 21 ++- .../initializers/click-interceptor.js.es6 | 4 +- .../initializers/es6-deprecations.js.es6 | 27 ++++ .../initializers/inject-objects.js.es6 | 3 +- .../initializers/keyboard-shortcuts.js.es6 | 8 +- .../initializers/url-redirects.js.es6 | 10 +- .../discourse/lib/click-track.js.es6 | 8 +- .../lib/{debounce.js => debounce.js.es6} | 8 +- .../lib/desktop-notifications.js.es6 | 3 +- app/assets/javascripts/discourse/lib/html.js | 28 ---- ...shortcuts.js => keyboard-shortcuts.js.es6} | 148 +++++++++--------- .../discourse/lib/{quote.js => quote.js.es6} | 2 +- .../discourse/lib/static-route-builder.js.es6 | 6 +- .../discourse/lib/{url.js => url.js.es6} | 88 +++++------ .../discourse/mixins/scroll-top.js.es6 | 4 +- .../discourse/mixins/scrolling.js.es6 | 4 +- .../discourse/models/composer.js.es6 | 8 +- .../models/{draft.js => draft.js.es6} | 22 +-- .../models/{invite.js => invite.js.es6} | 27 ++-- .../discourse/models/post-stream.js.es6 | 3 +- .../javascripts/discourse/models/post.js.es6 | 3 +- .../discourse/models/selectable_array.js | 35 ----- .../discourse/routes/topic-by-slug.js.es6 | 3 +- .../discourse/routes/topic-from-params.js.es6 | 8 +- .../javascripts/discourse/routes/topic.js.es6 | 3 +- .../discourse/routes/user-activity.js.es6 | 4 +- .../discourse/routes/user-invited-show.js.es6 | 3 +- .../discourse/views/choose-topic.js.es6 | 3 +- .../discourse/views/composer.js.es6 | 5 +- .../javascripts/discourse/views/post.js.es6 | 3 +- app/assets/javascripts/main_include.js | 5 + app/assets/javascripts/vendor.js | 4 +- lib/pretty_text.rb | 1 - .../acceptance/category-edit-test.js.es6 | 5 +- .../components/keyboard-shortcuts-test.js.es6 | 43 ++--- .../helpers/custom-html-test.js.es6 | 14 ++ test/javascripts/helpers/parse-html.js.es6 | 8 + test/javascripts/helpers/parse_html.js | 9 -- test/javascripts/lib/bbcode-test.js.es6 | 4 +- .../lib/category-badge-test.js.es6 | 1 + test/javascripts/lib/click-track-test.js.es6 | 19 +-- test/javascripts/lib/html-test.js.es6 | 14 -- test/javascripts/lib/url-test.js.es6 | 20 +-- test/javascripts/models/invite-test.js.es6 | 6 +- test/javascripts/test_helper.js | 9 +- .../assets/javascripts}/caret_position.js | 0 .../assets/javascripts}/div_resizer.js | 0 .../assets/javascripts}/probes.js | 0 78 files changed, 419 insertions(+), 387 deletions(-) create mode 100644 app/assets/javascripts/discourse/initializers/es6-deprecations.js.es6 rename app/assets/javascripts/discourse/lib/{debounce.js => debounce.js.es6} (82%) delete mode 100644 app/assets/javascripts/discourse/lib/html.js rename app/assets/javascripts/discourse/lib/{keyboard_shortcuts.js => keyboard-shortcuts.js.es6} (70%) rename app/assets/javascripts/discourse/lib/{quote.js => quote.js.es6} (98%) rename app/assets/javascripts/discourse/lib/{url.js => url.js.es6} (82%) rename app/assets/javascripts/discourse/models/{draft.js => draft.js.es6} (61%) rename app/assets/javascripts/discourse/models/{invite.js => invite.js.es6} (66%) delete mode 100644 app/assets/javascripts/discourse/models/selectable_array.js create mode 100644 test/javascripts/helpers/custom-html-test.js.es6 create mode 100644 test/javascripts/helpers/parse-html.js.es6 delete mode 100644 test/javascripts/helpers/parse_html.js delete mode 100644 test/javascripts/lib/html-test.js.es6 rename {app/assets/javascripts/discourse/lib => vendor/assets/javascripts}/caret_position.js (100%) rename {app/assets/javascripts/discourse/lib => vendor/assets/javascripts}/div_resizer.js (100%) rename {app/assets/javascripts/discourse/lib => vendor/assets/javascripts}/probes.js (100%) diff --git a/.jshintrc b/.jshintrc index 55cc59d193..6be05281b4 100644 --- a/.jshintrc +++ b/.jshintrc @@ -46,7 +46,6 @@ "_", "alert", "containsInstance", - "parseHTML", "deepEqual", "notEqual", "define", diff --git a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 index 0b23e90268..e00bfba61f 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 @@ -1,8 +1,9 @@ import DiscourseController from 'discourse/controllers/controller'; +import debounce from 'discourse/lib/debounce'; export default DiscourseController.extend({ - filterEmailLogs: Discourse.debounce(function() { + filterEmailLogs: debounce(function() { var self = this; Discourse.EmailLog.findAll(this.get("filter")).then(function(logs) { self.set("model", logs); diff --git a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 index 9e3affff6a..06cedc6dd3 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 @@ -1,7 +1,8 @@ +import debounce from 'discourse/lib/debounce'; import DiscourseController from 'discourse/controllers/controller'; export default DiscourseController.extend({ - filterEmailLogs: Discourse.debounce(function() { + filterEmailLogs: debounce(function() { var self = this; Discourse.EmailLog.findAll(this.get("filter")).then(function(logs) { self.set("model", logs); diff --git a/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 b/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 index 0ae12ff2c5..987b07a3b6 100644 --- a/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-logs-screened-ip-addresses.js.es6 @@ -1,3 +1,4 @@ +import debounce from 'discourse/lib/debounce'; import { outputExportResult } from 'discourse/lib/export-result'; import { exportEntity } from 'discourse/lib/export-csv'; @@ -6,7 +7,7 @@ export default Ember.ArrayController.extend({ itemController: 'admin-log-screened-ip-address', filter: null, - show: Discourse.debounce(function() { + show: debounce(function() { var self = this; self.set('loading', true); Discourse.ScreenedIpAddress.findAll(this.get("filter")).then(function(result) { diff --git a/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6 b/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6 index e03e5ebda4..a0d38e7b8c 100644 --- a/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-permalinks.js.es6 @@ -1,8 +1,10 @@ +import debounce from 'discourse/lib/debounce'; + export default Ember.ArrayController.extend({ loading: false, filter: null, - show: Discourse.debounce(function() { + show: debounce(function() { var self = this; self.set('loading', true); Discourse.Permalink.findAll(self.get("filter")).then(function(result) { diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 index da32c3f5eb..d89e55c991 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 @@ -1,3 +1,4 @@ +import debounce from 'discourse/lib/debounce'; import Presence from 'discourse/mixins/presence'; export default Ember.ArrayController.extend(Presence, { @@ -50,7 +51,7 @@ export default Ember.ArrayController.extend(Presence, { this.transitionToRoute("adminSiteSettingsCategory", category || "all_results"); }, - filterContent: Discourse.debounce(function() { + filterContent: debounce(function() { if (this.get("_skipBounce")) { this.set("_skipBounce", false); } else { diff --git a/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 index dcca2e31ba..a0eaf6e162 100644 --- a/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-users-list-show.js.es6 @@ -1,3 +1,4 @@ +import debounce from 'discourse/lib/debounce'; import { i18n } from 'discourse/lib/computed'; export default Ember.ArrayController.extend({ @@ -33,7 +34,7 @@ export default Ember.ArrayController.extend({ return I18n.t('admin.users.titles.' + this.get('query')); }.property('query'), - _filterUsers: Discourse.debounce(function() { + _filterUsers: debounce(function() { this._refreshUsers(); }, 250).observes('listFilter'), 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 d4f03cd138..98f7c3dfe2 100644 --- a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 @@ -1,3 +1,4 @@ +import debounce from 'discourse/lib/debounce'; import { renderSpinner } from 'discourse/helpers/loading-spinner'; export default Discourse.View.extend({ @@ -9,7 +10,7 @@ export default Discourse.View.extend({ this.setProperties({ formattedLogs: "", index: 0 }); }, - _updateFormattedLogs: Discourse.debounce(function() { + _updateFormattedLogs: debounce(function() { const logs = this.get("controller.model"); if (logs.length === 0) { this._reset(); // reset the cached logs whenever the model is reset diff --git a/app/assets/javascripts/admin/views/admin-backups.js.es6 b/app/assets/javascripts/admin/views/admin-backups.js.es6 index f0b9f108d7..f0adae194e 100644 --- a/app/assets/javascripts/admin/views/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + export default Discourse.View.extend({ classNames: ["admin-backups"], @@ -12,7 +14,7 @@ export default Discourse.View.extend({ $link.data("auto-route", true); } - Discourse.URL.redirectTo($link.data("href")); + DiscourseURL.redirectTo($link.data("href")); }); }.on("didInsertElement"), diff --git a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 index 66cff47730..5d323ce06a 100644 --- a/app/assets/javascripts/discourse/components/edit-category-general.js.es6 +++ b/app/assets/javascripts/discourse/components/edit-category-general.js.es6 @@ -1,3 +1,4 @@ +import DiscourseURL from 'discourse/lib/url'; import { buildCategoryPanel } from 'discourse/components/edit-category-panel'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; @@ -53,7 +54,7 @@ export default buildCategoryPanel('general', { actions: { showCategoryTopic() { - Discourse.URL.routeTo(this.get('category.topic_url')); + DiscourseURL.routeTo(this.get('category.topic_url')); return false; } } diff --git a/app/assets/javascripts/discourse/components/header-extra-info.js.es6 b/app/assets/javascripts/discourse/components/header-extra-info.js.es6 index 8948e24c95..e0c481ef18 100644 --- a/app/assets/javascripts/discourse/components/header-extra-info.js.es6 +++ b/app/assets/javascripts/discourse/components/header-extra-info.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + const TopicCategoryComponent = Ember.Component.extend({ needsSecondRow: Ember.computed.gt('secondRowItems.length', 0), secondRowItems: function() { return []; }.property(), @@ -10,7 +12,7 @@ const TopicCategoryComponent = Ember.Component.extend({ jumpToTopPost() { const topic = this.get('topic'); if (topic) { - Discourse.URL.routeTo(topic.get('firstPostUrl')); + DiscourseURL.routeTo(topic.get('firstPostUrl')); } } } diff --git a/app/assets/javascripts/discourse/components/home-logo.js.es6 b/app/assets/javascripts/discourse/components/home-logo.js.es6 index d4b429ef03..d2deabb8ae 100644 --- a/app/assets/javascripts/discourse/components/home-logo.js.es6 +++ b/app/assets/javascripts/discourse/components/home-logo.js.es6 @@ -1,3 +1,4 @@ +import DiscourseURL from 'discourse/lib/url'; import { setting } from 'discourse/lib/computed'; export default Ember.Component.extend({ @@ -26,7 +27,7 @@ export default Ember.Component.extend({ e.preventDefault(); - Discourse.URL.routeTo('/'); + DiscourseURL.routeTo('/'); return false; } }); diff --git a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 index 3cf9a741f1..a7ceba10cc 100644 --- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + export default Ember.Component.extend({ tagName: 'ul', classNameBindings: [':nav', ':nav-pills'], @@ -24,7 +26,7 @@ export default Ember.Component.extend({ this.set('expanded',false); } $(window).off('click.navigation-bar'); - Discourse.URL.appEvents.off('dom:clean', this, this.ensureDropClosed); + DiscourseURL.appEvents.off('dom:clean', this, this.ensureDropClosed); }, actions: { @@ -33,7 +35,7 @@ export default Ember.Component.extend({ var self = this; if (this.get('expanded')) { - Discourse.URL.appEvents.on('dom:clean', this, this.ensureDropClosed); + DiscourseURL.appEvents.on('dom:clean', this, this.ensureDropClosed); Em.run.next(function() { diff --git a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 b/app/assets/javascripts/discourse/controllers/change-owner.js.es6 index 1e11a1ccfb..611498268d 100644 --- a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 +++ b/app/assets/javascripts/discourse/controllers/change-owner.js.es6 @@ -1,6 +1,7 @@ import Presence from 'discourse/mixins/presence'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import DiscourseURL from 'discourse/lib/url'; // Modal related to changing the ownership of posts export default Ember.Controller.extend(Presence, SelectedPostsCount, ModalFunctionality, { @@ -43,7 +44,7 @@ export default Ember.Controller.extend(Presence, SelectedPostsCount, ModalFuncti // success self.send('closeModal'); self.get('topicController').send('toggleMultiSelect'); - Em.run.next(function() { Discourse.URL.routeTo(result.url); }); + Em.run.next(function() { DiscourseURL.routeTo(result.url); }); }, function() { // failure self.flash(I18n.t('topic.change_owner.error'), 'alert-error'); diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index f724a6cbce..c3a8a6827a 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -1,5 +1,8 @@ import { setting } from 'discourse/lib/computed'; import Presence from 'discourse/mixins/presence'; +import DiscourseURL from 'discourse/lib/url'; +import Quote from 'discourse/lib/quote'; +import Draft from 'discourse/models/draft'; export default Ember.ObjectController.extend(Presence, { needs: ['modal', 'topic', 'composer-messages', 'application'], @@ -72,7 +75,7 @@ export default Ember.ObjectController.extend(Presence, { const composer = this; return this.store.find('post', postId).then(function(post) { - const quote = Discourse.Quote.build(post, post.get("raw"), {raw: true, full: true}); + const quote = Quote.build(post, post.get("raw"), {raw: true, full: true}); composer.appendBlockAtCursor(quote); composer.set('model.loading', false); }); @@ -262,7 +265,7 @@ export default Ember.ObjectController.extend(Presence, { if (!composer.get('replyingToTopic') || !disableJumpReply) { const post = result.target; if (post && !staged) { - Discourse.URL.routeTo(post.get('url')); + DiscourseURL.routeTo(post.get('url')); } } }).catch(function(error) { @@ -278,7 +281,7 @@ export default Ember.ObjectController.extend(Presence, { Em.run.schedule('afterRender', function() { if (staged && !disableJumpReply) { const postNumber = staged.get('post_number'); - Discourse.URL.jumpToPost(postNumber, { skipIfOnScreen: true }); + DiscourseURL.jumpToPost(postNumber, { skipIfOnScreen: true }); self.appEvents.trigger('post:highlight', postNumber); } }); @@ -415,7 +418,7 @@ export default Ember.ObjectController.extend(Presence, { // we need a draft sequence for the composer to work if (opts.draftSequence === undefined) { - return Discourse.Draft.get(opts.draftKey).then(function(data) { + return Draft.get(opts.draftKey).then(function(data) { opts.draftSequence = data.draft_sequence; opts.draft = data.draft; self._setModel(composerModel, opts); @@ -477,7 +480,7 @@ export default Ember.ObjectController.extend(Presence, { // View a new reply we've made viewNewReply() { - Discourse.URL.routeTo(this.get('model.createdPost.url')); + DiscourseURL.routeTo(this.get('model.createdPost.url')); this.close(); return false; }, @@ -485,7 +488,7 @@ export default Ember.ObjectController.extend(Presence, { destroyDraft() { const key = this.get('model.draftKey'); if (key) { - Discourse.Draft.clear(key, this.get('model.draftSequence')); + Draft.clear(key, this.get('model.draftSequence')); } }, diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index d8995ad6fe..052a8bd4e2 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 debounce from 'discourse/lib/debounce'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import DiscourseController from 'discourse/controllers/controller'; import { setting } from 'discourse/lib/computed'; @@ -151,7 +152,7 @@ export default DiscourseController.extend(ModalFunctionality, { } }.observes('emailValidation', 'accountEmail'), - fetchExistingUsername: Discourse.debounce(function() { + fetchExistingUsername: debounce(function() { const self = this; Discourse.User.checkUsername(null, this.get('accountEmail')).then(function(result) { if (result.suggestion && (self.blank('accountUsername') || self.get('accountUsername') === self.get('authOptions.username'))) { @@ -227,7 +228,7 @@ export default DiscourseController.extend(ModalFunctionality, { return !this.blank('accountUsername') && this.get('accountUsername').length >= this.get('minUsernameLength'); }, - checkUsernameAvailability: Discourse.debounce(function() { + checkUsernameAvailability: debounce(function() { const _this = this; if (this.shouldCheckUsernameMatch()) { return Discourse.User.checkUsername(this.get('accountUsername'), this.get('accountEmail')).then(function(result) { diff --git a/app/assets/javascripts/discourse/controllers/discovery.js.es6 b/app/assets/javascripts/discourse/controllers/discovery.js.es6 index 5276b912d6..69e51d55d5 100644 --- a/app/assets/javascripts/discourse/controllers/discovery.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + export default Ember.ObjectController.extend({ needs: ['navigation/category', 'discovery/topics', 'application'], loading: false, @@ -22,7 +24,7 @@ export default Ember.ObjectController.extend({ actions: { changePeriod(p) { - Discourse.URL.routeTo(this.showMoreUrl(p)); + DiscourseURL.routeTo(this.showMoreUrl(p)); } } diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 index 8bd47259b9..9e7c637d61 100644 --- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 @@ -1,5 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ObjectController from 'discourse/controllers/object'; +import DiscourseURL from 'discourse/lib/url'; // Modal for editing / creating a category export default ObjectController.extend(ModalFunctionality, { @@ -71,7 +72,7 @@ export default ObjectController.extend(ModalFunctionality, { this.get('model').save().then(function(result) { self.send('closeModal'); model.setProperties({slug: result.category.slug, id: result.category.id }); - Discourse.URL.redirectTo("/c/" + Discourse.Category.slugFor(model)); + DiscourseURL.redirectTo("/c/" + Discourse.Category.slugFor(model)); }).catch(function(error) { if (error && error.responseText) { self.flash($.parseJSON(error.responseText).errors[0], 'error'); @@ -92,7 +93,7 @@ export default ObjectController.extend(ModalFunctionality, { self.get('model').destroy().then(function(){ // success self.send('closeModal'); - Discourse.URL.redirectTo("/categories"); + DiscourseURL.redirectTo("/categories"); }, function(error){ if (error && error.responseText) { diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index 048f984658..660bcdc89e 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -1,6 +1,7 @@ import Presence from 'discourse/mixins/presence'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import ObjectController from 'discourse/controllers/object'; +import Invite from 'discourse/models/invite'; export default ObjectController.extend(Presence, ModalFunctionality, { needs: ['user-invited-show'], @@ -140,7 +141,7 @@ export default ObjectController.extend(Presence, ModalFunctionality, { return this.get('model').createInvite(this.get('emailOrUsername').trim(), groupNames).then(result => { model.setProperties({ saving: false, finished: true }); if (!this.get('invitingToTopic')) { - Discourse.Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => { + Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => { userInvitedController.set('model', invite_model); userInvitedController.set('totalInvites', invite_model.invites.length); }); diff --git a/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 b/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 index 44f374ddbf..f81e0de676 100644 --- a/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 @@ -2,6 +2,7 @@ import Presence from 'discourse/mixins/presence'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { movePosts, mergeTopic } from 'discourse/models/topic'; +import DiscourseURL from 'discourse/lib/url'; // Modal related to merging of topics export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, Presence, { @@ -54,7 +55,7 @@ export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, P // Posts moved self.send('closeModal'); self.get('topicController').send('toggleMultiSelect'); - Em.run.next(function() { Discourse.URL.routeTo(result.url); }); + Em.run.next(function() { DiscourseURL.routeTo(result.url); }); }).catch(function() { self.flash(I18n.t('topic.merge_topic.error')); }).finally(function() { diff --git a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 index f74250976b..410fabe4d5 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 @@ -1,6 +1,7 @@ import { setting, propertyEqual } from 'discourse/lib/computed'; import Presence from 'discourse/mixins/presence'; import ObjectController from 'discourse/controllers/object'; +import DiscourseURL from 'discourse/lib/url'; export default ObjectController.extend(Presence, { taken: false, @@ -46,7 +47,7 @@ export default ObjectController.extend(Presence, { if (result) { self.set('saving', true); self.get('content').changeUsername(self.get('newUsername')).then(function() { - Discourse.URL.redirectTo("/users/" + self.get('newUsername').toLowerCase() + "/preferences"); + DiscourseURL.redirectTo("/users/" + self.get('newUsername').toLowerCase() + "/preferences"); }, function() { // error self.set('error', true); diff --git a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 index c99f57247d..bb255f8d2f 100644 --- a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 @@ -1,5 +1,6 @@ import DiscourseController from 'discourse/controllers/controller'; import loadScript from 'discourse/lib/load-script'; +import Quote from 'discourse/lib/quote'; export default DiscourseController.extend({ needs: ['topic', 'composer'], @@ -114,7 +115,7 @@ export default DiscourseController.extend({ } const buffer = this.get('buffer'); - const quotedText = Discourse.Quote.build(post, buffer); + const quotedText = Quote.build(post, buffer); composerOpts.quote = quotedText; if (composerController.get('content.viewOpen') || composerController.get('content.viewDraft')) { composerController.appendBlockAtCursor(quotedText.trim()); diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 82f893f050..71d650726d 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -1,5 +1,6 @@ import Presence from 'discourse/mixins/presence'; import searchForTerm from 'discourse/lib/search-for-term'; +import DiscourseURL from 'discourse/lib/url'; let _dontSearch = false; @@ -138,7 +139,7 @@ export default Em.Controller.extend(Presence, { const url = this.get('fullSearchUrlRelative'); if (url) { - Discourse.URL.routeTo(url); + DiscourseURL.routeTo(url); } }, diff --git a/app/assets/javascripts/discourse/controllers/split-topic.js.es6 b/app/assets/javascripts/discourse/controllers/split-topic.js.es6 index 0c75ae3503..dc391497c4 100644 --- a/app/assets/javascripts/discourse/controllers/split-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/split-topic.js.es6 @@ -3,6 +3,7 @@ import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { extractError } from 'discourse/lib/ajax-error'; import { movePosts } from 'discourse/models/topic'; +import DiscourseURL from 'discourse/lib/url'; // Modal related to auto closing of topics export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, Presence, { @@ -55,7 +56,7 @@ export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, P // Posts moved self.send('closeModal'); self.get('topicController').send('toggleMultiSelect'); - Ember.run.next(function() { Discourse.URL.routeTo(result.url); }); + Ember.run.next(function() { DiscourseURL.routeTo(result.url); }); }).catch(function(xhr) { self.flash(extractError(xhr, I18n.t('topic.split_topic.error'))); }).finally(function() { diff --git a/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 b/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 index b98b14550d..4f453b98f1 100644 --- a/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-entrance.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + function entranceDate(dt, showTime) { var today = new Date(); @@ -51,11 +53,11 @@ export default Ember.Controller.extend({ }, enterTop: function() { - Discourse.URL.routeTo(this.get('model.url')); + DiscourseURL.routeTo(this.get('model.url')); }, enterBottom: function() { - Discourse.URL.routeTo(this.get('model.lastPostUrl')); + DiscourseURL.routeTo(this.get('model.lastPostUrl')); } } }); diff --git a/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 b/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 index 7763157fe6..92f483df75 100644 --- a/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + export default Ember.ObjectController.extend({ needs: ['topic'], progressPosition: null, @@ -62,7 +64,7 @@ export default Ember.ObjectController.extend({ // Route and close the expansion jumpTo: function(url) { this.set('expanded', false); - Discourse.URL.routeTo(url); + DiscourseURL.routeTo(url); }, streamPercentage: function() { diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 5b9e0ae778..545549ffcb 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -3,6 +3,7 @@ import BufferedContent from 'discourse/mixins/buffered-content'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import { spinnerHTML } from 'discourse/helpers/loading-spinner'; import Topic from 'discourse/models/topic'; +import Quote from 'discourse/lib/quote'; import { setting } from 'discourse/lib/computed'; export default ObjectController.extend(SelectedPostsCount, BufferedContent, { @@ -109,7 +110,7 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, { replyToPost(post) { const composerController = this.get('controllers.composer'), quoteController = this.get('controllers.quote-button'), - quotedText = Discourse.Quote.build(quoteController.get('post'), quoteController.get('buffer')), + quotedText = Quote.build(quoteController.get('post'), quoteController.get('buffer')), topic = post ? post.get('topic') : this.get('model'); quoteController.set('buffer', ''); @@ -412,7 +413,7 @@ export default ObjectController.extend(SelectedPostsCount, BufferedContent, { replyAsNewTopic(post) { const composerController = this.get('controllers.composer'), quoteController = this.get('controllers.quote-button'), - quotedText = Discourse.Quote.build(quoteController.get('post'), quoteController.get('buffer')), + quotedText = Quote.build(quoteController.get('post'), quoteController.get('buffer')), self = this; quoteController.deselectText(); diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index 3279137dbf..1a4c3ad6d1 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -1,3 +1,4 @@ +import DiscourseURL from 'discourse/lib/url'; import { propertyNotEqual, setting } from 'discourse/lib/computed'; export default Ember.Controller.extend({ @@ -41,7 +42,7 @@ export default Ember.Controller.extend({ // Don't show on mobile if (Discourse.Mobile.mobileView) { const url = "/users/" + username; - Discourse.URL.routeTo(url); + DiscourseURL.routeTo(url); return; } diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index 7e9111a108..79131dbf10 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -1,3 +1,6 @@ +import Invite from 'discourse/models/invite'; +import debounce from 'discourse/lib/debounce'; + // This controller handles actions related to a user's invitations export default Ember.ObjectController.extend({ user: null, @@ -19,9 +22,9 @@ export default Ember.ObjectController.extend({ @observes searchTerm **/ - _searchTermChanged: Discourse.debounce(function() { + _searchTermChanged: debounce(function() { var self = this; - Discourse.Invite.findInvitedBy(self.get('user'), this.get('filter'), this.get('searchTerm')).then(function (invites) { + Invite.findInvitedBy(self.get('user'), this.get('filter'), this.get('searchTerm')).then(function (invites) { self.set('model', invites); }); }, 250).observes('searchTerm'), @@ -57,35 +60,23 @@ export default Ember.ObjectController.extend({ actions: { - /** - Rescind a given invite - - @method rescive - @param {Discourse.Invite} invite the invite to rescind. - **/ - rescind: function(invite) { + rescind(invite) { invite.rescind(); return false; }, - /** - Resend a given invite - - @method reinvite - @param {Discourse.Invite} invite the invite to resend. - **/ - reinvite: function(invite) { + reinvite(invite) { invite.reinvite(); return false; }, - loadMore: function() { + loadMore() { var self = this; var model = self.get('model'); if (self.get('canLoadMore') && !self.get('invitesLoading')) { self.set('invitesLoading', true); - Discourse.Invite.findInvitedBy(self.get('user'), self.get('filter'), self.get('searchTerm'), model.invites.length).then(function(invite_model) { + Invite.findInvitedBy(self.get('user'), self.get('filter'), self.get('searchTerm'), model.invites.length).then(function(invite_model) { self.set('invitesLoading', false); model.invites.pushObjects(invite_model.invites); if(invite_model.invites.length === 0 || invite_model.invites.length < Discourse.SiteSettings.invites_per_page) { diff --git a/app/assets/javascripts/discourse/controllers/users.js.es6 b/app/assets/javascripts/discourse/controllers/users.js.es6 index 8705fb850b..8ad3eabcba 100644 --- a/app/assets/javascripts/discourse/controllers/users.js.es6 +++ b/app/assets/javascripts/discourse/controllers/users.js.es6 @@ -1,3 +1,5 @@ +import debounce from 'discourse/lib/debounce'; + export default Ember.Controller.extend({ needs: ["application"], queryParams: ["period", "order", "asc", "name"], @@ -8,7 +10,7 @@ export default Ember.Controller.extend({ showTimeRead: Ember.computed.equal("period", "all"), - _setName: Discourse.debounce(function() { + _setName: debounce(function() { this.set("name", this.get("nameInput")); }, 500).observes("nameInput"), diff --git a/app/assets/javascripts/discourse/helpers/custom-html.js.es6 b/app/assets/javascripts/discourse/helpers/custom-html.js.es6 index 7929888dc0..90427ff625 100644 --- a/app/assets/javascripts/discourse/helpers/custom-html.js.es6 +++ b/app/assets/javascripts/discourse/helpers/custom-html.js.es6 @@ -1,6 +1,25 @@ +const _customizations = {}; + +export function getCustomHTML(key) { + const c = _customizations[key]; + if (c) { + return new Handlebars.SafeString(c); + } + + const html = PreloadStore.get("customHTML"); + if (html && html[key] && html[key].length) { + return new Handlebars.SafeString(html[key]); + } +} + +// Set a fragment of HTML by key. It can then be looked up with `getCustomHTML(key)`. +export function setCustomHTML(key, html) { + _customizations[key] = html; +} + Ember.HTMLBars._registerHelper('custom-html', function(params, hash, options, env) { const name = params[0]; - const html = Discourse.HTML.getCustomHTML(name); + const html = getCustomHTML(name); if (html) { return html; } const contextString = params[1]; diff --git a/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 b/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 index d788a0a417..5c04f67d01 100644 --- a/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 +++ b/app/assets/javascripts/discourse/initializers/click-interceptor.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + /** Discourse does some server side rendering of HTML, such as the `cooked` contents of posts. The downside of this in an Ember app is the links will not go through the router. @@ -28,7 +30,7 @@ export default { } e.preventDefault(); - Discourse.URL.routeTo(href); + DiscourseURL.routeTo(href); return false; }); } diff --git a/app/assets/javascripts/discourse/initializers/es6-deprecations.js.es6 b/app/assets/javascripts/discourse/initializers/es6-deprecations.js.es6 new file mode 100644 index 0000000000..93b6d61b65 --- /dev/null +++ b/app/assets/javascripts/discourse/initializers/es6-deprecations.js.es6 @@ -0,0 +1,27 @@ +import DiscourseURL from 'discourse/lib/url'; +import Quote from 'discourse/lib/quote'; +import debounce from 'discourse/lib/debounce'; + +function proxyDep(propName, module) { + if (Discourse.hasOwnProperty(propName)) { return; } + Object.defineProperty(Discourse, propName, { + get: function() { + Ember.warn(`DEPRECATION: \`Discourse.${propName}\` is deprecated, import the module.`); + return module; + } + }); +} + +export default { + name: 'es6-deprecations', + before: 'inject-objects', + + initialize: function() { + // TODO: Once things have migrated remove these + proxyDep('computed', require('discourse/lib/computed')); + proxyDep('Formatter', require('discourse/lib/formatter')); + proxyDep('URL', DiscourseURL); + proxyDep('Quote', Quote); + proxyDep('debounce', debounce); + } +}; diff --git a/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 b/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 index a3aceef950..197eb06d6b 100644 --- a/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 +++ b/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 @@ -1,6 +1,7 @@ import Session from 'discourse/models/session'; import AppEvents from 'discourse/lib/app-events'; import Store from 'discourse/models/store'; +import DiscourseURL from 'discourse/lib/url'; function inject() { const app = arguments[0], @@ -22,7 +23,7 @@ export default { const appEvents = AppEvents.create(); app.register('app-events:main', appEvents, { instantiate: false }); injectAll(app, 'appEvents'); - Discourse.URL.appEvents = appEvents; + DiscourseURL.appEvents = appEvents; app.register('store:main', Store); inject(app, 'store', 'route', 'controller'); diff --git a/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js.es6 b/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js.es6 index 5ae89cdf2c..41485db6e6 100644 --- a/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js.es6 +++ b/app/assets/javascripts/discourse/initializers/keyboard-shortcuts.js.es6 @@ -1,11 +1,9 @@ /*global Mousetrap:true*/ +import KeyboardShortcuts from 'discourse/lib/keyboard-shortcuts'; -/** - Initialize Global Keyboard Shortcuts -**/ export default { name: "keyboard-shortcuts", - initialize: function(container) { - Discourse.KeyboardShortcuts.bindEvents(Mousetrap, container); + initialize(container) { + KeyboardShortcuts.bindEvents(Mousetrap, container); } }; diff --git a/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 b/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 index fa8514e6d5..102e95f971 100644 --- a/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 +++ b/app/assets/javascripts/discourse/initializers/url-redirects.js.es6 @@ -1,11 +1,13 @@ +import DiscourseURL from 'discourse/lib/url'; + export default { name: 'url-redirects', initialize: function() { // URL rewrites (usually due to refactoring) - Discourse.URL.rewrite(/^\/category\//, "/c/"); - Discourse.URL.rewrite(/^\/group\//, "/groups/"); - Discourse.URL.rewrite(/\/private-messages\/$/, "/messages/"); - Discourse.URL.rewrite(/^\/users\/([^\/]+)\/?$/, "/users/$1/activity"); + DiscourseURL.rewrite(/^\/category\//, "/c/"); + DiscourseURL.rewrite(/^\/group\//, "/groups/"); + DiscourseURL.rewrite(/\/private-messages\/$/, "/messages/"); + DiscourseURL.rewrite(/^\/users\/([^\/]+)\/?$/, "/users/$1/activity"); } }; diff --git a/app/assets/javascripts/discourse/lib/click-track.js.es6 b/app/assets/javascripts/discourse/lib/click-track.js.es6 index 9195ec12b7..65800a3750 100644 --- a/app/assets/javascripts/discourse/lib/click-track.js.es6 +++ b/app/assets/javascripts/discourse/lib/click-track.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + export default { trackClick(e) { // cancel click if triggered as part of selection. @@ -87,7 +89,7 @@ export default { } // If we're on the same site, use the router and track via AJAX - if (Discourse.URL.isInternal(href) && !$link.hasClass('attachment')) { + if (DiscourseURL.isInternal(href) && !$link.hasClass('attachment')) { Discourse.ajax("/clicks/track", { data: { url: href, @@ -97,7 +99,7 @@ export default { }, dataType: 'html' }); - Discourse.URL.routeTo(href); + DiscourseURL.routeTo(href); return false; } @@ -106,7 +108,7 @@ export default { var win = window.open(trackingUrl, '_blank'); win.focus(); } else { - Discourse.URL.redirectTo(trackingUrl); + DiscourseURL.redirectTo(trackingUrl); } return false; diff --git a/app/assets/javascripts/discourse/lib/debounce.js b/app/assets/javascripts/discourse/lib/debounce.js.es6 similarity index 82% rename from app/assets/javascripts/discourse/lib/debounce.js rename to app/assets/javascripts/discourse/lib/debounce.js.es6 index 583e8bb9b4..6ab2eb1e58 100644 --- a/app/assets/javascripts/discourse/lib/debounce.js +++ b/app/assets/javascripts/discourse/lib/debounce.js.es6 @@ -3,9 +3,9 @@ should only be executed once (at the end of the limit counted from the last call made). Original function will be called with the context and arguments from the last call made. **/ -Discourse.debounce = function(func, wait) { - var self, args; - var later = function() { +export default function(func, wait) { + let self, args; + const later = function() { func.apply(self, args); }; @@ -15,4 +15,4 @@ Discourse.debounce = function(func, wait) { Ember.run.debounce(null, later, wait); }; -}; +} diff --git a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 index 5bdcf40d86..a0e2cc7d23 100644 --- a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 +++ b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 @@ -1,3 +1,4 @@ +import DiscourseURL from 'discourse/lib/url'; import PageTracker from 'discourse/lib/page-tracker'; let primaryTab = false; @@ -116,7 +117,7 @@ function onNotification(data) { }); function clickEventHandler() { - Discourse.URL.routeTo(data.post_url); + DiscourseURL.routeTo(data.post_url); // Cannot delay this until the page renders // due to trigger-based permissions window.focus(); diff --git a/app/assets/javascripts/discourse/lib/html.js b/app/assets/javascripts/discourse/lib/html.js deleted file mode 100644 index ccb807d6a8..0000000000 --- a/app/assets/javascripts/discourse/lib/html.js +++ /dev/null @@ -1,28 +0,0 @@ -var customizations = {}; - -Discourse.HTML = { - - /** - Return a custom fragment of HTML by key. It can be registered via a plugin - using `setCustomHTML(key, html)`. This is used by a handlebars helper to find - the HTML content it wants. It will also check the `PreloadStore` for any server - side preloaded HTML. - **/ - getCustomHTML: function(key) { - var c = customizations[key]; - if (c) { - return new Handlebars.SafeString(c); - } - - var html = PreloadStore.get("customHTML"); - if (html && html[key] && html[key].length) { - return new Handlebars.SafeString(html[key]); - } - }, - - // Set a fragment of HTML by key. It can then be looked up with `getCustomHTML(key)`. - setCustomHTML: function(key, html) { - customizations[key] = html; - } - -}; diff --git a/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 similarity index 70% rename from app/assets/javascripts/discourse/lib/keyboard_shortcuts.js rename to app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 index 1164e44968..2075a57ce6 100644 --- a/app/assets/javascripts/discourse/lib/keyboard_shortcuts.js +++ b/app/assets/javascripts/discourse/lib/keyboard-shortcuts.js.es6 @@ -1,4 +1,6 @@ -var PATH_BINDINGS = { +import DiscourseURL from 'discourse/lib/url'; + +const PATH_BINDINGS = { 'g h': '/', 'g l': '/latest', 'g n': '/new', @@ -55,8 +57,8 @@ var PATH_BINDINGS = { }; -Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ - bindEvents: function(keyTrapper, container) { +export default { + bindEvents(keyTrapper, container) { this.keyTrapper = keyTrapper; this.container = container; this._stopCallback(); @@ -67,13 +69,13 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ _.each(FUNCTION_BINDINGS, this._bindToFunction, this); }, - toggleBookmark: function(){ + toggleBookmark(){ this.sendToSelectedPost('toggleBookmark'); this.sendToTopicListItemView('toggleBookmark'); }, - toggleBookmarkTopic: function(){ - var topic = this.currentTopic(); + toggleBookmarkTopic(){ + const topic = this.currentTopic(); // BIG hack, need a cleaner way if(topic && $('.posts-wrapper').length > 0) { topic.toggleBookmark(); @@ -82,7 +84,7 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ } }, - quoteReply: function(){ + quoteReply(){ $('.topic-post.selected button.create').click(); // lazy but should work for now setTimeout(function(){ @@ -90,55 +92,55 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ }, 500); }, - goToFirstPost: function() { + goToFirstPost() { this._jumpTo('jumpTop'); }, - goToLastPost: function() { + goToLastPost() { this._jumpTo('jumpBottom'); }, - _jumpTo: function(direction) { + _jumpTo(direction) { if ($('.container.posts').length) { this.container.lookup('controller:topic-progress').send(direction); } }, - replyToTopic: function() { + replyToTopic() { this.container.lookup('controller:topic').send('replyToPost'); }, - selectDown: function() { + selectDown() { this._moveSelection(1); }, - selectUp: function() { + selectUp() { this._moveSelection(-1); }, - goBack: function() { + goBack() { history.back(); }, - nextSection: function() { + nextSection() { this._changeSection(1); }, - prevSection: function() { + prevSection() { this._changeSection(-1); }, - showBuiltinSearch: function() { + showBuiltinSearch() { if ($('#search-dropdown').is(':visible')) { this._toggleSearch(false); return true; } - var currentPath = this.container.lookup('controller:application').get('currentPath'), - blacklist = [ /^discovery\.categories/ ], - whitelist = [ /^topic\./ ], - check = function(regex) { return !!currentPath.match(regex); }, - showSearch = whitelist.any(check) && !blacklist.any(check); + const currentPath = this.container.lookup('controller:application').get('currentPath'), + blacklist = [ /^discovery\.categories/ ], + whitelist = [ /^topic\./ ], + check = function(regex) { return !!currentPath.match(regex); }; + let showSearch = whitelist.any(check) && !blacklist.any(check); // If we're viewing a topic, only intercept search if there are cloaked posts if (showSearch && currentPath.match(/^topic\./)) { @@ -153,61 +155,61 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ return true; }, - createTopic: function() { - Discourse.__container__.lookup('controller:composer').open({action: Discourse.Composer.CREATE_TOPIC, draftKey: Discourse.Composer.CREATE_TOPIC}); + createTopic() { + this.container.lookup('controller:composer').open({action: Discourse.Composer.CREATE_TOPIC, draftKey: Discourse.Composer.CREATE_TOPIC}); }, - pinUnpinTopic: function() { - Discourse.__container__.lookup('controller:topic').togglePinnedState(); + pinUnpinTopic() { + this.container.lookup('controller:topic').togglePinnedState(); }, - toggleProgress: function() { - Discourse.__container__.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true}); + toggleProgress() { + this.container.lookup('controller:topic-progress').send('toggleExpansion', {highlight: true}); }, - showSearch: function() { + showSearch() { this._toggleSearch(false); return false; }, - showSiteMap: function() { + showSiteMap() { $('#site-map').click(); $('#site-map-dropdown a:first').focus(); }, - showCurrentUser: function() { + showCurrentUser() { $('#current-user').click(); $('#user-dropdown a:first').focus(); }, - showHelpModal: function() { - Discourse.__container__.lookup('controller:application').send('showKeyboardShortcutsHelp'); + showHelpModal() { + this.container.lookup('controller:application').send('showKeyboardShortcutsHelp'); }, - sendToTopicListItemView: function(action){ - var elem = $('tr.selected.topic-list-item.ember-view')[0]; + sendToTopicListItemView(action){ + const elem = $('tr.selected.topic-list-item.ember-view')[0]; if(elem){ - var view = Ember.View.views[elem.id]; + const view = Ember.View.views[elem.id]; view.send(action); } }, - currentTopic: function(){ - var topicController = this.container.lookup('controller:topic'); + currentTopic(){ + const topicController = this.container.lookup('controller:topic'); if(topicController) { - var topic = topicController.get('model'); + const topic = topicController.get('model'); if(topic){ return topic; } } }, - sendToSelectedPost: function(action){ - var container = this.container; + sendToSelectedPost(action){ + const container = this.container; // TODO: We should keep track of the post without a CSS class - var selectedPostId = parseInt($('.topic-post.selected article.boxed').data('post-id'), 10); + const selectedPostId = parseInt($('.topic-post.selected article.boxed').data('post-id'), 10); if (selectedPostId) { - var topicController = container.lookup('controller:topic'), + const topicController = container.lookup('controller:topic'), post = topicController.get('model.postStream.posts').findBy('id', selectedPostId); if (post) { topicController.send(action, post); @@ -215,24 +217,24 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ } }, - _bindToSelectedPost: function(action, binding) { - var self = this; + _bindToSelectedPost(action, binding) { + const self = this; this.keyTrapper.bind(binding, function() { self.sendToSelectedPost(action); }); }, - _bindToPath: function(path, binding) { + _bindToPath(path, binding) { this.keyTrapper.bind(binding, function() { - Discourse.URL.routeTo(path); + DiscourseURL.routeTo(path); }); }, - _bindToClick: function(selector, binding) { + _bindToClick(selector, binding) { binding = binding.split(','); this.keyTrapper.bind(binding, function(e) { - var $sel = $(selector); + const $sel = $(selector); // Special case: We're binding to enter. if (e && e.keyCode === 13) { @@ -249,21 +251,21 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ }); }, - _bindToFunction: function(func, binding) { + _bindToFunction(func, binding) { if (typeof this[func] === 'function') { this.keyTrapper.bind(binding, _.bind(this[func], this)); } }, - _moveSelection: function(direction) { - var $articles = this._findArticles(); + _moveSelection(direction) { + const $articles = this._findArticles(); if (typeof $articles === 'undefined') { return; } - var $selected = $articles.filter('.selected'), - index = $articles.index($selected); + const $selected = $articles.filter('.selected'); + let index = $articles.index($selected); if($selected.length !== 0){ //boundries check // loop is not allowed @@ -273,11 +275,11 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ // if nothing is selected go to the first post on screen if ($selected.length === 0) { - var scrollTop = $(document).scrollTop(); + const scrollTop = $(document).scrollTop(); index = 0; $articles.each(function(){ - var top = $(this).position().top; + const top = $(this).position().top; if(top > scrollTop) { return false; } @@ -291,7 +293,7 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ direction = 0; } - var $article = $articles.eq(index + direction); + const $article = $articles.eq(index + direction); if ($article.size() > 0) { @@ -303,7 +305,7 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ } if ($article.is('.topic-post')) { - var tabLoc = $article.find('a.tabLoc'); + let tabLoc = $article.find('a.tabLoc'); if (tabLoc.length === 0) { tabLoc = $(''); $article.prepend(tabLoc); @@ -315,19 +317,19 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ } }, - _scrollList: function($article) { + _scrollList($article) { // Try to keep the article on screen - var pos = $article.offset(); - var height = $article.height(); - var scrollTop = $(window).scrollTop(); - var windowHeight = $(window).height(); + const pos = $article.offset(); + const height = $article.height(); + const scrollTop = $(window).scrollTop(); + const windowHeight = $(window).height(); // skip if completely on screen if (pos.top > scrollTop && (pos.top + height) < (scrollTop + windowHeight)) { return; } - var scrollPos = (pos.top + (height/2)) - (windowHeight * 0.5); + let scrollPos = (pos.top + (height/2)) - (windowHeight * 0.5); if (scrollPos < 0) { scrollPos = 0; } if (this._scrollAnimation) { @@ -337,8 +339,8 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ }, - _findArticles: function() { - var $topicList = $('.topic-list'), + _findArticles() { + const $topicList = $('.topic-list'), $topicArea = $('.posts-wrapper'); if ($topicArea.size() > 0) { @@ -349,8 +351,8 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ } }, - _changeSection: function(direction) { - var $sections = $('#navigation-bar li'), + _changeSection(direction) { + const $sections = $('#navigation-bar li'), active = $('#navigation-bar li.active'), index = $sections.index(active) + direction; @@ -359,8 +361,8 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ } }, - _stopCallback: function() { - var oldStopCallback = this.keyTrapper.stopCallback; + _stopCallback() { + const oldStopCallback = this.keyTrapper.stopCallback; this.keyTrapper.stopCallback = function(e, element, combo) { if ((combo === 'ctrl+f' || combo === 'command+f') && element.id === 'search-term') { @@ -371,10 +373,10 @@ Discourse.KeyboardShortcuts = Ember.Object.createWithMixins({ }; }, - _toggleSearch: function(selectContext) { + _toggleSearch(selectContext) { $('#search-button').click(); if (selectContext) { - Discourse.__container__.lookup('controller:search').set('searchContextEnabled', true); + this.container.lookup('controller:search').set('searchContextEnabled', true); } }, -}); +}; diff --git a/app/assets/javascripts/discourse/lib/quote.js b/app/assets/javascripts/discourse/lib/quote.js.es6 similarity index 98% rename from app/assets/javascripts/discourse/lib/quote.js rename to app/assets/javascripts/discourse/lib/quote.js.es6 index f6bf7aab28..d7c95bac49 100644 --- a/app/assets/javascripts/discourse/lib/quote.js +++ b/app/assets/javascripts/discourse/lib/quote.js.es6 @@ -1,4 +1,4 @@ -Discourse.Quote = { +export default { REGEXP: /\[quote=([^\]]*)\]((?:[\s\S](?!\[quote=[^\]]*\]))*?)\[\/quote\]/im, 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 09ef6736c2..3b0252eaae 100644 --- a/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 +++ b/app/assets/javascripts/discourse/lib/static-route-builder.js.es6 @@ -1,3 +1,5 @@ +import DiscourseURL from 'discourse/lib/url'; + const configs = { "faq": "faq_url", "tos": "tos_url", @@ -14,14 +16,14 @@ export default (page) => { const configKey = configs[page]; if (configKey && Discourse.SiteSettings[configKey].length > 0) { transition.abort(); - Discourse.URL.redirectTo(Discourse.SiteSettings[configKey]); + DiscourseURL.redirectTo(Discourse.SiteSettings[configKey]); } }, activate() { this._super(); // Scroll to an element if exists - Discourse.URL.scrollToId(document.location.hash); + DiscourseURL.scrollToId(document.location.hash); }, model() { diff --git a/app/assets/javascripts/discourse/lib/url.js b/app/assets/javascripts/discourse/lib/url.js.es6 similarity index 82% rename from app/assets/javascripts/discourse/lib/url.js rename to app/assets/javascripts/discourse/lib/url.js.es6 index f0726dbbda..fcfe6ecce6 100644 --- a/app/assets/javascripts/discourse/lib/url.js +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -1,25 +1,25 @@ /*global LockOn:true*/ -var jumpScheduled = false, - rewrites = []; +let _jumpScheduled = false; +const rewrites = []; -Discourse.URL = Ember.Object.createWithMixins({ +const DiscourseURL = Ember.Object.createWithMixins({ // Used for matching a topic TOPIC_REGEXP: /\/t\/([^\/]+)\/(\d+)\/?(\d+)?/, isJumpScheduled: function() { - return jumpScheduled; + return _jumpScheduled; }, /** Jumps to a particular post in the stream **/ jumpToPost: function(postNumber, opts) { - var holderId = '#post-cloak-' + postNumber; + const holderId = '#post-cloak-' + postNumber; - var offset = function(){ + const offset = function(){ - var $header = $('header'), + const $header = $('header'), $title = $('#topic-title'), windowHeight = $(window).height() - $title.height(), expectedOffset = $title.height() - $header.find('.contents').height() + (windowHeight / 5); @@ -34,13 +34,13 @@ Discourse.URL = Ember.Object.createWithMixins({ return; } - var lockon = new LockOn(holderId, {offsetCalculator: offset}); - var holder = $(holderId); + const lockon = new LockOn(holderId, {offsetCalculator: offset}); + const holder = $(holderId); if(holder.length > 0 && opts && opts.skipIfOnScreen){ // if we are on screen skip - var elementTop = lockon.elementTop(), + const elementTop = lockon.elementTop(), scrollTop = $(window).scrollTop(), windowHeight = $(window).height()-offset(), height = holder.height(); @@ -73,7 +73,7 @@ Discourse.URL = Ember.Object.createWithMixins({ // while URLs are loading. For example, while a topic loads it sets `currentPost` // which triggers a replaceState even though the topic hasn't fully loaded yet! Em.run.next(function() { - var location = Discourse.URL.get('router.location'); + const location = DiscourseURL.get('router.location'); if (location && location.replaceURL) { location.replaceURL(path); } @@ -85,15 +85,15 @@ Discourse.URL = Ember.Object.createWithMixins({ scrollToId: function(id) { if (Em.isEmpty(id)) { return; } - jumpScheduled = true; + _jumpScheduled = true; Em.run.schedule('afterRender', function() { - var $elem = $(id); + 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; + _jumpScheduled = false; } }); }, @@ -125,19 +125,19 @@ Discourse.URL = Ember.Object.createWithMixins({ return; } - var oldPath = window.location.pathname; + const oldPath = window.location.pathname; path = path.replace(/(https?\:)?\/\/[^\/]+/, ''); // handle prefixes if (path.match(/^\//)) { - var rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri); + let rootURL = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri); rootURL = rootURL.replace(/\/$/, ''); path = path.replace(rootURL, ''); } // Rewrite /my/* urls if (path.indexOf('/my/') === 0) { - var currentUser = Discourse.User.current(); + const currentUser = Discourse.User.current(); if (currentUser) { path = path.replace('/my/', '/users/' + currentUser.get('username_lower') + "/"); } else { @@ -203,40 +203,40 @@ Discourse.URL = Ember.Object.createWithMixins({ @param {String} path the path we're navigating to **/ navigatedToPost: function(oldPath, path) { - var newMatches = this.TOPIC_REGEXP.exec(path), + const newMatches = this.TOPIC_REGEXP.exec(path), newTopicId = newMatches ? newMatches[2] : null; if (newTopicId) { - var oldMatches = this.TOPIC_REGEXP.exec(oldPath), + const oldMatches = this.TOPIC_REGEXP.exec(oldPath), oldTopicId = oldMatches ? oldMatches[2] : null; // If the topic_id is the same if (oldTopicId === newTopicId) { - Discourse.URL.replaceState(path); + DiscourseURL.replaceState(path); - var container = Discourse.__container__, + const container = Discourse.__container__, topicController = container.lookup('controller:topic'), opts = {}, postStream = topicController.get('model.postStream'); if (newMatches[3]) opts.nearPost = newMatches[3]; if (path.match(/last$/)) { opts.nearPost = topicController.get('highest_post_number'); } - var closest = opts.nearPost || 1; + const closest = opts.nearPost || 1; - var self = this; + const self = this; postStream.refresh(opts).then(function() { topicController.setProperties({ 'model.currentPost': closest, enteredAt: new Date().getTime().toString() }); - var closestPost = postStream.closestPostForPostNumber(closest), + const closestPost = postStream.closestPostForPostNumber(closest), progress = postStream.progressIndexOfPost(closestPost), progressController = container.lookup('controller:topic-progress'); progressController.set('progressPosition', progress); self.appEvents.trigger('post:highlight', closest); }).then(function() { - Discourse.URL.jumpToPost(closest, {skipIfOnScreen: true}); + DiscourseURL.jumpToPost(closest, {skipIfOnScreen: true}); }); // Abort routing, we have replaced our state. @@ -256,7 +256,7 @@ Discourse.URL = Ember.Object.createWithMixins({ @param {String} path the path we're navigating to **/ navigatedToHome: function(oldPath, path) { - var homepage = Discourse.Utilities.defaultHomepage(); + const homepage = Discourse.Utilities.defaultHomepage(); if (window.history && window.history.pushState && @@ -269,14 +269,7 @@ Discourse.URL = Ember.Object.createWithMixins({ return false; }, - /** - @private - - Get the origin of the current location. - This has been extracted so it can be tested. - - @method origin - **/ + // This has been extracted so it can be tested. origin: function() { return window.location.origin; }, @@ -293,15 +286,8 @@ Discourse.URL = Ember.Object.createWithMixins({ return Discourse.__container__.lookup('router:main'); }.property().volatile(), - /** - @private - - Get a controller. Note that currently it uses `__container__` which is not - advised but there is no other way to access the router. - - @method controllerFor - @param {String} name the name of the controller - **/ + // 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) { return Discourse.__container__.lookup('controller:' + name); }, @@ -313,7 +299,7 @@ Discourse.URL = Ember.Object.createWithMixins({ handleURL: function(path, opts) { opts = opts || {}; - var router = this.get('router'); + const router = this.get('router'); if (opts.replaceURL) { this.replaceState(path); @@ -321,25 +307,25 @@ Discourse.URL = Ember.Object.createWithMixins({ router.router.updateURL(path); } - var split = path.split('#'), - elementId; + const split = path.split('#'); + let elementId; if (split.length === 2) { path = split[0]; elementId = split[1]; } - var transition = router.handleURL(path); + const transition = router.handleURL(path); transition._discourse_intercepted = true; transition.promise.then(function() { if (elementId) { - jumpScheduled = true; + _jumpScheduled = true; Em.run.next('afterRender', function() { - var offset = $('#' + elementId).offset(); + const offset = $('#' + elementId).offset(); if (offset && offset.top) { $('html, body').scrollTop(offset.top - $('header').height() - 10); - jumpScheduled = false; + _jumpScheduled = false; } }); } @@ -347,3 +333,5 @@ Discourse.URL = Ember.Object.createWithMixins({ } }); + +export default DiscourseURL; diff --git a/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 b/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 index e3a5a81f64..35af1d0eee 100644 --- a/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 +++ b/app/assets/javascripts/discourse/mixins/scroll-top.js.es6 @@ -1,5 +1,7 @@ +import DiscourseURL from 'discourse/lib/url'; + function scrollTop() { - if (Discourse.URL.isJumpScheduled()) { return; } + if (DiscourseURL.isJumpScheduled()) { return; } Ember.run.schedule('afterRender', function() { $(document).scrollTop(0); }); diff --git a/app/assets/javascripts/discourse/mixins/scrolling.js.es6 b/app/assets/javascripts/discourse/mixins/scrolling.js.es6 index 459bbd7119..ffa40294f0 100644 --- a/app/assets/javascripts/discourse/mixins/scrolling.js.es6 +++ b/app/assets/javascripts/discourse/mixins/scrolling.js.es6 @@ -1,3 +1,5 @@ +import debounce from 'discourse/lib/debounce'; + /** This object provides the DOM methods we need for our Mixin to bind to scrolling methods in the browser. By removing them from the Mixin we can test them @@ -34,7 +36,7 @@ const Scrolling = Ember.Mixin.create({ }; if (opts.debounce) { - onScrollMethod = Discourse.debounce(onScrollMethod, opts.debounce); + onScrollMethod = debounce(onScrollMethod, opts.debounce); } ScrollingDOMMethods.bindOnScroll(onScrollMethod, opts.name); diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index e58f62362e..d9805f630a 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -1,6 +1,8 @@ import RestModel from 'discourse/models/rest'; import Topic from 'discourse/models/topic'; import { throwAjaxError } from 'discourse/lib/ajax-error'; +import Quote from 'discourse/lib/quote'; +import Draft from 'discourse/models/draft'; const CLOSED = 'closed', SAVING = 'saving', @@ -274,7 +276,7 @@ const Composer = RestModel.extend({ **/ replyLength: function() { let reply = this.get('reply') || ""; - while (Discourse.Quote.REGEXP.test(reply)) { reply = reply.replace(Discourse.Quote.REGEXP, ""); } + while (Quote.REGEXP.test(reply)) { reply = reply.replace(Quote.REGEXP, ""); } return reply.replace(/\s+/img, " ").trim().length; }.property('reply'), @@ -662,7 +664,7 @@ const Composer = RestModel.extend({ } // try to save the draft - return Discourse.Draft.save(this.get('draftKey'), this.get('draftSequence'), data) + return Draft.save(this.get('draftKey'), this.get('draftSequence'), data) .then(function() { composer.set('draftStatus', I18n.t('composer.saved_draft_tip')); }).catch(function() { @@ -706,7 +708,7 @@ Composer.reopenClass({ } } catch (error) { draft = null; - Discourse.Draft.clear(draftKey, draftSequence); + Draft.clear(draftKey, draftSequence); } if (draft && ((draft.title && draft.title !== '') || (draft.reply && draft.reply !== ''))) { return this.open({ diff --git a/app/assets/javascripts/discourse/models/draft.js b/app/assets/javascripts/discourse/models/draft.js.es6 similarity index 61% rename from app/assets/javascripts/discourse/models/draft.js rename to app/assets/javascripts/discourse/models/draft.js.es6 index e049a80ec1..faf16a3ae2 100644 --- a/app/assets/javascripts/discourse/models/draft.js +++ b/app/assets/javascripts/discourse/models/draft.js.es6 @@ -1,16 +1,8 @@ -/** - A data model representing a draft post +const Draft = Discourse.Model.extend(); - @class Draft - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.Draft = Discourse.Model.extend({}); +Draft.reopenClass({ -Discourse.Draft.reopenClass({ - - clear: function(key, sequence) { + clear(key, sequence) { return Discourse.ajax("/draft.json", { type: 'DELETE', data: { @@ -20,19 +12,19 @@ Discourse.Draft.reopenClass({ }); }, - get: function(key) { + get(key) { return Discourse.ajax('/draft.json', { data: { draft_key: key }, dataType: 'json' }); }, - getLocal: function(key, current) { + getLocal(key, current) { // TODO: implement this return current; }, - save: function(key, sequence, data) { + save(key, sequence, data) { data = typeof data === "string" ? data : JSON.stringify(data); return Discourse.ajax("/draft.json", { type: 'POST', @@ -45,3 +37,5 @@ Discourse.Draft.reopenClass({ } }); + +export default Draft; diff --git a/app/assets/javascripts/discourse/models/invite.js b/app/assets/javascripts/discourse/models/invite.js.es6 similarity index 66% rename from app/assets/javascripts/discourse/models/invite.js rename to app/assets/javascripts/discourse/models/invite.js.es6 index da1cfd1614..8f9bcd0bde 100644 --- a/app/assets/javascripts/discourse/models/invite.js +++ b/app/assets/javascripts/discourse/models/invite.js.es6 @@ -1,15 +1,6 @@ -/** - A data model representing an Invite +const Invite = Discourse.Model.extend({ - @class Invite - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ - -Discourse.Invite = Discourse.Model.extend({ - - rescind: function() { + rescind() { Discourse.ajax('/invites', { type: 'DELETE', data: { email: this.get('email') } @@ -17,7 +8,7 @@ Discourse.Invite = Discourse.Model.extend({ this.set('rescinded', true); }, - reinvite: function() { + reinvite() { Discourse.ajax('/invites/reinvite', { type: 'POST', data: { email: this.get('email') } @@ -27,9 +18,9 @@ Discourse.Invite = Discourse.Model.extend({ }); -Discourse.Invite.reopenClass({ +Invite.reopenClass({ - create: function() { + create() { var result = this._super.apply(this, arguments); if (result.user) { result.user = Discourse.User.create(result.user); @@ -37,7 +28,7 @@ Discourse.Invite.reopenClass({ return result; }, - findInvitedBy: function(user, filter, search, offset) { + findInvitedBy(user, filter, search, offset) { if (!user) { return Em.RSVP.resolve(); } var data = {}; @@ -45,9 +36,9 @@ Discourse.Invite.reopenClass({ if (!Em.isNone(search)) { data.search = search; } data.offset = offset || 0; - return Discourse.ajax("/users/" + user.get('username_lower') + "/invited.json", {data: data}).then(function (result) { + return Discourse.ajax("/users/" + user.get('username_lower') + "/invited.json", {data}).then(function (result) { result.invites = result.invites.map(function (i) { - return Discourse.Invite.create(i); + return Invite.create(i); }); return Em.Object.create(result); @@ -55,3 +46,5 @@ Discourse.Invite.reopenClass({ } }); + +export default Invite; diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 5a45ddcc59..d8549b4ba4 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 DiscourseURL from 'discourse/lib/url'; import RestModel from 'discourse/models/rest'; function calcDayDiff(p1, p2) { @@ -159,7 +160,7 @@ const PostStream = RestModel.extend({ const posts = this.get('posts'); if (posts.length > 1) { const secondPostNum = posts[1].get('post_number'); - Discourse.URL.jumpToPost(secondPostNum); + DiscourseURL.jumpToPost(secondPostNum); } }, diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 7f6ccf38d8..17ac32b052 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -2,6 +2,7 @@ import RestModel from 'discourse/models/rest'; import { popupAjaxError } from 'discourse/lib/ajax-error'; import ActionSummary from 'discourse/models/action-summary'; import { url, fmt, propertyEqual } from 'discourse/lib/computed'; +import Quote from 'discourse/lib/quote'; const Post = RestModel.extend({ @@ -418,7 +419,7 @@ Post.reopenClass({ loadQuote(postId) { return Discourse.ajax("/posts/" + postId + ".json").then(function (result) { const post = Discourse.Post.create(result); - return Discourse.Quote.build(post, post.get('raw'), {raw: true, full: true}); + return Quote.build(post, post.get('raw'), {raw: true, full: true}); }); }, diff --git a/app/assets/javascripts/discourse/models/selectable_array.js b/app/assets/javascripts/discourse/models/selectable_array.js deleted file mode 100644 index 43577bcbb3..0000000000 --- a/app/assets/javascripts/discourse/models/selectable_array.js +++ /dev/null @@ -1,35 +0,0 @@ -// this allows you to track the selected item in an array, ghetto for now -Discourse.SelectableArray = Em.ArrayProxy.extend({ - - init: function() { - this.content = []; - this._super(); - }, - - selectIndex: function(index){ - this.select(this[index]); - }, - - select: function(selected){ - _.each(this.content,function(item){ - if(item === selected){ - Em.set(item, "active", true); - } else { - if (item.get("active")) { - Em.set(item, "active", false); - } - } - }); - this.set("active", selected); - }, - - removeObject: function(object) { - if(object === this.get("active")){ - this.set("active", null); - Em.set(object, "active", false); - } - - this._super(object); - } - -}); diff --git a/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 b/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 index a001be7f5d..fc402e43c9 100644 --- a/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic-by-slug.js.es6 @@ -1,4 +1,5 @@ import Topic from 'discourse/models/topic'; +import DiscourseURL from 'discourse/lib/url'; export default Discourse.Route.extend({ model: function(params) { @@ -6,6 +7,6 @@ export default Discourse.Route.extend({ }, afterModel: function(result) { - Discourse.URL.routeTo(result.url, { replaceURL: true }); + DiscourseURL.routeTo(result.url, { replaceURL: true }); } }); diff --git a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 index 6de3d07c54..3c1c843b1f 100644 --- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 @@ -1,3 +1,6 @@ +import DiscourseURL from 'discourse/lib/url'; +import Draft from 'discourse/models/draft'; + // This route is used for retrieving a topic based on params export default Discourse.Route.extend({ @@ -43,12 +46,11 @@ export default Discourse.Route.extend({ Ember.run.scheduleOnce('afterRender', function() { self.appEvents.trigger('post:highlight', closest); }); - - Discourse.URL.jumpToPost(closest); + DiscourseURL.jumpToPost(closest); if (topic.present('draft')) { composerController.open({ - draft: Discourse.Draft.getLocal(topic.get('draft_key'), topic.get('draft')), + draft: Draft.getLocal(topic.get('draft_key'), topic.get('draft')), draftKey: topic.get('draft_key'), draftSequence: topic.get('draft_sequence'), topic: topic, diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 6d730f058d..c79ccbe5c0 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -1,4 +1,5 @@ import ScreenTrack from 'discourse/lib/screen-track'; +import DiscourseURL from 'discourse/lib/url'; let isTransitioning = false, scheduledReplace = null, @@ -128,7 +129,7 @@ const TopicRoute = Discourse.Route.extend({ _replaceUnlessScrolling(url) { const currentPos = parseInt($(document).scrollTop(), 10); if (currentPos === lastScrollPos) { - Discourse.URL.replaceState(url); + DiscourseURL.replaceState(url); return; } lastScrollPos = currentPos; diff --git a/app/assets/javascripts/discourse/routes/user-activity.js.es6 b/app/assets/javascripts/discourse/routes/user-activity.js.es6 index e654ac9b2c..5f21730326 100644 --- a/app/assets/javascripts/discourse/routes/user-activity.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-activity.js.es6 @@ -1,3 +1,5 @@ +import Draft from 'discourse/models/draft'; + export default Discourse.Route.extend({ model() { return this.modelFor("user"); @@ -10,7 +12,7 @@ export default Discourse.Route.extend({ const composerController = this.controllerFor("composer"); controller.set("model", user); if (this.currentUser) { - Discourse.Draft.get("new_private_message").then(function(data) { + Draft.get("new_private_message").then(function(data) { if (data.draft) { composerController.open({ draft: data.draft, diff --git a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 index 21317f3090..48e6d415cb 100644 --- a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 @@ -1,10 +1,11 @@ +import Invite from 'discourse/models/invite'; import showModal from "discourse/lib/show-modal"; export default Discourse.Route.extend({ model(params) { this.inviteFilter = params.filter; - return Discourse.Invite.findInvitedBy(this.modelFor("user"), params.filter); + return Invite.findInvitedBy(this.modelFor("user"), params.filter); }, afterModel(model) { diff --git a/app/assets/javascripts/discourse/views/choose-topic.js.es6 b/app/assets/javascripts/discourse/views/choose-topic.js.es6 index b064528efa..b9733a5f21 100644 --- a/app/assets/javascripts/discourse/views/choose-topic.js.es6 +++ b/app/assets/javascripts/discourse/views/choose-topic.js.es6 @@ -1,3 +1,4 @@ +import debounce from 'discourse/lib/debounce'; import searchForTerm from 'discourse/lib/search-for-term'; export default Discourse.View.extend({ @@ -18,7 +19,7 @@ export default Discourse.View.extend({ this.set('loading', false); }.observes('topics'), - search: Discourse.debounce(function(title) { + search: debounce(function(title) { var self = this; if (Em.isEmpty(title)) { self.setProperties({ topics: null, loading: false }); diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index 0ab45500a8..b57a1a394b 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -3,6 +3,7 @@ import afterTransition from 'discourse/lib/after-transition'; import loadScript from 'discourse/lib/load-script'; import avatarTemplate from 'discourse/lib/avatar-template'; import positioningWorkaround from 'discourse/lib/safari-hacks'; +import debounce from 'discourse/lib/debounce'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; const ComposerView = Discourse.View.extend(Ember.Evented, { @@ -40,7 +41,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { return this.present('model.createdPost') ? 'created-post' : null; }.property('model.createdPost'), - refreshPreview: Discourse.debounce(function() { + refreshPreview: debounce(function() { if (this.editor) { this.editor.refreshPreview(); } @@ -279,7 +280,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { this.set('editor', this.editor); this.loadingChanged(); - const saveDraft = Discourse.debounce((function() { + const saveDraft = debounce((function() { return self.get('controller').saveDraft(); }), 2000); diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index 1d83deaff0..eff4293ba3 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -1,5 +1,6 @@ import ScreenTrack from 'discourse/lib/screen-track'; import { number } from 'discourse/lib/formatter'; +import DiscourseURL from 'discourse/lib/url'; const DAY = 60 * 50 * 1000; @@ -199,7 +200,7 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { self = this; if (Discourse.Mobile.mobileView) { - Discourse.URL.routeTo(this.get('post.topic').urlForPostNumber(replyPostNumber)); + DiscourseURL.routeTo(this.get('post.topic').urlForPostNumber(replyPostNumber)); return; } diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index a961cddc29..aff3ff2e44 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -9,6 +9,9 @@ //= require ./discourse/lib/notification-levels //= require ./discourse/lib/app-events //= require ./discourse/lib/avatar-template +//= require ./discourse/lib/url +//= require ./discourse/lib/debounce +//= require ./discourse/lib/quote //= require ./discourse/helpers/i18n //= require ./discourse/helpers/fa-icon //= require ./discourse/helpers/register-unbound @@ -41,7 +44,9 @@ //= require ./discourse/models/topic-details //= require ./discourse/models/topic //= require ./discourse/models/user-action +//= require ./discourse/models/draft //= require ./discourse/models/composer +//= require ./discourse/models/invite //= require ./discourse/controllers/controller //= require ./discourse/controllers/discovery-sortable //= require ./discourse/controllers/object diff --git a/app/assets/javascripts/vendor.js b/app/assets/javascripts/vendor.js index a76153ed7f..5c0788f8c7 100644 --- a/app/assets/javascripts/vendor.js +++ b/app/assets/javascripts/vendor.js @@ -1,6 +1,6 @@ //= require logster //= require ./env -//= require ./discourse/lib/probes.js +//= require probes.js //= require handlebars.js //= require jquery_include.js @@ -19,6 +19,8 @@ //= require bootstrap-modal.js //= require bootstrap-transition.js //= require select2.js +//= require div_resizer +//= require caret_position //= require favcount.js //= require jquery.ba-replacetext.js //= require jquery.ba-resize.min.js diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index dc77be2ca9..116b83b600 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -80,7 +80,6 @@ module PrettyText "app/assets/javascripts/defer/html-sanitizer-bundle.js", "app/assets/javascripts/discourse/dialects/dialect.js", "app/assets/javascripts/discourse/lib/utilities.js", - "app/assets/javascripts/discourse/lib/html.js", "app/assets/javascripts/discourse/lib/markdown.js", ) diff --git a/test/javascripts/acceptance/category-edit-test.js.es6 b/test/javascripts/acceptance/category-edit-test.js.es6 index 5101660079..46d3a2125e 100644 --- a/test/javascripts/acceptance/category-edit-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-test.js.es6 @@ -1,3 +1,4 @@ +import DiscourseURL from 'discourse/lib/url'; import { acceptance } from "helpers/qunit-helpers"; acceptance("Category Edit", { loggedIn: true }); @@ -24,7 +25,7 @@ test("Change the category color", (assert) => { click('#save-category'); andThen(() => { assert.ok(!visible('#discourse-modal'), 'it closes the modal'); - assert.equal(Discourse.URL.redirectedTo, '/c/bug', 'it does one of the rare full page redirects'); + assert.equal(DiscourseURL.redirectedTo, '/c/bug', 'it does one of the rare full page redirects'); }); }); @@ -37,6 +38,6 @@ test("Change the topic template", (assert) => { click('#save-category'); andThen(() => { assert.ok(!visible('#discourse-modal'), 'it closes the modal'); - assert.equal(Discourse.URL.redirectedTo, '/c/bug', 'it does one of the rare full page redirects'); + assert.equal(DiscourseURL.redirectedTo, '/c/bug', 'it does one of the rare full page redirects'); }); }); diff --git a/test/javascripts/components/keyboard-shortcuts-test.js.es6 b/test/javascripts/components/keyboard-shortcuts-test.js.es6 index 4a335328f8..40753a3477 100644 --- a/test/javascripts/components/keyboard-shortcuts-test.js.es6 +++ b/test/javascripts/components/keyboard-shortcuts-test.js.es6 @@ -1,6 +1,9 @@ -var testMouseTrap; +import DiscourseURL from 'discourse/lib/url'; -module("Discourse.KeyboardShortcuts", { +var testMouseTrap; +import KeyboardShortcuts from 'discourse/lib/keyboard-shortcuts'; + +module("lib:keyboard-shortcuts", { setup: function() { var _bindings = {}; @@ -23,7 +26,7 @@ module("Discourse.KeyboardShortcuts", { } }; - sandbox.stub(Discourse.URL, "routeTo"); + sandbox.stub(DiscourseURL, "routeTo"); $("#qunit-fixture").html([ "
", @@ -64,20 +67,20 @@ module("Discourse.KeyboardShortcuts", { } }); -var pathBindings = Discourse.KeyboardShortcuts.PATH_BINDINGS; +var pathBindings = KeyboardShortcuts.PATH_BINDINGS; _.each(pathBindings, function(path, binding) { var testName = binding + " goes to " + path; test(testName, function() { - Discourse.KeyboardShortcuts.bindEvents(testMouseTrap); + KeyboardShortcuts.bindEvents(testMouseTrap); testMouseTrap.trigger(binding); - ok(Discourse.URL.routeTo.calledWith(path)); + ok(DiscourseURL.routeTo.calledWith(path)); }); }); -var clickBindings = Discourse.KeyboardShortcuts.CLICK_BINDINGS; +var clickBindings = KeyboardShortcuts.CLICK_BINDINGS; _.each(clickBindings, function(selector, binding) { var bindings = binding.split(","); @@ -85,7 +88,7 @@ _.each(clickBindings, function(selector, binding) { var testName = binding + " clicks on " + selector; test(testName, function() { - Discourse.KeyboardShortcuts.bindEvents(testMouseTrap); + KeyboardShortcuts.bindEvents(testMouseTrap); $(selector).on("click", function() { ok(true, selector + " was clicked"); }); @@ -96,32 +99,32 @@ _.each(clickBindings, function(selector, binding) { }); }); -var functionBindings = Discourse.KeyboardShortcuts.FUNCTION_BINDINGS; +var functionBindings = KeyboardShortcuts.FUNCTION_BINDINGS; _.each(functionBindings, function(func, binding) { var testName = binding + " calls " + func; test(testName, function() { - sandbox.stub(Discourse.KeyboardShortcuts, func, function() { + sandbox.stub(KeyboardShortcuts, func, function() { ok(true, func + " is called when " + binding + " is triggered"); }); - Discourse.KeyboardShortcuts.bindEvents(testMouseTrap); + KeyboardShortcuts.bindEvents(testMouseTrap); testMouseTrap.trigger(binding); }); }); test("selectDown calls _moveSelection with 1", function() { - var spy = sandbox.spy(Discourse.KeyboardShortcuts, '_moveSelection'); + var spy = sandbox.spy(KeyboardShortcuts, '_moveSelection'); - Discourse.KeyboardShortcuts.selectDown(); + KeyboardShortcuts.selectDown(); ok(spy.calledWith(1), "_moveSelection is called with 1"); }); test("selectUp calls _moveSelection with -1", function() { - var spy = sandbox.spy(Discourse.KeyboardShortcuts, '_moveSelection'); + var spy = sandbox.spy(KeyboardShortcuts, '_moveSelection'); - Discourse.KeyboardShortcuts.selectUp(); + KeyboardShortcuts.selectUp(); ok(spy.calledWith(-1), "_moveSelection is called with -1"); }); @@ -131,20 +134,20 @@ test("goBack calls history.back", function() { called = true; }); - Discourse.KeyboardShortcuts.goBack(); + KeyboardShortcuts.goBack(); ok(called, "history.back is called"); }); test("nextSection calls _changeSection with 1", function() { - var spy = sandbox.spy(Discourse.KeyboardShortcuts, '_changeSection'); + var spy = sandbox.spy(KeyboardShortcuts, '_changeSection'); - Discourse.KeyboardShortcuts.nextSection(); + KeyboardShortcuts.nextSection(); ok(spy.calledWith(1), "_changeSection is called with 1"); }); test("prevSection calls _changeSection with -1", function() { - var spy = sandbox.spy(Discourse.KeyboardShortcuts, '_changeSection'); + var spy = sandbox.spy(KeyboardShortcuts, '_changeSection'); - Discourse.KeyboardShortcuts.prevSection(); + KeyboardShortcuts.prevSection(); ok(spy.calledWith(-1), "_changeSection is called with -1"); }); diff --git a/test/javascripts/helpers/custom-html-test.js.es6 b/test/javascripts/helpers/custom-html-test.js.es6 new file mode 100644 index 0000000000..7510aede58 --- /dev/null +++ b/test/javascripts/helpers/custom-html-test.js.es6 @@ -0,0 +1,14 @@ +module("helper:custom-html"); + +import { getCustomHTML, setCustomHTML } from 'discourse/helpers/custom-html'; + +test("customHTML", function() { + blank(getCustomHTML('evil'), "there is no custom HTML for a key by default"); + + setCustomHTML('evil', 'trout'); + equal(getCustomHTML('evil'), 'trout', 'it retrieves the custom html'); + + PreloadStore.store('customHTML', {cookie: 'monster'}); + equal(getCustomHTML('cookie'), 'monster', 'it returns HTML fragments from the PreloadStore'); + +}); diff --git a/test/javascripts/helpers/parse-html.js.es6 b/test/javascripts/helpers/parse-html.js.es6 new file mode 100644 index 0000000000..c9469fa6b9 --- /dev/null +++ b/test/javascripts/helpers/parse-html.js.es6 @@ -0,0 +1,8 @@ +/* global Tautologistics */ +export default function parseHTML(rawHtml) { + const builder = new Tautologistics.NodeHtmlParser.HtmlBuilder(); + const parser = new Tautologistics.NodeHtmlParser.Parser(builder); + + parser.parseComplete(rawHtml); + return builder.dom; +} diff --git a/test/javascripts/helpers/parse_html.js b/test/javascripts/helpers/parse_html.js deleted file mode 100644 index a451ffb013..0000000000 --- a/test/javascripts/helpers/parse_html.js +++ /dev/null @@ -1,9 +0,0 @@ -/* global Tautologistics */ -/* exported parseHTML */ -function parseHTML(rawHtml) { - var builder = new Tautologistics.NodeHtmlParser.HtmlBuilder(), - parser = new Tautologistics.NodeHtmlParser.Parser(builder); - - parser.parseComplete(rawHtml); - return builder.dom; -} diff --git a/test/javascripts/lib/bbcode-test.js.es6 b/test/javascripts/lib/bbcode-test.js.es6 index 4f9ff93eff..84901d7d51 100644 --- a/test/javascripts/lib/bbcode-test.js.es6 +++ b/test/javascripts/lib/bbcode-test.js.es6 @@ -1,3 +1,5 @@ +import Quote from 'discourse/lib/quote'; + module("Discourse.BBCode"); var format = function(input, expected, text) { @@ -90,7 +92,7 @@ test("quotes", function() { }); var formatQuote = function(val, expected, text) { - equal(Discourse.Quote.build(post, val), expected, text); + equal(Quote.build(post, val), expected, text); }; formatQuote(undefined, "", "empty string for undefined content"); diff --git a/test/javascripts/lib/category-badge-test.js.es6 b/test/javascripts/lib/category-badge-test.js.es6 index 84d2c9c9ae..95f5d897b7 100644 --- a/test/javascripts/lib/category-badge-test.js.es6 +++ b/test/javascripts/lib/category-badge-test.js.es6 @@ -1,5 +1,6 @@ module("lib:category-link"); +import parseHTML from 'helpers/parse-html'; import { categoryBadgeHTML } from "discourse/helpers/category-link"; test("categoryBadge without a category", function() { diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6 index 337a594339..0cbd0c09a0 100644 --- a/test/javascripts/lib/click-track-test.js.es6 +++ b/test/javascripts/lib/click-track-test.js.es6 @@ -1,3 +1,4 @@ +import DiscourseURL from "discourse/lib/url"; import ClickTrack from "discourse/lib/click-track"; var windowOpen, @@ -9,7 +10,7 @@ module("lib:click-track", { // Prevent any of these tests from navigating away win = {focus: function() { } }; - redirectTo = sandbox.stub(Discourse.URL, "redirectTo"); + redirectTo = sandbox.stub(DiscourseURL, "redirectTo"); sandbox.stub(Discourse, "ajax"); windowOpen = sandbox.stub(window, "open").returns(win); sandbox.stub(win, "focus"); @@ -51,7 +52,7 @@ test("it calls preventDefault when clicking on an a", function() { sandbox.stub(clickEvent, "preventDefault"); track(clickEvent); ok(clickEvent.preventDefault.calledOnce); - ok(Discourse.URL.redirectTo.calledOnce); + ok(DiscourseURL.redirectTo.calledOnce); }); test("does not track clicks on back buttons", function() { @@ -70,7 +71,7 @@ test("removes the href and put it as a data attribute", function() { equal($link.data('href'), 'http://www.google.com'); blank($link.attr('href')); ok($link.data('auto-route')); - ok(Discourse.URL.redirectTo.calledOnce); + ok(DiscourseURL.redirectTo.calledOnce); }); asyncTest("restores the href after a while", function() { @@ -159,20 +160,20 @@ testOpenInANewTab("it opens in a new tab on middle click", function(clickEvent) }); test("tracks via AJAX if we're on the same site", function() { - sandbox.stub(Discourse.URL, "routeTo"); - sandbox.stub(Discourse.URL, "origin").returns("http://discuss.domain.com"); + sandbox.stub(DiscourseURL, "routeTo"); + sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); ok(!track(generateClickEventOn('#same-site'))); ok(Discourse.ajax.calledOnce); - ok(Discourse.URL.routeTo.calledOnce); + ok(DiscourseURL.routeTo.calledOnce); }); test("does not track via AJAX for attachments", function() { - sandbox.stub(Discourse.URL, "routeTo"); - sandbox.stub(Discourse.URL, "origin").returns("http://discuss.domain.com"); + sandbox.stub(DiscourseURL, "routeTo"); + sandbox.stub(DiscourseURL, "origin").returns("http://discuss.domain.com"); ok(!track(generateClickEventOn('.attachment'))); - ok(Discourse.URL.redirectTo.calledOnce); + ok(DiscourseURL.redirectTo.calledOnce); }); test("tracks custom urls when opening in another window", function() { diff --git a/test/javascripts/lib/html-test.js.es6 b/test/javascripts/lib/html-test.js.es6 deleted file mode 100644 index 07ef2906a1..0000000000 --- a/test/javascripts/lib/html-test.js.es6 +++ /dev/null @@ -1,14 +0,0 @@ -module("Discourse.HTML"); - -var html = Discourse.HTML; - -test("customHTML", function() { - blank(html.getCustomHTML('evil'), "there is no custom HTML for a key by default"); - - html.setCustomHTML('evil', 'trout'); - equal(html.getCustomHTML('evil'), 'trout', 'it retrieves the custom html'); - - PreloadStore.store('customHTML', {cookie: 'monster'}); - equal(html.getCustomHTML('cookie'), 'monster', 'it returns HTML fragments from the PreloadStore'); - -}); diff --git a/test/javascripts/lib/url-test.js.es6 b/test/javascripts/lib/url-test.js.es6 index e2696a7793..0598c32e54 100644 --- a/test/javascripts/lib/url-test.js.es6 +++ b/test/javascripts/lib/url-test.js.es6 @@ -1,16 +1,18 @@ -module("Discourse.URL"); +import DiscourseURL from 'discourse/lib/url'; + +module("lib:url"); test("isInternal with a HTTP url", function() { - sandbox.stub(Discourse.URL, "origin").returns("http://eviltrout.com"); + sandbox.stub(DiscourseURL, "origin").returns("http://eviltrout.com"); - not(Discourse.URL.isInternal(null), "a blank URL is not internal"); - ok(Discourse.URL.isInternal("/test"), "relative URLs are internal"); - ok(Discourse.URL.isInternal("http://eviltrout.com/tophat"), "a url on the same host is internal"); - ok(Discourse.URL.isInternal("https://eviltrout.com/moustache"), "a url on a HTTPS of the same host is internal"); - not(Discourse.URL.isInternal("http://twitter.com"), "a different host is not internal"); + not(DiscourseURL.isInternal(null), "a blank URL is not internal"); + ok(DiscourseURL.isInternal("/test"), "relative URLs are internal"); + ok(DiscourseURL.isInternal("http://eviltrout.com/tophat"), "a url on the same host is internal"); + ok(DiscourseURL.isInternal("https://eviltrout.com/moustache"), "a url on a HTTPS of the same host is internal"); + not(DiscourseURL.isInternal("http://twitter.com"), "a different host is not internal"); }); test("isInternal with a HTTPS url", function() { - sandbox.stub(Discourse.URL, "origin").returns("https://eviltrout.com"); - ok(Discourse.URL.isInternal("http://eviltrout.com/monocle"), "HTTPS urls match HTTP urls"); + sandbox.stub(DiscourseURL, "origin").returns("https://eviltrout.com"); + ok(DiscourseURL.isInternal("http://eviltrout.com/monocle"), "HTTPS urls match HTTP urls"); }); diff --git a/test/javascripts/models/invite-test.js.es6 b/test/javascripts/models/invite-test.js.es6 index 0bdc51a016..e55a414293 100644 --- a/test/javascripts/models/invite-test.js.es6 +++ b/test/javascripts/models/invite-test.js.es6 @@ -1,5 +1,7 @@ -module("Discourse.Invite"); +import Invite from 'discourse/models/invite'; + +module("model:invite"); test("create", function() { - ok(Discourse.Invite.create(), "it can be created without arguments"); + ok(Invite.create(), "it can be created without arguments"); }); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 48cbf025e3..746badbd25 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -6,7 +6,7 @@ //= require ../../app/assets/javascripts/preload_store // probe framework first -//= require ../../app/assets/javascripts/discourse/lib/probes +//= require probes // Externals we need to load first //= require jquery.debug @@ -80,6 +80,7 @@ var origDebounce = Ember.run.debounce, fixtures = require('fixtures/site_fixtures', null, null, false).default, flushMap = require('discourse/models/store', null, null, false).flushMap, ScrollingDOMMethods = require('discourse/mixins/scrolling', null, null, false).ScrollingDOMMethods, + _DiscourseURL = require('discourse/lib/url', null, null, false).default, server; function dup(obj) { @@ -97,9 +98,9 @@ QUnit.testStart(function(ctx) { Discourse.User.resetCurrent(); Discourse.Site.resetCurrent(Discourse.Site.create(dup(fixtures['site.json'].site))); - Discourse.URL.redirectedTo = null; - Discourse.URL.redirectTo = function(url) { - Discourse.URL.redirectedTo = url; + _DiscourseURL.redirectedTo = null; + _DiscourseURL.redirectTo = function(url) { + _DiscourseURL.redirectedTo = url; }; PreloadStore.reset(); diff --git a/app/assets/javascripts/discourse/lib/caret_position.js b/vendor/assets/javascripts/caret_position.js similarity index 100% rename from app/assets/javascripts/discourse/lib/caret_position.js rename to vendor/assets/javascripts/caret_position.js diff --git a/app/assets/javascripts/discourse/lib/div_resizer.js b/vendor/assets/javascripts/div_resizer.js similarity index 100% rename from app/assets/javascripts/discourse/lib/div_resizer.js rename to vendor/assets/javascripts/div_resizer.js diff --git a/app/assets/javascripts/discourse/lib/probes.js b/vendor/assets/javascripts/probes.js similarity index 100% rename from app/assets/javascripts/discourse/lib/probes.js rename to vendor/assets/javascripts/probes.js From 02a968bd27c32a7413fdb63f696abfcecd6e018e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 11 Aug 2015 12:27:07 -0400 Subject: [PATCH 009/237] Remove ObjectController, Discouse Controllers + Presence --- .jshintrc | 2 -- .../admin/controllers/admin-backups.js.es6 | 2 +- .../controllers/admin-badges-show.js.es6 | 2 +- .../controllers/admin-email-index.js.es6 | 4 +-- .../admin-email-preview-digest.js.es6 | 4 +-- .../admin/controllers/admin-email-sent.js.es6 | 3 +-- .../controllers/admin-email-skipped.js.es6 | 3 +-- .../admin/controllers/admin-group.js.es6 | 2 +- .../admin-log-screened-ip-address.js.es6 | 2 +- .../admin/controllers/admin-reports.js.es6 | 2 +- .../admin-site-settings-category.js.es6 | 2 +- .../controllers/admin-site-settings.js.es6 | 5 ++-- .../admin/controllers/admin-user-index.js.es6 | 3 +-- .../admin/controllers/admin-user.js.es6 | 4 +-- .../admin/controllers/admin.js.es6 | 4 +-- .../modals/admin-agree-flag.js.es6 | 3 +-- .../modals/admin-delete-flag.js.es6 | 4 +-- .../admin-staff-action-log-details.js.es6 | 4 +-- .../modals/admin-start-backup.js.es6 | 3 +-- .../modals/admin-suspend-user.js.es6 | 3 +-- .../change-site-customization-details.js.es6 | 3 +-- .../javascripts/admin/models/flagged_post.js | 2 +- .../admin/views/admin-backups-logs.js.es6 | 2 +- .../admin/views/admin-backups.js.es6 | 2 +- .../admin/views/admin-flags-list.js.es6 | 2 +- .../javascripts/admin/views/admin.js.es6 | 2 +- .../views/admin_customize_colors_view.js | 10 +------ .../admin/views/admin_user_view.js | 11 +------- app/assets/javascripts/discourse.js | 6 +++++ .../controllers/avatar-selector.js.es6 | 3 +-- .../discourse/controllers/badges/show.js.es6 | 4 +-- .../discourse/controllers/change-owner.js.es6 | 5 ++-- .../discourse/controllers/composer.js.es6 | 5 ++-- .../discourse/controllers/controller.js.es6 | 3 --- .../controllers/create-account.js.es6 | 23 ++++++++-------- .../controllers/discovery-sortable.js.es6 | 4 +-- .../discourse/controllers/discovery.js.es6 | 2 +- .../controllers/edit-category.js.es6 | 5 ++-- .../controllers/edit-topic-auto-close.js.es6 | 3 +-- .../discourse/controllers/exception.js.es6 | 4 +-- .../controllers/feature-topic.js.es6 | 3 +-- .../controllers/flag-action-type.js.es6 | 3 +-- .../discourse/controllers/flag.js.es6 | 3 +-- .../controllers/forgot-password.js.es6 | 5 ++-- .../controllers/full-page-search.js.es6 | 3 +-- .../discourse/controllers/header.js.es6 | 4 +-- .../discourse/controllers/history.js.es6 | 3 +-- .../discourse/controllers/invite.js.es6 | 10 +++---- .../keyboard-shortcuts-help.js.es6 | 3 +-- .../discourse/controllers/login.js.es6 | 9 +++---- .../discourse/controllers/merge-topic.js.es6 | 5 ++-- .../discourse/controllers/modal.js.es6 | 4 +-- .../controllers/navigation/default.js.es6 | 4 +-- .../controllers/not-activated.js.es6 | 3 +-- .../discourse/controllers/object.js.es6 | 3 --- .../discourse/controllers/preferences.js.es6 | 3 +-- .../controllers/preferences/about.js.es6 | 4 +-- .../controllers/preferences/email.js.es6 | 3 +-- .../controllers/preferences/username.js.es6 | 6 ++--- .../discourse/controllers/quote-button.js.es6 | 5 ++-- .../discourse/controllers/raw-email.js.es6 | 3 +-- .../discourse/controllers/search-help.js.es6 | 3 +-- .../discourse/controllers/search.js.es6 | 5 ++-- .../discourse/controllers/split-topic.js.es6 | 5 ++-- .../controllers/topic-admin-menu.js.es6 | 4 +-- .../controllers/topic-progress.js.es6 | 2 +- .../discourse/controllers/topic.js.es6 | 3 +-- .../controllers/upload-selector.js.es6 | 3 +-- .../controllers/user-activity.js.es6 | 2 +- .../controllers/user-invited-show.js.es6 | 2 +- .../controllers/user-topics-list.js.es6 | 4 +-- .../discourse/controllers/user.js.es6 | 3 +-- .../initializers/es6-deprecations.js.es6 | 27 ------------------- .../discourse/mixins/presence.js.es6 | 20 -------------- .../javascripts/discourse/models/model.js.es6 | 4 +-- .../javascripts/discourse/models/post.js.es6 | 4 +-- .../javascripts/discourse/models/rest.js.es6 | 4 +-- .../discourse/models/topic-details.js.es6 | 2 +- .../javascripts/discourse/models/user.js.es6 | 6 ++--- .../discourse/routes/topic-from-params.js.es6 | 2 +- .../discourse/views/badges-show.js.es6 | 2 +- .../javascripts/discourse/views/button.js.es6 | 2 +- .../discourse/views/choose-topic.js.es6 | 2 +- .../discourse/views/composer-messages.js.es6 | 2 +- .../discourse/views/composer.js.es6 | 4 +-- .../discourse/views/container.js.es6 | 4 +-- .../views/discovery-categories.js.es6 | 2 +- .../discourse/views/discovery-top.js.es6 | 2 +- .../discourse/views/discovery-topics.js.es6 | 2 +- .../discourse/views/group-index.js.es6 | 2 +- .../discourse/views/group-members.js.es6 | 2 +- .../discourse/views/grouped.js.es6 | 4 +-- .../javascripts/discourse/views/header.js.es6 | 2 +- .../discourse/views/modal-body.js.es6 | 2 +- .../discourse/views/quote-button.js.es6 | 2 +- .../javascripts/discourse/views/search.js.es6 | 2 +- .../discourse/views/selected-posts.js.es6 | 2 +- .../javascripts/discourse/views/share.js.es6 | 6 ++--- .../discourse/views/topic-admin-menu.js.es6 | 4 +-- .../discourse/views/topic-closing.js.es6 | 4 +-- .../discourse/views/topic-list-item.js.es6 | 2 +- .../javascripts/discourse/views/topic.js.es6 | 2 +- .../discourse/views/user-card.js.es6 | 2 +- .../discourse/views/user-topics-list.js.es6 | 2 +- .../javascripts/discourse/views/users.js.es6 | 2 +- .../javascripts/discourse/views/view.js.es6 | 3 --- app/assets/javascripts/main_include.js | 3 --- .../admin/models/admin-user-test.js.es6 | 3 ++- .../admin/models/api-key-test.js.es6 | 2 ++ .../controllers/discourse-test.js.es6 | 8 ------ .../javascripts/controllers/topic-test.js.es6 | 2 ++ test/javascripts/helpers/assertions.js | 8 ------ .../helpers/custom-html-test.js.es6 | 1 + test/javascripts/helpers/qunit-helpers.js.es6 | 19 +++++++++++-- .../lib/category-badge-test.js.es6 | 2 ++ test/javascripts/lib/click-track-test.js.es6 | 1 + .../javascripts/lib/preload-store-test.js.es6 | 2 ++ test/javascripts/lib/utilities-test.js.es6 | 6 +++-- test/javascripts/mixins/presence-test.js.es6 | 27 ------------------- test/javascripts/mixins/singleton-test.js.es6 | 1 + test/javascripts/models/composer-test.js.es6 | 1 + test/javascripts/models/model-test.js.es6 | 7 +---- .../models/post-stream-test.js.es6 | 1 + test/javascripts/models/post-test.js.es6 | 2 ++ test/javascripts/models/report-test.js.es6 | 2 ++ test/javascripts/models/site-test.js.es6 | 2 ++ .../models/topic-details-test.js.es6 | 1 + test/javascripts/models/topic-test.js.es6 | 1 + .../models/user-stream-test.js.es6 | 4 ++- .../views/container-view-test.js.es6 | 7 ----- 130 files changed, 187 insertions(+), 349 deletions(-) delete mode 100644 app/assets/javascripts/discourse/controllers/controller.js.es6 delete mode 100644 app/assets/javascripts/discourse/controllers/object.js.es6 delete mode 100644 app/assets/javascripts/discourse/initializers/es6-deprecations.js.es6 delete mode 100644 app/assets/javascripts/discourse/mixins/presence.js.es6 delete mode 100644 app/assets/javascripts/discourse/views/view.js.es6 delete mode 100644 test/javascripts/controllers/discourse-test.js.es6 delete mode 100644 test/javascripts/mixins/presence-test.js.es6 diff --git a/.jshintrc b/.jshintrc index 6be05281b4..cbe6bffe52 100644 --- a/.jshintrc +++ b/.jshintrc @@ -20,8 +20,6 @@ "not", "expect", "equal", - "blank", - "present", "visit", "andThen", "click", diff --git a/app/assets/javascripts/admin/controllers/admin-backups.js.es6 b/app/assets/javascripts/admin/controllers/admin-backups.js.es6 index 05aa974150..a429883378 100644 --- a/app/assets/javascripts/admin/controllers/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-backups.js.es6 @@ -1,4 +1,4 @@ -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ noOperationIsRunning: Ember.computed.not("model.isOperationRunning"), rollbackEnabled: Ember.computed.and("model.canRollback", "model.restoreEnabled", "noOperationIsRunning"), rollbackDisabled: Ember.computed.not("rollbackEnabled") diff --git a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 b/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 index 15ba6bc011..a44b6a0b79 100644 --- a/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-badges-show.js.es6 @@ -2,7 +2,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; import BufferedContent from 'discourse/mixins/buffered-content'; import { propertyNotEqual } from 'discourse/lib/computed'; -export default Ember.ObjectController.extend(BufferedContent, { +export default Ember.Controller.extend(BufferedContent, { needs: ['admin-badges'], saving: false, savingStatus: '', 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 341ca4f652..b4006391b2 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-index.js.es6 @@ -1,6 +1,4 @@ -import DiscourseController from 'discourse/controllers/controller'; - -export default DiscourseController.extend({ +export default Ember.Controller.extend({ /** Is the "send test email" button disabled? diff --git a/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 index 84d1f3e4b5..108cb74f3a 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 @@ -1,6 +1,4 @@ -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend({ +export default Ember.Controller.extend({ actions: { refresh: function() { diff --git a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 index e00bfba61f..a03bd8212d 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-sent.js.es6 @@ -1,7 +1,6 @@ -import DiscourseController from 'discourse/controllers/controller'; import debounce from 'discourse/lib/debounce'; -export default DiscourseController.extend({ +export default Ember.Controller.extend({ filterEmailLogs: debounce(function() { var self = this; diff --git a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 index 06cedc6dd3..1d83f14c6d 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-skipped.js.es6 @@ -1,7 +1,6 @@ import debounce from 'discourse/lib/debounce'; -import DiscourseController from 'discourse/controllers/controller'; -export default DiscourseController.extend({ +export default Ember.Controller.extend({ filterEmailLogs: debounce(function() { var self = this; Discourse.EmailLog.findAll(this.get("filter")).then(function(logs) { diff --git a/app/assets/javascripts/admin/controllers/admin-group.js.es6 b/app/assets/javascripts/admin/controllers/admin-group.js.es6 index 93213e2c66..dee7c45bb8 100644 --- a/app/assets/javascripts/admin/controllers/admin-group.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-group.js.es6 @@ -1,7 +1,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; import { propertyEqual } from 'discourse/lib/computed'; -export default Em.ObjectController.extend({ +export default Ember.Controller.extend({ needs: ['adminGroupsType'], disableSave: false, diff --git a/app/assets/javascripts/admin/controllers/admin-log-screened-ip-address.js.es6 b/app/assets/javascripts/admin/controllers/admin-log-screened-ip-address.js.es6 index 81486978a4..199296d823 100644 --- a/app/assets/javascripts/admin/controllers/admin-log-screened-ip-address.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-log-screened-ip-address.js.es6 @@ -1,4 +1,4 @@ -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ editing: false, savedIpAddress: null, diff --git a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 b/app/assets/javascripts/admin/controllers/admin-reports.js.es6 index 1bf93fef7c..e964d17a25 100644 --- a/app/assets/javascripts/admin/controllers/admin-reports.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-reports.js.es6 @@ -1,4 +1,4 @@ -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ viewMode: 'table', viewingTable: Em.computed.equal('viewMode', 'table'), viewingBarChart: Em.computed.equal('viewMode', 'barChart'), diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 index 37bc9b3765..b16db34e3e 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-settings-category.js.es6 @@ -1,4 +1,4 @@ -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ categoryNameKey: null, needs: ['adminSiteSettings'], diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 index d89e55c991..648a0e549c 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 @@ -1,14 +1,13 @@ import debounce from 'discourse/lib/debounce'; -import Presence from 'discourse/mixins/presence'; -export default Ember.ArrayController.extend(Presence, { +export default Ember.ArrayController.extend({ filter: null, onlyOverridden: false, filtered: Ember.computed.notEmpty('filter'), filterContentNow: function(category) { // If we have no content, don't bother filtering anything - if (!this.present('allSiteSettings')) return; + if (!!Ember.isEmpty(this.get('allSiteSettings'))) return; let filter; if (this.get('filter')) { 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 339f34e247..0ad12bfccc 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -1,8 +1,7 @@ -import ObjectController from 'discourse/controllers/object'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; import { propertyNotEqual, setting } from 'discourse/lib/computed'; -export default ObjectController.extend(CanCheckEmails, { +export default Ember.Controller.extend(CanCheckEmails, { editingTitle: false, originalPrimaryGroupId: null, availableGroups: null, diff --git a/app/assets/javascripts/admin/controllers/admin-user.js.es6 b/app/assets/javascripts/admin/controllers/admin-user.js.es6 index 33f6459f8e..77c79b724a 100644 --- a/app/assets/javascripts/admin/controllers/admin-user.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user.js.es6 @@ -1,3 +1 @@ -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend(); +export default Ember.Controller.extend(); diff --git a/app/assets/javascripts/admin/controllers/admin.js.es6 b/app/assets/javascripts/admin/controllers/admin.js.es6 index ae2b81f087..3bdfbe6b50 100644 --- a/app/assets/javascripts/admin/controllers/admin.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin.js.es6 @@ -1,6 +1,4 @@ -import DiscourseController from 'discourse/controllers/controller'; - -export default DiscourseController.extend({ +export default Ember.Controller.extend({ showBadges: function() { return this.get('currentUser.admin') && this.siteSettings.enable_badges; }.property() diff --git a/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 index 635b8c87fe..2924eb6699 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-agree-flag.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ["admin-flags-list"], _agreeFlag: function (actionOnPost) { diff --git a/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 index de26828eca..284b8bfd09 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-delete-flag.js.es6 @@ -1,8 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ["admin-flags-list"], actions: { diff --git a/app/assets/javascripts/admin/controllers/modals/admin-staff-action-log-details.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-staff-action-log-details.js.es6 index 9d525e9827..1e2ae5adca 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-staff-action-log-details.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-staff-action-log-details.js.es6 @@ -1,5 +1,3 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend(ModalFunctionality); +export default Ember.Controller.extend(ModalFunctionality); diff --git a/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 index 1af962e41c..682b1ba25d 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-start-backup.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import Controller from 'discourse/controllers/controller'; -export default Controller.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ["adminBackupsLogs"], _startBackup: function (withUploads) { diff --git a/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 b/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 index 49999aa092..d3e19de569 100644 --- a/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/admin-suspend-user.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { submitDisabled: function() { return (!this.get('reason') || this.get('reason').length < 1); diff --git a/app/assets/javascripts/admin/controllers/modals/change-site-customization-details.js.es6 b/app/assets/javascripts/admin/controllers/modals/change-site-customization-details.js.es6 index 1ef3e217d5..ca6ac31db1 100644 --- a/app/assets/javascripts/admin/controllers/modals/change-site-customization-details.js.es6 +++ b/app/assets/javascripts/admin/controllers/modals/change-site-customization-details.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { previousSelected: Ember.computed.equal('selectedTab', 'previous'), newSelected: Ember.computed.equal('selectedTab', 'new'), diff --git a/app/assets/javascripts/admin/models/flagged_post.js b/app/assets/javascripts/admin/models/flagged_post.js index 0696067806..e3a01948ca 100644 --- a/app/assets/javascripts/admin/models/flagged_post.js +++ b/app/assets/javascripts/admin/models/flagged_post.js @@ -47,7 +47,7 @@ Discourse.FlaggedPost = Discourse.Post.extend({ }, wasEdited: function () { - if (this.blank("last_revised_at")) { return false; } + if (Ember.isEmpty(this.get("last_revised_at"))) { return false; } var lastRevisedAt = Date.parse(this.get("last_revised_at")); return _.some(this.get("post_actions"), function (postAction) { return Date.parse(postAction.created_at) < lastRevisedAt; 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 98f7c3dfe2..9d164142aa 100644 --- a/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups-logs.js.es6 @@ -1,7 +1,7 @@ import debounce from 'discourse/lib/debounce'; import { renderSpinner } from 'discourse/helpers/loading-spinner'; -export default Discourse.View.extend({ +export default Ember.View.extend({ classNames: ["admin-backups-logs"], _initialize: function() { this._reset(); }.on("init"), diff --git a/app/assets/javascripts/admin/views/admin-backups.js.es6 b/app/assets/javascripts/admin/views/admin-backups.js.es6 index f0adae194e..a779f4971d 100644 --- a/app/assets/javascripts/admin/views/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/views/admin-backups.js.es6 @@ -1,6 +1,6 @@ import DiscourseURL from 'discourse/lib/url'; -export default Discourse.View.extend({ +export default Ember.View.extend({ classNames: ["admin-backups"], _hijackDownloads: function() { diff --git a/app/assets/javascripts/admin/views/admin-flags-list.js.es6 b/app/assets/javascripts/admin/views/admin-flags-list.js.es6 index 0f720b1e56..bcf3867a2f 100644 --- a/app/assets/javascripts/admin/views/admin-flags-list.js.es6 +++ b/app/assets/javascripts/admin/views/admin-flags-list.js.es6 @@ -1,6 +1,6 @@ import LoadMore from "discourse/mixins/load-more"; -export default Discourse.View.extend(LoadMore, { +export default Ember.View.extend(LoadMore, { loading: false, eyelineSelector: '.admin-flags tbody tr', diff --git a/app/assets/javascripts/admin/views/admin.js.es6 b/app/assets/javascripts/admin/views/admin.js.es6 index f503a34063..fc4a611fe4 100644 --- a/app/assets/javascripts/admin/views/admin.js.es6 +++ b/app/assets/javascripts/admin/views/admin.js.es6 @@ -1,4 +1,4 @@ -export default Discourse.View.extend({ +export default Ember.View.extend({ _disableCustomStylesheets: function() { if (this.session.get("disableCustomCSS")) { $("link.custom-css").attr("rel", ""); diff --git a/app/assets/javascripts/admin/views/admin_customize_colors_view.js b/app/assets/javascripts/admin/views/admin_customize_colors_view.js index e503a6f6de..b03e8eb767 100644 --- a/app/assets/javascripts/admin/views/admin_customize_colors_view.js +++ b/app/assets/javascripts/admin/views/admin_customize_colors_view.js @@ -1,11 +1,3 @@ -/** - A view to handle color selections within a site customization - - @class AdminCustomizeColorsView - @extends Discourse.View - @namespace Discourse - @module Discourse -**/ -Discourse.AdminCustomizeColorsView = Discourse.View.extend({ +Discourse.AdminCustomizeColorsView = Ember.View.extend({ templateName: 'admin/templates/customize_colors' }); diff --git a/app/assets/javascripts/admin/views/admin_user_view.js b/app/assets/javascripts/admin/views/admin_user_view.js index 0d430af4f9..997eaf3b2c 100644 --- a/app/assets/javascripts/admin/views/admin_user_view.js +++ b/app/assets/javascripts/admin/views/admin_user_view.js @@ -1,10 +1 @@ -/** - The view class for an Admin User - - @class AdminUserView - @extends Discourse.View - @namespace Discourse - @module Discourse -**/ -Discourse.AdminUserView = Discourse.View.extend(Discourse.ScrollTop); - +Discourse.AdminUserView = Ember.View.extend(Discourse.ScrollTop); diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 70a5af9e4a..3d548b5fa1 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -155,3 +155,9 @@ function proxyDep(propName, moduleFunc, msg) { proxyDep('computed', function() { return require('discourse/lib/computed') }); proxyDep('Formatter', function() { return require('discourse/lib/formatter') }); proxyDep('PageTracker', function() { return require('discourse/lib/page-tracker').default }); +proxyDep('URL', function() { return require('discourse/lib/url').default }); +proxyDep('Quote', function() { return require('discourse/lib/quote').default }); +proxyDep('debounce', function() { return require('discourse/lib/debounce').default }); +proxyDep('View', function() { return Ember.View }, "Use `Ember.View` instead"); +proxyDep('Controller', function() { return Ember.Controller }, "Use `Ember.Controller` instead"); +proxyDep('ObjectController', function() { return Ember.ObjectController }, "Use `Ember.Controller` instead"); diff --git a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 index 6cdab98db9..f0bb9fcb3f 100644 --- a/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/avatar-selector.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DiscourseController from 'discourse/controllers/controller'; -export default DiscourseController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { uploadedAvatarTemplate: null, saveDisabled: Em.computed.alias("uploading"), hasUploadedAvatar: Em.computed.or('uploadedAvatarTemplate', 'custom_avatar_upload_id'), diff --git a/app/assets/javascripts/discourse/controllers/badges/show.js.es6 b/app/assets/javascripts/discourse/controllers/badges/show.js.es6 index 77bfa341bf..1083fd37f1 100644 --- a/app/assets/javascripts/discourse/controllers/badges/show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/badges/show.js.es6 @@ -1,6 +1,4 @@ -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend({ +export default Ember.Controller.extend({ noMoreBadges: false, userBadges: null, needs: ["application"], diff --git a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 b/app/assets/javascripts/discourse/controllers/change-owner.js.es6 index 611498268d..39ae9cddcc 100644 --- a/app/assets/javascripts/discourse/controllers/change-owner.js.es6 +++ b/app/assets/javascripts/discourse/controllers/change-owner.js.es6 @@ -1,10 +1,9 @@ -import Presence from 'discourse/mixins/presence'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import DiscourseURL from 'discourse/lib/url'; // Modal related to changing the ownership of posts -export default Ember.Controller.extend(Presence, SelectedPostsCount, ModalFunctionality, { +export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, { needs: ['topic'], topicController: Em.computed.alias('controllers.topic'), @@ -14,7 +13,7 @@ export default Ember.Controller.extend(Presence, SelectedPostsCount, ModalFuncti buttonDisabled: function() { if (this.get('saving')) return true; - return this.blank('new_user'); + return Ember.isEmpty(this.get('new_user')); }.property('saving', 'new_user'), buttonTitle: function() { diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index c3a8a6827a..77ecedadde 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -1,10 +1,9 @@ import { setting } from 'discourse/lib/computed'; -import Presence from 'discourse/mixins/presence'; import DiscourseURL from 'discourse/lib/url'; import Quote from 'discourse/lib/quote'; import Draft from 'discourse/models/draft'; -export default Ember.ObjectController.extend(Presence, { +export default Ember.Controller.extend({ needs: ['modal', 'topic', 'composer-messages', 'application'], replyAsNewTopicDraft: Em.computed.equal('model.draftKey', Discourse.Composer.REPLY_AS_NEW_TOPIC_KEY), @@ -152,7 +151,7 @@ export default Ember.ObjectController.extend(Presence, { this.closeAutocomplete(); switch (this.get('model.composeState')) { case Discourse.Composer.OPEN: - if (this.blank('model.reply') && this.blank('model.title')) { + if (Ember.isEmpty(this.get('model.reply')) && Ember.isEmpty(this.get('model.title'))) { this.close(); } else { this.shrink(); diff --git a/app/assets/javascripts/discourse/controllers/controller.js.es6 b/app/assets/javascripts/discourse/controllers/controller.js.es6 deleted file mode 100644 index 0be72a02a4..0000000000 --- a/app/assets/javascripts/discourse/controllers/controller.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -import Presence from 'discourse/mixins/presence'; - -export default Ember.Controller.extend(Presence); diff --git a/app/assets/javascripts/discourse/controllers/create-account.js.es6 b/app/assets/javascripts/discourse/controllers/create-account.js.es6 index 052a8bd4e2..b93363cb3b 100644 --- a/app/assets/javascripts/discourse/controllers/create-account.js.es6 +++ b/app/assets/javascripts/discourse/controllers/create-account.js.es6 @@ -1,9 +1,8 @@ import debounce from 'discourse/lib/debounce'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DiscourseController from 'discourse/controllers/controller'; import { setting } from 'discourse/lib/computed'; -export default DiscourseController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ['login'], uniqueUsernameValidation: null, @@ -66,7 +65,7 @@ export default DiscourseController.extend(ModalFunctionality, { usernameRequired: Ember.computed.not('authOptions.omit_username'), passwordRequired: function() { - return this.blank('authOptions.auth_provider'); + return Ember.isEmpty(this.get('authOptions.auth_provider')); }.property('authOptions.auth_provider'), passwordInstructions: function() { @@ -83,7 +82,7 @@ export default DiscourseController.extend(ModalFunctionality, { this.fetchConfirmationValue(); } - if (Discourse.SiteSettings.full_name_required && this.blank('accountName')) { + if (Discourse.SiteSettings.full_name_required && Ember.isEmpty(this.get('accountName'))) { return Discourse.InputValidation.create({ failed: true }); } @@ -94,7 +93,7 @@ export default DiscourseController.extend(ModalFunctionality, { emailValidation: function() { // If blank, fail without a reason let email; - if (this.blank('accountEmail')) { + if (Ember.isEmpty(this.get('accountEmail'))) { return Discourse.InputValidation.create({ failed: true }); @@ -144,7 +143,7 @@ export default DiscourseController.extend(ModalFunctionality, { } this.set('prefilledUsername', null); } - if (this.get('emailValidation.ok') && (this.blank('accountUsername') || this.get('authOptions.email'))) { + if (this.get('emailValidation.ok') && (Ember.isEmpty(this.get('accountUsername')) || this.get('authOptions.email'))) { // If email is valid and username has not been entered yet, // or email and username were filled automatically by 3rd parth auth, // then look for a registered username that matches the email. @@ -155,7 +154,7 @@ export default DiscourseController.extend(ModalFunctionality, { fetchExistingUsername: debounce(function() { const self = this; Discourse.User.checkUsername(null, this.get('accountEmail')).then(function(result) { - if (result.suggestion && (self.blank('accountUsername') || self.get('accountUsername') === self.get('authOptions.username'))) { + if (result.suggestion && (Ember.isEmpty(self.get('accountUsername')) || self.get('accountUsername') === self.get('authOptions.username'))) { self.set('accountUsername', result.suggestion); self.set('prefilledUsername', result.suggestion); } @@ -194,7 +193,7 @@ export default DiscourseController.extend(ModalFunctionality, { } // If blank, fail without a reason - if (this.blank('accountUsername')) { + if (Ember.isEmpty(this.get('accountUsername'))) { return Discourse.InputValidation.create({ failed: true }); @@ -225,7 +224,7 @@ export default DiscourseController.extend(ModalFunctionality, { }.property('accountUsername'), shouldCheckUsernameMatch: function() { - return !this.blank('accountUsername') && this.get('accountUsername').length >= this.get('minUsernameLength'); + return !Ember.isEmpty(this.get('accountUsername')) && this.get('accountUsername').length >= this.get('minUsernameLength'); }, checkUsernameAvailability: debounce(function() { @@ -296,7 +295,7 @@ export default DiscourseController.extend(ModalFunctionality, { // If blank, fail without a reason const password = this.get("accountPassword"); - if (this.blank('accountPassword')) { + if (Ember.isEmpty(this.get('accountPassword'))) { return Discourse.InputValidation.create({ failed: true }); } @@ -315,14 +314,14 @@ export default DiscourseController.extend(ModalFunctionality, { }); } - if (!this.blank('accountUsername') && this.get('accountPassword') === this.get('accountUsername')) { + if (!Ember.isEmpty(this.get('accountUsername')) && this.get('accountPassword') === this.get('accountUsername')) { return Discourse.InputValidation.create({ failed: true, reason: I18n.t('user.password.same_as_username') }); } - if (!this.blank('accountEmail') && this.get('accountPassword') === this.get('accountEmail')) { + if (!Ember.isEmpty(this.get('accountEmail')) && this.get('accountPassword') === this.get('accountEmail')) { return Discourse.InputValidation.create({ failed: true, reason: I18n.t('user.password.same_as_email') diff --git a/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 b/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 index 56d4d3a90d..3e7c2e52a9 100644 --- a/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery-sortable.js.es6 @@ -1,5 +1,3 @@ -import DiscourseController from 'discourse/controllers/controller'; - // Just add query params here to have them automatically passed to topic list filters. export var queryParams = { order: { replace: true, refreshModel: true }, @@ -22,4 +20,4 @@ controllerOpts.queryParams.forEach(function(p) { controllerOpts[p] = Em.computed.alias('controllers.discovery/topics.' + p); }); -export default DiscourseController.extend(controllerOpts); +export default Ember.Controller.extend(controllerOpts); diff --git a/app/assets/javascripts/discourse/controllers/discovery.js.es6 b/app/assets/javascripts/discourse/controllers/discovery.js.es6 index 69e51d55d5..2be7bfd206 100644 --- a/app/assets/javascripts/discourse/controllers/discovery.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery.js.es6 @@ -1,6 +1,6 @@ import DiscourseURL from 'discourse/lib/url'; -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ needs: ['navigation/category', 'discovery/topics', 'application'], loading: false, diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 index 9e7c637d61..bfda086b64 100644 --- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 @@ -1,9 +1,8 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; import DiscourseURL from 'discourse/lib/url'; // Modal for editing / creating a category -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { selectedTab: null, saving: false, deleting: false, @@ -19,7 +18,7 @@ export default ObjectController.extend(ModalFunctionality, { }, changeSize: function() { - if (this.present('model.description')) { + if (!Ember.isEmpty(this.get('model.description'))) { this.set('controllers.modal.modalClass', 'edit-category-modal full'); } else { this.set('controllers.modal.modalClass', 'edit-category-modal small'); 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 7fa4458b45..6958ff446d 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,8 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; // Modal related to auto closing of topics -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { auto_close_valid: true, auto_close_invalid: Em.computed.not('auto_close_valid'), diff --git a/app/assets/javascripts/discourse/controllers/exception.js.es6 b/app/assets/javascripts/discourse/controllers/exception.js.es6 index ccaf907c14..b2ff890eea 100644 --- a/app/assets/javascripts/discourse/controllers/exception.js.es6 +++ b/app/assets/javascripts/discourse/controllers/exception.js.es6 @@ -1,5 +1,3 @@ -import ObjectController from 'discourse/controllers/object'; - var ButtonBackBright = { classes: "btn-primary", action: "back", @@ -22,7 +20,7 @@ var ButtonBackBright = { }; // The controller for the nice error page -export default ObjectController.extend({ +export default Ember.Controller.extend({ thrown: null, lastTransition: null, diff --git a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 index 990d2b83ae..03bcebfcad 100644 --- a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 @@ -1,8 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; import { categoryLinkHTML } from 'discourse/helpers/category-link'; -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ["topic"], loading: true, diff --git a/app/assets/javascripts/discourse/controllers/flag-action-type.js.es6 b/app/assets/javascripts/discourse/controllers/flag-action-type.js.es6 index a5eba0df84..25912e7a22 100644 --- a/app/assets/javascripts/discourse/controllers/flag-action-type.js.es6 +++ b/app/assets/javascripts/discourse/controllers/flag-action-type.js.es6 @@ -1,8 +1,7 @@ -import ObjectController from 'discourse/controllers/object'; import { MAX_MESSAGE_LENGTH } from 'discourse/models/post-action-type'; // Supports logic for flags in the modal -export default ObjectController.extend({ +export default Ember.Controller.extend({ needs: ['flag'], message: Em.computed.alias('controllers.flag.message'), diff --git a/app/assets/javascripts/discourse/controllers/flag.js.es6 b/app/assets/javascripts/discourse/controllers/flag.js.es6 index 6a80da273b..956cbe2357 100644 --- a/app/assets/javascripts/discourse/controllers/flag.js.es6 +++ b/app/assets/javascripts/discourse/controllers/flag.js.es6 @@ -1,8 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; import { MAX_MESSAGE_LENGTH } from 'discourse/models/post-action-type'; -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { userDetails: null, selected: null, flagTopic: null, diff --git a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 index 6d38365bd7..64da750cfe 100644 --- a/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 +++ b/app/assets/javascripts/discourse/controllers/forgot-password.js.es6 @@ -1,11 +1,10 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DiscourseController from 'discourse/controllers/controller'; -export default DiscourseController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { // You need a value in the field to submit it. submitDisabled: function() { - return this.blank('accountEmailOrUsername') || this.get('disabled'); + return Ember.isEmpty(this.get('accountEmailOrUsername')) || this.get('disabled'); }.property('accountEmailOrUsername', 'disabled'), actions: { 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 4f3c38b629..86e240febf 100644 --- a/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/full-page-search.js.es6 @@ -1,7 +1,6 @@ -import DiscourseController from "discourse/controllers/controller"; import { translateResults } from "discourse/lib/search-for-term"; -export default DiscourseController.extend({ +export default Ember.Controller.extend({ needs: ["application"], loading: Em.computed.not("model"), diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6 index c414227b6b..d2bc112186 100644 --- a/app/assets/javascripts/discourse/controllers/header.js.es6 +++ b/app/assets/javascripts/discourse/controllers/header.js.es6 @@ -1,6 +1,4 @@ -import DiscourseController from 'discourse/controllers/controller'; - -const HeaderController = DiscourseController.extend({ +const HeaderController = Ember.Controller.extend({ topic: null, showExtraInfo: null, notifications: null, diff --git a/app/assets/javascripts/discourse/controllers/history.js.es6 b/app/assets/javascripts/discourse/controllers/history.js.es6 index e3603089e9..bef9a4c965 100644 --- a/app/assets/javascripts/discourse/controllers/history.js.es6 +++ b/app/assets/javascripts/discourse/controllers/history.js.es6 @@ -1,9 +1,8 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; // This controller handles displaying of history -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { loading: true, viewMode: "side_by_side", revisionsTextKey: "post.revisions.controls.comparing_previous_to_current_out_of_total", diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index 660bcdc89e..b9173b8757 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -1,9 +1,7 @@ -import Presence from 'discourse/mixins/presence'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; import Invite from 'discourse/models/invite'; -export default ObjectController.extend(Presence, ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ['user-invited-show'], // If this isn't defined, it will proxy to the user model on the preferences @@ -16,14 +14,14 @@ export default ObjectController.extend(Presence, ModalFunctionality, { disabled: function() { if (this.get('model.saving')) return true; - if (this.blank('emailOrUsername')) return true; + if (Ember.isEmpty(this.get('emailOrUsername'))) return true; const emailOrUsername = this.get('emailOrUsername').trim(); // when inviting to forum, email must be valid if (!this.get('invitingToTopic') && !Discourse.Utilities.emailValid(emailOrUsername)) return true; // normal users (not admin) can't invite users to private topic via email if (!this.get('isAdmin') && this.get('isPrivateTopic') && Discourse.Utilities.emailValid(emailOrUsername)) return true; // when invting to private topic via email, group name must be specified - if (this.get('isPrivateTopic') && this.blank('model.groupNames') && Discourse.Utilities.emailValid(emailOrUsername)) return true; + if (this.get('isPrivateTopic') && Ember.isEmpty(this.get('model.groupNames')) && Discourse.Utilities.emailValid(emailOrUsername)) return true; if (this.get('model.details.can_invite_to')) return false; return false; }.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'), @@ -71,7 +69,7 @@ export default ObjectController.extend(Presence, ModalFunctionality, { return I18n.t('topic.invite_reply.to_username'); } else { // when inviting to a topic, display instructions based on provided entity - if (this.blank('emailOrUsername')) { + if (Ember.isEmpty(this.get('emailOrUsername'))) { return I18n.t('topic.invite_reply.to_topic_blank'); } else if (Discourse.Utilities.emailValid(this.get('emailOrUsername'))) { return I18n.t('topic.invite_reply.to_topic_email'); diff --git a/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js.es6 b/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js.es6 index 30d4a00664..5e0414bcd0 100644 --- a/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js.es6 +++ b/app/assets/javascripts/discourse/controllers/keyboard-shortcuts-help.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DiscourseController from 'discourse/controllers/controller'; -export default DiscourseController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ['modal'], onShow: function() { diff --git a/app/assets/javascripts/discourse/controllers/login.js.es6 b/app/assets/javascripts/discourse/controllers/login.js.es6 index 6d39640e5b..824c3110f0 100644 --- a/app/assets/javascripts/discourse/controllers/login.js.es6 +++ b/app/assets/javascripts/discourse/controllers/login.js.es6 @@ -1,5 +1,4 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DiscourseController from 'discourse/controllers/controller'; import showModal from 'discourse/lib/show-modal'; import { setting } from 'discourse/lib/computed'; @@ -8,7 +7,7 @@ const AuthErrors = ['requires_invite', 'awaiting_approval', 'awaiting_confirmation', 'admin_not_allowed_from_ip_address', 'not_allowed_from_ip_address']; -export default DiscourseController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ['modal', 'createAccount', 'forgotPassword', 'application'], authenticate: null, loggingIn: false, @@ -39,7 +38,7 @@ export default DiscourseController.extend(ModalFunctionality, { showSignupLink: function() { return this.get('controllers.application.canSignUp') && !this.get('loggingIn') && - this.blank('authenticate'); + Ember.isEmpty(this.get('authenticate')); }.property('loggingIn', 'authenticate'), showSpinner: function() { @@ -50,7 +49,7 @@ export default DiscourseController.extend(ModalFunctionality, { login: function() { const self = this; - if(this.blank('loginName') || this.blank('loginPassword')){ + if(Ember.isEmpty(this.get('loginName')) || Ember.isEmpty(this.get('loginPassword'))){ self.flash(I18n.t('login.blank_username_or_password'), 'error'); return; } @@ -154,7 +153,7 @@ export default DiscourseController.extend(ModalFunctionality, { }, authMessage: (function() { - if (this.blank('authenticate')) return ""; + if (Ember.isEmpty(this.get('authenticate'))) return ""; const method = Discourse.get('LoginMethod.all').findProperty("name", this.get("authenticate")); if(method){ return method.get('message'); diff --git a/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 b/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 index f81e0de676..197c9fcb37 100644 --- a/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/merge-topic.js.es6 @@ -1,11 +1,10 @@ -import Presence from 'discourse/mixins/presence'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { movePosts, mergeTopic } from 'discourse/models/topic'; import DiscourseURL from 'discourse/lib/url'; // Modal related to merging of topics -export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, Presence, { +export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, { needs: ['topic'], saving: false, @@ -18,7 +17,7 @@ export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, P buttonDisabled: function() { if (this.get('saving')) return true; - return this.blank('selectedTopicId'); + return Ember.isEmpty(this.get('selectedTopicId')); }.property('selectedTopicId', 'saving'), buttonTitle: function() { diff --git a/app/assets/javascripts/discourse/controllers/modal.js.es6 b/app/assets/javascripts/discourse/controllers/modal.js.es6 index c744bdfc36..77c79b724a 100644 --- a/app/assets/javascripts/discourse/controllers/modal.js.es6 +++ b/app/assets/javascripts/discourse/controllers/modal.js.es6 @@ -1,3 +1 @@ -import DiscourseController from 'discourse/controllers/controller'; - -export default DiscourseController.extend({}); +export default Ember.Controller.extend(); diff --git a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 index 7832d4ab51..c2a48c4b7a 100644 --- a/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 +++ b/app/assets/javascripts/discourse/controllers/navigation/default.js.es6 @@ -1,6 +1,4 @@ -import DiscourseController from 'discourse/controllers/controller'; - -export default DiscourseController.extend({ +export default Ember.Controller.extend({ needs: ['discovery', 'discovery/topics'], categories: function() { diff --git a/app/assets/javascripts/discourse/controllers/not-activated.js.es6 b/app/assets/javascripts/discourse/controllers/not-activated.js.es6 index dd53320766..2c2e2b0758 100644 --- a/app/assets/javascripts/discourse/controllers/not-activated.js.es6 +++ b/app/assets/javascripts/discourse/controllers/not-activated.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DiscourseController from 'discourse/controllers/controller'; -export default DiscourseController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { emailSent: false, onShow() { diff --git a/app/assets/javascripts/discourse/controllers/object.js.es6 b/app/assets/javascripts/discourse/controllers/object.js.es6 deleted file mode 100644 index 7351a0a174..0000000000 --- a/app/assets/javascripts/discourse/controllers/object.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -import Presence from 'discourse/mixins/presence'; - -export default Ember.ObjectController.extend(Presence); diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index cacbd4b9e1..3a0bd90fe6 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -1,9 +1,8 @@ import { setting } from 'discourse/lib/computed'; -import ObjectController from 'discourse/controllers/object'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; import { popupAjaxError } from 'discourse/lib/ajax-error'; -export default ObjectController.extend(CanCheckEmails, { +export default Ember.Controller.extend(CanCheckEmails, { allowAvatarUpload: setting('allow_uploaded_avatars'), allowUserLocale: setting('allow_user_locale'), diff --git a/app/assets/javascripts/discourse/controllers/preferences/about.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/about.js.es6 index 742666333e..8d46eca005 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/about.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/about.js.es6 @@ -1,6 +1,4 @@ -import ObjectController from 'discourse/controllers/object'; - -export default ObjectController.extend({ +export default Ember.Controller.extend({ saving: false, newBio: null, diff --git a/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 index 547a9adbd7..83262902d1 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/email.js.es6 @@ -1,7 +1,6 @@ import { propertyEqual } from 'discourse/lib/computed'; -import ObjectController from 'discourse/controllers/object'; -export default ObjectController.extend({ +export default Ember.Controller.extend({ taken: false, saving: false, error: false, diff --git a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 index 410fabe4d5..c1976a91fb 100644 --- a/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences/username.js.es6 @@ -1,9 +1,7 @@ import { setting, propertyEqual } from 'discourse/lib/computed'; -import Presence from 'discourse/mixins/presence'; -import ObjectController from 'discourse/controllers/object'; import DiscourseURL from 'discourse/lib/url'; -export default ObjectController.extend(Presence, { +export default Ember.Controller.extend({ taken: false, saving: false, error: false, @@ -23,7 +21,7 @@ export default ObjectController.extend(Presence, { var self = this; this.set('taken', false); this.set('errorMessage', null); - if (this.blank('newUsername')) return; + if (Ember.isEmpty(this.get('newUsername'))) return; if (this.get('unchanged')) return; Discourse.User.checkUsername(this.get('newUsername'), undefined, this.get('content.id')).then(function(result) { if (result.errors) { diff --git a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 index bb255f8d2f..cfdd30b520 100644 --- a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 @@ -1,8 +1,7 @@ -import DiscourseController from 'discourse/controllers/controller'; import loadScript from 'discourse/lib/load-script'; import Quote from 'discourse/lib/quote'; -export default DiscourseController.extend({ +export default Ember.Controller.extend({ needs: ['topic', 'composer'], _loadSanitizer: function() { @@ -11,7 +10,7 @@ export default DiscourseController.extend({ // If the buffer is cleared, clear out other state (post) bufferChanged: function() { - if (this.blank('buffer')) this.set('post', null); + if (Ember.isEmpty(this.get('buffer'))) this.set('post', null); }.observes('buffer'), // Save the currently selected text and displays the diff --git a/app/assets/javascripts/discourse/controllers/raw-email.js.es6 b/app/assets/javascripts/discourse/controllers/raw-email.js.es6 index 8c74ce1110..caf410fd5c 100644 --- a/app/assets/javascripts/discourse/controllers/raw-email.js.es6 +++ b/app/assets/javascripts/discourse/controllers/raw-email.js.es6 @@ -1,8 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import ObjectController from 'discourse/controllers/object'; // This controller handles displaying of raw email -export default ObjectController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { rawEmail: "", loadRawEmail: function(postId) { diff --git a/app/assets/javascripts/discourse/controllers/search-help.js.es6 b/app/assets/javascripts/discourse/controllers/search-help.js.es6 index 2630424ff4..7f5958ae4d 100644 --- a/app/assets/javascripts/discourse/controllers/search-help.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search-help.js.es6 @@ -1,7 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DiscourseController from 'discourse/controllers/controller'; -export default DiscourseController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { needs: ['modal'], showGoogleSearch: function() { diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 71d650726d..eff99ec98e 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -1,10 +1,9 @@ -import Presence from 'discourse/mixins/presence'; import searchForTerm from 'discourse/lib/search-for-term'; import DiscourseURL from 'discourse/lib/url'; let _dontSearch = false; -export default Em.Controller.extend(Presence, { +export default Em.Controller.extend({ typeFilter: null, contextType: function(key, value){ @@ -115,7 +114,7 @@ export default Em.Controller.extend(Presence, { showCancelFilter: function() { if (this.get('loading')) return false; - return this.present('typeFilter'); + return !Ember.isEmpty(this.get('typeFilter')); }.property('typeFilter', 'loading'), termChanged: function() { diff --git a/app/assets/javascripts/discourse/controllers/split-topic.js.es6 b/app/assets/javascripts/discourse/controllers/split-topic.js.es6 index dc391497c4..b8a1a72509 100644 --- a/app/assets/javascripts/discourse/controllers/split-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/split-topic.js.es6 @@ -1,4 +1,3 @@ -import Presence from 'discourse/mixins/presence'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { extractError } from 'discourse/lib/ajax-error'; @@ -6,7 +5,7 @@ import { movePosts } from 'discourse/models/topic'; import DiscourseURL from 'discourse/lib/url'; // Modal related to auto closing of topics -export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, Presence, { +export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, { needs: ['topic'], topicName: null, saving: false, @@ -19,7 +18,7 @@ export default Ember.Controller.extend(SelectedPostsCount, ModalFunctionality, P buttonDisabled: function() { if (this.get('saving')) return true; - return this.blank('topicName'); + return Ember.isEmpty(this.get('topicName')); }.property('saving', 'topicName'), buttonTitle: function() { diff --git a/app/assets/javascripts/discourse/controllers/topic-admin-menu.js.es6 b/app/assets/javascripts/discourse/controllers/topic-admin-menu.js.es6 index 20bb123a0c..4e454b8523 100644 --- a/app/assets/javascripts/discourse/controllers/topic-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-admin-menu.js.es6 @@ -1,7 +1,5 @@ -import ObjectController from 'discourse/controllers/object'; - // This controller supports the admin menu on topics -export default ObjectController.extend({ +export default Ember.Controller.extend({ menuVisible: false, showRecover: Em.computed.and('model.deleted', 'model.details.can_recover'), isFeatured: Em.computed.or("model.pinned_at", "model.isBanner"), diff --git a/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 b/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 index 92f483df75..39e9ff2b7e 100644 --- a/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic-progress.js.es6 @@ -1,6 +1,6 @@ import DiscourseURL from 'discourse/lib/url'; -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ needs: ['topic'], progressPosition: null, expanded: false, diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 545549ffcb..736ad9415b 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -1,4 +1,3 @@ -import ObjectController from 'discourse/controllers/object'; import BufferedContent from 'discourse/mixins/buffered-content'; import SelectedPostsCount from 'discourse/mixins/selected-posts-count'; import { spinnerHTML } from 'discourse/helpers/loading-spinner'; @@ -6,7 +5,7 @@ import Topic from 'discourse/models/topic'; import Quote from 'discourse/lib/quote'; import { setting } from 'discourse/lib/computed'; -export default ObjectController.extend(SelectedPostsCount, BufferedContent, { +export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { multiSelect: false, needs: ['header', 'modal', 'composer', 'quote-button', 'search', 'topic-progress', 'application'], allPostsSelected: false, diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 index be9d21dafb..2857b2a5c8 100644 --- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 @@ -1,8 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import DiscourseController from 'discourse/controllers/controller'; import { setting } from 'discourse/lib/computed'; -export default DiscourseController.extend(ModalFunctionality, { +export default Ember.Controller.extend(ModalFunctionality, { remote: Em.computed.not("local"), local: false, showMore: false, diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 index 7e4ec2db92..9e6a6530df 100644 --- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 @@ -1,4 +1,4 @@ -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ userActionType: null, needs: ["application"], diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index 79131dbf10..781d9be9ab 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -2,7 +2,7 @@ import Invite from 'discourse/models/invite'; import debounce from 'discourse/lib/debounce'; // This controller handles actions related to a user's invitations -export default Ember.ObjectController.extend({ +export default Ember.Controller.extend({ user: null, model: null, filter: null, diff --git a/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 b/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 index 88b8b98c8c..a0073b7c0e 100644 --- a/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-topics-list.js.es6 @@ -1,7 +1,5 @@ -import ObjectController from 'discourse/controllers/object'; - // Lists of topics on a user's page. -export default ObjectController.extend({ +export default Ember.Controller.extend({ needs: ["application", "user"], hideCategory: false, showParticipants: false, diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 419e6f8a2f..7159c146ce 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -1,8 +1,7 @@ import { exportUserArchive } from 'discourse/lib/export-csv'; -import ObjectController from 'discourse/controllers/object'; import CanCheckEmails from 'discourse/mixins/can-check-emails'; -export default ObjectController.extend(CanCheckEmails, { +export default Ember.Controller.extend(CanCheckEmails, { indexStream: false, pmView: false, userActionType: null, diff --git a/app/assets/javascripts/discourse/initializers/es6-deprecations.js.es6 b/app/assets/javascripts/discourse/initializers/es6-deprecations.js.es6 deleted file mode 100644 index 93b6d61b65..0000000000 --- a/app/assets/javascripts/discourse/initializers/es6-deprecations.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -import DiscourseURL from 'discourse/lib/url'; -import Quote from 'discourse/lib/quote'; -import debounce from 'discourse/lib/debounce'; - -function proxyDep(propName, module) { - if (Discourse.hasOwnProperty(propName)) { return; } - Object.defineProperty(Discourse, propName, { - get: function() { - Ember.warn(`DEPRECATION: \`Discourse.${propName}\` is deprecated, import the module.`); - return module; - } - }); -} - -export default { - name: 'es6-deprecations', - before: 'inject-objects', - - initialize: function() { - // TODO: Once things have migrated remove these - proxyDep('computed', require('discourse/lib/computed')); - proxyDep('Formatter', require('discourse/lib/formatter')); - proxyDep('URL', DiscourseURL); - proxyDep('Quote', Quote); - proxyDep('debounce', debounce); - } -}; diff --git a/app/assets/javascripts/discourse/mixins/presence.js.es6 b/app/assets/javascripts/discourse/mixins/presence.js.es6 deleted file mode 100644 index 742f50f402..0000000000 --- a/app/assets/javascripts/discourse/mixins/presence.js.es6 +++ /dev/null @@ -1,20 +0,0 @@ -/** - This mixin provides `blank` and `present` to determine whether properties are - there, accounting for more cases than just null and undefined. -**/ -export default Ember.Mixin.create({ - - /** - Returns whether a property is blank. It considers empty arrays, string, objects, undefined and null - to be blank, otherwise true. - */ - blank(name) { - return Ember.isEmpty(this[name] || this.get(name)); - }, - - // Returns whether a property is present. A present property is the opposite of a `blank` one. - present(name) { - return !this.blank(name); - } - -}); diff --git a/app/assets/javascripts/discourse/models/model.js.es6 b/app/assets/javascripts/discourse/models/model.js.es6 index 4a5209bee5..d7e3e650b5 100644 --- a/app/assets/javascripts/discourse/models/model.js.es6 +++ b/app/assets/javascripts/discourse/models/model.js.es6 @@ -1,6 +1,4 @@ -import Presence from 'discourse/mixins/presence'; - -const Model = Ember.Object.extend(Presence); +const Model = Ember.Object.extend(); Model.reopenClass({ extractByKey: function(collection, klass) { diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 17ac32b052..878ce21c08 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -97,7 +97,7 @@ const Post = RestModel.extend({ }, internalLinks: function() { - if (this.blank('link_counts')) return null; + if (Ember.isEmpty(this.get('link_counts'))) return null; return this.get('link_counts').filterProperty('internal').filterProperty('title'); }.property('link_counts.@each.internal'), @@ -112,7 +112,7 @@ const Post = RestModel.extend({ }.property('actions_summary.@each.can_act'), actionsWithoutLikes: function() { - if (!this.present('actions_summary')) return null; + if (!!Ember.isEmpty(this.get('actions_summary'))) return null; return this.get('actions_summary').filter(function(i) { if (i.get('count') === 0) return false; diff --git a/app/assets/javascripts/discourse/models/rest.js.es6 b/app/assets/javascripts/discourse/models/rest.js.es6 index 6396f8e548..0c595bb735 100644 --- a/app/assets/javascripts/discourse/models/rest.js.es6 +++ b/app/assets/javascripts/discourse/models/rest.js.es6 @@ -1,6 +1,4 @@ -import Presence from 'discourse/mixins/presence'; - -const RestModel = Ember.Object.extend(Presence, { +const RestModel = Ember.Object.extend({ isNew: Ember.computed.equal('__state', 'new'), isCreated: Ember.computed.equal('__state', 'created'), isSaving: false, diff --git a/app/assets/javascripts/discourse/models/topic-details.js.es6 b/app/assets/javascripts/discourse/models/topic-details.js.es6 index 9c8ca30e27..c40213a72e 100644 --- a/app/assets/javascripts/discourse/models/topic-details.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-details.js.es6 @@ -35,7 +35,7 @@ const TopicDetails = RestModel.extend({ }, fewParticipants: function() { - if (!this.present('participants')) return null; + if (!!Ember.isEmpty(this.get('participants'))) return null; return this.get('participants').slice(0, 3); }.property('participants'), diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 279073957c..669a3853b9 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -47,7 +47,7 @@ const User = RestModel.extend({ @type {String} **/ displayName: function() { - if (Discourse.SiteSettings.enable_names && !this.blank('name')) { + if (Discourse.SiteSettings.enable_names && !Ember.isEmpty(this.get('name'))) { return this.get('name'); } return this.get('username'); @@ -263,7 +263,7 @@ const User = RestModel.extend({ statsCountNonPM: function() { var self = this; - if (this.blank('statsExcludingPms')) return 0; + if (Ember.isEmpty(this.get('statsExcludingPms'))) return 0; var count = 0; _.each(this.get('statsExcludingPms'), function(val) { if (self.inAllStream(val)){ @@ -275,7 +275,7 @@ const User = RestModel.extend({ // The user's stats, excluding PMs. statsExcludingPms: function() { - if (this.blank('stats')) return []; + if (Ember.isEmpty(this.get('stats'))) return []; return this.get('stats').rejectProperty('isPM'); }.property('stats.@each.isPM'), diff --git a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 index 3c1c843b1f..f6d7216f60 100644 --- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 @@ -48,7 +48,7 @@ export default Discourse.Route.extend({ }); DiscourseURL.jumpToPost(closest); - if (topic.present('draft')) { + if (!Ember.isEmpty(topic.get('draft'))) { composerController.open({ draft: Draft.getLocal(topic.get('draft_key'), topic.get('draft')), draftKey: topic.get('draft_key'), diff --git a/app/assets/javascripts/discourse/views/badges-show.js.es6 b/app/assets/javascripts/discourse/views/badges-show.js.es6 index 9b74c3e3f9..9f94ae6a3a 100644 --- a/app/assets/javascripts/discourse/views/badges-show.js.es6 +++ b/app/assets/javascripts/discourse/views/badges-show.js.es6 @@ -1,6 +1,6 @@ import LoadMore from "discourse/mixins/load-more"; -export default Discourse.View.extend(LoadMore, { +export default Ember.View.extend(LoadMore, { eyelineSelector: '.badge-user', tickOrX: function(field){ var icon = this.get('controller.model.' + field) ? "fa-check" : "fa-times"; diff --git a/app/assets/javascripts/discourse/views/button.js.es6 b/app/assets/javascripts/discourse/views/button.js.es6 index a791db4a60..9811e71956 100644 --- a/app/assets/javascripts/discourse/views/button.js.es6 +++ b/app/assets/javascripts/discourse/views/button.js.es6 @@ -1,6 +1,6 @@ import StringBuffer from 'discourse/mixins/string-buffer'; -export default Discourse.View.extend(StringBuffer, { +export default Ember.View.extend(StringBuffer, { tagName: 'button', classNameBindings: [':btn', ':standard', 'dropDownToggle'], attributeBindings: ['title', 'data-toggle', 'data-share-url'], diff --git a/app/assets/javascripts/discourse/views/choose-topic.js.es6 b/app/assets/javascripts/discourse/views/choose-topic.js.es6 index b9733a5f21..7dbd8e4248 100644 --- a/app/assets/javascripts/discourse/views/choose-topic.js.es6 +++ b/app/assets/javascripts/discourse/views/choose-topic.js.es6 @@ -1,7 +1,7 @@ import debounce from 'discourse/lib/debounce'; import searchForTerm from 'discourse/lib/search-for-term'; -export default Discourse.View.extend({ +export default Ember.View.extend({ templateName: 'choose_topic', topicTitleChanged: function() { diff --git a/app/assets/javascripts/discourse/views/composer-messages.js.es6 b/app/assets/javascripts/discourse/views/composer-messages.js.es6 index 39f911ec78..4541ff90c6 100644 --- a/app/assets/javascripts/discourse/views/composer-messages.js.es6 +++ b/app/assets/javascripts/discourse/views/composer-messages.js.es6 @@ -4,7 +4,7 @@ export default Ember.CollectionView.extend({ hidden: Em.computed.not('controller.controllers.composer.model.viewOpen'), - itemViewClass: Discourse.View.extend({ + itemViewClass: Ember.View.extend({ classNames: ['composer-popup', 'hidden'], templateName: Em.computed.alias('content.templateName'), diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index b57a1a394b..b48964e271 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -6,7 +6,7 @@ import positioningWorkaround from 'discourse/lib/safari-hacks'; import debounce from 'discourse/lib/debounce'; import { linkSeenMentions, fetchUnseenMentions } from 'discourse/lib/link-mentions'; -const ComposerView = Discourse.View.extend(Ember.Evented, { +const ComposerView = Ember.View.extend(Ember.Evented, { _lastKeyTimeout: null, templateName: 'composer', elementId: 'reply-control', @@ -38,7 +38,7 @@ const ComposerView = Discourse.View.extend(Ember.Evented, { }.observes('loading'), postMade: function() { - return this.present('model.createdPost') ? 'created-post' : null; + return !Ember.isEmpty(this.get('model.createdPost')) ? 'created-post' : null; }.property('model.createdPost'), refreshPreview: debounce(function() { diff --git a/app/assets/javascripts/discourse/views/container.js.es6 b/app/assets/javascripts/discourse/views/container.js.es6 index 68c6533532..39d623c84a 100644 --- a/app/assets/javascripts/discourse/views/container.js.es6 +++ b/app/assets/javascripts/discourse/views/container.js.es6 @@ -1,6 +1,4 @@ -import Presence from 'discourse/mixins/presence'; - -export default Ember.ContainerView.extend(Presence, { +export default Ember.ContainerView.extend({ attachViewWithArgs(viewArgs, viewClass) { if (!viewClass) { viewClass = Ember.View.extend(); } diff --git a/app/assets/javascripts/discourse/views/discovery-categories.js.es6 b/app/assets/javascripts/discourse/views/discovery-categories.js.es6 index 4d111d66cd..59721af089 100644 --- a/app/assets/javascripts/discourse/views/discovery-categories.js.es6 +++ b/app/assets/javascripts/discourse/views/discovery-categories.js.es6 @@ -1,6 +1,6 @@ import UrlRefresh from 'discourse/mixins/url-refresh'; -export default Discourse.View.extend(UrlRefresh, { +export default Ember.View.extend(UrlRefresh, { _addBodyClass: function() { $('body').addClass('categories-list'); }.on('didInsertElement'), diff --git a/app/assets/javascripts/discourse/views/discovery-top.js.es6 b/app/assets/javascripts/discourse/views/discovery-top.js.es6 index 62950838e5..72d1e7167e 100644 --- a/app/assets/javascripts/discourse/views/discovery-top.js.es6 +++ b/app/assets/javascripts/discourse/views/discovery-top.js.es6 @@ -1,4 +1,4 @@ import UrlRefresh from 'discourse/mixins/url-refresh'; import ScrollTop from 'discourse/mixins/scroll-top'; -export default Discourse.View.extend(ScrollTop, UrlRefresh); +export default Ember.View.extend(ScrollTop, UrlRefresh); diff --git a/app/assets/javascripts/discourse/views/discovery-topics.js.es6 b/app/assets/javascripts/discourse/views/discovery-topics.js.es6 index 25b66239b6..448db126e5 100644 --- a/app/assets/javascripts/discourse/views/discovery-topics.js.es6 +++ b/app/assets/javascripts/discourse/views/discovery-topics.js.es6 @@ -1,7 +1,7 @@ import UrlRefresh from 'discourse/mixins/url-refresh'; import LoadMore from "discourse/mixins/load-more"; -export default Discourse.View.extend(LoadMore, UrlRefresh, { +export default Ember.View.extend(LoadMore, UrlRefresh, { eyelineSelector: '.topic-list-item', actions: { diff --git a/app/assets/javascripts/discourse/views/group-index.js.es6 b/app/assets/javascripts/discourse/views/group-index.js.es6 index f16ebf262b..4e25173395 100644 --- a/app/assets/javascripts/discourse/views/group-index.js.es6 +++ b/app/assets/javascripts/discourse/views/group-index.js.es6 @@ -1,6 +1,6 @@ import ScrollTop from 'discourse/mixins/scroll-top'; import LoadMore from "discourse/mixins/load-more"; -export default Discourse.View.extend(ScrollTop, LoadMore, { +export default Ember.View.extend(ScrollTop, LoadMore, { eyelineSelector: '.user-stream .item', }); diff --git a/app/assets/javascripts/discourse/views/group-members.js.es6 b/app/assets/javascripts/discourse/views/group-members.js.es6 index e3715a47e0..cae92ca87e 100644 --- a/app/assets/javascripts/discourse/views/group-members.js.es6 +++ b/app/assets/javascripts/discourse/views/group-members.js.es6 @@ -1,6 +1,6 @@ import ScrollTop from 'discourse/mixins/scroll-top'; import LoadMore from "discourse/mixins/load-more"; -export default Discourse.View.extend(ScrollTop, LoadMore, { +export default Ember.View.extend(ScrollTop, LoadMore, { eyelineSelector: '.group-members tr', }); diff --git a/app/assets/javascripts/discourse/views/grouped.js.es6 b/app/assets/javascripts/discourse/views/grouped.js.es6 index ed06f49b50..988c166127 100644 --- a/app/assets/javascripts/discourse/views/grouped.js.es6 +++ b/app/assets/javascripts/discourse/views/grouped.js.es6 @@ -1,6 +1,4 @@ -import Presence from 'discourse/mixins/presence'; - -export default Ember.View.extend(Presence, { +export default Ember.View.extend({ _groupInit: function() { this.set('context', this.get('content')); diff --git a/app/assets/javascripts/discourse/views/header.js.es6 b/app/assets/javascripts/discourse/views/header.js.es6 index 906c479fe4..281f6bd86b 100644 --- a/app/assets/javascripts/discourse/views/header.js.es6 +++ b/app/assets/javascripts/discourse/views/header.js.es6 @@ -1,6 +1,6 @@ let originalZIndex; -export default Discourse.View.extend({ +export default Ember.View.extend({ tagName: 'header', classNames: ['d-header', 'clearfix'], classNameBindings: ['editingTopic'], diff --git a/app/assets/javascripts/discourse/views/modal-body.js.es6 b/app/assets/javascripts/discourse/views/modal-body.js.es6 index ee621d5e31..c5807ea32b 100644 --- a/app/assets/javascripts/discourse/views/modal-body.js.es6 +++ b/app/assets/javascripts/discourse/views/modal-body.js.es6 @@ -1,4 +1,4 @@ -export default Discourse.View.extend({ +export default Ember.View.extend({ focusInput: true, _setupModal: function() { diff --git a/app/assets/javascripts/discourse/views/quote-button.js.es6 b/app/assets/javascripts/discourse/views/quote-button.js.es6 index e8e1dac4af..1d4f7b7606 100644 --- a/app/assets/javascripts/discourse/views/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/views/quote-button.js.es6 @@ -1,4 +1,4 @@ -export default Discourse.View.extend({ +export default Ember.View.extend({ classNames: ['quote-button'], classNameBindings: ['visible'], isMouseDown: false, diff --git a/app/assets/javascripts/discourse/views/search.js.es6 b/app/assets/javascripts/discourse/views/search.js.es6 index a240a0dec9..a9ec93319f 100644 --- a/app/assets/javascripts/discourse/views/search.js.es6 +++ b/app/assets/javascripts/discourse/views/search.js.es6 @@ -1,4 +1,4 @@ -export default Discourse.View.extend({ +export default Ember.View.extend({ tagName: 'div', classNames: ['d-dropdown'], elementId: 'search-dropdown', diff --git a/app/assets/javascripts/discourse/views/selected-posts.js.es6 b/app/assets/javascripts/discourse/views/selected-posts.js.es6 index d173337d75..f54618bab1 100644 --- a/app/assets/javascripts/discourse/views/selected-posts.js.es6 +++ b/app/assets/javascripts/discourse/views/selected-posts.js.es6 @@ -1,4 +1,4 @@ -export default Discourse.View.extend({ +export default Ember.View.extend({ elementId: 'selected-posts', classNameBindings: ['customVisibility'], templateName: "selected-posts", diff --git a/app/assets/javascripts/discourse/views/share.js.es6 b/app/assets/javascripts/discourse/views/share.js.es6 index 4bc44acd9f..dc0925d7ae 100644 --- a/app/assets/javascripts/discourse/views/share.js.es6 +++ b/app/assets/javascripts/discourse/views/share.js.es6 @@ -1,5 +1,5 @@ -export default Discourse.View.extend({ +export default Ember.View.extend({ templateName: 'share', elementId: 'share-link', classNameBindings: ['hasLink'], @@ -15,13 +15,13 @@ export default Discourse.View.extend({ }.property('controller.type', 'controller.postNumber'), hasLink: function() { - if (this.present('controller.link')) return 'visible'; + if (!Ember.isEmpty(this.get('controller.link'))) return 'visible'; return null; }.property('controller.link'), linkChanged: function() { const self = this; - if (this.present('controller.link')) { + if (!Ember.isEmpty(this.get('controller.link'))) { Em.run.next(function() { if (!self.capabilities.touch) { var $linkInput = $('#share-link input'); diff --git a/app/assets/javascripts/discourse/views/topic-admin-menu.js.es6 b/app/assets/javascripts/discourse/views/topic-admin-menu.js.es6 index d19b33d970..05321a4547 100644 --- a/app/assets/javascripts/discourse/views/topic-admin-menu.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-admin-menu.js.es6 @@ -2,11 +2,11 @@ This view is used for rendering the topic admin menu @class TopicAdminMenuView - @extends Discourse.View + @extends Ember.View @namespace Discourse @module Discourse **/ -export default Discourse.View.extend({ +export default Ember.View.extend({ classNameBindings: ["controller.menuVisible::hidden", ":topic-admin-menu"], _setup: function() { diff --git a/app/assets/javascripts/discourse/views/topic-closing.js.es6 b/app/assets/javascripts/discourse/views/topic-closing.js.es6 index c5f22d503a..52c97c9dbb 100644 --- a/app/assets/javascripts/discourse/views/topic-closing.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-closing.js.es6 @@ -1,6 +1,6 @@ import StringBuffer from 'discourse/mixins/string-buffer'; -export default Discourse.View.extend(StringBuffer, { +export default Ember.View.extend(StringBuffer, { elementId: 'topic-closing-info', delayedRerender: null, @@ -10,7 +10,7 @@ export default Discourse.View.extend(StringBuffer, { 'topic.details.auto_close_hours'], renderString: function(buffer) { - if (!this.present('topic.details.auto_close_at')) return; + if (!!Ember.isEmpty(this.get('topic.details.auto_close_at'))) return; if (this.get("topic.closed")) return; var autoCloseAt = moment(this.get('topic.details.auto_close_at')); diff --git a/app/assets/javascripts/discourse/views/topic-list-item.js.es6 b/app/assets/javascripts/discourse/views/topic-list-item.js.es6 index d1d0ca7e6d..cf550a204a 100644 --- a/app/assets/javascripts/discourse/views/topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-list-item.js.es6 @@ -1,6 +1,6 @@ import StringBuffer from 'discourse/mixins/string-buffer'; -export default Discourse.View.extend(StringBuffer, { +export default Ember.View.extend(StringBuffer, { topic: Em.computed.alias("content"), rerenderTriggers: ['controller.bulkSelectEnabled', 'topic.pinned'], tagName: 'tr', diff --git a/app/assets/javascripts/discourse/views/topic.js.es6 b/app/assets/javascripts/discourse/views/topic.js.es6 index f31911ae0f..bd6265acea 100644 --- a/app/assets/javascripts/discourse/views/topic.js.es6 +++ b/app/assets/javascripts/discourse/views/topic.js.es6 @@ -5,7 +5,7 @@ import { listenForViewEvent } from 'discourse/lib/app-events'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import Scrolling from 'discourse/mixins/scrolling'; -const TopicView = Discourse.View.extend(AddCategoryClass, AddArchetypeClass, Scrolling, { +const TopicView = Ember.View.extend(AddCategoryClass, AddArchetypeClass, Scrolling, { templateName: 'topic', topicBinding: 'controller.model', diff --git a/app/assets/javascripts/discourse/views/user-card.js.es6 b/app/assets/javascripts/discourse/views/user-card.js.es6 index 2aa1b47593..6e1b42d22b 100644 --- a/app/assets/javascripts/discourse/views/user-card.js.es6 +++ b/app/assets/javascripts/discourse/views/user-card.js.es6 @@ -6,7 +6,7 @@ const clickOutsideEventName = "mousedown.outside-user-card", clickDataExpand = "click.discourse-user-card", clickMention = "click.discourse-user-mention"; -export default Discourse.View.extend(CleansUp, { +export default Ember.View.extend(CleansUp, { elementId: 'user-card', classNameBindings: ['controller.visible:show', 'controller.showBadges', 'controller.hasCardBadgeImage'], allowBackgrounds: setting('allow_profile_backgrounds'), diff --git a/app/assets/javascripts/discourse/views/user-topics-list.js.es6 b/app/assets/javascripts/discourse/views/user-topics-list.js.es6 index 3f21ea2459..b60d907613 100644 --- a/app/assets/javascripts/discourse/views/user-topics-list.js.es6 +++ b/app/assets/javascripts/discourse/views/user-topics-list.js.es6 @@ -1,6 +1,6 @@ import LoadMore from "discourse/mixins/load-more"; -export default Discourse.View.extend(LoadMore, { +export default Ember.View.extend(LoadMore, { classNames: ['paginated-topics-list'], eyelineSelector: '.paginated-topics-list .topic-list tr', }); diff --git a/app/assets/javascripts/discourse/views/users.js.es6 b/app/assets/javascripts/discourse/views/users.js.es6 index 9fdb9eb2d8..1af01b7481 100644 --- a/app/assets/javascripts/discourse/views/users.js.es6 +++ b/app/assets/javascripts/discourse/views/users.js.es6 @@ -1,5 +1,5 @@ import LoadMore from 'discourse/mixins/load-more'; -export default Discourse.View.extend(LoadMore, { +export default Ember.View.extend(LoadMore, { eyelineSelector: '.directory tbody tr' }); diff --git a/app/assets/javascripts/discourse/views/view.js.es6 b/app/assets/javascripts/discourse/views/view.js.es6 deleted file mode 100644 index 03f49906f8..0000000000 --- a/app/assets/javascripts/discourse/views/view.js.es6 +++ /dev/null @@ -1,3 +0,0 @@ -import Presence from 'discourse/mixins/presence'; - -export default Ember.View.extend(Presence); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index aff3ff2e44..403271d5fe 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -47,11 +47,8 @@ //= require ./discourse/models/draft //= require ./discourse/models/composer //= require ./discourse/models/invite -//= require ./discourse/controllers/controller //= require ./discourse/controllers/discovery-sortable -//= require ./discourse/controllers/object //= require ./discourse/controllers/navigation/default -//= require ./discourse/views/view //= require ./discourse/views/grouped //= require ./discourse/views/container //= require ./discourse/views/modal-body diff --git a/test/javascripts/admin/models/admin-user-test.js.es6 b/test/javascripts/admin/models/admin-user-test.js.es6 index 0ea725f36a..5d0b231857 100644 --- a/test/javascripts/admin/models/admin-user-test.js.es6 +++ b/test/javascripts/admin/models/admin-user-test.js.es6 @@ -1,5 +1,6 @@ -module("Discourse.AdminUser"); +import { blank, present } from 'helpers/qunit-helpers'; +module("Discourse.AdminUser"); asyncTestDiscourse('generate key', function() { sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({api_key: {id: 1234, key: 'asdfasdf'}})); diff --git a/test/javascripts/admin/models/api-key-test.js.es6 b/test/javascripts/admin/models/api-key-test.js.es6 index 44da70908c..07121c9e16 100644 --- a/test/javascripts/admin/models/api-key-test.js.es6 +++ b/test/javascripts/admin/models/api-key-test.js.es6 @@ -1,3 +1,5 @@ +import { present } from 'helpers/qunit-helpers'; + module("Discourse.ApiKey"); test('create', function() { diff --git a/test/javascripts/controllers/discourse-test.js.es6 b/test/javascripts/controllers/discourse-test.js.es6 deleted file mode 100644 index ab8239f851..0000000000 --- a/test/javascripts/controllers/discourse-test.js.es6 +++ /dev/null @@ -1,8 +0,0 @@ -import DiscourseController from 'discourse/controllers/controller'; -import Presence from 'discourse/mixins/presence'; - -module("DiscourseController"); - -test("includes mixins", function() { - ok(Presence.detect(DiscourseController.create()), "has Presence"); -}); diff --git a/test/javascripts/controllers/topic-test.js.es6 b/test/javascripts/controllers/topic-test.js.es6 index 7edb95f5bf..afef0d4f0a 100644 --- a/test/javascripts/controllers/topic-test.js.es6 +++ b/test/javascripts/controllers/topic-test.js.es6 @@ -1,3 +1,5 @@ +import { blank, present } from 'helpers/qunit-helpers'; + moduleFor('controller:topic', 'controller:topic', { needs: ['controller:header', 'controller:modal', 'controller:composer', 'controller:quote-button', 'controller:search', 'controller:topic-progress', 'controller:application'] diff --git a/test/javascripts/helpers/assertions.js b/test/javascripts/helpers/assertions.js index 6fcea32692..b5fa2a0f48 100644 --- a/test/javascripts/helpers/assertions.js +++ b/test/javascripts/helpers/assertions.js @@ -8,14 +8,6 @@ function count(selector) { return find(selector).length; } -function present(obj, text) { - ok(!Ember.isEmpty(obj), text); -} - -function blank(obj, text) { - ok(Ember.isEmpty(obj), text); -} - function containsInstance(collection, klass, text) { ok(klass.detectInstance(_.first(collection)), text); } diff --git a/test/javascripts/helpers/custom-html-test.js.es6 b/test/javascripts/helpers/custom-html-test.js.es6 index 7510aede58..1f2526d549 100644 --- a/test/javascripts/helpers/custom-html-test.js.es6 +++ b/test/javascripts/helpers/custom-html-test.js.es6 @@ -1,3 +1,4 @@ +import { blank } from 'helpers/qunit-helpers'; module("helper:custom-html"); import { getCustomHTML, setCustomHTML } from 'discourse/helpers/custom-html'; diff --git a/test/javascripts/helpers/qunit-helpers.js.es6 b/test/javascripts/helpers/qunit-helpers.js.es6 index 64547080b4..153bdb4b85 100644 --- a/test/javascripts/helpers/qunit-helpers.js.es6 +++ b/test/javascripts/helpers/qunit-helpers.js.es6 @@ -1,4 +1,4 @@ -/* global asyncTest */ +/* global asyncTest, fixtures */ import sessionFixtures from 'fixtures/session-fixtures'; import siteFixtures from 'fixtures/site_fixtures'; @@ -101,4 +101,19 @@ function fixture(selector) { return $("#qunit-fixture"); } -export { acceptance, controllerFor, asyncTestDiscourse, fixture, logIn, currentUser }; +function present(obj, text) { + ok(!Ember.isEmpty(obj), text); +} + +function blank(obj, text) { + ok(Ember.isEmpty(obj), text); +} + +export { acceptance, + controllerFor, + asyncTestDiscourse, + fixture, + logIn, + currentUser, + blank, + present }; diff --git a/test/javascripts/lib/category-badge-test.js.es6 b/test/javascripts/lib/category-badge-test.js.es6 index 95f5d897b7..55e2da5868 100644 --- a/test/javascripts/lib/category-badge-test.js.es6 +++ b/test/javascripts/lib/category-badge-test.js.es6 @@ -1,3 +1,5 @@ +import { blank, present } from 'helpers/qunit-helpers'; + module("lib:category-link"); import parseHTML from 'helpers/parse-html'; diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6 index 0cbd0c09a0..b01d8b4239 100644 --- a/test/javascripts/lib/click-track-test.js.es6 +++ b/test/javascripts/lib/click-track-test.js.es6 @@ -1,3 +1,4 @@ +import { blank } from 'helpers/qunit-helpers'; import DiscourseURL from "discourse/lib/url"; import ClickTrack from "discourse/lib/click-track"; diff --git a/test/javascripts/lib/preload-store-test.js.es6 b/test/javascripts/lib/preload-store-test.js.es6 index 0266d84bf7..0b296618f9 100644 --- a/test/javascripts/lib/preload-store-test.js.es6 +++ b/test/javascripts/lib/preload-store-test.js.es6 @@ -1,3 +1,5 @@ +import { blank } from 'helpers/qunit-helpers'; + module("Discourse.PreloadStore", { setup: function() { PreloadStore.store('bane', 'evil'); diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index 8a10c7fbe2..2f94980b31 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -1,3 +1,5 @@ +import { blank } from 'helpers/qunit-helpers'; + module("Discourse.Utilities"); var utils = Discourse.Utilities; @@ -123,8 +125,8 @@ test("avatarUrl", function() { }); var setDevicePixelRatio = function(value) { - if(Object.defineProperty) { - Object.defineProperty(window, "devicePixelRatio", { value: 2 }) + if (Object.defineProperty && !window.hasOwnProperty('devicePixelRatio')) { + Object.defineProperty(window, "devicePixelRatio", { value: 2 }); } else { window.devicePixelRatio = value; } diff --git a/test/javascripts/mixins/presence-test.js.es6 b/test/javascripts/mixins/presence-test.js.es6 deleted file mode 100644 index f11a94588a..0000000000 --- a/test/javascripts/mixins/presence-test.js.es6 +++ /dev/null @@ -1,27 +0,0 @@ -import Presence from 'discourse/mixins/presence'; - -module("mixin:presence"); - -var testObj = Em.Object.createWithMixins(Presence, { - emptyString: "", - nonEmptyString: "Evil Trout", - emptyArray: [], - nonEmptyArray: [1, 2, 3], - age: 34 -}); - -test("present", function() { - ok(testObj.present('nonEmptyString'), "Non empty strings are present"); - ok(!testObj.present('emptyString'), "Empty strings are not present"); - ok(testObj.present('nonEmptyArray'), "Non Empty Arrays are present"); - ok(!testObj.present('emptyArray'), "Empty arrays are not present"); - ok(testObj.present('age'), "integers are present"); -}); - -test("blank", function() { - ok(testObj.blank('emptyString'), "Empty strings are blank"); - ok(!testObj.blank('nonEmptyString'), "Non empty strings are not blank"); - ok(testObj.blank('emptyArray'), "Empty arrays are blank"); - ok(!testObj.blank('nonEmptyArray'), "Non empty arrays are not blank"); - ok(testObj.blank('missing'), "Missing properties are blank"); -}); diff --git a/test/javascripts/mixins/singleton-test.js.es6 b/test/javascripts/mixins/singleton-test.js.es6 index 80f0846dac..eeca3caa56 100644 --- a/test/javascripts/mixins/singleton-test.js.es6 +++ b/test/javascripts/mixins/singleton-test.js.es6 @@ -1,3 +1,4 @@ +import { blank, present } from 'helpers/qunit-helpers'; import Singleton from 'discourse/mixins/singleton'; module("mixin:singleton"); diff --git a/test/javascripts/models/composer-test.js.es6 b/test/javascripts/models/composer-test.js.es6 index 1727d2893a..e6bb0d74d1 100644 --- a/test/javascripts/models/composer-test.js.es6 +++ b/test/javascripts/models/composer-test.js.es6 @@ -1,3 +1,4 @@ +import { blank } from 'helpers/qunit-helpers'; import { currentUser } from 'helpers/qunit-helpers'; module("model:composer"); diff --git a/test/javascripts/models/model-test.js.es6 b/test/javascripts/models/model-test.js.es6 index b8d92921ec..3270af8a55 100644 --- a/test/javascripts/models/model-test.js.es6 +++ b/test/javascripts/models/model-test.js.es6 @@ -1,11 +1,6 @@ -import Presence from 'discourse/mixins/presence'; import Model from 'discourse/models/model'; -module("Discourse.Model"); - -test("mixes in Presence", function() { - ok(Presence.detect(Model.create())); -}); +module("model:discourse"); test("extractByKey: converts a list of hashes into a hash of instances of specified class, indexed by their ids", function() { var firstObject = {id: "id_1", foo: "foo_1"}; diff --git a/test/javascripts/models/post-stream-test.js.es6 b/test/javascripts/models/post-stream-test.js.es6 index 5a85360e5c..f7795de683 100644 --- a/test/javascripts/models/post-stream-test.js.es6 +++ b/test/javascripts/models/post-stream-test.js.es6 @@ -1,3 +1,4 @@ +import { blank, present } from 'helpers/qunit-helpers'; module("model:post-stream"); import createStore from 'helpers/create-store'; diff --git a/test/javascripts/models/post-test.js.es6 b/test/javascripts/models/post-test.js.es6 index 4b5359f9d6..7736577500 100644 --- a/test/javascripts/models/post-test.js.es6 +++ b/test/javascripts/models/post-test.js.es6 @@ -1,3 +1,5 @@ +import { present, blank } from 'helpers/qunit-helpers'; + module("Discourse.Post"); var buildPost = function(args) { diff --git a/test/javascripts/models/report-test.js.es6 b/test/javascripts/models/report-test.js.es6 index f66a84bfa6..9ae550d157 100644 --- a/test/javascripts/models/report-test.js.es6 +++ b/test/javascripts/models/report-test.js.es6 @@ -1,3 +1,5 @@ +import { blank } from 'helpers/qunit-helpers'; + module("Discourse.Report"); function reportWithData(data) { diff --git a/test/javascripts/models/site-test.js.es6 b/test/javascripts/models/site-test.js.es6 index fa38736aef..65804504d4 100644 --- a/test/javascripts/models/site-test.js.es6 +++ b/test/javascripts/models/site-test.js.es6 @@ -1,3 +1,5 @@ +import { blank, present } from 'helpers/qunit-helpers'; + module("Discourse.Site"); test('create', function() { diff --git a/test/javascripts/models/topic-details-test.js.es6 b/test/javascripts/models/topic-details-test.js.es6 index 7fb7fc2f25..75719ad3e9 100644 --- a/test/javascripts/models/topic-details-test.js.es6 +++ b/test/javascripts/models/topic-details-test.js.es6 @@ -1,3 +1,4 @@ +import { present } from 'helpers/qunit-helpers'; module("model:topic-details"); import Topic from 'discourse/models/topic'; diff --git a/test/javascripts/models/topic-test.js.es6 b/test/javascripts/models/topic-test.js.es6 index c5a2e578fc..a1f7d159e2 100644 --- a/test/javascripts/models/topic-test.js.es6 +++ b/test/javascripts/models/topic-test.js.es6 @@ -1,3 +1,4 @@ +import { blank, present } from 'helpers/qunit-helpers'; module("model:topic"); import Topic from 'discourse/models/topic'; diff --git a/test/javascripts/models/user-stream-test.js.es6 b/test/javascripts/models/user-stream-test.js.es6 index 135ee6f22e..c5b35985e3 100644 --- a/test/javascripts/models/user-stream-test.js.es6 +++ b/test/javascripts/models/user-stream-test.js.es6 @@ -1,4 +1,6 @@ -module("Discourse.User"); +import { blank, present } from 'helpers/qunit-helpers'; + +module("Discourse.UserStream"); test('basics', function(){ var user = Discourse.User.create({id: 1, username: 'eviltrout'}); diff --git a/test/javascripts/views/container-view-test.js.es6 b/test/javascripts/views/container-view-test.js.es6 index e8f3b2a8a4..0ebd5808d3 100644 --- a/test/javascripts/views/container-view-test.js.es6 +++ b/test/javascripts/views/container-view-test.js.es6 @@ -1,5 +1,3 @@ -import Presence from 'discourse/mixins/presence'; - var SomeViewClass = Ember.View.extend(); function containerHasOnlyOneChild(containerView, klass) { @@ -19,11 +17,6 @@ function childHasProperty(containerView, name) { moduleFor("view:container"); -test("mixes in Presence", function() { - var containerView = this.subject(); - ok(Presence.detect(containerView)); -}); - test("attachViewWithArgs: creates a view of a given class with given properties and appends it to the container", function() { var containerView = this.subject(); containerView.attachViewWithArgs({foo: "foo"}, SomeViewClass); From 22844b9e4663a1dd06040cd5913f02f571f9672f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 11 Aug 2015 17:34:02 -0400 Subject: [PATCH 010/237] Ember 1.12 support --- .jshintignore => .eslintignore | 2 +- .eslintrc | 105 + .jshintrc | 83 - Gemfile.lock | 8 +- .../admin/components/site-setting.js.es6 | 24 +- app/assets/javascripts/discourse.js | 51 +- .../desktop-notification-config.js.es6 | 32 +- .../discourse/controllers/quote-button.js.es6 | 2 +- .../discourse/controllers/search.js.es6 | 15 +- .../discourse/controllers/topic.js.es6 | 65 +- .../apply-flagged-properties.js.es6 | 1 - .../discourse/initializers/csrf-token.js.es6 | 1 - .../initializers/enable-emoji.js.es6 | 1 - .../initializers/inject-objects.js.es6 | 48 +- .../initializers/page-tracking.js.es6 | 1 - .../register-discourse-location.js.es6 | 8 +- .../dynamic-route-builders.js.es6 | 3 +- .../inject-discourse-objects.js.es6 | 53 + .../map-routes.js.es6 | 2 +- .../register-dom-templates.js.es6 | 0 .../sniff-capabilities.js.es6 | 0 .../discourse/views/topic-unsubscribe.js.es6 | 2 +- .../ember-addons/decorator-alias.js.es6 | 22 + .../ember-computed-decorators.js.es6 | 63 + .../ember-addons/macro-alias.js.es6 | 24 + .../ember-addons/utils/extract-value.js.es6 | 4 + .../utils/handle-descriptor.js.es6 | 67 + .../ember-addons/utils/is-descriptor.js.es6 | 7 + app/assets/javascripts/ember_include.js.erb | 2 +- app/assets/javascripts/main_include.js | 5 + .../common/_discourse_javascript.html.erb | 4 +- lib/discourse_iife.rb | 2 +- .../tilt/es6_module_transpiler_template.rb | 6 +- test/javascripts/jshint-test.js.es6.erb | 107 - test/javascripts/lib/click-track-test.js.es6 | 2 +- test/javascripts/test_helper.js | 4 +- .../javascripts/ember-template-compiler.js | 9924 +++- .../assets/javascripts/ember.custom.debug.js | 49400 ---------------- vendor/assets/javascripts/jshint.js | 12047 ---- 39 files changed, 10322 insertions(+), 61875 deletions(-) rename .jshintignore => .eslintignore (96%) create mode 100644 .eslintrc delete mode 100644 .jshintrc rename app/assets/javascripts/discourse/{initializers => pre-initializers}/dynamic-route-builders.js.es6 (95%) create mode 100644 app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 rename app/assets/javascripts/discourse/{initializers => pre-initializers}/map-routes.js.es6 (93%) rename app/assets/javascripts/discourse/{initializers => pre-initializers}/register-dom-templates.js.es6 (100%) rename app/assets/javascripts/discourse/{initializers => pre-initializers}/sniff-capabilities.js.es6 (100%) create mode 100644 app/assets/javascripts/ember-addons/decorator-alias.js.es6 create mode 100644 app/assets/javascripts/ember-addons/ember-computed-decorators.js.es6 create mode 100644 app/assets/javascripts/ember-addons/macro-alias.js.es6 create mode 100644 app/assets/javascripts/ember-addons/utils/extract-value.js.es6 create mode 100644 app/assets/javascripts/ember-addons/utils/handle-descriptor.js.es6 create mode 100644 app/assets/javascripts/ember-addons/utils/is-descriptor.js.es6 delete mode 100644 test/javascripts/jshint-test.js.es6.erb delete mode 100644 vendor/assets/javascripts/ember.custom.debug.js delete mode 100644 vendor/assets/javascripts/jshint.js diff --git a/.jshintignore b/.eslintignore similarity index 96% rename from .jshintignore rename to .eslintignore index ac11baa97e..1ecda00738 100644 --- a/.jshintignore +++ b/.eslintignore @@ -20,5 +20,5 @@ vendor/ test/javascripts/helpers/ test/javascripts/test_helper.js test/javascripts/test_helper.js +test/javascripts/fixtures app/assets/javascripts/ember-addons/ - diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000000..149d842488 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,105 @@ +{ + "env": { + "jasmine": true, + "node": true, + "mocha": true, + "browser": true, + "builtin": true + }, + ecmaVersion: 7, + "globals": + {"Ember":true, + "jQuery":true, + "$":true, + "RSVP":true, + "Discourse":true, + "Em":true, + "PreloadStore":true, + "Handlebars":true, + "I18n":true, + "bootbox":true, + "module":true, + "moduleFor":true, + "moduleForComponent":true, + "Pretender":true, + "sandbox":true, + "controllerFor":true, + "test":true, + "ok":true, + "not":true, + "expect":true, + "equal":true, + "visit":true, + "andThen":true, + "click":true, + "currentPath":true, + "currentRouteName":true, + "currentURL":true, + "fillIn":true, + "keyEvent":true, + "triggerEvent":true, + "count":true, + "exists":true, + "visible":true, + "invisible":true, + "asyncRender":true, + "selectDropdown":true, + "asyncTestDiscourse":true, + "fixture":true, + "find":true, + "sinon":true, + "moment":true, + "start":true, + "_":true, + "alert":true, + "containsInstance":true, + "deepEqual":true, + "notEqual":true, + "define":true, + "require":true, + "requirejs":true, + "hasModule":true, + "Blob":true, + "File":true}, + "rules": { + "block-scoped-var": 2, + "dot-notation": 0, + "eqeqeq": [ + 2, + "allow-null" + ], + "guard-for-in": 2, + "no-bitwise": 2, + "no-caller": 2, + "no-cond-assign": 0, + "no-debugger": 2, + "no-empty": 0, + "no-eval": 2, + "no-extend-native": 2, + "no-extra-parens": 0, + "no-irregular-whitespace": 2, + "no-iterator": 2, + "no-loop-func": 2, + "no-multi-str": 2, + "no-new": 2, + "no-plusplus": 0, + "no-proto": 2, + "no-script-url": 2, + "no-sequences": 2, + "no-shadow": 2, + "no-undef": 2, + "no-unused-vars": 2, + "no-with": 2, + "semi": [ + 0, + "never" + ], + "strict": 0, + "valid-typeof": 2, + "wrap-iife": [ + 2, + "inside" + ] + }, + "parser": "babel-eslint" +} diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index cbe6bffe52..0000000000 --- a/.jshintrc +++ /dev/null @@ -1,83 +0,0 @@ -{ - "predef":["Ember", - "jQuery", - "$", - "RSVP", - "Discourse", - "Em", - "PreloadStore", - "Handlebars", - "I18n", - "bootbox", - "module", - "moduleFor", - "moduleForComponent", - "Pretender", - "sandbox", - "controllerFor", - "test", - "ok", - "not", - "expect", - "equal", - "visit", - "andThen", - "click", - "currentPath", - "currentRouteName", - "currentURL", - "fillIn", - "keyEvent", - "triggerEvent", - "count", - "exists", - "visible", - "invisible", - "asyncRender", - "selectDropdown", - "asyncTestDiscourse", - "fixture", - "find", - "sinon", - "moment", - "start", - "_", - "alert", - "containsInstance", - "deepEqual", - "notEqual", - "define", - "require", - "requirejs", - "hasModule", - "Blob", - "File"], - "node" : false, - "browser" : true, - "boss" : true, - "curly": false, - "debug": false, - "devel": false, - "eqeqeq": true, - "evil": true, - "forin": false, - "immed": false, - "laxbreak": false, - "newcap": true, - "noarg": true, - "noempty": false, - "nonew": false, - "nomen": false, - "onevar": false, - "plusplus": false, - "regexp": false, - "undef": true, - "unused": true, - "sub": true, - "strict": false, - "white": false, - "eqnull": true, - "quotmark": false, - "lastsemic": true, - "esnext": true -} diff --git a/Gemfile.lock b/Gemfile.lock index d10b146ad8..c369c55fe8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,9 +46,9 @@ GEM multi_json (~> 1.0) aws-sdk-resources (2.0.45) aws-sdk-core (= 2.0.45) - babel-source (4.6.6) - babel-transpiler (0.6.0) - babel-source (>= 4.0, < 5) + babel-source (5.8.19) + babel-transpiler (0.7.0) + babel-source (>= 4.0, < 6) execjs (~> 2.0) barber (0.9.0) ember-source (>= 1.0, < 2) @@ -86,7 +86,7 @@ GEM ember-source (>= 1.1.0) jquery-rails (>= 1.0.17) railties (>= 3.1) - ember-source (1.11.3.1) + ember-source (1.12.1) erubis (2.7.0) eventmachine (1.0.7) excon (0.45.3) diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index 10925e8a2e..15058e8716 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -2,6 +2,7 @@ import BufferedContent from 'discourse/mixins/buffered-content'; import ScrollTop from 'discourse/mixins/scroll-top'; import SiteSetting from 'admin/models/site-setting'; import { propertyNotEqual } from 'discourse/lib/computed'; +import computed from 'ember-addons/ember-computed-decorators'; const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list']; @@ -20,19 +21,22 @@ export default Ember.Component.extend(BufferedContent, ScrollTop, { } }.property('buffered.value'), - typeClass: function() { + @computed('partialType') + typeClass() { return this.get('partialType').replace("_", "-"); - }.property('partialType'), + }, - enabled: function(key, value) { - if (arguments.length > 1) { + @computed('buffered.value') + enabled: { + get() { + const bufferedValue = this.get('buffered.value'); + if (Ember.isEmpty(bufferedValue)) { return false; } + return bufferedValue === 'true'; + }, + set(key, value) { this.set('buffered.value', value ? 'true' : 'false'); } - - const bufferedValue = this.get('buffered.value'); - if (Ember.isEmpty(bufferedValue)) { return false; } - return bufferedValue === 'true'; - }.property('buffered.value'), + }, settingName: function() { return this.get('setting.setting').replace(/\_/g, ' '); @@ -40,7 +44,7 @@ export default Ember.Component.extend(BufferedContent, ScrollTop, { partialType: function() { let type = this.get('setting.type'); - return (CustomTypes.indexOf(type) !== -1) ? type : 'string'; + return CustomTypes.indexOf(type) !== -1 ? type : 'string'; }.property('setting.type'), partialName: function() { diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 3d548b5fa1..7a26b76e7e 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -3,7 +3,7 @@ var DiscourseResolver = require('discourse/ember/resolver').default; // Allow us to import Ember define('ember', ['exports'], function(__exports__) { - __exports__["default"] = Ember; + __exports__.default = Ember; }); window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { @@ -16,7 +16,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { // if it's a non relative URL, return it. if (!/^\/[^\/]/.test(url)) return url; - var u = (Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri); + var u = Discourse.BaseUri === undefined ? "/" : Discourse.BaseUri; if (u[u.length-1] === '/') u = u.substring(0, u.length-1); if (url.indexOf(u) !== -1) return url; @@ -66,7 +66,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { // The classes of buttons to show on a post postButtons: function() { return Discourse.SiteSettings.post_menu.split("|").map(function(i) { - return (i.replace(/\+/, '').capitalize()); + return i.replace(/\+/, '').capitalize(); }); }.property(), @@ -109,12 +109,26 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { $('noscript').remove(); - // Load any ES6 initializers + Ember.keys(requirejs._eak_seen).forEach(function(key) { + if (/\/pre\-initializers\//.test(key)) { + var module = require(key, null, null, true); + if (!module) { throw new Error(key + ' must export an initializer.'); } + Discourse.initializer(module.default); + } + }); + Ember.keys(requirejs._eak_seen).forEach(function(key) { if (/\/initializers\//.test(key)) { var module = require(key, null, null, true); if (!module) { throw new Error(key + ' must export an initializer.'); } - Discourse.initializer(module.default); + + var init = module.default; + var oldInitialize = init.initialize; + init.initialize = function(app) { + oldInitialize.call(this, app.container, app); + }; + + Discourse.instanceInitializer(init); } }); @@ -125,17 +139,22 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { return desired && Discourse.get("currentAssetVersion") !== desired; }.property("currentAssetVersion", "desiredAssetVersion"), - assetVersion: function(prop, val) { - if(val) { - if(this.get("currentAssetVersion")){ - this.set("desiredAssetVersion", val); - } else { - this.set("currentAssetVersion", val); - } - } - return this.get("currentAssetVersion"); - }.property() + assetVersion: Ember.computed({ + get: function() { + return this.get("currentAssetVersion"); + }, + set: function(key, val) { + if(val) { + if (this.get("currentAssetVersion")) { + this.set("desiredAssetVersion", val); + } else { + this.set("currentAssetVersion", val); + } + } + return this.get("currentAssetVersion"); + } + }) }); // TODO: Remove this, it is in for backwards compatibiltiy with plugins @@ -159,5 +178,3 @@ proxyDep('URL', function() { return require('discourse/lib/url').default }); proxyDep('Quote', function() { return require('discourse/lib/quote').default }); proxyDep('debounce', function() { return require('discourse/lib/debounce').default }); proxyDep('View', function() { return Ember.View }, "Use `Ember.View` instead"); -proxyDep('Controller', function() { return Ember.Controller }, "Use `Ember.Controller` instead"); -proxyDep('ObjectController', function() { return Ember.ObjectController }, "Use `Ember.Controller` instead"); diff --git a/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 b/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 index 5a362280c3..1d5f4bd77b 100644 --- a/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 +++ b/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 @@ -1,23 +1,31 @@ +import computed from 'ember-addons/ember-computed-decorators'; + export default Ember.Component.extend({ classNames: ['controls'], - notificationsPermission: function() { + @computed + notificationsPermission() { if (this.get('isNotSupported')) return ''; - return Notification.permission; - }.property(), + }, - notificationsDisabled: function(_, value) { - if (arguments.length > 1) { - localStorage.setItem('notifications-disabled', value); + @computed + notificationsDisabled: { + set(key, value) { + if (arguments.length > 1) { + localStorage.setItem('notifications-disabled', value); + } + return localStorage.getItem('notifications-disabled'); + }, + get() { + return localStorage.getItem('notifications-disabled'); } - return localStorage.getItem('notifications-disabled'); - }.property(), + }, - - isNotSupported: function() { - return !window['Notification']; - }.property(), + @computed + isNotSupported() { + return typeof window.Notification === "undefined"; + }, isDefaultPermission: function() { if (this.get('isNotSupported')) return false; diff --git a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 index cfdd30b520..372728decb 100644 --- a/app/assets/javascripts/discourse/controllers/quote-button.js.es6 +++ b/app/assets/javascripts/discourse/controllers/quote-button.js.es6 @@ -50,7 +50,7 @@ export default Ember.Controller.extend({ // create a marker element const markerElement = document.createElement("span"); // containing a single invisible character - markerElement.appendChild(document.createTextNode("\u{feff}")); + markerElement.appendChild(document.createTextNode("\ufeff")); // collapse the range at the beginning/end of the selection range.collapse(!Discourse.Mobile.isMobileDevice); diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index eff99ec98e..1d9cb22ad2 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -1,20 +1,27 @@ import searchForTerm from 'discourse/lib/search-for-term'; import DiscourseURL from 'discourse/lib/url'; +import computed from 'ember-addons/ember-computed-decorators'; let _dontSearch = false; export default Em.Controller.extend({ typeFilter: null, - contextType: function(key, value){ - if(arguments.length > 1) { + @computed('searchContext') + contextType: { + get(searchContext) { + if (searchContext) { + return Ember.get(searchContext, 'type'); + } + }, + set(key, value) { // a bit hacky, consider cleaning this up, need to work through all observers though const context = $.extend({}, this.get('searchContext')); context.type = value; this.set('searchContext', context); + return this.get('searchContext.type'); } - return this.get('searchContext.type'); - }.property('searchContext'), + }, contextChanged: function(){ if (this.get('searchContextEnabled')) { diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 736ad9415b..c112f3458c 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -4,6 +4,7 @@ import { spinnerHTML } from 'discourse/helpers/loading-spinner'; import Topic from 'discourse/models/topic'; import Quote from 'discourse/lib/quote'; import { setting } from 'discourse/lib/computed'; +import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { multiSelect: false, @@ -65,35 +66,53 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { }.observes('model.postStream', 'model.postStream.loadedAllPosts'), - show_deleted: function(key, value) { - const postStream = this.get('model.postStream'); - if (!postStream) { return; } + @computed('model.postStream.summary') + show_deleted: { + set(key, value) { + const postStream = this.get('model.postStream'); + if (!postStream) { return; } - if (arguments.length > 1) { - postStream.set('show_deleted', value); + if (arguments.length > 1) { + postStream.set('show_deleted', value); + } + return postStream.get('show_deleted') ? true : undefined; + }, + get() { + return this.get('postStream.show_deleted') ? true : undefined; } - return postStream.get('show_deleted') ? true : undefined; - }.property('model.postStream.summary'), + }, - filter: function(key, value) { - const postStream = this.get('model.postStream'); - if (!postStream) { return; } + @computed('model.postStream.summary') + filter: { + set(key, value) { + const postStream = this.get('model.postStream'); + if (!postStream) { return; } - if (arguments.length > 1) { - postStream.set('summary', value === "summary"); + if (arguments.length > 1) { + postStream.set('summary', value === "summary"); + } + return postStream.get('summary') ? "summary" : undefined; + }, + get() { + return this.get('postStream.summary') ? "summary" : undefined; } - return postStream.get('summary') ? "summary" : undefined; - }.property('model.postStream.summary'), + }, - username_filters: function(key, value) { - const postStream = this.get('model.postStream'); - if (!postStream) { return; } + @computed('model.postStream.streamFilters.username_filters') + username_filters: { + set(key, value) { + const postStream = this.get('model.postStream'); + if (!postStream) { return; } - if (arguments.length > 1) { - postStream.set('streamFilters.username_filters', value); + if (arguments.length > 1) { + postStream.set('streamFilters.username_filters', value); + } + return postStream.get('streamFilters.username_filters'); + }, + get() { + return this.get('postStream.streamFilters.username_filters'); } - return postStream.get('streamFilters.username_filters'); - }.property('model.postStream.streamFilters.username_filters'), + }, _clearSelected: function() { this.set('selectedPosts', []); @@ -489,13 +508,13 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { canMergeTopic: function() { if (!this.get('model.details.can_move_posts')) return false; - return (this.get('selectedPostsCount') > 0); + return this.get('selectedPostsCount') > 0; }.property('selectedPostsCount'), canSplitTopic: function() { if (!this.get('model.details.can_move_posts')) return false; if (this.get('allPostsSelected')) return false; - return (this.get('selectedPostsCount') > 0); + return this.get('selectedPostsCount') > 0; }.property('selectedPostsCount'), canChangeOwner: function() { diff --git a/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 b/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 index f446f60c28..5408595451 100644 --- a/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 +++ b/app/assets/javascripts/discourse/initializers/apply-flagged-properties.js.es6 @@ -2,6 +2,5 @@ import { applyFlaggedProperties } from 'discourse/controllers/header'; export default { name: 'apply-flagged-properties', - after: 'register-discourse-location', initialize: applyFlaggedProperties }; diff --git a/app/assets/javascripts/discourse/initializers/csrf-token.js.es6 b/app/assets/javascripts/discourse/initializers/csrf-token.js.es6 index f83ea3caa8..90be518ddd 100644 --- a/app/assets/javascripts/discourse/initializers/csrf-token.js.es6 +++ b/app/assets/javascripts/discourse/initializers/csrf-token.js.es6 @@ -1,7 +1,6 @@ // Append our CSRF token to AJAX requests when necessary. export default { name: "csrf-token", - after: 'inject-objects', initialize: function(container) { var session = container.lookup('session:main'); diff --git a/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 b/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 index bda7e1b8eb..b7b269f78f 100644 --- a/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 +++ b/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 @@ -2,7 +2,6 @@ import { showSelector } from "discourse/lib/emoji/emoji-toolbar"; export default { name: 'enable-emoji', - after: 'inject-objects', initialize(container) { const siteSettings = container.lookup('site-settings:main'); diff --git a/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 b/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 index 197eb06d6b..f15f57fa8a 100644 --- a/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 +++ b/app/assets/javascripts/discourse/initializers/inject-objects.js.es6 @@ -1,50 +1,6 @@ -import Session from 'discourse/models/session'; -import AppEvents from 'discourse/lib/app-events'; -import Store from 'discourse/models/store'; -import DiscourseURL from 'discourse/lib/url'; - -function inject() { - const app = arguments[0], - name = arguments[1], - singletonName = Ember.String.underscore(name).replace(/_/, '-') + ':main'; - - Array.prototype.slice.call(arguments, 2).forEach(function(dest) { - app.inject(dest, name, singletonName); - }); -} - -function injectAll(app, name) { - inject(app, name, 'controller', 'component', 'route', 'view', 'model'); -} +// backwards compatibility for plugins that depend on this initializer export default { name: "inject-objects", - initialize(container, app) { - const appEvents = AppEvents.create(); - app.register('app-events:main', appEvents, { instantiate: false }); - injectAll(app, 'appEvents'); - DiscourseURL.appEvents = appEvents; - - app.register('store:main', Store); - inject(app, 'store', 'route', 'controller'); - - // Inject Discourse.Site to avoid using Discourse.Site.current() - const site = Discourse.Site.current(); - app.register('site:main', site, { instantiate: false }); - injectAll(app, 'site'); - - // Inject Discourse.SiteSettings to avoid using Discourse.SiteSettings globals - app.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); - injectAll(app, 'siteSettings'); - - // Inject Session for transient data - app.register('session:main', Session.current(), { instantiate: false }); - injectAll(app, 'session'); - - app.register('current-user:main', Discourse.User.current(), { instantiate: false }); - inject(app, 'currentUser', 'component', 'route', 'controller'); - - app.register('message-bus:main', window.MessageBus, { instantiate: false }); - inject(app, 'messageBus', 'route', 'controller', 'view', 'component'); - } + initialize: Ember.K }; diff --git a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 index 791e1983e6..c3a5dd46bd 100644 --- a/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 +++ b/app/assets/javascripts/discourse/initializers/page-tracking.js.es6 @@ -3,7 +3,6 @@ import PageTracker from 'discourse/lib/page-tracker'; export default { name: "page-tracking", - after: 'register-discourse-location', initialize(container) { diff --git a/app/assets/javascripts/discourse/initializers/register-discourse-location.js.es6 b/app/assets/javascripts/discourse/initializers/register-discourse-location.js.es6 index 2e17369755..1f073e95cf 100644 --- a/app/assets/javascripts/discourse/initializers/register-discourse-location.js.es6 +++ b/app/assets/javascripts/discourse/initializers/register-discourse-location.js.es6 @@ -1,10 +1,6 @@ -import DiscourseLocation from 'discourse/lib/discourse-location'; +// backwards compatibility for plugins that depend on this initializer export default { name: "register-discourse-location", - after: 'inject-objects', - - initialize: function(container, application) { - application.register('location:discourse-location', DiscourseLocation); - } + initialize: Ember.K }; diff --git a/app/assets/javascripts/discourse/initializers/dynamic-route-builders.js.es6 b/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6 similarity index 95% rename from app/assets/javascripts/discourse/initializers/dynamic-route-builders.js.es6 rename to app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6 index 64d7f8e035..a01c73def8 100644 --- a/app/assets/javascripts/discourse/initializers/dynamic-route-builders.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/dynamic-route-builders.js.es6 @@ -4,14 +4,13 @@ import DiscoverySortableController from 'discourse/controllers/discovery-sortabl export default { name: 'dynamic-route-builders', - after: 'register-discourse-location', initialize(container, app) { app.DiscoveryCategoryRoute = buildCategoryRoute('latest'); app.DiscoveryParentCategoryRoute = buildCategoryRoute('latest'); app.DiscoveryCategoryNoneRoute = buildCategoryRoute('latest', {no_subcategories: true}); - var site = container.lookup('site:main'); + const site = Discourse.Site.current(); site.get('filters').forEach(function(filter) { app["Discovery" + filter.capitalize() + "Controller"] = DiscoverySortableController.extend(); app["Discovery" + filter.capitalize() + "Route"] = buildTopicRoute(filter); diff --git a/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 new file mode 100644 index 0000000000..960277e02a --- /dev/null +++ b/app/assets/javascripts/discourse/pre-initializers/inject-discourse-objects.js.es6 @@ -0,0 +1,53 @@ +import Session from 'discourse/models/session'; +import AppEvents from 'discourse/lib/app-events'; +import Store from 'discourse/models/store'; +import DiscourseURL from 'discourse/lib/url'; +import DiscourseLocation from 'discourse/lib/discourse-location'; + +function inject() { + const app = arguments[0], + name = arguments[1], + singletonName = Ember.String.underscore(name).replace(/_/, '-') + ':main'; + + Array.prototype.slice.call(arguments, 2).forEach(function(dest) { + app.inject(dest, name, singletonName); + }); +} + +function injectAll(app, name) { + inject(app, name, 'controller', 'component', 'route', 'view', 'model'); +} + +export default { + name: "inject-discourse-objects", + initialize(container, app) { + const appEvents = AppEvents.create(); + app.register('app-events:main', appEvents, { instantiate: false }); + injectAll(app, 'appEvents'); + DiscourseURL.appEvents = appEvents; + + app.register('store:main', Store); + inject(app, 'store', 'route', 'controller'); + + // Inject Discourse.Site to avoid using Discourse.Site.current() + const site = Discourse.Site.current(); + app.register('site:main', site, { instantiate: false }); + injectAll(app, 'site'); + + // Inject Discourse.SiteSettings to avoid using Discourse.SiteSettings globals + app.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); + injectAll(app, 'siteSettings'); + + // Inject Session for transient data + app.register('session:main', Session.current(), { instantiate: false }); + injectAll(app, 'session'); + + app.register('current-user:main', Discourse.User.current(), { instantiate: false }); + inject(app, 'currentUser', 'component', 'route', 'controller'); + + app.register('message-bus:main', window.MessageBus, { instantiate: false }); + inject(app, 'messageBus', 'route', 'controller', 'view', 'component'); + + app.register('location:discourse-location', DiscourseLocation); + } +}; diff --git a/app/assets/javascripts/discourse/initializers/map-routes.js.es6 b/app/assets/javascripts/discourse/pre-initializers/map-routes.js.es6 similarity index 93% rename from app/assets/javascripts/discourse/initializers/map-routes.js.es6 rename to app/assets/javascripts/discourse/pre-initializers/map-routes.js.es6 index e83537569e..e349201442 100644 --- a/app/assets/javascripts/discourse/initializers/map-routes.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/map-routes.js.es6 @@ -2,7 +2,7 @@ import { mapRoutes } from 'discourse/router'; export default { name: "map-routes", - after: 'inject-objects', + after: 'inject-discourse-objects', initialize(container, app) { app.register('router:main', mapRoutes()); diff --git a/app/assets/javascripts/discourse/initializers/register-dom-templates.js.es6 b/app/assets/javascripts/discourse/pre-initializers/register-dom-templates.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/initializers/register-dom-templates.js.es6 rename to app/assets/javascripts/discourse/pre-initializers/register-dom-templates.js.es6 diff --git a/app/assets/javascripts/discourse/initializers/sniff-capabilities.js.es6 b/app/assets/javascripts/discourse/pre-initializers/sniff-capabilities.js.es6 similarity index 100% rename from app/assets/javascripts/discourse/initializers/sniff-capabilities.js.es6 rename to app/assets/javascripts/discourse/pre-initializers/sniff-capabilities.js.es6 diff --git a/app/assets/javascripts/discourse/views/topic-unsubscribe.js.es6 b/app/assets/javascripts/discourse/views/topic-unsubscribe.js.es6 index a846972817..46ac72e0f0 100644 --- a/app/assets/javascripts/discourse/views/topic-unsubscribe.js.es6 +++ b/app/assets/javascripts/discourse/views/topic-unsubscribe.js.es6 @@ -1,3 +1,3 @@ -export default Discourse.View.extend({ +export default Ember.View.extend({ classNames: ["topic-unsubscribe"] }); diff --git a/app/assets/javascripts/ember-addons/decorator-alias.js.es6 b/app/assets/javascripts/ember-addons/decorator-alias.js.es6 new file mode 100644 index 0000000000..615b6cc1e6 --- /dev/null +++ b/app/assets/javascripts/ember-addons/decorator-alias.js.es6 @@ -0,0 +1,22 @@ +import extractValue from './utils/extract-value'; + +export default function decoratorAlias(fn, errorMessage) { + return function(...params) { + // determine if user called as @computed('blah', 'blah') or @computed + if (params.length === 0) { + throw new Error(errorMessage); + } else { + return function(target, key, desc) { + return { + enumerable: desc.enumerable, + configurable: desc.configurable, + writable: desc.writable, + initializer: function() { + var value = extractValue(desc); + return fn.apply(null, params.concat(value)); + } + }; + }; + } + }; +} diff --git a/app/assets/javascripts/ember-addons/ember-computed-decorators.js.es6 b/app/assets/javascripts/ember-addons/ember-computed-decorators.js.es6 new file mode 100644 index 0000000000..e8d3b7ba88 --- /dev/null +++ b/app/assets/javascripts/ember-addons/ember-computed-decorators.js.es6 @@ -0,0 +1,63 @@ +import handleDescriptor from './utils/handle-descriptor'; +import isDescriptor from './utils/is-descriptor'; +import extractValue from './utils/extract-value'; + +export default function computedDecorator(...params) { + // determine if user called as @computed('blah', 'blah') or @computed + if (isDescriptor(params[params.length - 1])) { + return handleDescriptor(...arguments); + } else { + return function(/* target, key, desc */) { + return handleDescriptor(...arguments, params); + }; + } +} + +export function readOnly(target, name, desc) { + return { + writable: false, + enumerable: desc.enumerable, + configurable: desc.configurable, + initializer: function() { + var value = extractValue(desc); + return value.readOnly(); + } + }; +} + +import decoratorAlias from './decorator-alias'; + +export var on = decoratorAlias(Ember.on, 'Can not `on` without event names'); +export var observes = decoratorAlias(Ember.observer, 'Can not `observe` without property names'); + +import macroAlias from './macro-alias'; + +export var alias = macroAlias(Ember.computed.alias); +export var and = macroAlias(Ember.computed.and); +export var bool = macroAlias(Ember.computed.bool); +export var collect = macroAlias(Ember.computed.collect); +export var empty = macroAlias(Ember.computed.empty); +export var equal = macroAlias(Ember.computed.equal); +export var filter = macroAlias(Ember.computed.filter); +export var filterBy = macroAlias(Ember.computed.filterBy); +export var gt = macroAlias(Ember.computed.gt); +export var gte = macroAlias(Ember.computed.gte); +export var lt = macroAlias(Ember.computed.lt); +export var lte = macroAlias(Ember.computed.lte); +export var map = macroAlias(Ember.computed.map); +export var mapBy = macroAlias(Ember.computed.mapBy); +export var match = macroAlias(Ember.computed.match); +export var max = macroAlias(Ember.computed.max); +export var min = macroAlias(Ember.computed.min); +export var none = macroAlias(Ember.computed.none); +export var not = macroAlias(Ember.computed.not); +export var notEmpty = macroAlias(Ember.computed.notEmpty); +export var oneWay = macroAlias(Ember.computed.oneWay); +export var or = macroAlias(Ember.computed.or); +export var readOnly = macroAlias(Ember.computed.readOnly); +export var reads = macroAlias(Ember.computed.reads); +export var setDiff = macroAlias(Ember.computed.setDiff); +export var sort = macroAlias(Ember.computed.sort); +export var sum = macroAlias(Ember.computed.sum); +export var union = macroAlias(Ember.computed.union); +export var uniq = macroAlias(Ember.computed.uniq); diff --git a/app/assets/javascripts/ember-addons/macro-alias.js.es6 b/app/assets/javascripts/ember-addons/macro-alias.js.es6 new file mode 100644 index 0000000000..640e703ca0 --- /dev/null +++ b/app/assets/javascripts/ember-addons/macro-alias.js.es6 @@ -0,0 +1,24 @@ +import isDescriptor from './utils/is-descriptor'; + +function handleDescriptor(target, property, desc, fn, params = []) { + return { + enumerable: desc.enumerable, + configurable: desc.configurable, + writable: desc.writable, + initializer: function() { + return fn(...params); + } + }; +} + +export default function macroAlias(fn) { + return function(...params) { + if (isDescriptor(params[params.length - 1])) { + return handleDescriptor(...params, fn); + } else { + return function(target, property, desc) { + return handleDescriptor(target, property, desc, fn, params); + }; + } + }; +} diff --git a/app/assets/javascripts/ember-addons/utils/extract-value.js.es6 b/app/assets/javascripts/ember-addons/utils/extract-value.js.es6 new file mode 100644 index 0000000000..da83b54b37 --- /dev/null +++ b/app/assets/javascripts/ember-addons/utils/extract-value.js.es6 @@ -0,0 +1,4 @@ +export default function extractValue(desc) { + return desc.value || + (typeof desc.initializer === 'function' && desc.initializer()); +} diff --git a/app/assets/javascripts/ember-addons/utils/handle-descriptor.js.es6 b/app/assets/javascripts/ember-addons/utils/handle-descriptor.js.es6 new file mode 100644 index 0000000000..fee32cac71 --- /dev/null +++ b/app/assets/javascripts/ember-addons/utils/handle-descriptor.js.es6 @@ -0,0 +1,67 @@ +import Ember from 'ember'; +import extractValue from './extract-value'; + +const { computed, get } = Ember; + +export default function handleDescriptor(target, key, desc, params = []) { + return { + enumerable: desc.enumerable, + configurable: desc.configurable, + writeable: desc.writeable, + initializer: function() { + let computedDescriptor; + + if (desc.writable) { + var val = extractValue(desc); + if (typeof val === 'object') { + let value = { }; + if (val.get) { value.get = callUserSuppliedGet(params, val.get); } + if (val.set) { value.set = callUserSuppliedSet(params, val.set); } + computedDescriptor = value; + } else { + computedDescriptor = callUserSuppliedGet(params, val); + } + } else { + throw new Error('ember-computed-decorators does not support using getters and setters'); + } + + return computed.apply(null, params.concat(computedDescriptor)); + } + }; +} + +function niceAttr(attr) { + const parts = attr.split('.'); + let i; + + for (i = 0; i < parts.length; i++) { + if (parts[i] === '@each' || + parts[i] === '[]' || + parts[i].indexOf('{') !== -1) { + break; + } + } + + return parts.slice(0, i).join('.'); +} + +function callUserSuppliedGet(params, func) { + params = params.map(niceAttr); + return function() { + let paramValues = params.map(p => get(this, p)); + + return func.apply(this, paramValues); + }; +} + + +function callUserSuppliedSet(params, func) { + params = params.map(niceAttr); + return function(key, value) { + let paramValues = params.map(p => get(this, p)); + paramValues.unshift(value); + + return func.apply(this, paramValues); + }; +} + diff --git a/app/assets/javascripts/ember-addons/utils/is-descriptor.js.es6 b/app/assets/javascripts/ember-addons/utils/is-descriptor.js.es6 new file mode 100644 index 0000000000..f956952ab2 --- /dev/null +++ b/app/assets/javascripts/ember-addons/utils/is-descriptor.js.es6 @@ -0,0 +1,7 @@ +export default function isDescriptor(item) { + return item && + typeof item === 'object' && + 'writable' in item && + 'enumerable' in item && + 'configurable' in item; +} diff --git a/app/assets/javascripts/ember_include.js.erb b/app/assets/javascripts/ember_include.js.erb index a5842eb6a7..72857343a4 100644 --- a/app/assets/javascripts/ember_include.js.erb +++ b/app/assets/javascripts/ember_include.js.erb @@ -1,7 +1,7 @@ <% if Rails.env.development? || Rails.env.test? require_asset ("ember-template-compiler.js") - require_asset ("ember.custom.debug.js") + require_asset ("ember.debug.js") else require_asset ("ember-template-compiler.js") require_asset ("ember.prod.js") diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 403271d5fe..0c02cfe923 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -5,6 +5,10 @@ //= require ./pagedown_custom.js // Stuff we need to load first +//= require_tree ./ember-addons/utils +//= require ./ember-addons/decorator-alias +//= require ./ember-addons/macro-alias +//= require ./ember-addons/ember-computed-decorators //= require ./discourse/lib/load-script //= require ./discourse/lib/notification-levels //= require ./discourse/lib/app-events @@ -94,4 +98,5 @@ //= require_tree ./discourse/helpers //= require_tree ./discourse/templates //= require_tree ./discourse/routes +//= require_tree ./discourse/pre-initializers //= require_tree ./discourse/initializers diff --git a/app/views/common/_discourse_javascript.html.erb b/app/views/common/_discourse_javascript.html.erb index df1977566e..0f67c8f1c4 100644 --- a/app/views/common/_discourse_javascript.html.erb +++ b/app/views/common/_discourse_javascript.html.erb @@ -26,8 +26,8 @@ }); <% if Rails.env.development? || Rails.env.test? %> - Ember.ENV.RAISE_ON_DEPRECATION = true - Ember.LOG_STACKTRACE_ON_DEPRECATION = true + //Ember.ENV.RAISE_ON_DEPRECATION = true + //Ember.LOG_STACKTRACE_ON_DEPRECATION = true <% end %> diff --git a/lib/discourse_iife.rb b/lib/discourse_iife.rb index fa80aa43fd..5c25fc6320 100644 --- a/lib/discourse_iife.rb +++ b/lib/discourse_iife.rb @@ -31,7 +31,7 @@ class DiscourseIIFE < Sprockets::Processor req_path = path.sub(Rails.root.to_s, '') .sub("/app/assets/javascripts", "") .sub("/test/javascripts", "") - res << "\nwindow.__jshintSrc = window.__jshintSrc || {}; window.__jshintSrc['/assets#{req_path}'] = #{data.to_json};\n" + res << "\nwindow.__eslintSrc = window.__eslintSrc || {}; window.__eslintSrc['/assets#{req_path}'] = #{data.to_json};\n" end res diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb index 3c91c2623f..2a320ebf59 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -83,8 +83,6 @@ module Tilt @output = klass.v8.eval(generate_source(scope)) end - source = @output.dup - # For backwards compatibility with plugins, for now export the Global format too. # We should eventually have an upgrade system for plugins to use ES6 or some other # resolve based API. @@ -129,7 +127,7 @@ module Tilt end req_path = "/assets/#{scope.logical_path}.#{extension}" - @output << "\nwindow.__jshintSrc = window.__jshintSrc || {}; window.__jshintSrc['#{req_path}'] = #{data.to_json};\n" + @output << "\nwindow.__eslintSrc = window.__eslintSrc || {}; window.__eslintSrc['#{req_path}'] = #{data.to_json};\n" end @output @@ -139,7 +137,7 @@ module Tilt def generate_source(scope) js_source = ::JSON.generate(data, quirks_mode: true) - js_source = "babel.transform(#{js_source}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping', 'es6.destructuring', 'es6.templateLiterals', 'es6.regex.unicode']})['code']" + js_source = "babel.transform(#{js_source}, {ast: false, whitelist: ['es6.constants', 'es6.properties.shorthand', 'es6.arrowFunctions', 'es6.blockScoping', 'es6.destructuring', 'es6.spread', 'es6.parameters', 'es6.templateLiterals', 'es6.regex.unicode', 'es7.decorators']})['code']" "new module.exports.Compiler(#{js_source}, '#{module_name(scope.root_path, scope.logical_path)}', #{compiler_options}).#{compiler_method}()" end diff --git a/test/javascripts/jshint-test.js.es6.erb b/test/javascripts/jshint-test.js.es6.erb deleted file mode 100644 index 6c5884aeaa..0000000000 --- a/test/javascripts/jshint-test.js.es6.erb +++ /dev/null @@ -1,107 +0,0 @@ -module("JSHint"); - -<%= "const JSHINT_OPTS = #{File.read(File.join(Rails.root, '.jshintrc'))};" %> - -var qHint = function(name, sourceFile) { - return asyncTestDiscourse(name, function() { - if (typeof window.__jshintSrc !== "undefined") { - var src = window.__jshintSrc[sourceFile]; - if (src) { - start(); - qHint.validateFile(src, JSHINT_OPTS); - return; - } - } - }); -}; - -qHint.validateFile = function (source, options, globals) { - var i, len, err; - - source = source.replace(/^[^]*\/\/ IIFE Wrapped Content Begins:\n\n/m, ""); - source = source.replace(/\n\n\/\/ IIFE Wrapped Content Ends[^]*$/m, ""); - - if (JSHINT(source, options, globals)) { - ok(true); - return; - } - - for (i = 0, len = JSHINT.errors.length; i < len; i++) { - err = JSHINT.errors[i]; - if (!err) { - continue; - } - - ok(false, err.reason + - " on line " + err.line + - ", character " + err.character); - } -}; - -var XMLHttpFactories = [ - function () { return new XMLHttpRequest(); }, - function () { return new ActiveXObject("Msxml2.XMLHTTP"); }, - function () { return new ActiveXObject("Msxml3.XMLHTTP"); }, - function () { return new ActiveXObject("Microsoft.XMLHTTP"); } -]; - -function createXMLHTTPObject() { - for (var i = 0; i < XMLHttpFactories.length; i++) { - try { - return XMLHttpFactories[i](); - } catch (e) {} - } - return false; -} - -// modified version of XHR script by PPK -// http://www.quirksmode.org/js/xmlhttp.html -// attached to qHint to allow substitution / mocking -qHint.sendRequest = function (url, callback) { - var req = createXMLHTTPObject(); - if (!req) { - return; - } - - var method = "GET"; - req.open(method,url + "?" + (new Date().getTime()),true); - req.onreadystatechange = function () { - if (req.readyState != 4) { - return; - } - - callback(req); - }; - - if (req.readyState == 4) { - return; - } - req.send(); -}; - -<% - TO_IGNORE = File.read("#{Rails.root}/.jshintignore").split("\n") - - def jshint(dir, remove) - result = "" - - Dir.glob(dir).each do |f| - filename = f.sub("#{Rails.root}/#{remove}", "") - - ok = true - TO_IGNORE.each do |ig| - ok = false unless (filename.index(ig.sub(remove, '')).nil?) - end - - depend_on filename - result << "qHint('#{filename}', '/assets/#{filename}', JSHINT_OPTS);\n" if ok - - end - result - end -%> - -<%= jshint("#{Rails.root}/test/**/*.js", "test/javascripts/") %> -<%= jshint("#{Rails.root}/app/assets/javascripts/**/*.js", "app/assets/javascripts/") %> -<%= jshint("#{Rails.root}/app/assets/javascripts/**/*.es6", "app/assets/javascripts/") %> - diff --git a/test/javascripts/lib/click-track-test.js.es6 b/test/javascripts/lib/click-track-test.js.es6 index b01d8b4239..3b228d59ae 100644 --- a/test/javascripts/lib/click-track-test.js.es6 +++ b/test/javascripts/lib/click-track-test.js.es6 @@ -75,7 +75,7 @@ test("removes the href and put it as a data attribute", function() { ok(DiscourseURL.redirectTo.calledOnce); }); -asyncTest("restores the href after a while", function() { +asyncTestDiscourse("restores the href after a while", function() { expect(1); track(generateClickEventOn('a')); diff --git a/test/javascripts/test_helper.js b/test/javascripts/test_helper.js index 746badbd25..533f2286d8 100644 --- a/test/javascripts/test_helper.js +++ b/test/javascripts/test_helper.js @@ -1,4 +1,3 @@ -/*jshint maxlen:250 */ /*global document, sinon, QUnit, Logster */ //= require env @@ -12,7 +11,7 @@ //= require jquery.debug //= require jquery.ui.widget //= require handlebars -//= require ember.custom.debug +//= require ember.debug //= require message-bus //= require ember-qunit //= require fake_xml_http_request @@ -36,7 +35,6 @@ //= require sinon-1.7.1 //= require sinon-qunit-1.0.0 -//= require jshint //= require helpers/qunit-helpers //= require helpers/assertions diff --git a/vendor/assets/javascripts/ember-template-compiler.js b/vendor/assets/javascripts/ember-template-compiler.js index 13bb96ffcd..2e49d24354 100644 --- a/vendor/assets/javascripts/ember-template-compiler.js +++ b/vendor/assets/javascripts/ember-template-compiler.js @@ -5,7 +5,7 @@ * Portions Copyright 2008-2011 Apple Inc. All rights reserved. * @license Licensed under MIT license * See https://raw.github.com/emberjs/ember.js/master/LICENSE - * @version 1.11.3 + * @version 1.12.1 */ (function() { @@ -16,7 +16,6 @@ var mainContext = this; Ember = this.Ember = this.Ember || {}; if (typeof Ember === 'undefined') { Ember = {}; }; - function UNDEFINED() { } if (typeof Ember.__loader === 'undefined') { var registry = {}; @@ -37,35 +36,43 @@ var mainContext = this; }; requirejs = eriuqer = requireModule = function(name) { - var s = seen[name]; + return internalRequire(name, null); + } - if (s !== undefined) { return seen[name]; } - if (s === UNDEFINED) { return undefined; } + function internalRequire(name, referrerName) { + var exports = seen[name]; - seen[name] = {}; + if (exports !== undefined) { + return exports; + } + + exports = seen[name] = {}; if (!registry[name]) { - throw new Error('Could not find module ' + name); + if (referrerName) { + throw new Error('Could not find module ' + name + ' required by: ' + referrerName); + } else { + throw new Error('Could not find module ' + name); + } } var mod = registry[name]; var deps = mod.deps; var callback = mod.callback; var reified = []; - var exports; var length = deps.length; for (var i=0; i\s*\(([^\)]+)\)/gm, "{anonymous}($1)").split("\n"); + stack.shift(); + } else { + // Firefox + stack = error.stack.replace(/(?:\n@:0)?\s+$/m, "").replace(/^\(/gm, "{anonymous}(").split("\n"); + } + + stackStr = "\n " + stack.slice(2).join("\n "); + message = message + stackStr; + } + + Logger['default'].warn("DEPRECATION: " + message); + }; + + /** + Alias an old, deprecated method with its new counterpart. + + Display a deprecation warning with the provided message and a stack trace + (Chrome and Firefox only) when the assigned method is called. + + Ember build tools will not remove calls to `Ember.deprecateFunc()`, though + no warnings will be shown in production. + + ```javascript + Ember.oldMethod = Ember.deprecateFunc('Please use the new, updated method', Ember.newMethod); + ``` + + @method deprecateFunc + @param {String} message A description of the deprecation. + @param {Function} func The new function called to replace its deprecated counterpart. + @return {Function} a new function that wrapped the original function with a deprecation warning + */ + Ember['default'].deprecateFunc = function (message, func) { + return function () { + Ember['default'].deprecate(message); + return func.apply(this, arguments); + }; + }; + + /** + Run a function meant for debugging. Ember build tools will remove any calls to + `Ember.runInDebug()` when doing a production build. + + ```javascript + Ember.runInDebug(function() { + Ember.Handlebars.EachView.reopen({ + didInsertElement: function() { + console.log('I\'m happy'); + } + }); + }); + ``` + + @method runInDebug + @param {Function} func The function to be executed. + @since 1.5.0 + */ + Ember['default'].runInDebug = function (func) { + func(); + }; + + /** + Will call `Ember.warn()` if ENABLE_ALL_FEATURES, ENABLE_OPTIONAL_FEATURES, or + any specific FEATURES flag is truthy. + + This method is called automatically in debug canary builds. + + @private + @method _warnIfUsingStrippedFeatureFlags + @return {void} + */ + function _warnIfUsingStrippedFeatureFlags(FEATURES, featuresWereStripped) { + if (featuresWereStripped) { + Ember['default'].warn("Ember.ENV.ENABLE_ALL_FEATURES is only available in canary builds.", !Ember['default'].ENV.ENABLE_ALL_FEATURES); + Ember['default'].warn("Ember.ENV.ENABLE_OPTIONAL_FEATURES is only available in canary builds.", !Ember['default'].ENV.ENABLE_OPTIONAL_FEATURES); + + for (var key in FEATURES) { + if (FEATURES.hasOwnProperty(key) && key !== "isEnabled") { + Ember['default'].warn("FEATURE[\"" + key + "\"] is set as enabled, but FEATURE flags are only available in canary builds.", !FEATURES[key]); + } + } + } + } + + if (!Ember['default'].testing) { + // Complain if they're using FEATURE flags in builds other than canary + Ember['default'].FEATURES["features-stripped-test"] = true; + var featuresWereStripped = true; + + + delete Ember['default'].FEATURES["features-stripped-test"]; + _warnIfUsingStrippedFeatureFlags(Ember['default'].ENV.FEATURES, featuresWereStripped); + + // Inform the developer about the Ember Inspector if not installed. + var isFirefox = typeof InstallTrigger !== "undefined"; + var isChrome = environment['default'].isChrome; + + if (typeof window !== "undefined" && (isFirefox || isChrome) && window.addEventListener) { + window.addEventListener("load", function () { + if (document.documentElement && document.documentElement.dataset && !document.documentElement.dataset.emberExtension) { + var downloadURL; + + if (isChrome) { + downloadURL = "https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi"; + } else if (isFirefox) { + downloadURL = "https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/"; + } + + Ember['default'].debug("For more advanced debugging, install the Ember Inspector from " + downloadURL); + } + }, false); + } + } + + /* + We are transitioning away from `ember.js` to `ember.debug.js` to make + it much clearer that it is only for local development purposes. + + This flag value is changed by the tooling (by a simple string replacement) + so that if `ember.js` (which must be output for backwards compat reasons) is + used a nice helpful warning message will be printed out. + */ + var runningNonEmberDebugJS = false; + if (runningNonEmberDebugJS) { + Ember['default'].warn("Please use `ember.debug.js` instead of `ember.js` for development and debugging."); + } + + exports.runningNonEmberDebugJS = runningNonEmberDebugJS; + +}); +enifed('ember-metal', ['exports', 'ember-metal/core', 'ember-metal/merge', 'ember-metal/instrumentation', 'ember-metal/utils', 'ember-metal/error', 'ember-metal/enumerable_utils', 'ember-metal/cache', 'ember-metal/platform/define_property', 'ember-metal/platform/create', 'ember-metal/array', 'ember-metal/logger', 'ember-metal/property_get', 'ember-metal/events', 'ember-metal/observer_set', 'ember-metal/property_events', 'ember-metal/properties', 'ember-metal/property_set', 'ember-metal/map', 'ember-metal/get_properties', 'ember-metal/set_properties', 'ember-metal/watch_key', 'ember-metal/chains', 'ember-metal/watch_path', 'ember-metal/watching', 'ember-metal/expand_properties', 'ember-metal/computed', 'ember-metal/alias', 'ember-metal/computed_macros', 'ember-metal/observer', 'ember-metal/mixin', 'ember-metal/binding', 'ember-metal/run_loop', 'ember-metal/libraries', 'ember-metal/is_none', 'ember-metal/is_empty', 'ember-metal/is_blank', 'ember-metal/is_present', 'ember-metal/keys', 'backburner', 'ember-metal/streams/utils', 'ember-metal/streams/stream'], function (exports, Ember, merge, instrumentation, utils, EmberError, EnumerableUtils, Cache, define_property, create, array, Logger, property_get, events, ObserverSet, property_events, properties, property_set, map, getProperties, setProperties, watch_key, chains, watch_path, watching, expandProperties, computed, alias, computed_macros, observer, mixin, binding, run, Libraries, isNone, isEmpty, isBlank, isPresent, keys, Backburner, streams__utils, Stream) { + + 'use strict'; + + /** + Ember Metal + + @module ember + @submodule ember-metal + */ + + // BEGIN IMPORTS + computed.computed.empty = computed_macros.empty; + computed.computed.notEmpty = computed_macros.notEmpty; + computed.computed.none = computed_macros.none; + computed.computed.not = computed_macros.not; + computed.computed.bool = computed_macros.bool; + computed.computed.match = computed_macros.match; + computed.computed.equal = computed_macros.equal; + computed.computed.gt = computed_macros.gt; + computed.computed.gte = computed_macros.gte; + computed.computed.lt = computed_macros.lt; + computed.computed.lte = computed_macros.lte; + computed.computed.alias = alias['default']; + computed.computed.oneWay = computed_macros.oneWay; + computed.computed.reads = computed_macros.oneWay; + computed.computed.readOnly = computed_macros.readOnly; + computed.computed.defaultTo = computed_macros.defaultTo; + computed.computed.deprecatingAlias = computed_macros.deprecatingAlias; + computed.computed.and = computed_macros.and; + computed.computed.or = computed_macros.or; + computed.computed.any = computed_macros.any; + computed.computed.collect = computed_macros.collect; // END IMPORTS + + // BEGIN EXPORTS + var EmberInstrumentation = Ember['default'].Instrumentation = {}; + EmberInstrumentation.instrument = instrumentation.instrument; + EmberInstrumentation.subscribe = instrumentation.subscribe; + EmberInstrumentation.unsubscribe = instrumentation.unsubscribe; + EmberInstrumentation.reset = instrumentation.reset; + + Ember['default'].instrument = instrumentation.instrument; + Ember['default'].subscribe = instrumentation.subscribe; + + Ember['default']._Cache = Cache['default']; + + Ember['default'].generateGuid = utils.generateGuid; + Ember['default'].GUID_KEY = utils.GUID_KEY; + Ember['default'].create = create['default']; + Ember['default'].keys = keys['default']; + Ember['default'].platform = { + defineProperty: properties.defineProperty, + hasPropertyAccessors: define_property.hasPropertyAccessors + }; + + var EmberArrayPolyfills = Ember['default'].ArrayPolyfills = {}; + + EmberArrayPolyfills.map = array.map; + EmberArrayPolyfills.forEach = array.forEach; + EmberArrayPolyfills.filter = array.filter; + EmberArrayPolyfills.indexOf = array.indexOf; + + Ember['default'].Error = EmberError['default']; + Ember['default'].guidFor = utils.guidFor; + Ember['default'].META_DESC = utils.META_DESC; + Ember['default'].EMPTY_META = utils.EMPTY_META; + Ember['default'].meta = utils.meta; + Ember['default'].getMeta = utils.getMeta; + Ember['default'].setMeta = utils.setMeta; + Ember['default'].metaPath = utils.metaPath; + Ember['default'].inspect = utils.inspect; + Ember['default'].typeOf = utils.typeOf; + Ember['default'].tryCatchFinally = utils.deprecatedTryCatchFinally; + Ember['default'].isArray = utils.isArray; + Ember['default'].makeArray = utils.makeArray; + Ember['default'].canInvoke = utils.canInvoke; + Ember['default'].tryInvoke = utils.tryInvoke; + Ember['default'].tryFinally = utils.deprecatedTryFinally; + Ember['default'].wrap = utils.wrap; + Ember['default'].apply = utils.apply; + Ember['default'].applyStr = utils.applyStr; + Ember['default'].uuid = utils.uuid; + + Ember['default'].Logger = Logger['default']; + + Ember['default'].get = property_get.get; + Ember['default'].getWithDefault = property_get.getWithDefault; + Ember['default'].normalizeTuple = property_get.normalizeTuple; + Ember['default']._getPath = property_get._getPath; + + Ember['default'].EnumerableUtils = EnumerableUtils['default']; + + Ember['default'].on = events.on; + Ember['default'].addListener = events.addListener; + Ember['default'].removeListener = events.removeListener; + Ember['default']._suspendListener = events.suspendListener; + Ember['default']._suspendListeners = events.suspendListeners; + Ember['default'].sendEvent = events.sendEvent; + Ember['default'].hasListeners = events.hasListeners; + Ember['default'].watchedEvents = events.watchedEvents; + Ember['default'].listenersFor = events.listenersFor; + Ember['default'].accumulateListeners = events.accumulateListeners; + + Ember['default']._ObserverSet = ObserverSet['default']; + + Ember['default'].propertyWillChange = property_events.propertyWillChange; + Ember['default'].propertyDidChange = property_events.propertyDidChange; + Ember['default'].overrideChains = property_events.overrideChains; + Ember['default'].beginPropertyChanges = property_events.beginPropertyChanges; + Ember['default'].endPropertyChanges = property_events.endPropertyChanges; + Ember['default'].changeProperties = property_events.changeProperties; + + Ember['default'].defineProperty = properties.defineProperty; + + Ember['default'].set = property_set.set; + Ember['default'].trySet = property_set.trySet; + + Ember['default'].OrderedSet = map.OrderedSet; + Ember['default'].Map = map.Map; + Ember['default'].MapWithDefault = map.MapWithDefault; + + Ember['default'].getProperties = getProperties['default']; + Ember['default'].setProperties = setProperties['default']; + + Ember['default'].watchKey = watch_key.watchKey; + Ember['default'].unwatchKey = watch_key.unwatchKey; + + Ember['default'].flushPendingChains = chains.flushPendingChains; + Ember['default'].removeChainWatcher = chains.removeChainWatcher; + Ember['default']._ChainNode = chains.ChainNode; + Ember['default'].finishChains = chains.finishChains; + + Ember['default'].watchPath = watch_path.watchPath; + Ember['default'].unwatchPath = watch_path.unwatchPath; + + Ember['default'].watch = watching.watch; + Ember['default'].isWatching = watching.isWatching; + Ember['default'].unwatch = watching.unwatch; + Ember['default'].rewatch = watching.rewatch; + Ember['default'].destroy = watching.destroy; + + Ember['default'].expandProperties = expandProperties['default']; + + Ember['default'].ComputedProperty = computed.ComputedProperty; + Ember['default'].computed = computed.computed; + Ember['default'].cacheFor = computed.cacheFor; + + Ember['default'].addObserver = observer.addObserver; + Ember['default'].observersFor = observer.observersFor; + Ember['default'].removeObserver = observer.removeObserver; + Ember['default'].addBeforeObserver = observer.addBeforeObserver; + Ember['default']._suspendBeforeObserver = observer._suspendBeforeObserver; + Ember['default']._suspendBeforeObservers = observer._suspendBeforeObservers; + Ember['default']._suspendObserver = observer._suspendObserver; + Ember['default']._suspendObservers = observer._suspendObservers; + Ember['default'].beforeObserversFor = observer.beforeObserversFor; + Ember['default'].removeBeforeObserver = observer.removeBeforeObserver; + + Ember['default'].IS_BINDING = mixin.IS_BINDING; + Ember['default'].required = mixin.required; + Ember['default'].aliasMethod = mixin.aliasMethod; + Ember['default'].observer = mixin.observer; + Ember['default'].immediateObserver = mixin.immediateObserver; + Ember['default'].beforeObserver = mixin.beforeObserver; + Ember['default'].mixin = mixin.mixin; + Ember['default'].Mixin = mixin.Mixin; + + Ember['default'].oneWay = binding.oneWay; + Ember['default'].bind = binding.bind; + Ember['default'].Binding = binding.Binding; + Ember['default'].isGlobalPath = binding.isGlobalPath; + + Ember['default'].run = run['default']; + + /** + * @class Backburner + * @for Ember + * @private + */ + Ember['default'].Backburner = Backburner['default']; + + Ember['default'].libraries = new Libraries['default'](); + Ember['default'].libraries.registerCoreLibrary("Ember", Ember['default'].VERSION); + + Ember['default'].isNone = isNone['default']; + Ember['default'].isEmpty = isEmpty['default']; + Ember['default'].isBlank = isBlank['default']; + Ember['default'].isPresent = isPresent['default']; + + Ember['default'].merge = merge['default']; + + + /** + A function may be assigned to `Ember.onerror` to be called when Ember + internals encounter an error. This is useful for specialized error handling + and reporting code. + + ```javascript + Ember.onerror = function(error) { + Em.$.ajax('/report-error', 'POST', { + stack: error.stack, + otherInformation: 'whatever app state you want to provide' + }); + }; + ``` + + Internally, `Ember.onerror` is used as Backburner's error handler. + + @event onerror + @for Ember + @param {Exception} error the error object + */ + Ember['default'].onerror = null; + // END EXPORTS + + // do this for side-effects of updating Ember.assert, warn, etc when + // ember-debug is present + if (Ember['default'].__loader.registry["ember-debug"]) { + requireModule("ember-debug"); + } + + exports['default'] = Ember['default']; + +}); +enifed('ember-metal/alias', ['exports', 'ember-metal/property_get', 'ember-metal/property_set', 'ember-metal/core', 'ember-metal/error', 'ember-metal/properties', 'ember-metal/computed', 'ember-metal/platform/create', 'ember-metal/utils', 'ember-metal/dependent_keys'], function (exports, property_get, property_set, Ember, EmberError, properties, computed, create, utils, dependent_keys) { + + 'use strict'; + + exports.AliasedProperty = AliasedProperty; + + exports['default'] = alias; + + function alias(altKey) { + return new AliasedProperty(altKey); + } + + function AliasedProperty(altKey) { + this.isDescriptor = true; + this.altKey = altKey; + this._dependentKeys = [altKey]; + } + + AliasedProperty.prototype = create['default'](properties.Descriptor.prototype); + + AliasedProperty.prototype.get = function AliasedProperty_get(obj, keyName) { + return property_get.get(obj, this.altKey); + }; + + AliasedProperty.prototype.set = function AliasedProperty_set(obj, keyName, value) { + return property_set.set(obj, this.altKey, value); + }; + + AliasedProperty.prototype.willWatch = function (obj, keyName) { + dependent_keys.addDependentKeys(this, obj, keyName, utils.meta(obj)); + }; + + AliasedProperty.prototype.didUnwatch = function (obj, keyName) { + dependent_keys.removeDependentKeys(this, obj, keyName, utils.meta(obj)); + }; + + AliasedProperty.prototype.setup = function (obj, keyName) { + Ember['default'].assert("Setting alias '" + keyName + "' on self", this.altKey !== keyName); + var m = utils.meta(obj); + if (m.watching[keyName]) { + dependent_keys.addDependentKeys(this, obj, keyName, m); + } + }; + + AliasedProperty.prototype.teardown = function (obj, keyName) { + var m = utils.meta(obj); + if (m.watching[keyName]) { + dependent_keys.removeDependentKeys(this, obj, keyName, m); + } + }; + + AliasedProperty.prototype.readOnly = function () { + this.set = AliasedProperty_readOnlySet; + return this; + }; + + function AliasedProperty_readOnlySet(obj, keyName, value) { + throw new EmberError['default']("Cannot set read-only property '" + keyName + "' on object: " + utils.inspect(obj)); + } + + AliasedProperty.prototype.oneWay = function () { + this.set = AliasedProperty_oneWaySet; + return this; + }; + + function AliasedProperty_oneWaySet(obj, keyName, value) { + properties.defineProperty(obj, keyName, null); + return property_set.set(obj, keyName, value); + } + + // Backwards compatibility with Ember Data + AliasedProperty.prototype._meta = undefined; + AliasedProperty.prototype.meta = computed.ComputedProperty.prototype.meta; + +}); +enifed('ember-metal/array', ['exports'], function (exports) { + + 'use strict'; + + /** + @module ember-metal + */ + + var ArrayPrototype = Array.prototype; + + // Testing this is not ideal, but we want to use native functions + // if available, but not to use versions created by libraries like Prototype + var isNativeFunc = function (func) { + // This should probably work in all browsers likely to have ES5 array methods + return func && Function.prototype.toString.call(func).indexOf("[native code]") > -1; + }; + + var defineNativeShim = function (nativeFunc, shim) { + if (isNativeFunc(nativeFunc)) { + return nativeFunc; + } + return shim; + }; + + // From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/map + var map = defineNativeShim(ArrayPrototype.map, function (fun) { + //"use strict"; + + if (this === void 0 || this === null || typeof fun !== "function") { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + var res = new Array(len); + + for (var i = 0; i < len; i++) { + if (i in t) { + res[i] = fun.call(arguments[1], t[i], i, t); + } + } + + return res; + }); + + // From: https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/array/foreach + var forEach = defineNativeShim(ArrayPrototype.forEach, function (fun) { + //"use strict"; + + if (this === void 0 || this === null || typeof fun !== "function") { + throw new TypeError(); + } + + var t = Object(this); + var len = t.length >>> 0; + + for (var i = 0; i < len; i++) { + if (i in t) { + fun.call(arguments[1], t[i], i, t); + } + } + }); + + var indexOf = defineNativeShim(ArrayPrototype.indexOf, function (obj, fromIndex) { + if (fromIndex === null || fromIndex === undefined) { + fromIndex = 0; + } else if (fromIndex < 0) { + fromIndex = Math.max(0, this.length + fromIndex); + } + + for (var i = fromIndex, j = this.length; i < j; i++) { + if (this[i] === obj) { + return i; + } + } + return -1; + }); + + var lastIndexOf = defineNativeShim(ArrayPrototype.lastIndexOf, function (obj, fromIndex) { + var len = this.length; + var idx; + + if (fromIndex === undefined) { + fromIndex = len - 1; + } else { + fromIndex = fromIndex < 0 ? Math.ceil(fromIndex) : Math.floor(fromIndex); + } + + if (fromIndex < 0) { + fromIndex += len; + } + + for (idx = fromIndex; idx >= 0; idx--) { + if (this[idx] === obj) { + return idx; + } + } + return -1; + }); + + var filter = defineNativeShim(ArrayPrototype.filter, function (fn, context) { + var i, value; + var result = []; + var length = this.length; + + for (i = 0; i < length; i++) { + if (this.hasOwnProperty(i)) { + value = this[i]; + if (fn.call(context, value, i, this)) { + result.push(value); + } + } + } + return result; + }); + + if (Ember.SHIM_ES5) { + ArrayPrototype.map = ArrayPrototype.map || map; + ArrayPrototype.forEach = ArrayPrototype.forEach || forEach; + ArrayPrototype.filter = ArrayPrototype.filter || filter; + ArrayPrototype.indexOf = ArrayPrototype.indexOf || indexOf; + ArrayPrototype.lastIndexOf = ArrayPrototype.lastIndexOf || lastIndexOf; + } + + /** + Array polyfills to support ES5 features in older browsers. + + @namespace Ember + @property ArrayPolyfills + */ + + exports.map = map; + exports.forEach = forEach; + exports.filter = filter; + exports.indexOf = indexOf; + exports.lastIndexOf = lastIndexOf; + +}); +enifed('ember-metal/binding', ['exports', 'ember-metal/core', 'ember-metal/property_get', 'ember-metal/property_set', 'ember-metal/utils', 'ember-metal/observer', 'ember-metal/run_loop', 'ember-metal/path_cache'], function (exports, Ember, property_get, property_set, utils, observer, run, path_cache) { + + 'use strict'; + + exports.bind = bind; + exports.oneWay = oneWay; + exports.Binding = Binding; + + Ember['default'].LOG_BINDINGS = false || !!Ember['default'].ENV.LOG_BINDINGS; + + /** + Returns true if the provided path is global (e.g., `MyApp.fooController.bar`) + instead of local (`foo.bar.baz`). + + @method isGlobalPath + @for Ember + @private + @param {String} path + @return Boolean + */ + + function getWithGlobals(obj, path) { + return property_get.get(path_cache.isGlobal(path) ? Ember['default'].lookup : obj, path); + } + + // .......................................................... + // BINDING + // + + function Binding(toPath, fromPath) { + this._direction = undefined; + this._from = fromPath; + this._to = toPath; + this._readyToSync = undefined; + this._oneWay = undefined; + } + + /** + @class Binding + @namespace Ember + */ + + Binding.prototype = { + /** + This copies the Binding so it can be connected to another object. + @method copy + @return {Ember.Binding} `this` + */ + copy: function () { + var copy = new Binding(this._to, this._from); + if (this._oneWay) { + copy._oneWay = true; + } + return copy; + }, + + // .......................................................... + // CONFIG + // + + /** + This will set `from` property path to the specified value. It will not + attempt to resolve this property path to an actual object until you + connect the binding. + The binding will search for the property path starting at the root object + you pass when you `connect()` the binding. It follows the same rules as + `get()` - see that method for more information. + @method from + @param {String} path the property path to connect to + @return {Ember.Binding} `this` + */ + from: function (path) { + this._from = path; + return this; + }, + + /** + This will set the `to` property path to the specified value. It will not + attempt to resolve this property path to an actual object until you + connect the binding. + The binding will search for the property path starting at the root object + you pass when you `connect()` the binding. It follows the same rules as + `get()` - see that method for more information. + @method to + @param {String|Tuple} path A property path or tuple + @return {Ember.Binding} `this` + */ + to: function (path) { + this._to = path; + return this; + }, + + /** + Configures the binding as one way. A one-way binding will relay changes + on the `from` side to the `to` side, but not the other way around. This + means that if you change the `to` side directly, the `from` side may have + a different value. + @method oneWay + @return {Ember.Binding} `this` + */ + oneWay: function () { + this._oneWay = true; + return this; + }, + + /** + @method toString + @return {String} string representation of binding + */ + toString: function () { + var oneWay = this._oneWay ? "[oneWay]" : ""; + return "Ember.Binding<" + utils.guidFor(this) + ">(" + this._from + " -> " + this._to + ")" + oneWay; + }, + + // .......................................................... + // CONNECT AND SYNC + // + + /** + Attempts to connect this binding instance so that it can receive and relay + changes. This method will raise an exception if you have not set the + from/to properties yet. + @method connect + @param {Object} obj The root object for this binding. + @return {Ember.Binding} `this` + */ + connect: function (obj) { + Ember['default'].assert("Must pass a valid object to Ember.Binding.connect()", !!obj); + + var fromPath = this._from; + var toPath = this._to; + property_set.trySet(obj, toPath, getWithGlobals(obj, fromPath)); + + // add an observer on the object to be notified when the binding should be updated + observer.addObserver(obj, fromPath, this, this.fromDidChange); + + // if the binding is a two-way binding, also set up an observer on the target + if (!this._oneWay) { + observer.addObserver(obj, toPath, this, this.toDidChange); + } + + this._readyToSync = true; + + return this; + }, + + /** + Disconnects the binding instance. Changes will no longer be relayed. You + will not usually need to call this method. + @method disconnect + @param {Object} obj The root object you passed when connecting the binding. + @return {Ember.Binding} `this` + */ + disconnect: function (obj) { + Ember['default'].assert("Must pass a valid object to Ember.Binding.disconnect()", !!obj); + + var twoWay = !this._oneWay; + + // remove an observer on the object so we're no longer notified of + // changes that should update bindings. + observer.removeObserver(obj, this._from, this, this.fromDidChange); + + // if the binding is two-way, remove the observer from the target as well + if (twoWay) { + observer.removeObserver(obj, this._to, this, this.toDidChange); + } + + this._readyToSync = false; // disable scheduled syncs... + return this; + }, + + // .......................................................... + // PRIVATE + // + + /* called when the from side changes */ + fromDidChange: function (target) { + this._scheduleSync(target, "fwd"); + }, + + /* called when the to side changes */ + toDidChange: function (target) { + this._scheduleSync(target, "back"); + }, + + _scheduleSync: function (obj, dir) { + var existingDir = this._direction; + + // if we haven't scheduled the binding yet, schedule it + if (existingDir === undefined) { + run['default'].schedule("sync", this, this._sync, obj); + this._direction = dir; + } + + // If both a 'back' and 'fwd' sync have been scheduled on the same object, + // default to a 'fwd' sync so that it remains deterministic. + if (existingDir === "back" && dir === "fwd") { + this._direction = "fwd"; + } + }, + + _sync: function (obj) { + var log = Ember['default'].LOG_BINDINGS; + + // don't synchronize destroyed objects or disconnected bindings + if (obj.isDestroyed || !this._readyToSync) { + return; + } + + // get the direction of the binding for the object we are + // synchronizing from + var direction = this._direction; + + var fromPath = this._from; + var toPath = this._to; + + this._direction = undefined; + + // if we're synchronizing from the remote object... + if (direction === "fwd") { + var fromValue = getWithGlobals(obj, this._from); + if (log) { + Ember['default'].Logger.log(" ", this.toString(), "->", fromValue, obj); + } + if (this._oneWay) { + property_set.trySet(obj, toPath, fromValue); + } else { + observer._suspendObserver(obj, toPath, this, this.toDidChange, function () { + property_set.trySet(obj, toPath, fromValue); + }); + } + // if we're synchronizing *to* the remote object + } else if (direction === "back") { + var toValue = property_get.get(obj, this._to); + if (log) { + Ember['default'].Logger.log(" ", this.toString(), "<-", toValue, obj); + } + observer._suspendObserver(obj, fromPath, this, this.fromDidChange, function () { + property_set.trySet(path_cache.isGlobal(fromPath) ? Ember['default'].lookup : obj, fromPath, toValue); + }); + } + } + + }; + + function mixinProperties(to, from) { + for (var key in from) { + if (from.hasOwnProperty(key)) { + to[key] = from[key]; + } + } + } + + mixinProperties(Binding, { + + /* + See `Ember.Binding.from`. + @method from + @static + */ + from: function (from) { + var C = this; + return new C(undefined, from); + }, + + /* + See `Ember.Binding.to`. + @method to + @static + */ + to: function (to) { + var C = this; + return new C(to, undefined); + }, + + /** + Creates a new Binding instance and makes it apply in a single direction. + A one-way binding will relay changes on the `from` side object (supplied + as the `from` argument) the `to` side, but not the other way around. + This means that if you change the "to" side directly, the "from" side may have + a different value. + See `Binding.oneWay`. + @method oneWay + @param {String} from from path. + @param {Boolean} [flag] (Optional) passing nothing here will make the + binding `oneWay`. You can instead pass `false` to disable `oneWay`, making the + binding two way again. + @return {Ember.Binding} `this` + */ + oneWay: function (from, flag) { + var C = this; + return new C(undefined, from).oneWay(flag); + } + + }); + /** + An `Ember.Binding` connects the properties of two objects so that whenever + the value of one property changes, the other property will be changed also. + + ## Automatic Creation of Bindings with `/^*Binding/`-named Properties + + You do not usually create Binding objects directly but instead describe + bindings in your class or object definition using automatic binding + detection. + + Properties ending in a `Binding` suffix will be converted to `Ember.Binding` + instances. The value of this property should be a string representing a path + to another object or a custom binding instance created using Binding helpers + (see "One Way Bindings"): + + ``` + valueBinding: "MyApp.someController.title" + ``` + + This will create a binding from `MyApp.someController.title` to the `value` + property of your object instance automatically. Now the two values will be + kept in sync. + + ## One Way Bindings + + One especially useful binding customization you can use is the `oneWay()` + helper. This helper tells Ember that you are only interested in + receiving changes on the object you are binding from. For example, if you + are binding to a preference and you want to be notified if the preference + has changed, but your object will not be changing the preference itself, you + could do: + + ``` + bigTitlesBinding: Ember.Binding.oneWay("MyApp.preferencesController.bigTitles") + ``` + + This way if the value of `MyApp.preferencesController.bigTitles` changes the + `bigTitles` property of your object will change also. However, if you + change the value of your `bigTitles` property, it will not update the + `preferencesController`. + + One way bindings are almost twice as fast to setup and twice as fast to + execute because the binding only has to worry about changes to one side. + + You should consider using one way bindings anytime you have an object that + may be created frequently and you do not intend to change a property; only + to monitor it for changes (such as in the example above). + + ## Adding Bindings Manually + + All of the examples above show you how to configure a custom binding, but the + result of these customizations will be a binding template, not a fully active + Binding instance. The binding will actually become active only when you + instantiate the object the binding belongs to. It is useful however, to + understand what actually happens when the binding is activated. + + For a binding to function it must have at least a `from` property and a `to` + property. The `from` property path points to the object/key that you want to + bind from while the `to` path points to the object/key you want to bind to. + + When you define a custom binding, you are usually describing the property + you want to bind from (such as `MyApp.someController.value` in the examples + above). When your object is created, it will automatically assign the value + you want to bind `to` based on the name of your binding key. In the + examples above, during init, Ember objects will effectively call + something like this on your binding: + + ```javascript + binding = Ember.Binding.from("valueBinding").to("value"); + ``` + + This creates a new binding instance based on the template you provide, and + sets the to path to the `value` property of the new object. Now that the + binding is fully configured with a `from` and a `to`, it simply needs to be + connected to become active. This is done through the `connect()` method: + + ```javascript + binding.connect(this); + ``` + + Note that when you connect a binding you pass the object you want it to be + connected to. This object will be used as the root for both the from and + to side of the binding when inspecting relative paths. This allows the + binding to be automatically inherited by subclassed objects as well. + + This also allows you to bind between objects using the paths you declare in + `from` and `to`: + + ```javascript + // Example 1 + binding = Ember.Binding.from("App.someObject.value").to("value"); + binding.connect(this); + + // Example 2 + binding = Ember.Binding.from("parentView.value").to("App.someObject.value"); + binding.connect(this); + ``` + + Now that the binding is connected, it will observe both the from and to side + and relay changes. + + If you ever needed to do so (you almost never will, but it is useful to + understand this anyway), you could manually create an active binding by + using the `Ember.bind()` helper method. (This is the same method used by + to setup your bindings on objects): + + ```javascript + Ember.bind(MyApp.anotherObject, "value", "MyApp.someController.value"); + ``` + + Both of these code fragments have the same effect as doing the most friendly + form of binding creation like so: + + ```javascript + MyApp.anotherObject = Ember.Object.create({ + valueBinding: "MyApp.someController.value", + + // OTHER CODE FOR THIS OBJECT... + }); + ``` + + Ember's built in binding creation method makes it easy to automatically + create bindings for you. You should always use the highest-level APIs + available, even if you understand how it works underneath. + + @class Binding + @namespace Ember + @since Ember 0.9 + */ + // Ember.Binding = Binding; ES6TODO: where to put this? + + /** + Global helper method to create a new binding. Just pass the root object + along with a `to` and `from` path to create and connect the binding. + + @method bind + @for Ember + @param {Object} obj The root object of the transform. + @param {String} to The path to the 'to' side of the binding. + Must be relative to obj. + @param {String} from The path to the 'from' side of the binding. + Must be relative to obj or a global path. + @return {Ember.Binding} binding instance + */ + function bind(obj, to, from) { + return new Binding(to, from).connect(obj); + } + + /** + @method oneWay + @for Ember + @param {Object} obj The root object of the transform. + @param {String} to The path to the 'to' side of the binding. + Must be relative to obj. + @param {String} from The path to the 'from' side of the binding. + Must be relative to obj or a global path. + @return {Ember.Binding} binding instance + */ + function oneWay(obj, to, from) { + return new Binding(to, from).oneWay().connect(obj); + } + + exports.isGlobalPath = path_cache.isGlobal; + +}); +enifed('ember-metal/cache', ['exports', 'ember-metal/dictionary'], function (exports, dictionary) { + + 'use strict'; + + exports['default'] = Cache; + + function Cache(limit, func) { + this.store = dictionary['default'](null); + this.size = 0; + this.misses = 0; + this.hits = 0; + this.limit = limit; + this.func = func; + } + + var UNDEFINED = function () {}; + + Cache.prototype = { + set: function (key, value) { + if (this.limit > this.size) { + this.size++; + if (value === undefined) { + this.store[key] = UNDEFINED; + } else { + this.store[key] = value; + } + } + + return value; + }, + + get: function (key) { + var value = this.store[key]; + + if (value === undefined) { + this.misses++; + value = this.set(key, this.func(key)); + } else if (value === UNDEFINED) { + this.hits++; + value = undefined; + } else { + this.hits++; + // nothing to translate + } + + return value; + }, + + purge: function () { + this.store = dictionary['default'](null); + this.size = 0; + this.hits = 0; + this.misses = 0; + } + }; + +}); +enifed('ember-metal/chains', ['exports', 'ember-metal/core', 'ember-metal/property_get', 'ember-metal/utils', 'ember-metal/array', 'ember-metal/watch_key'], function (exports, Ember, property_get, utils, array, watch_key) { + + 'use strict'; + + exports.flushPendingChains = flushPendingChains; + exports.finishChains = finishChains; + exports.removeChainWatcher = removeChainWatcher; + exports.ChainNode = ChainNode; + + var warn = Ember['default'].warn; + var FIRST_KEY = /^([^\.]+)/; + + function firstKey(path) { + return path.match(FIRST_KEY)[0]; + } + + function isObject(obj) { + return obj && typeof obj === "object"; + } + + var pendingQueue = []; + + // attempts to add the pendingQueue chains again. If some of them end up + // back in the queue and reschedule is true, schedules a timeout to try + // again. + + function flushPendingChains() { + if (pendingQueue.length === 0) { + return; + } + + var queue = pendingQueue; + pendingQueue = []; + + array.forEach.call(queue, function (q) { + q[0].add(q[1]); + }); + + warn("Watching an undefined global, Ember expects watched globals to be" + " setup by the time the run loop is flushed, check for typos", pendingQueue.length === 0); + } + + function addChainWatcher(obj, keyName, node) { + if (!isObject(obj)) { + return; + } + + var m = utils.meta(obj); + var nodes = m.chainWatchers; + + if (!m.hasOwnProperty("chainWatchers")) { + // FIXME?! + nodes = m.chainWatchers = {}; + } + + if (!nodes[keyName]) { + nodes[keyName] = []; + } + nodes[keyName].push(node); + watch_key.watchKey(obj, keyName, m); + } + + function removeChainWatcher(obj, keyName, node) { + if (!isObject(obj)) { + return; + } + + var m = obj["__ember_meta__"]; + if (m && !m.hasOwnProperty("chainWatchers")) { + return; + } + + var nodes = m && m.chainWatchers; + + if (nodes && nodes[keyName]) { + nodes = nodes[keyName]; + for (var i = 0, l = nodes.length; i < l; i++) { + if (nodes[i] === node) { + nodes.splice(i, 1); + break; + } + } + } + watch_key.unwatchKey(obj, keyName, m); + } + + // A ChainNode watches a single key on an object. If you provide a starting + // value for the key then the node won't actually watch it. For a root node + // pass null for parent and key and object for value. + function ChainNode(parent, key, value) { + this._parent = parent; + this._key = key; + + // _watching is true when calling get(this._parent, this._key) will + // return the value of this node. + // + // It is false for the root of a chain (because we have no parent) + // and for global paths (because the parent node is the object with + // the observer on it) + this._watching = value === undefined; + + this._value = value; + this._paths = {}; + if (this._watching) { + this._object = parent.value(); + if (this._object) { + addChainWatcher(this._object, this._key, this); + } + } + + // Special-case: the EachProxy relies on immediate evaluation to + // establish its observers. + // + // TODO: Replace this with an efficient callback that the EachProxy + // can implement. + if (this._parent && this._parent._key === "@each") { + this.value(); + } + } + + function lazyGet(obj, key) { + if (!obj) { + return; + } + + var meta = obj["__ember_meta__"]; + // check if object meant only to be a prototype + if (meta && meta.proto === obj) { + return; + } + + if (key === "@each") { + return property_get.get(obj, key); + } + + // if a CP only return cached value + var possibleDesc = obj[key]; + var desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + if (desc && desc._cacheable) { + if (meta.cache && key in meta.cache) { + return meta.cache[key]; + } else { + return; + } + } + + return property_get.get(obj, key); + } + + ChainNode.prototype = { + value: function () { + if (this._value === undefined && this._watching) { + var obj = this._parent.value(); + this._value = lazyGet(obj, this._key); + } + return this._value; + }, + + destroy: function () { + if (this._watching) { + var obj = this._object; + if (obj) { + removeChainWatcher(obj, this._key, this); + } + this._watching = false; // so future calls do nothing + } + }, + + // copies a top level object only + copy: function (obj) { + var ret = new ChainNode(null, null, obj); + var paths = this._paths; + var path; + + for (path in paths) { + // this check will also catch non-number vals. + if (paths[path] <= 0) { + continue; + } + ret.add(path); + } + return ret; + }, + + // called on the root node of a chain to setup watchers on the specified + // path. + add: function (path) { + var obj, tuple, key, src, paths; + + paths = this._paths; + paths[path] = (paths[path] || 0) + 1; + + obj = this.value(); + tuple = property_get.normalizeTuple(obj, path); + + // the path was a local path + if (tuple[0] && tuple[0] === obj) { + path = tuple[1]; + key = firstKey(path); + path = path.slice(key.length + 1); + + // global path, but object does not exist yet. + // put into a queue and try to connect later. + } else if (!tuple[0]) { + pendingQueue.push([this, path]); + tuple.length = 0; + return; + + // global path, and object already exists + } else { + src = tuple[0]; + key = path.slice(0, 0 - (tuple[1].length + 1)); + path = tuple[1]; + } + + tuple.length = 0; + this.chain(key, path, src); + }, + + // called on the root node of a chain to teardown watcher on the specified + // path + remove: function (path) { + var obj, tuple, key, src, paths; + + paths = this._paths; + if (paths[path] > 0) { + paths[path]--; + } + + obj = this.value(); + tuple = property_get.normalizeTuple(obj, path); + if (tuple[0] === obj) { + path = tuple[1]; + key = firstKey(path); + path = path.slice(key.length + 1); + } else { + src = tuple[0]; + key = path.slice(0, 0 - (tuple[1].length + 1)); + path = tuple[1]; + } + + tuple.length = 0; + this.unchain(key, path); + }, + + count: 0, + + chain: function (key, path, src) { + var chains = this._chains; + var node; + if (!chains) { + chains = this._chains = {}; + } + + node = chains[key]; + if (!node) { + node = chains[key] = new ChainNode(this, key, src); + } + node.count++; // count chains... + + // chain rest of path if there is one + if (path) { + key = firstKey(path); + path = path.slice(key.length + 1); + node.chain(key, path); // NOTE: no src means it will observe changes... + } + }, + + unchain: function (key, path) { + var chains = this._chains; + var node = chains[key]; + + // unchain rest of path first... + if (path && path.length > 1) { + var nextKey = firstKey(path); + var nextPath = path.slice(nextKey.length + 1); + node.unchain(nextKey, nextPath); + } + + // delete node if needed. + node.count--; + if (node.count <= 0) { + delete chains[node._key]; + node.destroy(); + } + }, + + willChange: function (events) { + var chains = this._chains; + if (chains) { + for (var key in chains) { + if (!chains.hasOwnProperty(key)) { + continue; + } + chains[key].willChange(events); + } + } + + if (this._parent) { + this._parent.chainWillChange(this, this._key, 1, events); + } + }, + + chainWillChange: function (chain, path, depth, events) { + if (this._key) { + path = this._key + "." + path; + } + + if (this._parent) { + this._parent.chainWillChange(this, path, depth + 1, events); + } else { + if (depth > 1) { + events.push(this.value(), path); + } + path = "this." + path; + if (this._paths[path] > 0) { + events.push(this.value(), path); + } + } + }, + + chainDidChange: function (chain, path, depth, events) { + if (this._key) { + path = this._key + "." + path; + } + + if (this._parent) { + this._parent.chainDidChange(this, path, depth + 1, events); + } else { + if (depth > 1) { + events.push(this.value(), path); + } + path = "this." + path; + if (this._paths[path] > 0) { + events.push(this.value(), path); + } + } + }, + + didChange: function (events) { + // invalidate my own value first. + if (this._watching) { + var obj = this._parent.value(); + if (obj !== this._object) { + removeChainWatcher(this._object, this._key, this); + this._object = obj; + addChainWatcher(obj, this._key, this); + } + this._value = undefined; + + // Special-case: the EachProxy relies on immediate evaluation to + // establish its observers. + if (this._parent && this._parent._key === "@each") { + this.value(); + } + } + + // then notify chains... + var chains = this._chains; + if (chains) { + for (var key in chains) { + if (!chains.hasOwnProperty(key)) { + continue; + } + chains[key].didChange(events); + } + } + + // if no events are passed in then we only care about the above wiring update + if (events === null) { + return; + } + + // and finally tell parent about my path changing... + if (this._parent) { + this._parent.chainDidChange(this, this._key, 1, events); + } + } + }; + function finishChains(obj) { + // We only create meta if we really have to + var m = obj["__ember_meta__"]; + var chains, chainWatchers, chainNodes; + + if (m) { + // finish any current chains node watchers that reference obj + chainWatchers = m.chainWatchers; + if (chainWatchers) { + for (var key in chainWatchers) { + if (!chainWatchers.hasOwnProperty(key)) { + continue; + } + + chainNodes = chainWatchers[key]; + if (chainNodes) { + for (var i = 0, l = chainNodes.length; i < l; i++) { + chainNodes[i].didChange(null); + } + } + } + } + // copy chains from prototype + chains = m.chains; + if (chains && chains.value() !== obj) { + utils.meta(obj).chains = chains = chains.copy(obj); + } + } + } + +}); +enifed('ember-metal/computed', ['exports', 'ember-metal/property_set', 'ember-metal/utils', 'ember-metal/expand_properties', 'ember-metal/error', 'ember-metal/properties', 'ember-metal/property_events', 'ember-metal/dependent_keys'], function (exports, property_set, utils, expandProperties, EmberError, properties, property_events, dependent_keys) { + + 'use strict'; + + exports.ComputedProperty = ComputedProperty; + exports.computed = computed; + exports.cacheFor = cacheFor; + + var metaFor = utils.meta; + + function UNDEFINED() {} + + // .......................................................... + // COMPUTED PROPERTY + // + + /** + A computed property transforms an object's function into a property. + + By default the function backing the computed property will only be called + once and the result will be cached. You can specify various properties + that your computed property depends on. This will force the cached + result to be recomputed if the dependencies are modified. + + In the following example we declare a computed property (by calling + `.property()` on the fullName function) and setup the property + dependencies (depending on firstName and lastName). The fullName function + will be called once (regardless of how many times it is accessed) as long + as its dependencies have not changed. Once firstName or lastName are updated + any future calls (or anything bound) to fullName will incorporate the new + values. + + ```javascript + var Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function() { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + }.property('firstName', 'lastName') + }); + + var tom = Person.create({ + firstName: 'Tom', + lastName: 'Dale' + }); + + tom.get('fullName') // 'Tom Dale' + ``` + + You can also define what Ember should do when setting a computed property. + If you try to set a computed property, it will be invoked with the key and + value you want to set it to. You can also accept the previous value as the + third parameter. + + ```javascript + var Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function(key, value, oldValue) { + // getter + if (arguments.length === 1) { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + + // setter + } else { + var name = value.split(' '); + + this.set('firstName', name[0]); + this.set('lastName', name[1]); + + return value; + } + }.property('firstName', 'lastName') + }); + + var person = Person.create(); + + person.set('fullName', 'Peter Wagenet'); + person.get('firstName'); // 'Peter' + person.get('lastName'); // 'Wagenet' + ``` + + @class ComputedProperty + @namespace Ember + @constructor + */ + function ComputedProperty(config, opts) { + this.isDescriptor = true; + + if (typeof config === "function") { + config.__ember_arity = config.length; + this._getter = config; + if (config.__ember_arity > 1) { + Ember.deprecate("Using the same function as getter and setter is deprecated.", false, { + url: "http://emberjs.com/deprecations/v1.x/#toc_computed-properties-with-a-shared-getter-and-setter" + }); + this._setter = config; + } + } else { + this._getter = config.get; + this._setter = config.set; + if (this._setter && this._setter.__ember_arity === undefined) { + this._setter.__ember_arity = this._setter.length; + } + } + + this._dependentKeys = undefined; + this._suspended = undefined; + this._meta = undefined; + + Ember.deprecate("Passing opts.cacheable to the CP constructor is deprecated. Invoke `volatile()` on the CP instead.", !opts || !opts.hasOwnProperty("cacheable")); + this._cacheable = opts && opts.cacheable !== undefined ? opts.cacheable : true; // TODO: Set always to `true` once this deprecation is gone. + this._dependentKeys = opts && opts.dependentKeys; + Ember.deprecate("Passing opts.readOnly to the CP constructor is deprecated. All CPs are writable by default. You can invoke `readOnly()` on the CP to change this.", !opts || !opts.hasOwnProperty("readOnly")); + this._readOnly = opts && (opts.readOnly !== undefined || !!opts.readOnly) || false; // TODO: Set always to `false` once this deprecation is gone. + } + + ComputedProperty.prototype = new properties.Descriptor(); + + var ComputedPropertyPrototype = ComputedProperty.prototype; + + /** + Properties are cacheable by default. Computed property will automatically + cache the return value of your function until one of the dependent keys changes. + + Call `volatile()` to set it into non-cached mode. When in this mode + the computed property will not automatically cache the return value. + + However, if a property is properly observable, there is no reason to disable + caching. + + @method cacheable + @param {Boolean} aFlag optional set to `false` to disable caching + @return {Ember.ComputedProperty} this + @chainable + @deprecated All computed properties are cacheble by default. Use `volatile()` instead to opt-out to caching. + */ + ComputedPropertyPrototype.cacheable = function (aFlag) { + Ember.deprecate("ComputedProperty.cacheable() is deprecated. All computed properties are cacheable by default."); + this._cacheable = aFlag !== false; + return this; + }; + + /** + Call on a computed property to set it into non-cached mode. When in this + mode the computed property will not automatically cache the return value. + + ```javascript + var outsideService = Ember.Object.extend({ + value: function() { + return OutsideService.getValue(); + }.property().volatile() + }).create(); + ``` + + @method volatile + @return {Ember.ComputedProperty} this + @chainable + */ + ComputedPropertyPrototype["volatile"] = function () { + this._cacheable = false; + return this; + }; + + /** + Call on a computed property to set it into read-only mode. When in this + mode the computed property will throw an error when set. + + ```javascript + var Person = Ember.Object.extend({ + guid: function() { + return 'guid-guid-guid'; + }.property().readOnly() + }); + + var person = Person.create(); + + person.set('guid', 'new-guid'); // will throw an exception + ``` + + @method readOnly + @return {Ember.ComputedProperty} this + @chainable + */ + ComputedPropertyPrototype.readOnly = function (readOnly) { + Ember.deprecate("Passing arguments to ComputedProperty.readOnly() is deprecated.", arguments.length === 0); + this._readOnly = readOnly === undefined || !!readOnly; // Force to true once this deprecation is gone + Ember.assert("Computed properties that define a setter using the new syntax cannot be read-only", !(this._readOnly && this._setter && this._setter !== this._getter)); + + return this; + }; + + /** + Sets the dependent keys on this computed property. Pass any number of + arguments containing key paths that this computed property depends on. + + ```javascript + var President = Ember.Object.extend({ + fullName: computed(function() { + return this.get('firstName') + ' ' + this.get('lastName'); + + // Tell Ember that this computed property depends on firstName + // and lastName + }).property('firstName', 'lastName') + }); + + var president = President.create({ + firstName: 'Barack', + lastName: 'Obama' + }); + + president.get('fullName'); // 'Barack Obama' + ``` + + @method property + @param {String} path* zero or more property paths + @return {Ember.ComputedProperty} this + @chainable + */ + ComputedPropertyPrototype.property = function () { + var args; + + var addArg = function (property) { + args.push(property); + }; + + args = []; + for (var i = 0, l = arguments.length; i < l; i++) { + expandProperties['default'](arguments[i], addArg); + } + + this._dependentKeys = args; + return this; + }; + + /** + In some cases, you may want to annotate computed properties with additional + metadata about how they function or what values they operate on. For example, + computed property functions may close over variables that are then no longer + available for introspection. + + You can pass a hash of these values to a computed property like this: + + ``` + person: function() { + var personId = this.get('personId'); + return App.Person.create({ id: personId }); + }.property().meta({ type: App.Person }) + ``` + + The hash that you pass to the `meta()` function will be saved on the + computed property descriptor under the `_meta` key. Ember runtime + exposes a public API for retrieving these values from classes, + via the `metaForProperty()` function. + + @method meta + @param {Hash} meta + @chainable + */ + + ComputedPropertyPrototype.meta = function (meta) { + if (arguments.length === 0) { + return this._meta || {}; + } else { + this._meta = meta; + return this; + } + }; + + /* impl descriptor API */ + ComputedPropertyPrototype.didChange = function (obj, keyName) { + // _suspended is set via a CP.set to ensure we don't clear + // the cached value set by the setter + if (this._cacheable && this._suspended !== obj) { + var meta = metaFor(obj); + if (meta.cache && meta.cache[keyName] !== undefined) { + meta.cache[keyName] = undefined; + dependent_keys.removeDependentKeys(this, obj, keyName, meta); + } + } + }; + + function finishChains(chainNodes) { + for (var i = 0, l = chainNodes.length; i < l; i++) { + chainNodes[i].didChange(null); + } + } + + /** + Access the value of the function backing the computed property. + If this property has already been cached, return the cached result. + Otherwise, call the function passing the property name as an argument. + + ```javascript + var Person = Ember.Object.extend({ + fullName: function(keyName) { + // the keyName parameter is 'fullName' in this case. + return this.get('firstName') + ' ' + this.get('lastName'); + }.property('firstName', 'lastName') + }); + + + var tom = Person.create({ + firstName: 'Tom', + lastName: 'Dale' + }); + + tom.get('fullName') // 'Tom Dale' + ``` + + @method get + @param {String} keyName The key being accessed. + @return {Object} The return value of the function backing the CP. + */ + ComputedPropertyPrototype.get = function (obj, keyName) { + var ret, cache, meta, chainNodes; + if (this._cacheable) { + meta = metaFor(obj); + cache = meta.cache; + + var result = cache && cache[keyName]; + + if (result === UNDEFINED) { + return undefined; + } else if (result !== undefined) { + return result; + } + + ret = this._getter.call(obj, keyName); + cache = meta.cache; + if (!cache) { + cache = meta.cache = {}; + } + if (ret === undefined) { + cache[keyName] = UNDEFINED; + } else { + cache[keyName] = ret; + } + + chainNodes = meta.chainWatchers && meta.chainWatchers[keyName]; + if (chainNodes) { + finishChains(chainNodes); + } + dependent_keys.addDependentKeys(this, obj, keyName, meta); + } else { + ret = this._getter.call(obj, keyName); + } + return ret; + }; + + /** + Set the value of a computed property. If the function that backs your + computed property does not accept arguments then the default action for + setting would be to define the property on the current object, and set + the value of the property to the value being set. + + Generally speaking if you intend for your computed property to be set + your backing function should accept either two or three arguments. + + ```javascript + var Person = Ember.Object.extend({ + // these will be supplied by `create` + firstName: null, + lastName: null, + + fullName: function(key, value, oldValue) { + // getter + if (arguments.length === 1) { + var firstName = this.get('firstName'); + var lastName = this.get('lastName'); + + return firstName + ' ' + lastName; + + // setter + } else { + var name = value.split(' '); + + this.set('firstName', name[0]); + this.set('lastName', name[1]); + + return value; + } + }.property('firstName', 'lastName') + }); + + var person = Person.create(); + + person.set('fullName', 'Peter Wagenet'); + person.get('firstName'); // 'Peter' + person.get('lastName'); // 'Wagenet' + ``` + + @method set + @param {String} keyName The key being accessed. + @param {Object} newValue The new value being assigned. + @param {String} oldValue The old value being replaced. + @return {Object} The return value of the function backing the CP. + */ + ComputedPropertyPrototype.set = function computedPropertySetWithSuspend(obj, keyName, value) { + var oldSuspended = this._suspended; + + this._suspended = obj; + + try { + this._set(obj, keyName, value); + } finally { + this._suspended = oldSuspended; + } + }; + + ComputedPropertyPrototype._set = function computedPropertySet(obj, keyName, value) { + var cacheable = this._cacheable; + var setter = this._setter; + var meta = metaFor(obj, cacheable); + var cache = meta.cache; + var hadCachedValue = false; + + var cachedValue, ret; + + if (this._readOnly) { + throw new EmberError['default']("Cannot set read-only property \"" + keyName + "\" on object: " + utils.inspect(obj)); + } + + if (cacheable && cache && cache[keyName] !== undefined) { + if (cache[keyName] !== UNDEFINED) { + cachedValue = cache[keyName]; + } + + hadCachedValue = true; + } + + if (!setter) { + properties.defineProperty(obj, keyName, null, cachedValue); + property_set.set(obj, keyName, value); + return; + } else if (setter.__ember_arity === 2) { + // Is there any way of deprecate this in a sensitive way? + // Maybe now that getters and setters are the prefered options we can.... + ret = setter.call(obj, keyName, value); + } else { + ret = setter.call(obj, keyName, value, cachedValue); + } + + if (hadCachedValue && cachedValue === ret) { + return; + } + + var watched = meta.watching[keyName]; + if (watched) { + property_events.propertyWillChange(obj, keyName); + } + + if (hadCachedValue) { + cache[keyName] = undefined; + } + + if (cacheable) { + if (!hadCachedValue) { + dependent_keys.addDependentKeys(this, obj, keyName, meta); + } + if (!cache) { + cache = meta.cache = {}; + } + if (ret === undefined) { + cache[keyName] = UNDEFINED; + } else { + cache[keyName] = ret; + } + } + + if (watched) { + property_events.propertyDidChange(obj, keyName); + } + + return ret; + }; + + /* called before property is overridden */ + ComputedPropertyPrototype.teardown = function (obj, keyName) { + var meta = metaFor(obj); + + if (meta.cache) { + if (keyName in meta.cache) { + dependent_keys.removeDependentKeys(this, obj, keyName, meta); + } + + if (this._cacheable) { + delete meta.cache[keyName]; + } + } + + return null; // no value to restore + }; + + /** + This helper returns a new property descriptor that wraps the passed + computed property function. You can use this helper to define properties + with mixins or via `Ember.defineProperty()`. + + The function you pass will be used to both get and set property values. + The function should accept two parameters, key and value. If value is not + undefined you should set the value first. In either case return the + current value of the property. + + A computed property defined in this way might look like this: + + ```js + var Person = Ember.Object.extend({ + firstName: 'Betty', + lastName: 'Jones', + + fullName: Ember.computed('firstName', 'lastName', function(key, value) { + return this.get('firstName') + ' ' + this.get('lastName'); + }) + }); + + var client = Person.create(); + + client.get('fullName'); // 'Betty Jones' + + client.set('lastName', 'Fuller'); + client.get('fullName'); // 'Betty Fuller' + ``` + + _Note: This is the preferred way to define computed properties when writing third-party + libraries that depend on or use Ember, since there is no guarantee that the user + will have prototype extensions enabled._ + + You might use this method if you disabled + [Prototype Extensions](http://emberjs.com/guides/configuring-ember/disabling-prototype-extensions/). + The alternative syntax might look like this + (if prototype extensions are enabled, which is the default behavior): + + ```js + fullName: function () { + return this.get('firstName') + ' ' + this.get('lastName'); + }.property('firstName', 'lastName') + ``` + + @class computed + @namespace Ember + @constructor + @static + @param {String} [dependentKeys*] Optional dependent keys that trigger this computed property. + @param {Function} func The computed property function. + @return {Ember.ComputedProperty} property descriptor instance + */ + function computed(func) { + var args; + + if (arguments.length > 1) { + args = [].slice.call(arguments); + func = args.pop(); + } + + var cp = new ComputedProperty(func); + // jscs:disable + + if (args) { + cp.property.apply(cp, args); + } + + return cp; + } + + /** + Returns the cached value for a property, if one exists. + This can be useful for peeking at the value of a computed + property that is generated lazily, without accidentally causing + it to be created. + + @method cacheFor + @for Ember + @param {Object} obj the object whose property you want to check + @param {String} key the name of the property whose cached value you want + to return + @return {Object} the cached value + */ + function cacheFor(obj, key) { + var meta = obj["__ember_meta__"]; + var cache = meta && meta.cache; + var ret = cache && cache[key]; + + if (ret === UNDEFINED) { + return undefined; + } + return ret; + } + + cacheFor.set = function (cache, key, value) { + if (value === undefined) { + cache[key] = UNDEFINED; + } else { + cache[key] = value; + } + }; + + cacheFor.get = function (cache, key) { + var ret = cache[key]; + if (ret === UNDEFINED) { + return undefined; + } + return ret; + }; + + cacheFor.remove = function (cache, key) { + cache[key] = undefined; + }; + +}); +enifed('ember-metal/computed_macros', ['exports', 'ember-metal/core', 'ember-metal/property_get', 'ember-metal/property_set', 'ember-metal/computed', 'ember-metal/is_empty', 'ember-metal/is_none', 'ember-metal/alias'], function (exports, Ember, property_get, property_set, computed, isEmpty, isNone, alias) { + + 'use strict'; + + exports.empty = empty; + exports.notEmpty = notEmpty; + exports.none = none; + exports.not = not; + exports.bool = bool; + exports.match = match; + exports.equal = equal; + exports.gt = gt; + exports.gte = gte; + exports.lt = lt; + exports.lte = lte; + exports.oneWay = oneWay; + exports.readOnly = readOnly; + exports.defaultTo = defaultTo; + exports.deprecatingAlias = deprecatingAlias; + + function getProperties(self, propertyNames) { + var ret = {}; + for (var i = 0; i < propertyNames.length; i++) { + ret[propertyNames[i]] = property_get.get(self, propertyNames[i]); + } + return ret; + } + + function generateComputedWithProperties(macro) { + return function () { + for (var _len = arguments.length, properties = Array(_len), _key = 0; _key < _len; _key++) { + properties[_key] = arguments[_key]; + } + + var computedFunc = computed.computed(function () { + return macro.apply(this, [getProperties(this, properties)]); + }); + + return computedFunc.property.apply(computedFunc, properties); + }; + } + + /** + A computed property that returns true if the value of the dependent + property is null, an empty string, empty array, or empty function. + + Example + + ```javascript + var ToDoList = Ember.Object.extend({ + isDone: Ember.computed.empty('todos') + }); + + var todoList = ToDoList.create({ + todos: ['Unit Test', 'Documentation', 'Release'] + }); + + todoList.get('isDone'); // false + todoList.get('todos').clear(); + todoList.get('isDone'); // true + ``` + + @since 1.6.0 + @method empty + @for Ember.computed + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which negate + the original value for property + */ + function empty(dependentKey) { + return computed.computed(dependentKey + ".length", function () { + return isEmpty['default'](property_get.get(this, dependentKey)); + }); + } + + /** + A computed property that returns true if the value of the dependent + property is NOT null, an empty string, empty array, or empty function. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + hasStuff: Ember.computed.notEmpty('backpack') + }); + + var hamster = Hamster.create({ backpack: ['Food', 'Sleeping Bag', 'Tent'] }); + + hamster.get('hasStuff'); // true + hamster.get('backpack').clear(); // [] + hamster.get('hasStuff'); // false + ``` + + @method notEmpty + @for Ember.computed + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which returns true if + original value for property is not empty. + */ + function notEmpty(dependentKey) { + return computed.computed(dependentKey + ".length", function () { + return !isEmpty['default'](property_get.get(this, dependentKey)); + }); + } + + /** + A computed property that returns true if the value of the dependent + property is null or undefined. This avoids errors from JSLint complaining + about use of ==, which can be technically confusing. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + isHungry: Ember.computed.none('food') + }); + + var hamster = Hamster.create(); + + hamster.get('isHungry'); // true + hamster.set('food', 'Banana'); + hamster.get('isHungry'); // false + hamster.set('food', null); + hamster.get('isHungry'); // true + ``` + + @method none + @for Ember.computed + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which + returns true if original value for property is null or undefined. + */ + function none(dependentKey) { + return computed.computed(dependentKey, function () { + return isNone['default'](property_get.get(this, dependentKey)); + }); + } + + /** + A computed property that returns the inverse boolean value + of the original value for the dependent property. + + Example + + ```javascript + var User = Ember.Object.extend({ + isAnonymous: Ember.computed.not('loggedIn') + }); + + var user = User.create({loggedIn: false}); + + user.get('isAnonymous'); // true + user.set('loggedIn', true); + user.get('isAnonymous'); // false + ``` + + @method not + @for Ember.computed + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which returns + inverse of the original value for property + */ + function not(dependentKey) { + return computed.computed(dependentKey, function () { + return !property_get.get(this, dependentKey); + }); + } + + /** + A computed property that converts the provided dependent property + into a boolean value. + + ```javascript + var Hamster = Ember.Object.extend({ + hasBananas: Ember.computed.bool('numBananas') + }); + + var hamster = Hamster.create(); + + hamster.get('hasBananas'); // false + hamster.set('numBananas', 0); + hamster.get('hasBananas'); // false + hamster.set('numBananas', 1); + hamster.get('hasBananas'); // true + hamster.set('numBananas', null); + hamster.get('hasBananas'); // false + ``` + + @method bool + @for Ember.computed + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which converts + to boolean the original value for property + */ + function bool(dependentKey) { + return computed.computed(dependentKey, function () { + return !!property_get.get(this, dependentKey); + }); + } + + /** + A computed property which matches the original value for the + dependent property against a given RegExp, returning `true` + if they values matches the RegExp and `false` if it does not. + + Example + + ```javascript + var User = Ember.Object.extend({ + hasValidEmail: Ember.computed.match('email', /^.+@.+\..+$/) + }); + + var user = User.create({loggedIn: false}); + + user.get('hasValidEmail'); // false + user.set('email', ''); + user.get('hasValidEmail'); // false + user.set('email', 'ember_hamster@example.com'); + user.get('hasValidEmail'); // true + ``` + + @method match + @for Ember.computed + @param {String} dependentKey + @param {RegExp} regexp + @return {Ember.ComputedProperty} computed property which match + the original value for property against a given RegExp + */ + function match(dependentKey, regexp) { + return computed.computed(dependentKey, function () { + var value = property_get.get(this, dependentKey); + + return typeof value === "string" ? regexp.test(value) : false; + }); + } + + /** + A computed property that returns true if the provided dependent property + is equal to the given value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + napTime: Ember.computed.equal('state', 'sleepy') + }); + + var hamster = Hamster.create(); + + hamster.get('napTime'); // false + hamster.set('state', 'sleepy'); + hamster.get('napTime'); // true + hamster.set('state', 'hungry'); + hamster.get('napTime'); // false + ``` + + @method equal + @for Ember.computed + @param {String} dependentKey + @param {String|Number|Object} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is equal to the given value. + */ + function equal(dependentKey, value) { + return computed.computed(dependentKey, function () { + return property_get.get(this, dependentKey) === value; + }); + } + + /** + A computed property that returns true if the provided dependent property + is greater than the provided value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + hasTooManyBananas: Ember.computed.gt('numBananas', 10) + }); + + var hamster = Hamster.create(); + + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 3); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 11); + hamster.get('hasTooManyBananas'); // true + ``` + + @method gt + @for Ember.computed + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is greater than given value. + */ + function gt(dependentKey, value) { + return computed.computed(dependentKey, function () { + return property_get.get(this, dependentKey) > value; + }); + } + + /** + A computed property that returns true if the provided dependent property + is greater than or equal to the provided value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + hasTooManyBananas: Ember.computed.gte('numBananas', 10) + }); + + var hamster = Hamster.create(); + + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 3); + hamster.get('hasTooManyBananas'); // false + hamster.set('numBananas', 10); + hamster.get('hasTooManyBananas'); // true + ``` + + @method gte + @for Ember.computed + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is greater or equal then given value. + */ + function gte(dependentKey, value) { + return computed.computed(dependentKey, function () { + return property_get.get(this, dependentKey) >= value; + }); + } + + /** + A computed property that returns true if the provided dependent property + is less than the provided value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + needsMoreBananas: Ember.computed.lt('numBananas', 3) + }); + + var hamster = Hamster.create(); + + hamster.get('needsMoreBananas'); // true + hamster.set('numBananas', 3); + hamster.get('needsMoreBananas'); // false + hamster.set('numBananas', 2); + hamster.get('needsMoreBananas'); // true + ``` + + @method lt + @for Ember.computed + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is less then given value. + */ + function lt(dependentKey, value) { + return computed.computed(dependentKey, function () { + return property_get.get(this, dependentKey) < value; + }); + } + + /** + A computed property that returns true if the provided dependent property + is less than or equal to the provided value. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + needsMoreBananas: Ember.computed.lte('numBananas', 3) + }); + + var hamster = Hamster.create(); + + hamster.get('needsMoreBananas'); // true + hamster.set('numBananas', 5); + hamster.get('needsMoreBananas'); // false + hamster.set('numBananas', 3); + hamster.get('needsMoreBananas'); // true + ``` + + @method lte + @for Ember.computed + @param {String} dependentKey + @param {Number} value + @return {Ember.ComputedProperty} computed property which returns true if + the original value for property is less or equal than given value. + */ + function lte(dependentKey, value) { + return computed.computed(dependentKey, function () { + return property_get.get(this, dependentKey) <= value; + }); + } + + /** + A computed property that performs a logical `and` on the + original values for the provided dependent properties. + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + readyForCamp: Ember.computed.and('hasTent', 'hasBackpack') + }); + + var hamster = Hamster.create(); + + hamster.get('readyForCamp'); // false + hamster.set('hasTent', true); + hamster.get('readyForCamp'); // false + hamster.set('hasBackpack', true); + hamster.get('readyForCamp'); // true + hamster.set('hasBackpack', 'Yes'); + hamster.get('readyForCamp'); // 'Yes' + ``` + + @method and + @for Ember.computed + @param {String} dependentKey* + @return {Ember.ComputedProperty} computed property which performs + a logical `and` on the values of all the original values for properties. + */ + var and = generateComputedWithProperties(function (properties) { + var value; + for (var key in properties) { + value = properties[key]; + if (properties.hasOwnProperty(key) && !value) { + return false; + } + } + return value; + }); + + var or = generateComputedWithProperties(function (properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && properties[key]) { + return properties[key]; + } + } + return false; + }); + + var any = generateComputedWithProperties(function (properties) { + for (var key in properties) { + if (properties.hasOwnProperty(key) && properties[key]) { + return properties[key]; + } + } + return null; + }); + + var collect = generateComputedWithProperties(function (properties) { + var res = Ember['default'].A(); + for (var key in properties) { + if (properties.hasOwnProperty(key)) { + if (isNone['default'](properties[key])) { + res.push(null); + } else { + res.push(properties[key]); + } + } + } + return res; + }); + + function oneWay(dependentKey) { + return alias['default'](dependentKey).oneWay(); + } + + /** + This is a more semantically meaningful alias of `computed.oneWay`, + whose name is somewhat ambiguous as to which direction the data flows. + + @method reads + @for Ember.computed + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates a + one way computed property to the original value for property. + */ + + /** + Where `computed.oneWay` provides oneWay bindings, `computed.readOnly` provides + a readOnly one way binding. Very often when using `computed.oneWay` one does + not also want changes to propagate back up, as they will replace the value. + + This prevents the reverse flow, and also throws an exception when it occurs. + + Example + + ```javascript + var User = Ember.Object.extend({ + firstName: null, + lastName: null, + nickName: Ember.computed.readOnly('firstName') + }); + + var teddy = User.create({ + firstName: 'Teddy', + lastName: 'Zeenny' + }); + + teddy.get('nickName'); // 'Teddy' + teddy.set('nickName', 'TeddyBear'); // throws Exception + // throw new Ember.Error('Cannot Set: nickName on: ' );` + teddy.get('firstName'); // 'Teddy' + ``` + + @method readOnly + @for Ember.computed + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates a + one way computed property to the original value for property. + @since 1.5.0 + */ + function readOnly(dependentKey) { + return alias['default'](dependentKey).readOnly(); + } + + /** + A computed property that acts like a standard getter and setter, + but returns the value at the provided `defaultPath` if the + property itself has not been set to a value + + Example + + ```javascript + var Hamster = Ember.Object.extend({ + wishList: Ember.computed.defaultTo('favoriteFood') + }); + + var hamster = Hamster.create({ favoriteFood: 'Banana' }); + + hamster.get('wishList'); // 'Banana' + hamster.set('wishList', 'More Unit Tests'); + hamster.get('wishList'); // 'More Unit Tests' + hamster.get('favoriteFood'); // 'Banana' + ``` + + @method defaultTo + @for Ember.computed + @param {String} defaultPath + @return {Ember.ComputedProperty} computed property which acts like + a standard getter and setter, but defaults to the value from `defaultPath`. + @deprecated Use `Ember.computed.oneWay` or custom CP with default instead. + */ + function defaultTo(defaultPath) { + return computed.computed({ + get: function (key) { + Ember['default'].deprecate("Usage of Ember.computed.defaultTo is deprecated, use `Ember.computed.oneWay` instead."); + return property_get.get(this, defaultPath); + }, + + set: function (key, newValue, cachedValue) { + Ember['default'].deprecate("Usage of Ember.computed.defaultTo is deprecated, use `Ember.computed.oneWay` instead."); + return newValue != null ? newValue : property_get.get(this, defaultPath); + } + }); + } + + /** + Creates a new property that is an alias for another property + on an object. Calls to `get` or `set` this property behave as + though they were called on the original property, but also + print a deprecation warning. + + @method deprecatingAlias + @for Ember.computed + @param {String} dependentKey + @return {Ember.ComputedProperty} computed property which creates an + alias with a deprecation to the original value for property. + @since 1.7.0 + */ + function deprecatingAlias(dependentKey) { + return computed.computed(dependentKey, { + get: function (key) { + Ember['default'].deprecate("Usage of `" + key + "` is deprecated, use `" + dependentKey + "` instead."); + return property_get.get(this, dependentKey); + }, + set: function (key, value) { + Ember['default'].deprecate("Usage of `" + key + "` is deprecated, use `" + dependentKey + "` instead."); + property_set.set(this, dependentKey, value); + return value; + } + }); + } + + exports.and = and; + exports.or = or; + exports.any = any; + exports.collect = collect; + +}); enifed('ember-metal/core', ['exports'], function (exports) { 'use strict'; @@ -133,7 +2957,7 @@ enifed('ember-metal/core', ['exports'], function (exports) { @class Ember @static - @version 1.11.3 + @version 1.12.1 */ if ('undefined' === typeof Ember) { @@ -145,8 +2969,8 @@ enifed('ember-metal/core', ['exports'], function (exports) { // Default imports, exports and lookup to the global object; var global = mainContext || {}; // jshint ignore:line Ember.imports = Ember.imports || global; - Ember.lookup = Ember.lookup || global; - var emExports = Ember.exports = Ember.exports || global; + Ember.lookup = Ember.lookup || global; + var emExports = Ember.exports = Ember.exports || global; // aliases needed to keep minifiers from removing the global context emExports.Em = emExports.Ember = Ember; @@ -155,16 +2979,17 @@ enifed('ember-metal/core', ['exports'], function (exports) { Ember.isNamespace = true; - Ember.toString = function() { return "Ember"; }; - + Ember.toString = function () { + return 'Ember'; + }; /** @property VERSION @type String - @default '1.11.3' + @default '1.12.1' @static */ - Ember.VERSION = '1.11.3'; + Ember.VERSION = '1.12.1'; /** Standard environmental variables. You can define these in a global `EmberENV` @@ -205,11 +3030,14 @@ enifed('ember-metal/core', ['exports'], function (exports) { @static @since 1.1.0 */ + Ember.FEATURES = { 'features-stripped-test': false, 'ember-routing-named-substates': true, 'mandatory-setter': true, 'ember-htmlbars-component-generation': false, 'ember-htmlbars-component-helper': true, 'ember-htmlbars-inline-if-helper': true, 'ember-htmlbars-attribute-syntax': true, 'ember-routing-transitioning-classes': true, 'new-computed-syntax': true, 'ember-testing-checkbox-helpers': false, 'ember-metal-stream': false, 'ember-application-instance-initializers': true, 'ember-application-initializer-context': true, 'ember-router-willtransition': true, 'ember-application-visit': false, 'ember-views-component-block-info': false, 'ember-routing-core-outlet': false, 'ember-libraries-isregistered': false }; //jshint ignore:line - Ember.FEATURES = Ember.ENV.FEATURES; - - if (!Ember.FEATURES) { - Ember.FEATURES = {"features-stripped-test":false,"ember-routing-named-substates":true,"mandatory-setter":true,"ember-htmlbars-component-generation":false,"ember-htmlbars-component-helper":true,"ember-htmlbars-inline-if-helper":true,"ember-htmlbars-attribute-syntax":true,"ember-routing-transitioning-classes":true,"new-computed-syntax":false,"ember-testing-checkbox-helpers":false,"ember-metal-stream":false,"ember-htmlbars-each-with-index":true,"ember-application-instance-initializers":false,"ember-application-initializer-context":false,"ember-router-willtransition":true,"ember-application-visit":false}; //jshint ignore:line + if (Ember.ENV.FEATURES) { + for (var feature in Ember.ENV.FEATURES) { + if (Ember.ENV.FEATURES.hasOwnProperty(feature)) { + Ember.FEATURES[feature] = Ember.ENV.FEATURES[feature]; + } + } } /** @@ -229,7 +3057,7 @@ enifed('ember-metal/core', ['exports'], function (exports) { @since 1.1.0 */ - Ember.FEATURES.isEnabled = function(feature) { + Ember.FEATURES.isEnabled = function (feature) { var featureValue = Ember.FEATURES[feature]; if (Ember.ENV.ENABLE_ALL_FEATURES) { @@ -275,7 +3103,7 @@ enifed('ember-metal/core', ['exports'], function (exports) { @type Boolean @default true */ - Ember.LOG_STACKTRACE_ON_DEPRECATION = (Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION !== false); + Ember.LOG_STACKTRACE_ON_DEPRECATION = Ember.ENV.LOG_STACKTRACE_ON_DEPRECATION !== false; /** Determines whether Ember should add ECMAScript 5 Array shims to older browsers. @@ -284,7 +3112,7 @@ enifed('ember-metal/core', ['exports'], function (exports) { @type Boolean @default Ember.EXTEND_PROTOTYPES */ - Ember.SHIM_ES5 = (Ember.ENV.SHIM_ES5 === false) ? false : Ember.EXTEND_PROTOTYPES; + Ember.SHIM_ES5 = Ember.ENV.SHIM_ES5 === false ? false : Ember.EXTEND_PROTOTYPES; /** Determines whether Ember logs info about version of used libraries @@ -293,7 +3121,7 @@ enifed('ember-metal/core', ['exports'], function (exports) { @type Boolean @default true */ - Ember.LOG_VERSION = (Ember.ENV.LOG_VERSION === false) ? false : true; + Ember.LOG_VERSION = Ember.ENV.LOG_VERSION === false ? false : true; /** Empty function. Useful for some operations. Always returns `this`. @@ -302,36 +3130,6948 @@ enifed('ember-metal/core', ['exports'], function (exports) { @private @return {Object} */ - function K() { return this; } + function K() { + return this; + } Ember.K = K; //TODO: ES6 GLOBAL TODO // Stub out the methods defined by the ember-debug package in case it's not loaded - if ('undefined' === typeof Ember.assert) { Ember.assert = K; } - if ('undefined' === typeof Ember.warn) { Ember.warn = K; } - if ('undefined' === typeof Ember.debug) { Ember.debug = K; } - if ('undefined' === typeof Ember.runInDebug) { Ember.runInDebug = K; } - if ('undefined' === typeof Ember.deprecate) { Ember.deprecate = K; } + if ('undefined' === typeof Ember.assert) { + Ember.assert = K; + } + if ('undefined' === typeof Ember.warn) { + Ember.warn = K; + } + if ('undefined' === typeof Ember.debug) { + Ember.debug = K; + } + if ('undefined' === typeof Ember.runInDebug) { + Ember.runInDebug = K; + } + if ('undefined' === typeof Ember.deprecate) { + Ember.deprecate = K; + } if ('undefined' === typeof Ember.deprecateFunc) { - Ember.deprecateFunc = function(_, func) { return func; }; + Ember.deprecateFunc = function (_, func) { + return func; + }; } exports['default'] = Ember; }); -enifed('ember-template-compiler', ['exports', 'ember-metal/core', 'ember-template-compiler/system/precompile', 'ember-template-compiler/system/compile', 'ember-template-compiler/system/template', 'ember-template-compiler/plugins', 'ember-template-compiler/plugins/transform-each-in-to-hash', 'ember-template-compiler/plugins/transform-with-as-to-hash', 'ember-template-compiler/compat'], function (exports, _Ember, precompile, compile, template, plugins, TransformEachInToHash, TransformWithAsToHash) { +enifed('ember-metal/dependent_keys', ['exports', 'ember-metal/platform/create', 'ember-metal/watching'], function (exports, o_create, watching) { + + + exports.addDependentKeys = addDependentKeys; + exports.removeDependentKeys = removeDependentKeys; + + "REMOVE_USE_STRICT: true"; /** + @module ember-metal + */ + + // .......................................................... + // DEPENDENT KEYS + // + + // data structure: + // meta.deps = { + // 'depKey': { + // 'keyName': count, + // } + // } + + /* + This function returns a map of unique dependencies for a + given object and key. + */ + function keysForDep(depsMeta, depKey) { + var keys = depsMeta[depKey]; + if (!keys) { + // if there are no dependencies yet for a the given key + // create a new empty list of dependencies for the key + keys = depsMeta[depKey] = {}; + } else if (!depsMeta.hasOwnProperty(depKey)) { + // otherwise if the dependency list is inherited from + // a superclass, clone the hash + keys = depsMeta[depKey] = o_create['default'](keys); + } + return keys; + } + + function metaForDeps(meta) { + return keysForDep(meta, "deps"); + } + function addDependentKeys(desc, obj, keyName, meta) { + // the descriptor has a list of dependent keys, so + // add all of its dependent keys. + var depsMeta, idx, len, depKey, keys; + var depKeys = desc._dependentKeys; + if (!depKeys) { + return; + } + + depsMeta = metaForDeps(meta); + + for (idx = 0, len = depKeys.length; idx < len; idx++) { + depKey = depKeys[idx]; + // Lookup keys meta for depKey + keys = keysForDep(depsMeta, depKey); + // Increment the number of times depKey depends on keyName. + keys[keyName] = (keys[keyName] || 0) + 1; + // Watch the depKey + watching.watch(obj, depKey, meta); + } + } + + function removeDependentKeys(desc, obj, keyName, meta) { + // the descriptor has a list of dependent keys, so + // remove all of its dependent keys. + var depKeys = desc._dependentKeys; + var depsMeta, idx, len, depKey, keys; + if (!depKeys) { + return; + } + + depsMeta = metaForDeps(meta); + + for (idx = 0, len = depKeys.length; idx < len; idx++) { + depKey = depKeys[idx]; + // Lookup keys meta for depKey + keys = keysForDep(depsMeta, depKey); + // Decrement the number of times depKey depends on keyName. + keys[keyName] = (keys[keyName] || 0) - 1; + // Unwatch the depKey + watching.unwatch(obj, depKey, meta); + } + } + +}); +enifed('ember-metal/deprecate_property', ['exports', 'ember-metal/core', 'ember-metal/platform/define_property', 'ember-metal/properties', 'ember-metal/property_get', 'ember-metal/property_set'], function (exports, Ember, define_property, properties, property_get, property_set) { 'use strict'; - plugins.registerPlugin('ast', TransformWithAsToHash['default']); - plugins.registerPlugin('ast', TransformEachInToHash['default']); + exports.deprecateProperty = deprecateProperty; - exports._Ember = _Ember['default']; - exports.precompile = precompile['default']; - exports.compile = compile['default']; - exports.template = template['default']; - exports.registerPlugin = plugins.registerPlugin; + function deprecateProperty(object, deprecatedKey, newKey) { + function deprecate() { + Ember['default'].deprecate("Usage of `" + deprecatedKey + "` is deprecated, use `" + newKey + "` instead."); + } + + if (define_property.hasPropertyAccessors) { + properties.defineProperty(object, deprecatedKey, { + configurable: true, + enumerable: false, + set: function (value) { + deprecate(); + property_set.set(this, newKey, value); + }, + get: function () { + deprecate(); + return property_get.get(this, newKey); + } + }); + } + } + +}); +enifed('ember-metal/dictionary', ['exports', 'ember-metal/platform/create'], function (exports, create) { + + 'use strict'; + + + exports['default'] = makeDictionary; + function makeDictionary(parent) { + var dict = create['default'](parent); + dict['_dict'] = null; + delete dict['_dict']; + return dict; + } + +}); +enifed('ember-metal/enumerable_utils', ['exports', 'ember-metal/array'], function (exports, ember_metal__array) { + + 'use strict'; + + exports.map = map; + exports.forEach = forEach; + exports.filter = filter; + exports.indexOf = indexOf; + exports.indexesOf = indexesOf; + exports.addObject = addObject; + exports.removeObject = removeObject; + exports._replace = _replace; + exports.replace = replace; + exports.intersection = intersection; + + var splice = Array.prototype.splice; + + /** + * Defines some convenience methods for working with Enumerables. + * `Ember.EnumerableUtils` uses `Ember.ArrayPolyfills` when necessary. + * + * @class EnumerableUtils + * @namespace Ember + * @static + * */ + + /** + * Calls the map function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-map method when necessary. + * + * @method map + * @param {Object} obj The object that should be mapped + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + * @return {Array} An array of mapped values. + */ + function map(obj, callback, thisArg) { + return obj.map ? obj.map(callback, thisArg) : ember_metal__array.map.call(obj, callback, thisArg); + } + + /** + * Calls the forEach function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-forEach method when necessary. + * + * @method forEach + * @param {Object} obj The object to call forEach on + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + */ + function forEach(obj, callback, thisArg) { + return obj.forEach ? obj.forEach(callback, thisArg) : ember_metal__array.forEach.call(obj, callback, thisArg); + } + + /** + * Calls the filter function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-filter method when necessary. + * + * @method filter + * @param {Object} obj The object to call filter on + * @param {Function} callback The callback to execute + * @param {Object} thisArg Value to use as this when executing *callback* + * + * @return {Array} An array containing the filtered values + * @since 1.4.0 + */ + function filter(obj, callback, thisArg) { + return obj.filter ? obj.filter(callback, thisArg) : ember_metal__array.filter.call(obj, callback, thisArg); + } + + /** + * Calls the indexOf function on the passed object with a specified callback. This + * uses `Ember.ArrayPolyfill`'s-indexOf method when necessary. + * + * @method indexOf + * @param {Object} obj The object to call indexOn on + * @param {Function} callback The callback to execute + * @param {Object} index The index to start searching from + * + */ + function indexOf(obj, element, index) { + return obj.indexOf ? obj.indexOf(element, index) : ember_metal__array.indexOf.call(obj, element, index); + } + + /** + * Returns an array of indexes of the first occurrences of the passed elements + * on the passed object. + * + * ```javascript + * var array = [1, 2, 3, 4, 5]; + * Ember.EnumerableUtils.indexesOf(array, [2, 5]); // [1, 4] + * + * var fubar = "Fubarr"; + * Ember.EnumerableUtils.indexesOf(fubar, ['b', 'r']); // [2, 4] + * ``` + * + * @method indexesOf + * @param {Object} obj The object to check for element indexes + * @param {Array} elements The elements to search for on *obj* + * + * @return {Array} An array of indexes. + * + */ + function indexesOf(obj, elements) { + return elements === undefined ? [] : map(elements, function (item) { + return indexOf(obj, item); + }); + } + + /** + * Adds an object to an array. If the array already includes the object this + * method has no effect. + * + * @method addObject + * @param {Array} array The array the passed item should be added to + * @param {Object} item The item to add to the passed array + * + * @return 'undefined' + */ + function addObject(array, item) { + var index = indexOf(array, item); + if (index === -1) { + array.push(item); + } + } + + /** + * Removes an object from an array. If the array does not contain the passed + * object this method has no effect. + * + * @method removeObject + * @param {Array} array The array to remove the item from. + * @param {Object} item The item to remove from the passed array. + * + * @return 'undefined' + */ + function removeObject(array, item) { + var index = indexOf(array, item); + if (index !== -1) { + array.splice(index, 1); + } + } + + function _replace(array, idx, amt, objects) { + var args = [].concat(objects); + var ret = []; + // https://code.google.com/p/chromium/issues/detail?id=56588 + var size = 60000; + var start = idx; + var ends = amt; + var count, chunk; + + while (args.length) { + count = ends > size ? size : ends; + if (count <= 0) { + count = 0; + } + + chunk = args.splice(0, size); + chunk = [start, count].concat(chunk); + + start += size; + ends -= count; + + ret = ret.concat(splice.apply(array, chunk)); + } + return ret; + } + + /** + * Replaces objects in an array with the passed objects. + * + * ```javascript + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 1, 2, [4, 5]); // [1, 4, 5] + * + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 1, 1, [4, 5]); // [1, 4, 5, 3] + * + * var array = [1,2,3]; + * Ember.EnumerableUtils.replace(array, 10, 1, [4, 5]); // [1, 2, 3, 4, 5] + * ``` + * + * @method replace + * @param {Array} array The array the objects should be inserted into. + * @param {Number} idx Starting index in the array to replace. If *idx* >= + * length, then append to the end of the array. + * @param {Number} amt Number of elements that should be removed from the array, + * starting at *idx* + * @param {Array} objects An array of zero or more objects that should be + * inserted into the array at *idx* + * + * @return {Array} The modified array. + */ + function replace(array, idx, amt, objects) { + if (array.replace) { + return array.replace(idx, amt, objects); + } else { + return _replace(array, idx, amt, objects); + } + } + + /** + * Calculates the intersection of two arrays. This method returns a new array + * filled with the records that the two passed arrays share with each other. + * If there is no intersection, an empty array will be returned. + * + * ```javascript + * var array1 = [1, 2, 3, 4, 5]; + * var array2 = [1, 3, 5, 6, 7]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [1, 3, 5] + * + * var array1 = [1, 2, 3]; + * var array2 = [4, 5, 6]; + * + * Ember.EnumerableUtils.intersection(array1, array2); // [] + * ``` + * + * @method intersection + * @param {Array} array1 The first array + * @param {Array} array2 The second array + * + * @return {Array} The intersection of the two passed arrays. + */ + function intersection(array1, array2) { + var result = []; + forEach(array1, function (element) { + if (indexOf(array2, element) >= 0) { + result.push(element); + } + }); + + return result; + } + + // TODO: this only exists to maintain the existing api, as we move forward it + // should only be part of the "global build" via some shim + exports['default'] = { + _replace: _replace, + addObject: addObject, + filter: filter, + forEach: forEach, + indexOf: indexOf, + indexesOf: indexesOf, + intersection: intersection, + map: map, + removeObject: removeObject, + replace: replace + }; + +}); +enifed('ember-metal/environment', ['exports', 'ember-metal/core'], function (exports, Ember) { + + 'use strict'; + + var environment; + + // This code attempts to automatically detect an environment with DOM + // by searching for window and document.createElement. An environment + // with DOM may disable the DOM functionality of Ember explicitly by + // defining a `disableBrowserEnvironment` ENV. + var hasDOM = typeof window !== 'undefined' && typeof document !== 'undefined' && typeof document.createElement !== 'undefined' && !Ember['default'].ENV.disableBrowserEnvironment; + + if (hasDOM) { + environment = { + hasDOM: true, + isChrome: !!window.chrome && !window.opera, + location: window.location, + history: window.history, + userAgent: window.navigator.userAgent, + global: window + }; + } else { + environment = { + hasDOM: false, + isChrome: false, + location: null, + history: null, + userAgent: 'Lynx (textmode)', + global: null + }; + } + + exports['default'] = environment; + +}); +enifed('ember-metal/error', ['exports', 'ember-metal/platform/create'], function (exports, create) { + + 'use strict'; + + var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; + + /** + A subclass of the JavaScript Error object for use in Ember. + + @class Error + @namespace Ember + @extends Error + @constructor + */ + function EmberError() { + var tmp = Error.apply(this, arguments); + + // Adds a `stack` property to the given error object that will yield the + // stack trace at the time captureStackTrace was called. + // When collecting the stack trace all frames above the topmost call + // to this function, including that call, will be left out of the + // stack trace. + // This is useful because we can hide Ember implementation details + // that are not very helpful for the user. + if (Error.captureStackTrace) { + Error.captureStackTrace(this, Ember.Error); + } + // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. + for (var idx = 0; idx < errorProps.length; idx++) { + this[errorProps[idx]] = tmp[errorProps[idx]]; + } + } + + EmberError.prototype = create['default'](Error.prototype); + + exports['default'] = EmberError; + +}); +enifed('ember-metal/events', ['exports', 'ember-metal/core', 'ember-metal/utils', 'ember-metal/platform/create'], function (exports, Ember, utils, create) { + + + exports.accumulateListeners = accumulateListeners; + exports.addListener = addListener; + exports.suspendListener = suspendListener; + exports.suspendListeners = suspendListeners; + exports.watchedEvents = watchedEvents; + exports.sendEvent = sendEvent; + exports.hasListeners = hasListeners; + exports.listenersFor = listenersFor; + exports.on = on; + exports.removeListener = removeListener; + + "REMOVE_USE_STRICT: true"; /* listener flags */ + var ONCE = 1; + var SUSPENDED = 2; + + /* + The event system uses a series of nested hashes to store listeners on an + object. When a listener is registered, or when an event arrives, these + hashes are consulted to determine which target and action pair to invoke. + + The hashes are stored in the object's meta hash, and look like this: + + // Object's meta hash + { + listeners: { // variable name: `listenerSet` + "foo:changed": [ // variable name: `actions` + target, method, flags + ] + } + } + + */ + + function indexOf(array, target, method) { + var index = -1; + // hashes are added to the end of the event array + // so it makes sense to start searching at the end + // of the array and search in reverse + for (var i = array.length - 3; i >= 0; i -= 3) { + if (target === array[i] && method === array[i + 1]) { + index = i; + break; + } + } + return index; + } + + function actionsFor(obj, eventName) { + var meta = utils.meta(obj, true); + var actions; + var listeners = meta.listeners; + + if (!listeners) { + listeners = meta.listeners = create['default'](null); + listeners.__source__ = obj; + } else if (listeners.__source__ !== obj) { + // setup inherited copy of the listeners object + listeners = meta.listeners = create['default'](listeners); + listeners.__source__ = obj; + } + + actions = listeners[eventName]; + + // if there are actions, but the eventName doesn't exist in our listeners, then copy them from the prototype + if (actions && actions.__source__ !== obj) { + actions = listeners[eventName] = listeners[eventName].slice(); + actions.__source__ = obj; + } else if (!actions) { + actions = listeners[eventName] = []; + actions.__source__ = obj; + } + + return actions; + } + function accumulateListeners(obj, eventName, otherActions) { + var meta = obj["__ember_meta__"]; + var actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { + return; + } + + var newActions = []; + + for (var i = actions.length - 3; i >= 0; i -= 3) { + var target = actions[i]; + var method = actions[i + 1]; + var flags = actions[i + 2]; + var actionIndex = indexOf(otherActions, target, method); + + if (actionIndex === -1) { + otherActions.push(target, method, flags); + newActions.push(target, method, flags); + } + } + + return newActions; + } + + /** + Add an event listener + + @method addListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} target A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Boolean} once A flag whether a function should only be called once + */ + function addListener(obj, eventName, target, method, once) { + Ember['default'].assert("You must pass at least an object and event name to Ember.addListener", !!obj && !!eventName); + + if (!method && "function" === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName); + var actionIndex = indexOf(actions, target, method); + var flags = 0; + + if (once) { + flags |= ONCE; + } + + if (actionIndex !== -1) { + return; + } + + actions.push(target, method, flags); + + if ("function" === typeof obj.didAddListener) { + obj.didAddListener(eventName, target, method); + } + } + + /** + Remove an event listener + + Arguments should match those passed to `Ember.addListener`. + + @method removeListener + @for Ember + @param obj + @param {String} eventName + @param {Object|Function} target A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + */ + function removeListener(obj, eventName, target, method) { + Ember['default'].assert("You must pass at least an object and event name to Ember.removeListener", !!obj && !!eventName); + + if (!method && "function" === typeof target) { + method = target; + target = null; + } + + function _removeListener(target, method) { + var actions = actionsFor(obj, eventName); + var actionIndex = indexOf(actions, target, method); + + // action doesn't exist, give up silently + if (actionIndex === -1) { + return; + } + + actions.splice(actionIndex, 3); + + if ("function" === typeof obj.didRemoveListener) { + obj.didRemoveListener(eventName, target, method); + } + } + + if (method) { + _removeListener(target, method); + } else { + var meta = obj["__ember_meta__"]; + var actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { + return; + } + for (var i = actions.length - 3; i >= 0; i -= 3) { + _removeListener(actions[i], actions[i + 1]); + } + } + } + + /** + Suspend listener during callback. + + This should only be used by the target of the event listener + when it is taking an action that would cause the event, e.g. + an object might suspend its property change listener while it is + setting that property. + + @method suspendListener + @for Ember + + @private + @param obj + @param {String} eventName + @param {Object|Function} target A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback + */ + function suspendListener(obj, eventName, target, method, callback) { + if (!method && "function" === typeof target) { + method = target; + target = null; + } + + var actions = actionsFor(obj, eventName); + var actionIndex = indexOf(actions, target, method); + + if (actionIndex !== -1) { + actions[actionIndex + 2] |= SUSPENDED; // mark the action as suspended + } + + function tryable() { + return callback.call(target); + } + function finalizer() { + if (actionIndex !== -1) { + actions[actionIndex + 2] &= ~SUSPENDED; + } + } + + return utils.tryFinally(tryable, finalizer); + } + + /** + Suspends multiple listeners during a callback. + + @method suspendListeners + @for Ember + + @private + @param obj + @param {Array} eventNames Array of event names + @param {Object|Function} target A target object or a function + @param {Function|String} method A function or the name of a function to be called on `target` + @param {Function} callback + */ + function suspendListeners(obj, eventNames, target, method, callback) { + if (!method && "function" === typeof target) { + method = target; + target = null; + } + + var suspendedActions = []; + var actionsList = []; + var eventName, actions, i, l; + + for (i = 0, l = eventNames.length; i < l; i++) { + eventName = eventNames[i]; + actions = actionsFor(obj, eventName); + var actionIndex = indexOf(actions, target, method); + + if (actionIndex !== -1) { + actions[actionIndex + 2] |= SUSPENDED; + suspendedActions.push(actionIndex); + actionsList.push(actions); + } + } + + function tryable() { + return callback.call(target); + } + + function finalizer() { + for (var i = 0, l = suspendedActions.length; i < l; i++) { + var actionIndex = suspendedActions[i]; + actionsList[i][actionIndex + 2] &= ~SUSPENDED; + } + } + + return utils.tryFinally(tryable, finalizer); + } + + /** + Return a list of currently watched events + + @private + @method watchedEvents + @for Ember + @param obj + */ + function watchedEvents(obj) { + var listeners = obj["__ember_meta__"].listeners; + var ret = []; + + if (listeners) { + for (var eventName in listeners) { + if (eventName !== "__source__" && listeners[eventName]) { + ret.push(eventName); + } + } + } + return ret; + } + + /** + Send an event. The execution of suspended listeners + is skipped, and once listeners are removed. A listener without + a target is executed on the passed object. If an array of actions + is not passed, the actions stored on the passed object are invoked. + + @method sendEvent + @for Ember + @param obj + @param {String} eventName + @param {Array} params Optional parameters for each listener. + @param {Array} actions Optional array of actions (listeners). + @return true + */ + function sendEvent(obj, eventName, params, actions) { + // first give object a chance to handle it + if (obj !== Ember['default'] && "function" === typeof obj.sendEvent) { + obj.sendEvent(eventName, params); + } + + if (!actions) { + var meta = obj["__ember_meta__"]; + actions = meta && meta.listeners && meta.listeners[eventName]; + } + + if (!actions) { + return; + } + + for (var i = actions.length - 3; i >= 0; i -= 3) { + // looping in reverse for once listeners + var target = actions[i]; + var method = actions[i + 1]; + var flags = actions[i + 2]; + + if (!method) { + continue; + } + if (flags & SUSPENDED) { + continue; + } + if (flags & ONCE) { + removeListener(obj, eventName, target, method); + } + if (!target) { + target = obj; + } + if ("string" === typeof method) { + if (params) { + utils.applyStr(target, method, params); + } else { + target[method](); + } + } else { + if (params) { + utils.apply(target, method, params); + } else { + method.call(target); + } + } + } + return true; + } + + /** + @private + @method hasListeners + @for Ember + @param obj + @param {String} eventName + */ + function hasListeners(obj, eventName) { + var meta = obj["__ember_meta__"]; + var actions = meta && meta.listeners && meta.listeners[eventName]; + + return !!(actions && actions.length); + } + + /** + @private + @method listenersFor + @for Ember + @param obj + @param {String} eventName + */ + function listenersFor(obj, eventName) { + var ret = []; + var meta = obj["__ember_meta__"]; + var actions = meta && meta.listeners && meta.listeners[eventName]; + + if (!actions) { + return ret; + } + + for (var i = 0, l = actions.length; i < l; i += 3) { + var target = actions[i]; + var method = actions[i + 1]; + ret.push([target, method]); + } + + return ret; + } + + /** + Define a property as a function that should be executed when + a specified event or events are triggered. + + + ``` javascript + var Job = Ember.Object.extend({ + logCompleted: Ember.on('completed', function() { + console.log('Job completed!'); + }) + }); + + var job = Job.create(); + + Ember.sendEvent(job, 'completed'); // Logs 'Job completed!' + ``` + + @method on + @for Ember + @param {String} eventNames* + @param {Function} func + @return func + */ + function on() { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + var func = args.pop(); + var events = args; + func.__ember_listens__ = events; + return func; + } + +}); +enifed('ember-metal/expand_properties', ['exports', 'ember-metal/error', 'ember-metal/enumerable_utils', 'ember-metal/utils'], function (exports, EmberError, enumerable_utils, utils) { + + 'use strict'; + + + exports['default'] = expandProperties; + + var SPLIT_REGEX = /\{|\}/; + + /** + Expands `pattern`, invoking `callback` for each expansion. + + The only pattern supported is brace-expansion, anything else will be passed + once to `callback` directly. + + Example + + ```js + function echo(arg){ console.log(arg); } + + Ember.expandProperties('foo.bar', echo); //=> 'foo.bar' + Ember.expandProperties('{foo,bar}', echo); //=> 'foo', 'bar' + Ember.expandProperties('foo.{bar,baz}', echo); //=> 'foo.bar', 'foo.baz' + Ember.expandProperties('{foo,bar}.baz', echo); //=> 'foo.baz', 'bar.baz' + Ember.expandProperties('foo.{bar,baz}.@each', echo) //=> 'foo.bar.@each', 'foo.baz.@each' + Ember.expandProperties('{foo,bar}.{spam,eggs}', echo) //=> 'foo.spam', 'foo.eggs', 'bar.spam', 'bar.eggs' + Ember.expandProperties('{foo}.bar.{baz}') //=> 'foo.bar.baz' + ``` + + @method + @private + @param {String} pattern The property pattern to expand. + @param {Function} callback The callback to invoke. It is invoked once per + expansion, and is passed the expansion. + */ + function expandProperties(pattern, callback) { + if (pattern.indexOf(' ') > -1) { + throw new EmberError['default']('Brace expanded properties cannot contain spaces, e.g. \'user.{firstName, lastName}\' should be \'user.{firstName,lastName}\''); + } + + if ('string' === utils.typeOf(pattern)) { + var parts = pattern.split(SPLIT_REGEX); + var properties = [parts]; + + enumerable_utils.forEach(parts, function (part, index) { + if (part.indexOf(',') >= 0) { + properties = duplicateAndReplace(properties, part.split(','), index); + } + }); + + enumerable_utils.forEach(properties, function (property) { + callback(property.join('')); + }); + } else { + callback(pattern); + } + } + + function duplicateAndReplace(properties, currentParts, index) { + var all = []; + + enumerable_utils.forEach(properties, function (property) { + enumerable_utils.forEach(currentParts, function (part) { + var current = property.slice(0); + current[index] = part; + all.push(current); + }); + }); + + return all; + } + +}); +enifed('ember-metal/get_properties', ['exports', 'ember-metal/property_get', 'ember-metal/utils'], function (exports, property_get, utils) { + + 'use strict'; + + + exports['default'] = getProperties; + function getProperties(obj) { + var ret = {}; + var propertyNames = arguments; + var i = 1; + + if (arguments.length === 2 && utils.typeOf(arguments[1]) === "array") { + i = 0; + propertyNames = arguments[1]; + } + for (var len = propertyNames.length; i < len; i++) { + ret[propertyNames[i]] = property_get.get(obj, propertyNames[i]); + } + return ret; + } + +}); +enifed('ember-metal/injected_property', ['exports', 'ember-metal/core', 'ember-metal/computed', 'ember-metal/alias', 'ember-metal/properties', 'ember-metal/platform/create'], function (exports, Ember, computed, alias, properties, create) { + + 'use strict'; + + function InjectedProperty(type, name) { + this.type = type; + this.name = name; + + this._super$Constructor(injectedPropertyGet); + AliasedPropertyPrototype.oneWay.call(this); + } + + function injectedPropertyGet(keyName) { + var possibleDesc = this[keyName]; + var desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + + Ember['default'].assert("Attempting to lookup an injected property on an object without a container, ensure that the object was instantiated via a container.", this.container); + + return this.container.lookup(desc.type + ":" + (desc.name || keyName)); + } + + InjectedProperty.prototype = create['default'](properties.Descriptor.prototype); + + var InjectedPropertyPrototype = InjectedProperty.prototype; + var ComputedPropertyPrototype = computed.ComputedProperty.prototype; + var AliasedPropertyPrototype = alias.AliasedProperty.prototype; + + InjectedPropertyPrototype._super$Constructor = computed.ComputedProperty; + + InjectedPropertyPrototype.get = ComputedPropertyPrototype.get; + InjectedPropertyPrototype.readOnly = ComputedPropertyPrototype.readOnly; + + InjectedPropertyPrototype.teardown = ComputedPropertyPrototype.teardown; + + exports['default'] = InjectedProperty; + +}); +enifed('ember-metal/instrumentation', ['exports', 'ember-metal/core', 'ember-metal/utils'], function (exports, Ember, utils) { + + 'use strict'; + + exports.instrument = instrument; + exports._instrumentStart = _instrumentStart; + exports.subscribe = subscribe; + exports.unsubscribe = unsubscribe; + exports.reset = reset; + + var subscribers = []; + var cache = {}; + + var populateListeners = function (name) { + var listeners = []; + var subscriber; + + for (var i = 0, l = subscribers.length; i < l; i++) { + subscriber = subscribers[i]; + if (subscriber.regex.test(name)) { + listeners.push(subscriber.object); + } + } + + cache[name] = listeners; + return listeners; + }; + + var time = (function () { + var perf = "undefined" !== typeof window ? window.performance || {} : {}; + var fn = perf.now || perf.mozNow || perf.webkitNow || perf.msNow || perf.oNow; + // fn.bind will be available in all the browsers that support the advanced window.performance... ;-) + return fn ? fn.bind(perf) : function () { + return +new Date(); + }; + })(); + + /** + Notifies event's subscribers, calls `before` and `after` hooks. + + @method instrument + @namespace Ember.Instrumentation + + @param {String} [name] Namespaced event name. + @param {Object} payload + @param {Function} callback Function that you're instrumenting. + @param {Object} binding Context that instrument function is called with. + */ + function instrument(name, _payload, callback, binding) { + if (arguments.length <= 3 && typeof _payload === "function") { + binding = callback; + callback = _payload; + _payload = undefined; + } + if (subscribers.length === 0) { + return callback.call(binding); + } + var payload = _payload || {}; + var finalizer = _instrumentStart(name, function () { + return payload; + }); + if (finalizer) { + var tryable = function _instrumenTryable() { + return callback.call(binding); + }; + var catchable = function _instrumentCatchable(e) { + payload.exception = e; + }; + return utils.tryCatchFinally(tryable, catchable, finalizer); + } else { + return callback.call(binding); + } + } + + // private for now + + function _instrumentStart(name, _payload) { + var listeners = cache[name]; + + if (!listeners) { + listeners = populateListeners(name); + } + + if (listeners.length === 0) { + return; + } + + var payload = _payload(); + + var STRUCTURED_PROFILE = Ember['default'].STRUCTURED_PROFILE; + var timeName; + if (STRUCTURED_PROFILE) { + timeName = name + ": " + payload.object; + console.time(timeName); + } + + var l = listeners.length; + var beforeValues = new Array(l); + var i, listener; + var timestamp = time(); + for (i = 0; i < l; i++) { + listener = listeners[i]; + beforeValues[i] = listener.before(name, timestamp, payload); + } + + return function _instrumentEnd() { + var i, l, listener; + var timestamp = time(); + for (i = 0, l = listeners.length; i < l; i++) { + listener = listeners[i]; + listener.after(name, timestamp, payload, beforeValues[i]); + } + + if (STRUCTURED_PROFILE) { + console.timeEnd(timeName); + } + }; + } + + /** + Subscribes to a particular event or instrumented block of code. + + @method subscribe + @namespace Ember.Instrumentation + + @param {String} [pattern] Namespaced event name. + @param {Object} [object] Before and After hooks. + + @return {Subscriber} + */ + function subscribe(pattern, object) { + var paths = pattern.split("."); + var path; + var regex = []; + + for (var i = 0, l = paths.length; i < l; i++) { + path = paths[i]; + if (path === "*") { + regex.push("[^\\.]*"); + } else { + regex.push(path); + } + } + + regex = regex.join("\\."); + regex = regex + "(\\..*)?"; + + var subscriber = { + pattern: pattern, + regex: new RegExp("^" + regex + "$"), + object: object + }; + + subscribers.push(subscriber); + cache = {}; + + return subscriber; + } + + /** + Unsubscribes from a particular event or instrumented block of code. + + @method unsubscribe + @namespace Ember.Instrumentation + + @param {Object} [subscriber] + */ + function unsubscribe(subscriber) { + var index; + + for (var i = 0, l = subscribers.length; i < l; i++) { + if (subscribers[i] === subscriber) { + index = i; + } + } + + subscribers.splice(index, 1); + cache = {}; + } + + /** + Resets `Ember.Instrumentation` by flushing list of subscribers. + + @method reset + @namespace Ember.Instrumentation + */ + function reset() { + subscribers.length = 0; + cache = {}; + } + + exports.subscribers = subscribers; + +}); +enifed('ember-metal/is_blank', ['exports', 'ember-metal/is_empty'], function (exports, isEmpty) { + + 'use strict'; + + + exports['default'] = isBlank; + function isBlank(obj) { + return isEmpty['default'](obj) || typeof obj === 'string' && obj.match(/\S/) === null; + } + +}); +enifed('ember-metal/is_empty', ['exports', 'ember-metal/property_get', 'ember-metal/is_none'], function (exports, property_get, isNone) { + + 'use strict'; + + function isEmpty(obj) { + var none = isNone['default'](obj); + if (none) { + return none; + } + + if (typeof obj.size === 'number') { + return !obj.size; + } + + var objectType = typeof obj; + + if (objectType === 'object') { + var size = property_get.get(obj, 'size'); + if (typeof size === 'number') { + return !size; + } + } + + if (typeof obj.length === 'number' && objectType !== 'function') { + return !obj.length; + } + + if (objectType === 'object') { + var length = property_get.get(obj, 'length'); + if (typeof length === 'number') { + return !length; + } + } + + return false; + } + + exports['default'] = isEmpty; + +}); +enifed('ember-metal/is_none', ['exports'], function (exports) { + + 'use strict'; + + /** + Returns true if the passed value is null or undefined. This avoids errors + from JSLint complaining about use of ==, which can be technically + confusing. + + ```javascript + Ember.isNone(); // true + Ember.isNone(null); // true + Ember.isNone(undefined); // true + Ember.isNone(''); // false + Ember.isNone([]); // false + Ember.isNone(function() {}); // false + ``` + + @method isNone + @for Ember + @param {Object} obj Value to test + @return {Boolean} + */ + function isNone(obj) { + return obj === null || obj === undefined; + } + + exports['default'] = isNone; + +}); +enifed('ember-metal/is_present', ['exports', 'ember-metal/is_blank'], function (exports, isBlank) { + + 'use strict'; + + + exports['default'] = isPresent; + function isPresent(obj) { + return !isBlank['default'](obj); + } + +}); +enifed('ember-metal/keys', ['exports', 'ember-metal/platform/define_property'], function (exports, define_property) { + + 'use strict'; + + var keys = Object.keys; + + if (!keys || !define_property.canDefineNonEnumerableProperties) { + // modified from + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys + keys = (function () { + var hasOwnProperty = Object.prototype.hasOwnProperty; + var hasDontEnumBug = !({ toString: null }).propertyIsEnumerable('toString'); + var dontEnums = ['toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor']; + var dontEnumsLength = dontEnums.length; + + return function keys(obj) { + if (typeof obj !== 'object' && (typeof obj !== 'function' || obj === null)) { + throw new TypeError('Object.keys called on non-object'); + } + + var result = []; + var prop, i; + + for (prop in obj) { + if (prop !== '_super' && prop.lastIndexOf('__', 0) !== 0 && hasOwnProperty.call(obj, prop)) { + result.push(prop); + } + } + + if (hasDontEnumBug) { + for (i = 0; i < dontEnumsLength; i++) { + if (hasOwnProperty.call(obj, dontEnums[i])) { + result.push(dontEnums[i]); + } + } + } + return result; + }; + })(); + } + + exports['default'] = keys; + +}); +enifed('ember-metal/libraries', ['exports', 'ember-metal/core', 'ember-metal/enumerable_utils'], function (exports, Ember, enumerable_utils) { + + 'use strict'; + + function Libraries() { + this._registry = []; + this._coreLibIndex = 0; + } + + Libraries.prototype = { + constructor: Libraries, + + _getLibraryByName: function (name) { + var libs = this._registry; + var count = libs.length; + + for (var i = 0; i < count; i++) { + if (libs[i].name === name) { + return libs[i]; + } + } + }, + + register: function (name, version, isCoreLibrary) { + var index = this._registry.length; + + if (!this._getLibraryByName(name)) { + if (isCoreLibrary) { + index = this._coreLibIndex++; + } + this._registry.splice(index, 0, { name: name, version: version }); + } else { + Ember['default'].warn("Library \"" + name + "\" is already registered with Ember."); + } + }, + + registerCoreLibrary: function (name, version) { + this.register(name, version, true); + }, + + deRegister: function (name) { + var lib = this._getLibraryByName(name); + var index; + + if (lib) { + index = enumerable_utils.indexOf(this._registry, lib); + this._registry.splice(index, 1); + } + }, + + each: function (callback) { + Ember['default'].deprecate("Using Ember.libraries.each() is deprecated. Access to a list of registered libraries is currently a private API. If you are not knowingly accessing this method, your out-of-date Ember Inspector may be doing so."); + enumerable_utils.forEach(this._registry, function (lib) { + callback(lib.name, lib.version); + }); + } + }; + + + exports['default'] = Libraries; + +}); +enifed('ember-metal/logger', ['exports', 'ember-metal/core', 'ember-metal/error'], function (exports, Ember, EmberError) { + + 'use strict'; + + function K() { + return this; + } + + function consoleMethod(name) { + var consoleObj, logToConsole; + if (Ember['default'].imports.console) { + consoleObj = Ember['default'].imports.console; + } else if (typeof console !== "undefined") { + consoleObj = console; + } + + var method = typeof consoleObj === "object" ? consoleObj[name] : null; + + if (method) { + // Older IE doesn't support bind, but Chrome needs it + if (typeof method.bind === "function") { + logToConsole = method.bind(consoleObj); + logToConsole.displayName = "console." + name; + return logToConsole; + } else if (typeof method.apply === "function") { + logToConsole = function () { + method.apply(consoleObj, arguments); + }; + logToConsole.displayName = "console." + name; + return logToConsole; + } else { + return function () { + var message = Array.prototype.join.call(arguments, ", "); + method(message); + }; + } + } + } + + function assertPolyfill(test, message) { + if (!test) { + try { + // attempt to preserve the stack + throw new EmberError['default']("assertion failed: " + message); + } catch (error) { + setTimeout(function () { + throw error; + }, 0); + } + } + } + + /** + Inside Ember-Metal, simply uses the methods from `imports.console`. + Override this to provide more robust logging functionality. + + @class Logger + @namespace Ember + */ + exports['default'] = { + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + ```javascript + var foo = 1; + Ember.Logger.log('log value of foo:', foo); + // "log value of foo: 1" will be printed to the console + ``` + @method log + @for Ember.Logger + @param {*} arguments + */ + log: consoleMethod("log") || K, + + /** + Prints the arguments to the console with a warning icon. + You can pass as many arguments as you want and they will be joined together with a space. + ```javascript + Ember.Logger.warn('Something happened!'); + // "Something happened!" will be printed to the console with a warning icon. + ``` + @method warn + @for Ember.Logger + @param {*} arguments + */ + warn: consoleMethod("warn") || K, + + /** + Prints the arguments to the console with an error icon, red text and a stack trace. + You can pass as many arguments as you want and they will be joined together with a space. + ```javascript + Ember.Logger.error('Danger! Danger!'); + // "Danger! Danger!" will be printed to the console in red text. + ``` + @method error + @for Ember.Logger + @param {*} arguments + */ + error: consoleMethod("error") || K, + + /** + Logs the arguments to the console. + You can pass as many arguments as you want and they will be joined together with a space. + ```javascript + var foo = 1; + Ember.Logger.info('log value of foo:', foo); + // "log value of foo: 1" will be printed to the console + ``` + @method info + @for Ember.Logger + @param {*} arguments + */ + info: consoleMethod("info") || K, + + /** + Logs the arguments to the console in blue text. + You can pass as many arguments as you want and they will be joined together with a space. + ```javascript + var foo = 1; + Ember.Logger.debug('log value of foo:', foo); + // "log value of foo: 1" will be printed to the console + ``` + @method debug + @for Ember.Logger + @param {*} arguments + */ + debug: consoleMethod("debug") || consoleMethod("info") || K, + + /** + If the value passed into `Ember.Logger.assert` is not truthy it will throw an error with a stack trace. + ```javascript + Ember.Logger.assert(true); // undefined + Ember.Logger.assert(true === false); // Throws an Assertion failed error. + ``` + @method assert + @for Ember.Logger + @param {Boolean} bool Value to test + */ + assert: consoleMethod("assert") || assertPolyfill + }; + +}); +enifed('ember-metal/map', ['exports', 'ember-metal/utils', 'ember-metal/array', 'ember-metal/platform/create', 'ember-metal/deprecate_property'], function (exports, utils, array, create, deprecate_property) { + + 'use strict'; + + exports.OrderedSet = OrderedSet; + exports.Map = Map; + exports.MapWithDefault = MapWithDefault; + + /** + @module ember-metal + */ + + /* + JavaScript (before ES6) does not have a Map implementation. Objects, + which are often used as dictionaries, may only have Strings as keys. + + Because Ember has a way to get a unique identifier for every object + via `Ember.guidFor`, we can implement a performant Map with arbitrary + keys. Because it is commonly used in low-level bookkeeping, Map is + implemented as a pure JavaScript object for performance. + + This implementation follows the current iteration of the ES6 proposal for + maps (http://wiki.ecmascript.org/doku.php?id=harmony:simple_maps_and_sets), + with one exception: as we do not have the luxury of in-VM iteration, we implement a + forEach method for iteration. + + Map is mocked out to look like an Ember object, so you can do + `Ember.Map.create()` for symmetry with other Ember classes. + */ + + function missingFunction(fn) { + throw new TypeError("" + Object.prototype.toString.call(fn) + " is not a function"); + } + + function missingNew(name) { + throw new TypeError("Constructor " + name + " requires 'new'"); + } + + function copyNull(obj) { + var output = create['default'](null); + + for (var prop in obj) { + // hasOwnPropery is not needed because obj is Object.create(null); + output[prop] = obj[prop]; + } + + return output; + } + + function copyMap(original, newObject) { + var keys = original._keys.copy(); + var values = copyNull(original._values); + + newObject._keys = keys; + newObject._values = values; + newObject.size = original.size; + + return newObject; + } + + /** + This class is used internally by Ember and Ember Data. + Please do not use it at this time. We plan to clean it up + and add many tests soon. + + @class OrderedSet + @namespace Ember + @constructor + @private + */ + function OrderedSet() { + + if (this instanceof OrderedSet) { + this.clear(); + this._silenceRemoveDeprecation = false; + } else { + missingNew("OrderedSet"); + } + } + + /** + @method create + @static + @return {Ember.OrderedSet} + */ + OrderedSet.create = function () { + var Constructor = this; + + return new Constructor(); + }; + + OrderedSet.prototype = { + constructor: OrderedSet, + /** + @method clear + */ + clear: function () { + this.presenceSet = create['default'](null); + this.list = []; + this.size = 0; + }, + + /** + @method add + @param obj + @param guid (optional, and for internal use) + @return {Ember.OrderedSet} + */ + add: function (obj, _guid) { + var guid = _guid || utils.guidFor(obj); + var presenceSet = this.presenceSet; + var list = this.list; + + if (presenceSet[guid] !== true) { + presenceSet[guid] = true; + this.size = list.push(obj); + } + + return this; + }, + + /** + @deprecated + @method remove + @param obj + @param _guid (optional and for internal use only) + @return {Boolean} + */ + remove: function (obj, _guid) { + Ember.deprecate("Calling `OrderedSet.prototype.remove` has been deprecated, please use `OrderedSet.prototype.delete` instead.", this._silenceRemoveDeprecation); + + return this["delete"](obj, _guid); + }, + + /** + @since 1.8.0 + @method delete + @param obj + @param _guid (optional and for internal use only) + @return {Boolean} + */ + "delete": function (obj, _guid) { + var guid = _guid || utils.guidFor(obj); + var presenceSet = this.presenceSet; + var list = this.list; + + if (presenceSet[guid] === true) { + delete presenceSet[guid]; + var index = array.indexOf.call(list, obj); + if (index > -1) { + list.splice(index, 1); + } + this.size = list.length; + return true; + } else { + return false; + } + }, + + /** + @method isEmpty + @return {Boolean} + */ + isEmpty: function () { + return this.size === 0; + }, + + /** + @method has + @param obj + @return {Boolean} + */ + has: function (obj) { + if (this.size === 0) { + return false; + } + + var guid = utils.guidFor(obj); + var presenceSet = this.presenceSet; + + return presenceSet[guid] === true; + }, + + /** + @method forEach + @param {Function} fn + @param self + */ + forEach: function (fn /*, ...thisArg*/) { + if (typeof fn !== "function") { + missingFunction(fn); + } + + if (this.size === 0) { + return; + } + + var list = this.list; + var length = arguments.length; + var i; + + if (length === 2) { + for (i = 0; i < list.length; i++) { + fn.call(arguments[1], list[i]); + } + } else { + for (i = 0; i < list.length; i++) { + fn(list[i]); + } + } + }, + + /** + @method toArray + @return {Array} + */ + toArray: function () { + return this.list.slice(); + }, + + /** + @method copy + @return {Ember.OrderedSet} + */ + copy: function () { + var Constructor = this.constructor; + var set = new Constructor(); + + set._silenceRemoveDeprecation = this._silenceRemoveDeprecation; + set.presenceSet = copyNull(this.presenceSet); + set.list = this.toArray(); + set.size = this.size; + + return set; + } + }; + + deprecate_property.deprecateProperty(OrderedSet.prototype, "length", "size"); + + /** + A Map stores values indexed by keys. Unlike JavaScript's + default Objects, the keys of a Map can be any JavaScript + object. + + Internally, a Map has two data structures: + + 1. `keys`: an OrderedSet of all of the existing keys + 2. `values`: a JavaScript Object indexed by the `Ember.guidFor(key)` + + When a key/value pair is added for the first time, we + add the key to the `keys` OrderedSet, and create or + replace an entry in `values`. When an entry is deleted, + we delete its entry in `keys` and `values`. + + @class Map + @namespace Ember + @private + @constructor + */ + function Map() { + if (this instanceof this.constructor) { + this._keys = OrderedSet.create(); + this._keys._silenceRemoveDeprecation = true; + this._values = create['default'](null); + this.size = 0; + } else { + missingNew("OrderedSet"); + } + } + + Ember.Map = Map; + + /** + @method create + @static + */ + Map.create = function () { + var Constructor = this; + return new Constructor(); + }; + + Map.prototype = { + constructor: Map, + + /** + This property will change as the number of objects in the map changes. + @since 1.8.0 + @property size + @type number + @default 0 + */ + size: 0, + + /** + Retrieve the value associated with a given key. + @method get + @param {*} key + @return {*} the value associated with the key, or `undefined` + */ + get: function (key) { + if (this.size === 0) { + return; + } + + var values = this._values; + var guid = utils.guidFor(key); + + return values[guid]; + }, + + /** + Adds a value to the map. If a value for the given key has already been + provided, the new value will replace the old value. + @method set + @param {*} key + @param {*} value + @return {Ember.Map} + */ + set: function (key, value) { + var keys = this._keys; + var values = this._values; + var guid = utils.guidFor(key); + + // ensure we don't store -0 + var k = key === -0 ? 0 : key; + + keys.add(k, guid); + + values[guid] = value; + + this.size = keys.size; + + return this; + }, + + /** + @deprecated see delete + Removes a value from the map for an associated key. + @method remove + @param {*} key + @return {Boolean} true if an item was removed, false otherwise + */ + remove: function (key) { + Ember.deprecate("Calling `Map.prototype.remove` has been deprecated, please use `Map.prototype.delete` instead."); + + return this["delete"](key); + }, + + /** + Removes a value from the map for an associated key. + @since 1.8.0 + @method delete + @param {*} key + @return {Boolean} true if an item was removed, false otherwise + */ + "delete": function (key) { + if (this.size === 0) { + return false; + } + // don't use ES6 "delete" because it will be annoying + // to use in browsers that are not ES6 friendly; + var keys = this._keys; + var values = this._values; + var guid = utils.guidFor(key); + + if (keys["delete"](key, guid)) { + delete values[guid]; + this.size = keys.size; + return true; + } else { + return false; + } + }, + + /** + Check whether a key is present. + @method has + @param {*} key + @return {Boolean} true if the item was present, false otherwise + */ + has: function (key) { + return this._keys.has(key); + }, + + /** + Iterate over all the keys and values. Calls the function once + for each key, passing in value, key, and the map being iterated over, + in that order. + The keys are guaranteed to be iterated over in insertion order. + @method forEach + @param {Function} callback + @param {*} self if passed, the `this` value inside the + callback. By default, `this` is the map. + */ + forEach: function (callback /*, ...thisArg*/) { + if (typeof callback !== "function") { + missingFunction(callback); + } + + if (this.size === 0) { + return; + } + + var length = arguments.length; + var map = this; + var cb, thisArg; + + if (length === 2) { + thisArg = arguments[1]; + cb = function (key) { + callback.call(thisArg, map.get(key), key, map); + }; + } else { + cb = function (key) { + callback(map.get(key), key, map); + }; + } + + this._keys.forEach(cb); + }, + + /** + @method clear + */ + clear: function () { + this._keys.clear(); + this._values = create['default'](null); + this.size = 0; + }, + + /** + @method copy + @return {Ember.Map} + */ + copy: function () { + return copyMap(this, new Map()); + } + }; + + deprecate_property.deprecateProperty(Map.prototype, "length", "size"); + + /** + @class MapWithDefault + @namespace Ember + @extends Ember.Map + @private + @constructor + @param [options] + @param {*} [options.defaultValue] + */ + function MapWithDefault(options) { + this._super$constructor(); + this.defaultValue = options.defaultValue; + } + + /** + @method create + @static + @param [options] + @param {*} [options.defaultValue] + @return {Ember.MapWithDefault|Ember.Map} If options are passed, returns + `Ember.MapWithDefault` otherwise returns `Ember.Map` + */ + MapWithDefault.create = function (options) { + if (options) { + return new MapWithDefault(options); + } else { + return new Map(); + } + }; + + MapWithDefault.prototype = create['default'](Map.prototype); + MapWithDefault.prototype.constructor = MapWithDefault; + MapWithDefault.prototype._super$constructor = Map; + MapWithDefault.prototype._super$get = Map.prototype.get; + + /** + Retrieve the value associated with a given key. + + @method get + @param {*} key + @return {*} the value associated with the key, or the default value + */ + MapWithDefault.prototype.get = function (key) { + var hasValue = this.has(key); + + if (hasValue) { + return this._super$get(key); + } else { + var defaultValue = this.defaultValue(key); + this.set(key, defaultValue); + return defaultValue; + } + }; + + /** + @method copy + @return {Ember.MapWithDefault} + */ + MapWithDefault.prototype.copy = function () { + var Constructor = this.constructor; + return copyMap(this, new Constructor({ + defaultValue: this.defaultValue + })); + }; + + exports['default'] = Map; + +}); +enifed('ember-metal/merge', ['exports', 'ember-metal/keys'], function (exports, keys) { + + 'use strict'; + + + exports['default'] = merge; + function merge(original, updates) { + if (!updates || typeof updates !== 'object') { + return original; + } + + var props = keys['default'](updates); + var prop; + var length = props.length; + + for (var i = 0; i < length; i++) { + prop = props[i]; + original[prop] = updates[prop]; + } + + return original; + } + +}); +enifed('ember-metal/mixin', ['exports', 'ember-metal/core', 'ember-metal/merge', 'ember-metal/array', 'ember-metal/platform/create', 'ember-metal/property_get', 'ember-metal/property_set', 'ember-metal/utils', 'ember-metal/expand_properties', 'ember-metal/properties', 'ember-metal/computed', 'ember-metal/binding', 'ember-metal/observer', 'ember-metal/events', 'ember-metal/streams/utils'], function (exports, Ember, merge, array, o_create, property_get, property_set, utils, expandProperties, ember_metal__properties, computed, ember_metal__binding, ember_metal__observer, events, streams__utils) { + + + exports.mixin = mixin; + exports.required = required; + exports.aliasMethod = aliasMethod; + exports.observer = observer; + exports.immediateObserver = immediateObserver; + exports.beforeObserver = beforeObserver; + exports.Mixin = Mixin; + + "REMOVE_USE_STRICT: true";var REQUIRED; + var a_slice = [].slice; + + function superFunction() { + var func = this.__nextSuper; + var ret; + + if (func) { + var length = arguments.length; + this.__nextSuper = null; + if (length === 0) { + ret = func.call(this); + } else if (length === 1) { + ret = func.call(this, arguments[0]); + } else if (length === 2) { + ret = func.call(this, arguments[0], arguments[1]); + } else { + ret = func.apply(this, arguments); + } + this.__nextSuper = func; + return ret; + } + } + + // ensure we prime superFunction to mitigate + // v8 bug potentially incorrectly deopts this function: https://code.google.com/p/v8/issues/detail?id=3709 + var primer = { + __nextSuper: function (a, b, c, d) {} + }; + + superFunction.call(primer); + superFunction.call(primer, 1); + superFunction.call(primer, 1, 2); + superFunction.call(primer, 1, 2, 3); + + function mixinsMeta(obj) { + var m = utils.meta(obj, true); + var ret = m.mixins; + if (!ret) { + ret = m.mixins = {}; + } else if (!m.hasOwnProperty("mixins")) { + ret = m.mixins = o_create['default'](ret); + } + return ret; + } + + function isMethod(obj) { + return "function" === typeof obj && obj.isMethod !== false && obj !== Boolean && obj !== Object && obj !== Number && obj !== Array && obj !== Date && obj !== String; + } + + var CONTINUE = {}; + + function mixinProperties(mixinsMeta, mixin) { + var guid; + + if (mixin instanceof Mixin) { + guid = utils.guidFor(mixin); + if (mixinsMeta[guid]) { + return CONTINUE; + } + mixinsMeta[guid] = mixin; + return mixin.properties; + } else { + return mixin; // apply anonymous mixin properties + } + } + + function concatenatedMixinProperties(concatProp, props, values, base) { + var concats; + + // reset before adding each new mixin to pickup concats from previous + concats = values[concatProp] || base[concatProp]; + if (props[concatProp]) { + concats = concats ? concats.concat(props[concatProp]) : props[concatProp]; + } + + return concats; + } + + function giveDescriptorSuper(meta, key, property, values, descs, base) { + var superProperty; + + // Computed properties override methods, and do not call super to them + if (values[key] === undefined) { + // Find the original descriptor in a parent mixin + superProperty = descs[key]; + } + + // If we didn't find the original descriptor in a parent mixin, find + // it on the original object. + if (!superProperty) { + var possibleDesc = base[key]; + var superDesc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + + superProperty = superDesc; + } + + if (superProperty === undefined || !(superProperty instanceof computed.ComputedProperty)) { + return property; + } + + // Since multiple mixins may inherit from the same parent, we need + // to clone the computed property so that other mixins do not receive + // the wrapped version. + property = o_create['default'](property); + property._getter = utils.wrap(property._getter, superProperty._getter); + if (superProperty._setter) { + if (property._setter) { + property._setter = utils.wrap(property._setter, superProperty._setter); + } else { + property._setter = superProperty._setter; + } + } + + return property; + } + + var sourceAvailable = (function () { + return this; + }).toString().indexOf("return this;") > -1; + + function giveMethodSuper(obj, key, method, values, descs) { + var superMethod; + + // Methods overwrite computed properties, and do not call super to them. + if (descs[key] === undefined) { + // Find the original method in a parent mixin + superMethod = values[key]; + } + + // If we didn't find the original value in a parent mixin, find it in + // the original object + superMethod = superMethod || obj[key]; + + // Only wrap the new method if the original method was a function + if (superMethod === undefined || "function" !== typeof superMethod) { + return method; + } + + var hasSuper; + if (sourceAvailable) { + hasSuper = method.__hasSuper; + + if (hasSuper === undefined) { + hasSuper = method.toString().indexOf("_super") > -1; + method.__hasSuper = hasSuper; + } + } + + if (sourceAvailable === false || hasSuper) { + return utils.wrap(method, superMethod); + } else { + return method; + } + } + + function applyConcatenatedProperties(obj, key, value, values) { + var baseValue = values[key] || obj[key]; + + if (baseValue) { + if ("function" === typeof baseValue.concat) { + if (value === null || value === undefined) { + return baseValue; + } else { + return baseValue.concat(value); + } + } else { + return utils.makeArray(baseValue).concat(value); + } + } else { + return utils.makeArray(value); + } + } + + function applyMergedProperties(obj, key, value, values) { + var baseValue = values[key] || obj[key]; + + Ember['default'].assert("You passed in `" + JSON.stringify(value) + "` as the value for `" + key + "` but `" + key + "` cannot be an Array", !utils.isArray(value)); + + if (!baseValue) { + return value; + } + + var newBase = merge['default']({}, baseValue); + var hasFunction = false; + + for (var prop in value) { + if (!value.hasOwnProperty(prop)) { + continue; + } + + var propValue = value[prop]; + if (isMethod(propValue)) { + // TODO: support for Computed Properties, etc? + hasFunction = true; + newBase[prop] = giveMethodSuper(obj, prop, propValue, baseValue, {}); + } else { + newBase[prop] = propValue; + } + } + + if (hasFunction) { + newBase._super = superFunction; + } + + return newBase; + } + + function addNormalizedProperty(base, key, value, meta, descs, values, concats, mergings) { + if (value instanceof ember_metal__properties.Descriptor) { + if (value === REQUIRED && descs[key]) { + return CONTINUE; + } + + // Wrap descriptor function to implement + // __nextSuper() if needed + if (value._getter) { + value = giveDescriptorSuper(meta, key, value, values, descs, base); + } + + descs[key] = value; + values[key] = undefined; + } else { + if (concats && array.indexOf.call(concats, key) >= 0 || key === "concatenatedProperties" || key === "mergedProperties") { + value = applyConcatenatedProperties(base, key, value, values); + } else if (mergings && array.indexOf.call(mergings, key) >= 0) { + value = applyMergedProperties(base, key, value, values); + } else if (isMethod(value)) { + value = giveMethodSuper(base, key, value, values, descs); + } + + descs[key] = undefined; + values[key] = value; + } + } + + function mergeMixins(mixins, m, descs, values, base, keys) { + var currentMixin, props, key, concats, mergings, meta; + + function removeKeys(keyName) { + delete descs[keyName]; + delete values[keyName]; + } + + for (var i = 0, l = mixins.length; i < l; i++) { + currentMixin = mixins[i]; + Ember['default'].assert("Expected hash or Mixin instance, got " + Object.prototype.toString.call(currentMixin), typeof currentMixin === "object" && currentMixin !== null && Object.prototype.toString.call(currentMixin) !== "[object Array]"); + + props = mixinProperties(m, currentMixin); + if (props === CONTINUE) { + continue; + } + + if (props) { + meta = utils.meta(base); + if (base.willMergeMixin) { + base.willMergeMixin(props); + } + concats = concatenatedMixinProperties("concatenatedProperties", props, values, base); + mergings = concatenatedMixinProperties("mergedProperties", props, values, base); + + for (key in props) { + if (!props.hasOwnProperty(key)) { + continue; + } + keys.push(key); + addNormalizedProperty(base, key, props[key], meta, descs, values, concats, mergings); + } + + // manually copy toString() because some JS engines do not enumerate it + if (props.hasOwnProperty("toString")) { + base.toString = props.toString; + } + } else if (currentMixin.mixins) { + mergeMixins(currentMixin.mixins, m, descs, values, base, keys); + if (currentMixin._without) { + array.forEach.call(currentMixin._without, removeKeys); + } + } + } + } + + var IS_BINDING = /^.+Binding$/; + + function detectBinding(obj, key, value, m) { + if (IS_BINDING.test(key)) { + var bindings = m.bindings; + if (!bindings) { + bindings = m.bindings = {}; + } else if (!m.hasOwnProperty("bindings")) { + bindings = m.bindings = o_create['default'](m.bindings); + } + bindings[key] = value; + } + } + + function connectStreamBinding(obj, key, stream) { + var onNotify = function (stream) { + ember_metal__observer._suspendObserver(obj, key, null, didChange, function () { + property_set.trySet(obj, key, stream.value()); + }); + }; + + var didChange = function () { + stream.setValue(property_get.get(obj, key), onNotify); + }; + + // Initialize value + property_set.set(obj, key, stream.value()); + + ember_metal__observer.addObserver(obj, key, null, didChange); + + stream.subscribe(onNotify); + + if (obj._streamBindingSubscriptions === undefined) { + obj._streamBindingSubscriptions = o_create['default'](null); + } + + obj._streamBindingSubscriptions[key] = onNotify; + } + + function connectBindings(obj, m) { + // TODO Mixin.apply(instance) should disconnect binding if exists + var bindings = m.bindings; + var key, binding, to; + if (bindings) { + for (key in bindings) { + binding = bindings[key]; + if (binding) { + to = key.slice(0, -7); // strip Binding off end + if (streams__utils.isStream(binding)) { + connectStreamBinding(obj, to, binding); + continue; + } else if (binding instanceof ember_metal__binding.Binding) { + binding = binding.copy(); // copy prototypes' instance + binding.to(to); + } else { + // binding is string path + binding = new ember_metal__binding.Binding(to, binding); + } + binding.connect(obj); + obj[key] = binding; + } + } + // mark as applied + m.bindings = {}; + } + } + + function finishPartial(obj, m) { + connectBindings(obj, m || utils.meta(obj)); + return obj; + } + + function followAlias(obj, desc, m, descs, values) { + var altKey = desc.methodName; + var value; + var possibleDesc; + if (descs[altKey] || values[altKey]) { + value = values[altKey]; + desc = descs[altKey]; + } else if ((possibleDesc = obj[altKey]) && possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor) { + desc = possibleDesc; + value = undefined; + } else { + desc = undefined; + value = obj[altKey]; + } + + return { desc: desc, value: value }; + } + + function updateObserversAndListeners(obj, key, observerOrListener, pathsKey, updateMethod) { + var paths = observerOrListener[pathsKey]; + + if (paths) { + for (var i = 0, l = paths.length; i < l; i++) { + updateMethod(obj, paths[i], null, key); + } + } + } + + function replaceObserversAndListeners(obj, key, observerOrListener) { + var prev = obj[key]; + + if ("function" === typeof prev) { + updateObserversAndListeners(obj, key, prev, "__ember_observesBefore__", ember_metal__observer.removeBeforeObserver); + updateObserversAndListeners(obj, key, prev, "__ember_observes__", ember_metal__observer.removeObserver); + updateObserversAndListeners(obj, key, prev, "__ember_listens__", events.removeListener); + } + + if ("function" === typeof observerOrListener) { + updateObserversAndListeners(obj, key, observerOrListener, "__ember_observesBefore__", ember_metal__observer.addBeforeObserver); + updateObserversAndListeners(obj, key, observerOrListener, "__ember_observes__", ember_metal__observer.addObserver); + updateObserversAndListeners(obj, key, observerOrListener, "__ember_listens__", events.addListener); + } + } + + function applyMixin(obj, mixins, partial) { + var descs = {}; + var values = {}; + var m = utils.meta(obj); + var keys = []; + var key, value, desc; + + obj._super = superFunction; + + // Go through all mixins and hashes passed in, and: + // + // * Handle concatenated properties + // * Handle merged properties + // * Set up _super wrapping if necessary + // * Set up computed property descriptors + // * Copying `toString` in broken browsers + mergeMixins(mixins, mixinsMeta(obj), descs, values, obj, keys); + + for (var i = 0, l = keys.length; i < l; i++) { + key = keys[i]; + if (key === "constructor" || !values.hasOwnProperty(key)) { + continue; + } + + desc = descs[key]; + value = values[key]; + + if (desc === REQUIRED) { + continue; + } + + while (desc && desc instanceof Alias) { + var followed = followAlias(obj, desc, m, descs, values); + desc = followed.desc; + value = followed.value; + } + + if (desc === undefined && value === undefined) { + continue; + } + + replaceObserversAndListeners(obj, key, value); + detectBinding(obj, key, value, m); + ember_metal__properties.defineProperty(obj, key, desc, value, m); + } + + if (!partial) { + // don't apply to prototype + finishPartial(obj, m); + } + + return obj; + } + + /** + @method mixin + @for Ember + @param obj + @param mixins* + @return obj + */ + function mixin(obj) { + for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { + args[_key - 1] = arguments[_key]; + } + + applyMixin(obj, args, false); + return obj; + } + + /** + The `Ember.Mixin` class allows you to create mixins, whose properties can be + added to other classes. For instance, + + ```javascript + App.Editable = Ember.Mixin.create({ + edit: function() { + console.log('starting to edit'); + this.set('isEditing', true); + }, + isEditing: false + }); + + // Mix mixins into classes by passing them as the first arguments to + // .extend. + App.CommentView = Ember.View.extend(App.Editable, { + template: Ember.Handlebars.compile('{{#if view.isEditing}}...{{else}}...{{/if}}') + }); + + commentView = App.CommentView.create(); + commentView.edit(); // outputs 'starting to edit' + ``` + + Note that Mixins are created with `Ember.Mixin.create`, not + `Ember.Mixin.extend`. + + Note that mixins extend a constructor's prototype so arrays and object literals + defined as properties will be shared amongst objects that implement the mixin. + If you want to define a property in a mixin that is not shared, you can define + it either as a computed property or have it be created on initialization of the object. + + ```javascript + //filters array will be shared amongst any object implementing mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.A() + }); + + //filters will be a separate array for every object implementing the mixin + App.Filterable = Ember.Mixin.create({ + filters: Ember.computed(function() {return Ember.A();}) + }); + + //filters will be created as a separate array during the object's initialization + App.Filterable = Ember.Mixin.create({ + init: function() { + this._super.apply(this, arguments); + this.set("filters", Ember.A()); + } + }); + ``` + + @class Mixin + @namespace Ember + */ + exports['default'] = Mixin; + function Mixin(args, properties) { + this.properties = properties; + + var length = args && args.length; + + if (length > 0) { + var m = new Array(length); + + for (var i = 0; i < length; i++) { + var x = args[i]; + if (x instanceof Mixin) { + m[i] = x; + } else { + m[i] = new Mixin(undefined, x); + } + } + + this.mixins = m; + } else { + this.mixins = undefined; + } + this.ownerConstructor = undefined; + } + + Mixin._apply = applyMixin; + + Mixin.applyPartial = function (obj) { + var args = a_slice.call(arguments, 1); + return applyMixin(obj, args, true); + }; + + Mixin.finishPartial = finishPartial; + + // ES6TODO: this relies on a global state? + Ember['default'].anyUnprocessedMixins = false; + + /** + @method create + @static + @param arguments* + */ + Mixin.create = function () { + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + // ES6TODO: this relies on a global state? + Ember['default'].anyUnprocessedMixins = true; + var M = this; + return new M(args, undefined); + }; + + var MixinPrototype = Mixin.prototype; + + /** + @method reopen + @param arguments* + */ + MixinPrototype.reopen = function () { + var currentMixin; + + if (this.properties) { + currentMixin = new Mixin(undefined, this.properties); + this.properties = undefined; + this.mixins = [currentMixin]; + } else if (!this.mixins) { + this.mixins = []; + } + + var len = arguments.length; + var mixins = this.mixins; + var idx; + + for (idx = 0; idx < len; idx++) { + currentMixin = arguments[idx]; + Ember['default'].assert("Expected hash or Mixin instance, got " + Object.prototype.toString.call(currentMixin), typeof currentMixin === "object" && currentMixin !== null && Object.prototype.toString.call(currentMixin) !== "[object Array]"); + + if (currentMixin instanceof Mixin) { + mixins.push(currentMixin); + } else { + mixins.push(new Mixin(undefined, currentMixin)); + } + } + + return this; + }; + + /** + @method apply + @param obj + @return applied object + */ + MixinPrototype.apply = function (obj) { + return applyMixin(obj, [this], false); + }; + + MixinPrototype.applyPartial = function (obj) { + return applyMixin(obj, [this], true); + }; + + function _detect(curMixin, targetMixin, seen) { + var guid = utils.guidFor(curMixin); + + if (seen[guid]) { + return false; + } + seen[guid] = true; + + if (curMixin === targetMixin) { + return true; + } + var mixins = curMixin.mixins; + var loc = mixins ? mixins.length : 0; + while (--loc >= 0) { + if (_detect(mixins[loc], targetMixin, seen)) { + return true; + } + } + return false; + } + + /** + @method detect + @param obj + @return {Boolean} + */ + MixinPrototype.detect = function (obj) { + if (!obj) { + return false; + } + if (obj instanceof Mixin) { + return _detect(obj, this, {}); + } + var m = obj["__ember_meta__"]; + var mixins = m && m.mixins; + if (mixins) { + return !!mixins[utils.guidFor(this)]; + } + return false; + }; + + MixinPrototype.without = function () { + for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + + var ret = new Mixin([this]); + ret._without = args; + return ret; + }; + + function _keys(ret, mixin, seen) { + if (seen[utils.guidFor(mixin)]) { + return; + } + seen[utils.guidFor(mixin)] = true; + + if (mixin.properties) { + var props = mixin.properties; + for (var key in props) { + if (props.hasOwnProperty(key)) { + ret[key] = true; + } + } + } else if (mixin.mixins) { + array.forEach.call(mixin.mixins, function (x) { + _keys(ret, x, seen); + }); + } + } + + MixinPrototype.keys = function () { + var keys = {}; + var seen = {}; + var ret = []; + _keys(keys, this, seen); + for (var key in keys) { + if (keys.hasOwnProperty(key)) { + ret.push(key); + } + } + return ret; + }; + + // returns the mixins currently applied to the specified object + // TODO: Make Ember.mixin + Mixin.mixins = function (obj) { + var m = obj["__ember_meta__"]; + var mixins = m && m.mixins; + var ret = []; + + if (!mixins) { + return ret; + } + + for (var key in mixins) { + var currentMixin = mixins[key]; + + // skip primitive mixins since these are always anonymous + if (!currentMixin.properties) { + ret.push(currentMixin); + } + } + + return ret; + }; + + REQUIRED = new ember_metal__properties.Descriptor(); + REQUIRED.toString = function () { + return "(Required Property)"; + }; + + /** + Denotes a required property for a mixin + + @method required + @for Ember + */ + function required() { + Ember['default'].deprecate("Ember.required is deprecated as its behavior is inconsistent and unreliable.", false); + return REQUIRED; + } + + function Alias(methodName) { + this.isDescriptor = true; + this.methodName = methodName; + } + + Alias.prototype = new ember_metal__properties.Descriptor(); + + /** + Makes a method available via an additional name. + + ```javascript + App.Person = Ember.Object.extend({ + name: function() { + return 'Tomhuda Katzdale'; + }, + moniker: Ember.aliasMethod('name') + }); + + var goodGuy = App.Person.create(); + + goodGuy.name(); // 'Tomhuda Katzdale' + goodGuy.moniker(); // 'Tomhuda Katzdale' + ``` + + @method aliasMethod + @for Ember + @param {String} methodName name of the method to alias + */ + function aliasMethod(methodName) { + return new Alias(methodName); + } + + // .......................................................... + // OBSERVER HELPER + // + + /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.observer('value', function() { + // Executes whenever the "value" property changes + }) + }); + ``` + + In the future this method may become asynchronous. If you want to ensure + synchronous behavior, use `immediateObserver`. + + Also available as `Function.prototype.observes` if prototype extensions are + enabled. + + @method observer + @for Ember + @param {String} propertyNames* + @param {Function} func + @return func + */ + function observer() { + for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + args[_key4] = arguments[_key4]; + } + + var func = args.slice(-1)[0]; + var paths; + + var addWatchedProperty = function (path) { + paths.push(path); + }; + var _paths = args.slice(0, -1); + + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = args[0]; + _paths = args.slice(1); + } + + paths = []; + + for (var i = 0; i < _paths.length; ++i) { + expandProperties['default'](_paths[i], addWatchedProperty); + } + + if (typeof func !== "function") { + throw new Ember['default'].Error("Ember.observer called without a function"); + } + + func.__ember_observes__ = paths; + return func; + } + + /** + Specify a method that observes property changes. + + ```javascript + Ember.Object.extend({ + valueObserver: Ember.immediateObserver('value', function() { + // Executes whenever the "value" property changes + }) + }); + ``` + + In the future, `Ember.observer` may become asynchronous. In this event, + `Ember.immediateObserver` will maintain the synchronous behavior. + + Also available as `Function.prototype.observesImmediately` if prototype extensions are + enabled. + + @method immediateObserver + @for Ember + @param {String} propertyNames* + @param {Function} func + @return func + */ + function immediateObserver() { + for (var i = 0, l = arguments.length; i < l; i++) { + var arg = arguments[i]; + Ember['default'].assert("Immediate observers must observe internal properties only, not properties on other objects.", typeof arg !== "string" || arg.indexOf(".") === -1); + } + + return observer.apply(this, arguments); + } + + /** + When observers fire, they are called with the arguments `obj`, `keyName`. + + Note, `@each.property` observer is called per each add or replace of an element + and it's not called with a specific enumeration item. + + A `beforeObserver` fires before a property changes. + + A `beforeObserver` is an alternative form of `.observesBefore()`. + + ```javascript + App.PersonView = Ember.View.extend({ + friends: [{ name: 'Tom' }, { name: 'Stefan' }, { name: 'Kris' }], + + valueWillChange: Ember.beforeObserver('content.value', function(obj, keyName) { + this.changingFrom = obj.get(keyName); + }), + + valueDidChange: Ember.observer('content.value', function(obj, keyName) { + // only run if updating a value already in the DOM + if (this.get('state') === 'inDOM') { + var color = obj.get(keyName) > this.changingFrom ? 'green' : 'red'; + // logic + } + }), + + friendsDidChange: Ember.observer('friends.@each.name', function(obj, keyName) { + // some logic + // obj.get(keyName) returns friends array + }) + }); + ``` + + Also available as `Function.prototype.observesBefore` if prototype extensions are + enabled. + + @method beforeObserver + @for Ember + @param {String} propertyNames* + @param {Function} func + @return func + */ + function beforeObserver() { + for (var _len5 = arguments.length, args = Array(_len5), _key5 = 0; _key5 < _len5; _key5++) { + args[_key5] = arguments[_key5]; + } + + var func = args.slice(-1)[0]; + var paths; + + var addWatchedProperty = function (path) { + paths.push(path); + }; + + var _paths = args.slice(0, -1); + + if (typeof func !== "function") { + // revert to old, soft-deprecated argument ordering + + func = args[0]; + _paths = args.slice(1); + } + + paths = []; + + for (var i = 0; i < _paths.length; ++i) { + expandProperties['default'](_paths[i], addWatchedProperty); + } + + if (typeof func !== "function") { + throw new Ember['default'].Error("Ember.beforeObserver called without a function"); + } + + func.__ember_observesBefore__ = paths; + return func; + } + + exports.IS_BINDING = IS_BINDING; + exports.REQUIRED = REQUIRED; + +}); +enifed('ember-metal/observer', ['exports', 'ember-metal/watching', 'ember-metal/array', 'ember-metal/events'], function (exports, watching, array, ember_metal__events) { + + 'use strict'; + + exports.addObserver = addObserver; + exports.observersFor = observersFor; + exports.removeObserver = removeObserver; + exports.addBeforeObserver = addBeforeObserver; + exports._suspendBeforeObserver = _suspendBeforeObserver; + exports._suspendObserver = _suspendObserver; + exports._suspendBeforeObservers = _suspendBeforeObservers; + exports._suspendObservers = _suspendObservers; + exports.beforeObserversFor = beforeObserversFor; + exports.removeBeforeObserver = removeBeforeObserver; + + var AFTER_OBSERVERS = ":change"; + var BEFORE_OBSERVERS = ":before"; + + function changeEvent(keyName) { + return keyName + AFTER_OBSERVERS; + } + + function beforeEvent(keyName) { + return keyName + BEFORE_OBSERVERS; + } + + /** + @method addObserver + @for Ember + @param obj + @param {String} path + @param {Object|Function} targetOrMethod + @param {Function|String} [method] + */ + function addObserver(obj, _path, target, method) { + ember_metal__events.addListener(obj, changeEvent(_path), target, method); + watching.watch(obj, _path); + + return this; + } + + function observersFor(obj, path) { + return ember_metal__events.listenersFor(obj, changeEvent(path)); + } + + /** + @method removeObserver + @for Ember + @param obj + @param {String} path + @param {Object|Function} target + @param {Function|String} [method] + */ + function removeObserver(obj, path, target, method) { + watching.unwatch(obj, path); + ember_metal__events.removeListener(obj, changeEvent(path), target, method); + + return this; + } + + /** + @method addBeforeObserver + @for Ember + @param obj + @param {String} path + @param {Object|Function} target + @param {Function|String} [method] + */ + function addBeforeObserver(obj, path, target, method) { + ember_metal__events.addListener(obj, beforeEvent(path), target, method); + watching.watch(obj, path); + + return this; + } + + // Suspend observer during callback. + // + // This should only be used by the target of the observer + // while it is setting the observed path. + + function _suspendBeforeObserver(obj, path, target, method, callback) { + return ember_metal__events.suspendListener(obj, beforeEvent(path), target, method, callback); + } + + function _suspendObserver(obj, path, target, method, callback) { + return ember_metal__events.suspendListener(obj, changeEvent(path), target, method, callback); + } + + function _suspendBeforeObservers(obj, paths, target, method, callback) { + var events = array.map.call(paths, beforeEvent); + return ember_metal__events.suspendListeners(obj, events, target, method, callback); + } + + function _suspendObservers(obj, paths, target, method, callback) { + var events = array.map.call(paths, changeEvent); + return ember_metal__events.suspendListeners(obj, events, target, method, callback); + } + + function beforeObserversFor(obj, path) { + return ember_metal__events.listenersFor(obj, beforeEvent(path)); + } + + /** + @method removeBeforeObserver + @for Ember + @param obj + @param {String} path + @param {Object|Function} target + @param {Function|String} [method] + */ + function removeBeforeObserver(obj, path, target, method) { + watching.unwatch(obj, path); + ember_metal__events.removeListener(obj, beforeEvent(path), target, method); + + return this; + } + +}); +enifed('ember-metal/observer_set', ['exports', 'ember-metal/utils', 'ember-metal/events'], function (exports, utils, events) { + + 'use strict'; + + exports['default'] = ObserverSet; + function ObserverSet() { + this.clear(); + } + + ObserverSet.prototype.add = function (sender, keyName, eventName) { + var observerSet = this.observerSet; + var observers = this.observers; + var senderGuid = utils.guidFor(sender); + var keySet = observerSet[senderGuid]; + var index; + + if (!keySet) { + observerSet[senderGuid] = keySet = {}; + } + index = keySet[keyName]; + if (index === undefined) { + index = observers.push({ + sender: sender, + keyName: keyName, + eventName: eventName, + listeners: [] + }) - 1; + keySet[keyName] = index; + } + return observers[index].listeners; + }; + + ObserverSet.prototype.flush = function () { + var observers = this.observers; + var i, len, observer, sender; + this.clear(); + for (i = 0, len = observers.length; i < len; ++i) { + observer = observers[i]; + sender = observer.sender; + if (sender.isDestroying || sender.isDestroyed) { + continue; + } + events.sendEvent(sender, observer.eventName, [sender, observer.keyName], observer.listeners); + } + }; + + ObserverSet.prototype.clear = function () { + this.observerSet = {}; + this.observers = []; + }; + +}); +enifed('ember-metal/path_cache', ['exports', 'ember-metal/cache'], function (exports, Cache) { + + 'use strict'; + + exports.isGlobal = isGlobal; + exports.isGlobalPath = isGlobalPath; + exports.hasThis = hasThis; + exports.isPath = isPath; + exports.getFirstKey = getFirstKey; + exports.getTailPath = getTailPath; + + var IS_GLOBAL = /^[A-Z$]/; + var IS_GLOBAL_PATH = /^[A-Z$].*[\.]/; + var HAS_THIS = 'this.'; + + var isGlobalCache = new Cache['default'](1000, function (key) { + return IS_GLOBAL.test(key); + }); + + var isGlobalPathCache = new Cache['default'](1000, function (key) { + return IS_GLOBAL_PATH.test(key); + }); + + var hasThisCache = new Cache['default'](1000, function (key) { + return key.lastIndexOf(HAS_THIS, 0) === 0; + }); + + var firstDotIndexCache = new Cache['default'](1000, function (key) { + return key.indexOf('.'); + }); + + var firstKeyCache = new Cache['default'](1000, function (path) { + var index = firstDotIndexCache.get(path); + if (index === -1) { + return path; + } else { + return path.slice(0, index); + } + }); + + var tailPathCache = new Cache['default'](1000, function (path) { + var index = firstDotIndexCache.get(path); + if (index !== -1) { + return path.slice(index + 1); + } + }); + + var caches = { + isGlobalCache: isGlobalCache, + isGlobalPathCache: isGlobalPathCache, + hasThisCache: hasThisCache, + firstDotIndexCache: firstDotIndexCache, + firstKeyCache: firstKeyCache, + tailPathCache: tailPathCache + };function isGlobal(path) { + return isGlobalCache.get(path); + } + + function isGlobalPath(path) { + return isGlobalPathCache.get(path); + } + + function hasThis(path) { + return hasThisCache.get(path); + } + + function isPath(path) { + return firstDotIndexCache.get(path) !== -1; + } + + function getFirstKey(path) { + return firstKeyCache.get(path); + } + + function getTailPath(path) { + return tailPathCache.get(path); + } + + exports.caches = caches; + +}); +enifed('ember-metal/platform/create', ['exports', 'ember-metal/platform/define_properties'], function (exports, defineProperties) { + + + + + 'REMOVE_USE_STRICT: true'; /** + @class platform + @namespace Ember + @static + */ + + /** + Identical to `Object.create()`. Implements if not available natively. + + @since 1.8.0 + @method create + @for Ember + */ + var create; + // ES5 15.2.3.5 + // http://es5.github.com/#x15.2.3.5 + if (!(Object.create && !Object.create(null).hasOwnProperty)) { + /* jshint scripturl:true, proto:true */ + // Contributed by Brandon Benvie, October, 2012 + var createEmpty; + var supportsProto = !({ '__proto__': null } instanceof Object); + // the following produces false positives + // in Opera Mini => not a reliable check + // Object.prototype.__proto__ === null + if (supportsProto || typeof document === 'undefined') { + createEmpty = function () { + return { '__proto__': null }; + }; + } else { + // In old IE __proto__ can't be used to manually set `null`, nor does + // any other method exist to make an object that inherits from nothing, + // aside from Object.prototype itself. Instead, create a new global + // object and *steal* its Object.prototype and strip it bare. This is + // used as the prototype to create nullary objects. + createEmpty = function () { + var iframe = document.createElement('iframe'); + var parent = document.body || document.documentElement; + iframe.style.display = 'none'; + parent.appendChild(iframe); + iframe.src = 'javascript:'; + var empty = iframe.contentWindow.Object.prototype; + parent.removeChild(iframe); + iframe = null; + delete empty.constructor; + delete empty.hasOwnProperty; + delete empty.propertyIsEnumerable; + delete empty.isPrototypeOf; + delete empty.toLocaleString; + delete empty.toString; + delete empty.valueOf; + + function Empty() {} + Empty.prototype = empty; + // short-circuit future calls + createEmpty = function () { + return new Empty(); + }; + return new Empty(); + }; + } + + create = Object.create = function create(prototype, properties) { + + var object; + function Type() {} // An empty constructor. + + if (prototype === null) { + object = createEmpty(); + } else { + if (typeof prototype !== 'object' && typeof prototype !== 'function') { + // In the native implementation `parent` can be `null` + // OR *any* `instanceof Object` (Object|Function|Array|RegExp|etc) + // Use `typeof` tho, b/c in old IE, DOM elements are not `instanceof Object` + // like they are in modern browsers. Using `Object.create` on DOM elements + // is...err...probably inappropriate, but the native version allows for it. + throw new TypeError('Object prototype may only be an Object or null'); // same msg as Chrome + } + + Type.prototype = prototype; + + object = new Type(); + } + + if (properties !== undefined) { + defineProperties['default'](object, properties); + } + + return object; + }; + } else { + create = Object.create; + } + + exports['default'] = create; + +}); +enifed('ember-metal/platform/define_properties', ['exports', 'ember-metal/platform/define_property'], function (exports, define_property) { + + 'use strict'; + + var defineProperties = Object.defineProperties; + + // ES5 15.2.3.7 + // http://es5.github.com/#x15.2.3.7 + if (!defineProperties) { + defineProperties = function defineProperties(object, properties) { + for (var property in properties) { + if (properties.hasOwnProperty(property) && property !== "__proto__") { + define_property.defineProperty(object, property, properties[property]); + } + } + return object; + }; + + Object.defineProperties = defineProperties; + } + + exports['default'] = defineProperties; + +}); +enifed('ember-metal/platform/define_property', ['exports'], function (exports) { + + 'use strict'; + + /*globals Node */ + + /** + @class platform + @namespace Ember + @static + */ + + /** + Set to true if the platform supports native getters and setters. + + @property hasPropertyAccessors + @final + */ + + /** + Identical to `Object.defineProperty()`. Implements as much functionality + as possible if not available natively. + + @method defineProperty + @param {Object} obj The object to modify + @param {String} keyName property name to modify + @param {Object} desc descriptor hash + @return {void} + */ + var defineProperty = (function checkCompliance(defineProperty) { + if (!defineProperty) { + return; + } + + try { + var a = 5; + var obj = {}; + defineProperty(obj, 'a', { + configurable: true, + enumerable: true, + get: function () { + return a; + }, + set: function (v) { + a = v; + } + }); + if (obj.a !== 5) { + return; + } + + obj.a = 10; + if (a !== 10) { + return; + } + + // check non-enumerability + defineProperty(obj, 'a', { + configurable: true, + enumerable: false, + writable: true, + value: true + }); + for (var key in obj) { + if (key === 'a') { + return; + } + } + + // Detects a bug in Android <3.2 where you cannot redefine a property using + // Object.defineProperty once accessors have already been set. + if (obj.a !== true) { + return; + } + + // Detects a bug in Android <3 where redefining a property without a value changes the value + // Object.defineProperty once accessors have already been set. + defineProperty(obj, 'a', { + enumerable: false + }); + if (obj.a !== true) { + return; + } + + // defineProperty is compliant + return defineProperty; + } catch (e) { + // IE8 defines Object.defineProperty but calling it on an Object throws + return; + } + })(Object.defineProperty); + + var hasES5CompliantDefineProperty = !!defineProperty; + + if (hasES5CompliantDefineProperty && typeof document !== 'undefined') { + // This is for Safari 5.0, which supports Object.defineProperty, but not + // on DOM nodes. + var canDefinePropertyOnDOM = (function () { + try { + defineProperty(document.createElement('div'), 'definePropertyOnDOM', {}); + return true; + } catch (e) {} + + return false; + })(); + + if (!canDefinePropertyOnDOM) { + defineProperty = function (obj, keyName, desc) { + var isNode; + + if (typeof Node === 'object') { + isNode = obj instanceof Node; + } else { + isNode = typeof obj === 'object' && typeof obj.nodeType === 'number' && typeof obj.nodeName === 'string'; + } + + if (isNode) { + // TODO: Should we have a warning here? + return obj[keyName] = desc.value; + } else { + return Object.defineProperty(obj, keyName, desc); + } + }; + } + } + + if (!hasES5CompliantDefineProperty) { + defineProperty = function definePropertyPolyfill(obj, keyName, desc) { + if (!desc.get) { + obj[keyName] = desc.value; + } + }; + } + + var hasPropertyAccessors = hasES5CompliantDefineProperty; + var canDefineNonEnumerableProperties = hasES5CompliantDefineProperty; + + exports.hasES5CompliantDefineProperty = hasES5CompliantDefineProperty; + exports.defineProperty = defineProperty; + exports.hasPropertyAccessors = hasPropertyAccessors; + exports.canDefineNonEnumerableProperties = canDefineNonEnumerableProperties; + +}); +enifed('ember-metal/properties', ['exports', 'ember-metal/core', 'ember-metal/utils', 'ember-metal/platform/define_property', 'ember-metal/property_events'], function (exports, Ember, utils, define_property, property_events) { + + 'use strict'; + + exports.Descriptor = Descriptor; + exports.MANDATORY_SETTER_FUNCTION = MANDATORY_SETTER_FUNCTION; + exports.DEFAULT_GETTER_FUNCTION = DEFAULT_GETTER_FUNCTION; + exports.defineProperty = defineProperty; + + function Descriptor() { + this.isDescriptor = true; + } + + // .......................................................... + // DEFINING PROPERTIES API + // + + function MANDATORY_SETTER_FUNCTION(name) { + return function SETTER_FUNCTION(value) { + Ember['default'].assert("You must use Ember.set() to set the `" + name + "` property (of " + this + ") to `" + value + "`.", false); + }; + } + + function DEFAULT_GETTER_FUNCTION(name) { + return function GETTER_FUNCTION() { + var meta = this["__ember_meta__"]; + return meta && meta.values[name]; + }; + } + + /** + NOTE: This is a low-level method used by other parts of the API. You almost + never want to call this method directly. Instead you should use + `Ember.mixin()` to define new properties. + + Defines a property on an object. This method works much like the ES5 + `Object.defineProperty()` method except that it can also accept computed + properties and other special descriptors. + + Normally this method takes only three parameters. However if you pass an + instance of `Descriptor` as the third param then you can pass an + optional value as the fourth parameter. This is often more efficient than + creating new descriptor hashes for each property. + + ## Examples + + ```javascript + // ES5 compatible mode + Ember.defineProperty(contact, 'firstName', { + writable: true, + configurable: false, + enumerable: true, + value: 'Charles' + }); + + // define a simple property + Ember.defineProperty(contact, 'lastName', undefined, 'Jolley'); + + // define a computed property + Ember.defineProperty(contact, 'fullName', Ember.computed(function() { + return this.firstName+' '+this.lastName; + }).property('firstName', 'lastName')); + ``` + + @private + @method defineProperty + @for Ember + @param {Object} obj the object to define this property on. This may be a prototype. + @param {String} keyName the name of the property + @param {Descriptor} [desc] an instance of `Descriptor` (typically a + computed property) or an ES5 descriptor. + You must provide this or `data` but not both. + @param {*} [data] something other than a descriptor, that will + become the explicit value of this property. + */ + function defineProperty(obj, keyName, desc, data, meta) { + var possibleDesc, existingDesc, watching, value; + + if (!meta) { + meta = utils.meta(obj); + } + var watchEntry = meta.watching[keyName]; + possibleDesc = obj[keyName]; + existingDesc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + + watching = watchEntry !== undefined && watchEntry > 0; + + if (existingDesc) { + existingDesc.teardown(obj, keyName); + } + + if (desc instanceof Descriptor) { + value = desc; + + + if (watching && define_property.hasPropertyAccessors) { + define_property.defineProperty(obj, keyName, { + configurable: true, + enumerable: true, + writable: true, + value: value + }); + } else { + obj[keyName] = value; + } + if (desc.setup) { + desc.setup(obj, keyName); + } + } else { + if (desc == null) { + value = data; + + + if (watching && define_property.hasPropertyAccessors) { + meta.values[keyName] = data; + define_property.defineProperty(obj, keyName, { + configurable: true, + enumerable: true, + set: MANDATORY_SETTER_FUNCTION(keyName), + get: DEFAULT_GETTER_FUNCTION(keyName) + }); + } else { + obj[keyName] = data; + } + } else { + value = desc; + + // compatibility with ES5 + define_property.defineProperty(obj, keyName, desc); + } + } + + // if key is being watched, override chains that + // were initialized with the prototype + if (watching) { + property_events.overrideChains(obj, keyName, meta); + } + + // The `value` passed to the `didDefineProperty` hook is + // either the descriptor or data, whichever was passed. + if (obj.didDefineProperty) { + obj.didDefineProperty(obj, keyName, value); + } + + return this; + } + +}); +enifed('ember-metal/property_events', ['exports', 'ember-metal/utils', 'ember-metal/events', 'ember-metal/observer_set'], function (exports, utils, ember_metal__events, ObserverSet) { + + 'use strict'; + + exports.propertyWillChange = propertyWillChange; + exports.propertyDidChange = propertyDidChange; + exports.overrideChains = overrideChains; + exports.beginPropertyChanges = beginPropertyChanges; + exports.endPropertyChanges = endPropertyChanges; + exports.changeProperties = changeProperties; + + var beforeObserverSet = new ObserverSet['default'](); + var observerSet = new ObserverSet['default'](); + var deferred = 0; + + // .......................................................... + // PROPERTY CHANGES + // + + /** + This function is called just before an object property is about to change. + It will notify any before observers and prepare caches among other things. + + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyDidChange()` which you should call just + after the property value changes. + + @method propertyWillChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @return {void} + */ + function propertyWillChange(obj, keyName) { + var m = obj["__ember_meta__"]; + var watching = m && m.watching[keyName] > 0 || keyName === "length"; + var proto = m && m.proto; + var possibleDesc = obj[keyName]; + var desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + + if (!watching) { + return; + } + + if (proto === obj) { + return; + } + + if (desc && desc.willChange) { + desc.willChange(obj, keyName); + } + + dependentKeysWillChange(obj, keyName, m); + chainsWillChange(obj, keyName, m); + notifyBeforeObservers(obj, keyName); + } + + /** + This function is called just after an object property has changed. + It will notify any observers and clear caches among other things. + + Normally you will not need to call this method directly but if for some + reason you can't directly watch a property you can invoke this method + manually along with `Ember.propertyWillChange()` which you should call just + before the property value changes. + + @method propertyDidChange + @for Ember + @param {Object} obj The object with the property that will change + @param {String} keyName The property key (or path) that will change. + @return {void} + */ + function propertyDidChange(obj, keyName) { + var m = obj["__ember_meta__"]; + var watching = m && m.watching[keyName] > 0 || keyName === "length"; + var proto = m && m.proto; + var possibleDesc = obj[keyName]; + var desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + + if (proto === obj) { + return; + } + + // shouldn't this mean that we're watching this key? + if (desc && desc.didChange) { + desc.didChange(obj, keyName); + } + + if (!watching && keyName !== "length") { + return; + } + + if (m && m.deps && m.deps[keyName]) { + dependentKeysDidChange(obj, keyName, m); + } + + chainsDidChange(obj, keyName, m, false); + notifyObservers(obj, keyName); + } + + var WILL_SEEN, DID_SEEN; + // called whenever a property is about to change to clear the cache of any dependent keys (and notify those properties of changes, etc...) + function dependentKeysWillChange(obj, depKey, meta) { + if (obj.isDestroying) { + return; + } + + var deps; + if (meta && meta.deps && (deps = meta.deps[depKey])) { + var seen = WILL_SEEN; + var top = !seen; + + if (top) { + seen = WILL_SEEN = {}; + } + + iterDeps(propertyWillChange, obj, deps, depKey, seen, meta); + + if (top) { + WILL_SEEN = null; + } + } + } + + // called whenever a property has just changed to update dependent keys + function dependentKeysDidChange(obj, depKey, meta) { + if (obj.isDestroying) { + return; + } + + var deps; + if (meta && meta.deps && (deps = meta.deps[depKey])) { + var seen = DID_SEEN; + var top = !seen; + + if (top) { + seen = DID_SEEN = {}; + } + + iterDeps(propertyDidChange, obj, deps, depKey, seen, meta); + + if (top) { + DID_SEEN = null; + } + } + } + + function keysOf(obj) { + var keys = []; + + for (var key in obj) { + keys.push(key); + } + + return keys; + } + + function iterDeps(method, obj, deps, depKey, seen, meta) { + var keys, key, i, possibleDesc, desc; + var guid = utils.guidFor(obj); + var current = seen[guid]; + + if (!current) { + current = seen[guid] = {}; + } + + if (current[depKey]) { + return; + } + + current[depKey] = true; + + if (deps) { + keys = keysOf(deps); + for (i = 0; i < keys.length; i++) { + key = keys[i]; + possibleDesc = obj[key]; + desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + + if (desc && desc._suspended === obj) { + continue; + } + + method(obj, key); + } + } + } + + function chainsWillChange(obj, keyName, m) { + if (!(m.hasOwnProperty("chainWatchers") && m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName]; + var events = []; + var i, l; + + for (i = 0, l = nodes.length; i < l; i++) { + nodes[i].willChange(events); + } + + for (i = 0, l = events.length; i < l; i += 2) { + propertyWillChange(events[i], events[i + 1]); + } + } + + function chainsDidChange(obj, keyName, m, suppressEvents) { + if (!(m && m.hasOwnProperty("chainWatchers") && m.chainWatchers[keyName])) { + return; + } + + var nodes = m.chainWatchers[keyName]; + var events = suppressEvents ? null : []; + var i, l; + + for (i = 0, l = nodes.length; i < l; i++) { + nodes[i].didChange(events); + } + + if (suppressEvents) { + return; + } + + for (i = 0, l = events.length; i < l; i += 2) { + propertyDidChange(events[i], events[i + 1]); + } + } + + function overrideChains(obj, keyName, m) { + chainsDidChange(obj, keyName, m, true); + } + + /** + @method beginPropertyChanges + @chainable + @private + */ + function beginPropertyChanges() { + deferred++; + } + + /** + @method endPropertyChanges + @private + */ + function endPropertyChanges() { + deferred--; + if (deferred <= 0) { + beforeObserverSet.clear(); + observerSet.flush(); + } + } + + /** + Make a series of property changes together in an + exception-safe way. + + ```javascript + Ember.changeProperties(function() { + obj1.set('foo', mayBlowUpWhenSet); + obj2.set('bar', baz); + }); + ``` + + @method changeProperties + @param {Function} callback + @param [binding] + */ + function changeProperties(callback, binding) { + beginPropertyChanges(); + utils.tryFinally(callback, endPropertyChanges, binding); + } + + function notifyBeforeObservers(obj, keyName) { + if (obj.isDestroying) { + return; + } + + var eventName = keyName + ":before"; + var listeners, added; + if (deferred) { + listeners = beforeObserverSet.add(obj, keyName, eventName); + added = ember_metal__events.accumulateListeners(obj, eventName, listeners); + ember_metal__events.sendEvent(obj, eventName, [obj, keyName], added); + } else { + ember_metal__events.sendEvent(obj, eventName, [obj, keyName]); + } + } + + function notifyObservers(obj, keyName) { + if (obj.isDestroying) { + return; + } + + var eventName = keyName + ":change"; + var listeners; + if (deferred) { + listeners = observerSet.add(obj, keyName, eventName); + ember_metal__events.accumulateListeners(obj, eventName, listeners); + } else { + ember_metal__events.sendEvent(obj, eventName, [obj, keyName]); + } + } + +}); +enifed('ember-metal/property_get', ['exports', 'ember-metal/core', 'ember-metal/error', 'ember-metal/path_cache', 'ember-metal/platform/define_property', 'ember-metal/is_none'], function (exports, Ember, EmberError, path_cache, define_property, isNone) { + + 'use strict'; + + exports.get = get; + exports.normalizeTuple = normalizeTuple; + exports._getPath = _getPath; + exports.getWithDefault = getWithDefault; + + var FIRST_KEY = /^([^\.]+)/; + + // .......................................................... + // GET AND SET + // + // If we are on a platform that supports accessors we can use those. + // Otherwise simulate accessors by looking up the property directly on the + // object. + + /** + Gets the value of a property on an object. If the property is computed, + the function will be invoked. If the property is not defined but the + object implements the `unknownProperty` method then that will be invoked. + + If you plan to run on IE8 and older browsers then you should use this + method anytime you want to retrieve a property on an object that you don't + know for sure is private. (Properties beginning with an underscore '_' + are considered private.) + + On all newer browsers, you only need to use this method to retrieve + properties if the property might not be defined on the object and you want + to respect the `unknownProperty` handler. Otherwise you can ignore this + method. + + Note that if the object itself is `undefined`, this method will throw + an error. + + @method get + @for Ember + @param {Object} obj The object to retrieve from. + @param {String} keyName The property key to retrieve + @return {Object} the property value or `null`. + */ + function get(obj, keyName) { + // Helpers that operate with 'this' within an #each + if (keyName === "") { + return obj; + } + + if (!keyName && "string" === typeof obj) { + keyName = obj; + obj = Ember['default'].lookup; + } + + Ember['default'].assert("Cannot call get with " + keyName + " key.", !!keyName); + Ember['default'].assert("Cannot call get with '" + keyName + "' on an undefined object.", obj !== undefined); + + if (isNone['default'](obj)) { + return _getPath(obj, keyName); + } + + var meta = obj["__ember_meta__"]; + var possibleDesc = obj[keyName]; + var desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + var ret; + + if (desc === undefined && path_cache.isPath(keyName)) { + return _getPath(obj, keyName); + } + + if (desc) { + return desc.get(obj, keyName); + } else { + + if (define_property.hasPropertyAccessors && meta && meta.watching[keyName] > 0) { + ret = meta.values[keyName]; + } else { + ret = obj[keyName]; + } + + if (ret === undefined && "object" === typeof obj && !(keyName in obj) && "function" === typeof obj.unknownProperty) { + return obj.unknownProperty(keyName); + } + + return ret; + } + } + + /** + Normalizes a target/path pair to reflect that actual target/path that should + be observed, etc. This takes into account passing in global property + paths (i.e. a path beginning with a capital letter not defined on the + target). + + @private + @method normalizeTuple + @for Ember + @param {Object} target The current target. May be `null`. + @param {String} path A path on the target or a global property path. + @return {Array} a temporary array with the normalized target/path pair. + */ + function normalizeTuple(target, path) { + var hasThis = path_cache.hasThis(path); + var isGlobal = !hasThis && path_cache.isGlobal(path); + var key; + + if (!target && !isGlobal) { + return [undefined, ""]; + } + + if (hasThis) { + path = path.slice(5); + } + + if (!target || isGlobal) { + target = Ember['default'].lookup; + } + + if (isGlobal && path_cache.isPath(path)) { + key = path.match(FIRST_KEY)[0]; + target = get(target, key); + path = path.slice(key.length + 1); + } + + // must return some kind of path to be valid else other things will break. + validateIsPath(path); + + return [target, path]; + } + + function validateIsPath(path) { + if (!path || path.length === 0) { + throw new EmberError['default']("Object in path " + path + " could not be found or was destroyed."); + } + } + function _getPath(root, path) { + var hasThis, parts, tuple, idx, len; + + // detect complicated paths and normalize them + hasThis = path_cache.hasThis(path); + + if (!root || hasThis) { + tuple = normalizeTuple(root, path); + root = tuple[0]; + path = tuple[1]; + tuple.length = 0; + } + + parts = path.split("."); + len = parts.length; + for (idx = 0; root != null && idx < len; idx++) { + root = get(root, parts[idx], true); + if (root && root.isDestroyed) { + return undefined; + } + } + return root; + } + + function getWithDefault(root, key, defaultValue) { + var value = get(root, key); + + if (value === undefined) { + return defaultValue; + } + return value; + } + + exports['default'] = get; + +}); +enifed('ember-metal/property_set', ['exports', 'ember-metal/core', 'ember-metal/property_get', 'ember-metal/property_events', 'ember-metal/properties', 'ember-metal/error', 'ember-metal/path_cache', 'ember-metal/platform/define_property'], function (exports, Ember, property_get, property_events, properties, EmberError, path_cache, define_property) { + + 'use strict'; + + exports.set = set; + exports.trySet = trySet; + + function set(obj, keyName, value, tolerant) { + if (typeof obj === "string") { + Ember['default'].assert("Path '" + obj + "' must be global if no obj is given.", path_cache.isGlobalPath(obj)); + value = keyName; + keyName = obj; + obj = Ember['default'].lookup; + } + + Ember['default'].assert("Cannot call set with '" + keyName + "' key.", !!keyName); + + if (obj === Ember['default'].lookup) { + return setPath(obj, keyName, value, tolerant); + } + + var meta, possibleDesc, desc; + if (obj) { + meta = obj["__ember_meta__"]; + possibleDesc = obj[keyName]; + desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + } + + var isUnknown, currentValue; + if ((!obj || desc === undefined) && path_cache.isPath(keyName)) { + return setPath(obj, keyName, value, tolerant); + } + + Ember['default'].assert("You need to provide an object and key to `set`.", !!obj && keyName !== undefined); + Ember['default'].assert("calling set on destroyed object", !obj.isDestroyed); + + if (desc) { + desc.set(obj, keyName, value); + } else { + + if (obj !== null && value !== undefined && typeof obj === "object" && obj[keyName] === value) { + return value; + } + + isUnknown = "object" === typeof obj && !(keyName in obj); + + // setUnknownProperty is called if `obj` is an object, + // the property does not already exist, and the + // `setUnknownProperty` method exists on the object + if (isUnknown && "function" === typeof obj.setUnknownProperty) { + obj.setUnknownProperty(keyName, value); + } else if (meta && meta.watching[keyName] > 0) { + if (meta.proto !== obj) { + + if (define_property.hasPropertyAccessors) { + currentValue = meta.values[keyName]; + } else { + currentValue = obj[keyName]; + } + } + // only trigger a change if the value has changed + if (value !== currentValue) { + property_events.propertyWillChange(obj, keyName); + + if (define_property.hasPropertyAccessors) { + if (currentValue === undefined && !(keyName in obj) || !Object.prototype.propertyIsEnumerable.call(obj, keyName)) { + properties.defineProperty(obj, keyName, null, value); // setup mandatory setter + } else { + meta.values[keyName] = value; + } + } else { + obj[keyName] = value; + } + property_events.propertyDidChange(obj, keyName); + } + } else { + obj[keyName] = value; + } + } + return value; + } + + function setPath(root, path, value, tolerant) { + var keyName; + + // get the last part of the path + keyName = path.slice(path.lastIndexOf(".") + 1); + + // get the first part of the part + path = path === keyName ? keyName : path.slice(0, path.length - (keyName.length + 1)); + + // unless the path is this, look up the first part to + // get the root + if (path !== "this") { + root = property_get._getPath(root, path); + } + + if (!keyName || keyName.length === 0) { + throw new EmberError['default']("Property set failed: You passed an empty path"); + } + + if (!root) { + if (tolerant) { + return; + } else { + throw new EmberError['default']("Property set failed: object in path \"" + path + "\" could not be found or was destroyed."); + } + } + + return set(root, keyName, value); + } + + /** + Error-tolerant form of `Ember.set`. Will not blow up if any part of the + chain is `undefined`, `null`, or destroyed. + + This is primarily used when syncing bindings, which may try to update after + an object has been destroyed. + + @method trySet + @for Ember + @param {Object} obj The object to modify. + @param {String} path The property path to set + @param {Object} value The value to set + */ + function trySet(root, path, value) { + return set(root, path, value, true); + } + +}); +enifed('ember-metal/run_loop', ['exports', 'ember-metal/core', 'ember-metal/utils', 'ember-metal/array', 'ember-metal/property_events', 'backburner'], function (exports, Ember, utils, array, property_events, Backburner) { + + 'use strict'; + + function onBegin(current) { + run.currentRunLoop = current; + } + + function onEnd(current, next) { + run.currentRunLoop = next; + } + + // ES6TODO: should Backburner become es6? + var backburner = new Backburner['default'](['sync', 'actions', 'destroy'], { + GUID_KEY: utils.GUID_KEY, + sync: { + before: property_events.beginPropertyChanges, + after: property_events.endPropertyChanges + }, + defaultQueue: 'actions', + onBegin: onBegin, + onEnd: onEnd, + onErrorTarget: Ember['default'], + onErrorMethod: 'onerror' + }); + + // .......................................................... + // run - this is ideally the only public API the dev sees + // + + /** + Runs the passed target and method inside of a RunLoop, ensuring any + deferred actions including bindings and views updates are flushed at the + end. + + Normally you should not need to invoke this method yourself. However if + you are implementing raw event handlers when interfacing with other + libraries or plugins, you should probably wrap all of your code inside this + call. + + ```javascript + run(function() { + // code to be executed within a RunLoop + }); + ``` + + @class run + @namespace Ember + @static + @constructor + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} return value from invoking the passed function. + */ + exports['default'] = run; + function run() { + return backburner.run.apply(backburner, arguments); + } + + /** + If no run-loop is present, it creates a new one. If a run loop is + present it will queue itself to run on the existing run-loops action + queue. + + Please note: This is not for normal usage, and should be used sparingly. + + If invoked when not within a run loop: + + ```javascript + run.join(function() { + // creates a new run-loop + }); + ``` + + Alternatively, if called within an existing run loop: + + ```javascript + run(function() { + // creates a new run-loop + run.join(function() { + // joins with the existing run-loop, and queues for invocation on + // the existing run-loops action queue. + }); + }); + ``` + + @method join + @namespace Ember + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Object} Return value from invoking the passed function. Please note, + when called within an existing loop, no return value is possible. + */ + run.join = function () { + return backburner.join.apply(backburner, arguments); + }; + + /** + Allows you to specify which context to call the specified function in while + adding the execution of that function to the Ember run loop. This ability + makes this method a great way to asynchronously integrate third-party libraries + into your Ember application. + + `run.bind` takes two main arguments, the desired context and the function to + invoke in that context. Any additional arguments will be supplied as arguments + to the function that is passed in. + + Let's use the creation of a TinyMCE component as an example. Currently, + TinyMCE provides a setup configuration option we can use to do some processing + after the TinyMCE instance is initialized but before it is actually rendered. + We can use that setup option to do some additional setup for our component. + The component itself could look something like the following: + + ```javascript + App.RichTextEditorComponent = Ember.Component.extend({ + initializeTinyMCE: Ember.on('didInsertElement', function() { + tinymce.init({ + selector: '#' + this.$().prop('id'), + setup: Ember.run.bind(this, this.setupEditor) + }); + }), + + setupEditor: function(editor) { + this.set('editor', editor); + + editor.on('change', function() { + console.log('content changed!'); + }); + } + }); + ``` + + In this example, we use Ember.run.bind to bind the setupEditor method to the + context of the App.RichTextEditorComponent and to have the invocation of that + method be safely handled and executed by the Ember run loop. + + @method bind + @namespace Ember + @param {Object} [target] target of method to call + @param {Function|String} method Method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Any additional arguments you wish to pass to the method. + @return {Function} returns a new function that will always have a particular context + @since 1.4.0 + */ + run.bind = function () { + for (var _len = arguments.length, curried = Array(_len), _key = 0; _key < _len; _key++) { + curried[_key] = arguments[_key]; + } + + return function () { + for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { + args[_key2] = arguments[_key2]; + } + + return run.join.apply(run, curried.concat(args)); + }; + }; + + run.backburner = backburner; + run.currentRunLoop = null; + run.queues = backburner.queueNames; + + /** + Begins a new RunLoop. Any deferred actions invoked after the begin will + be buffered until you invoke a matching call to `run.end()`. This is + a lower-level way to use a RunLoop instead of using `run()`. + + ```javascript + run.begin(); + // code to be executed within a RunLoop + run.end(); + ``` + + @method begin + @return {void} + */ + run.begin = function () { + backburner.begin(); + }; + + /** + Ends a RunLoop. This must be called sometime after you call + `run.begin()` to flush any deferred actions. This is a lower-level way + to use a RunLoop instead of using `run()`. + + ```javascript + run.begin(); + // code to be executed within a RunLoop + run.end(); + ``` + + @method end + @return {void} + */ + run.end = function () { + backburner.end(); + }; + + /** + Array of named queues. This array determines the order in which queues + are flushed at the end of the RunLoop. You can define your own queues by + simply adding the queue name to this array. Normally you should not need + to inspect or modify this property. + + @property queues + @type Array + @default ['sync', 'actions', 'destroy'] + */ + + /** + Adds the passed target/method and any optional arguments to the named + queue to be executed at the end of the RunLoop. If you have not already + started a RunLoop when calling this method one will be started for you + automatically. + + At the end of a RunLoop, any methods scheduled in this way will be invoked. + Methods will be invoked in an order matching the named queues defined in + the `run.queues` property. + + ```javascript + run.schedule('sync', this, function() { + // this will be executed in the first RunLoop queue, when bindings are synced + console.log('scheduled on sync queue'); + }); + + run.schedule('actions', this, function() { + // this will be executed in the 'actions' queue, after bindings have synced. + console.log('scheduled on actions queue'); + }); + + // Note the functions will be run in order based on the run queues order. + // Output would be: + // scheduled on sync queue + // scheduled on actions queue + ``` + + @method schedule + @param {String} queue The name of the queue to schedule against. + Default queues are 'sync' and 'actions' + @param {Object} [target] target object to use as the context when invoking a method. + @param {String|Function} method The method to invoke. If you pass a string it + will be resolved on the target object at the time the scheduled item is + invoked allowing you to change the target function. + @param {Object} [arguments*] Optional arguments to be passed to the queued method. + @return {void} + */ + run.schedule = function () { + checkAutoRun(); + backburner.schedule.apply(backburner, arguments); + }; + + // Used by global test teardown + run.hasScheduledTimers = function () { + return backburner.hasTimers(); + }; + + // Used by global test teardown + run.cancelTimers = function () { + backburner.cancelTimers(); + }; + + /** + Immediately flushes any events scheduled in the 'sync' queue. Bindings + use this queue so this method is a useful way to immediately force all + bindings in the application to sync. + + You should call this method anytime you need any changed state to propagate + throughout the app immediately without repainting the UI (which happens + in the later 'render' queue added by the `ember-views` package). + + ```javascript + run.sync(); + ``` + + @method sync + @return {void} + */ + run.sync = function () { + if (backburner.currentInstance) { + backburner.currentInstance.queues.sync.flush(); + } + }; + + /** + Invokes the passed target/method and optional arguments after a specified + period of time. The last parameter of this method must always be a number + of milliseconds. + + You should use this method whenever you need to run some action after a + period of time instead of using `setTimeout()`. This method will ensure that + items that expire during the same script execution cycle all execute + together, which is often more efficient than using a real setTimeout. + + ```javascript + run.later(myContext, function() { + // code here will execute within a RunLoop in about 500ms with this == myContext + }, 500); + ``` + + @method later + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} wait Number of milliseconds to wait. + @return {*} Timer information for use in cancelling, see `run.cancel`. + */ + run.later = function () { + return backburner.later.apply(backburner, arguments); + }; + + /** + Schedule a function to run one time during the current RunLoop. This is equivalent + to calling `scheduleOnce` with the "actions" queue. + + @method once + @param {Object} [target] The target of the method to invoke. + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @return {Object} Timer information for use in cancelling, see `run.cancel`. + */ + run.once = function () { + for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { + args[_key3] = arguments[_key3]; + } + + checkAutoRun(); + args.unshift('actions'); + return backburner.scheduleOnce.apply(backburner, args); + }; + + /** + Schedules a function to run one time in a given queue of the current RunLoop. + Calling this method with the same queue/target/method combination will have + no effect (past the initial call). + + Note that although you can pass optional arguments these will not be + considered when looking for duplicates. New arguments will replace previous + calls. + + ```javascript + function sayHi() { + console.log('hi'); + } + + run(function() { + run.scheduleOnce('afterRender', myContext, sayHi); + run.scheduleOnce('afterRender', myContext, sayHi); + // sayHi will only be executed once, in the afterRender queue of the RunLoop + }); + ``` + + Also note that passing an anonymous function to `run.scheduleOnce` will + not prevent additional calls with an identical anonymous function from + scheduling the items multiple times, e.g.: + + ```javascript + function scheduleIt() { + run.scheduleOnce('actions', myContext, function() { + console.log('Closure'); + }); + } + + scheduleIt(); + scheduleIt(); + + // "Closure" will print twice, even though we're using `run.scheduleOnce`, + // because the function we pass to it is anonymous and won't match the + // previously scheduled operation. + ``` + + Available queues, and their order, can be found at `run.queues` + + @method scheduleOnce + @param {String} [queue] The name of the queue to schedule against. Default queues are 'sync' and 'actions'. + @param {Object} [target] The target of the method to invoke. + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @return {Object} Timer information for use in cancelling, see `run.cancel`. + */ + run.scheduleOnce = function () { + checkAutoRun(); + return backburner.scheduleOnce.apply(backburner, arguments); + }; + + /** + Schedules an item to run from within a separate run loop, after + control has been returned to the system. This is equivalent to calling + `run.later` with a wait time of 1ms. + + ```javascript + run.next(myContext, function() { + // code to be executed in the next run loop, + // which will be scheduled after the current one + }); + ``` + + Multiple operations scheduled with `run.next` will coalesce + into the same later run loop, along with any other operations + scheduled by `run.later` that expire right around the same + time that `run.next` operations will fire. + + Note that there are often alternatives to using `run.next`. + For instance, if you'd like to schedule an operation to happen + after all DOM element operations have completed within the current + run loop, you can make use of the `afterRender` run loop queue (added + by the `ember-views` package, along with the preceding `render` queue + where all the DOM element operations happen). Example: + + ```javascript + App.MyCollectionView = Ember.CollectionView.extend({ + didInsertElement: function() { + run.scheduleOnce('afterRender', this, 'processChildElements'); + }, + processChildElements: function() { + // ... do something with collectionView's child view + // elements after they've finished rendering, which + // can't be done within the CollectionView's + // `didInsertElement` hook because that gets run + // before the child elements have been added to the DOM. + } + }); + ``` + + One benefit of the above approach compared to using `run.next` is + that you will be able to perform DOM/CSS operations before unprocessed + elements are rendered to the screen, which may prevent flickering or + other artifacts caused by delaying processing until after rendering. + + The other major benefit to the above approach is that `run.next` + introduces an element of non-determinism, which can make things much + harder to test, due to its reliance on `setTimeout`; it's much harder + to guarantee the order of scheduled operations when they are scheduled + outside of the current run loop, i.e. with `run.next`. + + @method next + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + If you pass a string it will be resolved on the + target at the time the method is invoked. + @param {Object} [args*] Optional arguments to pass to the timeout. + @return {Object} Timer information for use in cancelling, see `run.cancel`. + */ + run.next = function () { + for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { + args[_key4] = arguments[_key4]; + } + + args.push(1); + return backburner.later.apply(backburner, args); + }; + + /** + Cancels a scheduled item. Must be a value returned by `run.later()`, + `run.once()`, `run.next()`, `run.debounce()`, or + `run.throttle()`. + + ```javascript + var runNext = run.next(myContext, function() { + // will not be executed + }); + + run.cancel(runNext); + + var runLater = run.later(myContext, function() { + // will not be executed + }, 500); + + run.cancel(runLater); + + var runOnce = run.once(myContext, function() { + // will not be executed + }); + + run.cancel(runOnce); + + var throttle = run.throttle(myContext, function() { + // will not be executed + }, 1, false); + + run.cancel(throttle); + + var debounce = run.debounce(myContext, function() { + // will not be executed + }, 1); + + run.cancel(debounce); + + var debounceImmediate = run.debounce(myContext, function() { + // will be executed since we passed in true (immediate) + }, 100, true); + + // the 100ms delay until this method can be called again will be cancelled + run.cancel(debounceImmediate); + ``` + + @method cancel + @param {Object} timer Timer object to cancel + @return {Boolean} true if cancelled or false/undefined if it wasn't found + */ + run.cancel = function (timer) { + return backburner.cancel(timer); + }; + + /** + Delay calling the target method until the debounce period has elapsed + with no additional debounce calls. If `debounce` is called again before + the specified time has elapsed, the timer is reset and the entire period + must pass again before the target method is called. + + This method should be used when an event may be called multiple times + but the action should only be called once when the event is done firing. + A common example is for scroll events where you only want updates to + happen once scrolling has ceased. + + ```javascript + function whoRan() { + console.log(this.name + ' ran.'); + } + + var myContext = { name: 'debounce' }; + + run.debounce(myContext, whoRan, 150); + + // less than 150ms passes + run.debounce(myContext, whoRan, 150); + + // 150ms passes + // whoRan is invoked with context myContext + // console logs 'debounce ran.' one time. + ``` + + Immediate allows you to run the function immediately, but debounce + other calls for this function until the wait time has elapsed. If + `debounce` is called again before the specified time has elapsed, + the timer is reset and the entire period must pass again before + the method can be called again. + + ```javascript + function whoRan() { + console.log(this.name + ' ran.'); + } + + var myContext = { name: 'debounce' }; + + run.debounce(myContext, whoRan, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 100ms passes + run.debounce(myContext, whoRan, 150, true); + + // 150ms passes and nothing else is logged to the console and + // the debouncee is no longer being watched + run.debounce(myContext, whoRan, 150, true); + + // console logs 'debounce ran.' one time immediately. + // 150ms passes and nothing else is logged to the console and + // the debouncee is no longer being watched + + ``` + + @method debounce + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} wait Number of milliseconds to wait. + @param {Boolean} immediate Trigger the function on the leading instead + of the trailing edge of the wait interval. Defaults to false. + @return {Array} Timer information for use in cancelling, see `run.cancel`. + */ + run.debounce = function () { + return backburner.debounce.apply(backburner, arguments); + }; + + /** + Ensure that the target method is never called more frequently than + the specified spacing period. The target method is called immediately. + + ```javascript + function whoRan() { + console.log(this.name + ' ran.'); + } + + var myContext = { name: 'throttle' }; + + run.throttle(myContext, whoRan, 150); + // whoRan is invoked with context myContext + // console logs 'throttle ran.' + + // 50ms passes + run.throttle(myContext, whoRan, 150); + + // 50ms passes + run.throttle(myContext, whoRan, 150); + + // 150ms passes + run.throttle(myContext, whoRan, 150); + // whoRan is invoked with context myContext + // console logs 'throttle ran.' + ``` + + @method throttle + @param {Object} [target] target of method to invoke + @param {Function|String} method The method to invoke. + May be a function or a string. If you pass a string + then it will be looked up on the passed target. + @param {Object} [args*] Optional arguments to pass to the timeout. + @param {Number} spacing Number of milliseconds to space out requests. + @param {Boolean} immediate Trigger the function on the leading instead + of the trailing edge of the wait interval. Defaults to true. + @return {Array} Timer information for use in cancelling, see `run.cancel`. + */ + run.throttle = function () { + return backburner.throttle.apply(backburner, arguments); + }; + + // Make sure it's not an autorun during testing + function checkAutoRun() { + if (!run.currentRunLoop) { + Ember['default'].assert('You have turned on testing mode, which disabled the run-loop\'s autorun.\n You will need to wrap any code with asynchronous side-effects in a run', !Ember['default'].testing); + } + } + + /** + Add a new named queue after the specified queue. + + The queue to add will only be added once. + + @method _addQueue + @param {String} name the name of the queue to add. + @param {String} after the name of the queue to add after. + @private + */ + run._addQueue = function (name, after) { + if (array.indexOf.call(run.queues, name) === -1) { + run.queues.splice(array.indexOf.call(run.queues, after) + 1, 0, name); + } + }; + /* queue, target, method */ /*target, method*/ /*queue, target, method*/ + +}); +enifed('ember-metal/set_properties', ['exports', 'ember-metal/property_events', 'ember-metal/property_set', 'ember-metal/keys'], function (exports, property_events, property_set, keys) { + + 'use strict'; + + + exports['default'] = setProperties; + function setProperties(obj, properties) { + if (!properties || typeof properties !== "object") { + return obj; + } + property_events.changeProperties(function () { + var props = keys['default'](properties); + var propertyName; + + for (var i = 0, l = props.length; i < l; i++) { + propertyName = props[i]; + + property_set.set(obj, propertyName, properties[propertyName]); + } + }); + return obj; + } + +}); +enifed('ember-metal/streams/conditional', ['exports', 'ember-metal/streams/stream', 'ember-metal/streams/utils', 'ember-metal/platform/create'], function (exports, Stream, utils, create) { + + 'use strict'; + + + + exports['default'] = conditional; + + function conditional(test, consequent, alternate) { + if (utils.isStream(test)) { + return new ConditionalStream(test, consequent, alternate); + } else { + if (test) { + return consequent; + } else { + return alternate; + } + } + } + + function ConditionalStream(test, consequent, alternate) { + this.init(); + + this.oldTestResult = undefined; + this.test = test; + this.consequent = consequent; + this.alternate = alternate; + } + + ConditionalStream.prototype = create['default'](Stream['default'].prototype); + + ConditionalStream.prototype.valueFn = function () { + var oldTestResult = this.oldTestResult; + var newTestResult = !!utils.read(this.test); + + if (newTestResult !== oldTestResult) { + switch (oldTestResult) { + case true: + utils.unsubscribe(this.consequent, this.notify, this);break; + case false: + utils.unsubscribe(this.alternate, this.notify, this);break; + case undefined: + utils.subscribe(this.test, this.notify, this); + } + + switch (newTestResult) { + case true: + utils.subscribe(this.consequent, this.notify, this);break; + case false: + utils.subscribe(this.alternate, this.notify, this); + } + + this.oldTestResult = newTestResult; + } + + return newTestResult ? utils.read(this.consequent) : utils.read(this.alternate); + }; + +}); +enifed('ember-metal/streams/simple', ['exports', 'ember-metal/merge', 'ember-metal/streams/stream', 'ember-metal/platform/create', 'ember-metal/streams/utils'], function (exports, merge, Stream, create, utils) { + + 'use strict'; + + function SimpleStream(source) { + this.init(); + this.source = source; + + if (utils.isStream(source)) { + source.subscribe(this._didChange, this); + } + } + + SimpleStream.prototype = create['default'](Stream['default'].prototype); + + merge['default'](SimpleStream.prototype, { + valueFn: function () { + return utils.read(this.source); + }, + + setValue: function (value) { + var source = this.source; + + if (utils.isStream(source)) { + source.setValue(value); + } + }, + + setSource: function (nextSource) { + var prevSource = this.source; + if (nextSource !== prevSource) { + if (utils.isStream(prevSource)) { + prevSource.unsubscribe(this._didChange, this); + } + + if (utils.isStream(nextSource)) { + nextSource.subscribe(this._didChange, this); + } + + this.source = nextSource; + this.notify(); + } + }, + + _didChange: function () { + this.notify(); + }, + + _super$destroy: Stream['default'].prototype.destroy, + + destroy: function () { + if (this._super$destroy()) { + if (utils.isStream(this.source)) { + this.source.unsubscribe(this._didChange, this); + } + this.source = undefined; + return true; + } + } + }); + + exports['default'] = SimpleStream; + +}); +enifed('ember-metal/streams/stream', ['exports', 'ember-metal/platform/create', 'ember-metal/path_cache'], function (exports, create, path_cache) { + + 'use strict'; + + function Subscriber(callback, context) { + this.next = null; + this.prev = null; + this.callback = callback; + this.context = context; + } + + Subscriber.prototype.removeFrom = function (stream) { + var next = this.next; + var prev = this.prev; + + if (prev) { + prev.next = next; + } else { + stream.subscriberHead = next; + } + + if (next) { + next.prev = prev; + } else { + stream.subscriberTail = prev; + } + }; + + /* + @public + @class Stream + @namespace Ember.stream + @constructor + */ + function Stream(fn) { + this.init(); + this.valueFn = fn; + } + + Stream.prototype = { + isStream: true, + + init: function () { + this.state = "dirty"; + this.cache = undefined; + this.subscriberHead = null; + this.subscriberTail = null; + this.children = undefined; + this._label = undefined; + }, + + get: function (path) { + var firstKey = path_cache.getFirstKey(path); + var tailPath = path_cache.getTailPath(path); + + if (this.children === undefined) { + this.children = create['default'](null); + } + + var keyStream = this.children[firstKey]; + + if (keyStream === undefined) { + keyStream = this._makeChildStream(firstKey, path); + this.children[firstKey] = keyStream; + } + + if (tailPath === undefined) { + return keyStream; + } else { + return keyStream.get(tailPath); + } + }, + + value: function () { + if (this.state === "clean") { + return this.cache; + } else if (this.state === "dirty") { + this.state = "clean"; + return this.cache = this.valueFn(); + } + // TODO: Ensure value is never called on a destroyed stream + // so that we can uncomment this assertion. + // + // Ember.assert("Stream error: value was called in an invalid state: " + this.state); + }, + + valueFn: function () { + throw new Error("Stream error: valueFn not implemented"); + }, + + setValue: function () { + throw new Error("Stream error: setValue not implemented"); + }, + + notify: function () { + this.notifyExcept(); + }, + + notifyExcept: function (callbackToSkip, contextToSkip) { + if (this.state === "clean") { + this.state = "dirty"; + this._notifySubscribers(callbackToSkip, contextToSkip); + } + }, + + subscribe: function (callback, context) { + var subscriber = new Subscriber(callback, context, this); + if (this.subscriberHead === null) { + this.subscriberHead = this.subscriberTail = subscriber; + } else { + var tail = this.subscriberTail; + tail.next = subscriber; + subscriber.prev = tail; + this.subscriberTail = subscriber; + } + + var stream = this; + return function () { + subscriber.removeFrom(stream); + }; + }, + + unsubscribe: function (callback, context) { + var subscriber = this.subscriberHead; + + while (subscriber) { + var next = subscriber.next; + if (subscriber.callback === callback && subscriber.context === context) { + subscriber.removeFrom(this); + } + subscriber = next; + } + }, + + _notifySubscribers: function (callbackToSkip, contextToSkip) { + var subscriber = this.subscriberHead; + + while (subscriber) { + var next = subscriber.next; + + var callback = subscriber.callback; + var context = subscriber.context; + + subscriber = next; + + if (callback === callbackToSkip && context === contextToSkip) { + continue; + } + + if (context === undefined) { + callback(this); + } else { + callback.call(context, this); + } + } + }, + + destroy: function () { + if (this.state !== "destroyed") { + this.state = "destroyed"; + + var children = this.children; + for (var key in children) { + children[key].destroy(); + } + + this.subscriberHead = this.subscriberTail = null; + + return true; + } + }, + + isGlobal: function () { + var stream = this; + while (stream !== undefined) { + if (stream._isRoot) { + return stream._isGlobal; + } + stream = stream.source; + } + } + }; + + exports['default'] = Stream; + +}); +enifed('ember-metal/streams/stream_binding', ['exports', 'ember-metal/platform/create', 'ember-metal/merge', 'ember-metal/run_loop', 'ember-metal/streams/stream'], function (exports, create, merge, run, Stream) { + + 'use strict'; + + function StreamBinding(stream) { + Ember.assert("StreamBinding error: tried to bind to object that is not a stream", stream && stream.isStream); + + this.init(); + this.stream = stream; + this.senderCallback = undefined; + this.senderContext = undefined; + this.senderValue = undefined; + + stream.subscribe(this._onNotify, this); + } + + StreamBinding.prototype = create['default'](Stream['default'].prototype); + + merge['default'](StreamBinding.prototype, { + valueFn: function () { + return this.stream.value(); + }, + + _onNotify: function () { + this._scheduleSync(undefined, undefined, this); + }, + + setValue: function (value, callback, context) { + this._scheduleSync(value, callback, context); + }, + + _scheduleSync: function (value, callback, context) { + if (this.senderCallback === undefined && this.senderContext === undefined) { + this.senderCallback = callback; + this.senderContext = context; + this.senderValue = value; + run['default'].schedule("sync", this, this._sync); + } else if (this.senderContext !== this) { + this.senderCallback = callback; + this.senderContext = context; + this.senderValue = value; + } + }, + + _sync: function () { + if (this.state === "destroyed") { + return; + } + + if (this.senderContext !== this) { + this.stream.setValue(this.senderValue); + } + + var senderCallback = this.senderCallback; + var senderContext = this.senderContext; + this.senderCallback = undefined; + this.senderContext = undefined; + this.senderValue = undefined; + + // Force StreamBindings to always notify + this.state = "clean"; + + this.notifyExcept(senderCallback, senderContext); + }, + + _super$destroy: Stream['default'].prototype.destroy, + + destroy: function () { + if (this._super$destroy()) { + this.stream.unsubscribe(this._onNotify, this); + return true; + } + } + }); + + exports['default'] = StreamBinding; + +}); +enifed('ember-metal/streams/utils', ['exports', './stream'], function (exports, Stream) { + + 'use strict'; + + exports.isStream = isStream; + exports.subscribe = subscribe; + exports.unsubscribe = unsubscribe; + exports.read = read; + exports.readArray = readArray; + exports.readHash = readHash; + exports.scanArray = scanArray; + exports.scanHash = scanHash; + exports.concat = concat; + exports.chain = chain; + + function isStream(object) { + return object && object.isStream; + } + + /* + A method of subscribing to a stream which is safe for use with a non-stream + object. If a non-stream object is passed, the function does nothing. + + @public + @for Ember.stream + @function subscribe + @param {Object|Stream} object object or stream to potentially subscribe to + @param {Function} callback function to run when stream value changes + @param {Object} [context] the callback will be executed with this context if it + is provided + */ + function subscribe(object, callback, context) { + if (object && object.isStream) { + object.subscribe(callback, context); + } + } + + /* + A method of unsubscribing from a stream which is safe for use with a non-stream + object. If a non-stream object is passed, the function does nothing. + + @public + @for Ember.stream + @function unsubscribe + @param {Object|Stream} object object or stream to potentially unsubscribe from + @param {Function} callback function originally passed to `subscribe()` + @param {Object} [context] object originally passed to `subscribe()` + */ + function unsubscribe(object, callback, context) { + if (object && object.isStream) { + object.unsubscribe(callback, context); + } + } + + /* + Retrieve the value of a stream, or in the case a non-stream object is passed, + return the object itself. + + @public + @for Ember.stream + @function read + @param {Object|Stream} object object to return the value of + @return the stream's current value, or the non-stream object itself + */ + function read(object) { + if (object && object.isStream) { + return object.value(); + } else { + return object; + } + } + + /* + Map an array, replacing any streams with their values. + + @public + @for Ember.stream + @function readArray + @param {Array} array The array to read values from + @return {Array} a new array of the same length with the values of non-stream + objects mapped from their original positions untouched, and + the values of stream objects retaining their original position + and replaced with the stream's current value. + */ + function readArray(array) { + var length = array.length; + var ret = new Array(length); + for (var i = 0; i < length; i++) { + ret[i] = read(array[i]); + } + return ret; + } + + /* + Map a hash, replacing any stream property values with the current value of that + stream. + + @public + @for Ember.stream + @function readHash + @param {Object} object The hash to read keys and values from + @return {Object} a new object with the same keys as the passed object. The + property values in the new object are the original values in + the case of non-stream objects, and the streams' current + values in the case of stream objects. + */ + function readHash(object) { + var ret = {}; + for (var key in object) { + ret[key] = read(object[key]); + } + return ret; + } + + /* + Check whether an array contains any stream values + + @public + @for Ember.stream + @function scanArray + @param {Array} array array given to a handlebars helper + @return {Boolean} `true` if the array contains a stream/bound value, `false` + otherwise + */ + function scanArray(array) { + var length = array.length; + var containsStream = false; + + for (var i = 0; i < length; i++) { + if (isStream(array[i])) { + containsStream = true; + break; + } + } + + return containsStream; + } + + /* + Check whether a hash has any stream property values + + @public + @for Ember.stream + @function scanHash + @param {Object} hash "hash" argument given to a handlebars helper + @return {Boolean} `true` if the object contains a stream/bound value, `false` + otherwise + */ + function scanHash(hash) { + var containsStream = false; + + for (var prop in hash) { + if (isStream(hash[prop])) { + containsStream = true; + break; + } + } + + return containsStream; + } + + /* + Join an array, with any streams replaced by their current values + + @public + @for Ember.stream + @function concat + @param {Array} array An array containing zero or more stream objects and + zero or more non-stream objects + @param {String} separator string to be used to join array elements + @return {String} String with array elements concatenated and joined by the + provided separator, and any stream array members having been + replaced by the current value of the stream + */ + function concat(array, separator) { + // TODO: Create subclass ConcatStream < Stream. Defer + // subscribing to streams until the value() is called. + var hasStream = scanArray(array); + if (hasStream) { + var i, l; + var stream = new Stream['default'](function () { + return readArray(array).join(separator); + }); + + for (i = 0, l = array.length; i < l; i++) { + subscribe(array[i], stream.notify, stream); + } + + return stream; + } else { + return array.join(separator); + } + } + + /* + Generate a new stream by providing a source stream and a function that can + be used to transform the stream's value. In the case of a non-stream object, + returns the result of the function. + + The value to transform would typically be available to the function you pass + to `chain()` via scope. For example: + + ```javascript + var source = ...; // stream returning a number + // or a numeric (non-stream) object + var result = chain(source, function() { + var currentValue = read(source); + return currentValue + 1; + }); + ``` + + In the example, result is a stream if source is a stream, or a number of + source was numeric. + + @public + @for Ember.stream + @function chain + @param {Object|Stream} value A stream or non-stream object + @param {Function} fn function to be run when the stream value changes, or to + be run once in the case of a non-stream object + @return {Object|Stream} In the case of a stream `value` parameter, a new + stream that will be updated with the return value of + the provided function `fn`. In the case of a + non-stream object, the return value of the provided + function `fn`. + */ + function chain(value, fn) { + if (isStream(value)) { + var stream = new Stream['default'](fn); + subscribe(value, stream.notify, stream); + return stream; + } else { + return fn(); + } + } + +}); +enifed('ember-metal/utils', ['exports', 'ember-metal/core', 'ember-metal/platform/create', 'ember-metal/platform/define_property', 'ember-metal/array'], function (exports, Ember, o_create, define_property, array) { + + + exports.uuid = uuid; + exports.generateGuid = generateGuid; + exports.guidFor = guidFor; + exports.getMeta = getMeta; + exports.setMeta = setMeta; + exports.metaPath = metaPath; + exports.wrap = wrap; + exports.makeArray = makeArray; + exports.tryInvoke = tryInvoke; + exports.inspect = inspect; + exports.apply = apply; + exports.applyStr = applyStr; + exports.meta = meta; + exports.typeOf = typeOf; + exports.isArray = isArray; + exports.canInvoke = canInvoke; + + "REMOVE_USE_STRICT: true"; /** + @module ember-metal + */ + + /** + Previously we used `Ember.$.uuid`, however `$.uuid` has been removed from + jQuery master. We'll just bootstrap our own uuid now. + + @private + @return {Number} the uuid + */ + var _uuid = 0; + + /** + Generates a universally unique identifier. This method + is used internally by Ember for assisting with + the generation of GUID's and other unique identifiers + such as `bind-attr` data attributes. + + @public + @return {Number} [description] + */ + function uuid() { + return ++_uuid; + } + + /** + Prefix used for guids through out Ember. + @private + @property GUID_PREFIX + @for Ember + @type String + @final + */ + var GUID_PREFIX = "ember"; + + // Used for guid generation... + var numberCache = []; + var stringCache = {}; + + /** + Strongly hint runtimes to intern the provided string. + + When do I need to use this function? + + For the most part, never. Pre-mature optimization is bad, and often the + runtime does exactly what you need it to, and more often the trade-off isn't + worth it. + + Why? + + Runtimes store strings in at least 2 different representations: + Ropes and Symbols (interned strings). The Rope provides a memory efficient + data-structure for strings created from concatenation or some other string + manipulation like splitting. + + Unfortunately checking equality of different ropes can be quite costly as + runtimes must resort to clever string comparison algorithms. These + algorithms typically cost in proportion to the length of the string. + Luckily, this is where the Symbols (interned strings) shine. As Symbols are + unique by their string content, equality checks can be done by pointer + comparison. + + How do I know if my string is a rope or symbol? + + Typically (warning general sweeping statement, but truthy in runtimes at + present) static strings created as part of the JS source are interned. + Strings often used for comparisons can be interned at runtime if some + criteria are met. One of these criteria can be the size of the entire rope. + For example, in chrome 38 a rope longer then 12 characters will not + intern, nor will segments of that rope. + + Some numbers: http://jsperf.com/eval-vs-keys/8 + + Known Trick™ + + @private + @return {String} interned version of the provided string + */ + function intern(str) { + var obj = {}; + obj[str] = 1; + for (var key in obj) { + if (key === str) { + return key; + } + } + return str; + } + + /** + A unique key used to assign guids and other private metadata to objects. + If you inspect an object in your browser debugger you will often see these. + They can be safely ignored. + + On browsers that support it, these properties are added with enumeration + disabled so they won't show up when you iterate over your properties. + + @private + @property GUID_KEY + @for Ember + @type String + @final + */ + var GUID_KEY = intern("__ember" + +new Date()); + + var GUID_DESC = { + writable: true, + configurable: true, + enumerable: false, + value: null + }; + + var undefinedDescriptor = { + configurable: true, + writable: true, + enumerable: false, + value: undefined + }; + + var nullDescriptor = { + configurable: true, + writable: true, + enumerable: false, + value: null + }; + + var META_DESC = { + writable: true, + configurable: true, + enumerable: false, + value: null + }; + + var EMBER_META_PROPERTY = { + name: "__ember_meta__", + descriptor: META_DESC + }; + + var GUID_KEY_PROPERTY = { + name: GUID_KEY, + descriptor: nullDescriptor + }; + + var NEXT_SUPER_PROPERTY = { + name: "__nextSuper", + descriptor: undefinedDescriptor + }; + + function generateGuid(obj, prefix) { + if (!prefix) { + prefix = GUID_PREFIX; + } + + var ret = prefix + uuid(); + if (obj) { + if (obj[GUID_KEY] === null) { + obj[GUID_KEY] = ret; + } else { + GUID_DESC.value = ret; + if (obj.__defineNonEnumerable) { + obj.__defineNonEnumerable(GUID_KEY_PROPERTY); + } else { + define_property.defineProperty(obj, GUID_KEY, GUID_DESC); + } + } + } + return ret; + } + + /** + Returns a unique id for the object. If the object does not yet have a guid, + one will be assigned to it. You can call this on any object, + `Ember.Object`-based or not, but be aware that it will add a `_guid` + property. + + You can also use this method on DOM Element objects. + + @private + @method guidFor + @for Ember + @param {Object} obj any object, string, number, Element, or primitive + @return {String} the unique guid for this instance. + */ + function guidFor(obj) { + + // special cases where we don't want to add a key to object + if (obj === undefined) { + return "(undefined)"; + } + + if (obj === null) { + return "(null)"; + } + + var ret; + var type = typeof obj; + + // Don't allow prototype changes to String etc. to change the guidFor + switch (type) { + case "number": + ret = numberCache[obj]; + + if (!ret) { + ret = numberCache[obj] = "nu" + obj; + } + + return ret; + + case "string": + ret = stringCache[obj]; + + if (!ret) { + ret = stringCache[obj] = "st" + uuid(); + } + + return ret; + + case "boolean": + return obj ? "(true)" : "(false)"; + + default: + if (obj[GUID_KEY]) { + return obj[GUID_KEY]; + } + + if (obj === Object) { + return "(Object)"; + } + + if (obj === Array) { + return "(Array)"; + } + + ret = GUID_PREFIX + uuid(); + + if (obj[GUID_KEY] === null) { + obj[GUID_KEY] = ret; + } else { + GUID_DESC.value = ret; + + if (obj.__defineNonEnumerable) { + obj.__defineNonEnumerable(GUID_KEY_PROPERTY); + } else { + define_property.defineProperty(obj, GUID_KEY, GUID_DESC); + } + } + return ret; + } + } + + // .......................................................... + // META + // + function Meta(obj) { + this.watching = {}; + this.cache = undefined; + this.cacheMeta = undefined; + this.source = obj; + this.deps = undefined; + this.listeners = undefined; + this.mixins = undefined; + this.bindings = undefined; + this.chains = undefined; + this.values = undefined; + this.proto = undefined; + } + + Meta.prototype = { + chainWatchers: null // FIXME + }; + + if (!define_property.canDefineNonEnumerableProperties) { + // on platforms that don't support enumerable false + // make meta fail jQuery.isPlainObject() to hide from + // jQuery.extend() by having a property that fails + // hasOwnProperty check. + Meta.prototype.__preventPlainObject__ = true; + + // Without non-enumerable properties, meta objects will be output in JSON + // unless explicitly suppressed + Meta.prototype.toJSON = function () {}; + } + + // Placeholder for non-writable metas. + var EMPTY_META = new Meta(null); + + + if (define_property.hasPropertyAccessors) { + EMPTY_META.values = {}; + } + + + /** + Retrieves the meta hash for an object. If `writable` is true ensures the + hash is writable for this object as well. + + The meta object contains information about computed property descriptors as + well as any watched properties and other information. You generally will + not access this information directly but instead work with higher level + methods that manipulate this hash indirectly. + + @method meta + @for Ember + @private + + @param {Object} obj The object to retrieve meta for + @param {Boolean} [writable=true] Pass `false` if you do not intend to modify + the meta hash, allowing the method to avoid making an unnecessary copy. + @return {Object} the meta hash for an object + */ + function meta(obj, writable) { + var ret = obj.__ember_meta__; + if (writable === false) { + return ret || EMPTY_META; + } + + if (!ret) { + if (define_property.canDefineNonEnumerableProperties) { + if (obj.__defineNonEnumerable) { + obj.__defineNonEnumerable(EMBER_META_PROPERTY); + } else { + define_property.defineProperty(obj, "__ember_meta__", META_DESC); + } + } + + ret = new Meta(obj); + + + if (define_property.hasPropertyAccessors) { + ret.values = {}; + } + + + obj.__ember_meta__ = ret; + } else if (ret.source !== obj) { + if (obj.__defineNonEnumerable) { + obj.__defineNonEnumerable(EMBER_META_PROPERTY); + } else { + define_property.defineProperty(obj, "__ember_meta__", META_DESC); + } + + ret = o_create['default'](ret); + ret.watching = o_create['default'](ret.watching); + ret.cache = undefined; + ret.cacheMeta = undefined; + ret.source = obj; + + + if (define_property.hasPropertyAccessors) { + ret.values = o_create['default'](ret.values); + } + + + obj["__ember_meta__"] = ret; + } + return ret; + } + function getMeta(obj, property) { + var _meta = meta(obj, false); + return _meta[property]; + } + + function setMeta(obj, property, value) { + var _meta = meta(obj, true); + _meta[property] = value; + return value; + } + + /** + @deprecated + @private + + In order to store defaults for a class, a prototype may need to create + a default meta object, which will be inherited by any objects instantiated + from the class's constructor. + + However, the properties of that meta object are only shallow-cloned, + so if a property is a hash (like the event system's `listeners` hash), + it will by default be shared across all instances of that class. + + This method allows extensions to deeply clone a series of nested hashes or + other complex objects. For instance, the event system might pass + `['listeners', 'foo:change', 'ember157']` to `prepareMetaPath`, which will + walk down the keys provided. + + For each key, if the key does not exist, it is created. If it already + exists and it was inherited from its constructor, the constructor's + key is cloned. + + You can also pass false for `writable`, which will simply return + undefined if `prepareMetaPath` discovers any part of the path that + shared or undefined. + + @method metaPath + @for Ember + @param {Object} obj The object whose meta we are examining + @param {Array} path An array of keys to walk down + @param {Boolean} writable whether or not to create a new meta + (or meta property) if one does not already exist or if it's + shared with its constructor + */ + function metaPath(obj, path, writable) { + Ember['default'].deprecate("Ember.metaPath is deprecated and will be removed from future releases."); + var _meta = meta(obj, writable); + var keyName, value; + + for (var i = 0, l = path.length; i < l; i++) { + keyName = path[i]; + value = _meta[keyName]; + + if (!value) { + if (!writable) { + return undefined; + } + value = _meta[keyName] = { __ember_source__: obj }; + } else if (value.__ember_source__ !== obj) { + if (!writable) { + return undefined; + } + value = _meta[keyName] = o_create['default'](value); + value.__ember_source__ = obj; + } + + _meta = value; + } + + return value; + } + + /** + Wraps the passed function so that `this._super` will point to the superFunc + when the function is invoked. This is the primitive we use to implement + calls to super. + + @private + @method wrap + @for Ember + @param {Function} func The function to call + @param {Function} superFunc The super function. + @return {Function} wrapped function. + */ + function wrap(func, superFunc) { + function superWrapper() { + var ret; + var sup = this && this.__nextSuper; + var length = arguments.length; + + if (this) { + this.__nextSuper = superFunc; + } + + if (length === 0) { + ret = func.call(this); + } else if (length === 1) { + ret = func.call(this, arguments[0]); + } else if (length === 2) { + ret = func.call(this, arguments[0], arguments[1]); + } else { + var args = new Array(length); + for (var i = 0; i < length; i++) { + args[i] = arguments[i]; + } + ret = apply(this, func, args); + } + + if (this) { + this.__nextSuper = sup; + } + + return ret; + } + + superWrapper.wrappedFunction = func; + superWrapper.__ember_observes__ = func.__ember_observes__; + superWrapper.__ember_observesBefore__ = func.__ember_observesBefore__; + superWrapper.__ember_listens__ = func.__ember_listens__; + + return superWrapper; + } + + var EmberArray; + + /** + Returns true if the passed object is an array or Array-like. + + Ember Array Protocol: + + - the object has an objectAt property + - the object is a native Array + - the object is an Object, and has a length property + + Unlike `Ember.typeOf` this method returns true even if the passed object is + not formally array but appears to be array-like (i.e. implements `Ember.Array`) + + ```javascript + Ember.isArray(); // false + Ember.isArray([]); // true + Ember.isArray(Ember.ArrayProxy.create({ content: [] })); // true + ``` + + @method isArray + @for Ember + @param {Object} obj The object to test + @return {Boolean} true if the passed object is an array or Array-like + */ + // ES6TODO: Move up to runtime? This is only use in ember-metal by concatenatedProperties + function isArray(obj) { + var modulePath, type; + + if (typeof EmberArray === "undefined") { + modulePath = "ember-runtime/mixins/array"; + if (Ember['default'].__loader.registry[modulePath]) { + EmberArray = Ember['default'].__loader.require(modulePath)["default"]; + } + } + + if (!obj || obj.setInterval) { + return false; + } + if (Array.isArray && Array.isArray(obj)) { + return true; + } + if (EmberArray && EmberArray.detect(obj)) { + return true; + } + + type = typeOf(obj); + if ("array" === type) { + return true; + } + if (obj.length !== undefined && "object" === type) { + return true; + } + return false; + } + + /** + Forces the passed object to be part of an array. If the object is already + an array or array-like, it will return the object. Otherwise, it will add the object to + an array. If obj is `null` or `undefined`, it will return an empty array. + + ```javascript + Ember.makeArray(); // [] + Ember.makeArray(null); // [] + Ember.makeArray(undefined); // [] + Ember.makeArray('lindsay'); // ['lindsay'] + Ember.makeArray([1, 2, 42]); // [1, 2, 42] + + var controller = Ember.ArrayProxy.create({ content: [] }); + + Ember.makeArray(controller) === controller; // true + ``` + + @method makeArray + @for Ember + @param {Object} obj the object + @return {Array} + */ + function makeArray(obj) { + if (obj === null || obj === undefined) { + return []; + } + return isArray(obj) ? obj : [obj]; + } + + /** + Checks to see if the `methodName` exists on the `obj`. + + ```javascript + var foo = { bar: function() { return 'bar'; }, baz: null }; + + Ember.canInvoke(foo, 'bar'); // true + Ember.canInvoke(foo, 'baz'); // false + Ember.canInvoke(foo, 'bat'); // false + ``` + + @method canInvoke + @for Ember + @param {Object} obj The object to check for the method + @param {String} methodName The method name to check for + @return {Boolean} + */ + function canInvoke(obj, methodName) { + return !!(obj && typeof obj[methodName] === "function"); + } + + /** + Checks to see if the `methodName` exists on the `obj`, + and if it does, invokes it with the arguments passed. + + ```javascript + var d = new Date('03/15/2013'); + + Ember.tryInvoke(d, 'getTime'); // 1363320000000 + Ember.tryInvoke(d, 'setFullYear', [2014]); // 1394856000000 + Ember.tryInvoke(d, 'noSuchMethod', [2014]); // undefined + ``` + + @method tryInvoke + @for Ember + @param {Object} obj The object to check for the method + @param {String} methodName The method name to check for + @param {Array} [args] The arguments to pass to the method + @return {*} the return value of the invoked method or undefined if it cannot be invoked + */ + function tryInvoke(obj, methodName, args) { + if (canInvoke(obj, methodName)) { + return args ? applyStr(obj, methodName, args) : applyStr(obj, methodName); + } + } + + // https://github.com/emberjs/ember.js/pull/1617 + var needsFinallyFix = (function () { + var count = 0; + try { + // jscs:disable + try {} finally { + count++; + throw new Error("needsFinallyFixTest"); + } + // jscs:enable + } catch (e) {} + + return count !== 1; + })(); + + /** + Provides try/finally functionality, while working + around Safari's double finally bug. + + ```javascript + var tryable = function() { + someResource.lock(); + runCallback(); // May throw error. + }; + + var finalizer = function() { + someResource.unlock(); + }; + + Ember.tryFinally(tryable, finalizer); + ``` + + @method tryFinally + @deprecated Use JavaScript's native try/finally + @for Ember + @param {Function} tryable The function to run the try callback + @param {Function} finalizer The function to run the finally callback + @param {Object} [binding] The optional calling object. Defaults to 'this' + @return {*} The return value is the that of the finalizer, + unless that value is undefined, in which case it is the return value + of the tryable + */ + + var tryFinally; + if (needsFinallyFix) { + tryFinally = function (tryable, finalizer, binding) { + var result, finalResult, finalError; + + binding = binding || this; + + try { + result = tryable.call(binding); + } finally { + try { + finalResult = finalizer.call(binding); + } catch (e) { + finalError = e; + } + } + + if (finalError) { + throw finalError; + } + + return finalResult === undefined ? result : finalResult; + }; + } else { + tryFinally = function (tryable, finalizer, binding) { + var result, finalResult; + + binding = binding || this; + + try { + result = tryable.call(binding); + } finally { + finalResult = finalizer.call(binding); + } + + return finalResult === undefined ? result : finalResult; + }; + } + + var deprecatedTryFinally = function () { + Ember['default'].deprecate("tryFinally is deprecated. Please use JavaScript's native try/finally.", false); + return tryFinally.apply(this, arguments); + }; + + /** + Provides try/catch/finally functionality, while working + around Safari's double finally bug. + + ```javascript + var tryable = function() { + for (i = 0, l = listeners.length; i < l; i++) { + listener = listeners[i]; + beforeValues[i] = listener.before(name, time(), payload); + } + + return callback.call(binding); + }; + + var catchable = function(e) { + payload = payload || {}; + payload.exception = e; + }; + + var finalizer = function() { + for (i = 0, l = listeners.length; i < l; i++) { + listener = listeners[i]; + listener.after(name, time(), payload, beforeValues[i]); + } + }; + + Ember.tryCatchFinally(tryable, catchable, finalizer); + ``` + + @method tryCatchFinally + @deprecated Use JavaScript's native try/catch/finally instead + @for Ember + @param {Function} tryable The function to run the try callback + @param {Function} catchable The function to run the catchable callback + @param {Function} finalizer The function to run the finally callback + @param {Object} [binding] The optional calling object. Defaults to 'this' + @return {*} The return value is the that of the finalizer, + unless that value is undefined, in which case it is the return value + of the tryable. + */ + var tryCatchFinally; + if (needsFinallyFix) { + tryCatchFinally = function (tryable, catchable, finalizer, binding) { + var result, finalResult, finalError; + + binding = binding || this; + + try { + result = tryable.call(binding); + } catch (error) { + result = catchable.call(binding, error); + } finally { + try { + finalResult = finalizer.call(binding); + } catch (e) { + finalError = e; + } + } + + if (finalError) { + throw finalError; + } + + return finalResult === undefined ? result : finalResult; + }; + } else { + tryCatchFinally = function (tryable, catchable, finalizer, binding) { + var result, finalResult; + + binding = binding || this; + + try { + result = tryable.call(binding); + } catch (error) { + result = catchable.call(binding, error); + } finally { + finalResult = finalizer.call(binding); + } + + return finalResult === undefined ? result : finalResult; + }; + } + + var deprecatedTryCatchFinally = function () { + Ember['default'].deprecate("tryCatchFinally is deprecated. Please use JavaScript's native try/catch/finally.", false); + return tryCatchFinally.apply(this, arguments); + }; + + // ........................................ + // TYPING & ARRAY MESSAGING + // + + var TYPE_MAP = {}; + var t = "Boolean Number String Function Array Date RegExp Object".split(" "); + array.forEach.call(t, function (name) { + TYPE_MAP["[object " + name + "]"] = name.toLowerCase(); + }); + + var toString = Object.prototype.toString; + + var EmberObject; + + /** + Returns a consistent type for the passed item. + + Use this instead of the built-in `typeof` to get the type of an item. + It will return the same result across all browsers and includes a bit + more detail. Here is what will be returned: + + | Return Value | Meaning | + |---------------|------------------------------------------------------| + | 'string' | String primitive or String object. | + | 'number' | Number primitive or Number object. | + | 'boolean' | Boolean primitive or Boolean object. | + | 'null' | Null value | + | 'undefined' | Undefined value | + | 'function' | A function | + | 'array' | An instance of Array | + | 'regexp' | An instance of RegExp | + | 'date' | An instance of Date | + | 'class' | An Ember class (created using Ember.Object.extend()) | + | 'instance' | An Ember object instance | + | 'error' | An instance of the Error object | + | 'object' | A JavaScript object not inheriting from Ember.Object | + + Examples: + + ```javascript + Ember.typeOf(); // 'undefined' + Ember.typeOf(null); // 'null' + Ember.typeOf(undefined); // 'undefined' + Ember.typeOf('michael'); // 'string' + Ember.typeOf(new String('michael')); // 'string' + Ember.typeOf(101); // 'number' + Ember.typeOf(new Number(101)); // 'number' + Ember.typeOf(true); // 'boolean' + Ember.typeOf(new Boolean(true)); // 'boolean' + Ember.typeOf(Ember.makeArray); // 'function' + Ember.typeOf([1, 2, 90]); // 'array' + Ember.typeOf(/abc/); // 'regexp' + Ember.typeOf(new Date()); // 'date' + Ember.typeOf(Ember.Object.extend()); // 'class' + Ember.typeOf(Ember.Object.create()); // 'instance' + Ember.typeOf(new Error('teamocil')); // 'error' + + // 'normal' JavaScript object + Ember.typeOf({ a: 'b' }); // 'object' + ``` + + @method typeOf + @for Ember + @param {Object} item the item to check + @return {String} the type + */ + function typeOf(item) { + var ret, modulePath; + + // ES6TODO: Depends on Ember.Object which is defined in runtime. + if (typeof EmberObject === "undefined") { + modulePath = "ember-runtime/system/object"; + if (Ember['default'].__loader.registry[modulePath]) { + EmberObject = Ember['default'].__loader.require(modulePath)["default"]; + } + } + + ret = item === null || item === undefined ? String(item) : TYPE_MAP[toString.call(item)] || "object"; + + if (ret === "function") { + if (EmberObject && EmberObject.detect(item)) { + ret = "class"; + } + } else if (ret === "object") { + if (item instanceof Error) { + ret = "error"; + } else if (EmberObject && item instanceof EmberObject) { + ret = "instance"; + } else if (item instanceof Date) { + ret = "date"; + } + } + + return ret; + } + + /** + Convenience method to inspect an object. This method will attempt to + convert the object into a useful string description. + + It is a pretty simple implementation. If you want something more robust, + use something like JSDump: https://github.com/NV/jsDump + + @method inspect + @for Ember + @param {Object} obj The object you want to inspect. + @return {String} A description of the object + @since 1.4.0 + */ + function inspect(obj) { + var type = typeOf(obj); + if (type === "array") { + return "[" + obj + "]"; + } + if (type !== "object") { + return obj + ""; + } + + var v; + var ret = []; + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + v = obj[key]; + if (v === "toString") { + continue; + } // ignore useless items + if (typeOf(v) === "function") { + v = "function() { ... }"; + } + + if (v && typeof v.toString !== "function") { + ret.push(key + ": " + toString.call(v)); + } else { + ret.push(key + ": " + v); + } + } + } + return "{" + ret.join(", ") + "}"; + } + + // The following functions are intentionally minified to keep the functions + // below Chrome's function body size inlining limit of 600 chars. + /** + @param {Object} target + @param {Function} method + @param {Array} args + */ + function apply(t, m, a) { + var l = a && a.length; + if (!a || !l) { + return m.call(t); + } + switch (l) { + case 1: + return m.call(t, a[0]); + case 2: + return m.call(t, a[0], a[1]); + case 3: + return m.call(t, a[0], a[1], a[2]); + case 4: + return m.call(t, a[0], a[1], a[2], a[3]); + case 5: + return m.call(t, a[0], a[1], a[2], a[3], a[4]); + default: + return m.apply(t, a); + } + } + + /** + @param {Object} target + @param {String} method + @param {Array} args + */ + function applyStr(t, m, a) { + var l = a && a.length; + if (!a || !l) { + return t[m](); + } + switch (l) { + case 1: + return t[m](a[0]); + case 2: + return t[m](a[0], a[1]); + case 3: + return t[m](a[0], a[1], a[2]); + case 4: + return t[m](a[0], a[1], a[2], a[3]); + case 5: + return t[m](a[0], a[1], a[2], a[3], a[4]); + default: + return t[m].apply(t, a); + } + } + + exports.GUID_DESC = GUID_DESC; + exports.EMBER_META_PROPERTY = EMBER_META_PROPERTY; + exports.GUID_KEY_PROPERTY = GUID_KEY_PROPERTY; + exports.NEXT_SUPER_PROPERTY = NEXT_SUPER_PROPERTY; + exports.GUID_KEY = GUID_KEY; + exports.META_DESC = META_DESC; + exports.EMPTY_META = EMPTY_META; + exports.tryCatchFinally = tryCatchFinally; + exports.deprecatedTryCatchFinally = deprecatedTryCatchFinally; + exports.tryFinally = tryFinally; + exports.deprecatedTryFinally = deprecatedTryFinally; + +}); +enifed('ember-metal/watch_key', ['exports', 'ember-metal/core', 'ember-metal/utils', 'ember-metal/platform/define_property', 'ember-metal/properties'], function (exports, Ember, utils, define_property, properties) { + + 'use strict'; + + exports.watchKey = watchKey; + exports.unwatchKey = unwatchKey; + + function watchKey(obj, keyName, meta) { + // can't watch length on Array - it is special... + if (keyName === "length" && utils.typeOf(obj) === "array") { + return; + } + + var m = meta || utils.meta(obj); + var watching = m.watching; + + // activate watching first time + if (!watching[keyName]) { + watching[keyName] = 1; + + var possibleDesc = obj[keyName]; + var desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + if (desc && desc.willWatch) { + desc.willWatch(obj, keyName); + } + + if ("function" === typeof obj.willWatchProperty) { + obj.willWatchProperty(keyName); + } + + + if (define_property.hasPropertyAccessors) { + handleMandatorySetter(m, obj, keyName); + } + + } else { + watching[keyName] = (watching[keyName] || 0) + 1; + } + } + + + var handleMandatorySetter = function handleMandatorySetter(m, obj, keyName) { + var descriptor = Object.getOwnPropertyDescriptor && Object.getOwnPropertyDescriptor(obj, keyName); + var configurable = descriptor ? descriptor.configurable : true; + var isWritable = descriptor ? descriptor.writable : true; + var hasValue = descriptor ? "value" in descriptor : true; + var possibleDesc = descriptor && descriptor.value; + var isDescriptor = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor; + + if (isDescriptor) { + return; + } + + // this x in Y deopts, so keeping it in this function is better; + if (configurable && isWritable && hasValue && keyName in obj) { + m.values[keyName] = obj[keyName]; + define_property.defineProperty(obj, keyName, { + configurable: true, + enumerable: Object.prototype.propertyIsEnumerable.call(obj, keyName), + set: properties.MANDATORY_SETTER_FUNCTION(keyName), + get: properties.DEFAULT_GETTER_FUNCTION(keyName) + }); + } + }; + + + // This is super annoying, but required until + // https://github.com/babel/babel/issues/906 is resolved + ; // jshint ignore:line + + function unwatchKey(obj, keyName, meta) { + var m = meta || utils.meta(obj); + var watching = m.watching; + + if (watching[keyName] === 1) { + watching[keyName] = 0; + + var possibleDesc = obj[keyName]; + var desc = possibleDesc !== null && typeof possibleDesc === "object" && possibleDesc.isDescriptor ? possibleDesc : undefined; + if (desc && desc.didUnwatch) { + desc.didUnwatch(obj, keyName); + } + + if ("function" === typeof obj.didUnwatchProperty) { + obj.didUnwatchProperty(keyName); + } + + + if (!desc && define_property.hasPropertyAccessors && keyName in obj) { + define_property.defineProperty(obj, keyName, { + configurable: true, + enumerable: Object.prototype.propertyIsEnumerable.call(obj, keyName), + set: function (val) { + // redefine to set as enumerable + define_property.defineProperty(obj, keyName, { + configurable: true, + writable: true, + enumerable: true, + value: val + }); + delete m.values[keyName]; + }, + get: properties.DEFAULT_GETTER_FUNCTION(keyName) + }); + } + + } else if (watching[keyName] > 1) { + watching[keyName]--; + } + } + +}); +enifed('ember-metal/watch_path', ['exports', 'ember-metal/utils', 'ember-metal/chains'], function (exports, utils, chains) { + + 'use strict'; + + exports.watchPath = watchPath; + exports.unwatchPath = unwatchPath; + + function chainsFor(obj, meta) { + var m = meta || utils.meta(obj); + var ret = m.chains; + if (!ret) { + ret = m.chains = new chains.ChainNode(null, null, obj); + } else if (ret.value() !== obj) { + ret = m.chains = ret.copy(obj); + } + return ret; + } + function watchPath(obj, keyPath, meta) { + // can't watch length on Array - it is special... + if (keyPath === "length" && utils.typeOf(obj) === "array") { + return; + } + + var m = meta || utils.meta(obj); + var watching = m.watching; + + if (!watching[keyPath]) { + // activate watching first time + watching[keyPath] = 1; + chainsFor(obj, m).add(keyPath); + } else { + watching[keyPath] = (watching[keyPath] || 0) + 1; + } + } + + function unwatchPath(obj, keyPath, meta) { + var m = meta || utils.meta(obj); + var watching = m.watching; + + if (watching[keyPath] === 1) { + watching[keyPath] = 0; + chainsFor(obj, m).remove(keyPath); + } else if (watching[keyPath] > 1) { + watching[keyPath]--; + } + } + +}); +enifed('ember-metal/watching', ['exports', 'ember-metal/utils', 'ember-metal/chains', 'ember-metal/watch_key', 'ember-metal/watch_path', 'ember-metal/path_cache'], function (exports, utils, chains, watch_key, watch_path, path_cache) { + + 'use strict'; + + exports.isWatching = isWatching; + exports.unwatch = unwatch; + exports.destroy = destroy; + exports.watch = watch; + + function watch(obj, _keyPath, m) { + // can't watch length on Array - it is special... + if (_keyPath === "length" && utils.typeOf(obj) === "array") { + return; + } + + if (!path_cache.isPath(_keyPath)) { + watch_key.watchKey(obj, _keyPath, m); + } else { + watch_path.watchPath(obj, _keyPath, m); + } + } + + function isWatching(obj, key) { + var meta = obj["__ember_meta__"]; + return (meta && meta.watching[key]) > 0; + } + + watch.flushPending = chains.flushPendingChains; + function unwatch(obj, _keyPath, m) { + // can't watch length on Array - it is special... + if (_keyPath === "length" && utils.typeOf(obj) === "array") { + return; + } + + if (!path_cache.isPath(_keyPath)) { + watch_key.unwatchKey(obj, _keyPath, m); + } else { + watch_path.unwatchPath(obj, _keyPath, m); + } + } + + var NODE_STACK = []; + + /** + Tears down the meta on an object so that it can be garbage collected. + Multiple calls will have no effect. + + @method destroy + @for Ember + @param {Object} obj the object to destroy + @return {void} + */ + function destroy(obj) { + var meta = obj["__ember_meta__"]; + var node, nodes, key, nodeObject; + + if (meta) { + obj["__ember_meta__"] = null; + // remove chainWatchers to remove circular references that would prevent GC + node = meta.chains; + if (node) { + NODE_STACK.push(node); + // process tree + while (NODE_STACK.length > 0) { + node = NODE_STACK.pop(); + // push children + nodes = node._chains; + if (nodes) { + for (key in nodes) { + if (nodes.hasOwnProperty(key)) { + NODE_STACK.push(nodes[key]); + } + } + } + // remove chainWatcher in node object + if (node._watching) { + nodeObject = node._object; + if (nodeObject) { + chains.removeChainWatcher(nodeObject, node._key, node); + } + } + } + } + } + } + +}); +enifed('ember-template-compiler', ['exports', 'ember-metal/core', 'ember-template-compiler/system/precompile', 'ember-template-compiler/system/compile', 'ember-template-compiler/system/template', 'ember-template-compiler/plugins', 'ember-template-compiler/plugins/transform-each-in-to-hash', 'ember-template-compiler/plugins/transform-with-as-to-hash', 'ember-template-compiler/compat'], function (exports, _Ember, precompile, compile, template, plugins, TransformEachInToHash, TransformWithAsToHash) { + + 'use strict'; + + plugins.registerPlugin("ast", TransformWithAsToHash['default']); + plugins.registerPlugin("ast", TransformEachInToHash['default']); + + exports._Ember = _Ember['default']; + exports.precompile = precompile['default']; + exports.compile = compile['default']; + exports.template = template['default']; + exports.registerPlugin = plugins.registerPlugin; }); enifed('ember-template-compiler/compat', ['ember-metal/core', 'ember-template-compiler/compat/precompile', 'ember-template-compiler/system/compile', 'ember-template-compiler/system/template'], function (Ember, precompile, compile, template) { @@ -355,7 +10095,7 @@ enifed('ember-template-compiler/compat/precompile', ['exports', 'ember-template- */ var compile, compileSpec; - exports['default'] = function(string) { + exports['default'] = function (string) { if ((!compile || !compileSpec) && Ember.__loader.registry['htmlbars-compiler/compiler']) { var Compiler = requireModule('htmlbars-compiler/compiler'); @@ -380,15 +10120,6 @@ enifed('ember-template-compiler/plugins', ['exports'], function (exports) { exports.registerPlugin = registerPlugin; - /** - @module ember - @submodule ember-template-compiler - */ - - /** - @private - @property helpers - */ var plugins = { ast: [] }; @@ -419,7 +10150,6 @@ enifed('ember-template-compiler/plugins/transform-each-in-to-hash', ['exports'], @submodule ember-htmlbars */ - /** An HTMLBars AST transformation that replaces all instances of @@ -438,9 +10168,10 @@ enifed('ember-template-compiler/plugins/transform-each-in-to-hash', ['exports'], @class TransformEachInToHash @private */ - function TransformEachInToHash() { + function TransformEachInToHash(options) { // set later within HTMLBars to the syntax package this.syntax = null; + this.options = options || {}; } /** @@ -453,7 +10184,7 @@ enifed('ember-template-compiler/plugins/transform-each-in-to-hash', ['exports'], var walker = new pluginContext.syntax.Walker(); var b = pluginContext.syntax.builders; - walker.visit(ast, function(node) { + walker.visit(ast, function (node) { if (pluginContext.validate(node)) { if (node.program && node.program.blockParams.length) { @@ -468,10 +10199,7 @@ enifed('ember-template-compiler/plugins/transform-each-in-to-hash', ['exports'], node.sexpr.hash = b.hash(); } - node.sexpr.hash.pairs.push(b.pair( - 'keyword', - b.string(keyword) - )); + node.sexpr.hash.pairs.push(b.pair('keyword', b.string(keyword))); } }); @@ -479,11 +10207,7 @@ enifed('ember-template-compiler/plugins/transform-each-in-to-hash', ['exports'], }; TransformEachInToHash.prototype.validate = function TransformEachInToHash_validate(node) { - return (node.type === 'BlockStatement' || node.type === 'MustacheStatement') && - node.sexpr.path.original === 'each' && - node.sexpr.params.length === 3 && - node.sexpr.params[1].type === 'PathExpression' && - node.sexpr.params[1].original === 'in'; + return (node.type === 'BlockStatement' || node.type === 'MustacheStatement') && node.sexpr.path.original === 'each' && node.sexpr.params.length === 3 && node.sexpr.params[1].type === 'PathExpression' && node.sexpr.params[1].original === 'in'; }; exports['default'] = TransformEachInToHash; @@ -516,9 +10240,10 @@ enifed('ember-template-compiler/plugins/transform-with-as-to-hash', ['exports'], @private @class TransformWithAsToHash */ - function TransformWithAsToHash() { + function TransformWithAsToHash(options) { // set later within HTMLBars to the syntax package this.syntax = null; + this.options = options; } /** @@ -529,14 +10254,17 @@ enifed('ember-template-compiler/plugins/transform-with-as-to-hash', ['exports'], TransformWithAsToHash.prototype.transform = function TransformWithAsToHash_transform(ast) { var pluginContext = this; var walker = new pluginContext.syntax.Walker(); + var moduleName = this.options.moduleName; - walker.visit(ast, function(node) { + walker.visit(ast, function (node) { if (pluginContext.validate(node)) { if (node.program && node.program.blockParams.length) { - throw new Error('You cannot use keyword (`{{with foo as bar}}`) and block params (`{{with foo as |bar|}}`) at the same time.'); + throw new Error("You cannot use keyword (`{{with foo as bar}}`) and block params (`{{with foo as |bar|}}`) at the same time."); } + Ember.deprecate("Using {{with}} without block syntax is deprecated. " + "Please use standard block form (`{{#with foo as |bar|}}`) " + (moduleName ? " in `" + moduleName + "` " : "") + "instead.", false, { url: "http://emberjs.com/deprecations/v1.x/#toc_code-as-code-sytnax-for-code-with-code" }); + var removedParams = node.sexpr.params.splice(1, 2); var keyword = removedParams[1].original; node.program.blockParams = [keyword]; @@ -547,11 +10275,7 @@ enifed('ember-template-compiler/plugins/transform-with-as-to-hash', ['exports'], }; TransformWithAsToHash.prototype.validate = function TransformWithAsToHash_validate(node) { - return node.type === 'BlockStatement' && - node.sexpr.path.original === 'with' && - node.sexpr.params.length === 3 && - node.sexpr.params[1].type === 'PathExpression' && - node.sexpr.params[1].original === 'as'; + return node.type === "BlockStatement" && node.sexpr.path.original === "with" && node.sexpr.params.length === 3 && node.sexpr.params[1].type === "PathExpression" && node.sexpr.params[1].original === "as"; }; exports['default'] = TransformWithAsToHash; @@ -561,22 +10285,27 @@ enifed('ember-template-compiler/system/compile', ['exports', 'ember-template-com 'use strict'; - /** - @module ember - @submodule ember-template-compiler - */ - var compile; - exports['default'] = function(templateString) { - if (!compile && Ember.__loader.registry['htmlbars-compiler/compiler']) { - compile = requireModule('htmlbars-compiler/compiler').compile; + var compile; /** + Uses HTMLBars `compile` function to process a string into a compiled template. + + This is not present in production builds. + + @private + @method compile + @param {String} templateString This is the string to be compiled by HTMLBars. + @param {Object} options This is an options hash to augment the compiler options. + */ + exports['default'] = function (templateString, options) { + if (!compile && Ember.__loader.registry["htmlbars-compiler/compiler"]) { + compile = requireModule("htmlbars-compiler/compiler").compile; } if (!compile) { - throw new Error('Cannot call `compile` without the template compiler loaded. Please load `ember-template-compiler.js` prior to calling `compile`.'); + throw new Error("Cannot call `compile` without the template compiler loaded. Please load `ember-template-compiler.js` prior to calling `compile`."); } - var templateSpec = compile(templateString, compileOptions['default']()); + var templateSpec = compile(templateString, compileOptions['default'](options)); return template['default'](templateSpec); } @@ -591,16 +10320,22 @@ enifed('ember-template-compiler/system/compile_options', ['exports', 'ember-meta @submodule ember-template-compiler */ - exports['default'] = function() { + exports['default'] = function (_options) { var disableComponentGeneration = true; - return { - revision: 'Ember@1.11.3', + var options = _options || {}; + // When calling `Ember.Handlebars.compile()` a second argument of `true` + // had a special meaning (long since lost), this just gaurds against + // `options` being true, and causing an error during compilation. + if (options === true) { + options = {}; + } - disableComponentGeneration: disableComponentGeneration, + options.revision = "Ember@1.12.1"; + options.disableComponentGeneration = disableComponentGeneration; + options.plugins = plugins['default']; - plugins: plugins['default'] - }; + return options; } }); @@ -625,7 +10360,7 @@ enifed('ember-template-compiler/system/precompile', ['exports', 'ember-template- @method precompile @param {String} templateString This is the string to be compiled by HTMLBars. */ - exports['default'] = function(templateString) { + exports['default'] = function (templateString, options) { if (!compileSpec && Ember.__loader.registry['htmlbars-compiler/compiler']) { compileSpec = requireModule('htmlbars-compiler/compiler').compileSpec; } @@ -634,7 +10369,7 @@ enifed('ember-template-compiler/system/precompile', ['exports', 'ember-template- throw new Error('Cannot call `compileSpec` without the template compiler loaded. Please load `ember-template-compiler.js` prior to calling `compileSpec`.'); } - return compileSpec(templateString, compileOptions['default']()); + return compileSpec(templateString, compileOptions['default'](options)); } }); @@ -656,7 +10391,7 @@ enifed('ember-template-compiler/system/template', ['exports'], function (exports @param {Function} templateSpec This is the compiled HTMLBars template spec. */ - exports['default'] = function(templateSpec) { + exports['default'] = function (templateSpec) { templateSpec.isTop = true; templateSpec.isMethod = false; @@ -1555,7 +11290,7 @@ enifed("htmlbars-compiler/template-compiler", function TemplateCompiler(options) { this.options = options || {}; - this.revision = this.options.revision || "HTMLBars@v0.11.2"; + this.revision = this.options.revision || "HTMLBars@v0.11.3"; this.fragmentOpcodeCompiler = new FragmentOpcodeCompiler(); this.fragmentCompiler = new FragmentJavaScriptCompiler(); this.hydrationOpcodeCompiler = new HydrationOpcodeCompiler(); @@ -3748,7 +13483,7 @@ enifed("htmlbars-syntax/parser", if (options && options.plugins && options.plugins.ast) { for (var i = 0, l = options.plugins.ast.length; i < l; i++) { - var plugin = new options.plugins.ast[i](); + var plugin = new options.plugins.ast[i](options); plugin.syntax = syntax; @@ -7347,10 +17082,11 @@ enifed("simple-html-tokenizer/utils", __exports__.preprocessInput = preprocessInput; }); +requireModule("ember-debug"); requireModule("ember-template-compiler"); })(); ; if (typeof exports === "object") { module.exports = Ember.__loader.require("ember-template-compiler"); - } \ No newline at end of file + }//# sourceMappingURL=ember-template-compiler.map \ No newline at end of file diff --git a/vendor/assets/javascripts/ember.custom.debug.js b/vendor/assets/javascripts/ember.custom.debug.js deleted file mode 100644 index 7cb4c446a4..0000000000 --- a/vendor/assets/javascripts/ember.custom.debug.js +++ /dev/null @@ -1,49400 +0,0 @@ -/*! - * @overview Ember - JavaScript Application Framework - * @copyright Copyright 2011-2015 Tilde Inc. and contributors - * Portions Copyright 2006-2011 Strobe Inc. - * Portions Copyright 2008-2011 Apple Inc. All rights reserved. - * @license Licensed under MIT license - * See https://raw.github.com/emberjs/ember.js/master/LICENSE - * @version 1.11.3 - */ - -(function() { -var enifed, requireModule, eriuqer, requirejs, Ember; -var mainContext = this; - -(function() { - - Ember = this.Ember = this.Ember || {}; - if (typeof Ember === 'undefined') { Ember = {}; }; - function UNDEFINED() { } - - if (typeof Ember.__loader === 'undefined') { - var registry = {}; - var seen = {}; - - enifed = function(name, deps, callback) { - var value = { }; - - if (!callback) { - value.deps = []; - value.callback = deps; - } else { - value.deps = deps; - value.callback = callback; - } - - registry[name] = value; - }; - - requirejs = eriuqer = requireModule = function(name) { - var s = seen[name]; - - if (s !== undefined) { return seen[name]; } - if (s === UNDEFINED) { return undefined; } - - seen[name] = {}; - - if (!registry[name]) { - throw new Error('Could not find module ' + name); - } - - var mod = registry[name]; - var deps = mod.deps; - var callback = mod.callback; - var reified = []; - var exports; - var length = deps.length; - - for (var i=0; i 3) { - args = new Array(length - 3); - for (var i = 3; i < length; i++) { - args[i-3] = arguments[i]; - } - } else { - args = undefined; - } - - if (!this.currentInstance) { createAutorun(this); } - return this.currentInstance.schedule(queueName, target, method, args, false, stack); - }, - - deferOnce: function(queueName, target, method /* , args */) { - if (!method) { - method = target; - target = null; - } - - if (isString(method)) { - method = target[method]; - } - - var stack = this.DEBUG ? new Error() : undefined; - var length = arguments.length; - var args; - - if (length > 3) { - args = new Array(length - 3); - for (var i = 3; i < length; i++) { - args[i-3] = arguments[i]; - } - } else { - args = undefined; - } - - if (!this.currentInstance) { - createAutorun(this); - } - return this.currentInstance.schedule(queueName, target, method, args, true, stack); - }, - - setTimeout: function() { - var l = arguments.length; - var args = new Array(l); - - for (var x = 0; x < l; x++) { - args[x] = arguments[x]; - } - - var length = args.length, - method, wait, target, - methodOrTarget, methodOrWait, methodOrArgs; - - if (length === 0) { - return; - } else if (length === 1) { - method = args.shift(); - wait = 0; - } else if (length === 2) { - methodOrTarget = args[0]; - methodOrWait = args[1]; - - if (isFunction(methodOrWait) || isFunction(methodOrTarget[methodOrWait])) { - target = args.shift(); - method = args.shift(); - wait = 0; - } else if (isCoercableNumber(methodOrWait)) { - method = args.shift(); - wait = args.shift(); - } else { - method = args.shift(); - wait = 0; - } - } else { - var last = args[args.length - 1]; - - if (isCoercableNumber(last)) { - wait = args.pop(); - } else { - wait = 0; - } - - methodOrTarget = args[0]; - methodOrArgs = args[1]; - - if (isFunction(methodOrArgs) || (isString(methodOrArgs) && - methodOrTarget !== null && - methodOrArgs in methodOrTarget)) { - target = args.shift(); - method = args.shift(); - } else { - method = args.shift(); - } - } - - var executeAt = now() + parseInt(wait, 10); - - if (isString(method)) { - method = target[method]; - } - - var onError = getOnError(this.options); - - function fn() { - if (onError) { - try { - method.apply(target, args); - } catch (e) { - onError(e); - } - } else { - method.apply(target, args); - } - } - - // find position to insert - var i = searchTimer(executeAt, this._timers); - - this._timers.splice(i, 0, executeAt, fn); - - updateLaterTimer(this, executeAt, wait); - - return fn; - }, - - throttle: function(target, method /* , args, wait, [immediate] */) { - var backburner = this; - var args = arguments; - var immediate = pop.call(args); - var wait, throttler, index, timer; - - if (isNumber(immediate) || isString(immediate)) { - wait = immediate; - immediate = true; - } else { - wait = pop.call(args); - } - - wait = parseInt(wait, 10); - - index = findThrottler(target, method, this._throttlers); - if (index > -1) { return this._throttlers[index]; } // throttled - - timer = global.setTimeout(function() { - if (!immediate) { - backburner.run.apply(backburner, args); - } - var index = findThrottler(target, method, backburner._throttlers); - if (index > -1) { - backburner._throttlers.splice(index, 1); - } - }, wait); - - if (immediate) { - this.run.apply(this, args); - } - - throttler = [target, method, timer]; - - this._throttlers.push(throttler); - - return throttler; - }, - - debounce: function(target, method /* , args, wait, [immediate] */) { - var backburner = this; - var args = arguments; - var immediate = pop.call(args); - var wait, index, debouncee, timer; - - if (isNumber(immediate) || isString(immediate)) { - wait = immediate; - immediate = false; - } else { - wait = pop.call(args); - } - - wait = parseInt(wait, 10); - // Remove debouncee - index = findDebouncee(target, method, this._debouncees); - - if (index > -1) { - debouncee = this._debouncees[index]; - this._debouncees.splice(index, 1); - clearTimeout(debouncee[2]); - } - - timer = global.setTimeout(function() { - if (!immediate) { - backburner.run.apply(backburner, args); - } - var index = findDebouncee(target, method, backburner._debouncees); - if (index > -1) { - backburner._debouncees.splice(index, 1); - } - }, wait); - - if (immediate && index === -1) { - backburner.run.apply(backburner, args); - } - - debouncee = [ - target, - method, - timer - ]; - - backburner._debouncees.push(debouncee); - - return debouncee; - }, - - cancelTimers: function() { - var clearItems = function(item) { - clearTimeout(item[2]); - }; - - each(this._throttlers, clearItems); - this._throttlers = []; - - each(this._debouncees, clearItems); - this._debouncees = []; - - if (this._laterTimer) { - clearTimeout(this._laterTimer); - this._laterTimer = null; - } - this._timers = []; - - if (this._autorun) { - clearTimeout(this._autorun); - this._autorun = null; - } - }, - - hasTimers: function() { - return !!this._timers.length || !!this._debouncees.length || !!this._throttlers.length || this._autorun; - }, - - cancel: function(timer) { - var timerType = typeof timer; - - if (timer && timerType === 'object' && timer.queue && timer.method) { // we're cancelling a deferOnce - return timer.queue.cancel(timer); - } else if (timerType === 'function') { // we're cancelling a setTimeout - for (var i = 0, l = this._timers.length; i < l; i += 2) { - if (this._timers[i + 1] === timer) { - this._timers.splice(i, 2); // remove the two elements - if (i === 0) { - if (this._laterTimer) { // Active timer? Then clear timer and reset for future timer - clearTimeout(this._laterTimer); - this._laterTimer = null; - } - if (this._timers.length > 0) { // Update to next available timer when available - updateLaterTimer(this, this._timers[0], this._timers[0] - now()); - } - } - return true; - } - } - } else if (Object.prototype.toString.call(timer) === "[object Array]"){ // we're cancelling a throttle or debounce - return this._cancelItem(findThrottler, this._throttlers, timer) || - this._cancelItem(findDebouncee, this._debouncees, timer); - } else { - return; // timer was null or not a timer - } - }, - - _cancelItem: function(findMethod, array, timer){ - var item, index; - - if (timer.length < 3) { return false; } - - index = findMethod(timer[0], timer[1], array); - - if (index > -1) { - - item = array[index]; - - if (item[2] === timer[2]) { - array.splice(index, 1); - clearTimeout(timer[2]); - return true; - } - } - - return false; - } - }; - - Backburner.prototype.schedule = Backburner.prototype.defer; - Backburner.prototype.scheduleOnce = Backburner.prototype.deferOnce; - Backburner.prototype.later = Backburner.prototype.setTimeout; - - if (needsIETryCatchFix) { - var originalRun = Backburner.prototype.run; - Backburner.prototype.run = wrapInTryCatch(originalRun); - - var originalEnd = Backburner.prototype.end; - Backburner.prototype.end = wrapInTryCatch(originalEnd); - } - - function getOnError(options) { - return options.onError || (options.onErrorTarget && options.onErrorTarget[options.onErrorMethod]); - } - - function createAutorun(backburner) { - backburner.begin(); - backburner._autorun = global.setTimeout(function() { - backburner._autorun = null; - backburner.end(); - }); - } - - function updateLaterTimer(backburner, executeAt, wait) { - var n = now(); - if (!backburner._laterTimer || executeAt < backburner._laterTimerExpiresAt || backburner._laterTimerExpiresAt < n) { - - if (backburner._laterTimer) { - // Clear when: - // - Already expired - // - New timer is earlier - clearTimeout(backburner._laterTimer); - - if (backburner._laterTimerExpiresAt < n) { // If timer was never triggered - // Calculate the left-over wait-time - wait = Math.max(0, executeAt - n); - } - } - - backburner._laterTimer = global.setTimeout(function() { - backburner._laterTimer = null; - backburner._laterTimerExpiresAt = null; - executeTimers(backburner); - }, wait); - - backburner._laterTimerExpiresAt = n + wait; - } - } - - function executeTimers(backburner) { - var n = now(); - var fns, i, l; - - backburner.run(function() { - i = searchTimer(n, backburner._timers); - - fns = backburner._timers.splice(0, i); - - for (i = 1, l = fns.length; i < l; i += 2) { - backburner.schedule(backburner.options.defaultQueue, null, fns[i]); - } - }); - - if (backburner._timers.length) { - updateLaterTimer(backburner, backburner._timers[0], backburner._timers[0] - n); - } - } - - function findDebouncee(target, method, debouncees) { - return findItem(target, method, debouncees); - } - - function findThrottler(target, method, throttlers) { - return findItem(target, method, throttlers); - } - - function findItem(target, method, collection) { - var item; - var index = -1; - - for (var i = 0, l = collection.length; i < l; i++) { - item = collection[i]; - if (item[0] === target && item[1] === method) { - index = i; - break; - } - } - - return index; - } - - __exports__["default"] = Backburner; - }); -enifed("backburner.umd", - ["./backburner"], - function(__dependency1__) { - "use strict"; - var Backburner = __dependency1__["default"]; - - /* global define:true module:true window: true */ - if (typeof enifed === 'function' && enifed.amd) { - enifed(function() { return Backburner; }); - } else if (typeof module !== 'undefined' && module.exports) { - module.exports = Backburner; - } else if (typeof this !== 'undefined') { - this['Backburner'] = Backburner; - } - }); -enifed("backburner/binary-search", - ["exports"], - function(__exports__) { - "use strict"; - __exports__["default"] = function binarySearch(time, timers) { - var start = 0; - var end = timers.length - 2; - var middle, l; - - while (start < end) { - // since timers is an array of pairs 'l' will always - // be an integer - l = (end - start) / 2; - - // compensate for the index in case even number - // of pairs inside timers - middle = start + l - (l % 2); - - if (time >= timers[middle]) { - start = middle + 2; - } else { - end = middle; - } - } - - return (time >= timers[start]) ? start + 2 : start; - } - }); -enifed("backburner/deferred-action-queues", - ["./utils","./queue","exports"], - function(__dependency1__, __dependency2__, __exports__) { - "use strict"; - var each = __dependency1__.each; - var Queue = __dependency2__["default"]; - - function DeferredActionQueues(queueNames, options) { - var queues = this.queues = Object.create(null); - this.queueNames = queueNames = queueNames || []; - - this.options = options; - - each(queueNames, function(queueName) { - queues[queueName] = new Queue(queueName, options[queueName], options); - }); - } - - function noSuchQueue(name) { - throw new Error("You attempted to schedule an action in a queue (" + name + ") that doesn't exist"); - } - - DeferredActionQueues.prototype = { - schedule: function(name, target, method, args, onceFlag, stack) { - var queues = this.queues; - var queue = queues[name]; - - if (!queue) { - noSuchQueue(name); - } - - if (onceFlag) { - return queue.pushUnique(target, method, args, stack); - } else { - return queue.push(target, method, args, stack); - } - }, - - flush: function() { - var queues = this.queues; - var queueNames = this.queueNames; - var queueName, queue, queueItems, priorQueueNameIndex; - var queueNameIndex = 0; - var numberOfQueues = queueNames.length; - var options = this.options; - - while (queueNameIndex < numberOfQueues) { - queueName = queueNames[queueNameIndex]; - queue = queues[queueName]; - - var numberOfQueueItems = queue._queue.length; - - if (numberOfQueueItems === 0) { - queueNameIndex++; - } else { - queue.flush(false /* async */); - queueNameIndex = 0; - } - } - } - }; - - __exports__["default"] = DeferredActionQueues; - }); -enifed("backburner/platform", - ["exports"], - function(__exports__) { - "use strict"; - // In IE 6-8, try/finally doesn't work without a catch. - // Unfortunately, this is impossible to test for since wrapping it in a parent try/catch doesn't trigger the bug. - // This tests for another broken try/catch behavior that only exhibits in the same versions of IE. - var needsIETryCatchFix = (function(e,x){ - try{ x(); } - catch(e) { } // jshint ignore:line - return !!e; - })(); - __exports__.needsIETryCatchFix = needsIETryCatchFix; - }); -enifed("backburner/queue", - ["./utils","exports"], - function(__dependency1__, __exports__) { - "use strict"; - var isString = __dependency1__.isString; - - function Queue(name, options, globalOptions) { - this.name = name; - this.globalOptions = globalOptions || {}; - this.options = options; - this._queue = []; - this.targetQueues = Object.create(null); - this._queueBeingFlushed = undefined; - } - - Queue.prototype = { - push: function(target, method, args, stack) { - var queue = this._queue; - queue.push(target, method, args, stack); - - return { - queue: this, - target: target, - method: method - }; - }, - - pushUniqueWithoutGuid: function(target, method, args, stack) { - var queue = this._queue; - - for (var i = 0, l = queue.length; i < l; i += 4) { - var currentTarget = queue[i]; - var currentMethod = queue[i+1]; - - if (currentTarget === target && currentMethod === method) { - queue[i+2] = args; // replace args - queue[i+3] = stack; // replace stack - return; - } - } - - queue.push(target, method, args, stack); - }, - - targetQueue: function(targetQueue, target, method, args, stack) { - var queue = this._queue; - - for (var i = 0, l = targetQueue.length; i < l; i += 4) { - var currentMethod = targetQueue[i]; - var currentIndex = targetQueue[i + 1]; - - if (currentMethod === method) { - queue[currentIndex + 2] = args; // replace args - queue[currentIndex + 3] = stack; // replace stack - return; - } - } - - targetQueue.push( - method, - queue.push(target, method, args, stack) - 4 - ); - }, - - pushUniqueWithGuid: function(guid, target, method, args, stack) { - var hasLocalQueue = this.targetQueues[guid]; - - if (hasLocalQueue) { - this.targetQueue(hasLocalQueue, target, method, args, stack); - } else { - this.targetQueues[guid] = [ - method, - this._queue.push(target, method, args, stack) - 4 - ]; - } - - return { - queue: this, - target: target, - method: method - }; - }, - - pushUnique: function(target, method, args, stack) { - var queue = this._queue, currentTarget, currentMethod, i, l; - var KEY = this.globalOptions.GUID_KEY; - - if (target && KEY) { - var guid = target[KEY]; - if (guid) { - return this.pushUniqueWithGuid(guid, target, method, args, stack); - } - } - - this.pushUniqueWithoutGuid(target, method, args, stack); - - return { - queue: this, - target: target, - method: method - }; - }, - - invoke: function(target, method, args, _, _errorRecordedForStack) { - if (args && args.length > 0) { - method.apply(target, args); - } else { - method.call(target); - } - }, - - invokeWithOnError: function(target, method, args, onError, errorRecordedForStack) { - try { - if (args && args.length > 0) { - method.apply(target, args); - } else { - method.call(target); - } - } catch(error) { - onError(error, errorRecordedForStack); - } - }, - - flush: function(sync) { - var queue = this._queue; - var length = queue.length; - - if (length === 0) { - return; - } - - var globalOptions = this.globalOptions; - var options = this.options; - var before = options && options.before; - var after = options && options.after; - var onError = globalOptions.onError || (globalOptions.onErrorTarget && - globalOptions.onErrorTarget[globalOptions.onErrorMethod]); - var target, method, args, errorRecordedForStack; - var invoke = onError ? this.invokeWithOnError : this.invoke; - - this.targetQueues = Object.create(null); - var queueItems = this._queueBeingFlushed = this._queue.slice(); - this._queue = []; - - if (before) { - before(); - } - - for (var i = 0; i < length; i += 4) { - target = queueItems[i]; - method = queueItems[i+1]; - args = queueItems[i+2]; - errorRecordedForStack = queueItems[i+3]; // Debugging assistance - - if (isString(method)) { - method = target[method]; - } - - // method could have been nullified / canceled during flush - if (method) { - // - // ** Attention intrepid developer ** - // - // To find out the stack of this task when it was scheduled onto - // the run loop, add the following to your app.js: - // - // Ember.run.backburner.DEBUG = true; // NOTE: This slows your app, don't leave it on in production. - // - // Once that is in place, when you are at a breakpoint and navigate - // here in the stack explorer, you can look at `errorRecordedForStack.stack`, - // which will be the captured stack when this job was scheduled. - // - invoke(target, method, args, onError, errorRecordedForStack); - } - } - - if (after) { - after(); - } - - this._queueBeingFlushed = undefined; - - if (sync !== false && - this._queue.length > 0) { - // check if new items have been added - this.flush(true); - } - }, - - cancel: function(actionToCancel) { - var queue = this._queue, currentTarget, currentMethod, i, l; - var target = actionToCancel.target; - var method = actionToCancel.method; - var GUID_KEY = this.globalOptions.GUID_KEY; - - if (GUID_KEY && this.targetQueues && target) { - var targetQueue = this.targetQueues[target[GUID_KEY]]; - - if (targetQueue) { - for (i = 0, l = targetQueue.length; i < l; i++) { - if (targetQueue[i] === method) { - targetQueue.splice(i, 1); - } - } - } - } - - for (i = 0, l = queue.length; i < l; i += 4) { - currentTarget = queue[i]; - currentMethod = queue[i+1]; - - if (currentTarget === target && - currentMethod === method) { - queue.splice(i, 4); - return true; - } - } - - // if not found in current queue - // could be in the queue that is being flushed - queue = this._queueBeingFlushed; - - if (!queue) { - return; - } - - for (i = 0, l = queue.length; i < l; i += 4) { - currentTarget = queue[i]; - currentMethod = queue[i+1]; - - if (currentTarget === target && - currentMethod === method) { - // don't mess with array during flush - // just nullify the method - queue[i+1] = null; - return true; - } - } - } - }; - - __exports__["default"] = Queue; - }); -enifed("backburner/utils", - ["exports"], - function(__exports__) { - "use strict"; - var NUMBER = /\d+/; - - function each(collection, callback) { - for (var i = 0; i < collection.length; i++) { - callback(collection[i]); - } - } - - __exports__.each = each;// Date.now is not available in browsers < IE9 - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility - var now = Date.now || function() { return new Date().getTime(); }; - __exports__.now = now; - function isString(suspect) { - return typeof suspect === 'string'; - } - - __exports__.isString = isString;function isFunction(suspect) { - return typeof suspect === 'function'; - } - - __exports__.isFunction = isFunction;function isNumber(suspect) { - return typeof suspect === 'number'; - } - - __exports__.isNumber = isNumber;function isCoercableNumber(number) { - return isNumber(number) || NUMBER.test(number); - } - - __exports__.isCoercableNumber = isCoercableNumber;function wrapInTryCatch(func) { - return function () { - try { - return func.apply(this, arguments); - } catch (e) { - throw e; - } - }; - } - - __exports__.wrapInTryCatch = wrapInTryCatch; - }); -enifed("calculateVersion", - [], - function() { - "use strict"; - 'use strict'; - - var fs = eriuqer('fs'); - var path = eriuqer('path'); - - module.exports = function () { - var packageVersion = eriuqer('../package.json').version; - var output = [packageVersion]; - var gitPath = path.join(__dirname,'..','.git'); - var headFilePath = path.join(gitPath, 'HEAD'); - - if (packageVersion.indexOf('+') > -1) { - try { - if (fs.existsSync(headFilePath)) { - var headFile = fs.readFileSync(headFilePath, {encoding: 'utf8'}); - var branchName = headFile.split('/').slice(-1)[0].trim(); - var refPath = headFile.split(' ')[1]; - var branchSHA; - - if (refPath) { - var branchPath = path.join(gitPath, refPath.trim()); - branchSHA = fs.readFileSync(branchPath); - } else { - branchSHA = branchName; - } - - output.push(branchSHA.slice(0,10)); - } - } catch (err) { - console.error(err.stack); - } - return output.join('.'); - } else { - return packageVersion; - } - }; - }); -enifed('container', ['exports', 'container/registry', 'container/container'], function (exports, Registry, Container) { - - 'use strict'; - - /* - Public api for the container is still in flux. - The public api, specified on the application namespace should be considered the stable api. - // @module container - @private - */ - - /* - Flag to enable/disable model factory injections (disabled by default) - If model factory injections are enabled, models should not be - accessed globally (only through `container.lookupFactory('model:modelName'))`); - */ - Ember.MODEL_FACTORY_INJECTIONS = false; - - if (Ember.ENV && typeof Ember.ENV.MODEL_FACTORY_INJECTIONS !== 'undefined') { - Ember.MODEL_FACTORY_INJECTIONS = !!Ember.ENV.MODEL_FACTORY_INJECTIONS; - } - - exports.Registry = Registry['default']; - exports.Container = Container['default']; - -}); -enifed('container/container', ['exports', 'ember-metal/core', 'ember-metal/keys', 'ember-metal/dictionary'], function (exports, Ember, emberKeys, dictionary) { - - 'use strict'; - - var Registry; - - /** - A lightweight container used to instantiate and cache objects. - - Every `Container` must be associated with a `Registry`, which is referenced - to determine the factory and options that should be used to instantiate - objects. - - The public API for `Container` is still in flux and should not be considered - stable. - - @private - @class Container - */ - function Container(registry, options) { - this._registry = registry || (function() { - Ember['default'].deprecate( - "A container should only be created for an already instantiated " + - "registry. For backward compatibility, an isolated registry will " + - "be instantiated just for this container." - ); - - // TODO - See note above about transpiler import workaround. - if (!Registry) { Registry = requireModule('container/registry')['default']; } - - return new Registry(); - }()); - - this.cache = dictionary['default'](options && options.cache ? options.cache : null); - this.factoryCache = dictionary['default'](options && options.factoryCache ? options.factoryCache : null); - this.validationCache = dictionary['default'](options && options.validationCache ? options.validationCache : null); - } - - Container.prototype = { - /** - @private - - @property _registry - @type Registry - @since 1.11.0 - */ - _registry: null, - - /** - @property cache - @type InheritingDict - */ - cache: null, - - /** - @property factoryCache - @type InheritingDict - */ - factoryCache: null, - - /** - @property validationCache - @type InheritingDict - */ - validationCache: null, - - /** - Given a fullName return a corresponding instance. - - The default behaviour is for lookup to return a singleton instance. - The singleton is scoped to the container, allowing multiple containers - to all have their own locally scoped singletons. - - ```javascript - var registry = new Registry(); - var container = registry.container(); - - registry.register('api:twitter', Twitter); - - var twitter = container.lookup('api:twitter'); - - twitter instanceof Twitter; // => true - - // by default the container will return singletons - var twitter2 = container.lookup('api:twitter'); - twitter2 instanceof Twitter; // => true - - twitter === twitter2; //=> true - ``` - - If singletons are not wanted an optional flag can be provided at lookup. - - ```javascript - var registry = new Registry(); - var container = registry.container(); - - registry.register('api:twitter', Twitter); - - var twitter = container.lookup('api:twitter', { singleton: false }); - var twitter2 = container.lookup('api:twitter', { singleton: false }); - - twitter === twitter2; //=> false - ``` - - @method lookup - @param {String} fullName - @param {Object} options - @return {any} - */ - lookup: function(fullName, options) { - Ember['default'].assert('fullName must be a proper full name', this._registry.validateFullName(fullName)); - return lookup(this, this._registry.normalize(fullName), options); - }, - - /** - Given a fullName return the corresponding factory. - - @method lookupFactory - @param {String} fullName - @return {any} - */ - lookupFactory: function(fullName) { - Ember['default'].assert('fullName must be a proper full name', this._registry.validateFullName(fullName)); - return factoryFor(this, this._registry.normalize(fullName)); - }, - - /** - A depth first traversal, destroying the container, its descendant containers and all - their managed objects. - - @method destroy - */ - destroy: function() { - eachDestroyable(this, function(item) { - if (item.destroy) { - item.destroy(); - } - }); - - this.isDestroyed = true; - }, - - /** - Clear either the entire cache or just the cache for a particular key. - - @method reset - @param {String} fullName optional key to reset; if missing, resets everything - */ - reset: function(fullName) { - if (arguments.length > 0) { - resetMember(this, this._registry.normalize(fullName)); - } else { - resetCache(this); - } - } - }; - - (function exposeRegistryMethods() { - var methods = [ - 'register', - 'unregister', - 'resolve', - 'normalize', - 'typeInjection', - 'injection', - 'factoryInjection', - 'factoryTypeInjection', - 'has', - 'options', - 'optionsForType' - ]; - - function exposeRegistryMethod(method) { - Container.prototype[method] = function() { - Ember['default'].deprecate(method + ' should be called on the registry instead of the container'); - return this._registry[method].apply(this._registry, arguments); - }; - } - - for (var i = 0, l = methods.length; i < l; i++) { - exposeRegistryMethod(methods[i]); - } - })(); - - function lookup(container, fullName, options) { - options = options || {}; - - if (container.cache[fullName] && options.singleton !== false) { - return container.cache[fullName]; - } - - var value = instantiate(container, fullName); - - if (value === undefined) { return; } - - if (container._registry.getOption(fullName, 'singleton') !== false && options.singleton !== false) { - container.cache[fullName] = value; - } - - return value; - } - - function buildInjections(container) { - var hash = {}; - - if (arguments.length > 1) { - var injectionArgs = Array.prototype.slice.call(arguments, 1); - var injections = []; - var injection; - - for (var i = 0, l = injectionArgs.length; i < l; i++) { - if (injectionArgs[i]) { - injections = injections.concat(injectionArgs[i]); - } - } - - container._registry.validateInjections(injections); - - for (i = 0, l = injections.length; i < l; i++) { - injection = injections[i]; - hash[injection.property] = lookup(container, injection.fullName); - } - } - - return hash; - } - - function factoryFor(container, fullName) { - var cache = container.factoryCache; - if (cache[fullName]) { - return cache[fullName]; - } - var registry = container._registry; - var factory = registry.resolve(fullName); - if (factory === undefined) { return; } - - var type = fullName.split(':')[0]; - if (!factory || typeof factory.extend !== 'function' || (!Ember['default'].MODEL_FACTORY_INJECTIONS && type === 'model')) { - if (factory && typeof factory._onLookup === 'function') { - factory._onLookup(fullName); - } - - // TODO: think about a 'safe' merge style extension - // for now just fallback to create time injection - cache[fullName] = factory; - return factory; - - } else { - var injections = injectionsFor(container, fullName); - var factoryInjections = factoryInjectionsFor(container, fullName); - - factoryInjections._toString = registry.makeToString(factory, fullName); - - var injectedFactory = factory.extend(injections); - injectedFactory.reopenClass(factoryInjections); - - if (factory && typeof factory._onLookup === 'function') { - factory._onLookup(fullName); - } - - cache[fullName] = injectedFactory; - - return injectedFactory; - } - } - - function injectionsFor(container, fullName) { - var registry = container._registry; - var splitName = fullName.split(':'); - var type = splitName[0]; - - var injections = buildInjections(container, - registry.getTypeInjections(type), - registry.getInjections(fullName)); - injections._debugContainerKey = fullName; - injections.container = container; - - return injections; - } - - function factoryInjectionsFor(container, fullName) { - var registry = container._registry; - var splitName = fullName.split(':'); - var type = splitName[0]; - - var factoryInjections = buildInjections(container, - registry.getFactoryTypeInjections(type), - registry.getFactoryInjections(fullName)); - factoryInjections._debugContainerKey = fullName; - - return factoryInjections; - } - - function instantiate(container, fullName) { - var factory = factoryFor(container, fullName); - var lazyInjections, validationCache; - - if (container._registry.getOption(fullName, 'instantiate') === false) { - return factory; - } - - if (factory) { - if (typeof factory.create !== 'function') { - throw new Error('Failed to create an instance of \'' + fullName + '\'. ' + - 'Most likely an improperly defined class or an invalid module export.'); - } - - validationCache = container.validationCache; - - // Ensure that all lazy injections are valid at instantiation time - if (!validationCache[fullName] && typeof factory._lazyInjections === 'function') { - lazyInjections = factory._lazyInjections(); - lazyInjections = container._registry.normalizeInjectionsHash(lazyInjections); - - container._registry.validateInjections(lazyInjections); - } - - validationCache[fullName] = true; - - if (typeof factory.extend === 'function') { - // assume the factory was extendable and is already injected - return factory.create(); - } else { - // assume the factory was extendable - // to create time injections - // TODO: support new'ing for instantiation and merge injections for pure JS Functions - return factory.create(injectionsFor(container, fullName)); - } - } - } - - function eachDestroyable(container, callback) { - var cache = container.cache; - var keys = emberKeys['default'](cache); - var key, value; - - for (var i = 0, l = keys.length; i < l; i++) { - key = keys[i]; - value = cache[key]; - - if (container._registry.getOption(key, 'instantiate') !== false) { - callback(value); - } - } - } - - function resetCache(container) { - eachDestroyable(container, function(value) { - if (value.destroy) { - value.destroy(); - } - }); - - container.cache.dict = dictionary['default'](null); - } - - function resetMember(container, fullName) { - var member = container.cache[fullName]; - - delete container.factoryCache[fullName]; - - if (member) { - delete container.cache[fullName]; - - if (member.destroy) { - member.destroy(); - } - } - } - - exports['default'] = Container; - -}); -enifed('container/registry', ['exports', 'ember-metal/core', 'ember-metal/dictionary', './container'], function (exports, Ember, dictionary, Container) { - - 'use strict'; - - var VALID_FULL_NAME_REGEXP = /^[^:]+.+:[^:]+$/; - - var instanceInitializersFeatureEnabled; - - /** - A lightweight registry used to store factory and option information keyed - by type. - - A `Registry` stores the factory and option information needed by a - `Container` to instantiate and cache objects. - - The public API for `Registry` is still in flux and should not be considered - stable. - - @private - @class Registry - @since 1.11.0 - */ - function Registry(options) { - this.fallback = options && options.fallback ? options.fallback : null; - - this.resolver = options && options.resolver ? options.resolver : function() {}; - - this.registrations = dictionary['default'](options && options.registrations ? options.registrations : null); - - this._typeInjections = dictionary['default'](null); - this._injections = dictionary['default'](null); - this._factoryTypeInjections = dictionary['default'](null); - this._factoryInjections = dictionary['default'](null); - - this._normalizeCache = dictionary['default'](null); - this._resolveCache = dictionary['default'](null); - - this._options = dictionary['default'](null); - this._typeOptions = dictionary['default'](null); - } - - Registry.prototype = { - /** - A backup registry for resolving registrations when no matches can be found. - - @property fallback - @type Registry - */ - fallback: null, - - /** - @property resolver - @type function - */ - resolver: null, - - /** - @property registrations - @type InheritingDict - */ - registrations: null, - - /** - @private - - @property _typeInjections - @type InheritingDict - */ - _typeInjections: null, - - /** - @private - - @property _injections - @type InheritingDict - */ - _injections: null, - - /** - @private - - @property _factoryTypeInjections - @type InheritingDict - */ - _factoryTypeInjections: null, - - /** - @private - - @property _factoryInjections - @type InheritingDict - */ - _factoryInjections: null, - - /** - @private - - @property _normalizeCache - @type InheritingDict - */ - _normalizeCache: null, - - /** - @private - - @property _resolveCache - @type InheritingDict - */ - _resolveCache: null, - - /** - @private - - @property _options - @type InheritingDict - */ - _options: null, - - /** - @private - - @property _typeOptions - @type InheritingDict - */ - _typeOptions: null, - - /** - The first container created for this registry. - - This allows deprecated access to `lookup` and `lookupFactory` to avoid - breaking compatibility for Ember 1.x initializers. - - @private - @property _defaultContainer - @type Container - */ - _defaultContainer: null, - - /** - Creates a container based on this registry. - - @method container - @param {Object} options - @return {Container} created container - */ - container: function(options) { - var container = new Container['default'](this, options); - - // 2.0TODO - remove `registerContainer` - this.registerContainer(container); - - return container; - }, - - /** - Register the first container created for a registery to allow deprecated - access to its `lookup` and `lookupFactory` methods to avoid breaking - compatibility for Ember 1.x initializers. - - 2.0TODO: Remove this method. The bookkeeping is only needed to support - deprecated behavior. - - @param {Container} newly created container - */ - registerContainer: function(container) { - if (!this._defaultContainer) { - this._defaultContainer = container; - } - if (this.fallback) { - this.fallback.registerContainer(container); - } - }, - - lookup: function(fullName, options) { - Ember['default'].assert('Create a container on the registry (with `registry.container()`) before calling `lookup`.', this._defaultContainer); - - if (instanceInitializersFeatureEnabled) { - Ember['default'].deprecate( - '`lookup` was called on a Registry. The `initializer` API no longer receives a container, and you should use an `instanceInitializer` to look up objects from the container.', - false, - { url: "http://emberjs.com/guides/deprecations#toc_access-to-instances-in-initializers" } - ); - } - - return this._defaultContainer.lookup(fullName, options); - }, - - lookupFactory: function(fullName) { - Ember['default'].assert('Create a container on the registry (with `registry.container()`) before calling `lookupFactory`.', this._defaultContainer); - - if (instanceInitializersFeatureEnabled) { - Ember['default'].deprecate( - '`lookupFactory` was called on a Registry. The `initializer` API no longer receives a container, and you should use an `instanceInitializer` to look up objects from the container.', - false, - { url: "http://emberjs.com/guides/deprecations#toc_access-to-instances-in-initializers" } - ); - } - - return this._defaultContainer.lookupFactory(fullName); - }, - - /** - Registers a factory for later injection. - - Example: - - ```javascript - var registry = new Registry(); - - registry.register('model:user', Person, {singleton: false }); - registry.register('fruit:favorite', Orange); - registry.register('communication:main', Email, {singleton: false}); - ``` - - @method register - @param {String} fullName - @param {Function} factory - @param {Object} options - */ - register: function(fullName, factory, options) { - Ember['default'].assert('fullName must be a proper full name', this.validateFullName(fullName)); - - if (factory === undefined) { - throw new TypeError('Attempting to register an unknown factory: `' + fullName + '`'); - } - - var normalizedName = this.normalize(fullName); - - if (this._resolveCache[normalizedName]) { - throw new Error('Cannot re-register: `' + fullName +'`, as it has already been resolved.'); - } - - this.registrations[normalizedName] = factory; - this._options[normalizedName] = (options || {}); - }, - - /** - Unregister a fullName - - ```javascript - var registry = new Registry(); - registry.register('model:user', User); - - registry.resolve('model:user').create() instanceof User //=> true - - registry.unregister('model:user') - registry.resolve('model:user') === undefined //=> true - ``` - - @method unregister - @param {String} fullName - */ - unregister: function(fullName) { - Ember['default'].assert('fullName must be a proper full name', this.validateFullName(fullName)); - - var normalizedName = this.normalize(fullName); - - delete this.registrations[normalizedName]; - delete this._resolveCache[normalizedName]; - delete this._options[normalizedName]; - }, - - /** - Given a fullName return the corresponding factory. - - By default `resolve` will retrieve the factory from - the registry. - - ```javascript - var registry = new Registry(); - registry.register('api:twitter', Twitter); - - registry.resolve('api:twitter') // => Twitter - ``` - - Optionally the registry can be provided with a custom resolver. - If provided, `resolve` will first provide the custom resolver - the opportunity to resolve the fullName, otherwise it will fallback - to the registry. - - ```javascript - var registry = new Registry(); - registry.resolver = function(fullName) { - // lookup via the module system of choice - }; - - // the twitter factory is added to the module system - registry.resolve('api:twitter') // => Twitter - ``` - - @method resolve - @param {String} fullName - @return {Function} fullName's factory - */ - resolve: function(fullName) { - Ember['default'].assert('fullName must be a proper full name', this.validateFullName(fullName)); - var factory = resolve(this, this.normalize(fullName)); - if (factory === undefined && this.fallback) { - factory = this.fallback.resolve(fullName); - } - return factory; - }, - - /** - A hook that can be used to describe how the resolver will - attempt to find the factory. - - For example, the default Ember `.describe` returns the full - class name (including namespace) where Ember's resolver expects - to find the `fullName`. - - @method describe - @param {String} fullName - @return {string} described fullName - */ - describe: function(fullName) { - return fullName; - }, - - /** - A hook to enable custom fullName normalization behaviour - - @method normalizeFullName - @param {String} fullName - @return {string} normalized fullName - */ - normalizeFullName: function(fullName) { - return fullName; - }, - - /** - normalize a fullName based on the applications conventions - - @method normalize - @param {String} fullName - @return {string} normalized fullName - */ - normalize: function(fullName) { - return this._normalizeCache[fullName] || ( - this._normalizeCache[fullName] = this.normalizeFullName(fullName) - ); - }, - - /** - @method makeToString - - @param {any} factory - @param {string} fullName - @return {function} toString function - */ - makeToString: function(factory, fullName) { - return factory.toString(); - }, - - /** - Given a fullName check if the container is aware of its factory - or singleton instance. - - @method has - @param {String} fullName - @return {Boolean} - */ - has: function(fullName) { - Ember['default'].assert('fullName must be a proper full name', this.validateFullName(fullName)); - return has(this, this.normalize(fullName)); - }, - - /** - Allow registering options for all factories of a type. - - ```javascript - var registry = new Registry(); - var container = registry.container(); - - // if all of type `connection` must not be singletons - registry.optionsForType('connection', { singleton: false }); - - registry.register('connection:twitter', TwitterConnection); - registry.register('connection:facebook', FacebookConnection); - - var twitter = container.lookup('connection:twitter'); - var twitter2 = container.lookup('connection:twitter'); - - twitter === twitter2; // => false - - var facebook = container.lookup('connection:facebook'); - var facebook2 = container.lookup('connection:facebook'); - - facebook === facebook2; // => false - ``` - - @method optionsForType - @param {String} type - @param {Object} options - */ - optionsForType: function(type, options) { - this._typeOptions[type] = options; - }, - - getOptionsForType: function(type) { - var optionsForType = this._typeOptions[type]; - if (optionsForType === undefined && this.fallback) { - optionsForType = this.fallback.getOptionsForType(type); - } - return optionsForType; - }, - - /** - @method options - @param {String} fullName - @param {Object} options - */ - options: function(fullName, options) { - options = options || {}; - var normalizedName = this.normalize(fullName); - this._options[normalizedName] = options; - }, - - getOptions: function(fullName) { - var normalizedName = this.normalize(fullName); - var options = this._options[normalizedName]; - if (options === undefined && this.fallback) { - options = this.fallback.getOptions(fullName); - } - return options; - }, - - getOption: function(fullName, optionName) { - var options = this._options[fullName]; - - if (options && options[optionName] !== undefined) { - return options[optionName]; - } - - var type = fullName.split(':')[0]; - options = this._typeOptions[type]; - - if (options && options[optionName] !== undefined) { - return options[optionName]; - - } else if (this.fallback) { - return this.fallback.getOption(fullName, optionName); - } - }, - - option: function(fullName, optionName) { - Ember['default'].deprecate('`Registry.option()` has been deprecated. Call `Registry.getOption()` instead.'); - return this.getOption(fullName, optionName); - }, - - /** - Used only via `injection`. - - Provides a specialized form of injection, specifically enabling - all objects of one type to be injected with a reference to another - object. - - For example, provided each object of type `controller` needed a `router`. - one would do the following: - - ```javascript - var registry = new Registry(); - var container = registry.container(); - - registry.register('router:main', Router); - registry.register('controller:user', UserController); - registry.register('controller:post', PostController); - - registry.typeInjection('controller', 'router', 'router:main'); - - var user = container.lookup('controller:user'); - var post = container.lookup('controller:post'); - - user.router instanceof Router; //=> true - post.router instanceof Router; //=> true - - // both controllers share the same router - user.router === post.router; //=> true - ``` - - @private - @method typeInjection - @param {String} type - @param {String} property - @param {String} fullName - */ - typeInjection: function(type, property, fullName) { - Ember['default'].assert('fullName must be a proper full name', this.validateFullName(fullName)); - - var fullNameType = fullName.split(':')[0]; - if (fullNameType === type) { - throw new Error('Cannot inject a `' + fullName + - '` on other ' + type + '(s).'); - } - - var injections = this._typeInjections[type] || - (this._typeInjections[type] = []); - - injections.push({ - property: property, - fullName: fullName - }); - }, - - /** - Defines injection rules. - - These rules are used to inject dependencies onto objects when they - are instantiated. - - Two forms of injections are possible: - - * Injecting one fullName on another fullName - * Injecting one fullName on a type - - Example: - - ```javascript - var registry = new Registry(); - var container = registry.container(); - - registry.register('source:main', Source); - registry.register('model:user', User); - registry.register('model:post', Post); - - // injecting one fullName on another fullName - // eg. each user model gets a post model - registry.injection('model:user', 'post', 'model:post'); - - // injecting one fullName on another type - registry.injection('model', 'source', 'source:main'); - - var user = container.lookup('model:user'); - var post = container.lookup('model:post'); - - user.source instanceof Source; //=> true - post.source instanceof Source; //=> true - - user.post instanceof Post; //=> true - - // and both models share the same source - user.source === post.source; //=> true - ``` - - @method injection - @param {String} factoryName - @param {String} property - @param {String} injectionName - */ - injection: function(fullName, property, injectionName) { - this.validateFullName(injectionName); - var normalizedInjectionName = this.normalize(injectionName); - - if (fullName.indexOf(':') === -1) { - return this.typeInjection(fullName, property, normalizedInjectionName); - } - - Ember['default'].assert('fullName must be a proper full name', this.validateFullName(fullName)); - var normalizedName = this.normalize(fullName); - - var injections = this._injections[normalizedName] || - (this._injections[normalizedName] = []); - - injections.push({ - property: property, - fullName: normalizedInjectionName - }); - }, - - - /** - Used only via `factoryInjection`. - - Provides a specialized form of injection, specifically enabling - all factory of one type to be injected with a reference to another - object. - - For example, provided each factory of type `model` needed a `store`. - one would do the following: - - ```javascript - var registry = new Registry(); - - registry.register('store:main', SomeStore); - - registry.factoryTypeInjection('model', 'store', 'store:main'); - - var store = registry.lookup('store:main'); - var UserFactory = registry.lookupFactory('model:user'); - - UserFactory.store instanceof SomeStore; //=> true - ``` - - @private - @method factoryTypeInjection - @param {String} type - @param {String} property - @param {String} fullName - */ - factoryTypeInjection: function(type, property, fullName) { - var injections = this._factoryTypeInjections[type] || - (this._factoryTypeInjections[type] = []); - - injections.push({ - property: property, - fullName: this.normalize(fullName) - }); - }, - - /** - Defines factory injection rules. - - Similar to regular injection rules, but are run against factories, via - `Registry#lookupFactory`. - - These rules are used to inject objects onto factories when they - are looked up. - - Two forms of injections are possible: - - * Injecting one fullName on another fullName - * Injecting one fullName on a type - - Example: - - ```javascript - var registry = new Registry(); - var container = registry.container(); - - registry.register('store:main', Store); - registry.register('store:secondary', OtherStore); - registry.register('model:user', User); - registry.register('model:post', Post); - - // injecting one fullName on another type - registry.factoryInjection('model', 'store', 'store:main'); - - // injecting one fullName on another fullName - registry.factoryInjection('model:post', 'secondaryStore', 'store:secondary'); - - var UserFactory = container.lookupFactory('model:user'); - var PostFactory = container.lookupFactory('model:post'); - var store = container.lookup('store:main'); - - UserFactory.store instanceof Store; //=> true - UserFactory.secondaryStore instanceof OtherStore; //=> false - - PostFactory.store instanceof Store; //=> true - PostFactory.secondaryStore instanceof OtherStore; //=> true - - // and both models share the same source instance - UserFactory.store === PostFactory.store; //=> true - ``` - - @method factoryInjection - @param {String} factoryName - @param {String} property - @param {String} injectionName - */ - factoryInjection: function(fullName, property, injectionName) { - var normalizedName = this.normalize(fullName); - var normalizedInjectionName = this.normalize(injectionName); - - this.validateFullName(injectionName); - - if (fullName.indexOf(':') === -1) { - return this.factoryTypeInjection(normalizedName, property, normalizedInjectionName); - } - - var injections = this._factoryInjections[normalizedName] || (this._factoryInjections[normalizedName] = []); - - injections.push({ - property: property, - fullName: normalizedInjectionName - }); - }, - - validateFullName: function(fullName) { - if (!VALID_FULL_NAME_REGEXP.test(fullName)) { - throw new TypeError('Invalid Fullname, expected: `type:name` got: ' + fullName); - } - return true; - }, - - validateInjections: function(injections) { - if (!injections) { return; } - - var fullName; - - for (var i = 0, length = injections.length; i < length; i++) { - fullName = injections[i].fullName; - - if (!this.has(fullName)) { - throw new Error('Attempting to inject an unknown injection: `' + fullName + '`'); - } - } - }, - - normalizeInjectionsHash: function(hash) { - var injections = []; - - for (var key in hash) { - if (hash.hasOwnProperty(key)) { - Ember['default'].assert("Expected a proper full name, given '" + hash[key] + "'", this.validateFullName(hash[key])); - - injections.push({ - property: key, - fullName: hash[key] - }); - } - } - - return injections; - }, - - getInjections: function(fullName) { - var injections = this._injections[fullName] || []; - if (this.fallback) { - injections = injections.concat(this.fallback.getInjections(fullName)); - } - return injections; - }, - - getTypeInjections: function(type) { - var injections = this._typeInjections[type] || []; - if (this.fallback) { - injections = injections.concat(this.fallback.getTypeInjections(type)); - } - return injections; - }, - - getFactoryInjections: function(fullName) { - var injections = this._factoryInjections[fullName] || []; - if (this.fallback) { - injections = injections.concat(this.fallback.getFactoryInjections(fullName)); - } - return injections; - }, - - getFactoryTypeInjections: function(type) { - var injections = this._factoryTypeInjections[type] || []; - if (this.fallback) { - injections = injections.concat(this.fallback.getFactoryTypeInjections(type)); - } - return injections; - } - }; - - function resolve(registry, normalizedName) { - var cached = registry._resolveCache[normalizedName]; - if (cached) { return cached; } - - var resolved = registry.resolver(normalizedName) || registry.registrations[normalizedName]; - registry._resolveCache[normalizedName] = resolved; - - return resolved; - } - - function has(registry, fullName) { - return registry.resolve(fullName) !== undefined; - } - - exports['default'] = Registry; - -}); -enifed("dag-map", - ["exports"], - function(__exports__) { - "use strict"; - function visit(vertex, fn, visited, path) { - var name = vertex.name; - var vertices = vertex.incoming; - var names = vertex.incomingNames; - var len = names.length; - var i; - - if (!visited) { - visited = {}; - } - if (!path) { - path = []; - } - if (visited.hasOwnProperty(name)) { - return; - } - path.push(name); - visited[name] = true; - for (i = 0; i < len; i++) { - visit(vertices[names[i]], fn, visited, path); - } - fn(vertex, path); - path.pop(); - } - - - /** - * DAG stands for Directed acyclic graph. - * - * It is used to build a graph of dependencies checking that there isn't circular - * dependencies. p.e Registering initializers with a certain precedence order. - * - * @class DAG - * @constructor - */ - function DAG() { - this.names = []; - this.vertices = Object.create(null); - } - - /** - * DAG Vertex - * - * @class Vertex - * @constructor - */ - - function Vertex(name) { - this.name = name; - this.incoming = {}; - this.incomingNames = []; - this.hasOutgoing = false; - this.value = null; - } - - /** - * Adds a vertex entry to the graph unless it is already added. - * - * @private - * @method add - * @param {String} name The name of the vertex to add - */ - DAG.prototype.add = function(name) { - if (!name) { - throw new Error("Can't add Vertex without name"); - } - if (this.vertices[name] !== undefined) { - return this.vertices[name]; - } - var vertex = new Vertex(name); - this.vertices[name] = vertex; - this.names.push(name); - return vertex; - }; - - /** - * Adds a vertex to the graph and sets its value. - * - * @private - * @method map - * @param {String} name The name of the vertex. - * @param value The value to put in the vertex. - */ - DAG.prototype.map = function(name, value) { - this.add(name).value = value; - }; - - /** - * Connects the vertices with the given names, adding them to the graph if - * necessary, only if this does not produce is any circular dependency. - * - * @private - * @method addEdge - * @param {String} fromName The name the vertex where the edge starts. - * @param {String} toName The name the vertex where the edge ends. - */ - DAG.prototype.addEdge = function(fromName, toName) { - if (!fromName || !toName || fromName === toName) { - return; - } - var from = this.add(fromName); - var to = this.add(toName); - if (to.incoming.hasOwnProperty(fromName)) { - return; - } - function checkCycle(vertex, path) { - if (vertex.name === toName) { - throw new Error("cycle detected: " + toName + " <- " + path.join(" <- ")); - } - } - visit(from, checkCycle); - from.hasOutgoing = true; - to.incoming[fromName] = from; - to.incomingNames.push(fromName); - }; - - /** - * Visits all the vertex of the graph calling the given function with each one, - * ensuring that the vertices are visited respecting their precedence. - * - * @method topsort - * @param {Function} fn The function to be invoked on each vertex. - */ - DAG.prototype.topsort = function(fn) { - var visited = {}; - var vertices = this.vertices; - var names = this.names; - var len = names.length; - var i, vertex; - - for (i = 0; i < len; i++) { - vertex = vertices[names[i]]; - if (!vertex.hasOutgoing) { - visit(vertex, fn, visited); - } - } - }; - - /** - * Adds a vertex with the given name and value to the graph and joins it with the - * vertices referenced in _before_ and _after_. If there isn't vertices with those - * names, they are added too. - * - * If either _before_ or _after_ are falsy/empty, the added vertex will not have - * an incoming/outgoing edge. - * - * @method addEdges - * @param {String} name The name of the vertex to be added. - * @param value The value of that vertex. - * @param before An string or array of strings with the names of the vertices before - * which this vertex must be visited. - * @param after An string or array of strings with the names of the vertex after - * which this vertex must be visited. - * - */ - DAG.prototype.addEdges = function(name, value, before, after) { - var i; - this.map(name, value); - if (before) { - if (typeof before === 'string') { - this.addEdge(name, before); - } else { - for (i = 0; i < before.length; i++) { - this.addEdge(name, before[i]); - } - } - } - if (after) { - if (typeof after === 'string') { - this.addEdge(after, name); - } else { - for (i = 0; i < after.length; i++) { - this.addEdge(after[i], name); - } - } - } - }; - - __exports__["default"] = DAG; - }); -enifed("dag-map.umd", - ["./dag-map"], - function(__dependency1__) { - "use strict"; - var DAG = __dependency1__["default"]; - - /* global define:true module:true window: true */ - if (typeof enifed === 'function' && enifed.amd) { - enifed(function() { return DAG; }); - } else if (typeof module !== 'undefined' && module.exports) { - module.exports = DAG; - } else if (typeof this !== 'undefined') { - this['DAG'] = DAG; - } - }); -enifed("dom-helper", - ["./morph-range","./morph-attr","./dom-helper/build-html-dom","./dom-helper/classes","./dom-helper/prop","exports"], - function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __exports__) { - "use strict"; - var Morph = __dependency1__["default"]; - var AttrMorph = __dependency2__["default"]; - var buildHTMLDOM = __dependency3__.buildHTMLDOM; - var svgNamespace = __dependency3__.svgNamespace; - var svgHTMLIntegrationPoints = __dependency3__.svgHTMLIntegrationPoints; - var addClasses = __dependency4__.addClasses; - var removeClasses = __dependency4__.removeClasses; - var normalizeProperty = __dependency5__.normalizeProperty; - var isAttrRemovalValue = __dependency5__.isAttrRemovalValue; - - var doc = typeof document === 'undefined' ? false : document; - - var deletesBlankTextNodes = doc && (function(document){ - var element = document.createElement('div'); - element.appendChild( document.createTextNode('') ); - var clonedElement = element.cloneNode(true); - return clonedElement.childNodes.length === 0; - })(doc); - - var ignoresCheckedAttribute = doc && (function(document){ - var element = document.createElement('input'); - element.setAttribute('checked', 'checked'); - var clonedElement = element.cloneNode(false); - return !clonedElement.checked; - })(doc); - - var canRemoveSvgViewBoxAttribute = doc && (doc.createElementNS ? (function(document){ - var element = document.createElementNS(svgNamespace, 'svg'); - element.setAttribute('viewBox', '0 0 100 100'); - element.removeAttribute('viewBox'); - return !element.getAttribute('viewBox'); - })(doc) : true); - - var canClone = doc && (function(document){ - var element = document.createElement('div'); - element.appendChild( document.createTextNode(' ')); - element.appendChild( document.createTextNode(' ')); - var clonedElement = element.cloneNode(true); - return clonedElement.childNodes[0].nodeValue === ' '; - })(doc); - - // This is not the namespace of the element, but of - // the elements inside that elements. - function interiorNamespace(element){ - if ( - element && - element.namespaceURI === svgNamespace && - !svgHTMLIntegrationPoints[element.tagName] - ) { - return svgNamespace; - } else { - return null; - } - } - - // The HTML spec allows for "omitted start tags". These tags are optional - // when their intended child is the first thing in the parent tag. For - // example, this is a tbody start tag: - // - // - // - // - // - // The tbody may be omitted, and the browser will accept and render: - // - //
- // - // - // However, the omitted start tag will still be added to the DOM. Here - // we test the string and context to see if the browser is about to - // perform this cleanup. - // - // http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#optional-tags - // describes which tags are omittable. The spec for tbody and colgroup - // explains this behavior: - // - // http://www.whatwg.org/specs/web-apps/current-work/multipage/tables.html#the-tbody-element - // http://www.whatwg.org/specs/web-apps/current-work/multipage/tables.html#the-colgroup-element - // - - var omittedStartTagChildTest = /<([\w:]+)/; - function detectOmittedStartTag(string, contextualElement){ - // Omitted start tags are only inside table tags. - if (contextualElement.tagName === 'TABLE') { - var omittedStartTagChildMatch = omittedStartTagChildTest.exec(string); - if (omittedStartTagChildMatch) { - var omittedStartTagChild = omittedStartTagChildMatch[1]; - // It is already asserted that the contextual element is a table - // and not the proper start tag. Just see if a tag was omitted. - return omittedStartTagChild === 'tr' || - omittedStartTagChild === 'col'; - } - } - } - - function buildSVGDOM(html, dom){ - var div = dom.document.createElement('div'); - div.innerHTML = ''+html+''; - return div.firstChild.childNodes; - } - - /* - * A class wrapping DOM functions to address environment compatibility, - * namespaces, contextual elements for morph un-escaped content - * insertion. - * - * When entering a template, a DOMHelper should be passed: - * - * template(context, { hooks: hooks, dom: new DOMHelper() }); - * - * TODO: support foreignObject as a passed contextual element. It has - * a namespace (svg) that does not match its internal namespace - * (xhtml). - * - * @class DOMHelper - * @constructor - * @param {HTMLDocument} _document The document DOM methods are proxied to - */ - function DOMHelper(_document){ - this.document = _document || document; - if (!this.document) { - throw new Error("A document object must be passed to the DOMHelper, or available on the global scope"); - } - this.canClone = canClone; - this.namespace = null; - } - - var prototype = DOMHelper.prototype; - prototype.constructor = DOMHelper; - - prototype.getElementById = function(id, rootNode) { - rootNode = rootNode || this.document; - return rootNode.getElementById(id); - }; - - prototype.insertBefore = function(element, childElement, referenceChild) { - return element.insertBefore(childElement, referenceChild); - }; - - prototype.appendChild = function(element, childElement) { - return element.appendChild(childElement); - }; - - prototype.childAt = function(element, indices) { - var child = element; - - for (var i = 0; i < indices.length; i++) { - child = child.childNodes.item(indices[i]); - } - - return child; - }; - - // Note to a Fellow Implementor: - // Ahh, accessing a child node at an index. Seems like it should be so simple, - // doesn't it? Unfortunately, this particular method has caused us a surprising - // amount of pain. As you'll note below, this method has been modified to walk - // the linked list of child nodes rather than access the child by index - // directly, even though there are two (2) APIs in the DOM that do this for us. - // If you're thinking to yourself, "What an oversight! What an opportunity to - // optimize this code!" then to you I say: stop! For I have a tale to tell. - // - // First, this code must be compatible with simple-dom for rendering on the - // server where there is no real DOM. Previously, we accessed a child node - // directly via `element.childNodes[index]`. While we *could* in theory do a - // full-fidelity simulation of a live `childNodes` array, this is slow, - // complicated and error-prone. - // - // "No problem," we thought, "we'll just use the similar - // `childNodes.item(index)` API." Then, we could just implement our own `item` - // method in simple-dom and walk the child node linked list there, allowing - // us to retain the performance advantages of the (surely optimized) `item()` - // API in the browser. - // - // Unfortunately, an enterprising soul named Samy Alzahrani discovered that in - // IE8, accessing an item out-of-bounds via `item()` causes an exception where - // other browsers return null. This necessitated a... check of - // `childNodes.length`, bringing us back around to having to support a - // full-fidelity `childNodes` array! - // - // Worst of all, Kris Selden investigated how browsers are actualy implemented - // and discovered that they're all linked lists under the hood anyway. Accessing - // `childNodes` requires them to allocate a new live collection backed by that - // linked list, which is itself a rather expensive operation. Our assumed - // optimization had backfired! That is the danger of magical thinking about - // the performance of native implementations. - // - // And this, my friends, is why the following implementation just walks the - // linked list, as surprised as that may make you. Please ensure you understand - // the above before changing this and submitting a PR. - // - // Tom Dale, January 18th, 2015, Portland OR - prototype.childAtIndex = function(element, index) { - var node = element.firstChild; - - for (var idx = 0; node && idx < index; idx++) { - node = node.nextSibling; - } - - return node; - }; - - prototype.appendText = function(element, text) { - return element.appendChild(this.document.createTextNode(text)); - }; - - prototype.setAttribute = function(element, name, value) { - element.setAttribute(name, String(value)); - }; - - prototype.setAttributeNS = function(element, namespace, name, value) { - element.setAttributeNS(namespace, name, String(value)); - }; - - if (canRemoveSvgViewBoxAttribute){ - prototype.removeAttribute = function(element, name) { - element.removeAttribute(name); - }; - } else { - prototype.removeAttribute = function(element, name) { - if (element.tagName === 'svg' && name === 'viewBox') { - element.setAttribute(name, null); - } else { - element.removeAttribute(name); - } - }; - } - - prototype.setPropertyStrict = function(element, name, value) { - element[name] = value; - }; - - prototype.setProperty = function(element, name, value, namespace) { - var lowercaseName = name.toLowerCase(); - if (element.namespaceURI === svgNamespace || lowercaseName === 'style') { - if (isAttrRemovalValue(value)) { - element.removeAttribute(name); - } else { - if (namespace) { - element.setAttributeNS(namespace, name, value); - } else { - element.setAttribute(name, value); - } - } - } else { - var normalized = normalizeProperty(element, name); - if (normalized) { - element[normalized] = value; - } else { - if (isAttrRemovalValue(value)) { - element.removeAttribute(name); - } else { - if (namespace && element.setAttributeNS) { - element.setAttributeNS(namespace, name, value); - } else { - element.setAttribute(name, value); - } - } - } - } - }; - - if (doc && doc.createElementNS) { - // Only opt into namespace detection if a contextualElement - // is passed. - prototype.createElement = function(tagName, contextualElement) { - var namespace = this.namespace; - if (contextualElement) { - if (tagName === 'svg') { - namespace = svgNamespace; - } else { - namespace = interiorNamespace(contextualElement); - } - } - if (namespace) { - return this.document.createElementNS(namespace, tagName); - } else { - return this.document.createElement(tagName); - } - }; - prototype.setAttributeNS = function(element, namespace, name, value) { - element.setAttributeNS(namespace, name, String(value)); - }; - } else { - prototype.createElement = function(tagName) { - return this.document.createElement(tagName); - }; - prototype.setAttributeNS = function(element, namespace, name, value) { - element.setAttribute(name, String(value)); - }; - } - - prototype.addClasses = addClasses; - prototype.removeClasses = removeClasses; - - prototype.setNamespace = function(ns) { - this.namespace = ns; - }; - - prototype.detectNamespace = function(element) { - this.namespace = interiorNamespace(element); - }; - - prototype.createDocumentFragment = function(){ - return this.document.createDocumentFragment(); - }; - - prototype.createTextNode = function(text){ - return this.document.createTextNode(text); - }; - - prototype.createComment = function(text){ - return this.document.createComment(text); - }; - - prototype.repairClonedNode = function(element, blankChildTextNodes, isChecked){ - if (deletesBlankTextNodes && blankChildTextNodes.length > 0) { - for (var i=0, len=blankChildTextNodes.length;i 0) { - var currentNode = childNodes[0]; - - // We prepend an
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{i18n 'admin.user.tl3_requirements.value_heading'}}{{i18n 'admin.user.tl3_requirements.requirement_heading'}}
{{i18n 'admin.user.tl3_requirements.visits'}} + {{model.tl3Requirements.days_visited_percent}}% ({{model.tl3Requirements.days_visited}} / {{model.tl3Requirements.time_period}} {{i18n 'admin.user.tl3_requirements.days'}}) + {{model.tl3Requirements.min_days_visited_percent}}%
{{i18n 'admin.user.tl3_requirements.topics_replied_to'}}{{model.tl3Requirements.num_topics_replied_to}}{{model.tl3Requirements.min_topics_replied_to}}
{{i18n 'admin.user.tl3_requirements.topics_viewed'}}{{model.tl3Requirements.topics_viewed}}{{model.tl3Requirements.min_topics_viewed}}
{{i18n 'admin.user.tl3_requirements.topics_viewed_all_time'}}{{model.tl3Requirements.topics_viewed_all_time}}{{model.tl3Requirements.min_topics_viewed_all_time}}
{{i18n 'admin.user.tl3_requirements.posts_read'}}{{model.tl3Requirements.posts_read}}{{model.tl3Requirements.min_posts_read}}
{{i18n 'admin.user.tl3_requirements.posts_read_all_time'}}{{model.tl3Requirements.posts_read_all_time}}{{model.tl3Requirements.min_posts_read_all_time}}
{{i18n 'admin.user.tl3_requirements.flagged_posts'}}{{model.tl3Requirements.num_flagged_posts}}{{i18n 'max_of_count' count=model.tl3Requirements.max_flagged_posts}}
{{i18n 'admin.user.tl3_requirements.flagged_by_users'}}{{model.tl3Requirements.num_flagged_by_users}}{{i18n 'max_of_count' count=model.tl3Requirements.max_flagged_by_users}}
{{i18n 'admin.user.tl3_requirements.likes_given'}}{{model.tl3Requirements.num_likes_given}}{{model.tl3Requirements.min_likes_given}}
{{i18n 'admin.user.tl3_requirements.likes_received'}}{{model.tl3Requirements.num_likes_received}}{{model.tl3Requirements.min_likes_received}}
{{i18n 'admin.user.tl3_requirements.likes_received_days'}}{{model.tl3Requirements.num_likes_received_days}}{{model.tl3Requirements.min_likes_received_days}}
{{i18n 'admin.user.tl3_requirements.likes_received_users'}}{{model.tl3Requirements.num_likes_received_users}}{{model.tl3Requirements.min_likes_received_users}}
+ +
+

+ {{#if model.istl3}} + {{#if model.tl3Requirements.requirements_lost}} + {{! tl implicitly not locked }} + {{#if model.tl3Requirements.on_grace_period}} + {{i18n 'admin.user.tl3_requirements.on_grace_period'}} + {{else}} {{! not on grace period }} + {{i18n 'admin.user.tl3_requirements.does_not_qualify'}} + {{i18n 'admin.user.tl3_requirements.will_be_demoted'}} + {{/if}} + {{else}} {{! requirements not lost - remains tl3 }} + {{#if model.tl3Requirements.trust_level_locked}} + {{i18n 'admin.user.tl3_requirements.locked_will_not_be_demoted'}} + {{else}} {{! tl not locked }} + {{i18n 'admin.user.tl3_requirements.qualifies'}} + {{#if model.tl3Requirements.on_grace_period}} + {{i18n 'admin.user.tl3_requirements.on_grace_period'}} + {{/if}} + {{/if}} + {{/if}} + {{else}} {{! is not tl3 }} + {{#if model.tl3Requirements.requirements_met}} + {{! met & not tl3 - will be promoted}} + {{i18n 'admin.user.tl3_requirements.qualifies'}} + {{i18n 'admin.user.tl3_requirements.will_be_promoted'}} + {{else}} {{! requirements not met - remains regular }} + {{#if model.tl3Requirements.trust_level_locked}} + {{i18n 'admin.user.tl3_requirements.locked_will_not_be_promoted'}} + {{else}} + {{i18n 'admin.user.tl3_requirements.does_not_qualify'}} + {{/if}} + {{/if}} + {{/if}} +

+ diff --git a/app/assets/javascripts/admin/templates/user_tl3_requirements.hbs b/app/assets/javascripts/admin/templates/user_tl3_requirements.hbs deleted file mode 100644 index db924081e5..0000000000 --- a/app/assets/javascripts/admin/templates/user_tl3_requirements.hbs +++ /dev/null @@ -1,137 +0,0 @@ -
-
- -
-
- -
-

{{username}} - {{i18n 'admin.user.tl3_requirements.title'}}

-
-

{{i18n 'admin.user.tl3_requirements.table_title'}}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{i18n 'admin.user.tl3_requirements.value_heading'}}{{i18n 'admin.user.tl3_requirements.requirement_heading'}}
{{i18n 'admin.user.tl3_requirements.visits'}} - {{tl3Requirements.days_visited_percent}}% ({{tl3Requirements.days_visited}} / {{tl3Requirements.time_period}} {{i18n 'admin.user.tl3_requirements.days'}}) - {{tl3Requirements.min_days_visited_percent}}%
{{i18n 'admin.user.tl3_requirements.topics_replied_to'}}{{tl3Requirements.num_topics_replied_to}}{{tl3Requirements.min_topics_replied_to}}
{{i18n 'admin.user.tl3_requirements.topics_viewed'}}{{tl3Requirements.topics_viewed}}{{tl3Requirements.min_topics_viewed}}
{{i18n 'admin.user.tl3_requirements.topics_viewed_all_time'}}{{tl3Requirements.topics_viewed_all_time}}{{tl3Requirements.min_topics_viewed_all_time}}
{{i18n 'admin.user.tl3_requirements.posts_read'}}{{tl3Requirements.posts_read}}{{tl3Requirements.min_posts_read}}
{{i18n 'admin.user.tl3_requirements.posts_read_all_time'}}{{tl3Requirements.posts_read_all_time}}{{tl3Requirements.min_posts_read_all_time}}
{{i18n 'admin.user.tl3_requirements.flagged_posts'}}{{tl3Requirements.num_flagged_posts}}{{i18n 'max_of_count' count=tl3Requirements.max_flagged_posts}}
{{i18n 'admin.user.tl3_requirements.flagged_by_users'}}{{tl3Requirements.num_flagged_by_users}}{{i18n 'max_of_count' count=tl3Requirements.max_flagged_by_users}}
{{i18n 'admin.user.tl3_requirements.likes_given'}}{{tl3Requirements.num_likes_given}}{{tl3Requirements.min_likes_given}}
{{i18n 'admin.user.tl3_requirements.likes_received'}}{{tl3Requirements.num_likes_received}}{{tl3Requirements.min_likes_received}}
{{i18n 'admin.user.tl3_requirements.likes_received_days'}}{{tl3Requirements.num_likes_received_days}}{{tl3Requirements.min_likes_received_days}}
{{i18n 'admin.user.tl3_requirements.likes_received_users'}}{{tl3Requirements.num_likes_received_users}}{{tl3Requirements.min_likes_received_users}}
- -
-

- {{#if istl3}} - {{#if tl3Requirements.requirements_lost}} - {{! tl implicitly not locked }} - {{#if tl3Requirements.on_grace_period}} - {{i18n 'admin.user.tl3_requirements.on_grace_period'}} - {{else}} {{! not on grace period }} - {{i18n 'admin.user.tl3_requirements.does_not_qualify'}} - {{i18n 'admin.user.tl3_requirements.will_be_demoted'}} - {{/if}} - {{else}} {{! requirements not lost - remains tl3 }} - {{#if tl3Requirements.trust_level_locked}} - {{i18n 'admin.user.tl3_requirements.locked_will_not_be_demoted'}} - {{else}} {{! tl not locked }} - {{i18n 'admin.user.tl3_requirements.qualifies'}} - {{#if tl3Requirements.on_grace_period}} - {{i18n 'admin.user.tl3_requirements.on_grace_period'}} - {{/if}} - {{/if}} - {{/if}} - {{else}} {{! is not tl3 }} - {{#if tl3Requirements.requirements_met}} - {{! met & not tl3 - will be promoted}} - {{i18n 'admin.user.tl3_requirements.qualifies'}} - {{i18n 'admin.user.tl3_requirements.will_be_promoted'}} - {{else}} {{! requirements not met - remains regular }} - {{#if tl3Requirements.trust_level_locked}} - {{i18n 'admin.user.tl3_requirements.locked_will_not_be_promoted'}} - {{else}} - {{i18n 'admin.user.tl3_requirements.does_not_qualify'}} - {{/if}} - {{/if}} - {{/if}} -

-
From 946e34f65cfcb02018c344a1ec0fff4f95dfacac Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 13 Aug 2015 14:55:53 -0400 Subject: [PATCH 014/237] Use eslint in `docker_test` --- lib/tasks/docker.rake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake index 430d9d768c..66f0660ca0 100644 --- a/lib/tasks/docker.rake +++ b/lib/tasks/docker.rake @@ -34,6 +34,8 @@ task 'docker:test' do @good &&= run_or_fail("bundle exec rspec") end unless ENV["RUBY_ONLY"] + @good &&= run_or_fail("eslint --ext \".es6\" app/assets/javascripts") + @good &&= run_or_fail("eslint --ext \".es6\" test/javascripts") @good &&= run_or_fail("bundle exec rake qunit:test") end From b0541500b4a14a98b8a2a4cbddf0491ad59affc4 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 13 Aug 2015 15:14:08 -0400 Subject: [PATCH 015/237] FIX: eslint deprecations --- .../controllers/admin-user-badges.js.es6 | 15 +++--------- .../components/navigation-bar.js.es6 | 4 ++-- .../discourse/components/topic-list.js.es6 | 4 ++-- .../discourse/helpers/application.js.es6 | 4 ++-- .../discourse/initializers/banner.js.es6 | 4 ++-- .../discourse/lib/after-transition.js.es6 | 4 ++-- .../discourse/lib/autocomplete.js.es6 | 3 +-- .../discourse/lib/avatar-template.js.es6 | 1 + .../discourse/lib/emoji/emoji-toolbar.js.es6 | 4 ++-- .../discourse/models/post-stream.js.es6 | 12 +++++----- .../javascripts/discourse/models/post.js.es6 | 9 ++++---- .../discourse/models/user-stream.js.es6 | 10 ++++---- .../javascripts/discourse/models/user.js.es6 | 23 ++++++++----------- .../discourse/routes/build-topic-route.js.es6 | 4 ++-- .../discourse/routes/discourse.js.es6 | 2 +- .../javascripts/discourse/routes/user.js.es6 | 2 +- .../discourse/views/cloaked-collection.js.es6 | 1 + .../discourse/views/composer.js.es6 | 4 ++-- .../javascripts/discourse/views/header.js.es6 | 1 - .../javascripts/discourse/views/post.js.es6 | 4 ++-- lib/tasks/docker.rake | 2 ++ .../components/keyboard-shortcuts-test.js.es6 | 4 ++-- .../controllers/create-account-test.js.es6 | 8 +++---- 23 files changed, 59 insertions(+), 70 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 index 5e6bb325fd..523668556c 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 @@ -1,12 +1,3 @@ -/** - This controller supports the interface for granting and revoking badges from - individual users. - - @class AdminUserBadgesController - @extends Ember.ArrayController - @namespace Discourse - @module Discourse -**/ export default Ember.ArrayController.extend({ needs: ["adminUser"], user: Em.computed.alias('controllers.adminUser'), @@ -14,12 +5,12 @@ export default Ember.ArrayController.extend({ sortAscending: false, groupedBadges: function(){ - const badges = this.get('model'); + const allBadges = this.get('model'); - var grouped = _.groupBy(badges, badge => badge.badge_id); + var grouped = _.groupBy(allBadges, badge => badge.badge_id); var expanded = []; - const expandedBadges = badges.get('expandedBadges'); + const expandedBadges = allBadges.get('expandedBadges'); _(grouped).each(function(badges){ var lastGranted = badges[0].granted_at; diff --git a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 index a7ceba10cc..46fc846dbe 100644 --- a/app/assets/javascripts/discourse/components/navigation-bar.js.es6 +++ b/app/assets/javascripts/discourse/components/navigation-bar.js.es6 @@ -8,8 +8,8 @@ export default Ember.Component.extend({ const filterMode = this.get('filterMode'), navItems = this.get('navItems'); - var item = navItems.find(function(item){ - return item.get('filterMode').indexOf(filterMode) === 0; + var item = navItems.find(function(i){ + return i.get('filterMode').indexOf(filterMode) === 0; }); return item || navItems[0]; diff --git a/app/assets/javascripts/discourse/components/topic-list.js.es6 b/app/assets/javascripts/discourse/components/topic-list.js.es6 index 865f5fb9a1..2cc0747b0e 100644 --- a/app/assets/javascripts/discourse/components/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list.js.es6 @@ -45,8 +45,8 @@ export default Ember.Component.extend({ this.rerender(); }); - on('th.sortable', function(e){ - this.sendAction('changeSort', e.data('sort-order')); + on('th.sortable', function(e2){ + this.sendAction('changeSort', e2.data('sort-order')); this.rerender(); }); } diff --git a/app/assets/javascripts/discourse/helpers/application.js.es6 b/app/assets/javascripts/discourse/helpers/application.js.es6 index 35c4f64936..cac2ea1f0a 100644 --- a/app/assets/javascripts/discourse/helpers/application.js.es6 +++ b/app/assets/javascripts/discourse/helpers/application.js.es6 @@ -19,8 +19,8 @@ Em.Handlebars.helper('bound-avatar', function(user, size, uploadId) { /* * Used when we only have a template */ -Em.Handlebars.helper('bound-avatar-template', function(avatarTemplate, size) { - return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: avatarTemplate })); +Em.Handlebars.helper('bound-avatar-template', function(at, size) { + return new safe(Discourse.Utilities.avatarImg({ size: size, avatarTemplate: at })); }); registerUnbound('raw-date', function(dt) { diff --git a/app/assets/javascripts/discourse/initializers/banner.js.es6 b/app/assets/javascripts/discourse/initializers/banner.js.es6 index a7dd7b61ef..f8bf99e558 100644 --- a/app/assets/javascripts/discourse/initializers/banner.js.es6 +++ b/app/assets/javascripts/discourse/initializers/banner.js.es6 @@ -12,8 +12,8 @@ export default { const messageBus = container.lookup('message-bus:main'); if (!messageBus) { return; } - messageBus.subscribe("/site/banner", function (banner) { - site.set("banner", Em.Object.create(banner)); + messageBus.subscribe("/site/banner", function (ban) { + site.set("banner", Em.Object.create(ban)); }); } }; diff --git a/app/assets/javascripts/discourse/lib/after-transition.js.es6 b/app/assets/javascripts/discourse/lib/after-transition.js.es6 index 30cc3054f0..11fe20b5e4 100644 --- a/app/assets/javascripts/discourse/lib/after-transition.js.es6 +++ b/app/assets/javascripts/discourse/lib/after-transition.js.es6 @@ -12,7 +12,7 @@ var dummy = document.createElement("div"), ms: "MSTransitionEnd" }; -var transitionEnd = function() { +var transitionEnd = (function() { var retValue; retValue = "transitionend"; Object.keys(eventNameHash).some(function(vendor) { @@ -22,7 +22,7 @@ var transitionEnd = function() { } }); return retValue; -}(); +})(); export default function (element, callback) { return $(element).on(transitionEnd, callback); diff --git a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 index d3f2ea0d17..aeca46186e 100644 --- a/app/assets/javascripts/discourse/lib/autocomplete.js.es6 +++ b/app/assets/javascripts/discourse/lib/autocomplete.js.es6 @@ -290,7 +290,7 @@ export default function(options) { }); $(this).on('keydown.autocomplete', function(e) { - var c, caretPosition, i, initial, next, prev, prevIsGood, stopFound, term, total, userToComplete; + var c, caretPosition, i, initial, prev, prevIsGood, stopFound, term, total, userToComplete; if(e.ctrlKey || e.altKey || e.metaKey){ return true; @@ -322,7 +322,6 @@ export default function(options) { if (e.which === keys.shift) return; if ((completeStart === null) && e.which === keys.backSpace && options.key) { c = Discourse.Utilities.caretPosition(me[0]); - next = me[0].value[c]; c -= 1; initial = c; prevIsGood = true; diff --git a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 index 28032d2a48..16b70cadc7 100644 --- a/app/assets/javascripts/discourse/lib/avatar-template.js.es6 +++ b/app/assets/javascripts/discourse/lib/avatar-template.js.es6 @@ -1,3 +1,4 @@ +/*eslint no-bitwise:0 */ let _splitAvatars; function defaultAvatar(username) { diff --git a/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 b/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 index c5b731da09..7ea7a7b029 100644 --- a/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 +++ b/app/assets/javascripts/discourse/lib/emoji/emoji-toolbar.js.es6 @@ -186,8 +186,8 @@ var bindEvents = function(page,offset){ }); $('.emoji-modal .toolbar a').click(function(){ - var page = parseInt($(this).data('group-id')); - render(page,0); + var p = parseInt($(this).data('group-id')); + render(p, 0); return false; }); }; diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index d8549b4ba4..9f625f3477 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -234,12 +234,12 @@ const PostStream = RestModel.extend({ this.set('gaps', this.get('gaps') || {before: {}, after: {}}); const before = this.get('gaps.before'); - const post = posts.find(function(post){ - return post.get('post_number') > to; + const post = posts.find(function(p){ + return p.get('post_number') > to; }); - before[post.get('id')] = remove.map(function(post){ - return post.get('id'); + before[post.get('id')] = remove.map(function(p){ + return p.get('id'); }); post.set('hasGap', true); @@ -491,8 +491,8 @@ const PostStream = RestModel.extend({ // we need to zip this into the stream let index = 0; - stream.forEach(function(postId){ - if(postId < p.id){ + stream.forEach(function(pid){ + if (pid < p.id){ index+= 1; } }); diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 878ce21c08..c8c4b400ff 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -3,6 +3,7 @@ import { popupAjaxError } from 'discourse/lib/ajax-error'; import ActionSummary from 'discourse/models/action-summary'; import { url, fmt, propertyEqual } from 'discourse/lib/computed'; import Quote from 'discourse/lib/quote'; +import computed from 'ember-addons/ember-computed-decorators'; const Post = RestModel.extend({ @@ -54,10 +55,10 @@ const Post = RestModel.extend({ }.property('post_number', 'topic_id', 'topic.slug'), // Don't drop the /1 - urlWithNumber: function() { - const url = this.get('url'); - return (this.get('post_number') === 1) ? url + "/1" : url; - }.property('post_number', 'url'), + @computed('post_number', 'url') + urlWithNumber(postNumber, postUrl) { + return postNumber === 1 ? postUrl + "/1" : postUrl; + }, usernameUrl: url('username', '/users/%@'), diff --git a/app/assets/javascripts/discourse/models/user-stream.js.es6 b/app/assets/javascripts/discourse/models/user-stream.js.es6 index 71d1bba89c..fe20fe7c54 100644 --- a/app/assets/javascripts/discourse/models/user-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/user-stream.js.es6 @@ -54,18 +54,18 @@ export default RestModel.extend({ findItems() { const self = this; - let url = this.get('baseUrl'); + let findUrl = this.get('baseUrl'); if (this.get('filterParam')) { - url += "&filter=" + this.get('filterParam'); + findUrl += "&filter=" + this.get('filterParam'); } // Don't load the same stream twice. We're probably at the end. const lastLoadedUrl = this.get('lastLoadedUrl'); - if (lastLoadedUrl === url) { return Ember.RSVP.resolve(); } + if (lastLoadedUrl === findUrl) { return Ember.RSVP.resolve(); } if (this.get('loading')) { return Ember.RSVP.resolve(); } this.set('loading', true); - return Discourse.ajax(url, {cache: 'false'}).then( function(result) { + return Discourse.ajax(findUrl, {cache: 'false'}).then( function(result) { if (result && result.user_actions) { const copy = Em.A(); result.user_actions.forEach(function(action) { @@ -81,7 +81,7 @@ export default RestModel.extend({ } }).finally(function() { self.set('loading', false); - self.set('lastLoadedUrl', url); + self.set('lastLoadedUrl', findUrl); }); } diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 669a3853b9..e270aaff7a 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -5,6 +5,7 @@ import UserStream from 'discourse/models/user-stream'; import UserPostsStream from 'discourse/models/user-posts-stream'; import Singleton from 'discourse/mixins/singleton'; import { longDate } from 'discourse/lib/formatter'; +import computed from 'ember-addons/ember-computed-decorators'; const User = RestModel.extend({ @@ -53,17 +54,11 @@ const User = RestModel.extend({ return this.get('username'); }.property('username', 'name'), - /** - This user's profile background(in CSS). - - @property profileBackground - @type {String} - **/ - profileBackground: function() { - var url = this.get('profile_background'); - if (Em.isEmpty(url) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; } - return ('background-image: url(' + Discourse.getURLWithCDN(url) + ')').htmlSafe(); - }.property('profile_background'), + @computed('profile_background') + profileBackground(bgUrl) { + if (Em.isEmpty(bgUrl) || !Discourse.SiteSettings.allow_profile_backgrounds) { return; } + return ('background-image: url(' + Discourse.getURLWithCDN(bgUrl) + ')').htmlSafe(); + }, /** Path to this user. @@ -206,10 +201,10 @@ const User = RestModel.extend({ return Discourse.ajax("/users/" + this.get('username_lower'), { data: data, type: 'PUT' - }).then(function(data) { - self.set('bio_excerpt',data.user.bio_excerpt); + }).then(function(result) { + self.set('bio_excerpt', result.user.bio_excerpt); - var userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); + const userProps = self.getProperties('enable_quoting', 'external_links_in_new_tab', 'dynamic_favicon'); Discourse.User.current().setProperties(userProps); }).finally(() => { this.set('isSaving', false); diff --git a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 index 69cd2e5c22..185c2fd529 100644 --- a/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/build-topic-route.js.es6 @@ -86,9 +86,9 @@ export default function(filter, extras) { ScreenTrack.current().stop(); const findOpts = filterQueryParams(transition.queryParams), - extras = { cached: this.isPoppedState(transition) }; + findExtras = { cached: this.isPoppedState(transition) }; - return findTopicList(this.store, filter, findOpts, extras); + return findTopicList(this.store, filter, findOpts, findExtras); }, titleToken() { diff --git a/app/assets/javascripts/discourse/routes/discourse.js.es6 b/app/assets/javascripts/discourse/routes/discourse.js.es6 index a6bba2cb6a..20ce38f40a 100644 --- a/app/assets/javascripts/discourse/routes/discourse.js.es6 +++ b/app/assets/javascripts/discourse/routes/discourse.js.es6 @@ -13,7 +13,7 @@ const DiscourseRoute = Ember.Route.extend({ params = this.controller.getProperties(Object.keys(this.queryParams)); model.set('loading', true); - this.model(params).then(model => this.setupController(controller, model)); + this.model(params).then(m => this.setupController(controller, m)); } }, diff --git a/app/assets/javascripts/discourse/routes/user.js.es6 b/app/assets/javascripts/discourse/routes/user.js.es6 index 832e1cb0eb..cfbbf173e0 100644 --- a/app/assets/javascripts/discourse/routes/user.js.es6 +++ b/app/assets/javascripts/discourse/routes/user.js.es6 @@ -31,7 +31,7 @@ export default Discourse.Route.extend({ willTransition(transition) { // will reset the indexStream when transitioning to routes that aren't "indexStream" // otherwise the "header" will jump - const isIndexStream = ~INDEX_STREAM_ROUTES.indexOf(transition.targetName); + const isIndexStream = INDEX_STREAM_ROUTES.indexOf(transition.targetName) !== -1; this.controllerFor('user').set('indexStream', isIndexStream); return true; } diff --git a/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 b/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 index 03a121efe8..f65666c78a 100644 --- a/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 +++ b/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 @@ -1,3 +1,4 @@ +/*eslint no-bitwise:0 */ import CloakedView from 'discourse/views/cloaked'; const CloakedCollectionView = Ember.CollectionView.extend({ diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index b48964e271..22ab46f0fc 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -217,7 +217,7 @@ const ComposerView = Ember.View.extend(Ember.Evented, { initEditor() { // not quite right, need a callback to pass in, meaning this gets called once, // but if you start replying to another topic it will get the avatars wrong - let $wmdInput, editor; + let $wmdInput; const self = this; this.wmdInput = $wmdInput = this.$('.wmd-input'); if ($wmdInput.length === 0 || $wmdInput.data('init') === true) return; @@ -243,7 +243,7 @@ const ComposerView = Ember.View.extend(Ember.Evented, { } }); - this.editor = editor = Discourse.Markdown.createEditor({ + this.editor = Discourse.Markdown.createEditor({ containerElement: this.element, lookupAvatarByPostNumber(postNumber, topicId) { const posts = self.get('controller.controllers.topic.model.postStream.posts'); diff --git a/app/assets/javascripts/discourse/views/header.js.es6 b/app/assets/javascripts/discourse/views/header.js.es6 index 281f6bd86b..310a066046 100644 --- a/app/assets/javascripts/discourse/views/header.js.es6 +++ b/app/assets/javascripts/discourse/views/header.js.es6 @@ -55,7 +55,6 @@ export default Ember.View.extend({ $li.removeClass('active'); $html.data('hide-dropdown', null); - const controller = self.get('controller'); if (controller && !controller.isDestroyed){ controller.set('visibleDropdown', null); } diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index eff4293ba3..ad9963aabf 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -256,8 +256,8 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { // Unless it's a full quote, allow click to expand if (!($aside.data('full') || $title.data('has-quote-controls'))) { - $title.on('click', function(e) { - if ($(e.target).is('a')) return true; + $title.on('click', function(e2) { + if ($(e2.target).is('a')) return true; self._toggleQuote($aside); }); $title.data('has-quote-controls', true); diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake index 66f0660ca0..c53ade0868 100644 --- a/lib/tasks/docker.rake +++ b/lib/tasks/docker.rake @@ -34,8 +34,10 @@ task 'docker:test' do @good &&= run_or_fail("bundle exec rspec") end unless ENV["RUBY_ONLY"] + @good &&= run_or_fail("eslint app/assets/javascripts") @good &&= run_or_fail("eslint --ext \".es6\" app/assets/javascripts") @good &&= run_or_fail("eslint --ext \".es6\" test/javascripts") + @good &&= run_or_fail("eslint test/javascripts") @good &&= run_or_fail("bundle exec rake qunit:test") end diff --git a/test/javascripts/components/keyboard-shortcuts-test.js.es6 b/test/javascripts/components/keyboard-shortcuts-test.js.es6 index 40753a3477..c21b8038c9 100644 --- a/test/javascripts/components/keyboard-shortcuts-test.js.es6 +++ b/test/javascripts/components/keyboard-shortcuts-test.js.es6 @@ -93,8 +93,8 @@ _.each(clickBindings, function(selector, binding) { ok(true, selector + " was clicked"); }); - _.each(bindings, function(binding) { - testMouseTrap.trigger(binding); + _.each(bindings, function(b) { + testMouseTrap.trigger(b); }, this); }); }); diff --git a/test/javascripts/controllers/create-account-test.js.es6 b/test/javascripts/controllers/create-account-test.js.es6 index ab3244915e..4c202880aa 100644 --- a/test/javascripts/controllers/create-account-test.js.es6 +++ b/test/javascripts/controllers/create-account-test.js.es6 @@ -37,10 +37,10 @@ test('passwordValidation', function() { equal(controller.get('passwordValidation.reason'), I18n.t('user.password.ok'), 'Password is valid'); var testInvalidPassword = function(password, expectedReason) { - var controller = subject(); - controller.set('accountPassword', password); - equal(controller.get('passwordValidation.failed'), true, 'password should be invalid: ' + password); - equal(controller.get('passwordValidation.reason'), expectedReason, 'password validation reason: ' + password + ', ' + expectedReason); + var c = subject(); + c.set('accountPassword', password); + equal(c.get('passwordValidation.failed'), true, 'password should be invalid: ' + password); + equal(c.get('passwordValidation.reason'), expectedReason, 'password validation reason: ' + password + ', ' + expectedReason); }; testInvalidPassword('', undefined); From a89241f0b92bf8fbf1f2255201004613e0f1153c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 13 Aug 2015 15:19:27 -0400 Subject: [PATCH 016/237] Don't include code in files for jshint anymore, eslint is run on command line --- lib/discourse_iife.rb | 12 +----------- .../tilt/es6_module_transpiler_template.rb | 14 -------------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/lib/discourse_iife.rb b/lib/discourse_iife.rb index 5c25fc6320..d966539bab 100644 --- a/lib/discourse_iife.rb +++ b/lib/discourse_iife.rb @@ -24,17 +24,7 @@ class DiscourseIIFE < Sprockets::Processor return data if path =~ /\.hbrs/ return data if path =~ /\.hbs/ - res = "(function () {\n\nvar $ = window.jQuery;\n// IIFE Wrapped Content Begins:\n\n#{data}\n\n// IIFE Wrapped Content Ends\n\n })(this);" - - # Include JS code for JSHint - unless Rails.env.production? - req_path = path.sub(Rails.root.to_s, '') - .sub("/app/assets/javascripts", "") - .sub("/test/javascripts", "") - res << "\nwindow.__eslintSrc = window.__eslintSrc || {}; window.__eslintSrc['/assets#{req_path}'] = #{data.to_json};\n" - end - - res + "(function () {\n\nvar $ = window.jQuery;\n// IIFE Wrapped Content Begins:\n\n#{data}\n\n// IIFE Wrapped Content Ends\n\n })(this);" end end diff --git a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb index 2a320ebf59..30f575f423 100644 --- a/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb +++ b/lib/es6_module_transpiler/tilt/es6_module_transpiler_template.rb @@ -116,20 +116,6 @@ module Tilt end end - # Include JS code for JSHint - unless Rails.env.production? - if scope.pathname.to_s =~ /js\.es6/ - extension = "js.es6" - elsif scope.pathname.to_s =~ /\.es6/ - extension = "es6" - else - extension = "js" - end - req_path = "/assets/#{scope.logical_path}.#{extension}" - - @output << "\nwindow.__eslintSrc = window.__eslintSrc || {}; window.__eslintSrc['#{req_path}'] = #{data.to_json};\n" - end - @output end From 9b7c4023e88c15027bb5764acec3c4e09e120e6a Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 13 Aug 2015 15:22:33 -0400 Subject: [PATCH 017/237] Run eslint instead of jshint, remove rbx --- .travis.yml | 8 +++++--- lib/tasks/docker.rake | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 352361f0f7..a57a5f7030 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,6 @@ rvm: - 2.0.0 - 2.1 - 2.2 - - rbx-2 services: - redis-server @@ -33,8 +32,11 @@ sudo: false cache: bundler before_install: - - npm i -g jshint - - jshint . + - npm i -g eslint babel-eslint + - eslint app/assets/javascripts + - eslint --ext .es6 app/assets/javascripts + - eslint --ext .es6 test/javascripts + - eslint test/javascripts before_script: - bundle exec rake db:create db:migrate diff --git a/lib/tasks/docker.rake b/lib/tasks/docker.rake index c53ade0868..0045fcee42 100644 --- a/lib/tasks/docker.rake +++ b/lib/tasks/docker.rake @@ -35,8 +35,8 @@ task 'docker:test' do end unless ENV["RUBY_ONLY"] @good &&= run_or_fail("eslint app/assets/javascripts") - @good &&= run_or_fail("eslint --ext \".es6\" app/assets/javascripts") - @good &&= run_or_fail("eslint --ext \".es6\" test/javascripts") + @good &&= run_or_fail("eslint --ext .es6 app/assets/javascripts") + @good &&= run_or_fail("eslint --ext .es6 test/javascripts") @good &&= run_or_fail("eslint test/javascripts") @good &&= run_or_fail("bundle exec rake qunit:test") end From e6b85d5be69e149a99efc544d0aa79fa9fc014c6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 13 Aug 2015 15:51:42 -0400 Subject: [PATCH 018/237] Add image software to travis to stop complaints --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index a57a5f7030..f69939856a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,7 @@ sudo: false cache: bundler before_install: + - sudo apt-get install -y gifsicle jpegoptim optipng jhead - npm i -g eslint babel-eslint - eslint app/assets/javascripts - eslint --ext .es6 app/assets/javascripts From 8ad2db7d2b159e37063f89f6db8527c63594f3c9 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 13 Aug 2015 16:04:31 -0400 Subject: [PATCH 019/237] Revert "Add image software to travis to stop complaints" This reverts commit e6b85d5be69e149a99efc544d0aa79fa9fc014c6. We apparently can't do this because we are on Travis' docker infrastructure. --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f69939856a..a57a5f7030 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,7 +32,6 @@ sudo: false cache: bundler before_install: - - sudo apt-get install -y gifsicle jpegoptim optipng jhead - npm i -g eslint babel-eslint - eslint app/assets/javascripts - eslint --ext .es6 app/assets/javascripts From e3cf8b17ba5d38cdc1f7658cf817cf062648e046 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 13 Aug 2015 17:28:38 -0400 Subject: [PATCH 020/237] FIX: Regression saving custom user title --- .../javascripts/admin/controllers/admin-user-index.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0ad12bfccc..2906a4cf87 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -36,7 +36,7 @@ export default Ember.Controller.extend(CanCheckEmails, { saveTitle() { const self = this; - return Discourse.ajax("/users/" + this.get('username').toLowerCase(), { + return Discourse.ajax("/users/" + this.get('model.username').toLowerCase(), { data: {title: this.get('title')}, type: 'PUT' }).catch(function(e) { From 6db98f52d6970173dd5b0afaaa2aa4216e2ea1cc Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 13 Aug 2015 17:32:12 -0400 Subject: [PATCH 021/237] FIX: Regression with suspended text --- .../admin/templates/{user_index.hbs => user-index.hbs} | 6 +++--- app/assets/javascripts/discourse/templates/user/user.hbs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename app/assets/javascripts/admin/templates/{user_index.hbs => user-index.hbs} (98%) diff --git a/app/assets/javascripts/admin/templates/user_index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs similarity index 98% rename from app/assets/javascripts/admin/templates/user_index.hbs rename to app/assets/javascripts/admin/templates/user-index.hbs index 302db5f4d0..60d888a416 100644 --- a/app/assets/javascripts/admin/templates/user_index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -306,12 +306,12 @@
{{i18n 'admin.user.suspended_by'}}
- {{#link-to 'adminUser' suspendedBy}}{{avatar suspendedBy imageSize="tiny"}}{{/link-to}} - {{#link-to 'adminUser' suspendedBy}}{{suspendedBy.username}}{{/link-to}} + {{#link-to 'adminUser' suspendedBy}}{{avatar model.suspendedBy imageSize="tiny"}}{{/link-to}} + {{#link-to 'adminUser' suspendedBy}}{{model.suspendedBy.username}}{{/link-to}}
{{i18n 'admin.user.suspend_reason'}}: - {{suspend_reason}} + {{model.suspend_reason}}
{{/if}} diff --git a/app/assets/javascripts/discourse/templates/user/user.hbs b/app/assets/javascripts/discourse/templates/user/user.hbs index 7ba76ca75f..36ced927f2 100644 --- a/app/assets/javascripts/discourse/templates/user/user.hbs +++ b/app/assets/javascripts/discourse/templates/user/user.hbs @@ -77,8 +77,8 @@ {{#if model.isSuspended}}
{{fa-icon "ban"}} - {{i18n 'user.suspended_notice' date=suspendedTillDate}}
- {{i18n 'user.suspended_reason'}} {{suspend_reason}} + {{i18n 'user.suspended_notice' date=model.suspendedTillDate}}
+ {{i18n 'user.suspended_reason'}} {{model.suspend_reason}}
{{/if}} {{{model.bio_cooked}}} From a2693668869d2b202496fc7b425bbc56427144c8 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 09:30:18 +1000 Subject: [PATCH 022/237] update gemfile to match gem lock --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 539bc0d34c..00cc6222be 100644 --- a/Gemfile +++ b/Gemfile @@ -40,7 +40,7 @@ gem 'active_model_serializers', '~> 0.8.3' gem 'onebox' gem 'ember-rails' -gem 'ember-source', '1.11.3.1' +gem 'ember-source', '1.12.1' gem 'handlebars-source', '2.0.0' gem 'barber' gem 'babel-transpiler' From b778b1931829bf58d12efc31b7b4c92fa3cc6f0d Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 09:37:03 +1000 Subject: [PATCH 023/237] fix all refs in gemfile lock careful when editing this by hand --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index c369c55fe8..19da891426 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -401,7 +401,7 @@ DEPENDENCIES discourse-qunit-rails email_reply_parser ember-rails - ember-source (= 1.11.3.1) + ember-source (= 1.12.1) excon fabrication (= 2.9.8) fakeweb (~> 1.3.0) @@ -483,4 +483,4 @@ DEPENDENCIES unicorn BUNDLED WITH - 1.10.3 + 1.10.6 From 1f044350f6d3ab31786b7decd8c4970495c1b3be Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 10:01:38 +1000 Subject: [PATCH 024/237] stop running exec_sql through active record this avoids double logging --- lib/freedom_patches/active_record_base.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/freedom_patches/active_record_base.rb b/lib/freedom_patches/active_record_base.rb index eafb7f7219..4f62d40647 100644 --- a/lib/freedom_patches/active_record_base.rb +++ b/lib/freedom_patches/active_record_base.rb @@ -4,7 +4,7 @@ class ActiveRecord::Base def self.exec_sql(*args) conn = ActiveRecord::Base.connection sql = ActiveRecord::Base.send(:sanitize_sql_array, args) - conn.execute(sql) + conn.raw_connection.exec(sql) end def self.exec_sql_row_count(*args) From 6d7cb865539649354dde06c50fc55c6c400444ca Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 10:38:46 +1000 Subject: [PATCH 025/237] missing model when saving primary group --- .../javascripts/admin/controllers/admin-user-index.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2906a4cf87..cf6719ac83 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -65,7 +65,7 @@ export default Ember.Controller.extend(CanCheckEmails, { savePrimaryGroup() { const self = this; - return Discourse.ajax("/admin/users/" + this.get('id') + "/primary_group", { + return Discourse.ajax("/admin/users/" + this.get('model.id') + "/primary_group", { type: 'PUT', data: {primary_group_id: this.get('model.primary_group_id')} }).then(function () { From e87ffcc4574742347ec2d3842e063fa5cbff90d2 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 10:40:35 +1000 Subject: [PATCH 026/237] missing model prefix for saving title --- .../javascripts/admin/controllers/admin-user-index.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 cf6719ac83..fdea272ab1 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-index.js.es6 @@ -37,7 +37,7 @@ export default Ember.Controller.extend(CanCheckEmails, { const self = this; return Discourse.ajax("/users/" + this.get('model.username').toLowerCase(), { - data: {title: this.get('title')}, + data: {title: this.get('model.title')}, type: 'PUT' }).catch(function(e) { bootbox.alert(I18n.t("generic_error_with_reason", {error: "http: " + e.status + " - " + e.body})); From e9e5a6c122c87183015b300755a310503d8716e7 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 10:42:16 +1000 Subject: [PATCH 027/237] logster version bump --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 19da891426..0228321123 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,7 +145,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (0.8.4.5.pre) + logster (0.8.4.6.pre) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From ad2de1804e7aac8ec42d4fd457842da871257fe4 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 11:53:16 +1000 Subject: [PATCH 028/237] Correct bad where clause when no category/user found --- lib/search.rb | 16 ++++++++++++---- spec/components/search_spec.rb | 6 ++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/search.rb b/lib/search.rb index a22766ef19..0b3bf5328c 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -215,13 +215,21 @@ class Search end advanced_filter(/category:(.+)/) do |posts,match| - category_id = Category.find_by('name ilike ? OR id = ?', match, match.to_i).try(:id) - posts.where("topics.category_id = ?", category_id) + category_id = Category.where('name ilike ? OR id = ?', match, match.to_i).pluck(:id).first + if category_id + posts.where("topics.category_id = ?", category_id) + else + posts.where("1 = 0") + end end advanced_filter(/user:(.+)/) do |posts,match| - user_id = User.find_by('username_lower = ? OR id = ?', match.downcase, match.to_i).try(:id) - posts.where("posts.user_id = #{user_id}") + user_id = User.where('username_lower = ? OR id = ?', match.downcase, match.to_i).pluck(:id).first + if user_id + posts.where("posts.user_id = #{user_id}") + else + posts.where("1 = 0") + end end advanced_filter(/min_age:(\d+)/) do |posts,match| diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index 4d94c2e5ee..498887c115 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -373,10 +373,10 @@ describe Search do describe 'Advanced search' do - it 'supports min_age and max_age in:first' do + it 'supports min_age and max_age in:first user:' do topic = Fabricate(:topic, created_at: 3.months.ago) Fabricate(:post, raw: 'hi this is a test 123 123', topic: topic) - Fabricate(:post, raw: 'boom boom shake the room', topic: topic) + _post = Fabricate(:post, raw: 'boom boom shake the room', topic: topic) expect(Search.execute('test min_age:100').posts.length).to eq(1) expect(Search.execute('test min_age:10').posts.length).to eq(0) @@ -387,6 +387,8 @@ describe Search do expect(Search.execute('boom').posts.length).to eq(1) expect(Search.execute('boom in:first').posts.length).to eq(0) + expect(Search.execute('user:nobody').posts.length).to eq(0) + expect(Search.execute("user:#{_post.user.username}").posts.length).to eq(1) end it 'can search numbers correctly, and match exact phrases' do From 1a245656e0a70960120d0c6f97ce193530f5d199 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 14 Aug 2015 10:00:07 +0800 Subject: [PATCH 029/237] FIX: HTML not being stripped in description meta tag. --- app/controllers/list_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/list_controller.rb b/app/controllers/list_controller.rb index 17e1a49545..e5b6f63887 100644 --- a/app/controllers/list_controller.rb +++ b/app/controllers/list_controller.rb @@ -230,7 +230,7 @@ class ListController < ApplicationController @category = Category.query_category(slug_or_id, parent_category_id) raise Discourse::NotFound if !@category - @description_meta = @category.description + @description_meta = @category.description_text guardian.ensure_can_see!(@category) end From 723d49d54337b02a594f838a22fec9d67b29b7e1 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 12:34:52 +1000 Subject: [PATCH 030/237] regression, users could not be deleted --- app/assets/javascripts/discourse/controllers/user.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/user.js.es6 b/app/assets/javascripts/discourse/controllers/user.js.es6 index 7159c146ce..6df5a32d6d 100644 --- a/app/assets/javascripts/discourse/controllers/user.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user.js.es6 @@ -60,7 +60,7 @@ export default Ember.Controller.extend(CanCheckEmails, { actions: { adminDelete: function() { - Discourse.AdminUser.find(this.get('username').toLowerCase()).then(function(user){ + Discourse.AdminUser.find(this.get('model.username').toLowerCase()).then(function(user){ user.destroy({deletePosts: true}); }); }, From c711c06bb8a0df4feebb25b98c866a0f06b25f5d Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 12:51:23 +1000 Subject: [PATCH 031/237] FIX: stop double reporting errors that were already reported --- config/initializers/sidekiq.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb index 2d634e49e8..7a5781f146 100644 --- a/config/initializers/sidekiq.rb +++ b/config/initializers/sidekiq.rb @@ -45,6 +45,9 @@ Sidekiq.logger.level = Logger::WARN class SidekiqLogsterReporter < Sidekiq::ExceptionHandler::Logger def call(ex, context = {}) + + return if Jobs::HandledExceptionWrapper === ex + # Pass context to Logster fake_env = {} context.each do |key, value| From 23b8a408f717efa8a86564041a5d28ba83bc0944 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 13:05:13 +1000 Subject: [PATCH 032/237] FIX: serialize post processing This avoids all sorts of nasty race conditions in job schedular --- lib/cooked_post_processor.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index fb67c991e0..5989891dd1 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -16,11 +16,13 @@ class CookedPostProcessor end def post_process(bypass_bump = false) - keep_reverse_index_up_to_date - post_process_images - post_process_oneboxes - optimize_urls - pull_hotlinked_images(bypass_bump) + DistributedMutex.synchronize("post_process_#{@post.id}") do + keep_reverse_index_up_to_date + post_process_images + post_process_oneboxes + optimize_urls + pull_hotlinked_images(bypass_bump) + end end def keep_reverse_index_up_to_date From 629fa1223f13de4df5a041ae0d6e605f6006239f Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 13:21:40 +1000 Subject: [PATCH 033/237] regression: broken categories page on mobile --- .../discourse/templates/mobile/discovery/categories.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs index 08c5446171..349cabd1a9 100644 --- a/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs +++ b/app/assets/javascripts/discourse/templates/mobile/discovery/categories.hbs @@ -1,4 +1,4 @@ -{{#each c in categories}} +{{#each model.categories as |c|}}
From 7d86d23eec455f2689151695deb187d6eb81cdaf Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 13:29:39 +1000 Subject: [PATCH 034/237] correct bad error reporting. --- app/jobs/regular/feature_topic_users.rb | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/app/jobs/regular/feature_topic_users.rb b/app/jobs/regular/feature_topic_users.rb index 6eabc024e2..702fb4f6c3 100644 --- a/app/jobs/regular/feature_topic_users.rb +++ b/app/jobs/regular/feature_topic_users.rb @@ -8,16 +8,10 @@ module Jobs topic = Topic.find_by(id: topic_id) - # there are 3 cases here - # 1. topic was atomically nuked, this should be skipped - # 2. topic was deleted, this should be skipped - # 3. error an incorrect topic_id was sent - - unless topic.present? - max_id = Topic.with_deleted.maximum(:id).to_i - raise Discourse::InvalidParameters.new(:topic_id) if max_id < topic_id - return - end + # Topic may be hard deleted due to spam, no point complaining + # we would have to look at the topics table id sequence to find cases + # where this was called with an invalid id, no point really + return unless topic.present? topic.feature_topic_users(args) end From 5ee4d3ba8cfce8fc6d11529cf0e708747bb69533 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 13:57:02 +1000 Subject: [PATCH 035/237] FIX: log post deletion even if user is deleted. --- app/services/staff_action_logger.rb | 5 ++++- spec/services/staff_action_logger_spec.rb | 7 +++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 0e420e5f4a..cd681eea92 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -42,10 +42,13 @@ class StaffActionLogger topic = deleted_post.topic || Topic.with_deleted.find(deleted_post.topic_id) + username = deleted_post.user.try(:username) || "unknown" + name = deleted_post.user.try(:name) || "unknown" + details = [ "id: #{deleted_post.id}", "created_at: #{deleted_post.created_at}", - "user: #{deleted_post.user.username} (#{deleted_post.user.name})", + "user: #{username} (#{name})", "topic: #{topic.title}", "post_number: #{deleted_post.post_number}", "raw: #{deleted_post.raw}" diff --git a/spec/services/staff_action_logger_spec.rb b/spec/services/staff_action_logger_spec.rb index f055737cf8..cb7d6e1758 100644 --- a/spec/services/staff_action_logger_spec.rb +++ b/spec/services/staff_action_logger_spec.rb @@ -63,6 +63,13 @@ describe StaffActionLogger do it 'creates a new UserHistory record' do expect { log_post_deletion }.to change { UserHistory.count }.by(1) end + + it 'does not explode if post does not have a user' do + expect { + deleted_post.update_columns(user_id: nil) + log_post_deletion + }.to change { UserHistory.count }.by(1) + end end describe 'log_topic_deletion' do From 07e66a5eff93057c5fa1aaebfc470d559d7184af Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 13 Aug 2015 22:20:22 -0700 Subject: [PATCH 036/237] set default OOB backup interval to 7 days vs 1 day --- config/site_settings.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index e000630c93..b45b5f7a9b 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -793,8 +793,8 @@ backups: default: 7 backup_frequency: min: 0 - max: 7 - default: 1 + max: 30 + default: 7 enable_s3_backups: default: false s3_backup_bucket: From e670ebb4332a7afb2a60645685b38f64e154bf83 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 16:25:29 +1000 Subject: [PATCH 037/237] FEATURE: allow backup settings to be overriden by globals FEATURE: allow backup interval of up to 30 days FIX: if a custom file exists in backup directory look at its date FEATURE: site setting automatic_backups_enabled default true --- app/jobs/regular/create_backup.rb | 1 - app/jobs/scheduled/schedule_backup.rb | 6 +++--- app/models/site_setting.rb | 4 ---- config/locales/server.en.yml | 1 + config/site_settings.yml | 9 ++++++++- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/jobs/regular/create_backup.rb b/app/jobs/regular/create_backup.rb index d455e6ab8c..5f1cf57602 100644 --- a/app/jobs/regular/create_backup.rb +++ b/app/jobs/regular/create_backup.rb @@ -5,7 +5,6 @@ module Jobs sidekiq_options retry: false def execute(args) - return unless SiteSetting.backups_enabled? BackupRestore.backup!(Discourse.system_user.id, publish_to_message_bus: false) end end diff --git a/app/jobs/scheduled/schedule_backup.rb b/app/jobs/scheduled/schedule_backup.rb index fa1584b6a0..e3e7455584 100644 --- a/app/jobs/scheduled/schedule_backup.rb +++ b/app/jobs/scheduled/schedule_backup.rb @@ -5,11 +5,11 @@ module Jobs sidekiq_options retry: false def execute(args) - return unless SiteSetting.backups_enabled? + return unless SiteSetting.automatic_backups_enabled? if latest_backup = Backup.all[0] - date = Date.parse(latest_backup.filename[/\d{4}-\d{2}-\d{2}/]) - return if date + SiteSetting.backup_frequency.days > Time.now + date = File.ctime(latest_backup.path).to_date + return if (date + SiteSetting.backup_frequency.days) > Time.now.to_date end Jobs.enqueue_in(rand(10.minutes), :create_backup) diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 67c2aa7a63..4c46e52d12 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -113,10 +113,6 @@ class SiteSetting < ActiveRecord::Base false end - def self.backups_enabled? - SiteSetting.backup_frequency > 0 - end - end # == Schema Information diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index b919935436..7d525bc314 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -938,6 +938,7 @@ en: allow_restore: "Allow restore, which can replace ALL site data! Leave false unless you plan to restore a backup" maximum_backups: "The maximum amount of backups to keep on disk. Older backups are automatically deleted" + automatic_backups_enabled: "Run automatic backups as defined in backup frequency" backup_frequency: "How frequently we create a site backup, in days." enable_s3_backups: "Upload backups to S3 when complete. IMPORTANT: requires valid S3 credentials entered in Files settings." s3_backup_bucket: "The remote bucket to hold backups. WARNING: Make sure it is a private bucket." diff --git a/config/site_settings.yml b/config/site_settings.yml index b45b5f7a9b..e1ff85b944 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -791,15 +791,22 @@ backups: maximum_backups: client: true default: 7 + shadowed_by_global: true + automatic_backups_enabled: + default: true + shadowed_by_global: true backup_frequency: - min: 0 + min: 1 max: 30 default: 7 + shadowed_by_global: true enable_s3_backups: default: false + shadowed_by_global: true s3_backup_bucket: default: '' regex: "^[^A-Z_.]+$" # can't use '.' when using HTTPS + shadowed_by_global: true uncategorized: version_checks: From a246e7c9c0c2361e63d95d929c020257176afd27 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 16:26:53 +1000 Subject: [PATCH 038/237] fix invalid spec --- spec/jobs/feature_topic_users_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/jobs/feature_topic_users_spec.rb b/spec/jobs/feature_topic_users_spec.rb index 5bd04a1f27..8bc9da9dac 100644 --- a/spec/jobs/feature_topic_users_spec.rb +++ b/spec/jobs/feature_topic_users_spec.rb @@ -8,8 +8,8 @@ describe Jobs::FeatureTopicUsers do expect { Jobs::FeatureTopicUsers.new.execute({}) }.to raise_error(Discourse::InvalidParameters) end - it "raises an error with a missing topic_id" do - expect { Jobs::FeatureTopicUsers.new.execute(topic_id: 123) }.to raise_error(Discourse::InvalidParameters) + it "raises no error with a missing topic_id" do + Jobs::FeatureTopicUsers.new.execute(topic_id: 123) end context 'with a topic' do From 9068f9f9bf0c56c22fcfa64c7c2df81aa86a9c74 Mon Sep 17 00:00:00 2001 From: James Kiesel Date: Thu, 13 Aug 2015 23:39:19 -0700 Subject: [PATCH 039/237] Skip latest posts with no topic in rss --- app/views/posts/latest.rss.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/posts/latest.rss.erb b/app/views/posts/latest.rss.erb index 7cfb0a8e1d..f0f452a907 100644 --- a/app/views/posts/latest.rss.erb +++ b/app/views/posts/latest.rss.erb @@ -7,7 +7,7 @@ <%= @link %> <%= @description %> <% @posts.each do |post| %> - <% next unless post.user %> + <% next unless post.user && post.topic %> <%= post.topic.title %> ]]> From bfd1bae6efa06595628720074cd8a83878c1e764 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 16:46:48 +1000 Subject: [PATCH 040/237] upgrade sidekiq --- Gemfile.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 0228321123..b9e2d98cb7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,7 +60,7 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) - celluloid (0.16.0) + celluloid (0.16.1) timers (~> 4.0.0) certified (1.0.0) coderay (1.1.0) @@ -324,12 +324,12 @@ GEM shoulda-context (1.2.1) shoulda-matchers (2.7.0) activesupport (>= 3.0.0) - sidekiq (3.3.4) - celluloid (>= 0.16.0) - connection_pool (>= 2.1.1) - json - redis (>= 3.0.6) - redis-namespace (>= 1.3.1) + sidekiq (3.4.2) + celluloid (~> 0.16.0) + connection_pool (~> 2.2, >= 2.2.0) + json (~> 1.0) + redis (~> 3.2, >= 3.2.1) + redis-namespace (~> 1.5, >= 1.5.2) simple-rss (1.3.1) simplecov (0.9.1) docile (~> 1.1.0) From e82f892c2d1390d2ee7e4f667a7ea778c696e671 Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 17:01:06 +1000 Subject: [PATCH 041/237] FIX: allow global settings to include keys that have numbers in them --- app/models/global_setting.rb | 2 +- spec/models/global_setting_spec.rb | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/global_setting.rb b/app/models/global_setting.rb index 78058e3e3c..10866f8e8b 100644 --- a/app/models/global_setting.rb +++ b/app/models/global_setting.rb @@ -88,7 +88,7 @@ class GlobalSetting def read ERB.new(File.read(@file)).result().split("\n").each do |line| - if line =~ /^\s*([a-z_]+)\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|[^#]*)/ + if line =~ /^\s*([a-z_]+[a-z0-9_]*)\s*=\s*(\"([^\"]*)\"|\'([^\']*)\'|[^#]*)/ @data[$1.strip.to_sym] = ($4 || $3 || $2).strip end end diff --git a/spec/models/global_setting_spec.rb b/spec/models/global_setting_spec.rb index e371b15d69..a29886b307 100644 --- a/spec/models/global_setting_spec.rb +++ b/spec/models/global_setting_spec.rb @@ -4,7 +4,9 @@ require 'tempfile' describe GlobalSetting::EnvProvider do it "can detect keys from env" do ENV['DISCOURSE_BLA'] = '1' + ENV['DISCOURSE_BLA_2'] = '2' expect(GlobalSetting::EnvProvider.new.keys).to include(:bla) + expect(GlobalSetting::EnvProvider.new.keys).to include(:bla_2) end end describe GlobalSetting::FileProvider do @@ -17,6 +19,7 @@ describe GlobalSetting::FileProvider do f.write("c = \'10 # = 00\' # this is a # comment\n") f.write("d =\n") f.write("#f = 1\n") + f.write("a1 = 1\n") f.close provider = GlobalSetting::FileProvider.from(f.path) @@ -27,8 +30,9 @@ describe GlobalSetting::FileProvider do expect(provider.lookup(:d,"bob")).to eq nil expect(provider.lookup(:e,"bob")).to eq "bob" expect(provider.lookup(:f,"bob")).to eq "bob" + expect(provider.lookup(:a1,"")).to eq 1 - expect(provider.keys.sort).to eq [:a, :b, :c, :d] + expect(provider.keys.sort).to eq [:a, :a1, :b, :c, :d] f.unlink end From 8cdc302d74d0c019ab28a9f6bb18896da0de4e6c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 14 Aug 2015 11:59:06 +0800 Subject: [PATCH 042/237] DEV: Add byebug. --- Gemfile | 1 + Gemfile.lock | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/Gemfile b/Gemfile index 00cc6222be..98cdfdd790 100644 --- a/Gemfile +++ b/Gemfile @@ -131,6 +131,7 @@ group :test, :development do gem 'rspec-given' gem 'pry-nav' gem 'spork-rails' + gem 'byebug' end group :development do diff --git a/Gemfile.lock b/Gemfile.lock index b9e2d98cb7..09bdeba853 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,10 +60,13 @@ GEM binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) builder (3.2.2) + byebug (5.0.0) + columnize (= 0.9.0) celluloid (0.16.1) timers (~> 4.0.0) certified (1.0.0) coderay (1.1.0) + columnize (0.9.0) connection_pool (2.2.0) crass (1.0.1) daemons (1.2.2) @@ -397,6 +400,7 @@ DEPENDENCIES barber better_errors binding_of_caller + byebug certified discourse-qunit-rails email_reply_parser From f743bc6e745073eb87064fd2fe7679e58dd79d3d Mon Sep 17 00:00:00 2001 From: Sam Date: Fri, 14 Aug 2015 17:50:24 +1000 Subject: [PATCH 043/237] stop adding users to a group if they are already in the group --- app/jobs/regular/automatic_group_membership.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/jobs/regular/automatic_group_membership.rb b/app/jobs/regular/automatic_group_membership.rb index b849b018b2..738d0db4ea 100644 --- a/app/jobs/regular/automatic_group_membership.rb +++ b/app/jobs/regular/automatic_group_membership.rb @@ -13,7 +13,9 @@ module Jobs domains = group.automatic_membership_email_domains.gsub('.', '\.') - User.where("email ~* '@(#{domains})$'").find_each do |user| + User.where("email ~* '@(#{domains})$' and users.id not in ( + select user_id from group_users where group_users.group_id = ? + )", group_id).find_each do |user| begin group.add(user) rescue ActiveRecord::RecordNotUnique, PG::UniqueViolation From 9a3f7a1e4456dbaba62753d349616013e3a588c4 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 14 Aug 2015 15:56:01 +0800 Subject: [PATCH 044/237] FIX: Use site settings for min_search_term_length. --- app/assets/javascripts/discourse/views/search.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/views/search.js.es6 b/app/assets/javascripts/discourse/views/search.js.es6 index a9ec93319f..861a29305f 100644 --- a/app/assets/javascripts/discourse/views/search.js.es6 +++ b/app/assets/javascripts/discourse/views/search.js.es6 @@ -5,7 +5,7 @@ export default Ember.View.extend({ templateName: 'search', keyDown: function(e){ var term = this.get('controller.term'); - if (e.which === 13 && term && term.length > 2) { + if (e.which === 13 && term && term.length >= Discourse.SiteSettings.min_search_term_length) { this.get('controller').send('fullSearch'); } } From 23c774a324554039d4a6d37585b353cba0d3e3fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 10:52:33 +0200 Subject: [PATCH 045/237] Let's try @wil93's recommendation to whitelist packages --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index a57a5f7030..5002e0ac5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,12 @@ env: addons: postgresql: 9.3 + apt: + packages: + - gifsicle + - jpegoptim + - optipng + - jhead matrix: allow_failures: @@ -32,6 +38,7 @@ sudo: false cache: bundler before_install: + - sudo apt-get install -y gifsicle jpegoptim optipng jhead - npm i -g eslint babel-eslint - eslint app/assets/javascripts - eslint --ext .es6 app/assets/javascripts From 1aa4aeadcba87d02d29f47734ab628b340de1a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 12:07:08 +0200 Subject: [PATCH 046/237] `sudo` doesn't work in Travic-CI --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5002e0ac5f..665cccff56 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ sudo: false cache: bundler before_install: - - sudo apt-get install -y gifsicle jpegoptim optipng jhead + - apt-get install -y gifsicle jpegoptim optipng jhead - npm i -g eslint babel-eslint - eslint app/assets/javascripts - eslint --ext .es6 app/assets/javascripts From 2ad24cf5dbd679e88ab4f7cbaab704fbefeb2bb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 12:22:32 +0200 Subject: [PATCH 047/237] UX: button was floating in topic unsubscribe page on Safari --- app/assets/stylesheets/common/base/topic.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/common/base/topic.scss b/app/assets/stylesheets/common/base/topic.scss index 873278a71a..015b4b6871 100644 --- a/app/assets/stylesheets/common/base/topic.scss +++ b/app/assets/stylesheets/common/base/topic.scss @@ -67,6 +67,8 @@ .topic-unsubscribe { .notification-options { display: inline-block; + float: none; + line-height: 2em; .dropdown-toggle { float: none; } From b9b1f8c23f6836ee584207a8e6d7c21e302cc1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 12:32:14 +0200 Subject: [PATCH 048/237] apt-get isn't needed actually --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 665cccff56..68cec6b75a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,6 @@ sudo: false cache: bundler before_install: - - apt-get install -y gifsicle jpegoptim optipng jhead - npm i -g eslint babel-eslint - eslint app/assets/javascripts - eslint --ext .es6 app/assets/javascripts From d59841974201d49ba281bdf14fc462b24dffecbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 12:40:18 +0200 Subject: [PATCH 049/237] FIX: don't use 'modelFor' --- app/assets/javascripts/admin/routes/admin-backups.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/routes/admin-backups.js.es6 b/app/assets/javascripts/admin/routes/admin-backups.js.es6 index d3b632e525..ac7f963cd4 100644 --- a/app/assets/javascripts/admin/routes/admin-backups.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-backups.js.es6 @@ -50,7 +50,7 @@ export default Discourse.Route.extend({ }, backupStarted() { - this.modelFor("adminBackups").set("isOperationRunning", true); + this.controllerFor("adminBackups").set("isOperationRunning", true); this.transitionTo("admin.backups.logs"); this.send("closeModal"); }, @@ -82,7 +82,7 @@ export default Discourse.Route.extend({ Discourse.User.currentProp("hideReadOnlyAlert", true); backup.restore().then(function() { self.controllerFor("adminBackupsLogs").clear(); - self.modelFor("adminBackups").set("model.isOperationRunning", true); + self.controllerFor("adminBackups").set("model.isOperationRunning", true); self.transitionTo("admin.backups.logs"); }); } From b6cd4af2baafb7bef9e9e2d979636a556e9cae89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 12:46:52 +0200 Subject: [PATCH 050/237] FIX: follow redirects when pulling hotlinked images --- app/jobs/regular/pull_hotlinked_images.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index bc2ece41f0..7d114653d4 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -32,7 +32,7 @@ module Jobs # have we already downloaded that file? unless downloaded_urls.include?(src) begin - hotlinked = FileHelper.download(src, @max_size, "discourse-hotlinked") + hotlinked = FileHelper.download(src, @max_size, "discourse-hotlinked", true) rescue Discourse::InvalidParameters end if hotlinked From b8cf797e31f06e0428f3e3bef888bfe7069744ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 13:03:49 +0200 Subject: [PATCH 051/237] FIX: ensure Badge consistency --- app/jobs/scheduled/ensure_db_consistency.rb | 1 + app/models/badge.rb | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/app/jobs/scheduled/ensure_db_consistency.rb b/app/jobs/scheduled/ensure_db_consistency.rb index 609dd3184a..274c666dea 100644 --- a/app/jobs/scheduled/ensure_db_consistency.rb +++ b/app/jobs/scheduled/ensure_db_consistency.rb @@ -12,6 +12,7 @@ module Jobs PostRevision.ensure_consistency! UserStat.update_view_counts(13.hours.ago) Topic.ensure_consistency! + Badge.ensure_consistency! end end end diff --git a/app/models/badge.rb b/app/models/badge.rb index 96c577f47d..f4442cad38 100644 --- a/app/models/badge.rb +++ b/app/models/badge.rb @@ -310,6 +310,10 @@ SQL end end + def self.ensure_consistency! + Badge.find_each(&:reset_grant_count!) + end + protected def ensure_not_system unless id From 87fd70cd4ae70cbdd171ca45a3cf55b8af03ffcb Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 14 Aug 2015 11:45:17 +0800 Subject: [PATCH 052/237] FIX: Broken private message search context. --- app/assets/javascripts/discourse/controllers/search.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 1d9cb22ad2..5f814e99f9 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -14,9 +14,9 @@ export default Em.Controller.extend({ return Ember.get(searchContext, 'type'); } }, - set(key, value) { + set(value, searchContext) { // a bit hacky, consider cleaning this up, need to work through all observers though - const context = $.extend({}, this.get('searchContext')); + const context = $.extend({}, searchContext); context.type = value; this.set('searchContext', context); return this.get('searchContext.type'); From 0a2f615aab78bac663db114c2f491ea1351f0ced Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 17:44:33 +0200 Subject: [PATCH 053/237] FIX: pin a topic globally wasn't working --- .../controllers/feature-topic.js.es6 | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 index 03bcebfcad..1cadd78ca4 100644 --- a/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/feature-topic.js.es6 @@ -1,5 +1,6 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { categoryLinkHTML } from 'discourse/helpers/category-link'; +import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend(ModalFunctionality, { needs: ["topic"], @@ -9,7 +10,7 @@ export default Ember.Controller.extend(ModalFunctionality, { pinnedGloballyCount: 0, bannerCount: 0, - reset: function() { + reset() { this.set("model.pinnedInCategoryUntil", null); this.set("model.pinnedGloballyUntil", null); }, @@ -27,21 +28,24 @@ export default Ember.Controller.extend(ModalFunctionality, { return I18n.t(name, { categoryLink: this.get("categoryLink"), until: until }); }.property("categoryLink", "model.{pinned_globally,pinned_until}"), - pinMessage: function() { - return I18n.t("topic.feature_topic.pin", { categoryLink: this.get("categoryLink") }); - }.property("categoryLink"), + @computed("categoryLink") + pinMessage(categoryLink) { + return I18n.t("topic.feature_topic.pin", { categoryLink }); + }, alreadyPinnedMessage: function() { return I18n.t("topic.feature_topic.already_pinned", { categoryLink: this.get("categoryLink"), count: this.get("pinnedInCategoryCount") }); }.property("categoryLink", "pinnedInCategoryCount"), - pinDisabled: function() { - return !this._isDateValid(this.get("parsedPinnedInCategoryUntil")); - }.property("parsedPinnedInCategoryUntil"), + @computed("parsedPinnedInCategoryUntil") + pinDisabled(parsedPinnedInCategoryUntil) { + return !this._isDateValid(parsedPinnedInCategoryUntil); + }, - pinGloballyDisabled: function() { - return !this._isDateValid(this.get("parsedPinnedGloballyUntil")); - }.property("pinnedGloballyUntil"), + @computed("parsedPinnedGloballyUntil") + pinGloballyDisabled(parsedPinnedGloballyUntil) { + return !this._isDateValid(parsedPinnedGloballyUntil); + }, parsedPinnedInCategoryUntil: function() { return this._parseDate(this.get("model.pinnedInCategoryUntil")); From b098e07cf1fe3c83fd151ebcb86872fa02c8b5c7 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Sat, 15 Aug 2015 00:11:41 +0800 Subject: [PATCH 054/237] FIX: Wrong value set when using ember-computed-decorators. --- .../admin/components/site-setting.js.es6 | 5 ++--- .../desktop-notification-config.js.es6 | 6 ++---- .../discourse/controllers/topic.js.es6 | 21 ++++++------------- 3 files changed, 10 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index 15058e8716..09b0dfe19e 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -28,12 +28,11 @@ export default Ember.Component.extend(BufferedContent, ScrollTop, { @computed('buffered.value') enabled: { - get() { - const bufferedValue = this.get('buffered.value'); + get(bufferedValue) { if (Ember.isEmpty(bufferedValue)) { return false; } return bufferedValue === 'true'; }, - set(key, value) { + set(value) { this.set('buffered.value', value ? 'true' : 'false'); } }, diff --git a/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 b/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 index 1d5f4bd77b..eb4e893f1f 100644 --- a/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 +++ b/app/assets/javascripts/discourse/components/desktop-notification-config.js.es6 @@ -11,10 +11,8 @@ export default Ember.Component.extend({ @computed notificationsDisabled: { - set(key, value) { - if (arguments.length > 1) { - localStorage.setItem('notifications-disabled', value); - } + set(value) { + localStorage.setItem('notifications-disabled', value); return localStorage.getItem('notifications-disabled'); }, get() { diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index c112f3458c..23c803e995 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -68,13 +68,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { @computed('model.postStream.summary') show_deleted: { - set(key, value) { + set(value) { const postStream = this.get('model.postStream'); if (!postStream) { return; } - - if (arguments.length > 1) { - postStream.set('show_deleted', value); - } + postStream.set('show_deleted', value); return postStream.get('show_deleted') ? true : undefined; }, get() { @@ -84,13 +81,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { @computed('model.postStream.summary') filter: { - set(key, value) { + set(value) { const postStream = this.get('model.postStream'); if (!postStream) { return; } - - if (arguments.length > 1) { - postStream.set('summary', value === "summary"); - } + postStream.set('summary', value === "summary"); return postStream.get('summary') ? "summary" : undefined; }, get() { @@ -100,13 +94,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { @computed('model.postStream.streamFilters.username_filters') username_filters: { - set(key, value) { + set(value) { const postStream = this.get('model.postStream'); if (!postStream) { return; } - - if (arguments.length > 1) { - postStream.set('streamFilters.username_filters', value); - } + postStream.set('streamFilters.username_filters', value); return postStream.get('streamFilters.username_filters'); }, get() { From 23a5c6444af079d4071a74365c757ea34e727eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 14 Aug 2015 19:33:32 +0200 Subject: [PATCH 055/237] FIX: move topic links and quoted posts extraction to the PostRevisor --- app/controllers/posts_controller.rb | 5 +---- lib/post_revisor.rb | 3 +++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index ea2c3a08f7..4161c36098 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -149,10 +149,7 @@ class PostsController < ApplicationController end revisor = PostRevisor.new(post) - if revisor.revise!(current_user, changes, opts) - TopicLink.extract_from(post) - QuotedPost.extract_from(post) - end + revisor.revise!(current_user, changes, opts) return render_json_error(post) if post.errors.present? return render_json_error(post.topic) if post.topic.errors.present? diff --git a/lib/post_revisor.rb b/lib/post_revisor.rb index b19583820d..6a97b16655 100644 --- a/lib/post_revisor.rb +++ b/lib/post_revisor.rb @@ -136,6 +136,9 @@ class PostRevisor publish_changes grant_badge + TopicLink.extract_from(@post) + QuotedPost.extract_from(@post) + successfully_saved_post_and_topic end From 7581a7d8696d3614307ca04cdfedd5c0a7a32e21 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 14 Aug 2015 17:46:15 -0400 Subject: [PATCH 056/237] move notification about low disk space into its own method --- lib/cooked_post_processor.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 5989891dd1..ea35cafc7e 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -274,12 +274,17 @@ class CookedPostProcessor reason = I18n.t("disable_remote_images_download_reason") staff_action_logger = StaffActionLogger.new(Discourse.system_user) staff_action_logger.log_site_setting_change("download_remote_images_to_local", true, false, { details: reason }) + # also send a private message to the site contact user - SystemMessage.create_from_system_user(Discourse.site_contact_user, :download_remote_images_disabled) + notify_about_low_disk_space true end + def notify_about_low_disk_space + SystemMessage.create_from_system_user(Discourse.site_contact_user, :download_remote_images_disabled) + end + def available_disk_space 100 - `df -P #{Rails.root}/public/uploads | tail -1 | tr -s ' ' | cut -d ' ' -f 5`.to_i end From e526fb0d1cd04bf55026f607ef793aca500d4e1e Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sat, 15 Aug 2015 12:18:37 +0530 Subject: [PATCH 057/237] FIX: fix new-topic composer issue --- app/assets/javascripts/discourse/mixins/open-composer.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/mixins/open-composer.js.es6 b/app/assets/javascripts/discourse/mixins/open-composer.js.es6 index fc4f9c784c..8717d21a5c 100644 --- a/app/assets/javascripts/discourse/mixins/open-composer.js.es6 +++ b/app/assets/javascripts/discourse/mixins/open-composer.js.es6 @@ -18,8 +18,8 @@ export default Ember.Mixin.create({ topicBody, topicCategoryId, topicCategory, - draftKey: controller.get('draft_key'), - draftSequence: controller.get('draft_sequence') + draftKey: controller.get('model.draft_key'), + draftSequence: controller.get('model.draft_sequence') }); } From 62fce639527a0f154cda391b1b6a1658cfd36016 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sat, 15 Aug 2015 17:39:07 +0530 Subject: [PATCH 058/237] FIX: do not load custom header in admin section --- app/views/layouts/application.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index a740289035..8a29518367 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -60,7 +60,7 @@ - <%- unless customization_disabled? %> + <%- unless customization_disabled? || loading_admin? %> <%= SiteCustomization.custom_header(session[:preview_style], mobile_view? ? :mobile : :desktop) %> <%- end %> From 8d66ca72f12335d18bf5bca17c655c321dca17ff Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 16 Aug 2015 11:13:19 +1000 Subject: [PATCH 059/237] fix revision dialog brokeness --- .../javascripts/discourse/controllers/history.js.es6 | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/history.js.es6 b/app/assets/javascripts/discourse/controllers/history.js.es6 index bd64800c68..4a04abb1af 100644 --- a/app/assets/javascripts/discourse/controllers/history.js.es6 +++ b/app/assets/javascripts/discourse/controllers/history.js.es6 @@ -116,13 +116,13 @@ export default Ember.Controller.extend(ModalFunctionality, { }, actions: { - loadFirstVersion: function() { this.refresh(this.get("post_id"), this.get("first_revision")); }, - loadPreviousVersion: function() { this.refresh(this.get("post_id"), this.get("previous_revision")); }, - loadNextVersion: function() { this.refresh(this.get("post_id"), this.get("next_revision")); }, - loadLastVersion: function() { this.refresh(this.get("post_id"), this.get("last_revision")); }, + loadFirstVersion: function() { this.refresh(this.get("model.post_id"), this.get("model.first_revision")); }, + loadPreviousVersion: function() { this.refresh(this.get("model.post_id"), this.get("model.previous_revision")); }, + loadNextVersion: function() { this.refresh(this.get("model.post_id"), this.get("model.next_revision")); }, + loadLastVersion: function() { this.refresh(this.get("model.post_id"), this.get("model.last_revision")); }, - hideVersion: function() { this.hide(this.get("post_id"), this.get("current_revision")); }, - showVersion: function() { this.show(this.get("post_id"), this.get("current_revision")); }, + hideVersion: function() { this.hide(this.get("model.post_id"), this.get("model.current_revision")); }, + showVersion: function() { this.show(this.get("model.post_id"), this.get("model.current_revision")); }, displayInline: function() { this.set("viewMode", "inline"); }, displaySideBySide: function() { this.set("viewMode", "side_by_side"); }, From da4c377277652773c2aac111afb041d26f7304eb Mon Sep 17 00:00:00 2001 From: Sam Date: Sun, 16 Aug 2015 14:02:22 +1000 Subject: [PATCH 060/237] FIX: can not approve users from admin dialog --- app/assets/javascripts/admin/templates/user-index.hbs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/templates/user-index.hbs b/app/assets/javascripts/admin/templates/user-index.hbs index 60d888a416..9afd15829a 100644 --- a/app/assets/javascripts/admin/templates/user-index.hbs +++ b/app/assets/javascripts/admin/templates/user-index.hbs @@ -143,19 +143,19 @@
{{i18n 'admin.users.approved'}}
- {{#if approved}} + {{#if model.approved}} {{i18n 'admin.user.approved_by'}} - {{#link-to 'adminUser' approvedBy}}{{avatar approvedBy imageSize="small"}}{{/link-to}} - {{#link-to 'adminUser' approvedBy}}{{approvedBy.username}}{{/link-to}} + {{#link-to 'adminUser' approvedBy}}{{avatar model.approvedBy imageSize="small"}}{{/link-to}} + {{#link-to 'adminUser' approvedBy}}{{model.approvedBy.username}}{{/link-to}} {{else}} {{i18n 'no_value'}} {{/if}}
- {{#if approved}} + {{#if model.approved}} {{i18n 'admin.user.approve_success'}} {{else}} - {{#if can_approve}} + {{#if model.can_approve}} -
-
- - {{#if showHtml}} - {{i18n 'admin.email.html'}} | {{i18n 'admin.email.text'}} - {{else}} - {{i18n 'admin.email.html'}} | {{i18n 'admin.email.text'}} - {{/if}} +
+ + {{#if showHtml}} + {{i18n 'admin.email.html'}} | {{i18n 'admin.email.text'}} + {{else}} + {{i18n 'admin.email.html'}} | {{i18n 'admin.email.text'}} + {{/if}} +
{{#conditional-loading-spinner condition=loading}} {{#if showHtml}} - {{{html_content}}} + {{{model.html_content}}} {{else}} -
{{{text_content}}}
+
{{{model.text_content}}}
{{/if}} {{/conditional-loading-spinner}} From 90388aa18e4239c05e73f2d54a684a3bf58c03f1 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sun, 16 Aug 2015 15:31:04 +0530 Subject: [PATCH 062/237] FIX: email preview --- .../admin/controllers/admin-email-preview-digest.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 b/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 index 6912115e1e..f259acba84 100644 --- a/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-email-preview-digest.js.es6 @@ -4,7 +4,7 @@ export default Ember.Controller.extend({ refresh() { const model = this.get('model'); - self.set('loading', true); + this.set('loading', true); Discourse.EmailPreview.findDigest(this.get('lastSeen')).then(email => { model.setProperties(email.getProperties('html_content', 'text_content')); this.set('loading', false); From c7a21b7c237c41ca5f46acd57d2ff828cae53324 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Sun, 26 Jul 2015 00:06:49 +0800 Subject: [PATCH 063/237] FEATURE: Allow admin to change timestamp of topic. --- .../controllers/change-timestamp.js.es6 | 58 +++++++++++++++++++ .../javascripts/discourse/models/topic.js.es6 | 11 ++++ .../javascripts/discourse/routes/topic.js.es6 | 4 ++ .../templates/modal/change-timestamp.hbs | 18 ++++++ .../discourse/templates/topic-admin-menu.hbs | 4 ++ app/controllers/topics_controller.rb | 17 ++++++ app/services/post_timestamp_changer.rb | 43 ++++++++++++++ config/locales/client.en.yml | 7 +++ config/routes.rb | 1 + spec/controllers/topics_controller_spec.rb | 39 +++++++++++++ spec/services/post_timestamp_changer_spec.rb | 55 ++++++++++++++++++ 11 files changed, 257 insertions(+) create mode 100644 app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/modal/change-timestamp.hbs create mode 100644 app/services/post_timestamp_changer.rb create mode 100644 spec/services/post_timestamp_changer_spec.rb diff --git a/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 b/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 new file mode 100644 index 0000000000..1d55e74cf9 --- /dev/null +++ b/app/assets/javascripts/discourse/controllers/change-timestamp.js.es6 @@ -0,0 +1,58 @@ +import ModalFunctionality from 'discourse/mixins/modal-functionality'; +import computed from 'ember-addons/ember-computed-decorators'; + +// Modal related to changing the timestamp of posts +export default Ember.Controller.extend(ModalFunctionality, { + needs: ['topic'], + + topicController: Em.computed.alias('controllers.topic'), + saving: false, + date: '', + time: '', + + @computed('saving') + buttonTitle(saving) { + return saving ? I18n.t('saving') : I18n.t('topic.change_timestamp.action'); + }, + + @computed('date', 'time') + createdAt(date, time) { + return moment(date + ' ' + time, 'YYYY-MM-DD HH:mm:ss'); + }, + + @computed('createdAt') + validTimestamp(createdAt) { + return moment().diff(createdAt, 'minutes') < 0; + }, + + @computed('saving', 'date', 'validTimestamp') + buttonDisabled() { + if (this.get('saving') || this.get('validTimestamp')) return true; + return Ember.isEmpty(this.get('date')); + }, + + onShow: function() { + this.setProperties({ + date: moment().format('YYYY-MM-DD') + }); + }, + + actions: { + changeTimestamp: function() { + this.set('saving', true); + const self = this; + + Discourse.Topic.changeTimestamp( + this.get('topicController.model.id'), + this.get('createdAt').unix() + ).then(function() { + self.send('closeModal'); + self.setProperties({ date: '', time: '', saving: false }); + }).catch(function() { + self.flash(I18n.t('topic.change_timestamp.error'), 'alert-error'); + self.set('saving', false); + }); + return false; + } + } +}); diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 0fab68fa10..5f6a48d6ba 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -476,6 +476,17 @@ Topic.reopenClass({ return promise; }, + changeTimestamp(topicId, timestamp) { + const promise = Discourse.ajax("/t/" + topicId + '/change-timestamp', { + type: 'PUT', + data: { timestamp: timestamp }, + }).then(function(result) { + if (result.success) return result; + promise.reject(new Error("error updating timestamp of topic")); + }); + return promise; + }, + bulkOperation(topics, operation) { return Discourse.ajax("/topics/bulk", { type: 'PUT', diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index c79ccbe5c0..04d3ae202c 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -60,6 +60,10 @@ const TopicRoute = Discourse.Route.extend({ this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal'); }, + showChangeTimestamp() { + showModal('change-timestamp', { model: this.modelFor('topic'), title: 'topic.change_timestamp.title' }); + }, + showFeatureTopic() { showModal('featureTopic', { model: this.modelFor('topic'), title: 'topic.feature_topic.title' }); this.controllerFor('modal').set('modalClass', 'feature-topic-modal'); diff --git a/app/assets/javascripts/discourse/templates/modal/change-timestamp.hbs b/app/assets/javascripts/discourse/templates/modal/change-timestamp.hbs new file mode 100644 index 0000000000..c76c58e124 --- /dev/null +++ b/app/assets/javascripts/discourse/templates/modal/change-timestamp.hbs @@ -0,0 +1,18 @@ + + + diff --git a/app/assets/javascripts/discourse/templates/topic-admin-menu.hbs b/app/assets/javascripts/discourse/templates/topic-admin-menu.hbs index 004df5255f..2f190df214 100644 --- a/app/assets/javascripts/discourse/templates/topic-admin-menu.hbs +++ b/app/assets/javascripts/discourse/templates/topic-admin-menu.hbs @@ -38,6 +38,10 @@ {{/if}} {{/unless}} +
  • + {{d-button action="showChangeTimestamp" icon="calendar" label="topic.change_timestamp.title" class="btn-admin"}} +
  • +
  • {{#if model.archived}} {{d-button action="toggleArchived" icon="folder" label="topic.actions.unarchive" class="btn-admin"}} diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 01d960e542..f728f35c63 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -24,6 +24,7 @@ class TopicsController < ApplicationController :bulk, :reset_new, :change_post_owners, + :change_timestamps, :bookmark, :unsubscribe] @@ -375,6 +376,22 @@ class TopicsController < ApplicationController end end + def change_timestamps + params.require(:topic_id) + params.require(:timestamp) + + guardian.ensure_can_change_post_owner! + + begin + PostTimestampChanger.new( topic_id: params[:topic_id].to_i, + timestamp: params[:timestamp].to_i ).change! + + render json: success_json + rescue ActiveRecord::RecordInvalid + render json: failed_json, status: 422 + end + end + def clear_pin topic = Topic.find_by(id: params[:topic_id].to_i) guardian.ensure_can_see!(topic) diff --git a/app/services/post_timestamp_changer.rb b/app/services/post_timestamp_changer.rb new file mode 100644 index 0000000000..65b056128f --- /dev/null +++ b/app/services/post_timestamp_changer.rb @@ -0,0 +1,43 @@ +class PostTimestampChanger + def initialize(params) + @topic = Topic.with_deleted.find(params[:topic_id]) + @posts = @topic.posts + @timestamp = Time.at(params[:timestamp]) + @time_difference = calculate_time_difference + end + + def change! + ActiveRecord::Base.transaction do + update_topic + + @posts.each do |post| + if post.is_first_post? + update_post(post, @timestamp) + else + update_post(post, Time.at(post.created_at.to_f + @time_difference)) + end + end + end + + # Burst the cache for stats + [AdminDashboardData, About].each { |klass| $redis.del klass.stats_cache_key } + end + + private + + def calculate_time_difference + @timestamp - @topic.created_at + end + + def update_topic + @topic.update_attributes( + created_at: @timestamp, + updated_at: @timestamp, + bumped_at: @timestamp + ) + end + + def update_post(post, timestamp) + post.update_attributes(created_at: timestamp, updated_at: timestamp) + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f674c76880..68382aa1ed 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1241,6 +1241,13 @@ en: other: "Please choose the new owner of the {{count}} posts by {{old_user}}." instructions_warn: "Note that any notifications about this post will not be transferred to the new user retroactively.
    Warning: Currently, no post-dependent data is transferred over to the new user. Use with caution." + change_timestamp: + title: "Change Timestamp" + action: "change timestamp" + invalid_timestamp: "Timestamp cannot be in the future." + error: "There was an error changing the timestamp of the topic." + instructions: "Please select the new timestamp of the topic. Posts in the topic will be updated to have the same time difference." + multi_select: select: 'select' selected: 'selected ({{count}})' diff --git a/config/routes.rb b/config/routes.rb index d4f87ce11a..a8375839a6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -466,6 +466,7 @@ Discourse::Application.routes.draw do post "t/:topic_id/move-posts" => "topics#move_posts", constraints: {topic_id: /\d+/} post "t/:topic_id/merge-topic" => "topics#merge_topic", constraints: {topic_id: /\d+/} post "t/:topic_id/change-owner" => "topics#change_post_owners", constraints: {topic_id: /\d+/} + put "t/:topic_id/change-timestamp" => "topics#change_timestamps", constraints: {topic_id: /\d+/} delete "t/:topic_id/timings" => "topics#destroy_timings", constraints: {topic_id: /\d+/} put "t/:topic_id/bookmark" => "topics#bookmark", constraints: {topic_id: /\d+/} put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: {topic_id: /\d+/} diff --git a/spec/controllers/topics_controller_spec.rb b/spec/controllers/topics_controller_spec.rb index 8994e5de9c..8b31305eef 100644 --- a/spec/controllers/topics_controller_spec.rb +++ b/spec/controllers/topics_controller_spec.rb @@ -263,6 +263,45 @@ describe TopicsController do end end + context 'change_timestamps' do + let(:params) { { topic_id: 1, timestamp: Time.zone.now } } + + it 'needs you to be logged in' do + expect { xhr :put, :change_timestamps, params }.to raise_error(Discourse::NotLoggedIn) + end + + [:moderator, :trust_level_4].each do |user| + describe "forbidden to #{user}" do + let!(user) { log_in(user) } + + it 'correctly denies' do + xhr :put, :change_timestamps, params + expect(response).to be_forbidden + end + end + end + + describe 'changing timestamps' do + let!(:admin) { log_in(:admin) } + let(:old_timestamp) { Time.zone.now } + let(:new_timestamp) { old_timestamp - 1.day } + let!(:topic) { Fabricate(:topic, created_at: old_timestamp) } + let!(:p1) { Fabricate(:post, topic_id: topic.id, created_at: old_timestamp) } + let!(:p2) { Fabricate(:post, topic_id: topic.id, created_at: old_timestamp + 1.day) } + + it 'raises an error with a missing parameter' do + expect { xhr :put, :change_timestamps, topic_id: 1 }.to raise_error(ActionController::ParameterMissing) + end + + it 'should update the timestamps of selected posts' do + xhr :put, :change_timestamps, topic_id: topic.id, timestamp: new_timestamp.to_f + expect(topic.reload.created_at.to_s).to eq(new_timestamp.to_s) + expect(p1.reload.created_at.to_s).to eq(new_timestamp.to_s) + expect(p2.reload.created_at.to_s).to eq(old_timestamp.to_s) + end + end + end + context 'clear_pin' do it 'needs you to be logged in' do expect { xhr :put, :clear_pin, topic_id: 1 }.to raise_error(Discourse::NotLoggedIn) diff --git a/spec/services/post_timestamp_changer_spec.rb b/spec/services/post_timestamp_changer_spec.rb new file mode 100644 index 0000000000..506459687e --- /dev/null +++ b/spec/services/post_timestamp_changer_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe PostTimestampChanger do + describe "change!" do + let(:old_timestamp) { Time.zone.now } + let(:new_timestamp) { old_timestamp + 1.day } + let!(:topic) { Fabricate(:topic, created_at: old_timestamp) } + let!(:p1) { Fabricate(:post, topic: topic, created_at: old_timestamp) } + let!(:p2) { Fabricate(:post, topic: topic, created_at: old_timestamp + 1.day) } + let(:params) { { topic_id: topic.id, timestamp: new_timestamp.to_f } } + + it 'changes the timestamp of the topic and opening post' do + PostTimestampChanger.new(params).change! + + topic.reload + [:created_at, :updated_at, :bumped_at].each do |column| + expect(topic.public_send(column).to_s).to eq(new_timestamp.to_s) + end + + p1.reload + [:created_at, :updated_at].each do |column| + expect(p1.public_send(column).to_s).to eq(new_timestamp.to_s) + end + end + + describe 'predated timestamp' do + it 'updates the timestamp of posts in the topic with the time difference applied' do + PostTimestampChanger.new(params).change! + + p2.reload + [:created_at, :updated_at].each do |column| + expect(p2.public_send(column).to_s).to eq((old_timestamp + 2.day).to_s) + end + end + end + + describe 'backdated timestamp' do + let(:new_timestamp) { old_timestamp - 1.day } + + it 'updates the timestamp of posts in the topic with the time difference applied' do + PostTimestampChanger.new(params).change! + + p2.reload + [:created_at, :updated_at].each do |column| + expect(p2.public_send(column).to_s).to eq((old_timestamp).to_s) + end + end + end + + it 'deletes the stats cache' do + $redis.expects(:del).twice + PostTimestampChanger.new(params).change! + end + end +end From ce2ae908317ee473576c93c3a95358c8c0a4192e Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 17 Aug 2015 01:05:57 +0800 Subject: [PATCH 064/237] Lets see if this properly caches bundler on travis. --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 68cec6b75a..62961abc4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,9 +35,12 @@ services: sudo: false -cache: bundler +cache: + directories: + - vendor/bundle before_install: + - gem install bundler - npm i -g eslint babel-eslint - eslint app/assets/javascripts - eslint --ext .es6 app/assets/javascripts From e0646635283c6ad24f18da3139687fcf3b0083e8 Mon Sep 17 00:00:00 2001 From: Simon Cossar Date: Sun, 16 Aug 2015 10:35:23 -0700 Subject: [PATCH 065/237] Add slide-out menu --- .../controllers/admin-site-settings.js.es6 | 4 ++ .../admin/templates/site-settings.hbs | 3 +- .../stylesheets/common/admin/admin_base.scss | 59 +++++++++++++++++-- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 index 648a0e549c..97b1c9ace9 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-settings.js.es6 @@ -64,6 +64,10 @@ export default Ember.ArrayController.extend({ filter: '', onlyOverridden: false }); + }, + + toggleMenu() { + $('.admin-detail').toggleClass('mobile-closed mobile-open'); } } diff --git a/app/assets/javascripts/admin/templates/site-settings.hbs b/app/assets/javascripts/admin/templates/site-settings.hbs index 823c8d6c52..b7f400a242 100644 --- a/app/assets/javascripts/admin/templates/site-settings.hbs +++ b/app/assets/javascripts/admin/templates/site-settings.hbs @@ -6,6 +6,7 @@
    + {{text-field value=filter placeholderKey="type_to_filter" class="no-blur"}}
    @@ -26,7 +27,7 @@ -
    +
    {{outlet}}
    diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 30a83703ca..a7c7665084 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -21,6 +21,12 @@ $mobile-breakpoint: 700px; } } +.admin-contents { + position: relative; + margin-left: -10px; + margin-right: -10px; +} + .admin-contents table { width: 100%; tr {text-align: left;} @@ -145,6 +151,24 @@ td.flaggers td { .controls { margin-left: 10px; } + + .controls .menu-toggle { + display: none; + float: left; + font-size: 24px; + padding: 3px 6px; + margin-right: 32px; + border: 1px solid lighten($primary, 40%); + border-radius: 3px; + background: transparent; + color: $primary; + &:hover { + background-color: lighten($primary, 60%); + } + @media (max-width: $mobile-breakpoint) { + display: inline-block; + } + } button { margin-right: 5px; @@ -214,12 +238,19 @@ td.flaggers td { .admin-nav { width: 18.018%; + position: relative; + // The admin-nav becomes a slide-out menu at the mobile-nav breakpoint @media (max-width: $mobile-breakpoint) { - width: 33%; + position: absolute; + z-index: 0; + width: 50%; } margin-top: 30px; .nav-stacked { border-right: none; + @media (max-width: $mobile-breakpoint) { + //margin-right: 10px; + } } li a.active { color: $secondary; @@ -230,18 +261,38 @@ td.flaggers td { .admin-detail { width: 76.5765%; @media (max-width: $mobile-breakpoint) { - width: 67%; + z-index: 10; + width: 100%; } - min-height: 800px; + background-color: $secondary; + // Todo: set this properly - it needs to be >= the menu height + min-height: 875px; margin-left: 0; border-left: solid 1px dark-light-diff($primary, $secondary, 90%, -60%); padding: 30px 0 30px 30px; @media (max-width: $mobile-breakpoint) { - padding: 30px 0 30px 16px; + padding: 30px 0; + border: none; + } +} + +.admin-detail.mobile-open { + @media (max-width: $mobile-breakpoint) { + transition: transform 0.3s ease; + transform: (translateX(50%)); + } +} + +.admin-detail.mobile-closed { + @media (max-width: $mobile-breakpoint) { + transition: transform 0.3s ease; + transform: (translateX(0)); } } .settings { + margin-left: 10px; + margin-right: 10px; .setting { padding-bottom: 20px; From 6086b07324d02f5d31a498790cf51cd081dbb74c Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Aug 2015 09:10:21 +1000 Subject: [PATCH 066/237] FIX: hitting enter on Msgs not searching Msgs --- app/assets/javascripts/discourse/controllers/search.js.es6 | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 5f814e99f9..25930fe4e8 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -40,7 +40,10 @@ export default Em.Controller.extend({ let url = '/search?q=' + encodeURIComponent(this.get('term')); const searchContext = this.get('searchContext'); - if (this.get('searchContextEnabled') && searchContext) { + if (this.get('searchContextEnabled') && + searchContext.id.toLowerCase() === this.get('currentUser.username_lower')) { + url += ' in:private'; + } else { url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id); } From 84c6c2b48c2f32c5b4df7e9279d1c199e1caf889 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Aug 2015 10:52:41 +1000 Subject: [PATCH 067/237] correct logic --- .../javascripts/discourse/controllers/search.js.es6 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 25930fe4e8..6380694945 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -40,11 +40,12 @@ export default Em.Controller.extend({ let url = '/search?q=' + encodeURIComponent(this.get('term')); const searchContext = this.get('searchContext'); - if (this.get('searchContextEnabled') && - searchContext.id.toLowerCase() === this.get('currentUser.username_lower')) { - url += ' in:private'; - } else { - url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id); + if (this.get('searchContextEnabled')) { + if (searchContext.id.toLowerCase() === this.get('currentUser.username_lower')) { + url += ' in:private'; + } else { + url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id); + } } return url; From f9deebefb95a2661fa78da3bf2e52a1c3696c880 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Aug 2015 10:59:46 +1000 Subject: [PATCH 068/237] FIX: include theme vars in site customizations --- app/models/site_customization.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/site_customization.rb b/app/models/site_customization.rb index 4b316a0671..ad04f261c5 100644 --- a/app/models/site_customization.rb +++ b/app/models/site_customization.rb @@ -17,7 +17,7 @@ class SiteCustomization < ActiveRecord::Base end def compile_stylesheet(scss) - DiscourseSassCompiler.compile(scss, 'custom') + DiscourseSassCompiler.compile("@import \"theme_variables\";\n" << scss, 'custom') rescue => e puts e.backtrace.join("\n") unless Sass::SyntaxError === e raise e From b4d7ff1dacc3fdc2059a60f089fd0e67aa7fb881 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Aug 2015 11:54:42 +1000 Subject: [PATCH 069/237] correct logic --- app/assets/javascripts/discourse/controllers/search.js.es6 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 6380694945..553543646f 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -41,7 +41,9 @@ export default Em.Controller.extend({ const searchContext = this.get('searchContext'); if (this.get('searchContextEnabled')) { - if (searchContext.id.toLowerCase() === this.get('currentUser.username_lower')) { + if (searchContext.id.toLowerCase() === this.get('currentUser.username_lower') && + searchContext.type === "private_messages" + ) { url += ' in:private'; } else { url += encodeURIComponent(" " + searchContext.type + ":" + searchContext.id); From f06137003b9d80d9f9bbccf6dcd840250181077e Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Aug 2015 16:54:44 +1000 Subject: [PATCH 070/237] logster needs application version --- config/initializers/logster.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb index da61432018..55e7801db6 100644 --- a/config/initializers/logster.rb +++ b/config/initializers/logster.rb @@ -50,3 +50,5 @@ Logster.config.current_context = lambda{|env,&blk| # TODO logster should be able to do this automatically Logster.config.subdirectory = "#{GlobalSetting.relative_url_root}/logs" + +Logster.config.application_version = Discourse.git_version From edcc43d76a7edd2964ecf32b479e647f81657090 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Aug 2015 16:55:44 +1000 Subject: [PATCH 071/237] update logster (has solved button now) --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index b9e2d98cb7..0940c5c26e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -145,7 +145,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (0.8.4.6.pre) + logster (0.8.4.7.pre) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From 56f098dc7d2f5bfefcb82efb6b700880556f72f0 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 17 Aug 2015 18:29:26 +1000 Subject: [PATCH 072/237] update logster --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 76f48280cd..c36478e1dd 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (0.8.4.7.pre) + logster (0.8.4.8.pre) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From fc87e71218f192ae7b16e22b95ff25a7c576c960 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 17 Aug 2015 16:36:59 +0800 Subject: [PATCH 073/237] FIX: Missing error message when bookmark rate limit is hit. --- app/assets/javascripts/discourse/controllers/topic.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 23c803e995..71b0eabfb9 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -233,8 +233,8 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { } if (post) { return post.toggleBookmark().catch(function(error) { - if (error && error.responseText) { - bootbox.alert($.parseJSON(error.responseText).errors[0]); + if (error && error.jqXHR && error.jqXHR.responseText) { + bootbox.alert($.parseJSON(error.jqXHR.responseText).errors[0]); } else { bootbox.alert(I18n.t('generic_error')); } From 5b9a01e3b6675101336dafc946b983a0fce98a25 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 17 Aug 2015 03:23:24 -0700 Subject: [PATCH 074/237] switch to

    for search help headings --- config/locales/client.en.yml | 2 +- config/locales/server.en.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index f674c76880..326f330244 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2672,7 +2672,7 @@ en: description: Read every post in a topic with more than 100 posts google_search: | -

    Search with Google

    +

    Search with Google

    diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 7d525bc314..bd7791a05f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2432,7 +2432,7 @@ en: static: search_help: | -

    Tips

    +

    Tips

    • Title matches are prioritized, so when in doubt, search for titles
    • @@ -2440,7 +2440,7 @@ en:
    • Whenever possible, scope your search to a particular category, user, or topic

    -

    Options

    +

    Options

  • From ddd3a8d3400161f96b2bd6a829f46d88f1559e68 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 17 Aug 2015 03:28:40 -0700 Subject: [PATCH 075/237] change search help word to "options" --- config/locales/client.en.yml | 2 +- config/locales/server.en.yml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 326f330244..b60cb7a198 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -158,7 +158,7 @@ en: admin_title: "Admin" flags_title: "Flags" show_more: "show more" - show_help: "help" + show_help: "options" links: "Links" links_lowercase: one: "link" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index bd7791a05f..7867e33991 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2435,9 +2435,9 @@ en:

    Tips

      -
    • Title matches are prioritized, so when in doubt, search for titles
    • -
    • Unique, uncommon words will always produce the best results
    • -
    • Whenever possible, scope your search to a particular category, user, or topic
    • +
    • Title matches are prioritized – when in doubt, search for titles
    • +
    • Unique, uncommon words will produce the best results
    • +
    • Try searching within a particular category, topic, or user

    Options

    From 58c9ca61327c0ba724e31a952ab07b6f2718d14f Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 17 Aug 2015 03:34:18 -0700 Subject: [PATCH 076/237] make topic map button dimmer --- app/assets/stylesheets/desktop/topic-post.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index fe3f9d7143..181d065511 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -413,7 +413,7 @@ a.star { .btn { border: 0; padding: 0 23px; - color: $primary; + color: scale-color($primary, $lightness: 60%); background: dark-light-diff($primary, $secondary, 97%, -75%); border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); From 80f36b81a6a9f2c246b675c1d6d074b30b7425e8 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 17 Aug 2015 21:10:44 +0800 Subject: [PATCH 077/237] FIX: Rate limit message not shown. --- app/assets/javascripts/discourse/models/composer.js.es6 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index d9805f630a..ae8f324b91 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -504,7 +504,9 @@ const Composer = RestModel.extend({ return post.save(props).then(function(result) { self.clearState(); return result; - }).catch(rollback); + }).catch(function(error) { + throw error; + }); }).catch(rollback); }, From 73e4c6ae4da37f5dac38487670f7876e56e698ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 17 Aug 2015 16:21:23 +0200 Subject: [PATCH 078/237] FIX: backups index template wasn't properly bound --- .../admin/controllers/admin-backups-index.js.es6 | 4 +--- .../javascripts/admin/models/backup-status.js.es6 | 12 ++++++++++++ app/assets/javascripts/admin/models/backup_status.js | 9 --------- .../javascripts/admin/templates/backups_index.hbs | 10 +++++----- 4 files changed, 18 insertions(+), 17 deletions(-) create mode 100644 app/assets/javascripts/admin/models/backup-status.js.es6 delete mode 100644 app/assets/javascripts/admin/models/backup_status.js 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 fb052e6fc5..1617e6133a 100644 --- a/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-backups-index.js.es6 @@ -1,8 +1,6 @@ export default Ember.ArrayController.extend({ needs: ["adminBackups"], - status: Em.computed.alias("controllers.adminBackups"), - isOperationRunning: Ember.computed.alias("status.model.isOperationRunning"), - restoreDisabled: Ember.computed.alias("status.model.restoreDisabled"), + status: Ember.computed.alias("controllers.adminBackups"), uploadLabel: function() { return I18n.t("admin.backups.upload.label"); }.property(), diff --git a/app/assets/javascripts/admin/models/backup-status.js.es6 b/app/assets/javascripts/admin/models/backup-status.js.es6 new file mode 100644 index 0000000000..2de301f955 --- /dev/null +++ b/app/assets/javascripts/admin/models/backup-status.js.es6 @@ -0,0 +1,12 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Discourse.Model.extend({ + + restoreDisabled: Em.computed.not("restoreEnabled"), + + @computed("allowRestore", "isOperationRunning") + restoreEnabled(allowRestore, isOperationRunning) { + return allowRestore && !isOperationRunning; + } + +}); diff --git a/app/assets/javascripts/admin/models/backup_status.js b/app/assets/javascripts/admin/models/backup_status.js deleted file mode 100644 index e67a34bc3f..0000000000 --- a/app/assets/javascripts/admin/models/backup_status.js +++ /dev/null @@ -1,9 +0,0 @@ -Discourse.BackupStatus = Discourse.Model.extend({ - - restoreDisabled: Em.computed.not("restoreEnabled"), - - restoreEnabled: function() { - return this.get('allowRestore') && !this.get("isOperationRunning"); - }.property("isOperationRunning", "allowRestore") - -}); diff --git a/app/assets/javascripts/admin/templates/backups_index.hbs b/app/assets/javascripts/admin/templates/backups_index.hbs index 0b66fcaf73..09bf83bc63 100644 --- a/app/assets/javascripts/admin/templates/backups_index.hbs +++ b/app/assets/javascripts/admin/templates/backups_index.hbs @@ -6,9 +6,9 @@
    {{resumable-upload target="/admin/backups/upload" success="uploadSuccess" error="uploadError" uploadText=uploadLabel title="admin.backups.upload.title"}} {{#if site.isReadOnly}} - {{d-button icon="eye" action="toggleReadOnlyMode" disabled=model.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}} + {{d-button icon="eye" action="toggleReadOnlyMode" disabled=status.model.isOperationRunning title="admin.backups.read_only.disable.title" label="admin.backups.read_only.disable.label"}} {{else}} - {{d-button icon="eye" action="toggleReadOnlyMode" disabled=model.isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}} + {{d-button icon="eye" action="toggleReadOnlyMode" disabled=status.model.isOperationRunning title="admin.backups.read_only.enable.title" label="admin.backups.read_only.enable.label"}} {{/if}}
    @@ -20,12 +20,12 @@
    From d87520a2cf9dc179f4fdec5038dedcaf112da19b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 17 Aug 2015 12:12:51 -0400 Subject: [PATCH 079/237] FIX: Checkboxes weren't always being set properly. Note to all, `set()` values for computed properties must return the new value the same as `get()` does. --- app/assets/javascripts/admin/components/site-setting.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index 09b0dfe19e..f89996c70a 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -34,6 +34,7 @@ export default Ember.Component.extend(BufferedContent, ScrollTop, { }, set(value) { this.set('buffered.value', value ? 'true' : 'false'); + return value; } }, From 827ea641b04396e2cc3f5c8298c3ad475b1a4eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 17 Aug 2015 18:57:28 +0200 Subject: [PATCH 080/237] FIX: Use File.size instead of IO.size --- app/controllers/uploads_controller.rb | 6 +++--- app/jobs/regular/pull_hotlinked_images.rb | 4 ++-- app/models/user_avatar.rb | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 57a1442ed6..3c3eef684d 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -61,15 +61,15 @@ class UploadsController < ApplicationController end # allow users to upload large images that will be automatically reduced to allowed size - if tempfile && tempfile.size > 0 && SiteSetting.max_image_size_kb > 0 && FileHelper.is_image?(filename) + if tempfile && File.size(tempfile.path) > 0 && SiteSetting.max_image_size_kb > 0 && FileHelper.is_image?(filename) attempt = 5 - while attempt > 0 && tempfile.size > SiteSetting.max_image_size_kb.kilobytes + while attempt > 0 && File.size(tempfile.path) > SiteSetting.max_image_size_kb.kilobytes OptimizedImage.downsize(tempfile.path, tempfile.path, "80%", allow_animation: SiteSetting.allow_animated_thumbnails) attempt -= 1 end end - upload = Upload.create_for(current_user.id, tempfile, filename, tempfile.size, content_type: content_type, image_type: type) + upload = Upload.create_for(current_user.id, tempfile, filename, File.size(tempfile.path), content_type: content_type, image_type: type) if upload.errors.empty? && current_user.admin? retain_hours = params[:retain_hours].to_i diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 7d114653d4..87a63b9855 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -36,9 +36,9 @@ module Jobs rescue Discourse::InvalidParameters end if hotlinked - if hotlinked.size <= @max_size + if File.size(hotlinked.path) <= @max_size filename = File.basename(URI.parse(src).path) - upload = Upload.create_for(post.user_id, hotlinked, filename, hotlinked.size, { origin: src }) + upload = Upload.create_for(post.user_id, hotlinked, filename, File.size(hotlinked.path), { origin: src }) downloaded_urls[src] = upload.url else Rails.logger.error("Failed to pull hotlinked image: #{src} - Image is bigger than #{@max_size}") diff --git a/app/models/user_avatar.rb b/app/models/user_avatar.rb index a2e6d1e631..f61df736bf 100644 --- a/app/models/user_avatar.rb +++ b/app/models/user_avatar.rb @@ -20,7 +20,7 @@ class UserAvatar < ActiveRecord::Base max = Discourse.avatar_sizes.max gravatar_url = "http://www.gravatar.com/avatar/#{email_hash}.png?s=#{max}&d=404" tempfile = FileHelper.download(gravatar_url, SiteSetting.max_image_size_kb.kilobytes, "gravatar") - upload = Upload.create_for(user.id, tempfile, 'gravatar.png', tempfile.size, origin: gravatar_url, image_type: "avatar") + upload = Upload.create_for(user.id, tempfile, 'gravatar.png', File.size(tempfile.path), origin: gravatar_url, image_type: "avatar") if gravatar_upload_id != upload.id gravatar_upload.try(:destroy!) @@ -68,7 +68,7 @@ class UserAvatar < ActiveRecord::Base ext = FastImage.type(tempfile).to_s tempfile.rewind - upload = Upload.create_for(user.id, tempfile, "external-avatar." + ext, tempfile.size, origin: avatar_url, image_type: "avatar") + upload = Upload.create_for(user.id, tempfile, "external-avatar." + ext, File.size(tempfile.path), origin: avatar_url, image_type: "avatar") user.uploaded_avatar_id = upload.id unless user.user_avatar From c0e88724c2a770efee6a3595d9dd36bf35eda8b1 Mon Sep 17 00:00:00 2001 From: Jonathan Brachthaeuser Date: Mon, 17 Aug 2015 19:01:15 +0200 Subject: [PATCH 081/237] Preserve user-field options when updating user-fields Avoid deleting options of the user-field when no options are transmitted. --- .../admin/user_fields_controller.rb | 8 ++++---- .../admin/user_fields_controller_spec.rb | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/app/controllers/admin/user_fields_controller.rb b/app/controllers/admin/user_fields_controller.rb index 0980381ec4..fceecfa7d1 100644 --- a/app/controllers/admin/user_fields_controller.rb +++ b/app/controllers/admin/user_fields_controller.rb @@ -9,7 +9,7 @@ class Admin::UserFieldsController < Admin::AdminController field.position = (UserField.maximum(:position) || 0) + 1 field.required = params[:required] == "true" - fetch_options(field) + update_options(field) json_result(field, serializer: UserFieldSerializer) do field.save @@ -30,8 +30,7 @@ class Admin::UserFieldsController < Admin::AdminController field.send("#{col}=", field_params[col]) end end - UserFieldOption.where(user_field_id: field.id).delete_all - fetch_options(field) + update_options(field) if field.save render_serialized(field, UserFieldSerializer, root: 'user_field') @@ -48,9 +47,10 @@ class Admin::UserFieldsController < Admin::AdminController protected - def fetch_options(field) + def update_options(field) options = params[:user_field][:options] if options.present? + UserFieldOption.where(user_field_id: field.id).delete_all field.user_field_options_attributes = options.map {|o| {value: o} }.uniq end end diff --git a/spec/controllers/admin/user_fields_controller_spec.rb b/spec/controllers/admin/user_fields_controller_spec.rb index bd6bd517db..fe63ce860d 100644 --- a/spec/controllers/admin/user_fields_controller_spec.rb +++ b/spec/controllers/admin/user_fields_controller_spec.rb @@ -74,6 +74,25 @@ describe Admin::UserFieldsController do expect(user_field.field_type).to eq('dropdown') expect(user_field.user_field_options.size).to eq(2) end + + it "keeps options when updating the user field" do + xhr :put, :update, id: user_field.id, user_field: {name: 'fraggle', + field_type: 'dropdown', + description: 'muppet', + options: ['hello', 'hello', 'world'], + position: 1} + expect(response).to be_success + user_field.reload + expect(user_field.user_field_options.size).to eq(2) + + xhr :put, :update, id: user_field.id, user_field: {name: 'fraggle', + field_type: 'dropdown', + description: 'muppet', + position: 2} + expect(response).to be_success + user_field.reload + expect(user_field.user_field_options.size).to eq(2) + end end end From a3e76dc193335f58338801d2eb193da44665f87b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 17 Aug 2015 19:21:30 +0200 Subject: [PATCH 082/237] FIX: allow HTTP <-> HTTPS redirections when downloading images --- Gemfile | 2 ++ Gemfile.lock | 2 ++ lib/file_helper.rb | 2 +- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 98cdfdd790..c6f88f8b81 100644 --- a/Gemfile +++ b/Gemfile @@ -53,6 +53,8 @@ gem 'fast_xs' gem 'fast_xor' +gem 'open_uri_redirections' + # while we sort out https://github.com/sdsykes/fastimage/pull/46 gem 'fastimage_discourse', require: 'fastimage' gem 'aws-sdk', require: false diff --git a/Gemfile.lock b/Gemfile.lock index c36478e1dd..38d014e9df 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -214,6 +214,7 @@ GEM multi_json (~> 1.11) mustache nokogiri (~> 1.6.6) + open_uri_redirections (0.2.1) openid-redis-store (0.0.2) redis ruby-openid @@ -443,6 +444,7 @@ DEPENDENCIES omniauth-openid omniauth-twitter onebox + open_uri_redirections openid-redis-store pg pry-nav diff --git a/lib/file_helper.rb b/lib/file_helper.rb index 008dc55216..30f9895a71 100644 --- a/lib/file_helper.rb +++ b/lib/file_helper.rb @@ -14,7 +14,7 @@ class FileHelper tmp = Tempfile.new([tmp_file_name, extension]) File.open(tmp.path, "wb") do |f| - downloaded = uri.open("rb", read_timeout: 5, redirect: follow_redirect) + downloaded = uri.open("rb", read_timeout: 5, redirect: follow_redirect, allow_redirections: :all) while f.size <= max_file_size && data = downloaded.read(512.kilobytes) f.write(data) end From 2d4729782e23c0476788bac2df634235cef698f1 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 17 Aug 2015 13:58:19 -0400 Subject: [PATCH 083/237] FIX: Support quarterly on user directory --- app/models/directory_item.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/directory_item.rb b/app/models/directory_item.rb index ec6146162b..8e6ad2d2ce 100644 --- a/app/models/directory_item.rb +++ b/app/models/directory_item.rb @@ -13,7 +13,7 @@ class DirectoryItem < ActiveRecord::Base end def self.period_types - @types ||= Enum.new(:all, :yearly, :monthly, :weekly, :daily) + @types ||= Enum.new(:all, :yearly, :monthly, :weekly, :daily, :quarterly) end def self.refresh! @@ -28,6 +28,7 @@ class DirectoryItem < ActiveRecord::Base since = case period_type when :daily then 1.day.ago when :weekly then 1.week.ago + when :quarterly then 3.weeks.ago when :monthly then 1.month.ago when :yearly then 1.year.ago else 1000.years.ago From 7eb32be4def71900a4356e3ac5595d0a0cd261c6 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 17 Aug 2015 15:03:55 -0400 Subject: [PATCH 084/237] Add support for plugins to declare ES6 in the admin bundle --- app/assets/javascripts/admin.js.erb | 8 ++++++++ app/assets/javascripts/application.js.erb | 14 +++++--------- lib/discourse_plugin_registry.rb | 18 ++++++++++++++++-- lib/plugin/instance.rb | 5 +++++ 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/admin.js.erb b/app/assets/javascripts/admin.js.erb index ce47046a21..41f0315deb 100644 --- a/app/assets/javascripts/admin.js.erb +++ b/app/assets/javascripts/admin.js.erb @@ -2,4 +2,12 @@ require_asset("main_include_admin.js") DiscoursePluginRegistry.admin_javascripts.each { |js| require_asset(js) } + +DiscoursePluginRegistry.each_globbed_asset(admin: true) do |f, ext| + if File.directory?(f) + depend_on(f) + elsif f.to_s.end_with?(".#{ext}") + require_asset(f) + end +end %> diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index 64a1f7dd39..4b040e3bd6 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -6,15 +6,11 @@ require_asset ("./main_include.js") DiscoursePluginRegistry.javascripts.each { |js| require_asset(js) } DiscoursePluginRegistry.handlebars.each { |hb| require_asset(hb) } -# Load any glob dependencies -DiscoursePluginRegistry.asset_globs.each do |g| - root, extension = *g - Dir.glob("#{root}/**/*") do |f| - if File.directory?(f) - depend_on(f) - elsif f.to_s.end_with?(".#{extension}") - require_asset(f) - end +DiscoursePluginRegistry.each_globbed_asset do |f, ext| + if File.directory?(f) + depend_on(f) + elsif f.to_s.end_with?(".#{ext}") + require_asset(f) end end diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index 8875807f5a..f3003c4e33 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -77,8 +77,22 @@ class DiscoursePluginRegistry Archetype.register(name, options) end - def self.register_glob(root, extension) - self.asset_globs << [root, extension] + def self.register_glob(root, extension, options=nil) + self.asset_globs << [root, extension, options || {}] + end + + def self.each_globbed_asset(each_options=nil) + each_options ||= {} + + self.asset_globs.each do |g| + root, ext, options = *g + + next if options[:admin] && !each_options[:admin] + + Dir.glob("#{root}/**/*") do |f| + yield f, ext + end + end end def self.register_asset(asset, opts=nil) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index b0f7c37db1..caa406ec24 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -223,6 +223,10 @@ class Plugin::Instance root_path = "#{File.dirname(@path)}/assets/javascripts" DiscoursePluginRegistry.register_glob(root_path, 'js.es6') DiscoursePluginRegistry.register_glob(root_path, 'hbs') + + admin_path = "#{File.dirname(@path)}/admin/assets/javascripts" + DiscoursePluginRegistry.register_glob(admin_path, 'js.es6', admin: true) + DiscoursePluginRegistry.register_glob(admin_path, 'hbs', admin: true) end self.instance_eval File.read(path), path @@ -241,6 +245,7 @@ class Plugin::Instance # Automatically include assets Rails.configuration.assets.paths << auto_generated_path Rails.configuration.assets.paths << File.dirname(path) + "/assets" + Rails.configuration.assets.paths << File.dirname(path) + "/admin/assets" # Automatically include rake tasks Rake.add_rakelib(File.dirname(path) + "/lib/tasks") From fc2fe5f02d83a277b9403472cd0fa9bd40e56bc1 Mon Sep 17 00:00:00 2001 From: Jonathan Brachthaeuser Date: Mon, 17 Aug 2015 21:44:08 +0200 Subject: [PATCH 085/237] Use userfield serializer in json dump Use userfield serializer for json dump to make sure that also the options are serialized correctly. --- app/models/site.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/models/site.rb b/app/models/site.rb index 4d3c159051..719cf1d712 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -80,7 +80,9 @@ class Site return { periods: TopTopic.periods.map(&:to_s), filters: Discourse.filters.map(&:to_s), - user_fields: UserField.all + user_fields: UserField.all.map do |userfield| + UserFieldSerializer.new(userfield, root: false, scope: guardian) + end }.to_json end From 7b5dea64814780714298a1098a82163fbe087e5a Mon Sep 17 00:00:00 2001 From: Kane York Date: Mon, 17 Aug 2015 14:28:00 -0700 Subject: [PATCH 086/237] FEATURE: Take advantage of Android browser features Declare a theme-color, provide high-resolution icon. --- app/views/layouts/_head.html.erb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb index c5f6677f22..a74f93bafe 100644 --- a/app/views/layouts/_head.html.erb +++ b/app/views/layouts/_head.html.erb @@ -3,6 +3,10 @@ +<% if SiteSetting.apple_touch_icon_url != "/images/default-apple-touch-icon.png" %> + +<% end %> + From bcb33ca69d70f1b4a3711cd2f5f6e49fd84a1aab Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 18 Aug 2015 12:26:36 +1000 Subject: [PATCH 087/237] logster update, fixes bad escaping in env --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 38d014e9df..046d43bf13 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (0.8.4.8.pre) + logster (0.9.9) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From fd1693482f887bbcb28439f37ee221b3e8cfeb01 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 18 Aug 2015 13:11:46 +1000 Subject: [PATCH 088/237] bump logster to fix solved button in logster --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 046d43bf13..fd507523c8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (0.9.9) + logster (1.0.0.0.pre) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From 4296bee86e0f86d2fa03c218afe83698b69119d6 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 18 Aug 2015 09:56:54 +0530 Subject: [PATCH 089/237] Update Translations --- config/locales/client.ar.yml | 1 + config/locales/client.fr.yml | 6 + config/locales/client.he.yml | 14 ++ config/locales/client.zh_CN.yml | 5 +- config/locales/server.ar.yml | 340 ++++++++++++++++++++++++++++++++ config/locales/server.fr.yml | 21 ++ config/locales/server.he.yml | 14 ++ config/locales/server.zh_CN.yml | 2 + 8 files changed, 402 insertions(+), 1 deletion(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 412e97dc95..9a16a4933b 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -2111,6 +2111,7 @@ ar: header: "Header" top: "Top" footer: "تذييل " + embedded_css: "تضمين CSS" head_tag: text: "" title: "HTML that will be inserted before the tag" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index d0f44d1124..9ea147e9f2 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -376,10 +376,13 @@ fr: label: "Notifications de bureau" not_supported: "Les notifications ne sont pas supportées sur ce navigateur. Désolé." perm_default: "Activer les notifications" + perm_denied_btn: "Permission Refusée" + perm_denied_expl: "Vous avez refusé la permission pour les notifications. Utilisez votre navigateur pour activer les notifications, puis appuyez sur le bouton une fois terminé. (Bureau : L'icône la plus à gauche dans la barre d'adresse. Mobile : 'Info Site'.)" disable: "Désactiver les notifications" currently_enabled: "(activé actuellement)" enable: "Activer les notifications" currently_disabled: "(désactivé pour le moment)" + each_browser_note: "Note : Vous devez changer ce paramètre sur chaque navigateur que vous utilisez." dismiss_notifications: "Marquer tout comme lu" dismiss_notifications_tooltip: "Marquer comme lues toutes les notifications non lues" disable_jump_reply: "Ne pas se déplacer à mon nouveau message après avoir répondu" @@ -1118,6 +1121,8 @@ fr: one: vous avez sélectionné 1 message. other: Vous avez sélectionné {{count}} messages. post: + reply: " {{replyAvatar}} {{usernameLink}}" + reply_topic: " {{link}}" quote_reply: "Citer" edit: "Éditer {{link}} par {{replyAvatar}} {{username}}" edit_reason: "Raison :" @@ -1790,6 +1795,7 @@ fr: header: "En-tête" top: "Top" footer: "Pied de page" + embedded_css: "CSS intégré" head_tag: text: "" title: "HTML qui sera inséré avant la balise " diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 69566eaa03..776c5faf1e 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -108,6 +108,7 @@ he: google+: 'שתף קישור זה בגוגל+' email: 'שלח קישור בדוא"ל' action_codes: + split_topic: "פצל נושא זה" autoclosed: enabled: 'סגר את הנושא %{when}' disabled: 'פתח את הנושא %{when}' @@ -370,6 +371,17 @@ he: invited_by: "הוזמן/הוזמנה על ידי" trust_level: "רמת אמון" notifications: "התראות" + desktop_notifications: + label: "התראות לשולחן העבודה" + not_supported: "התראות לא נתמכות בדפדפן זה. מצטערים." + perm_default: "הדלק התראות" + perm_denied_btn: "הרשאות נדחו" + perm_denied_expl: "נטרלת הראשות עבור התראות. השתמש בדפדפן שלך לאפשר התראות, לאחר מכן לחץ על הכפתור. " + disable: "כבה התראות" + currently_enabled: "(כרגע מאופשר)" + enable: "אפשר התראות" + currently_disabled: "(כרגע לא מאופשר)" + each_browser_note: "הערה: עליך לשנות הגדרה זו עבור כל דפדפן בנפרד." dismiss_notifications: "סימון הכל כנקרא" dismiss_notifications_tooltip: "סימון כל ההתראות שלא נקראו כהתראות שנקראו" disable_jump_reply: "אל תקפצו לפרסומים שלי לאחר שאני משיב/ה" @@ -382,6 +394,7 @@ he: admin: "{{user}} הוא מנהל ראשי" moderator_tooltip: "משתמש זה הינו מנחה (Moderator)" admin_tooltip: "משתמש זה הינו מנהל מערכת (Admin)" + blocked_tooltip: "משתמש זה חסום" suspended_notice: "המשתמש הזה מושעה עד לתאריך: {{date}}." suspended_reason: "הסיבה: " github_profile: "גיטהאב" @@ -442,6 +455,7 @@ he: upload_title: "העלה את התמונה שלך" upload_picture: "העלאת תמונה" image_is_not_a_square: "אזהרה: קיצצנו את התמונה שלך; האורך והרוחב לא היו שווים." + cache_notice: "שינית את תמונת הפרופיל שלך בהצלחה אבל יכול לקחת קצת זמן עד שהתמונה תופיע." change_profile_background: title: "שינוי רקע פרופיל" instructions: "רקעי הפרופיל ימורכזו ויוצגו ברוחב ברירת מחדל של 850px." diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 0b9f785a51..9dbe8fef21 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -183,7 +183,7 @@ zh_CN: remove: "删除书签" confirm_clear: "你确定要删除该主题的所有书签吗?" topic_count_latest: - other: "{{count}} 个新的或更新的主题。" + other: "{{count}} 个新主题或更新的主题。" topic_count_unread: other: "{{count}} 未读主题。" topic_count_new: @@ -1070,6 +1070,8 @@ zh_CN: description: other: 你已经选择了 {{count}} 个帖子。 post: + reply: " {{replyAvatar}} {{usernameLink}}" + reply_topic: " {{link}}" quote_reply: "引用回复" edit: "编辑 {{replyAvatar}} {{username}} 发表的 {{link}}" edit_reason: "理由:" @@ -1709,6 +1711,7 @@ zh_CN: header: "头部" top: "顶部" footer: "底部" + embedded_css: "嵌入的 CSS" head_tag: text: "" title: "将在 标签前插入的 HTML" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index fce7ed2d75..23e65e0b6d 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -188,6 +188,55 @@ ar: few: "مشاركات قليلة" many: "مشاركات كثيرة" other: "%{count} مشاركات" + new-topic: | + مرحبا بك في %{site_name} — **شكرا لك لأجل بدء محادثه جديدة!** + + هل يبدو العنوان مثيرا للاهتمام إذا كنت تقرأ بصوت عال؟ هو ملخص جيد؟ + + - من هو الشخص الذي سيكون مهتما بهذا ؟ لماذا يهم ؟ أي نوع من ردود تريد ؟ + + - تتضمن أكثر الكلمات الشائعة استخداما في في موضوعك بحيث يمكن للآخرين أن * يجدوها* لمجموعة موضوعك مع المواضيع ذات الصلة، حدد فئة. + + لمعلومات أكثر ، [انظر المبادئ التوجيهية مجتمعنا] (/guidelines). وهذا اللوحة ستظهر لك لأول مرة فقط %{education_posts_text}. + new-reply: | + مرحباً بك في %{site_name} — **شكراً للمساهمة**! + + -هل يُحسن ردّك المحادثة في جانب معين؟ + + -كن طيبا مع أصدقاءك أعضاء المجتمع. + + -نرحب بالنقد البناء, ولكن انتقاد الأفكار لا الناس! + + للمزيد, [انظر ارشادات المجتمع](/guidelines). هذه اللوحة ستظهر فقط لأول %{education_posts_text} لك. + avatar: | + ### ماذا عن صورة لحسابك؟ + + لقد شاركت بعض المواضيع والردود, لكن صورة ملف تعريفك ليست مميزة مثلك --هذه رسالة فقط؟ + + هل فكرت في ** [زيارة ملف التعريف الخاص بك] (%{profile_path})** ورفع الصورة التي تمثل لك؟ + + تسهل متابعة النقاشات وايجاد الاشخاص المهمين في المحادثة في حين أن الجميع لديه صورة حساب مميزة ! + sequential_replies: | + ### الرد على عدة مشاركات مرةً واحدة + + بدلاً من عدة ردود متتابعة على موضوع ما, فضلا رداً واحداً يتضمن اقتباسات من مشاركات سابقة أو @اسم كمراجع. + + تستطيع التعديل على ردك السابق لإضافة اقتباس بتضليلك النص و اختيارك زر quote reply الظاهر. + + يسهل على الكل قراءة المواضيع التي لها بعض الردود المتعمقة بدلاً من الكثير من الردود الفردية الصغيرة + dominating_topic: | + ### دع الآخرين ينضموا للمحادثة + + هذا الموضوع ذو أهمية واضحة بالنسبة لك – لقد شارك أكثر من {percent}% % من الردود هنا. + + هل أنت متأكد أنك توفر الوقت الكافي لأشخاص آخرين ليشاركوا وجهات نظرهم, أيضاً؟ + too_many_replies: | + ### لقد وصلت إلى حدود الرد في هذا الموضوع + + نحن آسفون ، ولكن الأعضاء الجدد محدودين مؤقتا %{newuser_max_replies_per_topic} بالردود على نفس الموضوع. + + بدلا من إضافة رد آخر، يرجى النظر في تحرير الردود السابقة، أو زيارة مواضيع أخرى. + reviving_old_topic: "### إحياء هذا الموضوع؟\n\nآخر رد لهذا الموضوع هو الآن قبل %{days} يوم. ردُّك سيرفع الموضوع الى \nأعلى القائمته وينبه أي شخص سبق وشارك في المحادثة.\n\nهل أنت متأكد أنك تردي الاستمرار في هذه المحادثة القديمة؟\n" activerecord: attributes: category: @@ -229,6 +278,7 @@ ar: meta_category_description: "نقاش حول هذا الموقع, كيفية تنظيمه والعمل عليه أكثر وكيف يمكننا تحسينه." staff_category_name: "الإدارة" staff_category_description: "فئة مخصصة للنقاش في مواضيع الإدارة. المواضيع مرئية فقط للأدارة وللمشرفين." + assets_topic_body: "هذا الموضوع مشاهد بشكل دائم للأعضاء فقط . ولحفظ الصورة والملف استخدم موقع التصميم. لا تقوم بحذفها\n\n\nكيف يكون العمل هنا.\n\n1. الرد على الموضوع.\n\n2. رفع كل الصور التي تود أن تستخدمها في الشعار." lounge_welcome: title: "مرحبا بك في الاستراحة." category: @@ -330,6 +380,41 @@ ar: few: "أيام قليلة" many: "أيام كثيرة" other: "%{count} أيام" + about_x_months: + zero: "لا يوجد شهور" + one: "شهر فقط" + two: "شهران" + few: "شهور قليلة" + many: "شهور كثيرة" + other: "%{count}شهر" + x_months: + zero: "1 شهر" + one: "1 شهر" + two: "شهرين{count}%" + few: "شهور{count}%" + many: "شهور{count}%" + other: "شهور{count}%" + about_x_years: + zero: "1 س" + one: "1 س" + two: "%{count} س" + few: "%{count} س" + many: "%{count} س" + other: "%{count} س" + over_x_years: + zero: "س > 1" + one: "س > 1" + two: "س > %{count}" + few: "س > %{count}" + many: "س > %{count}" + other: "س > %{count}" + almost_x_years: + zero: "1 س" + one: "1 س" + two: "س %{count}" + few: "س %{count}" + many: "س %{count}" + other: "س %{count}" distance_in_words_verbose: half_a_minute: "الأن" less_than_x_seconds: @@ -346,6 +431,13 @@ ar: few: "%{count} ثواني مضت" many: "%{count} ثواني مضت" other: "%{count} ثواني مضت" + less_than_x_minutes: + zero: "أقل من 1 دقيقة مضت" + one: "أقل من 1دقيقة مضت" + two: "أقل من %{count} دقيقتين مضت" + few: "أقل من %{count} دقائق مضت" + many: "أقل من %{count} دقائق مضت" + other: "أقل من %{count} دقائق مضت" x_minutes: zero: "1 دقيقة مضت" one: "1 دقيقة مضت" @@ -395,6 +487,13 @@ ar: few: "منذ %{count} سنوات مضت" many: "منذ %{count} سنوات مضت" other: "منذ %{count} سنوات مضت" + almost_x_years: + zero: "تقريبا 1 سنة مضت" + one: "تقريبا 1 سنة مضت" + two: "تقريبا %{count} سنتين مضت" + few: "تقريبا %{count} سنوات مضت" + many: "تقريبا %{count} سنوات مضت" + other: "تقريبا %{count} سنوات مضت" password_reset: no_token: "عذراً , رابط تغيير كلمة المرور قديم . رجاءً أعد عملية إسترجاع كلمة المرور ." choose_new: "يرجى اختيار كلمة مرور جديدة" @@ -415,6 +514,8 @@ ar: please_continue: "تأكيد حسابك الجديد : جاري توجيهك إلى الصفحة الرئيسية ." continue_button: "الاستمرار لـ %{site_name}" welcome_to: "مرحبا بك في %{site_name}!" + approval_required: "يجب على المشرف أن يقبل حسابك الجديد قبل أن تستطيع الدخول لهذا المنتدى. سوف تتلقى بريد إلكتروني عندما يتم قبول حسابك." + missing_session: "نحن لا نستطيع أن نتحقق أن حسابك قد تمّ إنشاءه ، يرجى التأكد بإن ملفات تعريف الاتباط ** الملفات المؤقته مفعّله لديك ** cookies ." post_action_types: off_topic: title: 'خارج الموضوع' @@ -435,6 +536,7 @@ ar: notify_moderators: title: "شيء آخر " description: 'تتطلب هذه الوظيفة الاهتمام مشرف لسبب آخر غير المذكورة اعلاه.' + long_form: 'علم هذا لتنبيه المراقب' email_title: 'المشاركة "%{title}" تتطلب موافقة المشرف' email_body: "%{link}\n\n%{message}" bookmark: @@ -459,17 +561,27 @@ ar: long_form: 'ترفع علم هذا عن صورة غير ملائمة' notify_moderators: title: "شيء آخر " + long_form: 'علم هذا لتنبيه المراقب' email_title: 'الموضوع "%{title}" يتطلب موافقة المشرف' email_body: "%{link}\n\n%{message}" + flagging: + user_must_edit: '

    هذه المشاركة كانت معلّمة بواسطة المجتمع وهي مخفية مؤقتاً.

    ' archetypes: regular: title: "موضوع عادي" banner: title: "موضوع إعلاني" + message: + make: "هذا الموضوع معلّم الآن. سوف يظهر في أعلى كل صفحة حتى يرفض من قبل المستخدم." + remove: "هذا الموضوع لم يعد معلّم. لن يظهر في أعلى كل صفحة." unsubscribed: title: 'غير مشترك' description: "لقد تم إلغاء اشتراكك. ونحن لن نتصال بك مرة أخرى!" + oops: "في حال أنك لا تقصد عمل هذا , أضغط أسفل." error: "خطأ في إلغاء الإشتراك" + preferences_link: "تستطيع أيضاً إلغاء الاشتراك من رسائل الربيد التلخيصية عن طريق صفحة إعداداتك preferences page" + different_user_description: "أنت حالياً سجلت دخولك كمستخدم مختلف عن الذي كان يُرسله له الملخص. الرجاء تسجيل الدخول والمحاولة مجدداً." + not_found_description: "نأسف ، لم نستطيع أن نلغي اشتراكك . من الممكن أن الرابط المرسل إليك قد انتهت مدته . " resubscribe: action: "إعادة الاشتراك." title: "إعادة الاشتراك." @@ -516,6 +628,7 @@ ar: xaxis: "يوم" yaxis: "عدد رسائل البريد الإلكتروني" user_to_user_private_messages: + title: "عضو لعضو" xaxis: "يوم" yaxis: "عدد الرسائل" system_private_messages: @@ -527,6 +640,7 @@ ar: xaxis: "يوم" yaxis: "عدد الرسائل" notify_moderators_private_messages: + title: "أبلغ المراقبين" xaxis: "اليوم" yaxis: "عدد الرسائل" notify_user_private_messages: @@ -539,11 +653,13 @@ ar: num_clicks: "نقرات" num_topics: "المواضيع" top_traffic_sources: + title: "أعلى مصادر مرور" xaxis: "مجال" num_clicks: "نقرات" num_topics: "المواضيع" num_users: "اﻷعضاء" top_referred_topics: + title: "أعلى مواضيع مشارة" xaxis: "موضوع" num_clicks: "نقرات" page_view_anon_reqs: @@ -579,24 +695,48 @@ ar: xaxis: "يوم" yaxis: "طلبات ناجحة (Status 2xx)" http_3xx_reqs: + title: "HTTP 3xx (إعادة توجيه)" xaxis: "يوم" + yaxis: "إعادة توجيه الطلبات (الحالة 3xx)" http_4xx_reqs: + title: "HTTP 4xx (خطأ العميل)" xaxis: "يوم" + yaxis: "خطأ العميل (الحالة 4xx)" http_5xx_reqs: + title: "HTTP 5xx (خطأ الخادم)" xaxis: "يوم" + yaxis: "خطأ الخادم (الحالة 5xx)" http_total_reqs: title: "مجموع" xaxis: "يوم" yaxis: "إجمالي طلبات" time_to_first_response: + title: "وقت أول استجابة" xaxis: "اليوم" + yaxis: "متوسط الوقت (ساعات)" topics_with_no_response: + title: "مواضيع بدون استجابة" xaxis: "اليوم" yaxis: "مجموع" mobile_visits: title: "زيارات العضو" xaxis: "اليوم" + yaxis: "عدد الزيارات" + dashboard: + rails_env_warning: "خادمك يعمل في وضع %{env}." + ruby_version_warning: "أنت تشغل إصدار روبي \"Ruby 2.0.0\" المعروف بالمشاكل. قم بترقيته إلى باتش مستوى 247 أو أحدث." + host_names_warning: "ملف config/database.yml الخاص بك يستخدم الآن المضيف المحلي الافتراضي hostname. حدّثه لتستخدم مضيف موقعك." + gc_warning: 'خادمك يستخدم معاير روبي "ruby" لجمع المهملات, الذي لن يقدم أفض كفاءة . اقرأ هذا الموضوع عن ضبط الآداء: Tuning Ruby and Rails for Discourse.' + sidekiq_warning: "\"Sidekiq\" لا يعمل! \nالعديد من المهام, كإرسال البريدوغيرها, يتم تنفيذها بشكل غير متزامن من قبل \"sidekiq\". الرجاء التحقيق من عمل احدى وضائف الـ\"Sidekiq\". Learn about Sidekiq here." + queue_size_warning: 'عدد المهام قيد الانتظار هو %{queue_size}, وهو مرتفع. وقد يسبب مشكلة مع احدى/كل مهام "Sidekiq", أو قد تحتاج الى إضافة المزيد من (Sidekiq workers).' + memory_warning: 'خادمك يعمل بأقل من 1 جيجا بايت من الذاكرة الإجمالية. يتطلب على الأقل 1 جيجا بايت من الذاكرة.' + contact_email_invalid: "البريد الالكتروني للموقع معطل. حدّثه Site Settings." + title_nag: "أدخل اسم موقعك. حدث العنوان في الاعدادات Site Settings." + site_description_missing: "أدخل جملة وصفية واحدة لموقعك والتي ستظهر في نتائج البحث. حدّث وصف_الموقع في اعدادات الموقع Site Settings." + notification_email_warning: "رسائل التنبية البريدية لم تكن مرسلة من عنوان بريدي صالح على مجالك؛ توصيل البريد سيكون غير منتظم ولايمكن الاعتماد عليها. الرجاء وضع رسائل التنبية في عنوان بريدي صالح في إعدادات الموقع Site Settings." content_types: + education_new_reply: + title: "مستخدم جديد للتعليم: الردود الأولى" education_new_topic: title: "مستخدم جديد للتعليم: موضوعات الأولى" usage_tips: @@ -604,13 +744,40 @@ ar: description: "الإرشادات والمعلومات الأساسية للمستخدمين الجدد. " welcome_user: title: "مرحباً: عضو جديد" + description: "هذه الرسالة تُرسل تلقائيًا لجميع أعضائنا الجدد حال اشتراكهم" + welcome_invite: + title: "مرحباً: عضو مدعو" + login_required_welcome_message: + title: "مطلوب لتسجيل الدخول : رسالة ترحيب" + login_required: + title: "مطلوب لتسجيل الدخول : الصفحة الرئيسية" + head: + title: "عنوان HTML رئيسي " + top: + title: "أعلى الصفحة" + bottom: + title: "أسفل الصفحة" site_settings: + delete_old_hidden_posts: "سيتم حذف الوظائف المخفية تلقائيًا إذا زادت مدة الإخفاء أكثر من 30 يومًا" + default_locale: "اللغة الإفتراضية لهذا الديسكورس نموذج (ISO 639-1 Code)" + allow_user_locale: "السماح للمستخدمين باختيار واجهة تفضيل لغة خاصة بهم." + min_post_length: "الحد الأدنى المسموح به لطول مشاركة في الأحرف" + min_first_post_length: "الحد الأدنى المسموح به لطول أول المشاركة (مقدمة العنوان) بالأحرف" + min_private_message_post_length: "الحد الأدنى المسموح به لطول مشاركة في الأحرف" + max_post_length: "الحد الأعلى المسموح به لطول مشاركة في الأحرف" min_topic_title_length: "الحد الأدنى المسموح به موضوع طول اللقب في الأحرف" + max_topic_title_length: "الحد الأعلى المسموح به لطول عنوان موضوع في الأحرف" + min_private_message_title_length: "الحد الأدنى المسموح به لطول عنوان لرسالة في الأحرف" + min_search_term_length: "الحد الأدنى الصالح لطول مصطلح في الأحرف" + uncategorized_description: "الوصف للفئة غير المصنفة. اتركه فارغا لعدم الوصف." favicon_url: "إيقونة لموقعك , شاهد http://en.wikipedia.org/wiki/Favicon" + summary_max_results: "الحد الأقصى للمشاركات العائدة بـ 'ملخص هذا الموضوع'" + enable_private_messages: "يسمح مستوى الثقة 1 للمستخدمين بإنشاء والرد على الرسائل" allow_moderators_to_create_categories: "السماح للمشرفين إنشاء قسم جديد" min_password_length: "أقل طول لكلمة المرور" block_common_passwords: "لا تسمح لكلمات المرور المسجلة في قائمة كلمات المرور الشائعةز" enable_yahoo_logins: "تفعيل مصادقة ياهو" + google_oauth2_client_id: "التسجيل بحسابك الشخصي في جوجل" enable_twitter_logins: "تفعيل مصادقة تويتر , يطلب : twitter_consumer_key و twitter_consumer_secret" max_likes_per_day: "أقصى عدد للإعجابات لكل عضو باليوم." max_flags_per_day: "أقصى عدد للإعلامات لكل عضو باليوم." @@ -620,16 +787,45 @@ ar: max_private_messages_per_day: "أقصى عدد لرسائل الأعضاء التي يمكن إنشائها باليوم." max_invites_per_day: "أقصى عدد للدعوات التي يمكن للعضو إرسالها باليوم." max_topic_invitations_per_day: "أقصى عدد لدعوات الموضوع التي يمكن للعضو إرسالها باليوم." + newuser_max_links: "عدد الروابط التي يمكن للمستخدم الجديد إضافتها للمشاركة." + newuser_max_images: "عدد الصور التي يمكن للمستخدم الجديد إضافتها للمشاركة." + newuser_max_attachments: "عدد المرفقات التي يمكن للمستخدم الجديد إضافتها للمشاركة." + title_max_word_length: "الحد الأقصى المسموح لطول كلمة، بالأحرف، في عنوان الموضوع." full_name_required: "الإسم الكامل مطلوب وهو ضروري لإكمال الحساب " enable_names: "عرض الاسم الكامل للعضو , بطاقة العضو , ورسائل البريد الالكتروني , تعطيل عرض الاسم في اي مكان " display_name_on_posts: "عرض الاسم الكامل للعضو على التعليقات بالاضافة الى @username." invites_per_page: "الدعوات الافتراضية تظهر في صفحة العضو" + embed_category: "فئة مواضيع مضمنة." + embed_post_limit: "أقصى عدد للمشاركات المضمنة." + delete_drafts_older_than_n_days: حذف المسودات مضى عليها أكثر من (ن) يوما. enable_emoji: "تمكين الرموز التعبيرية " emoji_set: "كيف تريد الرموز التعبيرية الخاصة بك؟" errors: invalid_email: "بريد الكتروني غير صالح " invalid_username: "لا يوجد مستخدم بهذا الاسم " + invalid_integer_min_max: "القيمة يجب أن تكون بين %{min} و %{max}." + invalid_integer_min: "القيمة يجب أن تكون %{min} أو أكثر." + invalid_integer_max: "القيمة لا يمكن أن تكون أكثر من %{max}." + invalid_integer: "القيمة يجب أن تكون عدد صحيح" + regex_mismatch: "القيمة لا تتناسب مع الشكل المطلوب" + must_include_latest: "ويجب أن تتضمن القائمة العلوية علامة التبويب 'أحدث'." + invalid_string: "قيمة غير صحيحة" + invalid_string_min_max: "يجب أن تكون الأحرف بين %{min} و %{max}." + invalid_string_min: "يجب أن تكون على الأقل %{min} أحرف" + invalid_string_max: "يجب ان لا تكون الاحرف اكثر من %{max} " + invalid_reply_by_email_address: "القيمة يجب أن تحتوي '%{reply_key}' وتختلف عن إشعار البريد الإلكتروني." + notification_types: + mentioned: "%{display_username} ذكرك في %{link}" + liked: "%{display_username} أعجب بمشاركتك في %{link}" + replied: "%{display_username} ردعلى مشاركتك في %{link}" + quoted: "%{display_username} أقتبس مشاركتك في %{link}" + edited: "%{display_username} عدل مشاركتك في %{link}" + posted: "%{display_username} شارك في %{link}" + moved_post: "%{display_username} نقل مشاركتك إلى %{link}" + private_message: "%{display_username} أرسلت لك رسالة: %{link}" + granted_badge: "كسبت %{link}" search: + within_post: "#%{post_number} بواسطة %{username}" types: category: 'فئات' topic: 'نتائج ' @@ -643,28 +839,47 @@ ar: most_posts: "معظم المشاركات" most_recent_poster: "معظم المشاركات الاخيرة " frequent_poster: "تكرار المشاركات " + change_owner: + deleted_user: "عضو محذوف" + emoji: + errors: + name_already_exists: "للأسف، الاسم '{name}%' مستخدم بواسطة رموز تعبيرية أخرى." + error_while_storing_emoji: "للأسف ، حدث خطأ عند تخزين الرموز التعبيرية." topic_statuses: + archived_enabled: "هذا الموضوع مؤرشف الأن. لن تستطيع أن تعدل عليه بأي طريقة." + archived_disabled: "هذا الموضوع غير مؤرشف الأن.لم يجمد، و تستطيع أن تعدل عليه." closed_enabled: "تم اغلاق هذا الموضوع لا يسمح باضافة ردود جديدة" closed_disabled: "تم فتح هذا الموضوع الان ويسمح باضافة ردود جديدة " autoclosed_disabled: "تم فتح هذا الموضوع الان ويسمح باضافة ردود جديدة " autoclosed_disabled_lastpost: "تم فتح هذا الموضوع الان ويسمح باضافة ردود جديدة " login: incorrect_username_email_or_password: "اسم المستخدم او كلمة المرور او البريد الالكتروني غير صحيح" + not_allowed_from_ip_address: "ﻻ يمكنك تسجيل الدخول كـ %{username} من هذا الـIP" + suspended: "ﻻ يمكنك تسجيل الدخول حتى %{date}." + errors: "%{errors}" + not_available: "غير متاح. جرّب %{suggestion} ؟" + omniauth_error: "نأسف, هناك خطأ في تصريح حسابك. ربما لم تعطي تصريح؟" new_registrations_disabled: "لا يُسمح بتسجيل حساب جديد بهذا الوقت " password_too_long: "الحد الاقصى لكلمة المرور 200 حرف" + reserved_username: "اسم المستخدم ذلك غير مسموح." missing_user_field: "لم تُكمل كافة الحقول المطلوبة " close_window: "تم التحقق . اغلق هذه النافذة للإستمرار" user: no_accounts_associated: "لا حسابات مرتبطة" username: + short: "يجب أن تكون على الأقل %{min} أحرف" long: "يجب ان تكون الاحرف اكثر من %{max} " characters: "يجب ان تحتوي الارقام والاحرف الصغيرة فقط " + unique: "يجب أن يكون فريدا" + blank: "يجب أن يكون موجود" must_begin_with_alphanumeric: "يجب ان يبدأ بحرف أو رقم " email: not_allowed: "بريد الكتروني غير مسموح . يرجى استخدام بريد الكتروني آخر " blocked: "غير مسموح" ip_address: blocked: "لا يُسمح بتسجيل جديد من عنوان ip الخاص بك " + invite_forum_mailer: + subject_template: "%{invitee_name} قام بدعوتك للإنضمام إلى %{site_domain_name}" invite_password_instructions: subject_template: "تعيين كلمة مرور %{site_name} حسابك" test_mailer: @@ -682,6 +897,14 @@ ar: deferred: "شكراً لإعلامنا سننظر في ذلك " deferred_and_deleted: "شكرا لاعلامنا تم ازالة هذا التعليق " system_messages: + post_hidden: + subject_template: "تم إخفاء المنشور بسبب حظر المجتمع" + welcome_user: + subject_template: "مرحبا بك في %{site_name}!" + welcome_invite: + subject_template: "مرحبا بك في %{site_name}!" + backup_succeeded: + subject_template: "اكتملت عملية النسخ الإحتياطي بنجاح" backup_failed: subject_template: "فشل النسخ الإحتياطي" restore_succeeded: @@ -693,39 +916,140 @@ ar: subject_template: "اكتمل تصدير البيانات" csv_export_failed: subject_template: "فشل تصدير البيانات" + text_body_template: "نحن آسفون، لكنه فشل تصدير البيانات الخاصة بك. يرجى التحقق من السجلات أو اتصل بأحد المشرفين." email_reject_trust_level: subject_template: "[%{site_name}] بريد الكتروني -- غير موثوق" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nحسابك لا يمتلك مستوى الثقة المطلوب للنشر مواضيع جديدة إلى عنوان البريد الإلكتروني. إذا كنت تعتقد أن هذا الخطأ، اتصل بأعضاء المشرفين.\n" email_reject_no_account: subject_template: "[%{site_name}] بريد الكتروني -- حساب غير معروف" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nليس هنالك حساب عضو يمتلك هذا البريد الالكتروني. حاول أن ترسل من بريد الكتروني مختلف، أو أتصل بـ أحد المشرفين.\n\n" email_reject_empty: subject_template: "[%{site_name}] بريد الكتروني -- بدون محتوى" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nلم نتمكن من العثور على أي محتوى في البريد الإلكتروني الخاص بك. تأكد من الرد الخاص بك هو في الجزء العلوي من البريد الإلكتروني -- نحن لا يمكننا معالجة الردود في سطر .\n\nإذا كنت الحصول على هذا وانت_ قمت _\ + \ بتضمين المحتوى، حاول مرة أخرى مع محتوى HTML المضمنه في بريدك الالكتروني ( ليس مجرد نص عادي).\n" email_reject_parsing: subject_template: "[%{site_name}] بريد الكتروني -- محتواه غير معروف" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nلم نتمكن من العثور على أي محتوى في البريد الإلكتروني الخاص بك. **تأكد من الرد الخاص بك هو في الجزء العلوي من البريد الإلكتروني -- نحن لا يمكننا معالجة الردود في سطر .\n" email_reject_invalid_access: subject_template: "[%{site_name}] بريد الكتروني -- غير صالح" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nحسابك لا يمتلك الصلاحيات لنشر مواضيع جديدة في تلك الفئة. إذا كنت تعتقد أن هذا الخطأ، اتصل بالاعضاء المشرفين.\n" + email_reject_post_error: + subject_template: "[%{site_name}] بريد الكتروني -- خطأ المشاركة" + text_body_template: | + نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. + + بعض الأسباب المحتملة هي: التنسيق المعقد، رسالة كبيرة جدا، رسالة صغيرة جدا. يرجى المحاولة مرة أخرى، أو الرد عبر الموقع الإلكتروني إذا استمر هذا الوضع. + email_reject_post_error_specified: + subject_template: "[%{site_name}] بريد الكتروني -- خطأ المشاركة" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nالسبب:\n\n%{post_error}\n\nاذا كنت تستطيع تصحيح المشكلة، يرجى المحاولة لاحقاً.\n" + email_reject_reply_key: + subject_template: "[%{site_name}] بريد الكتروني -- مفتاح رد غير معروف" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nمفتاح الرد المُقدم غير صالح أو غير معروف، لذلك نحن لا نعرف ما هو هذا البريد الإلكتروني للرد عليه. أتصل بالأعضاء المشرفين.\n" + email_reject_destination: + subject_template: "[%{site_name}] بريد الكتروني -- عنوان غير معروف" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nلم يتم التعرف على أي من العناوين. الرجاء التأكد من أن عنوان الموقع هو في To: line (not Cc: or Bcc:)، والتي انت تقوم بإرسالها إلى عنوان البريد الإلكتروني الصحيح المقدمة من الاعضاء المشرفين.\n" + email_reject_topic_not_found: + subject_template: "[%{site_name}] بريد الكتروني -- موضوع غير موجود" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nالموضوع الذي تريد الرد عليه لم يعد موجودا، وربما تم حذفه؟ إذا كنت تعتقد أن هذا خطأ، اتصل أحد الاعضاء المشرفين. \n" + email_reject_topic_closed: + subject_template: "[%{site_name}] بريد الكتروني -- موضوع مغلق" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\nالموضوع الذي تريد الرد عليه مغلق حاليا والتي لم تعد تقبل ردود. إذا كنت تعتقد أن هذا خطأ، اتصل أحد الأعضاء المشرفين.\n" + email_reject_auto_generated: + subject_template: "[%{site_name}] بريد الكتروني -- توليد رد آلي" + text_body_template: "نحن آسفون ، ولكن رسالة البريد الإلكتروني إلى %{destination} (titled %{former_title}) لا تعمل. \n\n بريدك الإلكتروني عُلّم كـ\"مولد تلقائي\"، وهو ما لا يمكن قبوله. إذا كنت تعتقد أن هذا خطأ، اتصل أحد الاعضاء المشرفين.\n" + email_error_notification: + subject_template: "[%{site_name}] بريد الكتروني -- خطأ مصادقة POP" too_many_spam_flags: subject_template: "حساب جديد مقفول" blocked_by_staff: subject_template: "الحساب مُعطل" + user_automatically_blocked: + subject_template: "العضو الجديد %{username} تم حجبه بسبب تبليغات عليه في المجتمع" + spam_post_blocked: + subject_template: "العضو الجديد %{username} تم حجب مشاركاته بسبب تكرار الروابط " unblocked: subject_template: "الحساب مُفعل " download_remote_images_disabled: subject_template: "الغاء تفعيل تحميل الصور عن بعد " subject_re: "اعادة " + subject_pm: "[مسائًا]" user_notifications: previous_discussion: "الردود السابقة " unsubscribe: title: "غير مشترك " + user_replied: + subject_template: "[%{site_name}] %{topic_title}" + text_body_template: | + %{message} + + %{context} + + --- + %{respond_instructions} + user_quoted: + subject_template: "[%{site_name}] %{topic_title}" + text_body_template: | + %{message} + + %{context} + + --- + %{respond_instructions} + user_mentioned: + subject_template: "[%{site_name}] %{topic_title}" + text_body_template: | + %{message} + + %{context} + + --- + %{respond_instructions} + user_posted: + subject_template: "[%{site_name}] %{topic_title}" + text_body_template: | + %{message} + + %{context} + + --- + %{respond_instructions} + user_posted_pm: + subject_template: "[%{site_name}] [مساءً] %{topic_title}" + text_body_template: | + %{message} + + %{context} + + --- + %{respond_instructions} digest: + subject_template: "[%{site_name}] الخلاصة" new_activity: "يوجد نشاط جديد في المواضيع والمشاركات الخاصة بك:" top_topics: "اشهر المشاركات " other_new_topics: "اشهر المواضيع " click_here: "أنقر هنا" + from: "%{site_name} الخلاصة" read_more: "قراءة المزيد" + more_topics: "هناك %{new_topics_since_seen} مواضيع جديدة أخرى." more_topics_category: "أكثر المواضيع الجديدة:" + forgot_password: + subject_template: "[%{site_name}] إعادة ضبط كلمة المرور" + set_password: + subject_template: "[%{site_name}] ضع كلمة المرور" + admin_login: + subject_template: "[%{site_name}] تسجيل الدخول" + account_created: + subject_template: "[%{site_name}] حسابك الجديد" + authorize_email: + subject_template: "[%{site_name}] تأكيد البريد الإلكتروني الجديد " + signup_after_approval: + subject_template: "قد وافقت على %{site_name}!" + signup: + subject_template: "[%{site_name}] تأكيد حسابك الجديد " page_not_found: title: "الصفحة المطلوبة غير موجودة او ليس لديك صلاحيات لرؤيتها " popular_topics: "شعبي " + recent_topics: "الأخيرة" see_more: "المزيد" search_title: "البحث في الموقع" search_google: "جوجل" @@ -734,12 +1058,21 @@ ar: deleted: 'حذف' upload: edit_reason: "تحميل نسخ محلية للصور" + unauthorized: "المعذرة، الملف الذي تحاول رفعه غير مسموح به (الامتدادات المسموح بها هي : %{authorized_extensions})." pasted_image_filename: "لصق الصورة " + attachments: + too_large: "نعتذر، الملف الذي تريد رفعه كبير جداً ( الحد الاقصى {max_size_kb} كيلوبايت )" + images: + too_large: "نعتذر، الصورة الذي تريد رفعها كبيرة جداً ( الحد الاقصى هو %{max_size_kb} كيلوبايت )،يرجى اعادة تغيير حجمها ثم حاول مرة اخرى." + size_not_found: "نعتذر، لكننا لا يمكن تحديد حجم الصورة. ربما صورتك تالفة؟" email_log: + no_user: "لا يمكنك إيجاد عضو بواسطة id %{user_id}" anonymous_user: "المستخدم مجهول" + suspended_not_pm: "عضو موقوف، ليست رسالة" seen_recently: "تم رؤية هذا المستخدم مسبقاً" notification_already_read: "تم قراءة هذه الاشعارات " post_deleted: "تم حذف الموضوع من قبل كاتبه " + user_suspended: "تم تعليق حساب المستخدم" already_read: "المستخدم قرأ هذه المشاركة " message_blank: "رسالة فارغة" message_to_blank: "رسالة فارغة " @@ -755,6 +1088,8 @@ ar: boolean_no: "لا" static_topic_first_reply: | تعديل محتويات المنشور الاول في هذا الموضوع %{page_name} page. + guidelines_topic: + title: "الأسئلة الشائعة/توجيهات" tos_topic: title: "شروط الخدمة" privacy_topic: @@ -773,6 +1108,11 @@ ar: هذه الشارة تمنح لوجود رد حصل على 50 إعجاب. ياللعجب! great_topic: | هذه الشارة تمنح لوجود رد حصل على 50 إعجاب. ياللعجب! + regular: |+ + يتم منح هذه الشارة عندما تصل لمستوى الثقة 3. شكرا لكونك جزءا منضبطاً من مجتمعنا على مدى أشهر، أحد القراء الأكثر نشاطا ومساهمة معتمدا، لما يجعل هذا المجتمع عظيم. يمكنك الآن إعادة تصنيف وإعادة تسمية الموضوعات، والوصول إلى الاستراحة الخاصة وأقدر على الإعلام بالبريد المزعج، وغيرها كثير من الإعجابات لكل يوم. + + leader: | + هذه الشارة تمنح لك عندما تصل للمرحلة 4. أنت قائد في هذا المجتمع مرشح من الطاقم كمثال جيد في المجتمع في أقوالك وأفعالك. لديك القدرة على تعديل كل المشاركات، إدارة المواضيع بالتثبيت والإغلاق والتقسيم والدمج والأرشفة والعديد من الإعجابات كل يوم. admin_login: success: "البريد أُرسل" error: "خطأ!" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index ebf41d24e6..a64f040504 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -176,6 +176,14 @@ fr: - Les critiques constructives sont les bienvenues, mais critiquez les *idées*, pas les gens. Pour plus d'informations, [consulter le règlement de la communauté](/guidelines). Cet encart apparaîtra pour votre premier %{education_posts_text}. + avatar: | + ### Avez-vous pensé à une photo de profil ? + + Vous avez posté quelques sujets et réponses, mais votre photo de profil n'est pas unique comme vous -- c'est juste une lettre. + + Avez-vous pensé à **[visiter votre profil utilisateur](%{profile_path})** et envoyer une photo qui vous représente ? + + C'est plus facile de suivre les discussions et de rencontrer des personnes intéressantes dans les conversations lorsque tout le monde a une photo de profil unique ! sequential_replies: | ### Envisagez de répondre à plusieurs messages en même temps @@ -614,6 +622,7 @@ fr: host_names_warning: "Votre fichier config/database.yml utilise le nom d'hôte par défaut. Veuillez renseigner votre nom d'hôte." gc_warning: 'Votre serveur utilise les paramètres par défaut de collection du GC de ruby, ce qui ne vous donnera pas les meilleures performances. Merci de lire ce sujet sur l''optimisation des performances de Ruby et Rails pour Discourse.' sidekiq_warning: 'Sidekiq n''est pas lancé. De nombreuses tâches, comme l''envoi des courriels, sont exécutées de manière asynchrone par sidekiq. Assurez-vous d''avoir au moins un processus sidekiq de lancé. En savoir plus sur sidekiq.' + queue_size_warning: 'Le nombre de jobs dans la file d''attente est de %{queue_size}, ce qui est assez élevé. Cela peut indiquer un problème avec le(s) process Sidekiq, ou la nécessité d''ajouter davantage de workers.' memory_warning: 'Votre serveur dispose de moins de 1 Go de mémoire vive. Au moins 1 Go de RAM est recommandé.' google_oauth2_config_warning: 'Le serveur est configuré pour permettre l''authentification via Google Oauth2 (enable_google_oauth2_logins), mais les paramètres client id et client secret ne sont pas renseignés. Allez dans les Paramètres du Site et mettez les à jour. Voir le guide pour en savoir plus.' facebook_config_warning: 'Le serveur est configuré pour permettre l''authentification par Facebook (enable_facebook_logins), mais les paramètres facebook_app_id et facebook_app_secret ne sont pas renseignés. Allez dans les Paramètres et mettez les à jour. Voir le guide pour en savoir plus.' @@ -708,6 +717,7 @@ fr: digest_logo_url: "Le logo alternatif sera utilisé en haut des notifications par e-mail. Il doit être de forme rectangulaire. Si vous laissez ce champ vide, `logo_url` sera utilisé." logo_small_url: "Le petit logo situé en haut à gauche de votre site doit être de forme carré. Si vous laissez ce champ vide, un logo de maison apparaîtra." favicon_url: "Le favicon de votre site, voir http://fr.wikipedia.org/wiki/Favicon" + mobile_logo_url: "L'image fixe utilisée tout en haut à gauche de votre site mobile. Devrait-être de forme carrée. Si laissé vide, `logo_url` sera utilisé. ex: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Icône utilisée pour les appareils d'Apple. Taille recommandée 144 px par 144 px." notification_email: "L'adresse de courriel dans le champs De qui sera utilisée pour envoyer les courriels systèmes essentiels. Le nom de domaine spécifié doit avoir les informations SPF, DKIM et PTR inversé renseignés correctement pour que le courriel arrive à destination." email_custom_headers: "Une liste délimité par des (|) pipes d'entêtes de courriel" @@ -813,6 +823,7 @@ fr: github_client_secret: "Secret client pour l'authentification Github, enregistré sur https://github.com/settings/applications" allow_restore: "Autoriser la restauration, qui peut remplacer TOUTES les données du site ! Laissez à faux, sauf si vous envisagez de faire restaurer une sauvegarde" maximum_backups: "Nombre maximum de sauvegardes à conserver sur le disque. Les anciennes sauvegardes seront automatiquement supprimées" + backup_frequency: "Fréquence de création des sauvegardes du site, en jours." enable_s3_backups: "Envoyer vos sauvegardes à S3 lorsqu'elles sont terminées. IMPORTANT: Vous devez avoir renseigné vos identifiants S3 dans les paramètres de fichiers." s3_backup_bucket: "Bucket distant qui contiendra les sauvegardes. ATTENTION: Vérifiez que c'est un bucket privé" active_user_rate_limit_secs: "A quelle fréquence mettre à jour le champ 'last_seen_at' (Dernière vu à), en secondes." @@ -922,6 +933,8 @@ fr: auto_respond_to_flag_actions: "Activer réponse automatique lors du traitement d'un signalement." min_first_post_typing_time: "Minimum de temps en millisecondes qu'un utilisateur doit passer à la saisie de son premier commentaire, is le seuil n'est pas atteint, il rejoindra automatiquement la file des commentaires en cours d'approbation. Définir à 0 pour désactiver (non recommandé)" auto_block_fast_typers_on_first_post: "Bloque automatiquement les utilisateurs qui n'ont pas rencontré min_first_post_typing_time" + auto_block_fast_typers_max_trust_level: "Niveau de confiance maximum pour bloquer automatiquement les \"fast typers\"" + auto_block_first_post_regex: "Regex non sensible à la casse qui, si elle est déclenchée, bloquera le premier message de l'utilisateur et l'enverra dans la file d'attente d'approbation.\nExemple: rageux|a[bc]a bloquera les premiers messages contenant rageux ou aba ou aca." reply_by_email_enabled: "Activer les réponses aux sujets via courriel." reply_by_email_address: "Modèle pour la réponse par courriel entrant; exemple : %{reply_key}@reply.example.com ou replies+%{reply_key}@example.com" disable_emails: "Désactiver l'envoi de les courriels depuis Discourse." @@ -957,6 +970,7 @@ fr: suppress_digest_email_after_days: "Ne pas envoyer de résumés courriel aux utilisateurs qui n'ont pas visité le site depuis (n) jours." disable_digest_emails: "Désactiver les résumés par courriels pour tous les utilisateurs." default_external_links_in_new_tab: "Les liens externes s'ouvrent dans un nouvel onglet. Les utilisateurs peuvent modifier ceci dans leurs préférences." + detect_custom_avatars: "Vérifier ou non si les utilisateurs ont envoyé une photo de profil personnalisée." max_daily_gravatar_crawls: "Nombre maximum de fois que Discourse vérifiera Gravatar pour des avatars personnalisés en une journée." public_user_custom_fields: "Une liste blanche des champs personnalisés pour un utilisateur qui peuvent être affichés publiquement." staff_user_custom_fields: "Une liste blanche des champs personnalisés pour un utilisateur qui peuvent être vus par l'équipe." @@ -1165,6 +1179,13 @@ fr: Cette invitation provient d'un utilisateur de confiance, vous n'avez pas besoin de vous connecter. invite_password_instructions: subject_template: "Renseignez le mot de passe pour votre compte utilisateur %{site_name} " + text_body_template: | + Merci d'avoir accepté l'invitation sur %{site_name} -- Bienvenue ! + + Cliquer sur ce lien pour choisir un mot de passe maintenant : + %{base_url}/users/password-reset/%{email_token} + + (Si le lien est expiré, choisissez "J'ai oublié mon mot de passe" lorsque vous essayez de vous connecter avec votre adresse mail.) test_mailer: subject_template: "[%{site_name}] Test de délivrabilité d'un courriel" text_body_template: | diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 7dbc817b8d..457843d5d9 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -367,6 +367,7 @@ he: one: "לפני כמעט שנה" other: "לפני כמעט %{count} שנים" password_reset: + no_token: "מצטערים, הקישור לשינוי הסיסמא ישן מדי. לחץ על כפתור הכניסה ובחר ב\"שכחתי את הסיסמא שלי\" כדי לקבל קישור חדש." choose_new: "אנא בחר סיסמה חדשה" choose: "אנא בחר סיסמה" update: 'עדכן סיסמה' @@ -696,7 +697,11 @@ he: post_excerpt_maxlength: "אורך מקסימלי של פרסום קטע / סיכום." post_onebox_maxlength: "מספר תוים מקסימאלי מותר כאורך פרסום Discourse אחד בקופסא (oneboxed Discourse post)." onebox_domains_whitelist: "רשימת מתחמים (דומיינים) מותרים לאריזה (oneboxing); על דומיינים אלה לתמוך ב-OpenGraph או ב-oEmbed. בדקו אותם ב-http://iframely.com/debug." + logo_url: "The logo image at the top left of your site, should be a wide rectangle shape. If left blank site title text will be shown." + digest_logo_url: "The alternate logo image used at the top of your site's email digest. Should be a wide rectangle shape. If left blank `logo_url` will be used. " + logo_small_url: "The small logo image at the top left of your site, should be a square shape, seen when scrolling down. If left blank a home glyph will be shown." favicon_url: "A favicon for your site, see http://en.wikipedia.org/wiki/Favicon" + mobile_logo_url: "The fixed position logo image used at the top left of your mobile site. Should be a square shape. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Icon used for Apple touch devices. Recommended size is 144px by 144px." notification_email: "הטופס: נעשה שימוש בכתובת הדוא\"ל כאשר שולחים את כל הודעות המערכת הנדרשות. כדי שהודעות הדוא\"ל יגיעו, המתחם (domain) המצויין כאן חייב לכלול SPF, DKIM ורשומות reverse PTR מוגדרים כהלכה." email_custom_headers: "A pipe-delimited list of custom email headers" @@ -802,6 +807,7 @@ he: github_client_secret: "Client secret for Github authentication, registered at https://github.com/settings/applications" allow_restore: "אפשר שחזור, אשר יכול להחליף את כל(!) המידע באתר! הותירו על \"שלילי\"/false אלא אם כן אתם מתכננים לשחזר גיבוי." maximum_backups: "The maximum amount of backups to keep on disk. Older backups are automatically deleted" + backup_frequency: "How frequently we create a site backup, in days." enable_s3_backups: "העלאת גיבויים ל-S3 לאחר השלמתם. חשוב: דורש הזנת הרשאות S3 תקפות להגדרות הקבצים." s3_backup_bucket: "The remote bucket to hold backups. WARNING: Make sure it is a private bucket." active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds" @@ -909,6 +915,10 @@ he: num_flaggers_to_close_topic: "מספר מינימלי של מסמנים שונים שנדרש כדי להשהות באופן אוטומטי אפשות להתערב בנושא" num_flags_to_close_topic: "מספר מינימלי של סימונים פעילים שנדרש כדי להשהות באופן אוטומטי את היכולת להתערב בנושא" auto_respond_to_flag_actions: "אפשרו תגובה אוטמטית עם הסרת סימון." + min_first_post_typing_time: "Minimum amount of time in milliseconds a user must type during first post, if threshold is not met post will automatically enter the needs approval queue. Set to 0 to disable (not recommended)" + auto_block_fast_typers_on_first_post: "Automatically block users that do not meet min_first_post_typing_time" + auto_block_fast_typers_max_trust_level: "Maximum trust level to auto block fast typers" + auto_block_first_post_regex: "Case insensitive regex that if passed will cause first post by user to be blocked and sent to approval queue. Example: raging|a[bc]a , will cause all posts containing raging or aba or aca to be blocked on first. Only applies to first post." reply_by_email_enabled: "אפשרו תגובה לנושאים באמצעות הדוא\"ל." reply_by_email_address: "Template for reply by email incoming email address, for example: %{reply_key}@reply.example.com or replies+%{reply_key}@example.com" disable_emails: "מנעו מ-Discourse ממשלו דוא\"ל כלשהו." @@ -921,6 +931,7 @@ he: pop3_polling_host: "השרת המארח (Host) למשיכת דוא\"ל דרך POP3." pop3_polling_username: "שם המשתמש/ת לחשבון ה-POP3 למשיכת דוא\"ל." pop3_polling_password: "הסיסא לחשבון ה-POP3 למשיכת הדוא\"ל." + log_mail_processing_failures: "Log all email processing failures to http://yoursitename.com/logs" email_in: "אפשרו למשתמשים לפרסם נושאים חדשים באמצעות דוא\"ל (דורש משיכה באמצעוצ pop3). הגדירו את הכתובת בלשונית \"הגדרות\" עבור כל קטגוריה." email_in_min_trust: "רמת האמון המינימלית הנדרשת למשתמש/ת כדי שיוכלו להעלות נושאים חדשים באמצעות הדוא\"ל." email_prefix: "ה[תווית] שתשמש בנושא הודעות הדוא\"ל. אם לא יוגדר, ברירת המחדל תכוון ל'כותרת' אם לא יוגדר אחרת." @@ -932,6 +943,8 @@ he: username_change_period: "The number of days after registration that accounts can change their username (0 to disallow username change)." email_editable: "Allow users to change their e-mail address after registration." logout_redirect: "מיקום להכוונת הדפדפן לאחר ההתנתקות לדוגמא: (http://somesite.com/logout)" + allow_uploaded_avatars: "Allow users to upload custom profile pictures." + allow_animated_avatars: "Allow users to use animated gif profile pictures. WARNING: run the avatars:refresh rake task after changing this setting." allow_animated_thumbnails: "יצירת תמונות אנימציה מוקטנות קטנות של קבצי אנימציית gif." default_avatars: "כתובות URL לאווטרים אשר ישמשו כברירת מחדל למשתמשים חדשים עד אשר ישנו אותם." automatically_download_gravatars: "הורדת גראווטרים למשתמשים בעת יצירת החשבון או שינוי כתובת הדוא\"ל." @@ -941,6 +954,7 @@ he: suppress_digest_email_after_days: "השהיית מיילים מסכמים עבור משתמשים שלא נראו באתר במשך יותר מ(n) ימים." disable_digest_emails: "נטרול דוא\"ל סיכום לכל המשתמשים." default_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." + detect_custom_avatars: "Whether or not to check that users have uploaded custom profile pictures." max_daily_gravatar_crawls: "מספר הפעמים המקסימלי ש-Discourse יבדוק אווטרים ב-Gravatar ביום" public_user_custom_fields: "רשימה לבנה (whitelist) של שדות מותאמים למשתמש שיכולים להיות מוצגים באופן פומבי." staff_user_custom_fields: "רשימה לבנה (whitelist) של שדות מותאמים למשתמש שיכולים להיות מוצגים לאנשי צוות." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 49fac5c4e5..d099202ba5 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -593,6 +593,7 @@ zh_CN: host_names_warning: "你的 config/database.yml 文件使用的是缺省的 localhost 主机名。请更改成你的站点主机名。" gc_warning: '你的服务器使用的是缺省的 ruby 垃圾回收参数,这个配置无法提供最高性能。请阅读此性能调优文档:Tuning Ruby and Rails for Discourse。' sidekiq_warning: 'Sidekiq 不在运行。很多任务,例如发送电子邮件,是异步的被 sidekiq 调度执行的。请确保至少运行一个 sidekiq 进程。了解 Sidekiq。' + queue_size_warning: '队列中有较多任务,为 %{queue_size} 个。这可能是因为 Sidekiq 进程的问题导致,或者需要更多的 Sidekiq 进程。' memory_warning: '你的服务器环境内存少于 1GB,我们建议至少要有 1GB 内存。' google_oauth2_config_warning: '服务器允许使用 Google Oauth2 登录(enable_google_oauth2_logins),但是 client id 和 client secret 没有被设定。 到站点设置更新此设定。 参考设定指南。' facebook_config_warning: '服务器允许使用 Facebook 账号登录(enable_facebook_logins),但是 app id 和 app secret 没有被设定。 到站点设置更新此设定。参考设定指南。' @@ -793,6 +794,7 @@ zh_CN: github_client_secret: "Github 帐号验证的客户端密码(Client secret),到 https://github.com/settings/applications 来注册获取" allow_restore: "允许导入数据,这将能替换所有全站数据!除非你计划导入数据,否则请保持设置为 false" maximum_backups: "磁盘保存的最大备份数量。老的备份将自动删除" + backup_frequency: "自动创建站点备份的频率,以天为单位。" enable_s3_backups: "当完成备份后上传备份到 S3。重要:需要在文件设置中填写有效的 S3 验证资料。" s3_backup_bucket: "远端备份 bucket。警告:确认它使私有的 bucket。" active_user_rate_limit_secs: "更新'最后一次见到'数据的间隔,单位为秒" From ffe06fbcb55692d11be4b8708a1fd5fbea886c61 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 18 Aug 2015 16:46:01 +1000 Subject: [PATCH 090/237] whitelist 404 pull hotlinked image --- config/initializers/logster.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb index 55e7801db6..82c183e9f0 100644 --- a/config/initializers/logster.rb +++ b/config/initializers/logster.rb @@ -28,7 +28,10 @@ if Rails.env.production? /^ActiveRecord::RecordNotFound /, # bad asset requested, no need to log - /^ActionController::BadRequest / + /^ActionController::BadRequest /, + + # hotlinked image error that is pointless + /^Failed to pull hotlinked image.*404 Not Found/m ] end From 45adeacd45b7381212dd7e2874db3240c4b7d0ea Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 18 Aug 2015 17:05:55 +1000 Subject: [PATCH 091/237] ignore empty script errors, line 0 gives us nothing. --- config/initializers/logster.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb index 82c183e9f0..23c01ab7ec 100644 --- a/config/initializers/logster.rb +++ b/config/initializers/logster.rb @@ -20,6 +20,9 @@ if Rails.env.production? # /(?m).*?Line: (?:\D|0).*?Column: (?:\D|0)/, + # also empty JS errors + /^Script error\..*Line: 0/m, + # CSRF errors are not providing enough data # suppress unconditionally for now /^Can't verify CSRF token authenticity$/, From add6e12ce4d10dd488d8fab1c64124488a5cd73b Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 18 Aug 2015 17:34:46 +1000 Subject: [PATCH 092/237] FIX: topic links with long titles can not be crawled 0..255 == 256 numbers column fits 255 --- app/jobs/regular/crawl_topic_link.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/jobs/regular/crawl_topic_link.rb b/app/jobs/regular/crawl_topic_link.rb index 7ad789cfed..e77fedfab6 100644 --- a/app/jobs/regular/crawl_topic_link.rb +++ b/app/jobs/regular/crawl_topic_link.rb @@ -119,7 +119,7 @@ module Jobs title.gsub!(/ +/, ' ') title.strip! if title.present? - crawled = (TopicLink.where(id: topic_link.id).update_all(['title = ?, crawled_at = CURRENT_TIMESTAMP', title[0..255]]) == 1) + crawled = (TopicLink.where(id: topic_link.id).update_all(['title = ?, crawled_at = CURRENT_TIMESTAMP', title[0..254]]) == 1) end end end From f1398f0650ac307a37f4c9c45f34a9fd2f0cdc79 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 18 Aug 2015 17:41:39 +1000 Subject: [PATCH 093/237] another hotlinked image whitelist --- config/initializers/logster.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb index 23c01ab7ec..25d696898a 100644 --- a/config/initializers/logster.rb +++ b/config/initializers/logster.rb @@ -34,7 +34,7 @@ if Rails.env.production? /^ActionController::BadRequest /, # hotlinked image error that is pointless - /^Failed to pull hotlinked image.*404 Not Found/m + /^Failed to pull hotlinked image.*(404 Not Found|gestaddrinfo: Name or service not known)/m ] end From b703af3d37625709cf43e223475107ef2989f3ef Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 18 Aug 2015 17:48:54 +1000 Subject: [PATCH 094/237] Skip 403 forbidden as well --- config/initializers/logster.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb index 25d696898a..e3e748faa6 100644 --- a/config/initializers/logster.rb +++ b/config/initializers/logster.rb @@ -34,7 +34,7 @@ if Rails.env.production? /^ActionController::BadRequest /, # hotlinked image error that is pointless - /^Failed to pull hotlinked image.*(404 Not Found|gestaddrinfo: Name or service not known)/m + /^Failed to pull hotlinked image.*(404 Not Found|403 Forbidden|gestaddrinfo: Name or service not known)/m ] end From 1c2f6b97c355c83e7f51870be900d852feb956c2 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 18 Aug 2015 15:15:42 +0800 Subject: [PATCH 095/237] Use ajax-error in controller:topic. --- .../discourse/controllers/topic.js.es6 | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 71b0eabfb9..e27e8a5e9a 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -4,6 +4,7 @@ import { spinnerHTML } from 'discourse/helpers/loading-spinner'; import Topic from 'discourse/models/topic'; import Quote from 'discourse/lib/quote'; import { setting } from 'discourse/lib/computed'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { @@ -201,14 +202,9 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { } ]); } else { - post.destroy(user).then(null, function(e) { + post.destroy(user).catch(function(error) { + popupAjaxError(error); post.undoDeleteState(); - const response = $.parseJSON(e.responseText); - if (response && response.errors) { - bootbox.alert(response.errors[0]); - } else { - bootbox.alert(I18n.t('generic_error')); - } }); } }, @@ -232,13 +228,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { return; } if (post) { - return post.toggleBookmark().catch(function(error) { - if (error && error.jqXHR && error.jqXHR.responseText) { - bootbox.alert($.parseJSON(error.jqXHR.responseText).errors[0]); - } else { - bootbox.alert(I18n.t('generic_error')); - } - }); + return post.toggleBookmark().catch(popupAjaxError); } else { return this.get("model").toggleBookmark(); } @@ -295,13 +285,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { // the properties to the topic. self.rollbackBuffer(); self.set('editingTopic', false); - }).catch(function(error) { - if (error && error.jqXHR && error.jqXHR.responseText) { - bootbox.alert($.parseJSON(error.jqXHR.responseText).errors[0]); - } else { - bootbox.alert(I18n.t('generic_error')); - } - }); + }).catch(popupAjaxError); }, toggledSelectedPost(post) { From ecd1bfe4cbf1c1423f8bc39517040431757243a8 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 18 Aug 2015 14:47:39 +0530 Subject: [PATCH 096/237] FIX: onebox youtube channels and handle deleted video links --- plugins/lazyYT/plugin.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/lazyYT/plugin.rb b/plugins/lazyYT/plugin.rb index 866765e7e6..3ba1206531 100644 --- a/plugins/lazyYT/plugin.rb +++ b/plugins/lazyYT/plugin.rb @@ -14,16 +14,17 @@ register_asset "stylesheets/lazyYT_mobile.scss", :mobile # freedom patch YouTube Onebox class Onebox::Engine::YoutubeOnebox include Onebox::Engine + alias_method :yt_onebox_to_html, :to_html def to_html if video_id video_width = (params['width'] && params['width'].to_i <= 695) ? params['width'] : 480 # embed width video_height = (params['height'] && params['height'].to_i <= 500) ? params['height'] : 270 # embed height - + # Put in the LazyYT div instead of the iframe "
    " else - super + yt_onebox_to_html end end From 4c2df814de4671880d532ae6514c470d831b7009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 18 Aug 2015 11:39:51 +0200 Subject: [PATCH 097/237] FIX: ensure a file is present when creating an upload --- app/controllers/uploads_controller.rb | 15 ++++++++++----- config/locales/server.en.yml | 1 + spec/controllers/uploads_controller_spec.rb | 11 +++++++++++ 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb index 3c3eef684d..07017714b4 100644 --- a/app/controllers/uploads_controller.rb +++ b/app/controllers/uploads_controller.rb @@ -50,18 +50,23 @@ class UploadsController < ApplicationController def create_upload(type, file, url) begin - # API can provide a URL - if file.nil? && url.present? && is_api? - tempfile = FileHelper.download(url, 10.megabytes, "discourse-upload-#{type}") rescue nil - filename = File.basename(URI.parse(url).path) + # ensure we have a file + if file.nil? + # API can provide a URL + if url.present? && is_api? + tempfile = FileHelper.download(url, 10.megabytes, "discourse-upload-#{type}") rescue nil + filename = File.basename(URI.parse(url).path) + end else tempfile = file.tempfile filename = file.original_filename content_type = file.content_type end + return { errors: I18n.t("upload.file_missing") } if tempfile.nil? + # allow users to upload large images that will be automatically reduced to allowed size - if tempfile && File.size(tempfile.path) > 0 && SiteSetting.max_image_size_kb > 0 && FileHelper.is_image?(filename) + if SiteSetting.max_image_size_kb > 0 && FileHelper.is_image?(filename) && File.size(tempfile.path) > 0 attempt = 5 while attempt > 0 && File.size(tempfile.path) > SiteSetting.max_image_size_kb.kilobytes OptimizedImage.downsize(tempfile.path, tempfile.path, "80%", allow_animation: SiteSetting.allow_animated_thumbnails) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 7867e33991..0589365879 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -2063,6 +2063,7 @@ en: unauthorized: "Sorry, the file you are trying to upload is not authorized (authorized extensions: %{authorized_extensions})." pasted_image_filename: "Pasted image" store_failure: "Failed to store upload #%{upload_id} for user #%{user_id}." + file_missing: "Sorry, you must provide a file to upload." attachments: too_large: "Sorry, the file you are trying to upload is too big (maximum size is %{max_size_kb}KB)." images: diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb index a738489d9d..b85c3cdc6f 100644 --- a/spec/controllers/uploads_controller_spec.rb +++ b/spec/controllers/uploads_controller_spec.rb @@ -82,6 +82,17 @@ describe UploadsController do expect(Upload.find(id).retain_hours).to eq(100) end + it 'requires a file' do + Jobs.expects(:enqueue).never + + message = MessageBus.track_publish do + xhr :post, :create, type: "composer" + end.first + + expect(response.status).to eq 200 + expect(message.data["errors"]).to eq(I18n.t("upload.file_missing")) + end + it 'properly returns errors' do SiteSetting.stubs(:max_attachment_size_kb).returns(1) From 1b44924cb0ae40509a9e224277860d89078f8bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 18 Aug 2015 14:56:36 +0200 Subject: [PATCH 098/237] replace 'open_uri_redirections' gem with a single freedom_patches file --- Gemfile | 2 - Gemfile.lock | 2 - lib/freedom_patches/open_uri_redirections.rb | 98 ++++++++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 lib/freedom_patches/open_uri_redirections.rb diff --git a/Gemfile b/Gemfile index c6f88f8b81..98cdfdd790 100644 --- a/Gemfile +++ b/Gemfile @@ -53,8 +53,6 @@ gem 'fast_xs' gem 'fast_xor' -gem 'open_uri_redirections' - # while we sort out https://github.com/sdsykes/fastimage/pull/46 gem 'fastimage_discourse', require: 'fastimage' gem 'aws-sdk', require: false diff --git a/Gemfile.lock b/Gemfile.lock index fd507523c8..5ce6b8971a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -214,7 +214,6 @@ GEM multi_json (~> 1.11) mustache nokogiri (~> 1.6.6) - open_uri_redirections (0.2.1) openid-redis-store (0.0.2) redis ruby-openid @@ -444,7 +443,6 @@ DEPENDENCIES omniauth-openid omniauth-twitter onebox - open_uri_redirections openid-redis-store pg pry-nav diff --git a/lib/freedom_patches/open_uri_redirections.rb b/lib/freedom_patches/open_uri_redirections.rb new file mode 100644 index 0000000000..78bb47b1f3 --- /dev/null +++ b/lib/freedom_patches/open_uri_redirections.rb @@ -0,0 +1,98 @@ +##### +# Patch to allow open-uri to follow safe (http to https) +# and unsafe redirections (https to http). +# +# Original gist URL: +# https://gist.github.com/1271420 +# +# Relevant issue: +# http://redmine.ruby-lang.org/issues/3719 +# +# Source here: +# https://github.com/ruby/ruby/blob/trunk/lib/open-uri.rb +# +# Thread-safe implementation adapted from: +# https://github.com/obfusk/open_uri_w_redirect_to_https +# +# Copy and pasted from: +# https://github.com/open-uri-redirections/open_uri_redirections +# + +require "open-uri" + +module OpenURI + class < HTTPS redirections. + # * :all will allow HTTP => HTTPS and HTTPS => HTTP redirections. + # + # OpenURI::open can receive different kinds of arguments, like a string for + # the mode or an integer for the permissions, and then a hash with options + # like UserAgent, etc. + # + # To find the :allow_redirections option, we look for this options hash. + # + def self.open_uri(name, *rest, &block) + Thread.current[:__open_uri_redirections__] = allow_redirections(rest) + + block2 = lambda do |io| + Thread.current[:__open_uri_redirections__] = nil + block[io] + end + + begin + open_uri_original name, *rest, &(block ? block2 : nil) + ensure + Thread.current[:__open_uri_redirections__] = nil + end + end + + private + + def self.allow_redirections(args) + options = first_hash_argument(args) + options.delete :allow_redirections if options + end + + def self.first_hash_argument(arguments) + arguments.select { |arg| arg.is_a? Hash }.first + end + + def self.http_to_https?(uri1, uri2) + schemes_from([uri1, uri2]) == %w(http https) + end + + def self.https_to_http?(uri1, uri2) + schemes_from([uri1, uri2]) == %w(https http) + end + + def self.schemes_from(uris) + uris.map { |u| u.scheme.downcase } + end +end From 2482cb8f9f0ce527ffd4634e248dea7f67d44ecf Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 18 Aug 2015 11:10:50 -0400 Subject: [PATCH 099/237] FIX: Backwards compatibility for plugin initializers --- app/assets/javascripts/discourse.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 7a26b76e7e..02b2bad076 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -125,7 +125,7 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { var init = module.default; var oldInitialize = init.initialize; init.initialize = function(app) { - oldInitialize.call(this, app.container, app); + oldInitialize.call(this, app.container, Discourse); }; Discourse.instanceInitializer(init); From 707c493e3cde9916e246c52f3ca129d1a0669526 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 18 Aug 2015 14:13:40 -0400 Subject: [PATCH 100/237] FIX: When changing colors, refresh the admin stylesheet --- app/assets/stylesheets/admin.css | 3 --- app/assets/stylesheets/admin.scss | 1 + app/assets/stylesheets/common/admin/admin_base.scss | 2 +- app/views/common/_discourse_stylesheet.html.erb | 2 +- 4 files changed, 3 insertions(+), 5 deletions(-) delete mode 100644 app/assets/stylesheets/admin.css create mode 100644 app/assets/stylesheets/admin.scss diff --git a/app/assets/stylesheets/admin.css b/app/assets/stylesheets/admin.css deleted file mode 100644 index 0bd2f7e77e..0000000000 --- a/app/assets/stylesheets/admin.css +++ /dev/null @@ -1,3 +0,0 @@ -// Manifest -// -//= require_tree ./common/admin diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss new file mode 100644 index 0000000000..d1de50da4e --- /dev/null +++ b/app/assets/stylesheets/admin.scss @@ -0,0 +1 @@ +@import "common/admin/admin_base" diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index a7c7665084..ca47c70d4c 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1662,4 +1662,4 @@ table#user-badges { .mobile-view .full-width { margin: 0; -} \ No newline at end of file +} diff --git a/app/views/common/_discourse_stylesheet.html.erb b/app/views/common/_discourse_stylesheet.html.erb index 2ccfc92b90..702bbd9f02 100644 --- a/app/views/common/_discourse_stylesheet.html.erb +++ b/app/views/common/_discourse_stylesheet.html.erb @@ -5,7 +5,7 @@ <%- end %> <%- if staff? %> - <%= stylesheet_link_tag "admin"%> + <%= DiscourseStylesheets.stylesheet_link_tag(:admin) %> <%- end %> <%- unless customization_disabled? %> From 3edf0e662fb50496731df1fe28ba5b260424582f Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 12:04:35 -0700 Subject: [PATCH 101/237] FIX: Make user card colors absolute (xcpt shadow) --- app/assets/stylesheets/desktop/user-card.scss | 51 +++++++++---------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/app/assets/stylesheets/desktop/user-card.scss b/app/assets/stylesheets/desktop/user-card.scss index 6781e222d3..d2e4b60abc 100644 --- a/app/assets/stylesheets/desktop/user-card.scss +++ b/app/assets/stylesheets/desktop/user-card.scss @@ -1,4 +1,5 @@ // styles that apply to the "share" popup when sharing a link to a post or topic +// Colors should mostly be absolute here, it will look the same in dark & light themes #user-card { position: absolute; @@ -6,20 +7,19 @@ left: -9999px; top: -9999px; z-index: 990; - box-shadow: 0 2px 6px rgba(0,0,0,.6); + box-shadow: 0 2px 12px rgba($primary, .6); margin-top: -2px; - background-color: $primary; - color: $secondary; + color: #ffffff; background-size: cover; - background-position: center center; + background: #222222 center center; min-height: 175px; + -webkit-transition: opacity .2s, -webkit-transform .2s; + transition: opacity .2s, transform .2s; + opacity: 0; -webkit-transform: scale(.9); -ms-transform: scale(.9); transform: scale(.9); - -webkit-transition: opacity .2s, -webkit-transform .2s; - transition: opacity .2s, transform .2s; - &.show { opacity: 1; -webkit-transform: scale(1); @@ -29,7 +29,7 @@ .card-content { padding: 12px 12px 0 12px; - background: rgba($primary, .85); + background: rgba(#222222, .85); margin-top: 80px; &:after { @@ -55,7 +55,6 @@ } h1 { - display: inline-block; min-width: 120px; font-size: 1.786em; line-height: 1.25; @@ -65,11 +64,11 @@ overflow: hidden; text-overflow: ellipsis; a { - color: $secondary; + color: #222; } i { font-size: .8em; - color: $secondary; + color: #222; } } @@ -83,7 +82,7 @@ overflow: hidden; text-overflow: ellipsis; a { - color: $secondary; + color: #fff; } } @@ -91,11 +90,11 @@ font-size: 0.929em; font-weight: normal; margin-top: 0; - color: dark-light-diff($secondary, $primary, 25%, -25%); + color: scale-color(#fff, $lightness: 25%); overflow: hidden; text-overflow: ellipsis; a { - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: scale-color(#222, $lightness: 50%); } } @@ -103,10 +102,10 @@ font-size: 0.929em; font-weight: normal; margin-top: 0; - color: $primary; + color: #222; .group-link { - color: $primary; + color: #222; } } @@ -118,11 +117,11 @@ display: inline; margin-right: 5px; .desc, a { - color: dark-light-diff($secondary, $primary, 50%, -50%); + color: scale-color(#fff, $lightness: 50%); } } - div {display: inline; color: scale-color($primary, $lightness: 50%); - .group-link {color: scale-color($primary, $lightness: 50%);} + div {display: inline; color: scale-color(#222, $lightness: 50%); + .group-link {color: scale-color(#222, $lightness: 50%);} } } @@ -140,7 +139,7 @@ clear: left; a { - color: $secondary; + color: #fff; text-decoration: underline; } img { @@ -148,7 +147,7 @@ } a.mention { - background-color: dark-light-diff($secondary, $primary, 50%, -60%); + background-color: scale-color(#fff, $lightness: 50%); } .overflow { max-height: 60px; @@ -180,7 +179,7 @@ } .new-user a { - color: scale-color($primary, $lightness: 70%); + color: scale-color(#222, $lightness: 70%); } &.show-badges { @@ -210,12 +209,12 @@ .user-badge { background: transparent; - color: dark-light-diff($primary, $secondary, 50%, -50%); - border-color: dark-light-diff($primary, $secondary, 50%, -50%); + color: scale-color(#222, $lightness: 50%); + border-color: scale-color(#222, $lightness: 50%); } h3 { - color: $primary; + color: #222; font-size: 1em; margin-bottom: -8px; } @@ -242,6 +241,6 @@ right: 12px; bottom: 12px; font-size: 2.143em; - i {color: $secondary;} + i {color: #fff;} } } From a4da72a83b93eeceae467173a132c5a912fa7ae6 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 12:23:06 -0700 Subject: [PATCH 102/237] FIX: Dark theme fixes for admin, quotes, code --- .../stylesheets/common/admin/admin_base.scss | 10 +++++----- .../stylesheets/common/base/code_highlighting.scss | 14 +++++++------- app/assets/stylesheets/common/base/discourse.scss | 4 ++-- app/assets/stylesheets/common/base/topic-post.scss | 10 +++++----- .../stylesheets/common/components/buttons.css.scss | 7 +++---- .../stylesheets/common/foundation/variables.scss | 7 +++++++ 6 files changed, 29 insertions(+), 23 deletions(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index ca47c70d4c..33da77e84c 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -129,16 +129,16 @@ td.flaggers td { } .admin-controls { - background-color: dark-light-diff($primary, $secondary, 90%, -75%); + background-color: dark-light-diff($primary, $secondary, 90%, -65%); padding: 10px 10px 3px 0; @include clearfix; .nav.nav-pills { li.active { a { - border-color: dark-light-diff($primary, $secondary, 90%, -60%); - background-color: lighten($primary, 40%); + border-color: dark-light-diff($primary, $secondary, 90%, -90%); + background-color: dark-light-diff($primary, $secondary, 40%, -10%); &:hover { - background-color: lighten($primary, 40%); + background-color: dark-light-diff($primary, $secondary, 40%, -10%); } } } @@ -1113,7 +1113,7 @@ table.api-keys { .content-list { h3 { - color: dark-light-diff($primary, $secondary, 50%, -60%); + color: dark-light-diff($primary, $secondary, 50%, -20%); font-size: 1.071em; padding-left: 5px; margin-bottom: 10px; diff --git a/app/assets/stylesheets/common/base/code_highlighting.scss b/app/assets/stylesheets/common/base/code_highlighting.scss index 02aa19e727..c5fe8d617c 100644 --- a/app/assets/stylesheets/common/base/code_highlighting.scss +++ b/app/assets/stylesheets/common/base/code_highlighting.scss @@ -7,7 +7,7 @@ github.com style (c) Vasily Polovnyov .hljs { display: block; padding: 0.5em; - color: #333; + color: dark-light-choose(#333, #f8f8f8); } .hljs-comment, @@ -26,7 +26,7 @@ github.com style (c) Vasily Polovnyov .hljs-subst, .hljs-request, .hljs-status { - color: #333; + color: dark-light-choose(#333, #f8f8f8); font-weight: bold; } @@ -40,7 +40,7 @@ github.com style (c) Vasily Polovnyov .hljs-tag .hljs-value, .hljs-phpdoc, .tex .hljs-formula { - color: #d14; + color: dark-light-choose(#d14, #f66); } .hljs-title, @@ -70,14 +70,14 @@ github.com style (c) Vasily Polovnyov .hljs-tag .hljs-title, .hljs-rules .hljs-property, .django .hljs-tag .hljs-keyword { - color: #000080; + color: dark-light-choose(#000080, #99f); font-weight: normal; } .hljs-attribute, .hljs-variable, .lisp .hljs-body { - color: #008080; + color: dark-light-choose(#008080, #0ee); } .hljs-regexp { @@ -132,8 +132,8 @@ github.com style (c) Vasily Polovnyov */ p > code, li > code, pre > code { - color: #333; - background: #f8f8f8; + color: dark-light-choose(#333, #f8f8f8); + background: dark-light-choose(#f8f8f8, #333); } // removed some unnecessary styles here diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 43da67c1e5..ce6ca6682c 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -32,8 +32,8 @@ small { blockquote { - background-color: dark-light-diff($primary, $secondary, 97%, -45%); - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -65%); + border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); + background-color: dark-light-diff($primary, $secondary, 97%, -65%); overflow: hidden; clear: both; } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index ec590ae39c..0013fab8cd 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -50,8 +50,8 @@ aside.quote { .badge-wrapper { margin-left: 5px; } .title { - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -65%); - background-color: dark-light-diff($primary, $secondary, 97%, -45%); + border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); + background-color: dark-light-diff($primary, $secondary, 97%, -65%); color: scale-color($primary, $lightness: 30%); // IE will screw up the blockquote underneath if bottom padding is 0px padding: 12px 12px 1px 12px; @@ -113,8 +113,8 @@ aside.quote { .quote-button { display: none; position: absolute; - background-color: scale-color($primary, $lightness: 50%); - color: $secondary; + background-color: dark-light-diff($primary, $secondary, 50%, -50%); + color: dark-light-choose($secondary, $primary); padding: 10px; z-index: 401; @@ -124,7 +124,7 @@ aside.quote { } &:hover { - background-color: scale-color($primary, $lightness: 40%); + background-color: dark-light-diff($primary, $secondary, 40%, -40%); cursor: pointer; } } diff --git a/app/assets/stylesheets/common/components/buttons.css.scss b/app/assets/stylesheets/common/components/buttons.css.scss index 2821356313..f55ff6d34e 100644 --- a/app/assets/stylesheets/common/components/buttons.css.scss +++ b/app/assets/stylesheets/common/components/buttons.css.scss @@ -73,17 +73,16 @@ .btn-primary { border: none; - color: $secondary; font-weight: normal; - color: #fff; - background: $tertiary; + color: dark-light-choose($primary, scale-color($primary, $lightness: 60%)); + background: dark-light-choose($tertiary, $tertiary); &[href] { color: $secondary; } &:hover { color: #fff; - background: scale-color($tertiary, $lightness: -20%); + background: dark-light-choose(scale-color($tertiary, $lightness: -20%), scale-color($tertiary, $lightness: -20%)); } &:active { @include linear-gradient(scale-color($tertiary, $lightness: -20%), scale-color($tertiary, $lightness: -10%)); diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 01e77684b3..0c51ce420a 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -42,3 +42,10 @@ $base-font-family: Helvetica, Arial, sans-serif !default; @return scale-color($adjusted-color, $lightness: $darkness); } } +@function dark-light-choose($light-theme-result, $dark-theme-result) { + @if brightness($primary) < brightness($secondary) { + @return $light-theme-result; + } @else { + @return $dark-theme-result; + } +} From c8c3b057cbe1a4ddb0acc87315ed7ccdb127426f Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 12:28:58 -0700 Subject: [PATCH 103/237] FIX: Unread posts in dark theme --- app/assets/stylesheets/common/components/badges.css.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 8008d48b21..9b5248091f 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -244,9 +244,9 @@ // New posts &.new-posts, &.unread-posts { - background-color: scale-color($tertiary, $lightness: 50%); + background-color: dark-light-choose(scale-color($tertiary, $lightness: 50%), scale-color($tertiary, $lightness: 20%)); color: $secondary; - font-weight: normal; + font-weight: dark-light-choose(normal, bold); } &.new-topic { From 94439ebdddbc4471e82d546891e5c4da2a81afeb Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 12:49:54 -0700 Subject: [PATCH 104/237] FIX: Tighter rate-limit for post self-deletions --- app/controllers/posts_controller.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 4161c36098..1e428c6e8c 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -184,6 +184,7 @@ class PostsController < ApplicationController def destroy post = find_post_from_params + RateLimiter.new(current_user, "delete_post", 3, 1.minute).performed! unless current_user.staff? if too_late_to(:delete_post, post) render json: {errors: [I18n.t('too_late_to_edit')]}, status: 422 @@ -206,6 +207,7 @@ class PostsController < ApplicationController def recover post = find_post_from_params + RateLimiter.new(current_user, "delete_post", 3, 1.minute).performed! unless current_user.staff? guardian.ensure_can_recover_post!(post) destroyer = PostDestroyer.new(current_user, post) destroyer.recover From 173126673b8a56d47c89d356f02c09be60b2d10d Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 12:53:44 -0700 Subject: [PATCH 105/237] FIX: Apply blockquote colors to onebox --- app/assets/stylesheets/common/base/onebox.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index 2ff7b0d3aa..c2072b8118 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -13,8 +13,8 @@ a.loading-onebox { .onebox-result { margin-top: 15px; padding: 12px 25px 12px 12px; - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -65%); - background: dark-light-diff($primary, $secondary, 97%, -45%); + border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); + background-color: dark-light-diff($primary, $secondary, 97%, -65%); font-size: 1em; > .source { margin-bottom: 12px; @@ -90,8 +90,8 @@ a.loading-onebox { aside.onebox { padding: 12px 25px 12px 12px; - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -65%); - background: dark-light-diff($primary, $secondary, 97%, -45%); + border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); + background-color: dark-light-diff($primary, $secondary, 97%, -65%); font-size: 1em; header { @@ -148,8 +148,8 @@ aside.onebox .onebox-body .onebox-avatar { blockquote { aside.onebox { - background: dark-light-diff($primary, $secondary, 97%, -45%); - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -65%); + border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); + background-color: dark-light-diff($primary, $secondary, 97%, -65%); } } From 6a0eba3ba228e553527d60a9d87da5f160d0d8bd Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 13:20:07 -0700 Subject: [PATCH 106/237] Oops, that should fix it.. --- app/assets/stylesheets/common/components/buttons.css.scss | 4 ++-- app/assets/stylesheets/desktop/user-card.scss | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/components/buttons.css.scss b/app/assets/stylesheets/common/components/buttons.css.scss index f55ff6d34e..2ea0e60a38 100644 --- a/app/assets/stylesheets/common/components/buttons.css.scss +++ b/app/assets/stylesheets/common/components/buttons.css.scss @@ -74,8 +74,8 @@ .btn-primary { border: none; font-weight: normal; - color: dark-light-choose($primary, scale-color($primary, $lightness: 60%)); - background: dark-light-choose($tertiary, $tertiary); + color: dark-light-choose(#fff, scale-color($primary, $lightness: 60%)); + background: $tertiary; &[href] { color: $secondary; diff --git a/app/assets/stylesheets/desktop/user-card.scss b/app/assets/stylesheets/desktop/user-card.scss index d2e4b60abc..e04cecafa0 100644 --- a/app/assets/stylesheets/desktop/user-card.scss +++ b/app/assets/stylesheets/desktop/user-card.scss @@ -64,11 +64,11 @@ overflow: hidden; text-overflow: ellipsis; a { - color: #222; + color: #fff; } i { font-size: .8em; - color: #222; + color: #fff; } } From 3baabd14f828a79b08cbe9e4c896bd53e71601b3 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 13:28:02 -0700 Subject: [PATCH 107/237] Use variables for user card colors --- app/assets/stylesheets/desktop/user-card.scss | 43 ++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/app/assets/stylesheets/desktop/user-card.scss b/app/assets/stylesheets/desktop/user-card.scss index e04cecafa0..bb9c61b515 100644 --- a/app/assets/stylesheets/desktop/user-card.scss +++ b/app/assets/stylesheets/desktop/user-card.scss @@ -1,6 +1,9 @@ // styles that apply to the "share" popup when sharing a link to a post or topic // Colors should mostly be absolute here, it will look the same in dark & light themes +$user_card_primary: #fff; +$user_card_background: #222; + #user-card { position: absolute; width: 500px; @@ -9,9 +12,9 @@ z-index: 990; box-shadow: 0 2px 12px rgba($primary, .6); margin-top: -2px; - color: #ffffff; + color: $user_card_primary; background-size: cover; - background: #222222 center center; + background: $user_card_background center center; min-height: 175px; -webkit-transition: opacity .2s, -webkit-transform .2s; transition: opacity .2s, transform .2s; @@ -29,7 +32,7 @@ .card-content { padding: 12px 12px 0 12px; - background: rgba(#222222, .85); + background: rgba($user_card_background, .85); margin-top: 80px; &:after { @@ -64,11 +67,11 @@ overflow: hidden; text-overflow: ellipsis; a { - color: #fff; + color: $user_card_primary; } i { font-size: .8em; - color: #fff; + color: $user_card_primary; } } @@ -82,7 +85,7 @@ overflow: hidden; text-overflow: ellipsis; a { - color: #fff; + color: $user_card_primary; } } @@ -90,11 +93,11 @@ font-size: 0.929em; font-weight: normal; margin-top: 0; - color: scale-color(#fff, $lightness: 25%); + color: scale-color($user_card_primary, $lightness: 25%); overflow: hidden; text-overflow: ellipsis; a { - color: scale-color(#222, $lightness: 50%); + color: scale-color($user_card_background, $lightness: 50%); } } @@ -102,10 +105,10 @@ font-size: 0.929em; font-weight: normal; margin-top: 0; - color: #222; + color: $user_card_background; .group-link { - color: #222; + color: $user_card_background; } } @@ -117,11 +120,11 @@ display: inline; margin-right: 5px; .desc, a { - color: scale-color(#fff, $lightness: 50%); + color: scale-color($user_card_primary, $lightness: 50%); } } - div {display: inline; color: scale-color(#222, $lightness: 50%); - .group-link {color: scale-color(#222, $lightness: 50%);} + div {display: inline; color: scale-color($user_card_background, $lightness: 50%); + .group-link {color: scale-color($user_card_background, $lightness: 50%);} } } @@ -139,7 +142,7 @@ clear: left; a { - color: #fff; + color: $user_card_primary; text-decoration: underline; } img { @@ -147,7 +150,7 @@ } a.mention { - background-color: scale-color(#fff, $lightness: 50%); + background-color: scale-color($user_card_primary, $lightness: 50%); } .overflow { max-height: 60px; @@ -179,7 +182,7 @@ } .new-user a { - color: scale-color(#222, $lightness: 70%); + color: scale-color($user_card_background, $lightness: 70%); } &.show-badges { @@ -209,12 +212,12 @@ .user-badge { background: transparent; - color: scale-color(#222, $lightness: 50%); - border-color: scale-color(#222, $lightness: 50%); + color: scale-color($user_card_background, $lightness: 50%); + border-color: scale-color($user_card_background, $lightness: 50%); } h3 { - color: #222; + color: $user_card_background; font-size: 1em; margin-bottom: -8px; } @@ -241,6 +244,6 @@ right: 12px; bottom: 12px; font-size: 2.143em; - i {color: #fff;} + i {color: $user_card_primary;} } } From cf559893b895c88340ac66afdb3e9474b1e19df2 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 14:03:01 -0700 Subject: [PATCH 108/237] FIX: fully-read topic style in dark theme --- app/assets/stylesheets/common/base/_topic-list.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index 7518c2cc0d..6a89dae2cf 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -16,8 +16,8 @@ } } -html.anon .topic-list a.title:visited:not(.badge-notification) {color: scale-color($primary, $lightness: 45%);} -.topic-list a.title.visited:not(.badge-notification) {color: scale-color($primary, $lightness: 45%);} +html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-light-diff($primary, $secondary, 45%, -30%);} +.topic-list a.title.visited:not(.badge-notification) {color: dark-light-diff($primary, $secondary, 45%, -30%);} .topic-list { width: 100%; From 0282d89b29dafbdcee18e47151c3adc82b8a0392 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 14:09:20 -0700 Subject: [PATCH 109/237] FIX: Lightboxes in dark theme --- app/assets/stylesheets/common/base/lightbox.scss | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/base/lightbox.scss b/app/assets/stylesheets/common/base/lightbox.scss index 681eec6253..818c9b0084 100644 --- a/app/assets/stylesheets/common/base/lightbox.scss +++ b/app/assets/stylesheets/common/base/lightbox.scss @@ -3,7 +3,6 @@ display: inline-block; &:hover .meta { - background: $primary; opacity: 1; transition: opacity .5s; } @@ -18,8 +17,8 @@ position: absolute; bottom: 0; width: 100%; - color: $secondary; - background: $primary; + color: dark-light-choose($secondary, $primary); + background: dark-light-choose($primary, lighten($secondary, 10%)); opacity: 0; transition: opacity .2s; @@ -39,7 +38,7 @@ .informations { margin: 6px; padding-right: 20px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-diff($primary, $secondary, 50%, -50%); font-size: 1em; } From 4d0c32840441e426ad703eff5c4656017a5a0f47 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 14:37:23 -0700 Subject: [PATCH 110/237] FIX: Header icons should be header_primary exactly --- app/assets/stylesheets/common/base/header.scss | 2 +- app/assets/stylesheets/common/foundation/colors.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index ed398f4a06..1b87a19c12 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -61,7 +61,7 @@ .icon { display: block; padding: 3px; - color: scale-color($header_primary, $lightness: 50%); + color: $header_primary; text-decoration: none; cursor: pointer; border-top: 1px solid transparent; diff --git a/app/assets/stylesheets/common/foundation/colors.scss b/app/assets/stylesheets/common/foundation/colors.scss index 8fe9170fc2..6a7277ca41 100644 --- a/app/assets/stylesheets/common/foundation/colors.scss +++ b/app/assets/stylesheets/common/foundation/colors.scss @@ -3,7 +3,7 @@ $secondary: #ffffff !default; $tertiary: #0088cc !default; $quaternary: #e45735 !default; $header_background: #ffffff !default; -$header_primary: #333333 !default; +$header_primary: #999999 !default; $highlight: #ffff4d !default; $danger: #e45735 !default; $success: #009900 !default; From ca577248d06c839b873b3cb747a1f2ba2cfe3507 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 14:44:52 -0700 Subject: [PATCH 111/237] FIX: Fix tag input select2 box --- app/assets/stylesheets/common/base/combobox.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/combobox.scss b/app/assets/stylesheets/common/base/combobox.scss index aa3d7d02fe..3910ed5559 100644 --- a/app/assets/stylesheets/common/base/combobox.scss +++ b/app/assets/stylesheets/common/base/combobox.scss @@ -37,7 +37,12 @@ } } - +.select2-container-multi .select2-choices { + background-color: $secondary; +} +.select2-container-multi .select2-choices .select2-search-field input.select2-active { + background: $secondary url(/assets/select2-spinner.gif) no-repeat 100% !important; +} .select2-container a.select2-choice { background: $secondary; From 78dcf30444b42c3d24a9609e701d415cbd2b87ad Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 14:50:36 -0700 Subject: [PATCH 112/237] FIX: Suggested topics box was bad in dark theme --- app/assets/stylesheets/desktop/compose.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index b09b0840d4..4f23d9378d 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -65,14 +65,14 @@ } .similar-topics { - background-color: scale-color($tertiary, $lightness: 60%); + background-color: dark-light-choose(scale-color($tertiary, $lightness: 60%), scale-color($tertiary, $lightness: -60%)); a[href] { - color: #000; + color: dark-light-diff($primary, $secondary, -10%, 10%); } .posts-count { - background-color: scale-color($tertiary, $lightness: -40%); + background-color: dark-light-choose(scale-color($tertiary, $lightness: -40%), scale-color($tertiary, $lightness: 40%)); } ul { @@ -82,7 +82,7 @@ } .search-link { .fa, .blurb { - color: scale-color($tertiary, $lightness: -40%); + color: dark-light-choose(scale-color($tertiary, $lightness: -40%), scale-color($tertiary, $lightness: 40%)); } } } From e81f21d8485811981cab84e14053316829f862bc Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 15:02:41 -0700 Subject: [PATCH 113/237] FIX: Github oneboxes in dark theme --- .../stylesheets/common/base/code_highlighting.scss | 10 +++++----- app/assets/stylesheets/common/base/onebox.scss | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/common/base/code_highlighting.scss b/app/assets/stylesheets/common/base/code_highlighting.scss index c5fe8d617c..25c8a72f00 100644 --- a/app/assets/stylesheets/common/base/code_highlighting.scss +++ b/app/assets/stylesheets/common/base/code_highlighting.scss @@ -14,7 +14,7 @@ github.com style (c) Vasily Polovnyov .hljs-template_comment, .diff .hljs-header, .hljs-javadoc { - color: #998; + color: dark-light-choose(#998, #bba); font-style: italic; } @@ -33,14 +33,14 @@ github.com style (c) Vasily Polovnyov .hljs-number, .hljs-hexcolor, .ruby .hljs-constant { - color: #099; + color: dark-light-choose(#099, #aff); } .hljs-string, .hljs-tag .hljs-value, .hljs-phpdoc, .tex .hljs-formula { - color: dark-light-choose(#d14, #f66); + color: dark-light-choose(#d14, #f99); } .hljs-title, @@ -62,7 +62,7 @@ github.com style (c) Vasily Polovnyov .haskell .hljs-type, .vhdl .hljs-literal, .tex .hljs-command { - color: #458; + color: dark-light-choose(#458, #9AE); font-weight: bold; } @@ -89,7 +89,7 @@ github.com style (c) Vasily Polovnyov .lisp .hljs-keyword, .tex .hljs-special, .hljs-prompt { - color: #990073; + color: dark-light-choose(#990073, #fbe); } .hljs-built_in, diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index c2072b8118..a533a15f1b 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -159,7 +159,7 @@ pre.onebox code ol.lines li:before { display:inline-block; width:35px; left: -40px; - background: #F7F7F7; + background: dark-light-choose(#F7F7F7, #070707); color:#AFAFAF; text-align:right; padding-right:5px; @@ -174,8 +174,10 @@ pre.onebox code ol{ margin-left:0px; line-height:1.5em; } +pre.onebox code { + background-color: dark-light-choose(#fff, #000); +} pre.onebox code li{ - background-color:#fff; padding-left:5px; } @@ -194,7 +196,7 @@ pre.onebox code ol.lines li { } pre.onebox code li.selected{ - background-color:#F8EEC7 + background-color: dark-light-choose(#F8EEC7, #541); } pre.onebox code { From e5f4020c75fc54eb885fdab0ffda20e05ddb783b Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 15:05:05 -0700 Subject: [PATCH 114/237] FIX: User card badge, interface language select2 dark theme --- app/assets/stylesheets/common/base/combobox.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/combobox.scss b/app/assets/stylesheets/common/base/combobox.scss index 3910ed5559..6b2e556902 100644 --- a/app/assets/stylesheets/common/base/combobox.scss +++ b/app/assets/stylesheets/common/base/combobox.scss @@ -56,7 +56,9 @@ border-radius: 3px 3px 0 0; border-color: $tertiary; } - +.select2-drop { + color: dark-light-diff($primary, $secondary, -50%, 50%); +} .select2-drop-active { border: 1px solid $tertiary; border-top: 0; From 13fdd35517222d4d4ba87541b8d89e0d82812f4f Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 15:11:13 -0700 Subject: [PATCH 115/237] Revert "FIX: Header icons should be header_primary exactly" This reverts commit 4d0c32840441e426ad703eff5c4656017a5a0f47. --- app/assets/stylesheets/common/base/header.scss | 2 +- app/assets/stylesheets/common/foundation/colors.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 1b87a19c12..ed398f4a06 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -61,7 +61,7 @@ .icon { display: block; padding: 3px; - color: $header_primary; + color: scale-color($header_primary, $lightness: 50%); text-decoration: none; cursor: pointer; border-top: 1px solid transparent; diff --git a/app/assets/stylesheets/common/foundation/colors.scss b/app/assets/stylesheets/common/foundation/colors.scss index 6a7277ca41..8fe9170fc2 100644 --- a/app/assets/stylesheets/common/foundation/colors.scss +++ b/app/assets/stylesheets/common/foundation/colors.scss @@ -3,7 +3,7 @@ $secondary: #ffffff !default; $tertiary: #0088cc !default; $quaternary: #e45735 !default; $header_background: #ffffff !default; -$header_primary: #999999 !default; +$header_primary: #333333 !default; $highlight: #ffff4d !default; $danger: #e45735 !default; $success: #009900 !default; From 924e67af9d6836fe1e00cf3fb02c8f399483270e Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 15:11:49 -0700 Subject: [PATCH 116/237] FIX: Header icons should be header_primray in dark theme --- app/assets/stylesheets/common/base/header.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index ed398f4a06..574d2744d6 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -61,7 +61,7 @@ .icon { display: block; padding: 3px; - color: scale-color($header_primary, $lightness: 50%); + color: dark-light-choose(scale-color($header_primary, $lightness: 50%), $header_primary); text-decoration: none; cursor: pointer; border-top: 1px solid transparent; From 75f7631367a5e537caddccdab1c5cab0e75ed758 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 15:20:19 -0700 Subject: [PATCH 117/237] FIX: Small-actions in dark theme --- app/assets/stylesheets/common/base/topic-post.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 0013fab8cd..8746d6e726 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -269,7 +269,7 @@ table.md-table { font-size: 35px; width: 45px; text-align: center; - color: lighten($primary, 75%); + color: dark-light-diff($primary, $secondary, 75%, 20%); } } @@ -279,7 +279,7 @@ table.md-table { text-transform: uppercase; font-weight: bold; font-size: 0.9em; - color: lighten($primary, 50%); + color: dark-light-diff($primary, $secondary, 50%, 0%); .custom-message { text-transform: none; From 689449b23376aef1f62d7f15d2058f2d2ee7d2df Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 15:51:50 -0700 Subject: [PATCH 118/237] FIX: Post highlight on mobile dark theme --- app/assets/stylesheets/common/base/topic-post.scss | 5 +++++ app/assets/stylesheets/desktop/topic-post.scss | 3 --- app/assets/stylesheets/mobile/topic-post.scss | 6 +----- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 8746d6e726..f0eee220af 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -129,6 +129,11 @@ aside.quote { } } +.topic-body { + &.highlighted { + background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + } +} .wiki .topic-body { background-color: dark-light-diff($wiki, $secondary, 95%, -50%); } diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 181d065511..b974353416 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -699,9 +699,6 @@ $topic-avatar-width: 45px; z-index: 2; border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); padding: 12px $topic-body-width-padding 15px $topic-body-width-padding; - &.highlighted { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); - } } .topic-avatar { border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -75%); diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 77b634c92e..fbde0923a3 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -463,12 +463,8 @@ blockquote { .posts-wrapper { position: relative; } -.topic-body.highlighted { - background-color: scale-color($tertiary, $lightness: 75%); -} - span.highlighted { - background-color: scale-color($highlight, $lightness: 70%); + background-color: dark-light-choose(scale-color($highlight, $lightness: 70%), $highlight); } .topic-avatar { From c2197de11ee4b43bdd7159ae31d524d97227e2b5 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 08:54:16 +1000 Subject: [PATCH 119/237] upgrade logster to resolve error forwarding issue --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5ce6b8971a..53e5f44644 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (1.0.0.0.pre) + logster (1.0.0.1.pre) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From 27b1ec29179e6cdca2d834d71afc36170e87552b Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 09:12:08 +1000 Subject: [PATCH 120/237] FIX: incorrect emoji stripping logic --- lib/email/styles.rb | 7 +++++-- spec/components/email/styles_spec.rb | 16 ++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/lib/email/styles.rb b/lib/email/styles.rb index 426c03d670..22f9ffe1e8 100644 --- a/lib/email/styles.rb +++ b/lib/email/styles.rb @@ -159,13 +159,16 @@ module Email end def strip_avatars_and_emojis - @fragment.css('img').each do |img| + @fragment.search('img').each do |img| if img['src'] =~ /_avatar/ img.parent['style'] = "vertical-align: top;" if img.parent.name == 'td' img.remove end - img.replace(img['title']) if (img['src'] =~ /images\/emoji/ || img['src'] =~ /uploads\/default\/_emoji/) + if img['title'] && (img['src'] =~ /images\/emoji/ || img['src'] =~ /uploads\/default\/_emoji/) + img.add_previous_sibling(img['title'] || "emoji") + img.remove + end end @fragment.to_s diff --git a/spec/components/email/styles_spec.rb b/spec/components/email/styles_spec.rb index e062d654bc..a4f8a5dde0 100644 --- a/spec/components/email/styles_spec.rb +++ b/spec/components/email/styles_spec.rb @@ -147,5 +147,21 @@ describe Email::Styles do end + context "strip_avatars_and_emojis" do + it "works for lonesome emoji with no title" do + emoji = "" + style = Email::Styles.new(emoji) + style.strip_avatars_and_emojis + expect(style.to_html).to match_html(emoji) + end + + it "works for lonesome emoji with title" do + emoji = "" + style = Email::Styles.new(emoji) + style.strip_avatars_and_emojis + expect(style.to_html).to match_html("cry_cry") + end + end + end From b38a1309f7740c457037df1ad8354b0d5e81894d Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 09:27:47 +1000 Subject: [PATCH 121/237] FIX: add more quoting to avoid invalid terms --- lib/search.rb | 2 +- spec/components/search_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/search.rb b/lib/search.rb index 0b3bf5328c..25840cd678 100644 --- a/lib/search.rb +++ b/lib/search.rb @@ -481,7 +481,7 @@ class Search t.split(/[\)\(&']/)[0] end.compact! - query = Post.sanitize(all_terms.map {|t| "#{PG::Connection.escape_string(t)}:*"}.join(" #{joiner} ")) + query = Post.sanitize(all_terms.map {|t| "'#{PG::Connection.escape_string(t)}':*"}.join(" #{joiner} ")) "TO_TSQUERY(#{locale || query_locale}, #{query})" end diff --git a/spec/components/search_spec.rb b/spec/components/search_spec.rb index 498887c115..a74bffee1b 100644 --- a/spec/components/search_spec.rb +++ b/spec/components/search_spec.rb @@ -447,7 +447,7 @@ describe Search do it 'can parse complex strings using ts_query helper' do str = " grigio:babel deprecated? " - str << "page page on Atmosphere](https://atmospherejs.com/grigio/babel)xxx: aaa'\"bbb" + str << "page page on Atmosphere](https://atmospherejs.com/grigio/babel)xxx: aaa.js:222 aaa'\"bbb" ts_query = Search.ts_query(str, "simple") Post.exec_sql("SELECT to_tsvector('bbb') @@ " << ts_query) From 019191a944641bcb3458c72c16e7bcbb96efc5eb Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 19 Aug 2015 09:31:09 +0800 Subject: [PATCH 122/237] FIX: undoDeleteState() should restore delete button. --- app/assets/javascripts/discourse/models/post.js.es6 | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index c8c4b400ff..8c12025ee4 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -220,6 +220,7 @@ const Post = RestModel.extend({ cooked: this.get('oldCooked'), version: this.get('version') - 1, can_recover: false, + can_delete: true, user_deleted: false }); } From feed822c48f6e80c73e5c8d0326d9ede238cbd8b Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 11:51:12 +1000 Subject: [PATCH 123/237] FIX: grant badge dialog not working --- .../javascripts/admin/controllers/admin-user-badges.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 index 523668556c..7404af5f2d 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 @@ -86,7 +86,7 @@ export default Ember.ArrayController.extend({ **/ grantBadge: function(badgeId) { var self = this; - Discourse.UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(function(userBadge) { + Discourse.UserBadge.grant(badgeId, this.get('user.model.username'), this.get('badgeReason')).then(function(userBadge) { self.set('badgeReason', ''); self.pushObject(userBadge); Ember.run.next(function() { From c493f82907090f6e0a44e2f3298527237a57ee09 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 11:54:12 +1000 Subject: [PATCH 124/237] cleaner fix --- .../javascripts/admin/controllers/admin-user-badges.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 index 7404af5f2d..e66d821621 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 @@ -1,6 +1,6 @@ export default Ember.ArrayController.extend({ needs: ["adminUser"], - user: Em.computed.alias('controllers.adminUser'), + user: Em.computed.alias('controllers.adminUser.model'), sortProperties: ['granted_at'], sortAscending: false, @@ -86,7 +86,7 @@ export default Ember.ArrayController.extend({ **/ grantBadge: function(badgeId) { var self = this; - Discourse.UserBadge.grant(badgeId, this.get('user.model.username'), this.get('badgeReason')).then(function(userBadge) { + Discourse.UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(function(userBadge) { self.set('badgeReason', ''); self.pushObject(userBadge); Ember.run.next(function() { From 714f841f0a1febb3aca0269678d82f0a403313af Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 12:15:38 +1000 Subject: [PATCH 125/237] FIX: null bytes in user input should not cause post creation to fail --- lib/post_creator.rb | 6 ++++++ spec/components/post_creator_spec.rb | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/lib/post_creator.rb b/lib/post_creator.rb index bbe0256ef2..52bd0621fe 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -54,9 +54,15 @@ class PostCreator # If we don't do this we introduce a rather risky dependency @user = user @opts = opts || {} + pg_clean_up!(opts[:title]) + pg_clean_up!(opts[:raw]) @spam = false end + def pg_clean_up!(str) + str.gsub!("\u0000", "") if str + end + # True if the post was considered spam def spam? @spam diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index f9619821b8..955663e4d2 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -21,6 +21,12 @@ describe PostCreator do let(:creator_with_meta_data) { PostCreator.new(user, basic_topic_params.merge(meta_data: {hello: "world"} )) } let(:creator_with_image_sizes) { PostCreator.new(user, basic_topic_params.merge(image_sizes: image_sizes)) } + it "can create a topic with null byte central" do + post = PostCreator.create(user, title: "hello\u0000world this is title", raw: "this is my\u0000 first topic") + expect(post.raw).to eq 'this is my first topic' + expect(post.topic.title).to eq 'Helloworld this is title' + end + it "can be created with auto tracking disabled" do p = PostCreator.create(user, basic_topic_params.merge(auto_track: false)) # must be 0 otherwise it will think we read the topic which is clearly untrue From 71644add2f8289341ce198759ef7d0d31a0d9434 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 18 Aug 2015 22:26:18 -0400 Subject: [PATCH 126/237] add plugin-outlet at end of site-map --- app/assets/javascripts/discourse/templates/site-map.hbs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/javascripts/discourse/templates/site-map.hbs b/app/assets/javascripts/discourse/templates/site-map.hbs index 7865eebdc6..99af0489cb 100644 --- a/app/assets/javascripts/discourse/templates/site-map.hbs +++ b/app/assets/javascripts/discourse/templates/site-map.hbs @@ -55,6 +55,8 @@ {{#if showMobileToggle}}
  • {{boundI18n mobileViewLinkTextKey}}
  • {{/if}} + + {{plugin-outlet "site-map-links-last"}} {{#if categories}} From 431def1f52669e2c2c117d05b788cbf9c345915b Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 12:27:55 +1000 Subject: [PATCH 127/237] need to dup strings, some may be frozen --- lib/post_creator.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 52bd0621fe..089d13f61d 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -54,13 +54,13 @@ class PostCreator # If we don't do this we introduce a rather risky dependency @user = user @opts = opts || {} - pg_clean_up!(opts[:title]) - pg_clean_up!(opts[:raw]) + opts[:title] = pg_clean_up(opts[:title]) if opts[:title] + opts[:raw] = pg_clean_up(opts[:raw]) if opts[:raw] @spam = false end - def pg_clean_up!(str) - str.gsub!("\u0000", "") if str + def pg_clean_up(str) + str.gsub("\u0000", "") end # True if the post was considered spam From 5f5d055a86dee1f9d54584bf61a5bb49aa7a7673 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 12:30:18 +1000 Subject: [PATCH 128/237] only if \u0000 is included for the perf --- lib/post_creator.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/post_creator.rb b/lib/post_creator.rb index 089d13f61d..9b869749b0 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -54,8 +54,8 @@ class PostCreator # If we don't do this we introduce a rather risky dependency @user = user @opts = opts || {} - opts[:title] = pg_clean_up(opts[:title]) if opts[:title] - opts[:raw] = pg_clean_up(opts[:raw]) if opts[:raw] + opts[:title] = pg_clean_up(opts[:title]) if opts[:title] && opts[:title].include?("\u0000") + opts[:raw] = pg_clean_up(opts[:raw]) if opts[:raw] && opts[:raw].include?("\u0000") @spam = false end From 82a6176b086a69b768c21b17c8e44ca4a7f7fd7b Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 12:32:31 +1000 Subject: [PATCH 129/237] lower the volume on failed to pull hotlinked image add more diagnostics --- app/jobs/regular/pull_hotlinked_images.rb | 6 +++--- config/initializers/logster.rb | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/jobs/regular/pull_hotlinked_images.rb b/app/jobs/regular/pull_hotlinked_images.rb index 87a63b9855..efe7457e5c 100644 --- a/app/jobs/regular/pull_hotlinked_images.rb +++ b/app/jobs/regular/pull_hotlinked_images.rb @@ -41,10 +41,10 @@ module Jobs upload = Upload.create_for(post.user_id, hotlinked, filename, File.size(hotlinked.path), { origin: src }) downloaded_urls[src] = upload.url else - Rails.logger.error("Failed to pull hotlinked image: #{src} - Image is bigger than #{@max_size}") + Rails.logger.info("Failed to pull hotlinked image for post: #{post_id}: #{src} - Image is bigger than #{@max_size}") end else - Rails.logger.error("There was an error while downloading '#{src}' locally.") + Rails.logger.error("There was an error while downloading '#{src}' locally for post: #{post_id}") end end # have we successfully downloaded that file? @@ -66,7 +66,7 @@ module Jobs raw.gsub!(src, "") end rescue => e - Rails.logger.error("Failed to pull hotlinked image: #{src}\n" + e.message + "\n" + e.backtrace.join("\n")) + Rails.logger.info("Failed to pull hotlinked image: #{src} post:#{post_id}\n" + e.message + "\n" + e.backtrace.join("\n")) ensure # close & delete the temp file hotlinked && hotlinked.close! diff --git a/config/initializers/logster.rb b/config/initializers/logster.rb index e3e748faa6..2daf7200fe 100644 --- a/config/initializers/logster.rb +++ b/config/initializers/logster.rb @@ -31,10 +31,7 @@ if Rails.env.production? /^ActiveRecord::RecordNotFound /, # bad asset requested, no need to log - /^ActionController::BadRequest /, - - # hotlinked image error that is pointless - /^Failed to pull hotlinked image.*(404 Not Found|403 Forbidden|gestaddrinfo: Name or service not known)/m + /^ActionController::BadRequest / ] end From 33260f320311ca4dc87277e7427aec93bddcaa10 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 19 Aug 2015 10:44:32 +0800 Subject: [PATCH 130/237] FIX: Errors raised for recovering post not being handled. --- app/assets/javascripts/discourse/models/post.js.es6 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 8c12025ee4..51e586014e 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -162,7 +162,9 @@ const Post = RestModel.extend({ // Recover a deleted post recover() { - const post = this; + const post = this, + initProperties = post.getProperties('deleted_at', 'deleted_by', 'user_deleted', 'can_delete'); + post.setProperties({ deleted_at: null, deleted_by: null, @@ -178,6 +180,9 @@ const Post = RestModel.extend({ can_delete: true, version: data.version }); + }).catch(function(error) { + popupAjaxError(error); + post.setProperties(initProperties); }); }, From f7b024eafe1c950232d7d9067dd97093c160d2a6 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 21:35:25 -0700 Subject: [PATCH 131/237] FIX: User profiles Note on removing the color: $secondary - I did a test with browser inspector by turning the text pink. The only text affected was 'helpful flags', which I fixed below. --- app/assets/stylesheets/desktop/user.scss | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 027beaa530..2f5af264d6 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -214,7 +214,6 @@ width: 100%; margin-bottom: 10px; overflow: hidden; - color: $secondary; &.group { .details { @@ -269,7 +268,7 @@ .details { padding: 15px 15px 4px 15px; margin-top: -200px; - background: rgba($primary, .85); + background: dark-light-choose(rgba($primary, .85), rgba($secondary, .85)); transition: margin .15s linear; h1 { @@ -316,20 +315,20 @@ width: 100%; position: relative; float: left; - color: $secondary; + color: dark-light-choose($secondary, lighten($primary, 10%)); h1 {font-weight: bold;} .primary-textual { padding: 3px; a[href] { - color: $secondary; + color: dark-light-choose($secondary, lighten($primary, 10%)); } } .bio { - color: $secondary; + color: dark-light-choose($secondary, lighten($primary, 10%)); max-height: 300px; overflow: auto; max-width: 750px; @@ -396,7 +395,7 @@ .details { padding: 12px 15px 2px 15px; margin-top: 0; - background: rgba($primary, 1); + background: dark-light-choose(rgba($primary, 1), scale-color($secondary, $lightness: 40%)); .bio { display: none; } .primary { @@ -533,21 +532,21 @@ .staff-counters { text-align: left; background: $primary; + color: $secondary; + a { + color: $secondary; + } > div { margin: 0 10px 0 0; display: inline-block; padding: 5px 0; &:first-of-type { padding-left: 10px; - } + } span { padding: 1px 5px; border-radius: 10px; - } - - } - a { - color: $secondary; + } } .active { font-weight: bold; From 7ee0ee67698d6492f355fd96e2832427e1c9f89c Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 21:40:19 -0700 Subject: [PATCH 132/237] Optimize CSS properties --- .../common/components/keyboard_shortcuts.css.scss | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss b/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss index 1aa845d893..3ad2ff4d83 100644 --- a/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss +++ b/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss @@ -28,17 +28,12 @@ } b { - color: #000; - background: #eee; - border-style: solid; - border-color: #ccc #aaa #888 #bbb; padding: 2px 6px; border-radius: 4px; box-shadow: 0 2px 0 rgba(0,0,0,0.2),0 0 0 1px #fff inset; - background-color: #fafafa; - border-color: #ccc #ccc #fff; - border-style: solid solid none; - border-width: 1px 1px medium; + background: #fafafa; + border: 1px solid #ccc; + border-bottom: medium none #fff; color: #444; white-space: nowrap; display: inline-block; From c4e5594826d85094dae9270f9420e3d35a4d8e45 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 21:42:41 -0700 Subject: [PATCH 133/237] FIX: Keyboard shortcuts dark theme --- .../common/components/keyboard_shortcuts.css.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss b/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss index 3ad2ff4d83..aeff2b7774 100644 --- a/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss +++ b/app/assets/stylesheets/common/components/keyboard_shortcuts.css.scss @@ -30,11 +30,11 @@ b { padding: 2px 6px; border-radius: 4px; - box-shadow: 0 2px 0 rgba(0,0,0,0.2),0 0 0 1px #fff inset; - background: #fafafa; - border: 1px solid #ccc; - border-bottom: medium none #fff; - color: #444; + box-shadow: 0 2px 0 rgba(0,0,0,0.2),0 0 0 1px dark-light-choose(#fff,#000) inset; + background: dark-light-choose(#fafafa, #333); + border: 1px solid dark-light-choose(#ccc, #555); + border-bottom: medium none dark-light-choose(#fff, #000); + color: dark-light-choose(#444, #aaa); white-space: nowrap; display: inline-block; } From 7ede107be9215bd409f6394396e13f1114f1cc58 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 21:45:05 -0700 Subject: [PATCH 134/237] FIX: User directory dark theme --- app/assets/stylesheets/common/base/directory.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/directory.scss b/app/assets/stylesheets/common/base/directory.scss index 7170b09218..ad4004264f 100644 --- a/app/assets/stylesheets/common/base/directory.scss +++ b/app/assets/stylesheets/common/base/directory.scss @@ -26,7 +26,7 @@ .number, .time-read { font-size: 1.4em; - color: dark-light-diff($primary, $secondary, 50%, -50%); + color: dark-light-diff($primary, $secondary, 50%, -20%); } } @@ -35,7 +35,7 @@ background-color: dark-light-diff($highlight, $secondary, 70%, -80%); .username a, .name, .title, .number, .time-read { - color: scale-color($highlight, $lightness: -50%); + color: dark-light-choose(scale-color($highlight, $lightness: -50%), scale-color($highlight, $lightness: 50%)); } } } From 2f595f27e9e2bc8a1d080d7cdad5783ee4a29fa1 Mon Sep 17 00:00:00 2001 From: Kane York Date: Tue, 18 Aug 2015 22:02:01 -0700 Subject: [PATCH 135/237] CLEANUP: Coalesce repeated CSS properties --- app/assets/stylesheets/common/base/alert.scss | 1 - app/assets/stylesheets/common/base/emoji.scss | 4 +- .../common/base/magnific-popup.scss | 3 +- app/assets/stylesheets/common/base/modal.scss | 1 - .../stylesheets/common/base/onebox.scss | 3 +- .../stylesheets/common/base/pagedown.scss | 14 +----- .../stylesheets/common/base/upload.scss | 3 +- .../stylesheets/common/base/user-badges.scss | 6 +-- .../common/base/username_tagsinput.scss | 13 ++++- .../common/components/navs.css.scss | 3 +- .../stylesheets/common/foundation/mixins.scss | 1 + .../stylesheets/common/topic-entrance.scss | 2 - app/assets/stylesheets/desktop/discourse.scss | 6 +-- app/assets/stylesheets/desktop/modal.scss | 1 - .../stylesheets/desktop/topic-list.scss | 3 +- .../stylesheets/desktop/topic-post.scss | 1 - app/assets/stylesheets/mobile/discourse.scss | 3 +- app/assets/stylesheets/mobile/modal.scss | 3 +- app/assets/stylesheets/mobile/topic-list.scss | 9 ++-- app/assets/stylesheets/mobile/topic.scss | 1 - app/assets/stylesheets/mobile/user.scss | 3 +- app/assets/stylesheets/vendor/bootstrap.scss | 1 + app/assets/stylesheets/vendor/pikaday.scss | 4 +- app/assets/stylesheets/vendor/select2.scss | 49 +++++++++---------- 24 files changed, 55 insertions(+), 83 deletions(-) diff --git a/app/assets/stylesheets/common/base/alert.scss b/app/assets/stylesheets/common/base/alert.scss index fe80bb2eba..1476a3f233 100644 --- a/app/assets/stylesheets/common/base/alert.scss +++ b/app/assets/stylesheets/common/base/alert.scss @@ -11,7 +11,6 @@ float: right; font-size: 1.429em; font-weight: bold; - line-height: 18px; color: $primary; opacity: 0.2; filter: alpha(opacity = 20); diff --git a/app/assets/stylesheets/common/base/emoji.scss b/app/assets/stylesheets/common/base/emoji.scss index fb658ea4a5..2d9061bffc 100644 --- a/app/assets/stylesheets/common/base/emoji.scss +++ b/app/assets/stylesheets/common/base/emoji.scss @@ -51,9 +51,7 @@ body img.emoji { } .emoji-modal .toolbar { - margin: 0; - margin-top: 8px; - margin-bottom: 5px + margin: 8px 0 5px; } .emoji-modal .toolbar li { diff --git a/app/assets/stylesheets/common/base/magnific-popup.scss b/app/assets/stylesheets/common/base/magnific-popup.scss index 98ead180b7..3aa7f39eb4 100644 --- a/app/assets/stylesheets/common/base/magnific-popup.scss +++ b/app/assets/stylesheets/common/base/magnific-popup.scss @@ -337,9 +337,8 @@ button { @if $IE7support { filter: unquote("alpha(opacity=#{$controls-opacity*100})"); } - margin: 0; top: 50%; - margin-top: -55px; + margin: -55px 0 0; padding: 0; width: 90px; height: 110px; diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 0df5f7b412..ee8de426b2 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -65,7 +65,6 @@ margin: 0 auto; background-color: $secondary; border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); box-shadow: 0 3px 7px rgba(0,0,0, .8); background-clip: padding-box; diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index 2ff7b0d3aa..84cfbca27d 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -82,8 +82,7 @@ a.loading-onebox { @mixin onebox-favicon($class, $image) { &.#{$class} .source { - background-image: image-url("favicons/#{$image}.png"); - background-repeat: no-repeat; + background: image-url("favicons/#{$image}.png") no-repeat; padding-left: 20px; } } diff --git a/app/assets/stylesheets/common/base/pagedown.scss b/app/assets/stylesheets/common/base/pagedown.scss index ec7b7c3757..8cac61a14f 100644 --- a/app/assets/stylesheets/common/base/pagedown.scss +++ b/app/assets/stylesheets/common/base/pagedown.scss @@ -13,10 +13,7 @@ } .wmd-button-row { - margin-left: 5px; - margin-right: 5px; - margin-bottom: 5px; - margin-top: 5px; + margin: 5px; padding: 0; height: 20px; overflow: hidden; @@ -33,18 +30,9 @@ } .wmd-button { - width: 20px; - height: 20px; - padding-left: 2px; - padding-right: 3px; margin-right: 5px; - background-repeat: no-repeat; - background-position: 0 0; border: 0; - width: 20px; - height: 20px; position: relative; - border: 0; float: left; font-family: FontAwesome; font-weight: normal; diff --git a/app/assets/stylesheets/common/base/upload.scss b/app/assets/stylesheets/common/base/upload.scss index 97fdf508ec..6afba003ef 100644 --- a/app/assets/stylesheets/common/base/upload.scss +++ b/app/assets/stylesheets/common/base/upload.scss @@ -1,5 +1,4 @@ .uploaded-image-preview { - background-position: center center; background-size: cover; - background-color: $primary; + background: $primary center center; } diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index 74b87c8265..e1bd5d0862 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -4,10 +4,9 @@ color: $primary; border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); line-height: 19px; - margin: 0; display: inline-block; background-color: $secondary; - margin-bottom: 3px; + margin: 0 0 3px; .fa { padding-right: 3px; @@ -52,8 +51,7 @@ img { display: block; - margin: auto; - margin-bottom: 4px; + margin: auto auto 4px; width: 55px; height: 55px; } diff --git a/app/assets/stylesheets/common/base/username_tagsinput.scss b/app/assets/stylesheets/common/base/username_tagsinput.scss index 8e0f60dca9..c18494c18c 100644 --- a/app/assets/stylesheets/common/base/username_tagsinput.scss +++ b/app/assets/stylesheets/common/base/username_tagsinput.scss @@ -24,8 +24,17 @@ div.tagsinput span.tag { } div.tagsinput span.tag a { font-weight: bold; color: #82ad2b; text-decoration:none; font-size: 11px; } -div.tagsinput input { width:80px; margin:0px; font-family: helvetica; font-size: 0.929em; border:1px solid transparent; -padding:2px 5px; background: transparent; color: #000; outline:0px; margin-right:5px; margin-bottom:5px; } +div.tagsinput input { + width:80px; + font-family: helvetica; + font-size: 0.929em; + border:1px solid transparent; + padding:2px 5px; + background: transparent; + color: #000; + outline:0px; + margin: 0px 5px 5px 0px; +} div.tagsinput div { display:block; float: left; } .tags_clear { clear: both; width: 100%; height: 0; } .not_valid {background: #FBD8DB !important; color: #90111A !important;} diff --git a/app/assets/stylesheets/common/components/navs.css.scss b/app/assets/stylesheets/common/components/navs.css.scss index 5934c2fb74..d29d05a3d0 100644 --- a/app/assets/stylesheets/common/components/navs.css.scss +++ b/app/assets/stylesheets/common/components/navs.css.scss @@ -75,10 +75,9 @@ { left: 90%; top: 33%; - border: solid transparent; content: " "; position: absolute; - border-width: 8px; + border: 8px solid transparent; border-left-color: $secondary; } diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index 60a682ac09..2243802d16 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -60,6 +60,7 @@ // Linear gradient +//noinspection CssOptimizeSimilarProperties @mixin linear-gradient($start-color, $end-color) { background-color: $start-color; background-image: linear-gradient(to bottom, $start-color, $end-color); diff --git a/app/assets/stylesheets/common/topic-entrance.scss b/app/assets/stylesheets/common/topic-entrance.scss index 8316aae9eb..847edc46a8 100644 --- a/app/assets/stylesheets/common/topic-entrance.scss +++ b/app/assets/stylesheets/common/topic-entrance.scss @@ -8,8 +8,6 @@ position: absolute; width: 133px; - padding: 5px; - @include unselectable; button.full { diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index 0d117b4ae2..e25653136a 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -77,12 +77,12 @@ body { .grippie { width: 100%; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - border-width: 1px 0; + border: 1px solid; + border-right-width: 0; + border-left-width: 0; cursor: row-resize; height: 11px; overflow: hidden; - background-color: dark-light-diff($primary, $secondary, 90%, -60%); display:block; background: image-url("grippie.png") dark-light-diff($primary, $secondary, 90%, -60%) no-repeat center 3px; } diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss index 8c527e21f7..c9d5b84c38 100644 --- a/app/assets/stylesheets/desktop/modal.scss +++ b/app/assets/stylesheets/desktop/modal.scss @@ -17,7 +17,6 @@ margin: -250px 0 0 -305px; background-color: $secondary; border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); box-shadow: 0 3px 7px rgba(0,0,0, .8); background-clip: padding-box; diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 2e90524da0..8d3a880b1c 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -182,8 +182,7 @@ } td.latest { vertical-align: top; - padding: 8px; - padding-top: 0; + padding: 0 8px 8px; } .last-user-info { font-size: 0.857em; diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 181d065511..1d26686237 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -800,7 +800,6 @@ $topic-avatar-width: 45px; #selected-posts { - padding-left: 20px; margin-left: 330px; width: 200px; position: fixed; diff --git a/app/assets/stylesheets/mobile/discourse.scss b/app/assets/stylesheets/mobile/discourse.scss index f4a32585b6..4d56b037f1 100644 --- a/app/assets/stylesheets/mobile/discourse.scss +++ b/app/assets/stylesheets/mobile/discourse.scss @@ -88,8 +88,7 @@ blockquote { // we must remove margins for text site titles h2#site-text-logo { - margin: 0; - margin-left: 10px; + margin: 0 0 0 10px; } // categories should not be bold on mobile; they fight with the topic title too much diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss index 8c94edf25c..e54ab173ce 100644 --- a/app/assets/stylesheets/mobile/modal.scss +++ b/app/assets/stylesheets/mobile/modal.scss @@ -17,7 +17,6 @@ width: 100%; height: auto; background-color: $secondary; - border: 1px solid scale-color($secondary, $lightness: 90%); border: 1px solid rgba(0, 0, 0, 0.3); @include border-radius-all (6px); @@ -178,4 +177,4 @@ #search-help p { margin: 5px; -} \ No newline at end of file +} diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 5f71cddcad..47c69376bb 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -268,17 +268,14 @@ tr.category-topic-link { float: left; width: 280px; padding: 4px 0; - margin: 1px 0 0; list-style: none; background-color: $secondary; - border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); - border: 1px solid rgba(0, 0, 0, 0.2); + border: 1px solid dark-light-choose(rgba(0, 0, 0, 0.2), scale-color($primary, $lightness: -60%)); border-radius: 5px; box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); background-clip: padding-box; - margin-bottom: 20px; -.title {font-weight: bold; display: block;} - + margin: 1px 0 20px; + .title {font-weight: bold; display: block;} } .dropdown-menu a { display: block; diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index db76dd41f7..9bc347b650 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -69,7 +69,6 @@ position: absolute; bottom: 34px; width: 133px; - padding: 5px; button.full { width: 100%; diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 1f45f68ee8..03ec7fb16f 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -221,9 +221,8 @@ } .about { - background-color: dark-light-diff($primary, $secondary, 0%, -75%); background-size: cover; - background-position: center center; + background: dark-light-diff($primary, $secondary, 0%, -75%) center center; width: 100%; margin-bottom: 10px; overflow: hidden; diff --git a/app/assets/stylesheets/vendor/bootstrap.scss b/app/assets/stylesheets/vendor/bootstrap.scss index cf73b4d7d8..df21cf6c8a 100644 --- a/app/assets/stylesheets/vendor/bootstrap.scss +++ b/app/assets/stylesheets/vendor/bootstrap.scss @@ -14,6 +14,7 @@ @import "common/foundation/mixins"; +//noinspection CssOverwrittenProperties a:focus { outline: thin dotted #333; outline: 5px auto -webkit-focus-ring-color; diff --git a/app/assets/stylesheets/vendor/pikaday.scss b/app/assets/stylesheets/vendor/pikaday.scss index 3b2a3c0a80..a12e8e5460 100644 --- a/app/assets/stylesheets/vendor/pikaday.scss +++ b/app/assets/stylesheets/vendor/pikaday.scss @@ -86,9 +86,7 @@ text-indent: 20px; // hide text using text-indent trick, using width value (it's enough) white-space: nowrap; overflow: hidden; - background-color: transparent; - background-position: center center; - background-repeat: no-repeat; + background: transparent no-repeat center center; background-size: 75% 75%; opacity: .5; *position: absolute; diff --git a/app/assets/stylesheets/vendor/select2.scss b/app/assets/stylesheets/vendor/select2.scss index aa3d61372a..828c718517 100644 --- a/app/assets/stylesheets/vendor/select2.scss +++ b/app/assets/stylesheets/vendor/select2.scss @@ -200,6 +200,7 @@ Version: @@ver@@ Timestamp: @@timestamp@@ white-space: nowrap; } +//noinspection CssOverwrittenProperties .select2-search input { width: 100%; height: auto !important; @@ -228,6 +229,7 @@ Version: @@ver@@ Timestamp: @@timestamp@@ margin-top: 4px; } +//noinspection CssOverwrittenProperties .select2-search input.select2-active { background: #fff asset-url('select2-spinner.gif') no-repeat 100%; background: asset-url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); @@ -389,16 +391,14 @@ disabled look for disabled choices in the results dropdown /* disabled styles */ .select2-container.select2-container-disabled .select2-choice { - background-color: #f4f4f4; - background-image: none; - border: 1px solid #ddd; - cursor: default; + background: #f4f4f4 none; + border: 1px solid #ddd; + cursor: default; } .select2-container.select2-container-disabled .select2-choice .select2-arrow { - background-color: #f4f4f4; - background-image: none; - border-left: 0; + background: #f4f4f4 none; + border-left: 0; } .select2-container.select2-container-disabled .select2-choice abbr { @@ -540,17 +540,15 @@ html[dir="rtl"] .select2-search-choice-close { /* disabled styles */ .select2-container-multi.select2-container-disabled .select2-choices { - background-color: #f4f4f4; - background-image: none; - border: 1px solid #ddd; - cursor: default; + background: #f4f4f4 none; + border: 1px solid #ddd; + cursor: default; } .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { - padding: 3px 5px 3px 5px; - border: 1px solid #ddd; - background-image: none; - background-color: #f4f4f4; + padding: 3px 5px 3px 5px; + border: 1px solid #ddd; + background: #f4f4f4 none; } .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; @@ -594,16 +592,15 @@ html[dir="rtl"] .select2-search-choice-close { /* Retina-ize icons */ @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 2dppx) { - .select2-search input, - .select2-search-choice-close, - .select2-container .select2-choice abbr, - .select2-container .select2-choice .select2-arrow b { - background-image: asset-url('select2x2.png') !important; - background-repeat: no-repeat !important; - background-size: 60px 40px !important; - } + .select2-search input, + .select2-search-choice-close, + .select2-container .select2-choice abbr, + .select2-container .select2-choice .select2-arrow b { + background: asset-url('select2x2.png') no-repeat !important; + background-size: 60px 40px !important; + } - .select2-search input { - background-position: 100% -21px !important; - } + .select2-search input { + background-position: 100% -21px !important; + } } From 157b1185597774a71b341cc7c8e5818b83a93117 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 16:28:04 +1000 Subject: [PATCH 136/237] FIX: pointless error in log when failing to save post rate_limit gets fired for rollback on in which case there is not post_number --- app/models/post.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/post.rb b/app/models/post.rb index 4c53442a1b..940cb17cf1 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -90,7 +90,7 @@ class Post < ActiveRecord::Base end def limit_posts_per_day - if user.first_day_user? && post_number > 1 + if user.first_day_user? && post_number && post_number > 1 RateLimiter.new(user, "first-day-replies-per-day", SiteSetting.max_replies_in_first_day, 1.day.to_i) end end From 9364194f36ad523669cf27fbee51559dc4591dc7 Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 16:57:26 +1000 Subject: [PATCH 137/237] cut out an exception --- app/models/application_request.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/models/application_request.rb b/app/models/application_request.rb index 041741fc5b..8f146f10fd 100644 --- a/app/models/application_request.rb +++ b/app/models/application_request.rb @@ -22,7 +22,8 @@ class ApplicationRequest < ActiveRecord::Base def self.increment!(type, opts=nil) key = redis_key(type) val = $redis.incr(key).to_i - $redis.expire key, 3.days + # 3.days, see: https://github.com/rails/rails/issues/21296 + $redis.expire(key, 259200) autoflush = (opts && opts[:autoflush]) || self.autoflush if autoflush > 0 && val >= autoflush From 2203a4147d0698482229a0b8cbc1e67324de59fd Mon Sep 17 00:00:00 2001 From: Sam Date: Wed, 19 Aug 2015 16:58:25 +1000 Subject: [PATCH 138/237] add some extra diagnostics --- config/initializers/06-mini_profiler.rb | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb index 5f7e017cb5..2b0eedb68b 100644 --- a/config/initializers/06-mini_profiler.rb +++ b/config/initializers/06-mini_profiler.rb @@ -93,3 +93,13 @@ if defined?(Rack::MiniProfiler) # Rack::MiniProfiler.profile_method ActionView::PathResolver, 'find_templates' end + + +if ENV["PRINT_EXCEPTIONS"] + trace = TracePoint.new(:raise) do |tp| + puts tp.raised_exception + puts tp.raised_exception.backtrace.join("\n") + puts + end + trace.enable +end From 5f5933b4bb11756d28b725944279659bb2fbbc9d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 19 Aug 2015 15:05:49 +0800 Subject: [PATCH 139/237] Add a plugin outlet beside the poster's name. --- app/assets/javascripts/discourse/templates/post.hbs | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/javascripts/discourse/templates/post.hbs b/app/assets/javascripts/discourse/templates/post.hbs index bcdc0f7f82..6790353025 100644 --- a/app/assets/javascripts/discourse/templates/post.hbs +++ b/app/assets/javascripts/discourse/templates/post.hbs @@ -25,6 +25,7 @@
    From 113e8d62bac17e65c93e5535af45d3a1d2879d86 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Aug 2015 10:14:13 -0400 Subject: [PATCH 143/237] FIX: Looks like a celluloid release was pulled --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 53e5f44644..e5ed9c4b55 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -62,7 +62,7 @@ GEM builder (3.2.2) byebug (5.0.0) columnize (= 0.9.0) - celluloid (0.16.1) + celluloid (0.16.0) timers (~> 4.0.0) certified (1.0.0) coderay (1.1.0) From dfd33c849dbf345a040f1a8180eff3f805ce1c8b Mon Sep 17 00:00:00 2001 From: Allen Hancock Date: Wed, 19 Aug 2015 10:20:55 -0500 Subject: [PATCH 144/237] fix for links in smtp providers --- docs/INSTALL-cloud.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/INSTALL-cloud.md b/docs/INSTALL-cloud.md index 1146b8b898..858f525bfe 100644 --- a/docs/INSTALL-cloud.md +++ b/docs/INSTALL-cloud.md @@ -77,7 +77,7 @@ After completing your edits, press CtrlO then Enter Date: Tue, 18 Aug 2015 21:24:09 -0400 Subject: [PATCH 145/237] Convert Badges / User Badges to ES6. --- Gemfile.lock | 3 -- .../controllers/admin-user-badges.js.es6 | 10 ++-- .../admin/routes/admin-badges-show.js.es6 | 3 +- .../admin/routes/admin-badges.js.es6 | 4 +- .../admin/routes/admin-user-badges.js.es6 | 26 +++++++++ .../admin/routes/admin_user_badges_route.js | 32 ----------- app/assets/javascripts/discourse.js | 3 -- .../discourse/controllers/badges/show.js.es6 | 4 +- .../mixins/badge-select-controller.js.es6 | 10 ++-- .../discourse/models/badge-grouping.js.es6 | 16 ++++++ .../models/{badge.js => badge.js.es6} | 53 ++++++++----------- .../discourse/models/badge_grouping.js | 10 ---- .../{user_badge.js => user-badge.js.es6} | 22 +++----- .../javascripts/discourse/models/user.js.es6 | 8 +-- .../discourse/routes/badges-index.js.es6 | 6 ++- .../discourse/routes/badges-show.js.es6 | 9 ++-- ...ute.js.es6 => discovery-categories.js.es6} | 4 +- .../routes/preferences-badge-title.js.es6 | 3 +- .../routes/preferences-card-badge.js.es6 | 3 +- .../discourse/routes/user-badges.js.es6 | 3 +- app/assets/javascripts/main_include.js | 4 ++ lib/discourse_plugin_registry.rb | 4 +- .../controllers/admin-user-badges-test.js.es6 | 16 +++--- test/javascripts/models/badge-test.js.es6 | 36 +++++++------ .../javascripts/models/user-badge-test.js.es6 | 18 ++++--- 25 files changed, 156 insertions(+), 154 deletions(-) create mode 100644 app/assets/javascripts/admin/routes/admin-user-badges.js.es6 delete mode 100644 app/assets/javascripts/admin/routes/admin_user_badges_route.js create mode 100644 app/assets/javascripts/discourse/models/badge-grouping.js.es6 rename app/assets/javascripts/discourse/models/{badge.js => badge.js.es6} (83%) delete mode 100644 app/assets/javascripts/discourse/models/badge_grouping.js rename app/assets/javascripts/discourse/models/{user_badge.js => user-badge.js.es6} (86%) rename app/assets/javascripts/discourse/routes/{discovery-categories-route.js.es6 => discovery-categories.js.es6} (94%) diff --git a/Gemfile.lock b/Gemfile.lock index e5ed9c4b55..59aaea207c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -485,6 +485,3 @@ DEPENDENCIES uglifier unf unicorn - -BUNDLED WITH - 1.10.6 diff --git a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 index e66d821621..b85fbaf069 100644 --- a/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-user-badges.js.es6 @@ -1,3 +1,5 @@ +import UserBadge from 'discourse/models/user-badge'; + export default Ember.ArrayController.extend({ needs: ["adminUser"], user: Em.computed.alias('controllers.adminUser.model'), @@ -86,7 +88,7 @@ export default Ember.ArrayController.extend({ **/ grantBadge: function(badgeId) { var self = this; - Discourse.UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(function(userBadge) { + UserBadge.grant(badgeId, this.get('user.username'), this.get('badgeReason')).then(function(userBadge) { self.set('badgeReason', ''); self.pushObject(userBadge); Ember.run.next(function() { @@ -102,12 +104,6 @@ export default Ember.ArrayController.extend({ }); }, - /** - Revoke the selected userBadge. - - @method revokeBadge - @param {Discourse.UserBadge} userBadge the `Discourse.UserBadge` instance that needs to be revoked. - **/ revokeBadge: function(userBadge) { var self = this; return bootbox.confirm(I18n.t("admin.badges.revoke_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(result) { 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 648730d9b0..c283ff737b 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 Badge from 'discourse/models/badge'; import showModal from 'discourse/lib/show-modal'; export default Ember.Route.extend({ @@ -7,7 +8,7 @@ export default Ember.Route.extend({ model(params) { if (params.badge_id === "new") { - return Discourse.Badge.create({ + return Badge.create({ name: I18n.t('admin.badges.new_badge') }); } diff --git a/app/assets/javascripts/admin/routes/admin-badges.js.es6 b/app/assets/javascripts/admin/routes/admin-badges.js.es6 index 3d417f53c2..5927437669 100644 --- a/app/assets/javascripts/admin/routes/admin-badges.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-badges.js.es6 @@ -1,3 +1,5 @@ +import Badge from 'discourse/models/badge'; + export default Discourse.Route.extend({ _json: null, @@ -5,7 +7,7 @@ export default Discourse.Route.extend({ var self = this; return Discourse.ajax('/admin/badges.json').then(function(json) { self._json = json; - return Discourse.Badge.createFromJson(json); + return Badge.createFromJson(json); }); }, diff --git a/app/assets/javascripts/admin/routes/admin-user-badges.js.es6 b/app/assets/javascripts/admin/routes/admin-user-badges.js.es6 new file mode 100644 index 0000000000..205c1647c9 --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-user-badges.js.es6 @@ -0,0 +1,26 @@ +import UserBadge from 'discourse/models/user-badge'; +import Badge from 'discourse/models/badge'; + +export default Discourse.Route.extend({ + model() { + const username = this.modelFor('adminUser').get('username'); + return UserBadge.findByUsername(username); + }, + + setupController(controller, model) { + // Find all badges. + controller.set('loading', true); + Badge.findAll().then(function(badges) { + controller.set('badges', badges); + if (badges.length > 0) { + var grantableBadges = controller.get('grantableBadges'); + if (grantableBadges.length > 0) { + controller.set('selectedBadgeId', grantableBadges[0].get('id')); + } + } + controller.set('loading', false); + }); + // Set the model. + controller.set('model', model); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin_user_badges_route.js b/app/assets/javascripts/admin/routes/admin_user_badges_route.js deleted file mode 100644 index f681e6f919..0000000000 --- a/app/assets/javascripts/admin/routes/admin_user_badges_route.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - Shows all of the badges that have been granted to a user, and allow granting and - revoking badges. - - @class AdminUserBadgesRoute - @extends Discourse.Route - @namespace Discourse - @module Discourse -**/ -Discourse.AdminUserBadgesRoute = Discourse.Route.extend({ - model: function() { - var username = this.modelFor('adminUser').get('username'); - return Discourse.UserBadge.findByUsername(username); - }, - - setupController: function(controller, model) { - // Find all badges. - controller.set('loading', true); - Discourse.Badge.findAll().then(function(badges) { - controller.set('badges', badges); - if (badges.length > 0) { - var grantableBadges = controller.get('grantableBadges'); - if (grantableBadges.length > 0) { - controller.set('selectedBadgeId', grantableBadges[0].get('id')); - } - } - controller.set('loading', false); - }); - // Set the model. - controller.set('model', model); - } -}); diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 02b2bad076..e12c8243d1 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -157,9 +157,6 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { }) }); -// TODO: Remove this, it is in for backwards compatibiltiy with plugins -Discourse.HasCurrentUser = {}; - function proxyDep(propName, moduleFunc, msg) { if (Discourse.hasOwnProperty(propName)) { return; } Object.defineProperty(Discourse, propName, { diff --git a/app/assets/javascripts/discourse/controllers/badges/show.js.es6 b/app/assets/javascripts/discourse/controllers/badges/show.js.es6 index 1083fd37f1..a5a899f649 100644 --- a/app/assets/javascripts/discourse/controllers/badges/show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/badges/show.js.es6 @@ -1,3 +1,5 @@ +import UserBadge from 'discourse/models/user-badge'; + export default Ember.Controller.extend({ noMoreBadges: false, userBadges: null, @@ -8,7 +10,7 @@ export default Ember.Controller.extend({ const self = this; const userBadges = this.get('userBadges'); - Discourse.UserBadge.findByBadgeId(this.get('model.id'), { + UserBadge.findByBadgeId(this.get('model.id'), { offset: userBadges.length }).then(function(result) { userBadges.pushObjects(result); diff --git a/app/assets/javascripts/discourse/mixins/badge-select-controller.js.es6 b/app/assets/javascripts/discourse/mixins/badge-select-controller.js.es6 index ea800461e8..5c57ffa425 100644 --- a/app/assets/javascripts/discourse/mixins/badge-select-controller.js.es6 +++ b/app/assets/javascripts/discourse/mixins/badge-select-controller.js.es6 @@ -1,12 +1,14 @@ +import Badge from 'discourse/models/badge'; + export default Ember.Mixin.create({ saving: false, saved: false, selectableUserBadges: function() { - var items = this.get('filteredList'); + let items = this.get('filteredList'); items = _.uniq(items, false, function(e) { return e.get('badge.name'); }); items.unshiftObject(Em.Object.create({ - badge: Discourse.Badge.create({name: I18n.t('badges.none')}) + badge: Badge.create({name: I18n.t('badges.none')}) })); return items; }.property('filteredList'), @@ -20,8 +22,8 @@ export default Ember.Mixin.create({ }.property('saving'), selectedUserBadge: function() { - var selectedUserBadgeId = parseInt(this.get('selectedUserBadgeId')); - var selectedUserBadge = null; + const selectedUserBadgeId = parseInt(this.get('selectedUserBadgeId')); + let selectedUserBadge = null; this.get('selectableUserBadges').forEach(function(userBadge) { if (userBadge.get('id') === selectedUserBadgeId) { selectedUserBadge = userBadge; diff --git a/app/assets/javascripts/discourse/models/badge-grouping.js.es6 b/app/assets/javascripts/discourse/models/badge-grouping.js.es6 new file mode 100644 index 0000000000..605d4d6d2c --- /dev/null +++ b/app/assets/javascripts/discourse/models/badge-grouping.js.es6 @@ -0,0 +1,16 @@ +import computed from 'ember-addons/ember-computed-decorators'; +import RestModel from 'discourse/models/rest'; + +export default RestModel.extend({ + + @computed('name') + i18nNameKey() { + return this.get('name').toLowerCase().replace(/\s/g, '_'); + }, + + @computed + displayName() { + const i18nKey = `badges.badge_grouping.${this.get('i18nNameKey')}.name`; + return I18n.t(i18nKey, {defaultValue: this.get('name')}); + } +}); diff --git a/app/assets/javascripts/discourse/models/badge.js b/app/assets/javascripts/discourse/models/badge.js.es6 similarity index 83% rename from app/assets/javascripts/discourse/models/badge.js rename to app/assets/javascripts/discourse/models/badge.js.es6 index 5ae1b6195a..670a84111c 100644 --- a/app/assets/javascripts/discourse/models/badge.js +++ b/app/assets/javascripts/discourse/models/badge.js.es6 @@ -1,22 +1,12 @@ -/** - A data model representing a badge on Discourse +import BadgeGrouping from 'discourse/models/badge-grouping'; +import RestModel from 'discourse/models/rest'; - @class Badge - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.Badge = Discourse.Model.extend({ - /** - Is this a new badge? +const Badge = RestModel.extend({ - @property newBadge - @type {String} - **/ newBadge: Em.computed.none('id'), hasQuery: function(){ - var query = this.get('query'); + const query = this.get('query'); return query && query.trim().length > 0; }.property('query'), @@ -40,7 +30,7 @@ Discourse.Badge = Discourse.Model.extend({ @type {String} **/ displayName: function() { - var i18nKey = "badges.badge." + this.get('i18nNameKey') + ".name"; + const i18nKey = "badges.badge." + this.get('i18nNameKey') + ".name"; return I18n.t(i18nKey, {defaultValue: this.get('name')}); }.property('name', 'i18nNameKey'), @@ -52,8 +42,8 @@ Discourse.Badge = Discourse.Model.extend({ @type {String} **/ translatedDescription: function() { - var i18nKey = "badges.badge." + this.get('i18nNameKey') + ".description", - translation = I18n.t(i18nKey); + const i18nKey = "badges.badge." + this.get('i18nNameKey') + ".description"; + let translation = I18n.t(i18nKey); if (translation.indexOf(i18nKey) !== -1) { translation = null; } @@ -73,7 +63,7 @@ Discourse.Badge = Discourse.Model.extend({ @type {String} **/ displayDescriptionHtml: function() { - var translated = this.get('translatedDescription'); + const translated = this.get('translatedDescription'); return (translated === null ? this.get('description') : translated) || ""; }.property('description', 'translatedDescription'), @@ -84,7 +74,7 @@ Discourse.Badge = Discourse.Model.extend({ @param {Object} json The JSON response returned by the server **/ updateFromJson: function(json) { - var self = this; + const self = this; if (json.badge) { Object.keys(json.badge).forEach(function(key) { self.set(key, json.badge[key]); @@ -100,7 +90,7 @@ Discourse.Badge = Discourse.Model.extend({ }, badgeTypeClassName: function() { - var type = this.get('badge_type.name') || ""; + const type = this.get('badge_type.name') || ""; return "badge-type-" + type.toLowerCase(); }.property('badge_type.name'), @@ -111,9 +101,9 @@ Discourse.Badge = Discourse.Model.extend({ @returns {Promise} A promise that resolves to the updated `Discourse.Badge` **/ save: function(data) { - var url = "/admin/badges", - requestType = "POST", - self = this; + let url = "/admin/badges", + requestType = "POST"; + const self = this; if (this.get('id')) { // We are updating an existing badge. @@ -146,7 +136,7 @@ Discourse.Badge = Discourse.Model.extend({ } }); -Discourse.Badge.reopenClass({ +Badge.reopenClass({ /** Create `Discourse.Badge` instances from the server JSON response. @@ -156,29 +146,29 @@ Discourse.Badge.reopenClass({ **/ createFromJson: function(json) { // Create BadgeType objects. - var badgeTypes = {}; + const badgeTypes = {}; if ('badge_types' in json) { json.badge_types.forEach(function(badgeTypeJson) { badgeTypes[badgeTypeJson.id] = Ember.Object.create(badgeTypeJson); }); } - var badgeGroupings = {}; + const badgeGroupings = {}; if ('badge_groupings' in json) { json.badge_groupings.forEach(function(badgeGroupingJson) { - badgeGroupings[badgeGroupingJson.id] = Discourse.BadgeGrouping.create(badgeGroupingJson); + badgeGroupings[badgeGroupingJson.id] = BadgeGrouping.create(badgeGroupingJson); }); } // Create Badge objects. - var badges = []; + let badges = []; if ("badge" in json) { badges = [json.badge]; } else { badges = json.badges; } badges = badges.map(function(badgeJson) { - var badge = Discourse.Badge.create(badgeJson); + const badge = Discourse.Badge.create(badgeJson); badge.set('badge_type', badgeTypes[badge.get('badge_type_id')]); badge.set('badge_grouping', badgeGroupings[badge.get('badge_grouping_id')]); return badge; @@ -198,7 +188,7 @@ Discourse.Badge.reopenClass({ @returns {Promise} a promise that resolves to an array of `Discourse.Badge` **/ findAll: function(opts) { - var listable = ""; + let listable = ""; if(opts && opts.onlyListable){ listable = "?only_listable=true"; } @@ -220,3 +210,6 @@ Discourse.Badge.reopenClass({ }); } }); + +export default Badge; + diff --git a/app/assets/javascripts/discourse/models/badge_grouping.js b/app/assets/javascripts/discourse/models/badge_grouping.js deleted file mode 100644 index dd59d078fa..0000000000 --- a/app/assets/javascripts/discourse/models/badge_grouping.js +++ /dev/null @@ -1,10 +0,0 @@ -Discourse.BadgeGrouping= Discourse.Model.extend({ - i18nNameKey: function() { - return this.get('name').toLowerCase().replace(/\s/g, '_'); - }.property('name'), - - displayName: function(){ - var i18nKey = "badges.badge_grouping." + this.get('i18nNameKey') + ".name"; - return I18n.t(i18nKey, {defaultValue: this.get('name')}); - }.property() -}); diff --git a/app/assets/javascripts/discourse/models/user_badge.js b/app/assets/javascripts/discourse/models/user-badge.js.es6 similarity index 86% rename from app/assets/javascripts/discourse/models/user_badge.js rename to app/assets/javascripts/discourse/models/user-badge.js.es6 index 158e70e46c..f5e2209f95 100644 --- a/app/assets/javascripts/discourse/models/user_badge.js +++ b/app/assets/javascripts/discourse/models/user-badge.js.es6 @@ -1,12 +1,6 @@ -/** - A data model representing a user badge grant on Discourse +import Badge from 'discourse/models/badge'; - @class UserBadge - @extends Discourse.Model - @namespace Discourse - @module Discourse -**/ -Discourse.UserBadge = Discourse.Model.extend({ +const UserBadge = Discourse.Model.extend({ postUrl: function() { if(this.get('topic_title')) { return "/t/-/" + this.get('topic_id') + "/" + this.get('post_number'); @@ -25,14 +19,8 @@ Discourse.UserBadge = Discourse.Model.extend({ } }); -Discourse.UserBadge.reopenClass({ - /** - Create `Discourse.UserBadge` instances from the server JSON response. +UserBadge.reopenClass({ - @method createFromJson - @param {Object} json The JSON returned by the server - @returns Array or instance of `Discourse.UserBadge` depending on the input JSON - **/ createFromJson: function(json) { // Create User objects. if (json.users === undefined) { json.users = []; } @@ -51,7 +39,7 @@ Discourse.UserBadge.reopenClass({ // Create the badges. if (json.badges === undefined) { json.badges = []; } var badges = {}; - Discourse.Badge.createFromJson(json).forEach(function(badge) { + Badge.createFromJson(json).forEach(function(badge) { badges[badge.get('id')] = badge; }); @@ -146,3 +134,5 @@ Discourse.UserBadge.reopenClass({ }); } }); + +export default UserBadge; diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index e270aaff7a..079c508286 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -6,6 +6,8 @@ import UserPostsStream from 'discourse/models/user-posts-stream'; import Singleton from 'discourse/mixins/singleton'; import { longDate } from 'discourse/lib/formatter'; import computed from 'ember-addons/ember-computed-decorators'; +import Badge from 'discourse/models/badge'; +import UserBadge from 'discourse/models/user-badge'; const User = RestModel.extend({ @@ -299,8 +301,8 @@ const User = RestModel.extend({ } if (!Em.isEmpty(json.user.featured_user_badge_ids)) { - var userBadgesMap = {}; - Discourse.UserBadge.createFromJson(json).forEach(function(userBadge) { + const userBadgesMap = {}; + UserBadge.createFromJson(json).forEach(function(userBadge) { userBadgesMap[ userBadge.get('id') ] = userBadge; }); json.user.featured_user_badges = json.user.featured_user_badge_ids.map(function(id) { @@ -309,7 +311,7 @@ const User = RestModel.extend({ } if (json.user.card_badge) { - json.user.card_badge = Discourse.Badge.create(json.user.card_badge); + json.user.card_badge = Badge.create(json.user.card_badge); } user.setProperties(json.user); diff --git a/app/assets/javascripts/discourse/routes/badges-index.js.es6 b/app/assets/javascripts/discourse/routes/badges-index.js.es6 index dfb207391f..683033faa9 100644 --- a/app/assets/javascripts/discourse/routes/badges-index.js.es6 +++ b/app/assets/javascripts/discourse/routes/badges-index.js.es6 @@ -1,9 +1,11 @@ +import Badge from 'discourse/models/badge'; + export default Discourse.Route.extend({ model() { if (PreloadStore.get("badges")) { - return PreloadStore.getAndRemove("badges").then(json => Discourse.Badge.createFromJson(json)); + return PreloadStore.getAndRemove("badges").then(json => Badge.createFromJson(json)); } else { - return Discourse.Badge.findAll({ onlyListable: true }); + return Badge.findAll({ onlyListable: true }); } }, diff --git a/app/assets/javascripts/discourse/routes/badges-show.js.es6 b/app/assets/javascripts/discourse/routes/badges-show.js.es6 index cc4cf0eaa9..5f835d76d0 100644 --- a/app/assets/javascripts/discourse/routes/badges-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/badges-show.js.es6 @@ -1,3 +1,6 @@ +import UserBadge from 'discourse/models/user-badge'; +import Badge from 'discourse/models/badge'; + export default Discourse.Route.extend({ actions: { didTransition() { @@ -15,14 +18,14 @@ export default Discourse.Route.extend({ model(params) { if (PreloadStore.get("badge")) { - return PreloadStore.getAndRemove("badge").then(json => Discourse.Badge.createFromJson(json)); + return PreloadStore.getAndRemove("badge").then(json => Badge.createFromJson(json)); } else { - return Discourse.Badge.findById(params.id); + return Badge.findById(params.id); } }, afterModel(model) { - return Discourse.UserBadge.findByBadgeId(model.get("id")).then(userBadges => { + return UserBadge.findByBadgeId(model.get("id")).then(userBadges => { this.userBadges = userBadges; }); }, diff --git a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 similarity index 94% rename from app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 rename to app/assets/javascripts/discourse/routes/discovery-categories.js.es6 index 61fd853d10..5bbd7aa66a 100644 --- a/app/assets/javascripts/discourse/routes/discovery-categories-route.js.es6 +++ b/app/assets/javascripts/discourse/routes/discovery-categories.js.es6 @@ -1,7 +1,7 @@ import showModal from "discourse/lib/show-modal"; import OpenComposer from "discourse/mixins/open-composer"; -Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { +const DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { renderTemplate() { this.render("navigation/categories", { outlet: "navigation-bar" }); this.render("discovery/categories", { outlet: "list-container" }); @@ -67,4 +67,4 @@ Discourse.DiscoveryCategoriesRoute = Discourse.Route.extend(OpenComposer, { } }); -export default Discourse.DiscoveryCategoriesRoute; +export default DiscoveryCategoriesRoute; diff --git a/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 b/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 index 0c7a22d8b4..141a852b99 100644 --- a/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 @@ -1,8 +1,9 @@ +import UserBadge from 'discourse/models/badge'; import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ model: function() { - return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username')); + return UserBadge.findByUsername(this.modelFor('user').get('username')); }, renderTemplate: function() { diff --git a/app/assets/javascripts/discourse/routes/preferences-card-badge.js.es6 b/app/assets/javascripts/discourse/routes/preferences-card-badge.js.es6 index 37ad604ec0..8ec81a95d1 100644 --- a/app/assets/javascripts/discourse/routes/preferences-card-badge.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-card-badge.js.es6 @@ -1,8 +1,9 @@ +import UserBadge from 'discourse/models/user-badge'; import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ model: function() { - return Discourse.UserBadge.findByUsername(this.modelFor('user').get('username')); + return UserBadge.findByUsername(this.modelFor('user').get('username')); }, renderTemplate: function() { diff --git a/app/assets/javascripts/discourse/routes/user-badges.js.es6 b/app/assets/javascripts/discourse/routes/user-badges.js.es6 index fcd099d765..9a67afbb92 100644 --- a/app/assets/javascripts/discourse/routes/user-badges.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-badges.js.es6 @@ -1,8 +1,9 @@ import ViewingActionType from "discourse/mixins/viewing-action-type"; +import UserBadge from 'discourse/models/user-badge'; export default Discourse.Route.extend(ViewingActionType, { model() { - return Discourse.UserBadge.findByUsername(this.modelFor("user").get("username_lower"), { grouped: true }); + return UserBadge.findByUsername(this.modelFor("user").get("username_lower"), { grouped: true }); }, setupController(controller, model) { diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 0c02cfe923..ce051e83d5 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -25,6 +25,9 @@ //= require ./discourse/lib/eyeline //= require ./discourse/helpers/register-unbound //= require ./discourse/mixins/scrolling +//= require ./discourse/models/rest +//= require ./discourse/models/badge-grouping +//= require ./discourse/models/badge //= require_tree ./discourse/mixins //= require ./discourse/lib/ajax-error //= require ./discourse/lib/markdown @@ -51,6 +54,7 @@ //= require ./discourse/models/draft //= require ./discourse/models/composer //= require ./discourse/models/invite +//= require ./discourse/models/user-badge //= require ./discourse/controllers/discovery-sortable //= require ./discourse/controllers/navigation/default //= require ./discourse/views/grouped diff --git a/lib/discourse_plugin_registry.rb b/lib/discourse_plugin_registry.rb index f3003c4e33..5ca364b1a0 100644 --- a/lib/discourse_plugin_registry.rb +++ b/lib/discourse_plugin_registry.rb @@ -87,7 +87,9 @@ class DiscoursePluginRegistry self.asset_globs.each do |g| root, ext, options = *g - next if options[:admin] && !each_options[:admin] + if each_options[:admin] + next unless options[:admin] + end Dir.glob("#{root}/**/*") do |f| yield f, ext diff --git a/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 b/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 index 8f1a61976f..5c06c69c52 100644 --- a/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 +++ b/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 @@ -1,16 +1,18 @@ +import Badge from 'discourse/models/badge'; + moduleFor('controller:admin-user-badges', { needs: ['controller:adminUser'] }); test("grantableBadges", function() { - var badge_first = Discourse.Badge.create({id: 3, name: "A Badge"}); - var badge_middle = Discourse.Badge.create({id: 1, name: "My Badge"}); - var badge_last = Discourse.Badge.create({id: 2, name: "Zoo Badge"}); - var controller = this.subject({ badges: [badge_last, badge_first, badge_middle] }); - var sorted_names = [badge_first.name, badge_middle.name, badge_last.name]; - var badge_names = controller.get('grantableBadges').map(function(badge) { + const badgeFirst = Badge.create({id: 3, name: "A Badge"}); + const badgeMiddle = Badge.create({id: 1, name: "My Badge"}); + const badgeLast = Badge.create({id: 2, name: "Zoo Badge"}); + const controller = this.subject({ badges: [badgeLast, badgeFirst, badgeMiddle] }); + const sortedNames = [badgeFirst.name, badgeMiddle.name, badgeLast.name]; + const badgeNames = controller.get('grantableBadges').map(function(badge) { return badge.name; }); - deepEqual(badge_names, sorted_names, "sorts badges by name"); + deepEqual(badgeNames, sortedNames, "sorts badges by name"); }); diff --git a/test/javascripts/models/badge-test.js.es6 b/test/javascripts/models/badge-test.js.es6 index 5087321ee1..a91b49ecd3 100644 --- a/test/javascripts/models/badge-test.js.es6 +++ b/test/javascripts/models/badge-test.js.es6 @@ -1,43 +1,45 @@ -module("Discourse.Badge"); +import Badge from 'discourse/models/badge'; + +module("model:badge"); test('newBadge', function() { - var badge1 = Discourse.Badge.create({name: "New Badge"}), - badge2 = Discourse.Badge.create({id: 1, name: "Old Badge"}); + const badge1 = Badge.create({name: "New Badge"}), + badge2 = Badge.create({id: 1, name: "Old Badge"}); ok(badge1.get('newBadge'), "badges without ids are new"); ok(!badge2.get('newBadge'), "badges with ids are not new"); }); test('displayName', function() { - var badge1 = Discourse.Badge.create({id: 1, name: "Test Badge 1"}); + const badge1 = Badge.create({id: 1, name: "Test Badge 1"}); equal(badge1.get('displayName'), "Test Badge 1", "falls back to the original name in the absence of a translation"); sandbox.stub(I18n, "t").returnsArg(0); - var badge2 = Discourse.Badge.create({id: 2, name: "Test Badge 2"}); + const badge2 = Badge.create({id: 2, name: "Test Badge 2"}); equal(badge2.get('displayName'), "badges.badge.test_badge_2.name", "uses translation when available"); }); test('translatedDescription', function() { - var badge1 = Discourse.Badge.create({id: 1, name: "Test Badge 1", description: "TEST"}); + const badge1 = Badge.create({id: 1, name: "Test Badge 1", description: "TEST"}); equal(badge1.get('translatedDescription'), null, "returns null when no translation exists"); - var badge2 = Discourse.Badge.create({id: 2, name: "Test Badge 2 **"}); + const badge2 = Badge.create({id: 2, name: "Test Badge 2 **"}); sandbox.stub(I18n, "t").returns("description translation"); equal(badge2.get('translatedDescription'), "description translation", "users translated description"); }); test('displayDescription', function() { - var badge1 = Discourse.Badge.create({id: 1, name: "Test Badge 1", description: "TEST"}); + const badge1 = Badge.create({id: 1, name: "Test Badge 1", description: "TEST"}); equal(badge1.get('displayDescription'), "TEST", "returns original description when no translation exists"); - var badge2 = Discourse.Badge.create({id: 2, name: "Test Badge 2 **"}); + const badge2 = Badge.create({id: 2, name: "Test Badge 2 **"}); sandbox.stub(I18n, "t").returns("description translation"); equal(badge2.get('displayDescription'), "description translation", "users translated description"); }); test('createFromJson array', function() { - var badgesJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badges":[{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}]}; + const badgesJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badges":[{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}]}; - var badges = Discourse.Badge.createFromJson(badgesJson); + const badges = Badge.createFromJson(badgesJson); ok(Array.isArray(badges), "returns an array"); equal(badges[0].get('name'), "Badge 1", "badge details are set"); @@ -45,16 +47,16 @@ test('createFromJson array', function() { }); test('createFromJson single', function() { - var badgeJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}}; + const badgeJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}}; - var badge = Discourse.Badge.createFromJson(badgeJson); + const badge = Badge.createFromJson(badgeJson); ok(!Array.isArray(badge), "does not returns an array"); }); test('updateFromJson', function() { - var badgeJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}}; - var badge = Discourse.Badge.create({name: "Badge 1"}); + const badgeJson = {"badge_types":[{"id":6,"name":"Silver 1"}],"badge":{"id":1126,"name":"Badge 1","description":null,"badge_type_id":6}}; + const badge = Badge.create({name: "Badge 1"}); badge.updateFromJson(badgeJson); equal(badge.get('id'), 1126, "id is set"); equal(badge.get('badge_type.name'), "Silver 1", "badge_type reference is set"); @@ -62,7 +64,7 @@ test('updateFromJson', function() { test('save', function() { sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve({})); - var badge = Discourse.Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1}); + 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"); @@ -70,7 +72,7 @@ test('save', function() { test('destroy', function() { sandbox.stub(Discourse, 'ajax'); - var badge = Discourse.Badge.create({name: "New Badge", description: "This is a new badge.", badge_type_id: 1}); + 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); diff --git a/test/javascripts/models/user-badge-test.js.es6 b/test/javascripts/models/user-badge-test.js.es6 index 70942b3958..bf2a5bf98a 100644 --- a/test/javascripts/models/user-badge-test.js.es6 +++ b/test/javascripts/models/user-badge-test.js.es6 @@ -1,10 +1,12 @@ -module("Discourse.UserBadge"); +import UserBadge from 'discourse/models/user-badge'; -var 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}}, +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() { - var userBadge = Discourse.UserBadge.createFromJson(singleBadgeJson); + const userBadge = UserBadge.createFromJson(singleBadgeJson); 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"); @@ -12,7 +14,7 @@ test('createFromJson single', function() { }); test('createFromJson array', function() { - var userBadges = Discourse.UserBadge.createFromJson(multipleBadgesJson); + const userBadges = UserBadge.createFromJson(multipleBadgesJson); ok(Array.isArray(userBadges), "returns an array"); equal(userBadges[0].get('granted_by'), null, "granted_by reference is not set when null"); }); @@ -20,7 +22,7 @@ test('createFromJson array', function() { asyncTestDiscourse('findByUsername', function() { expect(2); sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(multipleBadgesJson)); - Discourse.UserBadge.findByUsername("anne3").then(function(badges) { + UserBadge.findByUsername("anne3").then(function(badges) { ok(Array.isArray(badges), "returns an array"); start(); }); @@ -30,7 +32,7 @@ asyncTestDiscourse('findByUsername', function() { asyncTestDiscourse('findByBadgeId', function() { expect(2); sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(multipleBadgesJson)); - Discourse.UserBadge.findByBadgeId(880).then(function(badges) { + UserBadge.findByBadgeId(880).then(function(badges) { ok(Array.isArray(badges), "returns an array"); start(); }); @@ -40,7 +42,7 @@ asyncTestDiscourse('findByBadgeId', function() { asyncTestDiscourse('grant', function() { expect(2); sandbox.stub(Discourse, 'ajax').returns(Ember.RSVP.resolve(singleBadgeJson)); - Discourse.UserBadge.grant(1, "username").then(function(userBadge) { + UserBadge.grant(1, "username").then(function(userBadge) { ok(!Array.isArray(userBadge), "does not return an array"); start(); }); @@ -49,7 +51,7 @@ asyncTestDiscourse('grant', function() { test('revoke', function() { sandbox.stub(Discourse, 'ajax'); - var userBadge = Discourse.UserBadge.create({id: 1}); + const userBadge = UserBadge.create({id: 1}); userBadge.revoke(); ok(Discourse.ajax.calledOnce, "makes an AJAX call"); }); From 54c0bea294c368eea05e0f2927f30bfed8450461 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Aug 2015 13:27:17 -0400 Subject: [PATCH 146/237] Darken asides on a dark theme. Create a mixin to DRY things up. --- app/assets/stylesheets/common/base/discourse.scss | 3 +-- app/assets/stylesheets/common/base/onebox.scss | 11 +++++------ app/assets/stylesheets/common/base/topic-post.scss | 4 ++-- app/assets/stylesheets/common/foundation/mixins.scss | 7 +++++++ .../stylesheets/common/foundation/variables.scss | 1 + 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index ce6ca6682c..ae6aba12da 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -32,8 +32,7 @@ small { blockquote { - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); - background-color: dark-light-diff($primary, $secondary, 97%, -65%); + @include post-aside; overflow: hidden; clear: both; } diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index 11e3fecf3b..390ac83d52 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -11,10 +11,10 @@ a.loading-onebox { .onebox-result { + @include post-aside; + margin-top: 15px; padding: 12px 25px 12px 12px; - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); - background-color: dark-light-diff($primary, $secondary, 97%, -65%); font-size: 1em; > .source { margin-bottom: 12px; @@ -88,9 +88,9 @@ a.loading-onebox { } aside.onebox { + @include post-aside; + padding: 12px 25px 12px 12px; - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); - background-color: dark-light-diff($primary, $secondary, 97%, -65%); font-size: 1em; header { @@ -147,8 +147,7 @@ aside.onebox .onebox-body .onebox-avatar { blockquote { aside.onebox { - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); - background-color: dark-light-diff($primary, $secondary, 97%, -65%); + @include post-aside; } } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index f0eee220af..cb9565288d 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -50,8 +50,8 @@ aside.quote { .badge-wrapper { margin-left: 5px; } .title { - border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -75%); - background-color: dark-light-diff($primary, $secondary, 97%, -65%); + @include post-aside; + color: scale-color($primary, $lightness: 30%); // IE will screw up the blockquote underneath if bottom padding is 0px padding: 12px 12px 1px 12px; diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index 2243802d16..4b560cc46f 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -93,3 +93,10 @@ -moz-user-select: none; -ms-user-select: none; } + + +// Stuff we repeat +@mixin post-aside { + border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -85%); + background-color: dark-light-diff($primary, $secondary, 97%, -75%); +} diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 0c51ce420a..e4b97ef9ed 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -19,6 +19,7 @@ $twitter: #00bced !default; $yahoo: #810293 !default; $github: #6d6d6d !default; + // Fonts // -------------------------------------------------- From aa36671de3401fbbcccf200b9dd82ac111b32484 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Aug 2015 13:30:16 -0400 Subject: [PATCH 147/237] Lighten code blocks --- app/assets/stylesheets/common/base/topic-post.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index cb9565288d..894e5db08f 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -169,7 +169,7 @@ pre { display: block; padding: 5px 10px; color: $primary; - background: dark-light-diff($primary, $secondary, 97%, -45%); + background: dark-light-diff($primary, $secondary, 97%, -65%); max-height: 500px; } } From 42cb1fcaf6021ff7d689b4bb9518da3cd8d17003 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 19 Aug 2015 14:14:39 -0400 Subject: [PATCH 148/237] FIX: Embed now needs mixins --- app/assets/stylesheets/embed.css.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/assets/stylesheets/embed.css.scss b/app/assets/stylesheets/embed.css.scss index 2f6b1ebf96..d7193d5533 100644 --- a/app/assets/stylesheets/embed.css.scss +++ b/app/assets/stylesheets/embed.css.scss @@ -3,6 +3,7 @@ @import "./common/foundation/variables"; @import "./common/foundation/colors"; +@import "./common/foundation/mixins"; @import "./common/base/onebox"; article.post { From ffb06901193f04d256e5ad69e0faa1d2637d1b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 19 Aug 2015 21:10:12 +0200 Subject: [PATCH 149/237] FIX: edit history navigation issues --- .../discourse/components/d-button.js.es6 | 19 +++-- .../discourse/controllers/history.js.es6 | 80 ++++++++++++------- .../javascripts/discourse/lib/computed.js.es6 | 12 +++ .../discourse/templates/modal/history.hbs | 18 ++--- app/assets/stylesheets/desktop/history.scss | 8 +- 5 files changed, 85 insertions(+), 52 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-button.js.es6 b/app/assets/javascripts/discourse/components/d-button.js.es6 index 45fe5effce..5fcc00a0fc 100644 --- a/app/assets/javascripts/discourse/components/d-button.js.es6 +++ b/app/assets/javascripts/discourse/components/d-button.js.es6 @@ -1,4 +1,5 @@ import { iconHTML } from 'discourse/helpers/fa-icon'; +import computed from 'ember-addons/ember-computed-decorators'; export default Ember.Component.extend({ tagName: 'button', @@ -7,17 +8,15 @@ export default Ember.Component.extend({ noText: Ember.computed.empty('translatedLabel'), - translatedTitle: function() { - const title = this.get('title'); - return title ? I18n.t(title) : this.get('translatedLabel'); - }.property('title', 'translatedLabel'), + @computed("title", "translatedLabel") + translatedTitle(title, translatedLabel) { + return title ? I18n.t(title) : translatedLabel; + }, - translatedLabel: function() { - const label = this.get('label'); - if (label) { - return I18n.t(this.get('label')); - } - }.property('label'), + @computed("label") + translatedLabel(label) { + if (label) return I18n.t(label); + }, render(buffer) { const label = this.get('translatedLabel'), diff --git a/app/assets/javascripts/discourse/controllers/history.js.es6 b/app/assets/javascripts/discourse/controllers/history.js.es6 index 4a04abb1af..b0588750f9 100644 --- a/app/assets/javascripts/discourse/controllers/history.js.es6 +++ b/app/assets/javascripts/discourse/controllers/history.js.es6 @@ -1,6 +1,7 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import computed from 'ember-addons/ember-computed-decorators'; +import { propertyGreaterThan, propertyLessThan } from 'discourse/lib/computed'; // This controller handles displaying of history export default Ember.Controller.extend(ModalFunctionality, { @@ -15,30 +16,28 @@ export default Ember.Controller.extend(ModalFunctionality, { refresh(postId, postVersion) { this.set("loading", true); - var self = this; - Discourse.Post.loadRevision(postId, postVersion).then(function (result) { - self.setProperties({ loading: false, model: result }); + Discourse.Post.loadRevision(postId, postVersion).then(result => { + this.setProperties({ loading: false, model: result }); }); }, hide(postId, postVersion) { - var self = this; - Discourse.Post.hideRevision(postId, postVersion).then(function () { - self.refresh(postId, postVersion); - }); + Discourse.Post.hideRevision(postId, postVersion).then(() => this.refresh(postId, postVersion)); }, show(postId, postVersion) { - var self = this; - Discourse.Post.showRevision(postId, postVersion).then(function () { - self.refresh(postId, postVersion); - }); + Discourse.Post.showRevision(postId, postVersion).then(() => this.refresh(postId, postVersion)); }, - createdAtDate: function() { return moment(this.get("created_at")).format("LLLL"); }.property("created_at"), + @computed('model.created_at') + createdAtDate(createdAt) { + return moment(createdAt).format("LLLL"); + }, @computed('model.current_version') - previousVersion(current) { return current - 1; }, + previousVersion(current) { + return current - 1; + }, @computed('model.current_revision', 'model.previous_revision') displayGoToPrevious(current, prev) { @@ -46,18 +45,28 @@ export default Ember.Controller.extend(ModalFunctionality, { }, displayRevisions: Ember.computed.gt("model.version_count", 2), - displayGoToFirst: Ember.computed.gt('model.current_revision', 'model.first_revision'), - displayGoToNext: Ember.computed.lt("model.current_revision", "model.next_revision"), - displayGoToLast: Ember.computed.lt("model.current_revision", "model.next_revision"), + displayGoToFirst: propertyGreaterThan("model.current_revision", "model.first_revision"), + displayGoToNext: propertyLessThan("model.current_revision", "model.next_revision"), + displayGoToLast: propertyLessThan("model.current_revision", "model.next_revision"), - @computed('model.previous_hidden', 'loading') - displayShow: function(prevHidden, loading) { - return prevHidden && this.currentUser.get('staff') && !loading; + hideGoToFirst: Ember.computed.not("displayGoToFirst"), + hideGoToPrevious: Ember.computed.not("displayGoToPrevious"), + hideGoToNext: Ember.computed.not("displayGoToNext"), + hideGoToLast: Ember.computed.not("displayGoToLast"), + + loadFirstDisabled: Ember.computed.or("loading", "hideGoToFirst"), + loadPreviousDisabled: Ember.computed.or("loading", "hideGoToPrevious"), + loadNextDisabled: Ember.computed.or("loading", "hideGoToNext"), + loadLastDisabled: Ember.computed.or("loading", "hideGoToLast"), + + @computed('model.previous_hidden') + displayShow(prevHidden) { + return prevHidden && this.currentUser.get('staff'); }, - @computed('model.previous_hidden', 'loading') - displayHide: function(prevHidden, loading) { - return !prevHidden && this.currentUser.get('staff') && !loading; + @computed('model.previous_hidden') + displayHide(prevHidden) { + return !prevHidden && this.currentUser.get('staff'); }, isEitherRevisionHidden: Ember.computed.or("model.previous_hidden", "model.current_hidden"), @@ -78,6 +87,15 @@ export default Ember.Controller.extend(ModalFunctionality, { displayingSideBySide: Em.computed.equal("viewMode", "side_by_side"), displayingSideBySideMarkdown: Em.computed.equal("viewMode", "side_by_side_markdown"), + @computed("displayingInline") + inlineClass(displayingInline) { return displayingInline ? "btn-primary" : ""; }, + + @computed("displayingSideBySide") + sideBySideClass(displayingSideBySide) { return displayingSideBySide ? "btn-primary" : ""; }, + + @computed("displayingSideBySideMarkdown") + sideBySideMarkdownClass(displayingSideBySideMarkdown) { return displayingSideBySideMarkdown ? "btn-primary" : ""; }, + @computed('model.category_id_changes') previousCategory(changes) { if (changes) { @@ -116,16 +134,16 @@ export default Ember.Controller.extend(ModalFunctionality, { }, actions: { - loadFirstVersion: function() { this.refresh(this.get("model.post_id"), this.get("model.first_revision")); }, - loadPreviousVersion: function() { this.refresh(this.get("model.post_id"), this.get("model.previous_revision")); }, - loadNextVersion: function() { this.refresh(this.get("model.post_id"), this.get("model.next_revision")); }, - loadLastVersion: function() { this.refresh(this.get("model.post_id"), this.get("model.last_revision")); }, + loadFirstVersion() { this.refresh(this.get("model.post_id"), this.get("model.first_revision")); }, + loadPreviousVersion() { this.refresh(this.get("model.post_id"), this.get("model.previous_revision")); }, + loadNextVersion() { this.refresh(this.get("model.post_id"), this.get("model.next_revision")); }, + loadLastVersion() { this.refresh(this.get("model.post_id"), this.get("model.last_revision")); }, - hideVersion: function() { this.hide(this.get("model.post_id"), this.get("model.current_revision")); }, - showVersion: function() { this.show(this.get("model.post_id"), this.get("model.current_revision")); }, + hideVersion() { this.hide(this.get("model.post_id"), this.get("model.current_revision")); }, + showVersion() { this.show(this.get("model.post_id"), this.get("model.current_revision")); }, - displayInline: function() { this.set("viewMode", "inline"); }, - displaySideBySide: function() { this.set("viewMode", "side_by_side"); }, - displaySideBySideMarkdown: function() { this.set("viewMode", "side_by_side_markdown"); } + displayInline() { this.set("viewMode", "inline"); }, + displaySideBySide() { this.set("viewMode", "side_by_side"); }, + displaySideBySideMarkdown() { this.set("viewMode", "side_by_side_markdown"); } } }); diff --git a/app/assets/javascripts/discourse/lib/computed.js.es6 b/app/assets/javascripts/discourse/lib/computed.js.es6 index b1b7186b0e..1fb7e745cb 100644 --- a/app/assets/javascripts/discourse/lib/computed.js.es6 +++ b/app/assets/javascripts/discourse/lib/computed.js.es6 @@ -26,6 +26,18 @@ export function propertyNotEqual(p1, p2) { }).property(p1, p2); } +export function propertyGreaterThan(p1, p2) { + return Ember.computed(function() { + return this.get(p1) > this.get(p2); + }).property(p1, p2); +} + +export function propertyLessThan(p1, p2) { + return Ember.computed(function() { + return this.get(p1) < this.get(p2); + }).property(p1, p2); +} + /** Returns i18n version of a string based on a property. diff --git a/app/assets/javascripts/discourse/templates/modal/history.hbs b/app/assets/javascripts/discourse/templates/modal/history.hbs index cd790800c1..bcf20fbc1c 100644 --- a/app/assets/javascripts/discourse/templates/modal/history.hbs +++ b/app/assets/javascripts/discourse/templates/modal/history.hbs @@ -1,27 +1,27 @@ diff --git a/app/assets/javascripts/discourse/views/choose-topic.js.es6 b/app/assets/javascripts/discourse/views/choose-topic.js.es6 index 7dbd8e4248..da4cbac2f4 100644 --- a/app/assets/javascripts/discourse/views/choose-topic.js.es6 +++ b/app/assets/javascripts/discourse/views/choose-topic.js.es6 @@ -5,14 +5,16 @@ export default Ember.View.extend({ templateName: 'choose_topic', topicTitleChanged: function() { - this.set('loading', true); - this.set('noResults', true); - this.set('selectedTopicId', null); + this.setProperties({ + loading: true, + noResults: true, + selectedTopicId: null, + }); this.search(this.get('topicTitle')); }.observes('topicTitle'), topicsChanged: function() { - var topics = this.get('topics'); + const topics = this.get('topics'); if (topics) { this.set('noResults', topics.length === 0); } @@ -20,14 +22,17 @@ export default Ember.View.extend({ }.observes('topics'), search: debounce(function(title) { - var self = this; + const self = this, + currentTopicId = this.get("currentTopicId"); + if (Em.isEmpty(title)) { self.setProperties({ topics: null, loading: false }); return; } - searchForTerm(title, {typeFilter: 'topic', searchForId: true}).then(function (results) { + + searchForTerm(title, { typeFilter: 'topic', searchForId: true }).then(function (results) { if (results && results.posts && results.posts.length > 0) { - self.set('topics', results.posts.mapBy('topic')); + self.set('topics', results.posts.mapBy('topic').filter(t => t.get("id") !== currentTopicId)); } else { self.setProperties({ topics: null, loading: false }); } @@ -36,7 +41,7 @@ export default Ember.View.extend({ actions: { chooseTopic: function (topic) { - var topicId = Em.get(topic, 'id'); + const topicId = Em.get(topic, 'id'); this.set('selectedTopicId', topicId); Em.run.next(function () { diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 7430d9f410..4446a0af99 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -12,13 +12,12 @@ class SearchController < ApplicationController search = Search.new(params[:q], type_filter: 'topic', guardian: guardian, include_blurbs: true, blurb_length: 300) result = search.execute - serializer = serialize_data(result, GroupedSearchResultSerializer, :result => result) + serializer = serialize_data(result, GroupedSearchResultSerializer, result: result) respond_to do |format| format.html do store_preloaded("search", MultiJson.dump(serializer)) end - format.json do render_json_dump(serializer) end @@ -29,14 +28,14 @@ class SearchController < ApplicationController def query params.require(:term) - search_args = {guardian: guardian} - search_args[:type_filter] = params[:type_filter] if params[:type_filter].present? - if params[:include_blurbs].present? - search_args[:include_blurbs] = params[:include_blurbs] == "true" - end - search_args[:search_for_id] = true if params[:search_for_id].present? + search_args = { guardian: guardian } + + search_args[:type_filter] = params[:type_filter] if params[:type_filter].present? + search_args[:include_blurbs] = params[:include_blurbs] == "true" if params[:include_blurbs].present? + search_args[:search_for_id] = true if params[:search_for_id].present? search_context = params[:search_context] + if search_context.present? raise Discourse::InvalidParameters.new(:search_context) unless SearchController.valid_context_types.include?(search_context[:type]) raise Discourse::InvalidParameters.new(:search_context) if search_context[:id].blank? @@ -60,7 +59,7 @@ class SearchController < ApplicationController search = Search.new(params[:term], search_args.symbolize_keys) result = search.execute - render_serialized(result, GroupedSearchResultSerializer, :result => result) + render_serialized(result, GroupedSearchResultSerializer, result: result) end end From 9ae9aed01068d90bf337f91e871f9dfe56b8a969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 19 Aug 2015 22:40:20 +0200 Subject: [PATCH 152/237] FIX: change **default** notification state when a topic is recategorized within 5 days of creation --- app/models/category_user.rb | 47 +++++++++++++++++++++---------- app/models/topic.rb | 4 +-- spec/models/category_user_spec.rb | 17 ++++++++++- 3 files changed, 50 insertions(+), 18 deletions(-) diff --git a/app/models/category_user.rb b/app/models/category_user.rb index d53fb804e2..1fb537f546 100644 --- a/app/models/category_user.rb +++ b/app/models/category_user.rb @@ -15,18 +15,19 @@ class CategoryUser < ActiveRecord::Base TopicUser.notification_levels end - def self.auto_track_new_topic(topic) - apply_default_to_topic(topic, - TopicUser.notification_levels[:tracking], - TopicUser.notification_reasons[:auto_track_category] - ) - end + %w{watch track}.each do |s| + define_singleton_method("auto_#{s}_new_topic") do |topic, new_category=nil| + category_id = topic.category_id - def self.auto_watch_new_topic(topic) - apply_default_to_topic(topic, - TopicUser.notification_levels[:watching], - TopicUser.notification_reasons[:auto_watch_category] - ) + if new_category && topic.created_at > 5.days.ago + # we want to apply default of the new category + category_id = new_category.id + # remove defaults from previous category + remove_default_from_topic(topic.id, TopicUser.notification_levels[:"#{s}ing"], TopicUser.notification_reasons[:"auto_#{s}_category"]) + end + + apply_default_to_topic(topic.id, category_id, TopicUser.notification_levels[:"#{s}ing"], TopicUser.notification_reasons[:"auto_#{s}_category"]) + end end def self.batch_set(user, level, category_ids) @@ -56,7 +57,7 @@ class CategoryUser < ActiveRecord::Base end end - def self.apply_default_to_topic(topic, level, reason) + def self.apply_default_to_topic(topic_id, category_id, level, reason) # Can not afford to slow down creation of topics when a pile of users are watching new topics, reverting to SQL for max perf here sql = <<-SQL INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id) @@ -68,14 +69,30 @@ class CategoryUser < ActiveRecord::Base SQL exec_sql(sql, - topic_id: topic.id, - category_id: topic.category_id, + topic_id: topic_id, + category_id: category_id, level: level, reason: reason ) end - private_class_method :apply_default_to_topic + def self.remove_default_from_topic(topic_id, level, reason) + sql = <<-SQL + DELETE FROM topic_users + WHERE topic_id = :topic_id + AND notifications_changed_at IS NULL + AND notification_level = :level + AND notifications_reason_id = :reason + SQL + + exec_sql(sql, + topic_id: topic_id, + level: level, + reason: reason + ) + end + + private_class_method :apply_default_to_topic, :remove_default_from_topic end # == Schema Information diff --git a/app/models/topic.rb b/app/models/topic.rb index 30bd8fc343..297f840be3 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -482,8 +482,8 @@ class Topic < ActiveRecord::Base Category.where(id: new_category.id).update_all("topic_count = topic_count + 1") CategoryFeaturedTopic.feature_topics_for(old_category) unless @import_mode CategoryFeaturedTopic.feature_topics_for(new_category) unless @import_mode || old_category.id == new_category.id - CategoryUser.auto_watch_new_topic(self) - CategoryUser.auto_track_new_topic(self) + CategoryUser.auto_watch_new_topic(self, new_category) + CategoryUser.auto_track_new_topic(self, new_category) end true diff --git a/spec/models/category_user_spec.rb b/spec/models/category_user_spec.rb index 749368e64e..3492713999 100644 --- a/spec/models/category_user_spec.rb +++ b/spec/models/category_user_spec.rb @@ -52,8 +52,8 @@ describe CategoryUser do end it "watches categories that have been changed" do - watched_category = Fabricate(:category) user = Fabricate(:user) + watched_category = Fabricate(:category) CategoryUser.create!(user: user, category: watched_category, notification_level: CategoryUser.notification_levels[:watching]) post = create_post @@ -65,6 +65,21 @@ describe CategoryUser do expect(tu.notification_level).to eq TopicUser.notification_levels[:watching] end + it "unwatches categories that have been changed" do + user = Fabricate(:user) + watched_category = Fabricate(:category) + CategoryUser.create!(user: user, category: watched_category, notification_level: CategoryUser.notification_levels[:watching]) + + post = create_post(category: watched_category) + tu = TopicUser.get(post.topic, user) + expect(tu.notification_level).to eq TopicUser.notification_levels[:watching] + + # Now, change the topic's category + unwatched_category = Fabricate(:category) + post.topic.change_category_to_id(unwatched_category.id) + expect(TopicUser.get(post.topic, user)).to be_blank + end + end end From ee804f608f7335b04d82c3e74daa7a3382bad74a Mon Sep 17 00:00:00 2001 From: kerryliu Date: Tue, 18 Aug 2015 17:28:30 -0700 Subject: [PATCH 153/237] spoiler tag uses replaceBBCode instead of rawBBCode for emoji and text formatting support. --- .../javascripts/discourse/dialects/bbcode_dialect.js | 8 +------- test/javascripts/lib/bbcode-test.js.es6 | 5 ++++- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js index 4a138a7d83..816ed3934b 100644 --- a/app/assets/javascripts/discourse/dialects/bbcode_dialect.js +++ b/app/assets/javascripts/discourse/dialects/bbcode_dialect.js @@ -133,16 +133,10 @@ Discourse.Markdown.whiteListTag('span', 'class', /^bbcode-[bius]$/); Discourse.BBCode.replaceBBCode('ul', function(contents) { return ['ul'].concat(Discourse.BBCode.removeEmptyLines(contents)); }); Discourse.BBCode.replaceBBCode('ol', function(contents) { return ['ol'].concat(Discourse.BBCode.removeEmptyLines(contents)); }); Discourse.BBCode.replaceBBCode('li', function(contents) { return ['li'].concat(Discourse.BBCode.removeEmptyLines(contents)); }); +Discourse.BBCode.replaceBBCode('spoiler', function(contents) { return ['span', {'class': 'spoiler'}].concat(contents); }); Discourse.BBCode.rawBBCode('img', function(contents) { return ['img', {href: contents}]; }); Discourse.BBCode.rawBBCode('email', function(contents) { return ['a', {href: "mailto:" + contents, 'data-bbcode': true}, contents]; }); -Discourse.BBCode.rawBBCode('spoiler', function(contents) { - if (/it's a sled", "supports spoiler tags on text"); format("[spoiler][/spoiler]", - "
    ", "supports spoiler tags on images"); + "", "supports spoiler tags on images"); + format("[spoiler] This is the **bold** :smiley: [/spoiler]", " This is the bold \"smiley\" ", "supports spoiler tags on emojis"); + format("[spoiler] Why not both ?[/spoiler]", " Why not both ?", "supports images and text"); + format("In a p tag a spoiler [spoiler] [/spoiler] can work.", "In a p tag a spoiler can work.", "supports images and text in a p tag"); }); test('lists', function() { From 9d28518ef58eed003faaf6d94bf55203a1257d4c Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Aug 2015 13:57:07 +1000 Subject: [PATCH 154/237] logster favicon and title --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 59aaea207c..40cf290b74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (1.0.0.1.pre) + logster (1.0.0.2.pre) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From 803484f1f7c31fc841f4f33bad36298c68a589f4 Mon Sep 17 00:00:00 2001 From: Sam Date: Thu, 20 Aug 2015 14:54:28 +1000 Subject: [PATCH 155/237] bump logster --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 40cf290b74..0ba09f4ca8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -148,7 +148,7 @@ GEM thor (~> 0.15) libv8 (3.16.14.7) listen (0.7.3) - logster (1.0.0.2.pre) + logster (1.0.0.3.pre) lru_redux (1.1.0) mail (2.5.4) mime-types (~> 1.16) From 7554b5e3c458dc79b591d5e874e79ab10cf30c2f Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 20 Aug 2015 01:29:12 -0700 Subject: [PATCH 156/237] different button color for dark themes --- app/assets/stylesheets/desktop/topic-post.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 736eb7ff60..3e89870d83 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -81,7 +81,7 @@ nav.post-controls { color: scale-color($primary, $lightness: 60%); } a, button { - color: scale-color($primary, $lightness: 75%); + color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); margin-right: 2px; display: inline-block; } From d38c4d5f7443223c81529c22470293771baf9f38 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Thu, 20 Aug 2015 02:42:12 -0700 Subject: [PATCH 157/237] scale-color $lightness must use $secondary for dark themes --- .../stylesheets/common/admin/admin_base.scss | 16 +++++----- .../stylesheets/common/base/_topic-list.scss | 12 +++---- .../stylesheets/common/base/discourse.scss | 6 ++-- .../stylesheets/common/base/header.scss | 10 +++--- app/assets/stylesheets/common/base/login.scss | 4 +-- .../common/base/notification-options.scss | 2 +- .../stylesheets/common/base/search.scss | 6 ++-- .../stylesheets/common/base/share_link.scss | 2 +- .../stylesheets/common/base/topic-post.scss | 12 +++---- .../stylesheets/common/base/user-badges.scss | 4 +-- app/assets/stylesheets/common/base/user.scss | 6 ++-- .../common/components/badges.css.scss | 4 +-- .../common/components/buttons.css.scss | 2 +- app/assets/stylesheets/desktop/compose.scss | 2 +- app/assets/stylesheets/desktop/header.scss | 4 +-- app/assets/stylesheets/desktop/login.scss | 4 +-- app/assets/stylesheets/desktop/modal.scss | 12 +++---- .../stylesheets/desktop/queued-posts.scss | 2 +- .../stylesheets/desktop/topic-list.scss | 16 +++++----- .../stylesheets/desktop/topic-post.scss | 32 +++++++++---------- app/assets/stylesheets/desktop/topic.scss | 4 +-- app/assets/stylesheets/desktop/upload.scss | 2 +- app/assets/stylesheets/desktop/user.scss | 12 +++---- app/assets/stylesheets/embed.css.scss | 2 +- app/assets/stylesheets/mobile/compose.scss | 8 ++--- app/assets/stylesheets/mobile/login.scss | 4 +-- app/assets/stylesheets/mobile/modal.scss | 2 +- app/assets/stylesheets/mobile/topic-list.scss | 8 ++--- app/assets/stylesheets/mobile/topic-post.scss | 16 +++++----- app/assets/stylesheets/mobile/topic.scss | 2 +- app/assets/stylesheets/mobile/upload.scss | 2 +- app/assets/stylesheets/mobile/user.scss | 10 +++--- 32 files changed, 115 insertions(+), 115 deletions(-) diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 33da77e84c..a988f54389 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -382,7 +382,7 @@ td.flaggers td { } .desc { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } h3 { @@ -503,7 +503,7 @@ section.details { p.help { margin: 0; - color: scale-color($primary, $lightness: 40%); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); font-size: 0.9em; } } @@ -521,7 +521,7 @@ section.details { .current-badge-actions { margin: 10px; padding: 10px; - border-top: 1px solid scale-color($primary, $lightness: 80%); + border-top: 1px solid dark-light-choose(scale-color($primary, $lightness: 80%), scale-color($secondary, $lightness: 20%)); } .buttons { @@ -574,7 +574,7 @@ section.details { .groups { .ac-wrap { width: 100% !important; - border-color: scale-color($primary, $lightness: 75%); + border-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); .item { width: 190px; margin-right: 0 !important; @@ -597,7 +597,7 @@ section.details { } .select2-choices { width: 100%; - border-color: scale-color($primary, $lightness: 75%); + border-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } } @@ -723,7 +723,7 @@ section.details { td.actions { width: 200px; } .hex-input { width: 80px; margin-bottom: 0; } .hex { text-align: center; } - .description { color: scale-color($primary, $lightness: 50%); } + .description { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } .invalid .hex input { background-color: white; @@ -1291,7 +1291,7 @@ table.api-keys { margin: 0 0 20px 6px; a.filter { display: inline-block; - background-color: scale-color($primary, $lightness: 75%); + background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); padding: 3px 10px; border-radius: 3px; @@ -1328,7 +1328,7 @@ table.api-keys { .staff-actions, .screened-emails, .screened-urls, .screened-ip-addresses, .permalinks { - border-bottom: dotted 1px scale-color($primary, $lightness: 75%); + border-bottom: dotted 1px dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); .heading-container { width: 100%; diff --git a/app/assets/stylesheets/common/base/_topic-list.scss b/app/assets/stylesheets/common/base/_topic-list.scss index 6a89dae2cf..71a5ef8054 100644 --- a/app/assets/stylesheets/common/base/_topic-list.scss +++ b/app/assets/stylesheets/common/base/_topic-list.scss @@ -45,14 +45,14 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-ligh } th { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-weight: normal; font-size: 1em; - button i.fa {color: scale-color($primary, $lightness: 50%);} + button i.fa {color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%));} } td { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-size: 1em; } @@ -66,7 +66,7 @@ html.anon .topic-list a.title:visited:not(.badge-notification) {color: dark-ligh .topic-excerpt { font-size: 0.929em; margin-top: 8px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); word-wrap: break-word; line-height: 1.4; padding-right: 20px; @@ -243,7 +243,7 @@ ol.category-breadcrumb { margin: 5px 0 10px; .top-date-string { - color: scale-color($primary, $lightness: 60%); + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); font-weight: normal; font-size: 0.7em; text-transform: uppercase; @@ -298,5 +298,5 @@ ol.category-breadcrumb { } div.education { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index ae6aba12da..c8cedb4a6c 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -73,13 +73,13 @@ body { // is scale-color($primary, $lightness: 50%) // numbers get dimmer as they get colder .coldmap-high { - color: scale-color($primary, $lightness: 70%) !important; + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)) !important; } .coldmap-med { - color: scale-color($primary, $lightness: 60%) !important; + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)) !important; } .coldmap-low { - color: scale-color($primary, $lightness: 50%) !important; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)) !important; } .heatmap-high { color: #fe7a15 !important; diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index 574d2744d6..3126899315 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -150,7 +150,7 @@ // note these topic counts only appear for anons in the category hamburger drop down b.topics-count { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-weight: normal; font-size: 11px; } @@ -192,8 +192,8 @@ // Notifications &#notifications-dropdown { - .fa { color: scale-color($primary, $lightness: 50%); } - .icon { color: scale-color($primary, $lightness: 30%); } + .fa { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } + .icon { color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); } li { background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); i { @@ -298,7 +298,7 @@ margin: 5px 5px 0 5px; .box {margin-top: 0;} .badge-notification { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); background-color: transparent; vertical-align: top; padding: 5px 5px 2px 5px; @@ -329,7 +329,7 @@ .topic-statuses { float: none; display: inline-block; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin: 0; .fa { margin: 0; diff --git a/app/assets/stylesheets/common/base/login.scss b/app/assets/stylesheets/common/base/login.scss index 6d30268e12..f66944b9d2 100644 --- a/app/assets/stylesheets/common/base/login.scss +++ b/app/assets/stylesheets/common/base/login.scss @@ -36,7 +36,7 @@ float: auto; } p { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin: 0; } } @@ -51,5 +51,5 @@ button#login-link, button#new-account-link background: transparent; padding-left: 0; margin-left: 20px; - color: scale-color($primary, $lightness: 35%); + color: dark-light-choose(scale-color($primary, $lightness: 35%), scale-color($secondary, $lightness: 65%)); } diff --git a/app/assets/stylesheets/common/base/notification-options.scss b/app/assets/stylesheets/common/base/notification-options.scss index c1d52c3b74..745f0aa650 100644 --- a/app/assets/stylesheets/common/base/notification-options.scss +++ b/app/assets/stylesheets/common/base/notification-options.scss @@ -1,5 +1,5 @@ .fa.muted { - color: scale-color($primary, $lightness: 40%); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); } .fa.tracking, .fa.watching { color: $tertiary; diff --git a/app/assets/stylesheets/common/base/search.scss b/app/assets/stylesheets/common/base/search.scss index 75de8a32a8..ec75223b5d 100644 --- a/app/assets/stylesheets/common/base/search.scss +++ b/app/assets/stylesheets/common/base/search.scss @@ -28,13 +28,13 @@ line-height: 20px; word-wrap: break-word; clear: both; - color: scale-color($primary, $lightness: 40%); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); .date { - color: scale-color($primary, $lightness: 40%); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); } .search-highlight { - color: scale-color($primary, $lightness: 25%); + color: dark-light-choose(scale-color($primary, $lightness: 25%), scale-color($secondary, $lightness: 75%)); } } } diff --git a/app/assets/stylesheets/common/base/share_link.scss b/app/assets/stylesheets/common/base/share_link.scss index 49f56f747e..25f2fdfdc4 100644 --- a/app/assets/stylesheets/common/base/share_link.scss +++ b/app/assets/stylesheets/common/base/share_link.scss @@ -59,7 +59,7 @@ .date { float: right; margin: 5px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } input[type=text] { diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 894e5db08f..c655b6d055 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -12,16 +12,16 @@ overflow: hidden; text-overflow: ellipsis; a { - color: scale-color($primary, $lightness: 30%); + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); } } .fa { font-size: 11px; margin-left: 3px; - color: scale-color($primary, $lightness: 40%); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); } .new_user a, .user-title, .user-title a { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } @@ -52,7 +52,7 @@ aside.quote { .title { @include post-aside; - color: scale-color($primary, $lightness: 30%); + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); // IE will screw up the blockquote underneath if bottom padding is 0px padding: 12px 12px 1px 12px; // blockquote is underneath this and has top margin @@ -68,7 +68,7 @@ aside.quote { } .quote-controls, .quote-controls .back:before, .quote-controls .quote-other-topic:before { - color: scale-color($primary, $lightness: 70%); + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } .cooked .highlight { @@ -156,7 +156,7 @@ aside.quote { color: $wiki; } &.via-email { - color: scale-color($primary, $lightness: 70%); + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } &.raw-email { cursor: pointer; diff --git a/app/assets/stylesheets/common/base/user-badges.scss b/app/assets/stylesheets/common/base/user-badges.scss index e1bd5d0862..9f812fa899 100644 --- a/app/assets/stylesheets/common/base/user-badges.scss +++ b/app/assets/stylesheets/common/base/user-badges.scss @@ -59,7 +59,7 @@ .count { display: block; font-size: 0.8em; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } } @@ -94,7 +94,7 @@ table.badges-listing { td.grant-count { text-align: center; - color: scale-color($primary, $lightness: 40%); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); font-size: 120%; } diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index cbca0bece7..5843e84cae 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -116,17 +116,17 @@ .username a { font-weight: bold; - color: scale-color($primary, $lightness: 30%); + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); } .name { margin-left: 5px; - color: scale-color($primary, $lightness: 30%); + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); } .title { margin-top: 3px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 9b5248091f..0a03ef0aa0 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -236,7 +236,7 @@ font-size: 11px; line-height: 1; text-align: center; - background-color: scale-color($primary, $lightness: 70%); + background-color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); &[href] { color: $secondary; } @@ -282,7 +282,7 @@ font-size: 1em; line-height: 1; &[href] { - color: scale-color($primary, $lightness: 40%); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); } } diff --git a/app/assets/stylesheets/common/components/buttons.css.scss b/app/assets/stylesheets/common/components/buttons.css.scss index 2ea0e60a38..af8f0df24a 100644 --- a/app/assets/stylesheets/common/components/buttons.css.scss +++ b/app/assets/stylesheets/common/components/buttons.css.scss @@ -57,7 +57,7 @@ } &[disabled] { background: dark-light-diff($primary, $secondary, 90%, -60%); - &:hover { color: scale-color($primary, $lightness: 70%); } + &:hover { color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } cursor: not-allowed; } } diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 4f23d9378d..829387d9d1 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -337,7 +337,7 @@ margin-bottom: 10px; i { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } diff --git a/app/assets/stylesheets/desktop/header.scss b/app/assets/stylesheets/desktop/header.scss index 3caae9f485..74416dcf18 100644 --- a/app/assets/stylesheets/desktop/header.scss +++ b/app/assets/stylesheets/desktop/header.scss @@ -50,13 +50,13 @@ and (max-width : 570px) { } .search-link .blurb { - color: scale-color($primary, $lightness: 45%); + color: dark-light-choose(scale-color($primary, $lightness: 45%), scale-color($secondary, $lightness: 55%)); display: block; word-wrap: break-word; font-size: 11px; line-height: 1.3em; .search-highlight { - color: scale-color($primary, $lightness: 25%); + color: dark-light-choose(scale-color($primary, $lightness: 25%), scale-color($secondary, $lightness: 75%)); } } diff --git a/app/assets/stylesheets/desktop/login.scss b/app/assets/stylesheets/desktop/login.scss index 39c86a70e6..1bf2a7988a 100644 --- a/app/assets/stylesheets/desktop/login.scss +++ b/app/assets/stylesheets/desktop/login.scss @@ -14,7 +14,7 @@ #login-form { a { - color: scale-color($primary, $lightness: 35%); + color: dark-light-choose(scale-color($primary, $lightness: 35%), scale-color($secondary, $lightness: 65%)); } } @@ -45,7 +45,7 @@ tr.instructions { label { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } diff --git a/app/assets/stylesheets/desktop/modal.scss b/app/assets/stylesheets/desktop/modal.scss index c9d5b84c38..77e4ea12e7 100644 --- a/app/assets/stylesheets/desktop/modal.scss +++ b/app/assets/stylesheets/desktop/modal.scss @@ -47,7 +47,7 @@ } .modal-footer span.hint { - color: scale-color($primary, $lightness: 70%); + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); float: right; line-height: 30px; } @@ -64,7 +64,7 @@ float: right; font-size: 1.429em; text-decoration: none; - color: scale-color($primary, $lightness: 35%); + color: dark-light-choose(scale-color($primary, $lightness: 35%), scale-color($secondary, $lightness: 65%)); cursor: pointer; &:hover { color: $primary; @@ -91,7 +91,7 @@ .custom-message-length { margin: -10px 0 10px 20px; - color: scale-color($primary, $lightness: 70%); + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); font-size: 85%; } @@ -114,11 +114,11 @@ li { margin: 0 4px 8px 0; a { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); cursor: pointer; } a:hover { - color: scale-color($primary, $lightness: 40%); + color: dark-light-choose(scale-color($primary, $lightness: 40%), scale-color($secondary, $lightness: 60%)); } } } @@ -128,7 +128,7 @@ float: right; text-align: right; max-width: 380px; - color: scale-color($primary, $lightness: 60%); + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } } diff --git a/app/assets/stylesheets/desktop/queued-posts.scss b/app/assets/stylesheets/desktop/queued-posts.scss index 1fdf5b30fb..688e85f3c8 100644 --- a/app/assets/stylesheets/desktop/queued-posts.scss +++ b/app/assets/stylesheets/desktop/queued-posts.scss @@ -11,7 +11,7 @@ float: right; font-size: 0.929em; margin-top: 1px; - span {color: scale-color($primary, $lightness: 50%);} + span {color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } .cooked { diff --git a/app/assets/stylesheets/desktop/topic-list.scss b/app/assets/stylesheets/desktop/topic-list.scss index 8d3a880b1c..c5391f26a9 100644 --- a/app/assets/stylesheets/desktop/topic-list.scss +++ b/app/assets/stylesheets/desktop/topic-list.scss @@ -36,10 +36,10 @@ .topic-list { margin: 0 0 10px; - .fa-thumb-tack { color: scale-color($primary, $lightness: 50%); } - .fa-thumb-tack.unpinned { color: scale-color($primary, $lightness: 50%); } + .fa-thumb-tack { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } + .fa-thumb-tack.unpinned { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } a.title {color: $primary;} - .fa-bookmark { color: scale-color($primary, $lightness: 50%); } + .fa-bookmark { color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } th, td { padding: 12px 5px; @@ -51,7 +51,7 @@ } } th { - button i.fa {color: scale-color($primary, $lightness: 50%);} + button i.fa {color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } > tbody > tr { @@ -132,7 +132,7 @@ .post-actions { clear: both; width: auto; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); text-align: left; font-size: 12px; margin-top: 5px; @@ -141,7 +141,7 @@ } a { font-size: 11px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin-right: 3px; line-height: 20px; } @@ -220,11 +220,11 @@ margin: 10px 0 0; /* topic status glyphs */ i { - color: scale-color($primary, $lightness: 50%) !important; + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)) !important; font-size: 0.929em; } a.last-posted-at, a.last-posted-at:visited { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-size: 0.88em; } .badge { diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 3e89870d83..2a97d2dc9e 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -31,7 +31,7 @@ h1 .topic-statuses .topic-status i { font-size: 0.929em; float: right; margin: 1px 25px 0 0; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } .gutter { @@ -78,7 +78,7 @@ nav.post-controls { padding: 0; .highlight-action { - color: scale-color($primary, $lightness: 60%); + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } a, button { color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); @@ -110,7 +110,7 @@ nav.post-controls { .show-replies { margin-left: -10px; font-size: inherit; - span.badge-posts {color: scale-color($primary, $lightness: 60%);} + span.badge-posts {color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } &:hover { background: dark-light-diff($primary, $secondary, 90%, -65%); span.badge-posts {color: $primary;} @@ -123,7 +123,7 @@ nav.post-controls { button.create { margin-right: 0; - color: scale-color($primary, $lightness: 20%); + color: dark-light-choose(scale-color($primary, $lightness: 20%), scale-color($secondary, $lightness: 80%)); margin-left: 10px; } @@ -275,7 +275,7 @@ nav.post-controls { padding-right: 0; } - .post-date { color: scale-color($primary, $lightness: 60%); } + .post-date { color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } .fa-arrow-up, .fa-arrow-down { margin-left: 5px; } .reply:first-of-type .row { border-top: none; } @@ -288,10 +288,10 @@ nav.post-controls { font-size: 0.929em; a { font-weight: bold; - color: scale-color($primary, $lightness: 30%); + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } } - .arrow {color: scale-color($primary, $lightness: 60%);} + .arrow {color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); } } .post-action { @@ -318,7 +318,7 @@ a.star { h3 { margin-bottom: 4px; - color: scale-color($primary, $lightness: 20%); + color: dark-light-choose(scale-color($primary, $lightness: 20%), scale-color($secondary, $lightness: 80%)); line-height: 23px; font-weight: normal; font-size: 1em; @@ -326,7 +326,7 @@ a.star { h4 { margin: 1px 0 2px 0; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-weight: normal; font-size: 0.857em; line-height: 15px; @@ -339,7 +339,7 @@ a.star { span.domain { font-size: 0.714em; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } .avatars { @@ -379,7 +379,7 @@ a.star { line-height: 20px; } .number, i { - color: scale-color($primary, $lightness: 20%); + color: dark-light-choose(scale-color($primary, $lightness: 20%), scale-color($secondary, $lightness: 80%)); font-size: 130%; } .avatar a { @@ -413,7 +413,7 @@ a.star { .btn { border: 0; padding: 0 23px; - color: scale-color($primary, $lightness: 60%); + color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); background: dark-light-diff($primary, $secondary, 97%, -75%); border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); @@ -893,8 +893,8 @@ $topic-avatar-width: 45px; button { margin-left: 8px; - background-color: scale-color($primary, $lightness: 70%); - border: 1px solid scale-color($primary, $lightness: 60%); + background-color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); + border: 1px solid dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); color: $primary; } } @@ -938,7 +938,7 @@ a.attachment:before { float: right; font-size: 0.929em; margin-top: 1px; - a {color: scale-color($primary, $lightness: 50%);} + a {color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } } @@ -955,7 +955,7 @@ span.highlighted { } .username.new-user a { - color: scale-color($primary, $lightness: 70%); + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } .read-state { diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss index fbec592bdd..11553f5c91 100644 --- a/app/assets/stylesheets/desktop/topic.scss +++ b/app/assets/stylesheets/desktop/topic.scss @@ -58,7 +58,7 @@ } .private-message-glyph { - color: scale-color($primary, $lightness: 75%); + color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); float: left; margin: 0 5px 0 0; } @@ -66,7 +66,7 @@ a.reply-new { margin-top: 3px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); i { margin-right: 3px; background: $secondary; diff --git a/app/assets/stylesheets/desktop/upload.scss b/app/assets/stylesheets/desktop/upload.scss index 431130be68..5dc9fe66e4 100644 --- a/app/assets/stylesheets/desktop/upload.scss +++ b/app/assets/stylesheets/desktop/upload.scss @@ -17,7 +17,7 @@ line-height: 18px; } .description, .hint { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } .hint { font-style: italic; diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 2f5af264d6..5637f8a16d 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -56,7 +56,7 @@ display: inline-block; } .instructions { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin-left: 160px; margin-top: 5px; margin-bottom: 10px; @@ -170,7 +170,7 @@ text-align: left; border-bottom: 3px solid dark-light-diff($primary, $secondary, 90%, -60%); padding: 0 0 10px 0; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-weight: normal; } @@ -511,7 +511,7 @@ } // common/base/header.scss .fa, .icon { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-size: 1.714em; } } @@ -519,12 +519,12 @@ .name { display: inline-block; margin-top: 5px; - color: scale-color($primary, $lightness: 30%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } .title { display: inline-block; margin-top: 5px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } } @@ -600,7 +600,7 @@ float: auto; } p { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin-top: 5px; margin-bottom: 10px; font-size: 80%; diff --git a/app/assets/stylesheets/embed.css.scss b/app/assets/stylesheets/embed.css.scss index d7193d5533..40eab28757 100644 --- a/app/assets/stylesheets/embed.css.scss +++ b/app/assets/stylesheets/embed.css.scss @@ -86,7 +86,7 @@ article.post { } a.new-user { - color: scale-color($primary, $lightness: 70%); + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } span.title { diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index 30725c4d6f..c850e804d0 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -33,7 +33,7 @@ display: none !important; // can be removed if inline JS CSS is removed from com color: scale-color($secondary, $lightness: 50%); } #draft-status { - color: scale-color($primary, $lightness: 75%); + color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } transition: height 0.4s ease; width: 100%; @@ -48,7 +48,7 @@ display: none !important; // can be removed if inline JS CSS is removed from com right: 1px; position: absolute; font-size: 1.071em; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); padding: 0 10px 5px 10px; &:before { font-family: "FontAwesome"; @@ -165,11 +165,11 @@ display: none !important; // can be removed if inline JS CSS is removed from com #reply-title { margin-right: 10px; &:disabled { - background-color: scale-color($primary, $lightness: 75%); + background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } } .wmd-input:disabled { - background-color: scale-color($primary, $lightness: 75%); + background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } .wmd-input { color: darken($primary, 40%); diff --git a/app/assets/stylesheets/mobile/login.scss b/app/assets/stylesheets/mobile/login.scss index 6093849bfe..790d84864c 100644 --- a/app/assets/stylesheets/mobile/login.scss +++ b/app/assets/stylesheets/mobile/login.scss @@ -14,7 +14,7 @@ #login-form { a { - color: scale-color($primary, $lightness: 35%); + color: dark-light-choose(scale-color($primary, $lightness: 35%), scale-color($secondary, $lightness: 65%)); } label { float: left; display: block; } textarea, input, select {font-size: 1.143em; clear: left; margin-top: 0; } @@ -42,7 +42,7 @@ a#forgot-password-link {clear: left; float: left; } tr.instructions { label { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } diff --git a/app/assets/stylesheets/mobile/modal.scss b/app/assets/stylesheets/mobile/modal.scss index e54ab173ce..3111b60132 100644 --- a/app/assets/stylesheets/mobile/modal.scss +++ b/app/assets/stylesheets/mobile/modal.scss @@ -99,7 +99,7 @@ .custom-message-length { margin: -10px 0 10px 20px; - color: scale-color($primary, $lightness: 70%); + color: dark-light-choose(scale-color($primary, $lightness: 30%), scale-color($secondary, $lightness: 70%)); font-size: 85%; } diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 47c69376bb..6c8c770b7b 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -72,7 +72,7 @@ th, td { padding: 7px 0; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } a.title {color: $primary;} @@ -92,7 +92,7 @@ max-width: 160px; } .num .fa { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } @@ -143,7 +143,7 @@ tr.category-topic-link { .featured-topic { margin: 8px 0; a.last-posted-at, a.last-posted-at:visited { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } } @@ -206,7 +206,7 @@ tr.category-topic-link { figure { float: left; margin: 3px 7px 0 0; - color: scale-color($primary, $lightness: 10%); + color: dark-light-choose(scale-color($primary, $lightness: 10%), scale-color($secondary, $lightness: 90%)); font-weight: bold; font-size: 0.857em; } diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index fbde0923a3..d69d3b1372 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -46,7 +46,7 @@ button { padding: 8px 10px; vertical-align: top; background: transparent; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); float: left; &.hidden { display: none; @@ -148,7 +148,7 @@ a.reply-to-tab { position: absolute; z-index: 400; right: 80px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); span { display: none; } } @@ -179,7 +179,7 @@ a.star { h3 { margin-bottom: 4px; margin-top: 0; - color: scale-color($primary, $lightness: 20%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); line-height: 23px; font-weight: normal; font-size: 1em; @@ -187,7 +187,7 @@ a.star { h4 { margin: 0 0 3px 0; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-weight: normal; font-size: 0.857em; line-height: 15px; @@ -244,7 +244,7 @@ a.star { line-height: 20px; } .number, i { - color: scale-color($primary, $lightness: 20%); + color: dark-light-choose(scale-color($primary, $lightness: 20%), scale-color($secondary, $lightness: 80%)); font-size: 110%; } @@ -267,7 +267,7 @@ a.star { } .domain { - color: scale-color($primary, $lightness: 75%); + color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } .topic-links { @@ -439,7 +439,7 @@ button.select-post { } #show-topic-admin { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); right: 0; border-right: 0; padding-right: 4px; @@ -495,7 +495,7 @@ span.highlighted { } .username.new-user a { - color: scale-color($primary, $lightness: 70%); + color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } .user-title { diff --git a/app/assets/stylesheets/mobile/topic.scss b/app/assets/stylesheets/mobile/topic.scss index 9bc347b650..9e0d6e2164 100644 --- a/app/assets/stylesheets/mobile/topic.scss +++ b/app/assets/stylesheets/mobile/topic.scss @@ -31,7 +31,7 @@ .private-message-glyph { display: none; } } -.private-message-glyph { color: scale-color($primary, $lightness: 75%); } +.private-message-glyph { color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } .private_message #topic-title .private-message-glyph { display: inline; } diff --git a/app/assets/stylesheets/mobile/upload.scss b/app/assets/stylesheets/mobile/upload.scss index d639f20dbd..00797993b0 100644 --- a/app/assets/stylesheets/mobile/upload.scss +++ b/app/assets/stylesheets/mobile/upload.scss @@ -7,7 +7,7 @@ line-height: 18px; } .description { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 03ec7fb16f..f64f925616 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -112,7 +112,7 @@ display: inline-block; } .instructions { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin-top: 5px; margin-bottom: 10px; font-size: 80%; @@ -486,7 +486,7 @@ } // common/base/header.scss .fa, .icon { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); font-size: 1.714em; } } @@ -494,13 +494,13 @@ .name { display: inline-block; margin-top: 5px; - color: scale-color($primary, $lightness: 30%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); vertical-align: inherit; } .title { display: inline-block; margin-top: 5px; - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); } } } @@ -577,7 +577,7 @@ float: auto; } p { - color: scale-color($primary, $lightness: 50%); + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); margin-top: 5px; margin-bottom: 10px; font-size: 80%; From 49996bcdea6cdfa2979249ace416f6e222f5eef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 20 Aug 2015 11:59:28 +0200 Subject: [PATCH 158/237] FIX: don't suggest name when email is empty --- app/models/user.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/models/user.rb b/app/models/user.rb index 1317decac5..6b25f4f556 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -171,9 +171,8 @@ class User < ActiveRecord::Base end def self.suggest_name(email) - return "" unless email - name = email.split(/[@\+]/)[0] - name = name.gsub(".", " ") + return "" if email.blank? + name = email.split(/[@\+]/)[0].gsub(".", " ") name.titleize end From e1575746f2acaa4d6f44a6cbd7c9f45eb1f9bc42 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 20 Aug 2015 17:33:13 +0530 Subject: [PATCH 159/237] Update Translations --- config/locales/client.ar.yml | 31 +---- config/locales/client.bs_BA.yml | 5 - config/locales/client.cs.yml | 12 -- config/locales/client.da.yml | 21 ---- config/locales/client.de.yml | 36 +++--- config/locales/client.es.yml | 43 +++---- config/locales/client.fa_IR.yml | 12 -- config/locales/client.fi.yml | 30 ----- config/locales/client.fr.yml | 30 ----- config/locales/client.he.yml | 30 ----- config/locales/client.id.yml | 1 - config/locales/client.it.yml | 30 ----- config/locales/client.ja.yml | 30 ----- config/locales/client.ko.yml | 30 ----- config/locales/client.nb_NO.yml | 35 +++--- config/locales/client.nl.yml | 12 -- config/locales/client.pl_PL.yml | 31 ++--- config/locales/client.pt.yml | 32 +++--- config/locales/client.pt_BR.yml | 12 -- config/locales/client.ro.yml | 14 --- config/locales/client.ru.yml | 12 -- config/locales/client.sq.yml | 30 ----- config/locales/client.sv.yml | 12 -- config/locales/client.te.yml | 14 --- config/locales/client.tr_TR.yml | 12 -- config/locales/client.uk.yml | 107 +++++++++++++++++- config/locales/client.zh_CN.yml | 30 ----- config/locales/client.zh_TW.yml | 14 --- config/locales/server.ar.yml | 12 +- config/locales/server.bs_BA.yml | 1 - config/locales/server.cs.yml | 1 - config/locales/server.da.yml | 1 - config/locales/server.de.yml | 1 - config/locales/server.es.yml | 16 ++- config/locales/server.fa_IR.yml | 1 - config/locales/server.fi.yml | 25 ---- config/locales/server.fr.yml | 1 - config/locales/server.he.yml | 1 - config/locales/server.it.yml | 1 - config/locales/server.ja.yml | 1 - config/locales/server.ko.yml | 25 ---- config/locales/server.nl.yml | 1 - config/locales/server.pl_PL.yml | 26 ----- config/locales/server.pt.yml | 18 +-- config/locales/server.pt_BR.yml | 1 - config/locales/server.ro.yml | 1 - config/locales/server.ru.yml | 25 ---- config/locales/server.sq.yml | 25 ---- config/locales/server.sv.yml | 1 - config/locales/server.tr_TR.yml | 3 - config/locales/server.uk.yml | 1 - config/locales/server.zh_CN.yml | 24 ---- config/locales/server.zh_TW.yml | 1 - plugins/poll/config/locales/server.uk.yml | 11 ++ .../lib/discourse_imgur/locale/server.uk.yml | 3 +- 55 files changed, 240 insertions(+), 696 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 9a16a4933b..45b20c5cec 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -187,23 +187,8 @@ ar: action_codes: split_topic: "اقسم هذا الموضوع" autoclosed: - enabled: 'الموضوع مغلق منذ: {when}%' - disabled: ' الموضوع مفتوح منذ : {when}%' - closed: - enabled: 'الموضوع مغلق منذ: {when}%' - disabled: ' الموضوع مفتوح منذ : {when}%' - archived: - enabled: 'تمت أرشفة هذا الموضوع في تاريخ : {when}%' - disabled: 'تم إلغاء أرشفة هذا الموضوع منذ : {when}%' - pinned: - enabled: 'هذا الموضوع مثبت منذ : {when}%' - disabled: 'تم إلغاء تثبيت هذا الموضوع : {when}%' - pinned_globally: - enabled: 'هذا الموضوع مثبت منذ : {when}%' - disabled: 'تم إلغاء تثبيت هذا الموضوع : {when}%' - visible: - enabled: 'تم تسجيل هذا الموضوع في القائمة منذ : {when}%' - disabled: 'تم إلغاء تسجيل هذا الموضوع في القائمة منذ : {when}%' + enabled: 'أغلق %{when}' + disabled: 'مفتوح %{when}' topic_admin_menu: "عمليات المدير" emails_are_disabled: "جميع الرسائل الالكترونية تم تعطيلها من قبل المدير , لن يتم ارسال اي بريد الكتروني " edit: 'عدّل العنوان و التصنيف على هذا الموضوع' @@ -219,7 +204,6 @@ ar: admin_title: "المدير" flags_title: "بلاغات" show_more: "أعرض المزيد" - show_help: "مساعدة" links: "روابط" links_lowercase: zero: "رابط" @@ -1146,10 +1130,8 @@ ar: title: "تحت المتابعة" description: "سيتم عرض عدد الردود جديدة لهذا الموضوع. سيتم إعلامك إذا ذكر أحد name@ أو ردود لك." regular: - title: "منتظم" description: ".سيتم إشعارك إذا ذكر أحد ما @اسمك أو رد لك" regular_pm: - title: "منتظم" description: "سيتم تنبيهك إذا قام احدٌ بالاشارة إلى حسابك @name أو الرد عليك." muted_pm: title: "كتم" @@ -2828,12 +2810,3 @@ ar: reader: name: قارئ description: قراءة أكثر من 100 تعليق في الموضوع - google_search: | -

    البحث في جوجل

    -

    - - - - - -

    diff --git a/config/locales/client.bs_BA.yml b/config/locales/client.bs_BA.yml index a0594301b2..794c45e6ff 100644 --- a/config/locales/client.bs_BA.yml +++ b/config/locales/client.bs_BA.yml @@ -109,7 +109,6 @@ bs_BA: admin_title: "Admin" flags_title: "Opomene" show_more: "pokaži još" - show_help: "Pomoć" links: "Linkovi" links_lowercase: one: "Link" @@ -805,10 +804,6 @@ bs_BA: title: "Praćenje" tracking: title: "Praćenje" - regular: - title: "Regularno" - regular_pm: - title: "Regularno" muted_pm: title: "Mutirano" description: "You will never be notified of anything about this private message." diff --git a/config/locales/client.cs.yml b/config/locales/client.cs.yml index 1c3f903948..7bd3814b79 100644 --- a/config/locales/client.cs.yml +++ b/config/locales/client.cs.yml @@ -142,7 +142,6 @@ cs: admin_title: "Administrace" flags_title: "Nahlášení" show_more: "zobrazit více" - show_help: "pomoc" links: "Odkazy" links_lowercase: one: "odkaz" @@ -978,10 +977,8 @@ cs: title: "Sledované" description: "U tohoto tématu se zobrazí počet nových příspěvků. Budete upozorněni, pokud někdo zmíní vaše @jméno nebo odpoví na váš příspěvek." regular: - title: "Klasicky" description: "Budete informováni pokud někdo zmíní vaše @jméno nebo odpoví na váš příspěvek." regular_pm: - title: "Normální" description: "Budete informováni pokud někdo zmíní vaše @jméno nebo odpoví na váš příspěvek." muted_pm: title: "Ztišení" @@ -2452,12 +2449,3 @@ cs: reader: name: Čtenář description: Přečíst každý příspěvek v tématu s více než 100 příspěvky - google_search: | -

    Vyhledání v Google

    -

    - - - - - -

    diff --git a/config/locales/client.da.yml b/config/locales/client.da.yml index 69e1e3130b..7afc96078d 100644 --- a/config/locales/client.da.yml +++ b/config/locales/client.da.yml @@ -108,15 +108,6 @@ da: facebook: 'del dette link på Facebook' google+: 'del dette link på Google+' email: 'send dette link i en e-mail' - action_codes: - autoclosed: - enabled: 'lukkede dette emne %{when}' - disabled: 'åbnede dette emne %{when}' - closed: - enabled: 'lukkede dette emne %{when}' - disabled: 'åbnede dette emne %{when}' - archived: - enabled: 'arkiverede dette emne %{when}' topic_admin_menu: "administrationshandlinger på emne" emails_are_disabled: "Alle udgående emails er blevet deaktiveret globalt af en administrator. Ingen emailnotifikationer af nogen slags vil blive sendt." edit: 'redigér titel og kategori for dette emne' @@ -132,7 +123,6 @@ da: admin_title: "Admin" flags_title: "Flag" show_more: "vis mere" - show_help: "hjælp" links: "Links" links_lowercase: one: "link" @@ -950,10 +940,8 @@ da: title: "Følger" description: "En optælling af nye svar vil blive vist for denne tråd. Du vil modtage en notifikation, hvis nogen nævner dit @name eller svarer dig." regular: - title: "Standard" description: "Du vil modtage en notifikation, hvis nogen nævner dit @name eller svarer dig." regular_pm: - title: "Standard" description: "Du vil modtage en notifikation, hvis nogen nævner dit @name eller svarer dig." muted_pm: title: "Lydløs" @@ -2431,12 +2419,3 @@ da: reader: name: Læser description: Har læst hver eneste indlæg i et emne med mere end 100 posts - google_search: | -

    Søg med Google

    -

    - - - - - -

    diff --git a/config/locales/client.de.yml b/config/locales/client.de.yml index 681d0c53f7..37f86c0c4e 100644 --- a/config/locales/client.de.yml +++ b/config/locales/client.de.yml @@ -111,23 +111,23 @@ de: action_codes: split_topic: "Thema aufteilen" autoclosed: - enabled: 'hat das Thema %{when} geschlossen' - disabled: 'hat das Thema %{when} geöffnet' + enabled: 'geschlossen, %{when}' + disabled: 'geöffnet, %{when}' closed: - enabled: 'hat das Thema %{when} geschlossen' - disabled: 'hat das Thema %{when} geöffnet' + enabled: 'geschlossen, %{when}' + disabled: 'geöffnet, %{when}' archived: - enabled: 'hat das Thema %{when} archiviert' - disabled: 'hat das Thema %{when} aus dem Archiv geholt' + enabled: 'archiviert, %{when}' + disabled: 'aus dem Archiv geholt, %{when}' pinned: - enabled: 'hat das Thema %{when} angeheftet' - disabled: 'hat das Thema %{when} losgelöst' + enabled: 'angeheftet, %{when}' + disabled: 'losgelöst, %{when}' pinned_globally: - enabled: 'hat das Thema %{when} global angeheftet' - disabled: 'hat das Thema %{when} losgelöst' + enabled: 'global angeheftet, %{when}' + disabled: 'losgelöst, %{when}' visible: - enabled: 'hat das Thema %{when} sichtbar gemacht' - disabled: 'hat das Thema %{when} unsichtbar gemacht' + enabled: 'sichtbar gemacht, %{when}' + disabled: 'unsichtbar gemacht, {when}' topic_admin_menu: "Thema administrieren" emails_are_disabled: "Die ausgehende E-Mail-Kommunikation wurde von einem Administrator global deaktiviert. Es werden keinerlei Benachrichtigungen per E-Mail verschickt." edit: 'Titel und Kategorie dieses Themas ändern' @@ -143,7 +143,7 @@ de: admin_title: "Administration" flags_title: "Meldungen" show_more: "mehr anzeigen" - show_help: "Hilfe" + show_help: "Optionen" links: "Links" links_lowercase: one: "Link" @@ -542,6 +542,7 @@ de: search: "schreib zum Suchen nach Einladungen..." title: "Einladungen" user: "Eingeladener Benutzer" + sent: "Gesendet" none: "Du hast bis jetzt noch niemanden hierher eingeladen." truncated: "Zeige die ersten {{count}} Einladungen." redeemed: "Angenommene Einladungen" @@ -2465,12 +2466,3 @@ de: reader: name: Leser description: Hat in einem Thema mit mehr als 100 Beiträgen jeden Beitrag gelesen - google_search: | -

    Mit Google suchen

    -

    - - - - - -

    diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 291d0dfab9..2f0abd235a 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -111,23 +111,23 @@ es: action_codes: split_topic: "dividió este tema" autoclosed: - enabled: 'cerró este tema %{when}' - disabled: 'abrió este tema %{when}' + enabled: 'cerrado %{when}' + disabled: 'abierto %{when}' closed: - enabled: 'cerró este tema %{when}' - disabled: 'abrió este tema %{when}' + enabled: 'cerrado %{when}' + disabled: 'abierto %{when}' archived: - enabled: 'archivó este tema %{when}' - disabled: 'desarchivó este tema %{when}' + enabled: 'archivado %{when}' + disabled: 'desarchivado %{when}' pinned: - enabled: 'puso este tema como destacado %{when}' - disabled: 'quitó este tema como destacado %{when}' + enabled: 'destacado %{when}' + disabled: 'sin destacar %{when}' pinned_globally: - enabled: 'puso en destacados este team de forma global %{when}' - disabled: 'quitó este tema de destacados %{when}' + enabled: 'destacado globalmente %{when}' + disabled: 'sin destacar %{when}' visible: - enabled: 'puso en lista este tema %{when}' - disabled: 'quitó de la lista este tema %{when}' + enabled: 'listado %{when}' + disabled: 'sin listar %{when}' topic_admin_menu: "acciones de administrador para el tema" emails_are_disabled: "Todos los emails salientes han sido desactivados por un administrador. No se enviará ninguna notificación por email." edit: 'editar el título y la categoría de este tema' @@ -143,7 +143,7 @@ es: admin_title: "Admin" flags_title: "Reportes" show_more: "ver más" - show_help: "ayuda" + show_help: "opciones" links: "Enlaces" links_lowercase: one: "enlace" @@ -543,6 +543,7 @@ es: search: "escribe para buscar invitaciones..." title: "Invitaciones" user: "Invitar Usuario" + sent: "Enviadas" none: "No has invitado a nadie todavía." truncated: "Mostrando las primeras {{count}} invitaciones." redeemed: "Invitaciones aceptadas" @@ -885,6 +886,9 @@ es: bookmarks: "No hay más temas guardados en marcadores." search: "No hay más resultados de búsqueda." topic: + unsubscribe: + stop_notifications: "Ahora recibirás menos notificaciones desde {{title}}" + change_notification_state: "El estado actual de notificación para ti es" filter_to: "{{post_count}} posts en este tema" create: 'Crear tema' create_long: 'Crear un nuevo tema' @@ -1791,6 +1795,7 @@ es: header: "Encabezado" top: "Top" footer: "Pie de página" + embedded_css: "CSS embebido" head_tag: text: "" title: "HTML insertado antes de la etiqueta " @@ -2468,11 +2473,11 @@ es: name: Lector description: Leyó todos los posts en un tema con más de 100 google_search: | -

    Buscar con Google

    +

    Buscar con Google

    - - - - - + + + + +

    diff --git a/config/locales/client.fa_IR.yml b/config/locales/client.fa_IR.yml index f73dc1bfae..fafee55b49 100644 --- a/config/locales/client.fa_IR.yml +++ b/config/locales/client.fa_IR.yml @@ -104,7 +104,6 @@ fa_IR: admin_title: "مدیر" flags_title: "پرچم‌ها" show_more: "بیش‌تر نشان بده" - show_help: "کمک" links: "پیوندها" links_lowercase: other: "پیوندها" @@ -901,10 +900,8 @@ fa_IR: title: "ردگیری" description: "تعداد پاسخ‌های جدید برای این عنوان نمایش داده خواهد شد. در صورتی که فردی با @name به شما اشاره کند یا به شما پاسخی دهد، به شما اطلاع رسانی خواهد شد." regular: - title: "منظم" description: "در صورتی که فردی با @name به شما اشاره کند یا به شما پاسخی دهد به شما اطلاع داده خواهد شد." regular_pm: - title: "عادی" description: "در صورتی که فردی با @name به شما اشاره کند یا به شما پاسخی دهد به شما اطلاع داده خواهد شد." muted_pm: title: "بی صدا شد" @@ -2333,12 +2330,3 @@ fa_IR: reader: name: خواننده description: مطالعه تمام نوشته‌هایی در یک موضوع که بیش از 100 نوشته دارد. - google_search: | -

    جستجو با گوگل

    -

    - - - - - -

    diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 26891ffb84..5e5d5db20e 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -110,24 +110,6 @@ fi: email: 'lähetä tämä linkki sähköpostissa' action_codes: split_topic: "jaa tämä ketju" - autoclosed: - enabled: 'sulki tämän ketjun %{when}' - disabled: 'avasi tämän ketjun %{when}' - closed: - enabled: 'sulki tämän ketjun %{when}' - disabled: 'avasi tämän ketjun %{when}' - archived: - enabled: 'arkistoi tämän ketjun %{when}' - disabled: 'palautti tämän ketjun %{when}' - pinned: - enabled: 'kiinnitti tämän ketjun %{when}' - disabled: 'poisti tämän ketjun kiinnityksen %{when}' - pinned_globally: - enabled: 'kiinnitti tämän ketjun koko palstalle %{when}' - disabled: 'poisti ketjun kiinnityksen %{when}' - visible: - enabled: 'lisäsi tämän ketjun listauksiin %{when}' - disabled: 'poisi tämän ketjun listauksista %{when}' topic_admin_menu: "ketjun ylläpitotoimet" emails_are_disabled: "Ylläpitäjä on estänyt kaiken lähtevän sähköpostiliikenteen. Mitään sähköposti-ilmoituksia ei lähetetä." edit: 'muokkaa tämän ketjun otsikkoa ja aluetta' @@ -143,7 +125,6 @@ fi: admin_title: "Ylläpito" flags_title: "Liput" show_more: "näytä lisää" - show_help: "ohje" links: "Linkit" links_lowercase: one: "linkki" @@ -978,10 +959,8 @@ fi: title: "Seuraa" description: "Tälle ketjulle näytetään uusien vastausten lukumäärä. Saat ilmoituksen jos joku mainitsee @nimesi tai vastaa sinulle." regular: - title: "Tavallinen" description: "Saat ilmoituksen jos joku mainitsee @nimesi tai vastaa sinulle." regular_pm: - title: "Tavallinen" description: "Saat ilmoituksen jos joku mainitsee @nimesi tai vastaa sinulle." muted_pm: title: "Vaimenna" @@ -2468,12 +2447,3 @@ fi: reader: name: Lukija description: Luki kaikki viestit ketjusta, jossa on yli 100 viestiä - google_search: | -

    Etsi Googlella

    -

    - - - - - -

    diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 9ea147e9f2..2565eb87ac 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -110,24 +110,6 @@ fr: email: 'envoyer ce lien dans un courriel' action_codes: split_topic: "diviser ce sujet" - autoclosed: - enabled: 'Ce sujet a été fermé %{when}' - disabled: 'Ce sujet a été ouvert %{when}' - closed: - enabled: 'Ce sujet a été fermé %{when}' - disabled: 'Ce sujet a été ouvert %{when}' - archived: - enabled: 'Ce sujet a été archivé %{when}' - disabled: 'Ce sujet a été sorti des archives %{when}' - pinned: - enabled: 'Ce sujet a été épinglé %{when}' - disabled: 'Ce sujet a été désépinglé %{when}' - pinned_globally: - enabled: 'Ce sujet a été épinglé globalement %{when}' - disabled: 'Ce sujet a été désépinglé %{when}' - visible: - enabled: 'Ce sujet a été listé %{when}' - disabled: 'Ce sujet a été sorti de la liste %{when}' topic_admin_menu: "actions administrateur pour ce sujet" emails_are_disabled: "Le courriel sortant a été désactivé par un administrateur. Aucune notification courriel ne sera envoyée." edit: 'éditer le titre et la catégorie de ce sujet' @@ -143,7 +125,6 @@ fr: admin_title: "Admin" flags_title: "Signalements" show_more: "afficher plus" - show_help: "aide" links: "Liens" links_lowercase: one: "lien" @@ -982,10 +963,8 @@ fr: title: "Suivi" description: "Le nombre de nouvelles réponses apparaîtra pour ce sujet. Vous serez notifié si quelqu'un mentionne votre @pseudo ou vous répond." regular: - title: "Normal" description: "Vous serez notifié si quelqu'un mentionne votre @pseudo ou vous répond." regular_pm: - title: "Normal" description: "Vous serez notifié si quelqu'un mentionne votre @pseudo ou vous répond." muted_pm: title: "Silencieux" @@ -2472,12 +2451,3 @@ fr: reader: name: Lecteur description: A lu tous les messages d'un sujet contenant plus de 100 messages - google_search: | -

    Rechercher avec Google

    -

    - - - - - -

    diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 776c5faf1e..7559c02fd6 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -109,24 +109,6 @@ he: email: 'שלח קישור בדוא"ל' action_codes: split_topic: "פצל נושא זה" - autoclosed: - enabled: 'סגר את הנושא %{when}' - disabled: 'פתח את הנושא %{when}' - closed: - enabled: 'סגר את הנושא %{when}' - disabled: 'פתח את הנושא %{when}' - archived: - enabled: 'הוסיף את הנושא לארכיון %{when}' - disabled: 'הוציא את הנושא מהארכיון %{when}' - pinned: - enabled: 'נעץ את הנושא %{when}' - disabled: 'הסיר את הנושא מנעיצה %{when}' - pinned_globally: - enabled: 'נעץ את הנושא גלובלית %{when}' - disabled: 'הסיר את הנושא מנעיצה %{when}' - visible: - enabled: 'הוסיף את הנושא לרשימה %{when}' - disabled: 'הוציא את הנושא מהרשימה %{when}' topic_admin_menu: "פעולות ניהול לנושא" emails_are_disabled: "כל הדוא\"ל היוצא נוטרל באופן גורף על ידי מנהל אתר. שום הודעת דוא\"ל, מכל סוג שהוא, תשלח." edit: 'ערוך את הכותרת והקטגוריה של הנושא' @@ -142,7 +124,6 @@ he: admin_title: "ניהול" flags_title: "סימוני הודעה" show_more: "הראה עוד" - show_help: "עזרה" links: "קישורים" links_lowercase: one: "קישור" @@ -977,10 +958,8 @@ he: title: "רגיל+" description: "כמו רגיל, בנוסף מספר התגובות שלא נקראו יוצג לנושא זה. " regular: - title: "רגיל" description: "תקבלו התראה אם מישהו יזכיר את @שם_המשתמש/ת שלך או ישיב לפרסום שלך." regular_pm: - title: "רגיל" description: "תקבלו התראה אם מישהו יזכיר את @שם_המשתמש/ת שלך או ישיב לפרסום שלך." muted_pm: title: "מושתק" @@ -2455,12 +2434,3 @@ he: reader: name: מקראה description: קראו כל פרסום בנושא עם יותר מ-100 פרסומים - google_search: | -

    חפשו עם גוגל

    -

    - - - - - -

    diff --git a/config/locales/client.id.yml b/config/locales/client.id.yml index 349e5d8df3..b3a559e5d6 100644 --- a/config/locales/client.id.yml +++ b/config/locales/client.id.yml @@ -88,7 +88,6 @@ id: admin_title: "Admin" flags_title: "Flags" show_more: "show more" - show_help: "Bantuan" links: "Links" faq: "FAQ" guidelines: "Guidelines" diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index 077873607c..deb1fd61ef 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -110,24 +110,6 @@ it: email: 'invia questo collegamento via email' action_codes: split_topic: "dividi questo argomento" - autoclosed: - enabled: 'ha chiuso questo argomento il %{when}' - disabled: 'ha aperto questo argomento il %{when}' - closed: - enabled: 'ha chiuso questo argomento il %{when}' - disabled: 'ha aperto questo argomento il %{when}' - archived: - enabled: 'ha archiviato questo argomento il %{when}' - disabled: 'ha recuperato dall''archivio questo argomento il %{when}' - pinned: - enabled: 'ha evidenziato questo argomento il %{when}' - disabled: 'ha tolto questo argomento da quelli evidenziati il %{when}' - pinned_globally: - enabled: 'ha evidenziato a livello globale questo argomento il %{when}' - disabled: 'ha tolto questo argomento da quelli evidenziati il %{when}' - visible: - enabled: 'ha inserito nell''elenco questo argomento il %{when}' - disabled: 'ha rimosso dall''elenco questo argomento il %{when}' topic_admin_menu: "azioni amministrative sull'argomento" emails_are_disabled: "Tutte le email in uscita sono state disabilitate a livello globale da un amministratore. Non sarà inviata nessun tipo di notifica via email." edit: 'modifica titolo e categoria dell''argomento' @@ -143,7 +125,6 @@ it: admin_title: "Amministrazione" flags_title: "Segnalazioni" show_more: "Altro" - show_help: "aiuto" links: "Link" links_lowercase: one: "collegamento" @@ -978,10 +959,8 @@ it: title: "Seguito" description: "Per questo argomento apparirà un conteggio delle nuove risposte. Riceverai una notifica se qualcuno menziona il tuo @nome o ti risponde." regular: - title: "Normale" description: "Riceverai una notifica se qualcuno menziona il tuo @nome o ti risponde." regular_pm: - title: "Normale" description: "Riceverai una notifica se qualcuno menziona il tuo @nome o ti risponde." muted_pm: title: "Silenziato" @@ -2464,12 +2443,3 @@ it: reader: name: Lettore description: Letto tutti i messaggi in un argomento con più di 100 messaggi - google_search: | -

    Cerca con Google

    -

    - - - - - -

    diff --git a/config/locales/client.ja.yml b/config/locales/client.ja.yml index a5e7ce12aa..ca12ef4bd3 100644 --- a/config/locales/client.ja.yml +++ b/config/locales/client.ja.yml @@ -91,24 +91,6 @@ ja: email: 'メールでこのリンクを送る' action_codes: split_topic: "トピックを分割" - autoclosed: - enabled: '%{when} にこのトピックを閉じる' - disabled: '%{when} にこのトピックを開く' - closed: - enabled: '%{when} にこのトピックを閉じる' - disabled: '%{when} にこのトピックを開く' - archived: - enabled: '%{when} にこのトピックをアーカイブする' - disabled: '%{when} にこのトピックをアーカイブ解除する' - pinned: - enabled: '%{when} にこのトピックをピン留めする' - disabled: '%{when} にこのトピックのピン留めを解除する' - pinned_globally: - enabled: '%{when} にこのトピックをグローバルにピン留めする' - disabled: '%{when} にこのトピックのピン留めを解除する' - visible: - enabled: '%{when} にこのトピックをリストする' - disabled: '%{when} にこのトピックをリストから外す' topic_admin_menu: "トピック管理" emails_are_disabled: "全てのメールアドレスの送信が管理者によって無効化されています。全ての種類のメール通知は行われません" edit: 'このトピックのタイトルとカテゴリを編集' @@ -124,7 +106,6 @@ ja: admin_title: "管理者" flags_title: "フラグ" show_more: "もっと見る" - show_help: "ヘルプ" links: "リンク" links_lowercase: other: "リンク集" @@ -936,10 +917,8 @@ ja: title: "トラック中" description: "新規回答件数がこのトピックに表示されます。他ユーザから@ユーザ名でタグ付けされた場合、またはあなたの投稿に回答がついた場合に通知されます。" regular: - title: "通常" description: "他ユーザからタグ付けをされた場合、またはあなたの投稿に回答が付いた場合に通知されます。" regular_pm: - title: "通常" description: "他ユーザからタグ付けをされた場合、またはあなたのメッセージ内の投稿に回答が付いた場合に通知されます。" muted_pm: title: "ミュートされました" @@ -2377,12 +2356,3 @@ ja: reader: name: 閲覧者 description: 100以上の投稿があるトピック内の投稿をすべて読みました。 - google_search: | -

    Googleで探す

    -

    - - - - - -

    diff --git a/config/locales/client.ko.yml b/config/locales/client.ko.yml index 8cbd0aa127..c2fcbe3083 100644 --- a/config/locales/client.ko.yml +++ b/config/locales/client.ko.yml @@ -91,24 +91,6 @@ ko: email: '이메일로 공유' action_codes: split_topic: "이 토픽을 분리" - autoclosed: - enabled: '이 토픽을 %{when} 닫기' - disabled: '이 토픽을 %{when} 열기' - closed: - enabled: '이 토픽을 %{when} 닫기' - disabled: '이 토픽을 %{when} 열기' - archived: - enabled: '이 토픽을 %{when} 보관' - disabled: '이 토픽을 %{when} 보관 최소' - pinned: - enabled: '이 토픽을 %{when} 고정' - disabled: '이 토픽을 %{when} 고정 취소' - pinned_globally: - enabled: '이 토픽을 %{when} 전역적으로 고정' - disabled: '이 토픽을 %{when} 전역적으로 고정 취소' - visible: - enabled: '이 토픽을 %{when} 목록에 보이기' - disabled: '이 토픽을 %{when} 목록에서 감추기' topic_admin_menu: "토픽 관리자 기능" emails_are_disabled: "관리자가 이메일 송신을 전체 비활성화 했습니다. 어떤 종류의 이메일 알림도 보내지지 않습니다." edit: '이 토픽의 제목과 카테고리 편집' @@ -124,7 +106,6 @@ ko: admin_title: "관리자" flags_title: "신고" show_more: "더 보기" - show_help: "도움말" links: "링크" links_lowercase: other: "링크" @@ -936,10 +917,8 @@ ko: title: "새 글 표시 중" description: "이 토픽의 새로운 답글의 수가 표시됩니다. 누군가 내 @아이디를 멘션했거나 내게 답글을 작성하면 알림을 받습니다." regular: - title: "알림: 일반" description: "누군가 내 @아아디 으로 멘션했거나 내 글에 답글이 달릴 때 알림을 받게 됩니다." regular_pm: - title: "알림: 일반" description: "누군가 내 @아아디 으로 멘션했거나 내 글에 답글이 달릴 때 알림을 받게 됩니다." muted_pm: title: "알림 : 끔" @@ -2377,12 +2356,3 @@ ko: reader: name: 독서가 description: 100개가 넘는 댓글이 달린 토픽의 댓글을 모두 읽었습니다. - google_search: | -

    Google로 검색

    -

    - - - - - -

    diff --git a/config/locales/client.nb_NO.yml b/config/locales/client.nb_NO.yml index c60efa330d..b3cb1f7e56 100644 --- a/config/locales/client.nb_NO.yml +++ b/config/locales/client.nb_NO.yml @@ -109,17 +109,21 @@ nb_NO: google+: 'del denne lenken på Google+' email: 'del denne lenken i en e-post' action_codes: + split_topic: "del opp dette emnet" autoclosed: - enabled: 'stengte dette emnet %{when}' - disabled: 'åpnet dette emnet %{when}' + enabled: 'lukket %{when}' + disabled: 'åpnet %{when}' closed: - enabled: 'stengte dette emnet %{when}' - disabled: 'åpnet dette emnet %{when}' + enabled: 'lukket %{when}' + disabled: 'åpnet %{when}' archived: - enabled: 'arkiverte dette emnet %{when}' + enabled: 'arkivert %{when}' + disabled: 'fjernet fra arkiv %{when}' pinned: - enabled: 'festet dette emnet %{when}' - disabled: 'løsgjorde dette emnet %{when}' + enabled: 'festet %{when}' + disabled: 'avfestet %{when}' + pinned_globally: + enabled: 'festet globalt %{when}' topic_admin_menu: "admin-handlinger for emne" emails_are_disabled: "All utgående e-post har blitt deaktivert globalt av en administrator. Ingen e-postvarslinger vil bli sendt." edit: 'rediger tittelen og kategorien til dette emnet' @@ -135,7 +139,6 @@ nb_NO: admin_title: "Admin" flags_title: "Rapporteringer" show_more: "vis mer" - show_help: "hjelp" links: "Lenker" links_lowercase: one: "link" @@ -966,10 +969,10 @@ nb_NO: title: "Følger" description: "Antall nye svar vil bli vist for dette emnet. Du vil bli varslet om noen nevner ditt @name eller svarer på ditt innlegg.. " regular: - title: "Aktivt medlem" + title: "Normal" description: "Du vil bli varslet om noen nevner ditt @navn eller svarer på ditt innlegg." regular_pm: - title: "Aktivt medlem" + title: "Normal" description: "Du vil bli varslet om noen nevner ditt @navn eller svarer på ditt innlegg." muted_pm: title: "Dempet" @@ -1510,6 +1513,8 @@ nb_NO: title: "Totalt" yearly: title: "Årlig" + quarterly: + title: "Kvartalsvis" monthly: title: "Månedlig" weekly: @@ -1771,6 +1776,7 @@ nb_NO: header: "Header" top: "Topp" footer: "Footer" + embedded_css: "Innebygd CSS" head_tag: text: "" title: "HTML som settes inn før taggen." @@ -2446,12 +2452,3 @@ nb_NO: reader: name: Leser description: Leste hvert innlegg i et emne med mer enn 100 innlegg - google_search: | -

    Søk med Google

    -

    - - - - - -

    diff --git a/config/locales/client.nl.yml b/config/locales/client.nl.yml index 4343afe8ff..a60c09db8c 100644 --- a/config/locales/client.nl.yml +++ b/config/locales/client.nl.yml @@ -123,7 +123,6 @@ nl: admin_title: "Beheer" flags_title: "Meldingen" show_more: "meer..." - show_help: "help" links: "Links" links_lowercase: one: "link" @@ -943,10 +942,8 @@ nl: title: "Volgen" description: "Het aantal nieuwe reacties op dit bericht wordt weergegeven. Je krijgt een notificatie als iemand je @name noemt of reageert." regular: - title: "Normaal" description: "Je krijgt een notificatie als iemand je @naam noemt of reageert op een bericht van jou." regular_pm: - title: "Normaal" description: "Je krijgt een notificatie als iemand je @naam noemt of reageert op een bericht van jou." muted_pm: title: "Negeren" @@ -2422,12 +2419,3 @@ nl: reader: name: Lezer description: Lees elk bericht in een topic met meer dan 100 berichten. - google_search: | -

    Zoek met Google

    -

    - - - - - -

    diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index e323f6171a..9a2826624a 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -130,23 +130,23 @@ pl_PL: action_codes: split_topic: "podziel ten temat" autoclosed: - enabled: 'zamknięcie tematu %{when}' - disabled: 'otworzenie tematu %{when}' + enabled: 'zamknięcie %{when}' + disabled: 'otworzenie %{when}' closed: - enabled: 'zamknięcie tematu %{when}' - disabled: 'otwarcie tematu %{when}' + enabled: 'zamknięcie %{when}' + disabled: 'otworzenie %{when}' archived: - enabled: 'archiwizacja tematu %{when}' - disabled: 'dearchiwizacja tematu %{when}' + enabled: 'archiwizacja %{when}' + disabled: 'dearchiwizacja %{when}' pinned: - enabled: 'przypięcie tematu %{when}' - disabled: 'odpięcie tematu %{when}' + enabled: 'przypięcie %{when}' + disabled: 'odpięcie %{when}' pinned_globally: - enabled: 'globalne przypięcie tematu %{when}' - disabled: 'globalne odprzypięcie tematu %{when}' + enabled: 'globalne przypięcie %{when}' + disabled: 'globalne odpięcie %{when}' visible: - enabled: 'listowanie tematu %{when}' - disabled: 'odlistowanie tematu %{when}' + enabled: 'wylistowanie %{when}' + disabled: 'odlistowanie %{when}' topic_admin_menu: "akcje administratora" emails_are_disabled: "Wysyłanie e-maili zostało globalnie wyłączone przez administrację. Powiadomienia e-mail nie będą dostarczane." edit: 'edytuj tytuł i kategorię tego tematu' @@ -577,6 +577,7 @@ pl_PL: search: "wpisz aby szukać zaproszeń…" title: "Zaproszenia" user: "Zaproszony(-a) użytkownik(-czka)" + sent: "Wysłane" none: "Jeszcze nikt nie został przez ciebie zaproszony." truncated: "Pokaż pierwsze {{count}} zaproszeń." redeemed: "Cofnięte zaproszenia" @@ -923,6 +924,9 @@ pl_PL: bookmarks: "Nie ma więcej zakładek." search: "Nie znaleziono więcej wyników." topic: + unsubscribe: + stop_notifications: "Będziesz otrzymywać mniej powiadomień o {{title}}" + change_notification_state: "Twój aktualny stan powiadomień to" filter_to: "{{post_count}} wpisów w temacie" create: 'Nowy temat' create_long: 'Utwórz nowy temat' @@ -1866,6 +1870,7 @@ pl_PL: header: "Nagłówki" top: "Nagłówek" footer: "Stopka" + embedded_css: "Osadzony CSS" head_tag: text: "" title: "Kod HTML, który zostanie umieszczony przed tagiem " @@ -2553,7 +2558,7 @@ pl_PL: name: Czytelnik description: Przeczytanie każdego wpisu w temacie z ponad 100 wpisami google_search: | -

    Szukaj z Google

    +

    Wyszukaj z Google

    diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index ba3615baa3..a53e51f80e 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -111,23 +111,23 @@ pt: action_codes: split_topic: "dividir este tópico" autoclosed: - enabled: 'fechou este tópico %{when}' - disabled: 'abriu este tópico %{when}' + enabled: 'fechado %{when}' + disabled: 'aberto %{when}' closed: - enabled: 'fechou este tópico %{when}' - disabled: 'abriu este tópico %{when}' + enabled: 'fechado %{when}' + disabled: 'aberto %{when}' archived: - enabled: 'arquivou este tópico %{when}' - disabled: 'removeu este tópico do arquivo %{when}' + enabled: 'arquivado %{when}' + disabled: 'removido do arquivo %{when}' pinned: - enabled: 'fixou este tópico %{when}' - disabled: 'desafixou este tópico %{when}' + enabled: 'fixado %{when}' + disabled: 'desafixado %{when}' pinned_globally: - enabled: 'fixou este tópico globalmente %{when}' - disabled: 'desafixou este tópico %{when}' + enabled: 'fixado globalmente %{when}' + disabled: 'desafixado %{when}' visible: - enabled: 'listou este tópico %{when}' - disabled: 'removeu este tópico da lista %{when}' + enabled: 'listado %{when}' + disabled: 'removido da lista %{when}' topic_admin_menu: "Ações administrativas dos Tópicos" emails_are_disabled: "Todos os envios de e-mail foram globalmente desativados por um administrador. Nenhum e-mail de notificação será enviado." edit: 'editar o título e a categoria deste tópico' @@ -143,7 +143,7 @@ pt: admin_title: "Administração" flags_title: "Sinalizações" show_more: "mostrar mais" - show_help: "Ajuda" + show_help: "opções" links: "Hiperligações" links_lowercase: one: "hiperligação" @@ -543,6 +543,7 @@ pt: search: "digite para procurar convites..." title: "Convites" user: "Utilizadores Convidados" + sent: "Enviado" none: "Ainda não convidou ninguém." truncated: "A mostrar os primeiros {{count}} convites." redeemed: "Convites Resgatados" @@ -885,6 +886,9 @@ pt: bookmarks: "Não há mais tópicos marcados." search: "Não há mais resultados na pesquisa." topic: + unsubscribe: + stop_notifications: "Irá passar a receber menos notificações para {{title}}" + change_notification_state: "O seu estado de notificação atual é" filter_to: "{{post_count}} mensagens no tópico" create: 'Novo Tópico' create_long: 'Criar um novo Tópico' @@ -2469,7 +2473,7 @@ pt: name: Leitor description: Ler todas as mensagens num tópico com mais de 100 mensagens google_search: | -

    Pesquisar com o Google

    +

    Pesquise com o Google

    diff --git a/config/locales/client.pt_BR.yml b/config/locales/client.pt_BR.yml index 209ddac4b7..82c3450dda 100644 --- a/config/locales/client.pt_BR.yml +++ b/config/locales/client.pt_BR.yml @@ -123,7 +123,6 @@ pt_BR: admin_title: "Admin" flags_title: "Sinalizações" show_more: "mostrar mais" - show_help: "Ajuda" links: "Links" links_lowercase: one: "link" @@ -948,10 +947,8 @@ pt_BR: title: "Monitorar" description: "Um contador de novas respostas será mostrado para este tópico. Você será notificado se alguém mencionar seu @nome ou responder à sua mensagem." regular: - title: "Normal" description: "Você será notificado se alguém mencionar o seu @nome ou responder à sua mensagem." regular_pm: - title: "Regular" description: "Você será notificado se alguém mencionar o seu @nome ou responder à sua mensagem." muted_pm: title: "Silenciado" @@ -2421,12 +2418,3 @@ pt_BR: reader: name: Leitor description: Leia cada resposta em um tópico com mais de 100 respostas - google_search: | -

    Procurar com Google

    -

    - - - - - -

    diff --git a/config/locales/client.ro.yml b/config/locales/client.ro.yml index 49c2a76817..f8486b403c 100644 --- a/config/locales/client.ro.yml +++ b/config/locales/client.ro.yml @@ -121,7 +121,6 @@ ro: admin_title: "Admin" flags_title: "Semnalare" show_more: "Detaliază" - show_help: "ajutor" links: "Adrese" links_lowercase: one: "adresă" @@ -937,10 +936,6 @@ ro: title: "Urmărind" tracking: title: "Urmărind" - regular: - title: "Normal" - regular_pm: - title: "Normal" muted_pm: title: "Silențios" description: "Nu veţi fi niciodată notificat despre acest mesaj." @@ -2389,12 +2384,3 @@ ro: reader: name: Cititorul description: Citeşte fiecare mesaj dintr-o discuție cu mai mult de 100 de mesaje - google_search: | -

    Căutare cu Google

    -

    - - - - - -

    diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 4469060efe..83a2c06df4 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -161,7 +161,6 @@ ru: admin_title: "Админка" flags_title: "Жалобы" show_more: "показать дальше" - show_help: "справка" links: "Ссылки" links_lowercase: one: "ссылка" @@ -1019,10 +1018,8 @@ ru: title: "Следить" description: "Количество непрочитанных сообщений появится рядом с названием этой темы. Вам придёт уведомление, только если кто-нибудь упомянет ваш @псевдоним или ответит на ваше сообщение." regular: - title: "Уведомлять" description: "Вам придёт уведомление, только если кто-нибудь упомянет ваш @псевдоним или ответит на ваше сообщение." regular_pm: - title: "Уведомлять" description: "Вам придёт уведомление, только если кто-нибудь упомянет ваш @псевдоним или ответит на ваше сообщение." muted_pm: title: "Без уведомлений" @@ -2573,12 +2570,3 @@ ru: reader: name: Читатель description: Прочитал каждое сообщение в теме с более чем 100 сообщениями - google_search: | -

    Искать с помощью Google

    -

    - - - - - -

    diff --git a/config/locales/client.sq.yml b/config/locales/client.sq.yml index d30f578ae6..fd6cd73af0 100644 --- a/config/locales/client.sq.yml +++ b/config/locales/client.sq.yml @@ -110,24 +110,6 @@ sq: email: 'dërgo këtë lidhje me email' action_codes: split_topic: "nda këtë temë" - autoclosed: - enabled: 'closed this topic %{when}' - disabled: 'opened this topic %{when}' - closed: - enabled: 'closed this topic %{when}' - disabled: 'opened this topic %{when}' - archived: - enabled: 'archived this topic %{when}' - disabled: 'unarchived this topic %{when}' - pinned: - enabled: 'pinned this topic %{when}' - disabled: 'unpinned this topic %{when}' - pinned_globally: - enabled: 'pinned globally this topic %{when}' - disabled: 'unpinned this topic %{when}' - visible: - enabled: 'listed this topic %{when}' - disabled: 'unlisted this topic %{when}' topic_admin_menu: "topic admin actions" emails_are_disabled: "All outgoing email has been globally disabled by an administrator. No email notifications of any kind will be sent." edit: 'redakto titullin dhe kategorinë e kësaj teme' @@ -143,7 +125,6 @@ sq: admin_title: "Admin" flags_title: "Flags" show_more: "trego më shumë" - show_help: "ndihmë" links: "Lidhjet" links_lowercase: one: "lidhje" @@ -978,10 +959,8 @@ sq: title: "Tracking" description: "A count of new replies will be shown for this topic. You will be notified if someone mentions your @name or replies to you. " regular: - title: "Regular" description: "You will be notified if someone mentions your @name or replies to you." regular_pm: - title: "Regular" description: "You will be notified if someone mentions your @name or replies to you." muted_pm: title: "Muted" @@ -2467,12 +2446,3 @@ sq: reader: name: Lexues description: Read every post in a topic with more than 100 posts - google_search: | -

    Search with Google

    -

    - - - - - -

    diff --git a/config/locales/client.sv.yml b/config/locales/client.sv.yml index c305daee0c..c7d0f0ee6a 100644 --- a/config/locales/client.sv.yml +++ b/config/locales/client.sv.yml @@ -115,7 +115,6 @@ sv: admin_title: "Admin" flags_title: "Flaggningar" show_more: "visa mer" - show_help: "hjälp" links: "Länkar" links_lowercase: one: "länk" @@ -926,10 +925,8 @@ sv: title: "Följer" description: "En räknare över antal nya svar visas för detta ämne. Du notifieras om någon nämner ditt @namn eller svarar dig." regular: - title: "Vanlig" description: "Du kommer att få en notifiering om någon nämner ditt @namn eller svarar dig." regular_pm: - title: "Vanlig(t)" description: "Du kommer att notifieras om någon nämner ditt @namn eller svarar dig." muted_pm: title: "tystade" @@ -2342,12 +2339,3 @@ sv: reader: name: Läsare description: Läs varje inlägg i en diskussion med över 100 inlägg - google_search: | -

    Sök med Google

    -

    - - - - - -

    diff --git a/config/locales/client.te.yml b/config/locales/client.te.yml index 942e03bc79..fbcd93e908 100644 --- a/config/locales/client.te.yml +++ b/config/locales/client.te.yml @@ -105,7 +105,6 @@ te: admin_title: "అధికారి" flags_title: "కేతనాలు" show_more: "మరింత చూపు" - show_help: "సహాయం" links: "లంకెలు" links_lowercase: one: "లంకె" @@ -817,10 +816,6 @@ te: title: "గమనిస్తున్నారు" tracking: title: "గమనిస్తున్నారు" - regular: - title: "రెగ్యులరు" - regular_pm: - title: "రెగ్యులరు" muted_pm: title: "నిశ్శబ్దం" muted: @@ -2084,12 +2079,3 @@ te: reader: name: చదువరి description: 100 టపాల కన్నా ఎక్కువ ఉన్న అంశంలో ప్రతి టపా చదవండి - google_search: | -

    గూగుల్ తో వెతుకు

    -

    - - - - - -

    diff --git a/config/locales/client.tr_TR.yml b/config/locales/client.tr_TR.yml index 3dd0640b7c..8c88dd937d 100644 --- a/config/locales/client.tr_TR.yml +++ b/config/locales/client.tr_TR.yml @@ -104,7 +104,6 @@ tr_TR: admin_title: "Yönetici" flags_title: "Bayraklar" show_more: "devamını göster" - show_help: "yardım" links: "Bağlantılar" links_lowercase: other: "bağlantılar" @@ -899,10 +898,8 @@ tr_TR: title: "Takip Ediliyor" description: "Okunmamış ve yeni gönderi sayısı başlığın yanında belirecek. Birisi @isim şeklinde sizden bahsederse ya da gönderinize cevap verirse bildirim alacaksınız." regular: - title: "Standart" description: "Birisi @isim şeklinde sizden bahsederse ya da gönderinize cevap verirse bildirim alacaksınız." regular_pm: - title: "Standart" description: "Birisi @isim şeklinde sizden bahsederse ya da gönderinize mesajla cevap verirse bildirim alacaksınız." muted_pm: title: "Susturuldu" @@ -2329,12 +2326,3 @@ tr_TR: reader: name: Okuyucu description: 100'den fazla gönderiye sahip bir konudaki tüm gönderileri oku - google_search: | -

    Google ile arayın

    -

    - - - - - -

    diff --git a/config/locales/client.uk.yml b/config/locales/client.uk.yml index 855868f06f..58f39f591f 100644 --- a/config/locales/client.uk.yml +++ b/config/locales/client.uk.yml @@ -8,6 +8,9 @@ uk: js: number: + format: + separator: "." + delimiter: "," human: storage_units: format: '%n %u' @@ -25,6 +28,19 @@ uk: tiny: half_a_minute: "< 1 хв" date_year: "MMM 'YY" + medium: + x_minutes: + one: "1 хвилина" + few: "%{count} хвилини" + other: "%{count} хвилин" + x_hours: + one: "1 година" + few: "%{count} години" + other: "%{count} годин" + x_days: + one: "1 день" + few: "%{count} дні" + other: "%{count} днів" medium_with_ago: x_hours: one: "1 годину тому" @@ -57,7 +73,6 @@ uk: admin_title: "Адмін" flags_title: "Скарги" show_more: "показати більше" - show_help: "Допомога" links: "Посилання" faq: "Часті запитання" guidelines: "Інструкції" @@ -78,6 +93,7 @@ uk: every_two_weeks: "кожні два тижні" every_three_days: "Кожні три дня" max_of_count: "Не більше {{count}}" + alternation: "або" character_count: one: "{{count}} символ" few: "{{count}} символи" @@ -113,6 +129,7 @@ uk: not_bookmarked: "ви прочитали цей допис; натисніть, щоб додати його до закладок" last_read: "це останній допис, що ви прочитали; натисніть, щоб додати його до закладок" remove: "Видалити закладку" + confirm_clear: "Ви впевнені, що хочете видалити всі закладки з цієї теми?" topic_count_latest: one: "{{count}} нова чи оновлена тема." few: "нових чи оновлених тем: {{count}}." @@ -154,9 +171,12 @@ uk: reject: 'Відхилити' delete_user: 'Видалити користувача' title: "Потребує схвалення" + edit: "Редагувати" + cancel: "Скасувати" confirm: "Зберегти зміни" approval: title: "Допис потребує схвалення" + ok: "OK" user_action: user_posted_topic: "{{user}} написав(ла) тему" you_posted_topic: "Ви написали тему" @@ -172,6 +192,11 @@ uk: sent_by_user: "Надіслано користувачем {{user}}" sent_by_you: "Надіслано Вами" directory: + title: "Користувачі" + topic_count: "Теми" + topic_count_long: "Тем створено" + post_count: "Відповіді" + no_results: "Нічого не знайдено." total_rows: one: "%{count} користувач" few: "%{count} користувачі" @@ -196,6 +221,7 @@ uk: '2': "Отримані вподобання" '3': "Закладки" '4': "Теми" + '5': "Відповіді" '7': "Згадки" '9': "Цитати" '10': "Позначені зірочкою" @@ -235,6 +261,8 @@ uk: mute: "Mute" edit: "Редагувати налаштування" download_archive: "Завантажити мої дописи" + new_private_message: "Нове повідомлення" + private_message: "Повідомлення" private_messages: "Повідомлення" activity_stream: "Активність" preferences: "Налаштування" @@ -243,6 +271,13 @@ uk: invited_by: "Запрошений(а)" trust_level: "Рівень довіри" notifications: "Сповіщення" + desktop_notifications: + perm_default: "Ввімкнути сповіщення" + perm_denied_btn: "Немає доступу" + disable: "Вимкнути сповіщення" + currently_enabled: "(зараз увімкнено)" + enable: "Увімкнути сповіщення" + currently_disabled: "(зараз вимкнено)" dismiss_notifications: "Позначити все як прочитане" dismiss_notifications_tooltip: "Позначити всі сповіщення як прочитані" disable_jump_reply: "Не перескакувати до мого допису коли я відповім" @@ -254,8 +289,10 @@ uk: admin: "{{user}} є адміном" moderator_tooltip: "Цей користувач є модератором" admin_tooltip: "Цей користувач є адміністратором" + blocked_tooltip: "Цього користувача заблоковано" suspended_notice: "Цього користувача призупинено до {{date}}." suspended_reason: "Причина: " + github_profile: "Github" mailing_list_mode: "Надсилати всі нові дописи мені електронною поштою (доки я це не вимкну)" watched_categories: "Відслідковувані" tracked_categories: "Відстежувані" @@ -311,8 +348,13 @@ uk: title: "Фон вашої візитки" email: title: "Електронна пошта" + invalid: "Будь ласка, введіть вірний email" name: title: "Ім'я" + instructions: "Ваше повне ім’я (необов’язково)" + instructions_required: "Ваше повне ім’я" + too_short: "Ваше ім’я надто коротке" + ok: "Ваше ім’я виглядає добре" username: title: "Ім'я користувача" global_mismatch: "Вже зареєстровано. Спробуєте {{suggestion}}?" @@ -337,11 +379,15 @@ uk: email_settings: "Електронна пошта" email_digests: daily: "щодня" + every_three_days: "кожні 3 дні" weekly: "щотижня" + every_two_weeks: "кожні 2 тижні" other_settings: "Інше" categories_settings: "Категорії" new_topic_duration: label: "Вважати теми новими, якщо" + not_viewed: "я їх ще не переглянув" + auto_track_topics: "Автоматично слідкувати за темами, що я відвідав" auto_track_options: never: "ніколи" invited: @@ -363,10 +409,13 @@ uk: create: "Надіслати Запрошення" bulk_invite: text: "Масове Запрошення з Файлу" + error: "Під час завантаження '{{filename}}' сталася помилка: {{message}}" password: title: "Пароль" too_short: "Ваш пароль надто короткий." common: "Цей пароль надто простий." + same_as_username: "Ваш пароль ідентичний імені користувача" + same_as_email: "Ваш пароль ідентичний Вашому email" ok: "Ваш пароль добрий." ip_address: title: "Остання IP-адреса" @@ -386,15 +435,20 @@ uk: errors: reasons: network: "Помилка Мережі" + server: "Серверна помилка" + forbidden: "Немає доступу" unknown: "Помилка" desc: network: "Будь ласка, перевірте з'єднання." + server: "Код помилки: {{status}}" + forbidden: "Вам не дозволено це переглядати." unknown: "Щось пішло не так." buttons: back: "Повернутися" again: "Спробувати ще раз" fixed: "Завантаження Сторінки" close: "Закрити" + refresh: "Оновити" read_only_mode: login_disabled: "Login is disabled while the site is in read only mode." learn_more: "дізнатися більше..." @@ -417,8 +471,10 @@ uk: enabled_description: "Ця тема містить видалені дописи, які були сховані." disabled_description: "Видалені дописи в цій темі показано." enable: "Сховати Видалені Дописи" + disable: "Показати видалені дописи" private_message_info: invite: "Запросити інших..." + remove_allowed_user: "Ви впевнені, що хочете видалити {{name}} з цього повідомлення?" email: 'Електронна пошта' username: 'Ім''я користувача' last_seen: 'Помічено востаннє' @@ -629,8 +685,6 @@ uk: title: "Стежити" tracking: title: "Стежити" - regular: - title: "Звичайно" muted_pm: title: "Ігнорувати" muted: @@ -864,6 +918,7 @@ uk: archived: help: "цю тему заархівовано; вона заморожена і її не можна змінити" posts: "Дописи" + posts_lowercase: "дописи" posts_long: "тема містить {{number}} дописів" original_post: "Перший допис" views: "Перегляди" @@ -905,6 +960,8 @@ uk: posted: title: "Мої дописи" help: "теми, в які Ви дописували" + bookmarks: + title: "Закладки" category: title: zero: "{{categoryName}}" @@ -944,6 +1001,7 @@ uk: admins: 'Адміни:' blocked: 'Заблоковані:' suspended: 'Призупинені:' + private_messages_title: "Повідомлення" reports: today: "Сьогодні" yesterday: "Вчора" @@ -999,6 +1057,8 @@ uk: delete: "Видалити" delete_confirm: "Видалити цю групу?" delete_failed: "Не вдалося видалити групу. Якщо це - автоматична група, її неможливо знищити." + add: "Додати" + add_members: "Додати учасників" api: generate_master: "Згенерувати Головний ключ API" none: "Наразі немає жодного активного ключа API." @@ -1055,6 +1115,7 @@ uk: customize: title: "Customize" long_title: "Site Customizations" + css: "CSS" header: "Header" override_default: "Do not include standard style sheet" enabled: "Enabled?" @@ -1283,6 +1344,11 @@ uk: posts_read: "Прочитано Дописів" posts_read_all_time: "Прочитано дописів (весь час)" flagged_posts: "Оскаржені дописи" + user_fields: + save: "Зберегти" + edit: "Редагувати" + delete: "Видалити" + cancel: "Скасувати" site_settings: show_overriden: 'Показувати тільки перевизначені' title: 'Налаштування' @@ -1319,6 +1385,7 @@ uk: no_badges: Тут немає значків, які можуть бути надані. allow_title: Дозволити використання значків як звань multiple_grant: Можуть бути надані декілька разів + image: Зображення trigger_type: post_action: "Коли користувач щось робить у допису" post_revision: "Коли користувач редагує або створює допис" @@ -1326,9 +1393,23 @@ uk: user_change: "Коли користувач змінений або створений" preview: sample: "Зразок:" + permalink: + title: "Постійні посилання" + url: "Посилання" + topic_id: "ID теми" + topic_title: "Тема" + post_id: "ID допису" + post_title: "Допис" + category_id: "ID категорії" + category_title: "Категорія" + external_url: "Зовнішнє посилання" + delete_confirm: Ви впевнені, що хочете видалити це постійне посилання? + form: + add: "Додати" lightbox: download: "звантажити" keyboard_shortcuts_help: + title: 'Поєднання клавіш' jump_to: title: 'Перейти до' categories: 'g, c Категорії' @@ -1370,3 +1451,23 @@ uk: name: Редактор basic_user: description: Надані всі основні функції спільноти + welcome: + name: Ласкаво просимо + autobiographer: + name: Автобіографіст + anniversary: + name: Річниця + nice_topic: + name: Непогана тема + good_topic: + name: Хороша тема + great_topic: + name: Чудова тема + first_link: + name: Перше посилання + first_quote: + name: Перша цитата + read_guidelines: + name: Читати інструкції + reader: + name: Читач diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 9dbe8fef21..8a27f72d5c 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -91,24 +91,6 @@ zh_CN: email: '用电子邮件发送这个链接' action_codes: split_topic: "分割该主题" - autoclosed: - enabled: '主题关闭于 %{when}' - disabled: '主题开启于 %{when}' - closed: - enabled: '主题关闭于 %{when}' - disabled: '主题开启于 %{when}' - archived: - enabled: '主题存档于 %{when}' - disabled: '主题解除存档于 %{when}' - pinned: - enabled: '主题置顶于 %{when}' - disabled: '主题接触置顶于 %{when}' - pinned_globally: - enabled: '主题全局置顶于 %{when}' - disabled: '主题接触置顶于 %{when}' - visible: - enabled: '主题在列表中显示于 %{when}' - disabled: '主题在列表中不显示于 %{when}' topic_admin_menu: "主题管理操作" emails_are_disabled: "所有的出站邮件已经被管理员全局禁用。将不发送任何邮件提醒。" edit: '编辑本主题的标题和分类' @@ -124,7 +106,6 @@ zh_CN: admin_title: "管理" flags_title: "标记" show_more: "显示更多" - show_help: "帮助" links: "链接" links_lowercase: other: "链接" @@ -936,10 +917,8 @@ zh_CN: title: "追踪" description: "该主题标题后将显示新回复数量。你只会在别人@你或回复你的帖子时才会收到通知。" regular: - title: "常规" description: "如果某人@你或者回复你,你将收到通知。" regular_pm: - title: "常规" description: "如果某人@你或者回复你,你将收到通知。" muted_pm: title: "防打扰" @@ -2378,12 +2357,3 @@ zh_CN: reader: name: 读者 description: 阅读一个超过 100 个帖子的主题中的每一个帖子 - google_search: | -

    用 Google 搜索

    -

    - - - - - -

    diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 96ec2e956a..cac2a7fe26 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -89,7 +89,6 @@ zh_TW: admin_title: "管理員" flags_title: "投訴" show_more: "顯示更多" - show_help: "幫助" links: "連結" links_lowercase: other: "鏈結" @@ -843,10 +842,6 @@ zh_TW: title: "追蹤" tracking: title: "追蹤" - regular: - title: "普通" - regular_pm: - title: "一般" muted_pm: title: "靜音" description: "你將不會再收到關於此訊息的通知。" @@ -2191,12 +2186,3 @@ zh_TW: reader: name: 讀者 description: 觀看每個超過100篇文章的討論話題 - google_search: | -

    Google 搜尋

    -

    - - - - - -

    diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index 23e65e0b6d..cc247b3007 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -776,6 +776,8 @@ ar: allow_moderators_to_create_categories: "السماح للمشرفين إنشاء قسم جديد" min_password_length: "أقل طول لكلمة المرور" block_common_passwords: "لا تسمح لكلمات المرور المسجلة في قائمة كلمات المرور الشائعةز" + sso_url: "نقطة نهاية URL الدخول الموحد" + sso_not_approved_url: "إعادة التوجيه لم توافق على حسابات SSO لهذا URL" enable_yahoo_logins: "تفعيل مصادقة ياهو" google_oauth2_client_id: "التسجيل بحسابك الشخصي في جوجل" enable_twitter_logins: "تفعيل مصادقة تويتر , يطلب : twitter_consumer_key و twitter_consumer_secret" @@ -817,12 +819,16 @@ ar: notification_types: mentioned: "%{display_username} ذكرك في %{link}" liked: "%{display_username} أعجب بمشاركتك في %{link}" - replied: "%{display_username} ردعلى مشاركتك في %{link}" + replied: "%{display_username} رد على مشاركتك في %{link}" quoted: "%{display_username} أقتبس مشاركتك في %{link}" edited: "%{display_username} عدل مشاركتك في %{link}" posted: "%{display_username} شارك في %{link}" moved_post: "%{display_username} نقل مشاركتك إلى %{link}" - private_message: "%{display_username} أرسلت لك رسالة: %{link}" + private_message: "%{display_username} أرسل لك رسالة: %{link}" + invited_to_private_message: "%{display_username} دعاك لرسالة: %{link}" + invited_to_topic: "%{display_username} دعاك لموضوع: %{link}" + invitee_accepted: "%{display_username} قبل دعوتك" + linked: "%{display_username} ربطك في %{link}" granted_badge: "كسبت %{link}" search: within_post: "#%{post_number} بواسطة %{username}" @@ -886,6 +892,8 @@ ar: subject_template: "[%{site_name}] بريد الكتروني بهدف الاختبار" new_version_mailer: subject_template: "[%{site_name}] يوجد اصدار جديد , تحديث متوفر" + new_version_mailer_with_notes: + subject_template: "[%{site_name}] التحديث متوفر" flags_reminder: please_review: "يرجى مراجعة ذلك " post_number: "مشاركة" diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index bd2029c269..6757d882ad 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -1111,7 +1111,6 @@ bs_BA: download_remote_images_disabled: subject_template: "Downloading remote images disabled" text_body_template: "The `download_remote_images_to_local` setting was disabled because the disk space limit at `download_remote_images_threshold` was reached." - unsubscribe_link: "Da se odjavite od ovih email-a, posjetite vaše [korisničke postavke](%{user_preferences_url})." subject_re: "Re: " subject_pm: "[PM] " user_notifications: diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index 84650518bb..d185b59964 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -766,7 +766,6 @@ cs: There are new user signups waiting to be approved (or rejected) before they can access this forum. [Please review them in the admin section](%{base_url}/admin/users/list/pending). - unsubscribe_link: "Pokud již od nás nechcete dostávat emaily, navšivte stránku s vašimi [uživatelskými preferencemi](%{user_preferences_url})." user_notifications: previous_discussion: "Předchozí diskuze" unsubscribe: diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index 2afdab7cc8..68addb92fa 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -713,7 +713,6 @@ da: Der er nye brugere, som afventer godkendelse (eller afvisning) før de kan tilgå dette forum. [Gennemgå dem venligst på administrationssiden](%{base_url}/admin/users/list/pending). - unsubscribe_link: "Hvis du ikke ønsker at modtage disse mails, du kan framelde dem på [din brugerprofil](%{user_preferences_url})." user_notifications: previous_discussion: "Forrige svar" unsubscribe: diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 914a8fb37d..356ef447ba 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -1340,7 +1340,6 @@ de: download_remote_images_disabled: subject_template: "Download von externen Bildern deaktiviert" text_body_template: "Die `download_remote_images_to_local` Einstellung wurde deaktiviert, da das Speicherplatz Limit von `download_remote_images_threshold` erreicht wurde." - unsubscribe_link: "Wenn du diese E-Mails nicht mehr erhalten möchtest, verändere deine [Benutzereinstellungen](%{user_preferences_url})." subject_re: "Re: " subject_pm: "[PN]" user_notifications: diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index ac42efdaae..825daf4a65 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -819,6 +819,7 @@ es: github_client_secret: "Client secret para la autenticación por Github, registrado en https://github.com/settings/applications" allow_restore: "Permitir restauración, la cual puede sobreescribir TODOS los datos del sitio! Dejela en falso a menos que tenga planeado recuperar sus datos desde una copia de respaldo. " maximum_backups: "La cantidad máxima de copias de seguridad a tener en el disco. Las copias de seguridad más antiguas se eliminan automáticamente" + automatic_backups_enabled: "Ejecutar backups automáticos definidos por la opción de frecuencia de backups" backup_frequency: "Con qué frecuencia, en días, crearemos un backup del sitio." enable_s3_backups: "Sube copias de seguridad a S3 cuando complete. IMPORTANTE: requiere credenciales validas de S3 puestas Archivos configuración." s3_backup_bucket: "El bucket remoto para mantener copias de seguridad. AVISO: Asegúrate de que es un bucket privado." @@ -1512,7 +1513,10 @@ es: download_remote_images_disabled: subject_template: "Inhabilitar la descarga de imágenes remotas" text_body_template: "La opción `download_remote_images_to_local` ha sido inhabilitada porque se ha llegado al límite de espacio en disco configurado en `download_remote_images_threshold`." - unsubscribe_link: "Si quieres eliminar tu suscripción a estos emails, visita tu [user preferences](%{user_preferences_url})." + unsubscribe_link: | + Para dar de baja tu suscripción a estos emails, visita tus [preferencias](%{user_preferences_url}). + + Para dejar de recibir notificaciones sobre este tema en particular, [haz clic aquí](%{unsubscribe_url}). subject_re: "Re:" subject_pm: "[MP]" user_notifications: @@ -1741,27 +1745,27 @@ es: title: "Políticas de Privacidad" static: search_help: | -

    Consejos

    +

    Consejos

      -
    • Se prioriza que la búsqueda coincida con el título, por lo que en caso de duda, busca por títulos
    • +
    • Se priorizan las coincidencias por título – de manera que si tienes dudas, mejor busca por títulos
    • Buscar palabras diferentes, no muy comunes, siempre dará mejores resultados
    • Cuando sea posible, centra tu búsqueda en una categoría, un tema o un usuario en concreto

    -

    Opciones

    +

    Opciones

    order:viewsorder:latest
    {{fa-icon "download"}}{{i18n 'admin.backups.operations.download.label'}} - {{#if model.isOperationRunning}} + {{#if status.model.isOperationRunning}} {{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger" disabled="true" title="admin.backups.operations.is_running"}} - {{d-button icon="play" action="startRestore" actionParam=backup disabled=model.restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} + {{d-button icon="play" action="startRestore" actionParam=backup disabled=status.model.restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} {{else}} {{d-button icon="trash-o" action="destroyBackup" actionParam=backup class="btn-danger" title="admin.backups.operations.destroy.title"}} - {{d-button icon="play" action="startRestore" actionParam=backup disabled=model.restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} + {{d-button icon="play" action="startRestore" actionParam=backup disabled=status.model.restoreDisabled title=restoreTitle label="admin.backups.operations.restore.label"}} {{/if}}
    - +
    order:viewsorder:latest
    status:openstatus:closedstatus:archivedstatus:norepliesstatus:single_user
    category:foouser:foo
    category:cualquierauser:mengano
    in:likesin:postedin:watchingin:trackingin:private
    in:bookmarksin:first
    posts_count:nummin_age:daysmax_age:days

    - arcoiris category:parques status:open order:latest buscará temas que contengan la palabra "arcoiris" en la categoría "parques" que no estén cerrados o archivados, ordenados por la fecha de la última publicación. + gatos category:parques status:open order:latest buscará temas que contengan la palabra "gatos" en la categoría "parques" que no estén cerrados o archivados, ordenados por fecha del último post.

    badges: long_descriptions: diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index af2c236f68..60c71fcf50 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -1292,7 +1292,6 @@ fa_IR: download_remote_images_disabled: subject_template: "دانلود کردن عکس های سیار از کار افتاده" text_body_template: " تنظیمات `download_remote_images_to_local` غیرفعال شده زیرا به محدودیت فضای دیسک در `download_remote_images_threshold` رسیده شده است. " - unsubscribe_link: "برای لغو اشتراک ار ایمیل ها٬‌ بازدید کن از [تنظیمات کاربر] (%{user_preferences_url})." subject_re: "پاسخ:" subject_pm: "[PM] " user_notifications: diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index ce40c4d852..fb87dbf2bb 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -1626,7 +1626,6 @@ fi: download_remote_images_disabled: subject_template: "Linkattujen kuvien lataaminen on otettu pois käytöstä" text_body_template: "Asetus `download_remote_images_to_local` on otettu pois käytöstä, koska vapaan tilan rajoitus `download_remote_images_threshold` saavutettiin." - unsubscribe_link: "Muokkaa [käyttäjäasetuksiasi](%{user_preferences_url}) lopettaaksesi nämä viestit." subject_re: "VS:" subject_pm: "[YV]" user_notifications: @@ -1853,30 +1852,6 @@ fi: title: "Käyttöehdot" privacy_topic: title: "Rekisteriseloste" - static: - search_help: | -

    Vinkkejä

    -

    -

      -
    • Otsikon vastaavuutta priorisoidaan, joten jos olet epävarma, etsi otsikkoa
    • -
    • Uniikit, harvinaiset sanat tuottavat aina parhaan lopputuloksen
    • -
    • Jos mahdollista, rajoita hakusi tietylle alueelle, käyttäjään tai ketjuun
    • -
    -

    -

    Vaihtoehdot

    -

    - - - - - - - -
    order:viewsorder:latest
    status:openstatus:closedstatus:archivedstatus:norepliesstatus:single_user
    category:foouser:foo
    in:likesin:postedin:watchingin:trackingin:private
    in:bookmarksin:first
    posts_count:nummin_age:daysmax_age:days
    -

    -

    - sateenkaaria category:puistot status:open order:latest etsii ketjuja, joissa käytetään sanaa "sateenkaaria", alueella "puistot" ja joita ei ole suljettu tai arkistoitu, ja antaa tulokset järjestettynä ketjun viimeisimmän viestin päivämäärän mukaan. -

    admin_login: success: "Sähköposti lähetetty" error: "Virhe!" diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index a64f040504..137666d213 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -1594,7 +1594,6 @@ fr: download_remote_images_disabled: subject_template: "Téléchargement d'images distantes désactivé" text_body_template: "Le paramètre `download_remote_images_to_local` a été désactivé car la limite (`download_remote_images_threshold`) d'espace disque utilisé par les images vient d'être dépassée." - unsubscribe_link: "Si vous ne souhaitez plus recevoir ces courriels, visitez vos [préférences utilisateur](%{user_preferences_url})." subject_re: "Re:" subject_pm: "[MP]" user_notifications: diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 457843d5d9..5b5fc89d7b 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -1327,7 +1327,6 @@ he: download_remote_images_disabled: subject_template: "הורדת תמונות מרחוק מנוטרלת" text_body_template: "האפשרות \"הורדת תמונות מרוחקות\" נוטרלה בגלל שכל שטח האכסון שמוקצה ל\"תמונות שהורדו מרחוק\" נוצל." - unsubscribe_link: "To unsubscribe from these emails, visit your [user preferences](%{user_preferences_url})." subject_re: "תגובה: " subject_pm: "[PM] " user_notifications: diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 6b8163f584..0e72e1dead 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -1146,7 +1146,6 @@ it: download_remote_images_disabled: subject_template: "Lo scaricamento delle immagini remote è disabilitato" text_body_template: "L'impostazione `download_remote_images_to_local` è stata disabilitata perché è stato raggiunto il limite di spazio su disco definito in `download_remote_images_threshold`." - unsubscribe_link: "Se vuoi annullare l'iscrizione a queste email, visita le tue [preferenze utente](%{user_preferences_url})." subject_re: "R:" subject_pm: "[MP]" user_notifications: diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index da0c8d3049..2c997ccda0 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -1470,7 +1470,6 @@ ja: download_remote_images_disabled: subject_template: "リモート画像のダウンロードを無効化" text_body_template: "'download_remote_images_threshold'の制限に達したため、`download_remote_images_to_local`の設定は無効になりました" - unsubscribe_link: "メール解除は [ユーザ設定画面](%{user_preferences_url}) で行ってください。" subject_re: "Re:" subject_pm: "[プライベートメッセージ]" user_notifications: diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 695e026561..0b2b79d333 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -1410,7 +1410,6 @@ ko: download_remote_images_disabled: subject_template: "외부에서 이미지 다운로드 못함" text_body_template: "`download_remote_images_to_local` 설정이`download_remote_images_threshold`에서 정한 디스크 용량에 도달하여 비활성화 되었습니다." - unsubscribe_link: "만약 구독해지를 원하시면 [사용자 환경설정](%{user_preferences_url})을 방문하세요." subject_re: "덧: " subject_pm: "[PM] " user_notifications: @@ -1634,30 +1633,6 @@ ko: title: "서비스 이용약관" privacy_topic: title: "개인정보취급방침" - static: - search_help: | -

    -

    -

      -
    • 제목 매칭이 제일 우선이므로, 잘 모르겠으면 제목으로 검색을 먼저 해보세요.
    • -
    • 유니크하고 흔치 않은 단어들을 입력하는게 최상의 결과를 줍니다.
    • -
    • 가능하다면 특정 카테고리, 사용자, 토픽으로 범위를 좁혀보세요.
    • -
    -

    -

    옵션

    -

    - - - - - - - -
    order:viewsorder:latest
    status:openstatus:closedstatus:archivedstatus:norepliesstatus:single_user
    category:foouser:foo
    in:likesin:postedin:watchingin:trackingin:private
    in:bookmarksin:first
    posts_count:nummin_age:daysmax_age:days
    -

    -

    - 무지개 category:공원 status:open order:latest 는 "무지개"를 "공원" 카테고리에서 최신순으로 닫히지 않고 아카이브되지 않은 것들을 검색합니다. -

    admin_login: success: "이메일 보냄" error: "에러!" diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 2818b0550e..9ccb24406a 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -1028,7 +1028,6 @@ nl: [Beoordeel deze gebruikers in het admingedeelte](/admin/users/list/pending). download_remote_images_disabled: subject_template: "Het downloaden van externe afbeeldingen is uitgeschakeld" - unsubscribe_link: "Om deze e-mails niet langer willen ontvangen, ga naar [je gebruikersinstellingen](%{user_preferences_url})." subject_re: "Re:" subject_pm: "[PM]" user_notifications: diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index e07b810a69..1677c38ad6 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -1012,7 +1012,6 @@ pl_PL: one: "1 użytkownik czeka na zatwierdzenie" few: "%{count} użytkowników czeka na zatwierdzenie" other: "%{count} użytkowników czeka na zatwierdzenie" - unsubscribe_link: "Możesz zrezygnować z powiadomień email w [ustawieniach konta](%{user_preferences_url})." subject_re: "Re: " subject_pm: "[PW] " user_notifications: @@ -1161,31 +1160,6 @@ pl_PL: \ plików cookies?](#cookies)\n\nTak. Cookies to małe pliki, wysyłane przez serwis internetowy, który odwiedzamy i zapisywane na urządzeniu końcowym (komputerze, laptopie, smartfonie), z którego korzystamy podczas przeglądania stron internetowych. Pozwalają naszej stronie na rozpoznanie czy jesteś naszym użytkownikiem.\n\nUżywamy ich, aby zrozumieć i zapisać twoje preferencje. Dzięki nim możemy zapewnić Tobie lepszą jakość dostarczanych usług. Możemy teżudostępniać je firmom trzecim, aby mogły rozpoznawać twoje zachowania. \n\n\n\n## [Czy udostępniamy jakiekolwiek informacje osobom trzecim?](#disclose)\n\nNie sprzedajemy, anie nie oddajemy żadnych informacji naszym partnerom.\ \ emy używa do celów komercyjnych, marketingowych i innych. \n\n\n\n## [Linki osób trzecich](#third-party)\n\nNa naszej stronie możemy promować linki osób trzecich. Strony te mają swoją oddzielną politykę prywatności. Nie bierzemy żadnej odpowiedzialności za te strony. \n\n\n\n## [Children's Online Privacy Protection Act Compliance](#coppa)\n\nOur site, products and services are all directed to people who are at least 13 years old or older. If this server is in the USA, and you are under the age of 13, per the requirements of COPPA ([Children's Online Privacy Protection Act](http://en.wikipedia.org/wiki/Children)), do not use this site.\n\n\n\n## [Online Privacy Policy Only](#online)\n\nTa polityka prywatności odnosi się tylko do danych zbieranych przez przez naszą strone, nie odnosi się ona do informacji zbieranych w trybie offline. \n\n\n\n## [Twoja Zgoda](#consent)\n\nKorzystając z tej strony zgadzasz się na jej politykę prywatności.\n\n\n\n## [Zmiany w polityce prywatności](#changes)\n\nJeżeli zostaną zmienione zasady polityki prywatności, zostaną one opublikowane na tej stronie. \n\nThis document is CC-BY-SA. It was last updated May 31, 2013.\n" - static: - search_help: |+ -

    Triki

    -

    -

      -
    • Powiązania tytułów są bardzo ważne, aby w razie wątpliwości móc je wyszukać
    • -
    • Unikatowe oraz rzadkie wyrazy zawsze będą dawać lepsze rezultaty
    • -
    • Ilekroć to możliwe, ogranicz zakres wyszukiwania do wybranych kategorii, użytkowników lub tematów
    • -
    -

    -

    Opcje

    -

    - - - - - - - -
    order:viewsorder:latest
    status:openstatus:closedstatus:archivedstatus:norepliesstatus:single_user
    category:foouser:foo
    in:likesin:postedin:watchingin:trackingin:private
    in:bookmarksin:first
    posts_count:nummin_age:daysmax_age:days
    -

    -

    - rainbows category:parks status:open order:latest will search for topics containing the word "rainbows" in the category "parks" that are not closed or archived, ordered by date of last post. -

    - admin_login: success: "Email wysłany" error: "Błąd!" diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index 08b67dd49e..d702d48251 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -831,6 +831,7 @@ pt: github_client_secret: "Chave do Cliente para autenticação pelo Github, registado em https://github.com/settings/applications" allow_restore: "Permitir o restauro, que pode substituir TODOS os dados do sítio. Deixar a falso a menos que planeie restaurar uma cópia de segurança" maximum_backups: "Valor máximo de cópias de segurança a serem guardadas em disco. Cópias de Segurança antigas são automaticamente eliminadas." + automatic_backups_enabled: "Executar cópias de segurança automáticas de acordo com as definições de frequência de cópia de segurança" backup_frequency: "Com que frequência criamos cópias de segurança do sítio, em dias." enable_s3_backups: "Carregar cópias de segurança para S3 quando completo. IMPORTANTE: requer credenciais S3 válidas inseridas nas configurações dos ficheiros." s3_backup_bucket: "Balde remoto para guardar cópias de segurança. AVISO: Certifique-se que este é um balde privado." @@ -1470,7 +1471,10 @@ pt: download_remote_images_disabled: subject_template: "Descarregamento de imagens remotas desativado" text_body_template: "A configuração `download_remote_images_to_local` foi desativada porque o limite de espaço em disco em `download_remote_images_threshold` foi alcançado." - unsubscribe_link: "Para cancelar a subscrição destes emails, visite as suas [preferências de utilizador](%{user_preferences_url})." + unsubscribe_link: | + Para cancelar a subscrição destes emails, visite as suas [preferências de utilizador](%{user_preferences_url}). + + Para parar de receber notificações acerca deste tópico específico, [clique aqui](%{unsubscribe_url}). subject_re: "Re:" subject_pm: "[MP]" user_notifications: @@ -1792,15 +1796,15 @@ pt: Este documento é CC-BY-SA. Foi atualizado pela última vez em 31 de Maio de 2013. static: search_help: | -

    Dicas

    +

    Dicas

      -
    • A correspondência de títulos está priorizada, por isso em caso de dúvida, pesquise por títulos
    • -
    • Palavras únicas e incomuns irão sempre produzir os melhores resultados
    • -
    • Sempre que possível, direcione a sua pesquisa para uma categoria, utilizador ou tópico em particular
    • +
    • A correspondência de títulos está priorizada – em caso de dúvida, pesquise por títulos
    • +
    • Palavras únicas e incomuns irão sempre produzir os melhores resultados
    • +
    • Tente pesquisar dentro de uma categoria, utilizador ou tópico em particular

    -

    Opções

    +

    Opções

    @@ -1812,7 +1816,7 @@ pt:
    order:viewsorder:latest

    - rainbows category:parks status:open order:latest irá retornar uma pesquisa por tópicos que contém a palavra "rainbows" na categoria "parks" que não estão fechados ou arquivados, ordenados pela data da última mensagem. + rainbows category:parks status:open order:latest irá retornar uma pesquisa por tópicos que contêm a palavra "rainbows" na categoria "parks" que não estão fechados ou arquivados, ordenados pela data da última mensagem.

    badges: long_descriptions: diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index d8b9201e8b..6ea4621962 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -1410,7 +1410,6 @@ pt_BR: download_remote_images_disabled: subject_template: "Baixar imagens remotas desativado" text_body_template: "A configuração `download_remote_images_to_local` está desativada porque o espaço limite no disco configurado em `download_remote_images_threshold` foi alcançado." - unsubscribe_link: "Se você deseja se desinscrever destes emails, visite suas [preferências do usuário](%{user_preferences_url})." subject_re: "Re:" subject_pm: "[PM]" user_notifications: diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index 0ab5c63c9d..fc589c87cf 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -1030,7 +1030,6 @@ ro: download_remote_images_disabled: subject_template: "Downloadul de imagini de legătura dezactivat" text_body_template: "Funcția `download_remote_images_to_local` a fost dezactivată pentru că limita de spațiu din `download_remote_images_threshold` a fost atinsă." - unsubscribe_link: "Pentru dezabonare de la aceste emailuri, vizitați pagina [preferințele utilizatorului](%{user_preferences_url})." subject_re: "Re: " subject_pm: "[PM] " user_notifications: diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index e7ba6a9658..faade29e25 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -1563,7 +1563,6 @@ ru: download_remote_images_disabled: subject_template: "Загрузка копий изображений выключена" text_body_template: "Настройка `download_remote_images_to_local` была отключена, т.к. диск заполнился до отменки, указанной в настройке `download_remote_images_threshold`." - unsubscribe_link: "Для того, чтобы отписаться от подобных сообщений, перейдите в [настройки профиля](%{user_preferences_url})." subject_re: "Re:" subject_pm: "[PM]" user_notifications: @@ -1871,30 +1870,6 @@ ru: If we decide to change our privacy policy, we will post those changes on this page. This document is CC-BY-SA. It was last updated May 31, 2013. - static: - search_help: | -

    Подсказки

    -

    -

      -
    • Если не получается найти то, что нужно, ищите по словам из заголовка, т.к. заголовки всегда приоритетнее в результатах поиска.
    • -
    • Самые точные результаты поиска будут по уникальным и не частовстречающимся словам.
    • -
    • Попробуйте сузить поиск до определенного раздела, автора или даже определенной темы.
    • -
    -

    -

    Возможности поиска

    -

    - - - - - - - -
    order:viewsorder:latest
    status:openstatus:closedstatus:archivedstatus:norepliesstatus:single_user
    category:foouser:foo
    in:likesin:postedin:watchingin:trackingin:private
    in:bookmarksin:first
    posts_count:nummin_age:daysmax_age:days
    -

    -

    - Пример: радуга category:парки status:open order:latest. Такой поиск выдаст незакрытые и незаархивированные темы со словом "радуга", отсортированные по дате последнего сообщения. -

    badges: long_descriptions: autobiographer: | diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index 3055c6c5fb..3cf162ef81 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -1624,7 +1624,6 @@ sq: download_remote_images_disabled: subject_template: "Downloading remote images disabled" text_body_template: "The `download_remote_images_to_local` setting was disabled because the disk space limit at `download_remote_images_threshold` was reached." - unsubscribe_link: "To unsubscribe from these emails, visit your [user preferences](%{user_preferences_url})." subject_re: "Re: " subject_pm: "[PM] " user_notifications: @@ -2165,30 +2164,6 @@ sq: If we decide to change our privacy policy, we will post those changes on this page. This document is CC-BY-SA. It was last updated May 31, 2013. - static: - search_help: | -

    Tips

    -

    -

      -
    • Title matches are prioritized, so when in doubt, search for titles
    • -
    • Unique, uncommon words will always produce the best results
    • -
    • Whenever possible, scope your search to a particular category, user, or topic
    • -
    -

    -

    Options

    -

    - - - - - - - -
    order:viewsorder:latest
    status:openstatus:closedstatus:archivedstatus:norepliesstatus:single_user
    category:foouser:foo
    in:likesin:postedin:watchingin:trackingin:private
    in:bookmarksin:first
    posts_count:nummin_age:daysmax_age:days
    -

    -

    - rainbows category:parks status:open order:latest will search for topics containing the word "rainbows" in the category "parks" that are not closed or archived, ordered by date of last post. -

    badges: long_descriptions: autobiographer: | diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 02be9c535b..6e939fcfd3 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -977,7 +977,6 @@ sv: subject_template: one: "1 användare väntar på godkännande" other: "%{count} användare väntar på godkännande" - unsubscribe_link: "To unsubscribe from these emails, visit your [user preferences](%{user_preferences_url})." subject_re: "Sv:" user_notifications: previous_discussion: "Föregående Svar" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 0da1bd0014..053737520b 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -1308,7 +1308,6 @@ tr_TR: download_remote_images_disabled: subject_template: "Uzaktaki resimlerin indirilmesi devre dışı bırakıldı" text_body_template: "`download_remote_images_to_local` ayarı harddisk alanı limiti `download_remote_images_threshold` aşıldığı için devre dışı bırakıldı." - unsubscribe_link: "Bu e-postalara aboneliğinizi iptal etmek için [kullanıcı ayarları](%{user_preferences_url}) sayfanızı ziyaret edin." subject_re: "Cvp:" subject_pm: "[ÖM]" user_notifications: @@ -1844,8 +1843,6 @@ tr_TR: If we decide to change our privacy policy, we will post those changes on this page. This document is CC-BY-SA. It was last updated May 31, 2013. - static: - search_help: "

    Öneriler

    \n

    \n

      \n
    • Başlık eşleşmelerine öncelik verilir, emin olmadığınız zaman, başlık için arama yapın
    • \n
    • Her zaman tekil, nadir kelimeler en iyi sonuçları verir
    • \n
    • Mümkünse, aramanızı belirli bir kategori, kullanıcı, veya konuya daraltın
    • \n
    \n

    \n

    Seçenekler

    \n

    \n \n \n\n \n\nin:bookmarks\n\n
    order:viewsorder:latest
    status:openstatus:closedstatus:archivedstatus:norepliesstatus:single_user
    category:foouser:foo
    in:likesin:postedin:watchingin:trackingin:private
    in:first
    posts_count:nummin_age:daysmax_age:days
    \n

    \n

    \n gökkuşağı category:parks status:open order:latest \"parklar\" kategorisinde, kapalı veya arşivlenmiş olmayan konular arasında \"gökkuşağı\" kelimesi içerenleri arar ve sonuçları gönderi tarihi sırasına göre gösterir.

    \n" badges: long_descriptions: autobiographer: | diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 938b06e466..9ad9d639e0 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -473,7 +473,6 @@ uk: [Будь ласка, розгляньте їх в адміністративній секції](%{base_url}/admin/users/list/pending). download_remote_images_disabled: subject_template: "Завантаження віддалених зображені відключено" - unsubscribe_link: "Щоб відписатися від цих листів, відідайте [сторінку своїх налаштувань](%{user_preferences_url})." user_notifications: previous_discussion: "Попередні відповіді" unsubscribe: diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index d099202ba5..13821a1111 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -1560,7 +1560,6 @@ zh_CN: download_remote_images_disabled: subject_template: "远程图片下载已禁用。" text_body_template: "`download_remote_images_to_local` 设定已被禁用,因为已经达到 ``download_remote_images_threshold` 设定中的磁盘空间限制。" - unsubscribe_link: "如果你希望取消这些邮件的订阅,请访问你的[用户设置](%{user_preferences_url})。" subject_re: "回复:" subject_pm: "[私信]" user_notifications: @@ -2006,29 +2005,6 @@ zh_CN: 如果我们决定更改我们的隐私政策,我们将在此页更新这些改变。 文档以 CC-BY-SA 发布。最后更新时间为2013年5月31日。 - static: - search_help: | -

    小技巧

    -

    -

      -
    • 匹配有优先级区别。不确定时,搜索标题
    • -
    • 独一无二的不常见的单词将总是产生最好的结果
    • -
    • 只要有可能,限制你的搜索范围至一个特定的分类、用户或主题
    • -
    -

    -

    选项

    -

    - - - - - - - -
    order:viewsorder:latest
    status:openstatus:closedstatus:archivedstatus:norepliesstatus:single_user
    category:foouser:foo
    in:likesin:postedin:watchingin:trackingin:private
    in:bookmarksin:first
    posts_count:nummin_age:daysmax_age:days
    -

    -

    - 彩虹 category:公园 status:open order:latest 将搜索在“公园”分类中没有关闭或存档中的名字包含“彩虹”的主题,并按最后一个帖子的日期来排序。

    badges: long_descriptions: autobiographer: | diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index f05f8ae03b..4c8efad4cf 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -952,7 +952,6 @@ zh_TW: other: "%{count} 個用戶等待審核" download_remote_images_disabled: subject_template: "停用下載遠端圖片" - unsubscribe_link: "如果你希望取消這些郵件的訂閱,請瀏覽 [用戶偏好設定](%{user_preferences_url})。" subject_re: "回覆:" subject_pm: "[私訊]" user_notifications: diff --git a/plugins/poll/config/locales/server.uk.yml b/plugins/poll/config/locales/server.uk.yml index 7e72e9bbe6..026536d754 100644 --- a/plugins/poll/config/locales/server.uk.yml +++ b/plugins/poll/config/locales/server.uk.yml @@ -9,3 +9,14 @@ uk: site_settings: poll_enabled: "Дозволити користувачам створювати опитування?" poll_maximum_options: "Максимальна кількість варіантів в опитуванні" + poll: + default_poll_must_have_at_least_2_options: "Голосування повинно мати щонайменше 2 варіанти." + named_poll_must_have_at_least_2_options: "Голосування під назвою %{name} повинно мати щонайменше 2 варіанти." + default_poll_must_have_different_options: "Голосування повинно мати різні варіанти." + named_poll_must_have_different_options: "Голосування під назвою %{name} повинно мати різні варіанти." + requires_at_least_1_valid_option: "Ви маєте обрати щонайменше 1 валідний варіант." + cannot_change_polls_after_5_minutes: "Ви не можете додавати, видаляти або перейменовувати голосування протягом перших 5 хвилин." + no_polls_associated_with_this_post: "Для цього допису голосування відсутні." + no_poll_with_this_name: "Для цього допису відсутнє голосування під назвою %{name}." + topic_must_be_open_to_vote: "Тема має бути відкрита для голосування." + poll_must_be_open_to_vote: "Опитування має бути відкритим для голосування." diff --git a/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.uk.yml b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.uk.yml index 7130995ba4..5a24cdaf53 100644 --- a/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.uk.yml +++ b/vendor/gems/discourse_imgur/lib/discourse_imgur/locale/server.uk.yml @@ -8,4 +8,5 @@ uk: site_settings: enable_imgur: "Завантажувати зображення на imgur замість того, щоб зберігати їх локально" - imgur_client_id: "Потрібен ваш imgur.com client ID, щоб завантажування могло працювати" + imgur_client_id: "Ваше client ID imgur.com. Необхідне для завантаження зображень" + imgur_client_secret: "Ваш секретний ключ imgur.com. Зараз його можна не вказувати, але він може знадобитися." From 11d1619e2c5239dd5f7fb1446c313e13a60fc26d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 20 Aug 2015 11:06:23 -0400 Subject: [PATCH 160/237] Hack to allow posts to have access to `siteSettings` --- app/assets/javascripts/discourse/models/post.js.es6 | 6 ++++++ .../javascripts/discourse/views/cloaked-collection.js.es6 | 7 +++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index 51e586014e..3719dc3d0d 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -11,6 +11,12 @@ const Post = RestModel.extend({ this.set('replyHistory', []); }, + @computed() + siteSettings() { + // TODO: Remove this once one instantiate all `Discourse.Post` models via the store. + return Discourse.SiteSettings; + }, + shareUrl: function() { const user = Discourse.User.current(); const userSuffix = user ? '?u=' + user.get('username_lower') : ''; diff --git a/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 b/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 index f65666c78a..a00c4fa13d 100644 --- a/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 +++ b/app/assets/javascripts/discourse/views/cloaked-collection.js.es6 @@ -1,6 +1,4 @@ /*eslint no-bitwise:0 */ -import CloakedView from 'discourse/views/cloaked'; - const CloakedCollectionView = Ember.CollectionView.extend({ cloakView: Ember.computed.alias('itemViewClass'), topVisible: null, @@ -10,7 +8,7 @@ const CloakedCollectionView = Ember.CollectionView.extend({ loadingHTML: 'Loading...', scrollDebounce: 10, - init: function() { + init() { const cloakView = this.get('cloakView'), idProperty = this.get('idProperty'), uncloakDefault = !!this.get('uncloakDefault'); @@ -19,6 +17,7 @@ const CloakedCollectionView = Ember.CollectionView.extend({ const slackRatio = parseFloat(this.get('slackRatio')); if (!slackRatio) { this.set('slackRatio', 1.0); } + const CloakedView = this.container.lookupFactory('view:cloaked'); this.set('itemViewClass', CloakedView.extend({ classNames: [cloakView + '-cloak'], cloaks: cloakView, @@ -26,7 +25,7 @@ const CloakedCollectionView = Ember.CollectionView.extend({ cloaksController: this.get('itemController'), defaultHeight: this.get('defaultHeight'), - init: function() { + init() { this._super(); if (idProperty) { From 473ebe2e620688b7a3fb7f0582556a77b26cd395 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 20 Aug 2015 17:37:04 +0200 Subject: [PATCH 161/237] FIX: cannot change user title --- .../javascripts/discourse/routes/preferences-badge-title.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 b/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 index 141a852b99..4491aff254 100644 --- a/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 +++ b/app/assets/javascripts/discourse/routes/preferences-badge-title.js.es6 @@ -1,4 +1,4 @@ -import UserBadge from 'discourse/models/badge'; +import UserBadge from 'discourse/models/user-badge'; import RestrictedUserRoute from "discourse/routes/restricted-user"; export default RestrictedUserRoute.extend({ From 320e4a83ac52077fd5797217842d2470370cf76d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 20 Aug 2015 12:03:43 -0400 Subject: [PATCH 162/237] FIX: Don't cache translations in development mode --- lib/js_locale_helper.rb | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/lib/js_locale_helper.rb b/lib/js_locale_helper.rb index 22f7ebb835..af06c33409 100644 --- a/lib/js_locale_helper.rb +++ b/lib/js_locale_helper.rb @@ -1,6 +1,10 @@ module JsLocaleHelper - def self.load_translations(locale) + def self.load_translations(locale, opts=nil) + opts ||= {} + + @loaded_translations = nil if opts[:force] + @loaded_translations ||= HashWithIndifferentAccess.new @loaded_translations[locale] ||= begin locale_str = locale.to_s @@ -78,7 +82,7 @@ module JsLocaleHelper site_locale = SiteSetting.default_locale.to_sym if Rails.env.development? - translations = load_translations(locale_sym) + translations = load_translations(locale_sym, force: true) else if locale_sym == :en translations = load_translations(locale_sym) @@ -115,7 +119,7 @@ module JsLocaleHelper def self.moment_format_function(name) format = I18n.t("dates.#{name}") - result = "moment.fn.#{name.camelize(:lower)} = function(){ return this.format('#{format}'); };\n" + "moment.fn.#{name.camelize(:lower)} = function(){ return this.format('#{format}'); };\n" end def self.moment_locale(locale_str) From ca6e516f865cee28b2c5679c5e77d1688fadf4e3 Mon Sep 17 00:00:00 2001 From: maiainternet Date: Thu, 20 Aug 2015 19:57:31 +0300 Subject: [PATCH 163/237] Create kunena3.rb Kunena import script customised for Kunena v3+ --- script/import_scripts/kunena3.rb | 141 +++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 script/import_scripts/kunena3.rb diff --git a/script/import_scripts/kunena3.rb b/script/import_scripts/kunena3.rb new file mode 100644 index 0000000000..56103861fd --- /dev/null +++ b/script/import_scripts/kunena3.rb @@ -0,0 +1,141 @@ +require "mysql2" +require File.expand_path(File.dirname(__FILE__) + "/base.rb") + +class ImportScripts::Kunena < ImportScripts::Base + + KUNENA_DB = "accentral_jos1" + + def initialize + super + + @users = {} + + @client = Mysql2::Client.new( + host: "aircadetcentral.net", + username: "accentral_jos1", + password: "Lc3bwPL7iEY(8", + database: KUNENA_DB + ) + end + + def execute + parse_users + + puts "creating users" + + create_users(@users) do |id, user| + { id: id, + email: user[:email], + username: user[:username], + created_at: user[:created_at], + bio_raw: user[:bio], + moderator: user[:moderator] ? true : false, + admin: user[:admin] ? true : false, + suspended_at: user[:suspended] ? Time.zone.now : nil, + suspended_till: user[:suspended] ? 100.years.from_now : nil } + end + + @users = nil + + create_categories(@client.query("SELECT id, parent_id, name, description, ordering FROM jos_kunena_categories ORDER BY parent_id, id;")) do |c| + h = {id: c['id'], name: c['name'], description: c['description'], position: c['ordering'].to_i} + if c['parent_id'].to_i > 0 + h[:parent_category_id] = category_id_from_imported_category_id(c['parent_id']) + end + h + end + + import_posts + + begin + create_admin(email: 'dave@ricey.co', username: UserNameSuggester.suggest('DJRice')) + rescue => e + puts '', "Failed to create admin user" + puts e.message + end + end + + def parse_users + # Need to merge data from joomla with kunena + + puts "fetching Joomla users data from mysql" + results = @client.query("SELECT id, username, email, registerDate FROM jos_users;", cache_rows: false) + results.each do |u| + next unless u['id'].to_i > 0 and u['username'].present? and u['email'].present? + username = u['username'].gsub(' ', '_').gsub(/[^A-Za-z0-9_]/, '')[0,User.username_length.end] + if username.length < User.username_length.first + username = username * User.username_length.first + end + @users[u['id'].to_i] = {id: u['id'].to_i, username: username, email: u['email'], created_at: u['registerDate']} + end + + puts "fetching Kunena user data from mysql" + results = @client.query("SELECT userid, signature, moderator, banned FROM jos_kunena_users;", cache_rows: false) + results.each do |u| + next unless u['userid'].to_i > 0 + user = @users[u['userid'].to_i] + if user + user[:bio] = u['signature'] + user[:moderator] = (u['moderator'].to_i == 1) + user[:suspended] = u['banned'].present? + end + end + end + + def import_posts + puts '', "creating topics and posts" + + total_count = @client.query("SELECT COUNT(*) count FROM jos_kunena_messages m;").first['count'] + + batch_size = 1000 + + batches(batch_size) do |offset| + results = @client.query(" + SELECT m.id id, + m.thread thread, + m.parent parent, + m.catid catid, + m.userid userid, + m.subject subject, + m.time time, + t.message message + FROM jos_kunena_messages m, + jos_kunena_messages_text t + WHERE m.id = t.mesid + ORDER BY m.id + LIMIT #{batch_size} + OFFSET #{offset}; + ", cache_rows: false) + + break if results.size < 1 + + 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['userid']) || -1 + mapped[:raw] = m["message"] + mapped[:created_at] = Time.zone.at(m['time']) + + if m['parent'] == 0 + mapped[:category] = category_id_from_imported_category_id(m['catid']) + mapped[:title] = m['subject'] + else + parent = topic_lookup_from_imported_post_id(m['parent']) + if parent + mapped[:topic_id] = parent[:topic_id] + mapped[:reply_to_post_number] = parent[:post_number] if parent[:post_number] > 1 + else + puts "Parent post #{m['parent']} doesn't exist. Skipping #{m["id"]}: #{m["subject"][0..40]}" + skip = true + end + end + + skip ? nil : mapped + end + end + end +end + +ImportScripts::Kunena.new.perform From 0ae9d9930823e999a83f63ae6578db6452918054 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 11:49:11 -0700 Subject: [PATCH 164/237] FIX: User profile collpased header (again) --- app/assets/stylesheets/desktop/user.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 5637f8a16d..4823de49ab 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -395,7 +395,7 @@ .details { padding: 12px 15px 2px 15px; margin-top: 0; - background: dark-light-choose(rgba($primary, 1), scale-color($secondary, $lightness: 40%)); + background: dark-light-choose(rgba($primary, 1), scale-color($secondary, $lightness: 15%)); .bio { display: none; } .primary { From 156c3651b558a4d335c641329a59c23a29afd672 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 12:54:07 -0700 Subject: [PATCH 165/237] DEV: Add choose-grey() function for better greys --- .../common/foundation/variables.scss | 46 +++++++++++++++++++ app/assets/stylesheets/mobile/topic-list.scss | 2 +- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index e4b97ef9ed..b5c4f27071 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -31,11 +31,57 @@ $base-font-family: Helvetica, Arial, sans-serif !default; @import "theme_variables"; @import "plugins_variables"; +// Color Utilities + +// Square Root function +// http://codepen.io/Designer023/pen/DkEtw +@function approximateSq($num, $approx) { + $root : (( $num / $approx ) + $approx) / 2; + @return $root; +} + +@function sqrt($num) { + $root:0; + $testRoot : 0.001; + $upperBounds : round($num / 2) + 1; //never need over half the main number. Add one just to be sure! + $loops : $upperBounds; + @for $test from 2 through $upperBounds { + $sq : $test * $test; + @if $sq <= $num { + $testRoot : $test; + } + } + + $root : (approximateSq($num, $testRoot)); + + @return $root; +} + // w3c definition of color brightness @function brightness($color) { @return ((red($color) * .299) + (green($color) * .587) + (blue($color) * .114)); } +// Replaces dark-light-diff($primary, $secondary, 50%, -50%) +// Uses an approximation of sRGB blending, GAMMA=2 instead of GAMMA=2.2 +@function choose-grey($percent) { + $ratio: ($percent / 100%); + $iratio: 1 - $ratio; + $pr2: red($primary) * red($primary); + $pg2: green($primary) * green($primary); + $pb2: blue($primary) * blue($primary); + $sr2: red($secondary) * red($secondary); + $sg2: green($secondary) * green($secondary); + $sb2: blue($secondary) * blue($secondary); + $rr2: $pr2 * $ratio + $sr2 * $iratio; + $rg2: $pg2 * $ratio + $sg2 * $iratio; + $rb2: $pb2 * $ratio + $sb2 * $iratio; + $rr: sqrt($rr2); + $rg: sqrt($rg2); + $rb: sqrt($rb2); + @return rgb($rr, $rg, $rb); +} + @function dark-light-diff($adjusted-color, $comparison-color, $lightness, $darkness) { @if brightness($adjusted-color) < brightness($comparison-color) { @return scale-color($adjusted-color, $lightness: $lightness); diff --git a/app/assets/stylesheets/mobile/topic-list.scss b/app/assets/stylesheets/mobile/topic-list.scss index 6c8c770b7b..859998d3e5 100644 --- a/app/assets/stylesheets/mobile/topic-list.scss +++ b/app/assets/stylesheets/mobile/topic-list.scss @@ -65,7 +65,7 @@ > tbody > tr { &.highlighted { - background-color: scale-color($tertiary, $lightness: 85%); + background-color: dark-light-choose(scale-color($tertiary, $lightness: 85%), scale-color($tertiary, $lightness: -55%)); } } From d1c69189f3c90ecf56013a8da904da9bff9a8e19 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 18 Aug 2015 17:15:46 -0400 Subject: [PATCH 166/237] FEATURE: Can edit category/host relationships for embedding --- .../admin/adapters/embedding.js.es6 | 7 +++ .../admin/components/embeddable-host.js.es6 | 63 +++++++++++++++++++ .../admin/controllers/admin-embedding.js.es6 | 18 ++++++ .../admin/routes/admin-embedding.js.es6 | 9 +++ .../admin/routes/admin-route-map.js.es6 | 1 + .../templates/components/embeddable-host.hbs | 19 ++++++ .../javascripts/admin/templates/customize.hbs | 1 + .../javascripts/admin/templates/embedding.hbs | 15 +++++ .../discourse/adapters/rest.js.es6 | 4 +- .../javascripts/discourse/models/store.js.es6 | 18 ++++-- .../admin/embeddable_hosts_controller.rb | 34 ++++++++++ app/controllers/admin/embedding_controller.rb | 21 +++++++ app/controllers/embed_controller.rb | 3 +- app/models/embeddable_host.rb | 24 +++++++ app/models/site_setting.rb | 14 ----- app/models/topic.rb | 2 +- app/models/topic_embed.rb | 4 +- app/serializers/embeddable_host_serializer.rb | 16 +++++ app/serializers/embedding_serializer.rb | 8 +++ config/locales/client.en.yml | 8 +++ config/locales/server.en.yml | 2 - config/routes.rb | 3 + config/site_settings.yml | 4 -- .../20150818190757_create_embeddable_hosts.rb | 33 ++++++++++ lib/topic_retriever.rb | 2 +- .../admin/embeddable_hosts_controller_spec.rb | 9 +++ .../admin/embedding_controller_spec.rb | 9 +++ spec/controllers/embed_controller_spec.rb | 11 ++-- spec/fabricators/category_fabricator.rb | 29 +-------- .../fabricators/embeddable_host_fabricator.rb | 27 ++++++++ spec/models/embeddable_host_spec.rb | 40 ++++++++++++ spec/models/site_setting_spec.rb | 30 --------- spec/models/topic_embed_spec.rb | 3 + spec/models/topic_spec.rb | 42 +++++++------ .../helpers/create-pretender.js.es6 | 15 +++-- test/javascripts/models/store-test.js.es6 | 28 ++++++--- 36 files changed, 449 insertions(+), 127 deletions(-) create mode 100644 app/assets/javascripts/admin/adapters/embedding.js.es6 create mode 100644 app/assets/javascripts/admin/components/embeddable-host.js.es6 create mode 100644 app/assets/javascripts/admin/controllers/admin-embedding.js.es6 create mode 100644 app/assets/javascripts/admin/routes/admin-embedding.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/embeddable-host.hbs create mode 100644 app/assets/javascripts/admin/templates/embedding.hbs create mode 100644 app/controllers/admin/embeddable_hosts_controller.rb create mode 100644 app/controllers/admin/embedding_controller.rb create mode 100644 app/models/embeddable_host.rb create mode 100644 app/serializers/embeddable_host_serializer.rb create mode 100644 app/serializers/embedding_serializer.rb create mode 100644 db/migrate/20150818190757_create_embeddable_hosts.rb create mode 100644 spec/controllers/admin/embeddable_hosts_controller_spec.rb create mode 100644 spec/controllers/admin/embedding_controller_spec.rb create mode 100644 spec/fabricators/embeddable_host_fabricator.rb create mode 100644 spec/models/embeddable_host_spec.rb diff --git a/app/assets/javascripts/admin/adapters/embedding.js.es6 b/app/assets/javascripts/admin/adapters/embedding.js.es6 new file mode 100644 index 0000000000..c8985cfdca --- /dev/null +++ b/app/assets/javascripts/admin/adapters/embedding.js.es6 @@ -0,0 +1,7 @@ +import RestAdapter from 'discourse/adapters/rest'; + +export default RestAdapter.extend({ + pathFor() { + return "/admin/customize/embedding"; + } +}); diff --git a/app/assets/javascripts/admin/components/embeddable-host.js.es6 b/app/assets/javascripts/admin/components/embeddable-host.js.es6 new file mode 100644 index 0000000000..f33c750965 --- /dev/null +++ b/app/assets/javascripts/admin/components/embeddable-host.js.es6 @@ -0,0 +1,63 @@ +import { bufferedProperty } from 'discourse/mixins/buffered-content'; +import computed from 'ember-addons/ember-computed-decorators'; +import { on, observes } from 'ember-addons/ember-computed-decorators'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + +export default Ember.Component.extend(bufferedProperty('host'), { + editToggled: false, + tagName: 'tr', + categoryId: null, + + editing: Ember.computed.or('host.isNew', 'editToggled'), + + @on('didInsertElement') + @observes('editing') + _focusOnInput() { + Ember.run.schedule('afterRender', () => { this.$('.host-name').focus(); }); + }, + + @computed('buffered.host', 'host.isSaving') + cantSave(host, isSaving) { + return isSaving || Ember.isEmpty(host); + }, + + actions: { + edit() { + this.set('categoryId', this.get('host.category.id')); + this.set('editToggled', true); + }, + + save() { + if (this.get('cantSave')) { return; } + + const props = this.get('buffered').getProperties('host'); + props.category_id = this.get('categoryId'); + + const host = this.get('host'); + host.save(props).then(() => { + host.set('category', Discourse.Category.findById(this.get('categoryId'))); + this.set('editToggled', false); + }).catch(popupAjaxError); + }, + + delete() { + bootbox.confirm(I18n.t('admin.embedding.confirm_delete'), (result) => { + if (result) { + this.get('host').destroyRecord().then(() => { + this.sendAction('deleteHost', this.get('host')); + }); + } + }); + }, + + cancel() { + const host = this.get('host'); + if (host.get('isNew')) { + this.sendAction('deleteHost', host); + } else { + this.rollbackBuffer(); + this.set('editToggled', false); + } + } + } +}); diff --git a/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 b/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 new file mode 100644 index 0000000000..d0e554831a --- /dev/null +++ b/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 @@ -0,0 +1,18 @@ +export default Ember.Controller.extend({ + embedding: null, + + actions: { + saveChanges() { + this.get('embedding').update({}); + }, + + addHost() { + const host = this.store.createRecord('embeddable-host'); + this.get('embedding.embeddable_hosts').pushObject(host); + }, + + deleteHost(host) { + this.get('embedding.embeddable_hosts').removeObject(host); + } + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-embedding.js.es6 b/app/assets/javascripts/admin/routes/admin-embedding.js.es6 new file mode 100644 index 0000000000..d9a249c60c --- /dev/null +++ b/app/assets/javascripts/admin/routes/admin-embedding.js.es6 @@ -0,0 +1,9 @@ +export default Ember.Route.extend({ + model() { + return this.store.find('embedding'); + }, + + setupController(controller, model) { + controller.set('embedding', model); + } +}); diff --git a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 index 047e5d51af..e01d0f8f0d 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -27,6 +27,7 @@ export default { this.resource('adminUserFields', { path: '/user_fields' }); this.resource('adminEmojis', { path: '/emojis' }); this.resource('adminPermalinks', { path: '/permalinks' }); + this.resource('adminEmbedding', { path: '/embedding' }); }); this.route('api'); diff --git a/app/assets/javascripts/admin/templates/components/embeddable-host.hbs b/app/assets/javascripts/admin/templates/components/embeddable-host.hbs new file mode 100644 index 0000000000..c35d40e1d6 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/embeddable-host.hbs @@ -0,0 +1,19 @@ +{{#if editing}} + + {{input value=buffered.host placeholder="example.com" enter="save" class="host-name"}} + + + {{category-chooser value=categoryId}} + + + {{d-button icon="check" action="save" class="btn-primary" disabled=cantSave}} + {{d-button icon="times" action="cancel" class="btn-danger" disabled=host.isSaving}} + +{{else}} + {{host.host}} + {{category-badge host.category}} + + {{d-button icon="pencil" action="edit"}} + {{d-button icon="trash-o" action="delete" class='btn-danger'}} + +{{/if}} diff --git a/app/assets/javascripts/admin/templates/customize.hbs b/app/assets/javascripts/admin/templates/customize.hbs index 130b75e0ac..8ab4c662e7 100644 --- a/app/assets/javascripts/admin/templates/customize.hbs +++ b/app/assets/javascripts/admin/templates/customize.hbs @@ -5,6 +5,7 @@ {{nav-item route='adminUserFields' label='admin.user_fields.title'}} {{nav-item route='adminEmojis' label='admin.emoji.title'}} {{nav-item route='adminPermalinks' label='admin.permalink.title'}} + {{nav-item route='adminEmbedding' label='admin.embedding.title'}} {{/admin-nav}}
    diff --git a/app/assets/javascripts/admin/templates/embedding.hbs b/app/assets/javascripts/admin/templates/embedding.hbs new file mode 100644 index 0000000000..838e3e8e94 --- /dev/null +++ b/app/assets/javascripts/admin/templates/embedding.hbs @@ -0,0 +1,15 @@ +{{#if embedding.embeddable_hosts}} + + + + + + + {{#each embedding.embeddable_hosts as |host|}} + {{embeddable-host host=host deleteHost="deleteHost"}} + {{/each}} +
    {{i18n "admin.embedding.host"}}{{i18n "admin.embedding.category"}} 
    +{{/if}} + +{{d-button label="admin.embedding.add_host" action="addHost" icon="plus" class="btn-primary"}} + diff --git a/app/assets/javascripts/discourse/adapters/rest.js.es6 b/app/assets/javascripts/discourse/adapters/rest.js.es6 index 5e427a8658..1a9fa03274 100644 --- a/app/assets/javascripts/discourse/adapters/rest.js.es6 +++ b/app/assets/javascripts/discourse/adapters/rest.js.es6 @@ -1,4 +1,4 @@ -const ADMIN_MODELS = ['plugin', 'site-customization']; +const ADMIN_MODELS = ['plugin', 'site-customization', 'embeddable-host']; export function Result(payload, responseJson) { this.payload = payload; @@ -19,7 +19,7 @@ function rethrow(error) { export default Ember.Object.extend({ basePath(store, type) { - if (ADMIN_MODELS.indexOf(type) !== -1) { return "/admin/"; } + if (ADMIN_MODELS.indexOf(type.replace('_', '-')) !== -1) { return "/admin/"; } return "/"; }, diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index a9aa86f294..dc6acae822 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -189,14 +189,24 @@ export default Ember.Object.extend({ _hydrateEmbedded(type, obj, root) { const self = this; Object.keys(obj).forEach(function(k) { - const m = /(.+)\_id$/.exec(k); + const m = /(.+)\_id(s?)$/.exec(k); if (m) { const subType = m[1]; - const hydrated = self._lookupSubType(subType, type, obj[k], root); - if (hydrated) { - obj[subType] = hydrated; + + if (m[2]) { + const hydrated = obj[k].map(function(id) { + return self._lookupSubType(subType, type, id, root); + }); + obj[self.pluralize(subType)] = hydrated || []; delete obj[k]; + } else { + const hydrated = self._lookupSubType(subType, type, obj[k], root); + if (hydrated) { + obj[subType] = hydrated; + delete obj[k]; + } } + } }); }, diff --git a/app/controllers/admin/embeddable_hosts_controller.rb b/app/controllers/admin/embeddable_hosts_controller.rb new file mode 100644 index 0000000000..2d90d46f2e --- /dev/null +++ b/app/controllers/admin/embeddable_hosts_controller.rb @@ -0,0 +1,34 @@ +class Admin::EmbeddableHostsController < Admin::AdminController + + before_filter :ensure_logged_in, :ensure_staff + + def create + save_host(EmbeddableHost.new) + end + + def update + host = EmbeddableHost.where(id: params[:id]).first + save_host(host) + end + + def destroy + host = EmbeddableHost.where(id: params[:id]).first + host.destroy + render json: success_json + end + + protected + + def save_host(host) + host.host = params[:embeddable_host][:host] + host.category_id = params[:embeddable_host][:category_id] + host.category_id = SiteSetting.uncategorized_category_id if host.category_id.blank? + + if host.save + render_serialized(host, EmbeddableHostSerializer, root: 'embeddable_host', rest_serializer: true) + else + render_json_error(host) + end + end + +end diff --git a/app/controllers/admin/embedding_controller.rb b/app/controllers/admin/embedding_controller.rb new file mode 100644 index 0000000000..656ff25af0 --- /dev/null +++ b/app/controllers/admin/embedding_controller.rb @@ -0,0 +1,21 @@ +class Admin::EmbeddingController < Admin::AdminController + + before_filter :ensure_logged_in, :ensure_staff, :fetch_embedding + + def show + render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true) + end + + def update + render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true) + end + + protected + + def fetch_embedding + @embedding = OpenStruct.new({ + id: 'default', + embeddable_hosts: EmbeddableHost.all.order(:host) + }) + end +end diff --git a/app/controllers/embed_controller.rb b/app/controllers/embed_controller.rb index f296998ef0..e309776c65 100644 --- a/app/controllers/embed_controller.rb +++ b/app/controllers/embed_controller.rb @@ -58,8 +58,7 @@ class EmbedController < ApplicationController def ensure_embeddable if !(Rails.env.development? && current_user.try(:admin?)) - raise Discourse::InvalidAccess.new('embeddable hosts not set') if SiteSetting.embeddable_hosts.blank? - raise Discourse::InvalidAccess.new('invalid referer host') unless SiteSetting.allows_embeddable_host?(request.referer) + raise Discourse::InvalidAccess.new('invalid referer host') unless EmbeddableHost.host_allowed?(request.referer) end response.headers['X-Frame-Options'] = "ALLOWALL" diff --git a/app/models/embeddable_host.rb b/app/models/embeddable_host.rb new file mode 100644 index 0000000000..1fdda54e28 --- /dev/null +++ b/app/models/embeddable_host.rb @@ -0,0 +1,24 @@ +class EmbeddableHost < ActiveRecord::Base + validates_format_of :host, :with => /\A[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(:[0-9]{1,5})?(\/.*)?\Z/i + belongs_to :category + + before_validation do + self.host.sub!(/^https?:\/\//, '') + self.host.sub!(/\/.*$/, '') + end + + def self.record_for_host(host) + uri = URI(host) rescue nil + return false unless uri.present? + + host = uri.host + return false unless host.present? + + where("lower(host) = ?", host).first + end + + def self.host_allowed?(host) + record_for_host(host).present? + end + +end diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index 4c46e52d12..ff1d17c005 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -68,20 +68,6 @@ class SiteSetting < ActiveRecord::Base @anonymous_menu_items ||= Set.new Discourse.anonymous_filters.map(&:to_s) end - def self.allows_embeddable_host?(host) - return false if embeddable_hosts.blank? - uri = URI(host) rescue nil - return false unless uri.present? - - host = uri.host - return false unless host.present? - - !!embeddable_hosts.split("\n").detect {|h| h.sub(/^https?\:\/\//, '') == host } - - hosts = embeddable_hosts.split("\n").map {|h| (URI(h).host rescue nil) || h } - !!hosts.detect {|h| h == host} - end - def self.anonymous_homepage top_menu_items.map { |item| item.name } .select { |item| anonymous_menu_items.include?(item) } diff --git a/app/models/topic.rb b/app/models/topic.rb index 297f840be3..442fb43c2d 100644 --- a/app/models/topic.rb +++ b/app/models/topic.rb @@ -866,7 +866,7 @@ class Topic < ActiveRecord::Base end def expandable_first_post? - SiteSetting.embeddable_hosts.present? && SiteSetting.embed_truncate? && has_topic_embed? + SiteSetting.embed_truncate? && has_topic_embed? end TIME_TO_FIRST_RESPONSE_SQL ||= <<-SQL diff --git a/app/models/topic_embed.rb b/app/models/topic_embed.rb index 823ce54843..461695e5a0 100644 --- a/app/models/topic_embed.rb +++ b/app/models/topic_embed.rb @@ -33,12 +33,14 @@ class TopicEmbed < ActiveRecord::Base # If there is no embed, create a topic, post and the embed. if embed.blank? Topic.transaction do + eh = EmbeddableHost.record_for_host(url) + creator = PostCreator.new(user, title: title, raw: absolutize_urls(url, contents), skip_validations: true, cook_method: Post.cook_methods[:raw_html], - category: SiteSetting.embed_category) + category: eh.try(:category_id)) post = creator.create if post.present? TopicEmbed.create!(topic_id: post.topic_id, diff --git a/app/serializers/embeddable_host_serializer.rb b/app/serializers/embeddable_host_serializer.rb new file mode 100644 index 0000000000..f5de82a0cf --- /dev/null +++ b/app/serializers/embeddable_host_serializer.rb @@ -0,0 +1,16 @@ +class EmbeddableHostSerializer < ApplicationSerializer + attributes :id, :host, :category_id + + def id + object.id + end + + def host + object.host + end + + def category_id + object.category_id + end +end + diff --git a/app/serializers/embedding_serializer.rb b/app/serializers/embedding_serializer.rb new file mode 100644 index 0000000000..0413573569 --- /dev/null +++ b/app/serializers/embedding_serializer.rb @@ -0,0 +1,8 @@ +class EmbeddingSerializer < ApplicationSerializer + attributes :id + has_many :embeddable_hosts, serializer: EmbeddableHostSerializer, embed: :ids + + def id + object.id + end +end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index b60cb7a198..5ff7e9198c 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2491,6 +2491,14 @@ en: image: "Image" delete_confirm: "Are you sure you want to delete the :%{name}: emoji?" + embedding: + confirm_delete: "Are you sure you want to delete that host?" + title: "Embedding" + host: "Allowed Hosts" + edit: "edit" + category: "Post to Category" + add_host: "Add Host" + permalink: title: "Permalinks" url: "URL" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 0589365879..429fe03dfe 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1164,13 +1164,11 @@ en: autohighlight_all_code: "Force apply code highlighting to all preformatted code blocks even when they didn't explicitly specify the language." highlighted_languages: "Included syntax highlighting rules. (Warning: including too many langauges may impact performance) see: https://highlightjs.org/static/demo/ for a demo" - embeddable_hosts: "Host(s) that can embed the comments from this Discourse forum. Hostname only, do not begin with http://" feed_polling_enabled: "EMBEDDING ONLY: Whether to embed a RSS/ATOM feed as posts." feed_polling_url: "EMBEDDING ONLY: URL of RSS/ATOM feed to embed." embed_by_username: "Discourse username of the user who creates the embedded topics." embed_username_key_from_feed: "Key to pull discourse username from feed." embed_truncate: "Truncate the embedded posts." - embed_category: "Category of embedded topics." embed_post_limit: "Maximum number of posts to embed." embed_whitelist_selector: "CSS selector for elements that are allowed in embeds." embed_blacklist_selector: "CSS selector for elements that are removed from embeds." diff --git a/config/routes.rb b/config/routes.rb index d4f87ce11a..9b0e6cc40c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -135,6 +135,8 @@ Discourse::Application.routes.draw do get "customize/css_html/:id/:section" => "site_customizations#index", constraints: AdminConstraint.new get "customize/colors" => "color_schemes#index", constraints: AdminConstraint.new get "customize/permalinks" => "permalinks#index", constraints: AdminConstraint.new + get "customize/embedding" => "embedding#show", constraints: AdminConstraint.new + put "customize/embedding" => "embedding#update", constraints: AdminConstraint.new get "flags" => "flags#index" get "flags/:filter" => "flags#index" post "flags/agree/:id" => "flags#agree" @@ -148,6 +150,7 @@ Discourse::Application.routes.draw do resources :emojis, constraints: AdminConstraint.new end + resources :embeddable_hosts, constraints: AdminConstraint.new resources :color_schemes, constraints: AdminConstraint.new resources :permalinks, constraints: AdminConstraint.new diff --git a/config/site_settings.yml b/config/site_settings.yml index e1ff85b944..4e5b60d5e0 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -759,16 +759,12 @@ developer: default: false embedding: - embeddable_hosts: - default: '' - type: host_list feed_polling_enabled: false feed_polling_url: '' embed_by_username: default: '' type: username embed_username_key_from_feed: '' - embed_category: '' embed_post_limit: 100 embed_truncate: false embed_whitelist_selector: '' diff --git a/db/migrate/20150818190757_create_embeddable_hosts.rb b/db/migrate/20150818190757_create_embeddable_hosts.rb new file mode 100644 index 0000000000..a0293d3f8d --- /dev/null +++ b/db/migrate/20150818190757_create_embeddable_hosts.rb @@ -0,0 +1,33 @@ +class CreateEmbeddableHosts < ActiveRecord::Migration + def change + create_table :embeddable_hosts, force: true do |t| + t.string :host, null: false + t.integer :category_id, null: false + t.timestamps + end + + category_id = execute("SELECT c.id FROM categories AS c + INNER JOIN site_settings AS s ON s.value = c.name + WHERE s.name = 'embed_category'")[0]['id'].to_i + + + if category_id == 0 + category_id = execute("SELECT value FROM site_settings WHERE name = 'uncategorized_category_id'")[0]['value'].to_i + end + + embeddable_hosts = execute("SELECT value FROM site_settings WHERE name = 'embeddable_hosts'") + if embeddable_hosts && embeddable_hosts.cmd_tuples > 0 + val = embeddable_hosts[0]['value'] + if val.present? + records = val.split("\n") + if records.present? + records.each do |h| + execute "INSERT INTO embeddable_hosts (host, category_id, created_at, updated_at) VALUES ('#{h}', #{category_id}, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)" + end + end + end + end + + execute "DELETE FROM site_settings WHERE name IN ('embeddable_hosts', 'embed_category')" + end +end diff --git a/lib/topic_retriever.rb b/lib/topic_retriever.rb index 090ad6244a..2af8401afb 100644 --- a/lib/topic_retriever.rb +++ b/lib/topic_retriever.rb @@ -13,7 +13,7 @@ class TopicRetriever private def invalid_host? - !SiteSetting.allows_embeddable_host?(@embed_url) + !EmbeddableHost.host_allowed?(@embed_url) end def retrieved_recently? diff --git a/spec/controllers/admin/embeddable_hosts_controller_spec.rb b/spec/controllers/admin/embeddable_hosts_controller_spec.rb new file mode 100644 index 0000000000..d3d83cac5c --- /dev/null +++ b/spec/controllers/admin/embeddable_hosts_controller_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Admin::EmbeddableHostsController do + + it "is a subclass of AdminController" do + expect(Admin::EmbeddableHostsController < Admin::AdminController).to eq(true) + end + +end diff --git a/spec/controllers/admin/embedding_controller_spec.rb b/spec/controllers/admin/embedding_controller_spec.rb new file mode 100644 index 0000000000..5913e9fdcf --- /dev/null +++ b/spec/controllers/admin/embedding_controller_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Admin::EmbeddingController do + + it "is a subclass of AdminController" do + expect(Admin::EmbeddingController < Admin::AdminController).to eq(true) + end + +end diff --git a/spec/controllers/embed_controller_spec.rb b/spec/controllers/embed_controller_spec.rb index ee91d6cb24..316c9a6894 100644 --- a/spec/controllers/embed_controller_spec.rb +++ b/spec/controllers/embed_controller_spec.rb @@ -11,7 +11,6 @@ describe EmbedController do end it "raises an error with a missing host" do - SiteSetting.embeddable_hosts = nil get :comments, embed_url: embed_url expect(response).not_to be_success end @@ -19,7 +18,7 @@ describe EmbedController do context "by topic id" do before do - SiteSetting.embeddable_hosts = host + Fabricate(:embeddable_host) controller.request.stubs(:referer).returns('http://eviltrout.com/some-page') end @@ -31,9 +30,7 @@ describe EmbedController do end context "with a host" do - before do - SiteSetting.embeddable_hosts = host - end + let!(:embeddable_host) { Fabricate(:embeddable_host) } it "raises an error with no referer" do get :comments, embed_url: embed_url @@ -68,7 +65,9 @@ describe EmbedController do context "with multiple hosts" do before do - SiteSetting.embeddable_hosts = "#{host}\nhttp://discourse.org\nhttps://example.com/1234" + Fabricate(:embeddable_host) + Fabricate(:embeddable_host, host: 'http://discourse.org') + Fabricate(:embeddable_host, host: 'https://example.com/1234') end context "success" do diff --git a/spec/fabricators/category_fabricator.rb b/spec/fabricators/category_fabricator.rb index a37f5b4c8b..0c668579c9 100644 --- a/spec/fabricators/category_fabricator.rb +++ b/spec/fabricators/category_fabricator.rb @@ -1,27 +1,4 @@ -Fabricator(:category) do - name { sequence(:name) { |n| "Amazing Category #{n}" } } - user -end - -Fabricator(:diff_category, from: :category) do - name "Different Category" - user -end - -Fabricator(:happy_category, from: :category) do - name 'Happy Category' - slug 'happy' - user -end - -Fabricator(:private_category, from: :category) do - transient :group - - name 'Private Category' - slug 'private' - user - after_build do |cat, transients| - cat.update!(read_restricted: true) - cat.category_groups.build(group_id: transients[:group].id, permission_type: CategoryGroup.permission_types[:full]) - end +Fabricator(:embeddable_host) do + host "eviltrout.com" + category end diff --git a/spec/fabricators/embeddable_host_fabricator.rb b/spec/fabricators/embeddable_host_fabricator.rb new file mode 100644 index 0000000000..a37f5b4c8b --- /dev/null +++ b/spec/fabricators/embeddable_host_fabricator.rb @@ -0,0 +1,27 @@ +Fabricator(:category) do + name { sequence(:name) { |n| "Amazing Category #{n}" } } + user +end + +Fabricator(:diff_category, from: :category) do + name "Different Category" + user +end + +Fabricator(:happy_category, from: :category) do + name 'Happy Category' + slug 'happy' + user +end + +Fabricator(:private_category, from: :category) do + transient :group + + name 'Private Category' + slug 'private' + user + after_build do |cat, transients| + cat.update!(read_restricted: true) + cat.category_groups.build(group_id: transients[:group].id, permission_type: CategoryGroup.permission_types[:full]) + end +end diff --git a/spec/models/embeddable_host_spec.rb b/spec/models/embeddable_host_spec.rb new file mode 100644 index 0000000000..83043104d9 --- /dev/null +++ b/spec/models/embeddable_host_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe EmbeddableHost do + + it "trims http" do + eh = EmbeddableHost.new(host: 'http://example.com') + expect(eh).to be_valid + expect(eh.host).to eq('example.com') + end + + it "trims https" do + eh = EmbeddableHost.new(host: 'https://example.com') + expect(eh).to be_valid + expect(eh.host).to eq('example.com') + end + + it "trims paths" do + eh = EmbeddableHost.new(host: 'https://example.com/1234/45') + expect(eh).to be_valid + expect(eh.host).to eq('example.com') + end + + describe "allows_embeddable_host" do + let!(:host) { Fabricate(:embeddable_host) } + + it 'works as expected' do + expect(EmbeddableHost.host_allowed?('http://eviltrout.com')).to eq(true) + expect(EmbeddableHost.host_allowed?('https://eviltrout.com')).to eq(true) + expect(EmbeddableHost.host_allowed?('https://not-eviltrout.com')).to eq(false) + end + + it 'works with multiple hosts' do + Fabricate(:embeddable_host, host: 'discourse.org') + expect(EmbeddableHost.host_allowed?('http://eviltrout.com')).to eq(true) + expect(EmbeddableHost.host_allowed?('http://discourse.org')).to eq(true) + end + + end + +end diff --git a/spec/models/site_setting_spec.rb b/spec/models/site_setting_spec.rb index a4ca870d5d..444bd6f032 100644 --- a/spec/models/site_setting_spec.rb +++ b/spec/models/site_setting_spec.rb @@ -4,36 +4,6 @@ require_dependency 'site_setting_extension' describe SiteSetting do - describe "allows_embeddable_host" do - it 'works as expected' do - SiteSetting.embeddable_hosts = 'eviltrout.com' - expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true) - expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true) - expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false) - end - - it 'works with a http host' do - SiteSetting.embeddable_hosts = 'http://eviltrout.com' - expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true) - expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true) - expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false) - end - - it 'works with a https host' do - SiteSetting.embeddable_hosts = 'https://eviltrout.com' - expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true) - expect(SiteSetting.allows_embeddable_host?('https://eviltrout.com')).to eq(true) - expect(SiteSetting.allows_embeddable_host?('https://not-eviltrout.com')).to eq(false) - end - - it 'works with multiple hosts' do - SiteSetting.embeddable_hosts = "https://eviltrout.com\nhttps://discourse.org" - expect(SiteSetting.allows_embeddable_host?('http://eviltrout.com')).to eq(true) - expect(SiteSetting.allows_embeddable_host?('http://discourse.org')).to eq(true) - end - - end - describe 'topic_title_length' do it 'returns a range of min/max topic title length' do expect(SiteSetting.topic_title_length).to eq( diff --git a/spec/models/topic_embed_spec.rb b/spec/models/topic_embed_spec.rb index c1bf49c7e9..8c9b3409d8 100644 --- a/spec/models/topic_embed_spec.rb +++ b/spec/models/topic_embed_spec.rb @@ -12,6 +12,7 @@ describe TopicEmbed do let(:title) { "How to turn a fish from good to evil in 30 seconds" } let(:url) { 'http://eviltrout.com/123' } let(:contents) { "hello world new post hello " } + let!(:embeddable_host) { Fabricate(:embeddable_host) } it "returns nil when the URL is malformed" do expect(TopicEmbed.import(user, "invalid url", title, contents)).to eq(nil) @@ -33,6 +34,8 @@ describe TopicEmbed do expect(post.topic.has_topic_embed?).to eq(true) expect(TopicEmbed.where(topic_id: post.topic_id)).to be_present + + expect(post.topic.category).to eq(embeddable_host.category) end it "Supports updating the post" do diff --git a/spec/models/topic_spec.rb b/spec/models/topic_spec.rb index ff971fa737..7471c5cd7b 100644 --- a/spec/models/topic_spec.rb +++ b/spec/models/topic_spec.rb @@ -1238,7 +1238,7 @@ describe Topic do it "doesn't return topics from muted categories" do user = Fabricate(:user) category = Fabricate(:category) - topic = Fabricate(:topic, category: category) + Fabricate(:topic, category: category) CategoryUser.set_notification_level_for_category(user, CategoryUser.notification_levels[:muted], category.id) @@ -1247,7 +1247,7 @@ describe Topic do it "doesn't return topics from TL0 users" do new_user = Fabricate(:user, trust_level: 0) - topic = Fabricate(:topic, user_id: new_user.id) + Fabricate(:topic, user_id: new_user.id) expect(Topic.for_digest(user, 1.year.ago, top_order: true)).to be_blank end @@ -1397,32 +1397,34 @@ describe Topic do end describe "expandable_first_post?" do + let(:topic) { Fabricate.build(:topic) } - before do - SiteSetting.embeddable_hosts = "http://eviltrout.com" - SiteSetting.embed_truncate = true - topic.stubs(:has_topic_embed?).returns(true) - end - - it "is true with the correct settings and topic_embed" do - expect(topic.expandable_first_post?).to eq(true) - end - it "is false if embeddable_host is blank" do - SiteSetting.embeddable_hosts = nil expect(topic.expandable_first_post?).to eq(false) end - it "is false if embed_truncate? is false" do - SiteSetting.embed_truncate = false - expect(topic.expandable_first_post?).to eq(false) + describe 'with an emeddable host' do + before do + Fabricate(:embeddable_host) + SiteSetting.embed_truncate = true + topic.stubs(:has_topic_embed?).returns(true) + end + + it "is true with the correct settings and topic_embed" do + expect(topic.expandable_first_post?).to eq(true) + end + it "is false if embed_truncate? is false" do + SiteSetting.embed_truncate = false + expect(topic.expandable_first_post?).to eq(false) + end + + it "is false if has_topic_embed? is false" do + topic.stubs(:has_topic_embed?).returns(false) + expect(topic.expandable_first_post?).to eq(false) + end end - it "is false if has_topic_embed? is false" do - topic.stubs(:has_topic_embed?).returns(false) - expect(topic.expandable_first_post?).to eq(false) - end end it "has custom fields" do diff --git a/test/javascripts/helpers/create-pretender.js.es6 b/test/javascripts/helpers/create-pretender.js.es6 index fe00b4838d..f747f15fd0 100644 --- a/test/javascripts/helpers/create-pretender.js.es6 +++ b/test/javascripts/helpers/create-pretender.js.es6 @@ -39,13 +39,17 @@ const _moreWidgets = [ {id: 224, name: 'Good Repellant'} ]; -const fruits = [{id: 1, name: 'apple', farmer_id: 1, category_id: 4}, - {id: 2, name: 'banana', farmer_id: 1, category_id: 3}, - {id: 3, name: 'grape', farmer_id: 2, category_id: 5}]; +const fruits = [{id: 1, name: 'apple', farmer_id: 1, color_ids: [1,2], category_id: 4}, + {id: 2, name: 'banana', farmer_id: 1, color_ids: [3], category_id: 3}, + {id: 3, name: 'grape', farmer_id: 2, color_ids: [2], category_id: 5}]; const farmers = [{id: 1, name: 'Old MacDonald'}, {id: 2, name: 'Luke Skywalker'}]; +const colors = [{id: 1, name: 'Red'}, + {id: 2, name: 'Green'}, + {id: 3, name: 'Yellow'}]; + function loggedIn() { return !!Discourse.User.current(); } @@ -221,12 +225,11 @@ export default function() { this.get('/fruits/:id', function() { const fruit = fruits[0]; - - return response({ __rest_serializer: "1", fruit, farmers: [farmers[0]] }); + return response({ __rest_serializer: "1", fruit, farmers, colors }); }); this.get('/fruits', function() { - return response({ __rest_serializer: "1", fruits, farmers }); + return response({ __rest_serializer: "1", fruits, farmers, colors }); }); this.get('/widgets/:widget_id', function(request) { diff --git a/test/javascripts/models/store-test.js.es6 b/test/javascripts/models/store-test.js.es6 index 46585ce682..266eb06fa2 100644 --- a/test/javascripts/models/store-test.js.es6 +++ b/test/javascripts/models/store-test.js.es6 @@ -106,19 +106,31 @@ test('destroyRecord when new', function(assert) { }); -test('find embedded', function() { +test('find embedded', function(assert) { const store = createStore(); - return store.find('fruit', 1).then(function(f) { - ok(f.get('farmer'), 'it has the embedded object'); - ok(f.get('category'), 'categories are found automatically'); + return store.find('fruit', 2).then(function(f) { + assert.ok(f.get('farmer'), 'it has the embedded object'); + + const fruitCols = f.get('colors'); + assert.equal(fruitCols.length, 2); + assert.equal(fruitCols[0].get('id'), 1); + assert.equal(fruitCols[1].get('id'), 2); + + assert.ok(f.get('category'), 'categories are found automatically'); }); }); -test('findAll embedded', function() { +test('findAll embedded', function(assert) { const store = createStore(); return store.findAll('fruit').then(function(fruits) { - equal(fruits.objectAt(0).get('farmer.name'), 'Old MacDonald'); - equal(fruits.objectAt(0).get('farmer'), fruits.objectAt(1).get('farmer'), 'points at the same object'); - equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker'); + assert.equal(fruits.objectAt(0).get('farmer.name'), 'Old MacDonald'); + assert.equal(fruits.objectAt(0).get('farmer'), fruits.objectAt(1).get('farmer'), 'points at the same object'); + + const fruitCols = fruits.objectAt(0).get('colors'); + assert.equal(fruitCols.length, 2); + assert.equal(fruitCols[0].get('id'), 1); + assert.equal(fruitCols[1].get('id'), 2); + + assert.equal(fruits.objectAt(2).get('farmer.name'), 'Luke Skywalker'); }); }); From 146f2eab7fcc4f27f2f69bb2d55a9775c5e7e059 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 20 Aug 2015 13:43:12 -0400 Subject: [PATCH 167/237] Can edit settings on the embedding page --- .../admin/components/embedding-setting.js.es6 | 23 ++++++ .../admin/components/highlighted-code.js.es6 | 12 +++ .../admin/controllers/admin-embedding.js.es6 | 39 +++++++++- .../components/embedding-setting.hbs | 11 +++ .../templates/components/highlighted-code.hbs | 1 + .../javascripts/admin/templates/embedding.hbs | 74 +++++++++++++++---- .../stylesheets/common/admin/admin_base.scss | 30 ++++++++ app/controllers/admin/embedding_controller.rb | 18 +++-- app/models/embedding.rb | 41 ++++++++++ app/serializers/embedding_serializer.rb | 12 ++- config/locales/client.en.yml | 17 +++++ config/site_settings.yml | 29 ++++++-- 12 files changed, 277 insertions(+), 30 deletions(-) create mode 100644 app/assets/javascripts/admin/components/embedding-setting.js.es6 create mode 100644 app/assets/javascripts/admin/components/highlighted-code.js.es6 create mode 100644 app/assets/javascripts/admin/templates/components/embedding-setting.hbs create mode 100644 app/assets/javascripts/admin/templates/components/highlighted-code.hbs create mode 100644 app/models/embedding.rb diff --git a/app/assets/javascripts/admin/components/embedding-setting.js.es6 b/app/assets/javascripts/admin/components/embedding-setting.js.es6 new file mode 100644 index 0000000000..904afacfee --- /dev/null +++ b/app/assets/javascripts/admin/components/embedding-setting.js.es6 @@ -0,0 +1,23 @@ +import computed from 'ember-addons/ember-computed-decorators'; + +export default Ember.Component.extend({ + classNames: ['embed-setting'], + + @computed('field') + inputId(field) { return field.dasherize(); }, + + @computed('field') + translationKey(field) { return `admin.embedding.${field}`; }, + + @computed('type') + isCheckbox(type) { return type === "checkbox"; }, + + @computed('value') + checked: { + get(value) { return !!value; }, + set(value) { + this.set('value', value); + return value; + } + } +}); diff --git a/app/assets/javascripts/admin/components/highlighted-code.js.es6 b/app/assets/javascripts/admin/components/highlighted-code.js.es6 new file mode 100644 index 0000000000..4fc413fd89 --- /dev/null +++ b/app/assets/javascripts/admin/components/highlighted-code.js.es6 @@ -0,0 +1,12 @@ +import { on, observes } from 'ember-addons/ember-computed-decorators'; +import highlightSyntax from 'discourse/lib/highlight-syntax'; + +export default Ember.Component.extend({ + + @on('didInsertElement') + @observes('code') + _refresh: function() { + highlightSyntax(this.$()); + } + +}); diff --git a/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 b/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 index d0e554831a..780c61f3f9 100644 --- a/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-embedding.js.es6 @@ -1,9 +1,46 @@ +import computed from 'ember-addons/ember-computed-decorators'; +import { popupAjaxError } from 'discourse/lib/ajax-error'; + export default Ember.Controller.extend({ + saved: false, embedding: null, + // show settings if we have at least one created host + @computed('embedding.embeddable_hosts.@each.isCreated') + showSecondary() { + const hosts = this.get('embedding.embeddable_hosts'); + return hosts.length && hosts.findProperty('isCreated'); + }, + + @computed('embedding.base_url') + embeddingCode(baseUrl) { + + const html = +`
    + +`; + + return html; + }, + actions: { saveChanges() { - this.get('embedding').update({}); + const embedding = this.get('embedding'); + const updates = embedding.getProperties(embedding.get('fields')); + + this.set('saved', false); + this.get('embedding').update(updates).then(() => { + this.set('saved', true); + }).catch(popupAjaxError); }, addHost() { diff --git a/app/assets/javascripts/admin/templates/components/embedding-setting.hbs b/app/assets/javascripts/admin/templates/components/embedding-setting.hbs new file mode 100644 index 0000000000..36dbb88692 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/embedding-setting.hbs @@ -0,0 +1,11 @@ +{{#if isCheckbox}} + +{{else}} + + {{input value=value id=inputId}} +{{/if}} + +
    diff --git a/app/assets/javascripts/admin/templates/components/highlighted-code.hbs b/app/assets/javascripts/admin/templates/components/highlighted-code.hbs new file mode 100644 index 0000000000..4d67dd6fd2 --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/highlighted-code.hbs @@ -0,0 +1 @@ +
    {{code}}
    diff --git a/app/assets/javascripts/admin/templates/embedding.hbs b/app/assets/javascripts/admin/templates/embedding.hbs index 838e3e8e94..15d021a9c1 100644 --- a/app/assets/javascripts/admin/templates/embedding.hbs +++ b/app/assets/javascripts/admin/templates/embedding.hbs @@ -1,15 +1,61 @@ -{{#if embedding.embeddable_hosts}} - - - - - - - {{#each embedding.embeddable_hosts as |host|}} - {{embeddable-host host=host deleteHost="deleteHost"}} - {{/each}} -
    {{i18n "admin.embedding.host"}}{{i18n "admin.embedding.category"}} 
    +
    + {{#if embedding.embeddable_hosts}} + + + + + + + {{#each embedding.embeddable_hosts as |host|}} + {{embeddable-host host=host deleteHost="deleteHost"}} + {{/each}} +
    {{i18n "admin.embedding.host"}}{{i18n "admin.embedding.category"}} 
    + {{else}} +

    {{i18n "admin.embedding.get_started"}}

    + {{/if}} + + {{d-button label="admin.embedding.add_host" action="addHost" icon="plus" class="btn-primary add-host"}} +
    + +{{#if showSecondary}} +
    +

    {{{i18n "admin.embedding.sample"}}}

    + {{highlighted-code code=embeddingCode lang="html"}} +
    + +
    + +
    +

    {{i18n "admin.embedding.settings"}}

    + + {{embedding-setting field="embed_by_username" value=embedding.embed_by_username}} + {{embedding-setting field="embed_post_limit" value=embedding.embed_post_limit}} + {{embedding-setting field="embed_truncate" value=embedding.embed_truncate type="checkbox"}} +
    + +
    +

    {{i18n "admin.embedding.feed_settings"}}

    +

    {{i18n "admin.embedding.feed_description"}}

    + + {{embedding-setting field="feed_polling_enabled" value=embedding.feed_polling_enabled type="checkbox"}} + {{embedding-setting field="feed_polling_url" value=embedding.feed_polling_url}} + {{embedding-setting field="embed_username_key_from_feed" value=embedding.embed_username_key_from_feed}} +
    + +
    +

    {{i18n "admin.embedding.crawling_settings"}}

    +

    {{i18n "admin.embedding.crawling_description"}}

    + + {{embedding-setting field="embed_whitelist_selector" value=embedding.embed_whitelist_selector}} + {{embedding-setting field="embed_blacklist_selector" value=embedding.embed_blacklist_selector}} +
    + +
    + {{d-button label="admin.embedding.save" + action="saveChanges" + class="btn-primary embed-save" + disabled=embedding.isSaving}} + + {{#if saved}}{{i18n "saved"}}{{/if}} +
    {{/if}} - -{{d-button label="admin.embedding.add_host" action="addHost" icon="plus" class="btn-primary"}} - diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index a988f54389..f58a79f1e9 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1648,6 +1648,36 @@ table#user-badges { margin-bottom: 10px; } + +// embedding + +.embeddable-hosts { + table { + margin-bottom: 1em; + } + margin-bottom: 2em; +} + +.embedding-secondary { + h3 { + margin: 1em 0; + } + margin-bottom: 2em; + + .embed-setting { + input[type=text] { + width: 50%; + } + margin: 0.75em 0; + } + + p.description { + color: dark-light-choose(scale-color($primary, $lightness: 50%), scale-color($secondary, $lightness: 50%)); + margin-bottom: 1em; + max-width: 700px; + } +} + // Mobile specific styles // Mobile view text-inputs need some padding .mobile-view .admin-contents { diff --git a/app/controllers/admin/embedding_controller.rb b/app/controllers/admin/embedding_controller.rb index 656ff25af0..623c2a25a2 100644 --- a/app/controllers/admin/embedding_controller.rb +++ b/app/controllers/admin/embedding_controller.rb @@ -1,3 +1,5 @@ +require_dependency 'embedding' + class Admin::EmbeddingController < Admin::AdminController before_filter :ensure_logged_in, :ensure_staff, :fetch_embedding @@ -7,15 +9,21 @@ class Admin::EmbeddingController < Admin::AdminController end def update - render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true) + Embedding.settings.each do |s| + @embedding.send("#{s}=", params[:embedding][s]) + end + + if @embedding.save + fetch_embedding + render_serialized(@embedding, EmbeddingSerializer, root: 'embedding', rest_serializer: true) + else + render_json_error(@embedding) + end end protected def fetch_embedding - @embedding = OpenStruct.new({ - id: 'default', - embeddable_hosts: EmbeddableHost.all.order(:host) - }) + @embedding = Embedding.find end end diff --git a/app/models/embedding.rb b/app/models/embedding.rb new file mode 100644 index 0000000000..c6081b1896 --- /dev/null +++ b/app/models/embedding.rb @@ -0,0 +1,41 @@ +require 'has_errors' + +class Embedding < OpenStruct + include HasErrors + + def self.settings + %i(embed_by_username + embed_post_limit + embed_truncate + embed_whitelist_selector + embed_blacklist_selector + feed_polling_enabled + feed_polling_url + embed_username_key_from_feed) + end + + def base_url + Discourse.base_url + end + + def save + Embedding.settings.each do |s| + SiteSetting.send("#{s}=", send(s)) + end + true + rescue Discourse::InvalidParameters => p + errors.add :base, p.to_s + false + end + + def embeddable_hosts + EmbeddableHost.all.order(:host) + end + + def self.find + embedding_args = { id: 'default' } + + Embedding.settings.each {|s| embedding_args[s] = SiteSetting.send(s) } + Embedding.new(embedding_args) + end +end diff --git a/app/serializers/embedding_serializer.rb b/app/serializers/embedding_serializer.rb index 0413573569..dda224edc0 100644 --- a/app/serializers/embedding_serializer.rb +++ b/app/serializers/embedding_serializer.rb @@ -1,8 +1,14 @@ class EmbeddingSerializer < ApplicationSerializer - attributes :id + attributes :id, :fields, :base_url + attributes *Embedding.settings + has_many :embeddable_hosts, serializer: EmbeddableHostSerializer, embed: :ids - def id - object.id + def fields + Embedding.settings + end + + def read_attribute_for_serialization(attr) + object.respond_to?(attr) ? object.send(attr) : send(attr) end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 5ff7e9198c..520b881c7b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2492,12 +2492,29 @@ en: delete_confirm: "Are you sure you want to delete the :%{name}: emoji?" embedding: + get_started: "If you'd like to embed Discourse on another website, begin by adding its host." confirm_delete: "Are you sure you want to delete that host?" + sample: "Use the following HTML code into your site to create and embed discourse topics. Replace REPLACE_ME with the canonical URL of the page you are embedding it on." title: "Embedding" host: "Allowed Hosts" edit: "edit" category: "Post to Category" add_host: "Add Host" + settings: "Embedding Settings" + feed_settings: "Feed Settings" + feed_description: "Providing an RSS/ATOM feed for your site can improve Discourse's ability to import your content." + crawling_settings: "Crawler Settings" + crawling_description: "When Discourse creates topics for your posts, if no RSS/ATOM feed is present it will attempt to parse your content out of your HTML. Sometimes it can be challenging to extract your content, so we provide the ability to specify CSS rules to make extraction easier." + + embed_by_username: "Username for topic creation" + embed_post_limit: "Maximum number of posts to embed" + embed_username_key_from_feed: "Key to pull discourse username from feed" + embed_truncate: "Truncate the embedded posts" + embed_whitelist_selector: "CSS selector for elements that are allowed in embeds" + embed_blacklist_selector: "CSS selector for elements that are removed from embeds" + feed_polling_enabled: "Import posts via RSS/ATOM" + feed_polling_url: "URL of RSS/ATOM feed to crawl" + save: "Save Embedding Settings" permalink: title: "Permalinks" diff --git a/config/site_settings.yml b/config/site_settings.yml index 4e5b60d5e0..c639543923 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -759,16 +759,31 @@ developer: default: false embedding: - feed_polling_enabled: false - feed_polling_url: '' + feed_polling_enabled: + default: false + hidden: true + feed_polling_url: + default: '' + hidden: true embed_by_username: default: '' type: username - embed_username_key_from_feed: '' - embed_post_limit: 100 - embed_truncate: false - embed_whitelist_selector: '' - embed_blacklist_selector: '' + hidden: true + embed_username_key_from_feed: + default: '' + hidden: true + embed_post_limit: + default: 100 + hidden: true + embed_truncate: + default: false + hidden: true + embed_whitelist_selector: + default: '' + hidden: true + embed_blacklist_selector: + default: '' + hidden: true legal: tos_url: From 676416f4787d7abb5a268f84c033dda820fdd897 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 20 Aug 2015 16:10:19 -0400 Subject: [PATCH 168/237] FIX: Build broken when someone didn't have an embed category --- db/migrate/20150818190757_create_embeddable_hosts.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/db/migrate/20150818190757_create_embeddable_hosts.rb b/db/migrate/20150818190757_create_embeddable_hosts.rb index a0293d3f8d..1d4e1b0d46 100644 --- a/db/migrate/20150818190757_create_embeddable_hosts.rb +++ b/db/migrate/20150818190757_create_embeddable_hosts.rb @@ -6,10 +6,14 @@ class CreateEmbeddableHosts < ActiveRecord::Migration t.timestamps end - category_id = execute("SELECT c.id FROM categories AS c + category_id = 0; + category_row = execute("SELECT c.id FROM categories AS c INNER JOIN site_settings AS s ON s.value = c.name - WHERE s.name = 'embed_category'")[0]['id'].to_i + WHERE s.name = 'embed_category'") + if category_row.cmd_tuples > 0 + category_id = category_row[0]['id'].to_i + end if category_id == 0 category_id = execute("SELECT value FROM site_settings WHERE name = 'uncategorized_category_id'")[0]['value'].to_i From 7147c0e8af480ea461181776fad726bcca11b65e Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 13:10:54 -0700 Subject: [PATCH 169/237] DEV: Replace sqrt() implementation with lib Copied the needed functions out of https://github.com/terkel/mathsass MIT license --- .../stylesheets/common/foundation/math.scss | 165 ++++++++++++++++++ .../common/foundation/variables.scss | 25 +-- lib/sass/discourse_sass_compiler.rb | 6 + 3 files changed, 172 insertions(+), 24 deletions(-) create mode 100644 app/assets/stylesheets/common/foundation/math.scss diff --git a/app/assets/stylesheets/common/foundation/math.scss b/app/assets/stylesheets/common/foundation/math.scss new file mode 100644 index 0000000000..f7132c4270 --- /dev/null +++ b/app/assets/stylesheets/common/foundation/math.scss @@ -0,0 +1,165 @@ +// This file: +// Copyright (c) 2013 Takeru Suzuki +// Licensed under the MIT license. +// https://github.com/terkel/mathsass + +// Constants +$E: 2.718281828459045; +$PI: 3.141592653589793; +$LN2: 0.6931471805599453; +$SQRT2: 1.4142135623730951; + +@function error($message) { + @warn "#{_error("The direction used does not exist")}"; + @return null; +} + +// Returns the factorial of a non-negative integer. +// @param {Number} $x A non-negative integer. +// @return {Number} +// @example +// fact(0) // 1 +// fact(8) // 40320 +@function fact ($x) { + @if $x < 0 or $x != floor($x) { + @warn "Argument for `fact()` must be a positive integer."; + @return null; + } + $ret: 1; + @while $x > 0 { + $ret: $ret * $x; + $x: $x - 1; + } + @return $ret; +} + + +// Returns a two-element list containing the normalized fraction and exponent of number. +// @param {Number} $x +// @return {List} fraction, exponent +@function frexp ($x) { + $exp: 0; + @if $x < 0 { + $x: $x * -1; + } + @if $x < 0.5 { + @while $x < 0.5 { + $x: $x * 2; + $exp: $exp - 1; + } + } @else if $x >= 1 { + @while $x >= 1 { + $x: $x / 2; + $exp: $exp + 1; + } + } + @return $x, $exp; +} + +// Returns $x * 2^$exp +// @param {Number} $x +// @param {Number} $exp +@function ldexp ($x, $exp) { + $b: if($exp >= 0, 2, 1 / 2); + @if $exp < 0 { + $exp: $exp * -1; + } + @while $exp > 0 { + @if $exp % 2 == 1 { + $x: $x * $b; + } + $b: $b * $b; + $exp: floor($exp * 0.5); + } + @return $x; +} + +// Returns the natural logarithm of a number. +// @param {Number} $x +// @example +// log(2) // 0.69315 +// log(10) // 2.30259 +@function log ($x) { + @if $x <= 0 { + @return 0 / 0; + } + $k: nth(frexp($x / $SQRT2), 2); + $x: $x / ldexp(1, $k); + $x: ($x - 1) / ($x + 1); + $x2: $x * $x; + $i: 1; + $s: $x; + $sp: null; + @while $sp != $s { + $x: $x * $x2; + $i: $i + 2; + $sp: $s; + $s: $s + $x / $i; + } + @return $LN2 * $k + 2 * $s; +} + +@function ipow($base, $exp) { + @if $exp != floor($exp) { + @return error("Exponent for `ipow()` must be an integer."); + } + $r: 1; + $s: 0; + @if $exp < 0 { + $exp: $exp * -1; + $s: 1; + } + @while $exp > 0 { + @if $exp % 2 == 1 { + $r: $r * $base; + } + $exp: floor($exp * 0.5); + $base: $base * $base; + } + @return if($s != 0, 1 / $r, $r); +} + +// Returns E^x, where x is the argument, and E is Euler's constant, the base of the natural logarithms. +// @param {Number} $x +// @example +// exp(1) // 2.71828 +// exp(-1) // 0.36788 +@function exp ($x) { + $ret: 0; + @for $n from 0 to 24 { + $ret: $ret + ipow($x, $n) / fact($n); + } + @return $ret; +} + +// Returns base to the exponent power. +// @param {Number} $base The base number +// @param {Number} $exp The exponent to which to raise base +// @return {Number} +// @example +// pow(4, 2) // 16 +// pow(4, -2) // 0.0625 +// pow(4, 0.2) // 1.31951 +@function pow ($base, $exp) { + @if $exp == floor($exp) { + @return ipow($base, $exp); + } @else { + @return exp(log($base) * $exp); + } +} + +// Returns the square root of a number. +// @param {Number} $x +// @example +// sqrt(2) // 1.41421 +// sqrt(5) // 2.23607 +@function sqrt ($x) { + @if $x < 0 { + @return error("Argument for `sqrt()` must be a positive number."); + } + $ret: 1; + @for $i from 1 through 24 { + $ret: $ret - (pow($ret, 2) - $x) / (2 * $ret); + } + @return $ret; +} diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index b5c4f27071..ae0b4e01bf 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -30,33 +30,10 @@ $base-font-family: Helvetica, Arial, sans-serif !default; /* These files don't actually exist. They're injected by DiscourseSassImporter. */ @import "theme_variables"; @import "plugins_variables"; +@import "common/foundation/math"; // Color Utilities -// Square Root function -// http://codepen.io/Designer023/pen/DkEtw -@function approximateSq($num, $approx) { - $root : (( $num / $approx ) + $approx) / 2; - @return $root; -} - -@function sqrt($num) { - $root:0; - $testRoot : 0.001; - $upperBounds : round($num / 2) + 1; //never need over half the main number. Add one just to be sure! - $loops : $upperBounds; - @for $test from 2 through $upperBounds { - $sq : $test * $test; - @if $sq <= $num { - $testRoot : $test; - } - } - - $root : (approximateSq($num, $testRoot)); - - @return $root; -} - // w3c definition of color brightness @function brightness($color) { @return ((red($color) * .299) + (green($color) * .587) + (blue($color) * .114)); diff --git a/lib/sass/discourse_sass_compiler.rb b/lib/sass/discourse_sass_compiler.rb index c8f1908245..fb0c2a66c3 100644 --- a/lib/sass/discourse_sass_compiler.rb +++ b/lib/sass/discourse_sass_compiler.rb @@ -1,6 +1,12 @@ require_dependency 'sass/discourse_sass_importer' require 'pathname' +module Sass::Script::Functions + def _error(message) + raise Sass::SyntaxError, mesage + end +end + class DiscourseSassCompiler def self.compile(scss, target, opts={}) From 8c03dd16af73dd936a1b97aa5b6cfc689d21bc9b Mon Sep 17 00:00:00 2001 From: Gerhard Schlager Date: Thu, 20 Aug 2015 22:15:57 +0200 Subject: [PATCH 170/237] Adds support for importing password hashes used by "migratepassword" plugin Adds setting to phpBB3 importer for importing passwords (default: off) Plugin: https://github.com/discoursehosting/discourse-migratepassword --- script/import_scripts/base.rb | 5 +++++ script/import_scripts/phpbb3/database/database_3_0.rb | 2 +- script/import_scripts/phpbb3/database/database_3_1.rb | 2 +- script/import_scripts/phpbb3/importers/user_importer.rb | 1 + script/import_scripts/phpbb3/settings.yml | 5 +++++ script/import_scripts/phpbb3/support/settings.rb | 2 ++ 6 files changed, 15 insertions(+), 2 deletions(-) diff --git a/script/import_scripts/base.rb b/script/import_scripts/base.rb index b7326e946c..0bc796c730 100644 --- a/script/import_scripts/base.rb +++ b/script/import_scripts/base.rb @@ -273,6 +273,7 @@ class ImportScripts::Base u.custom_fields["import_id"] = import_id u.custom_fields["import_username"] = opts[:username] if opts[:username].present? u.custom_fields["import_avatar_url"] = avatar_url if avatar_url.present? + u.custom_fields["import_pass"] = opts[:password] if opts[:password].present? begin User.transaction do @@ -284,6 +285,10 @@ class ImportScripts::Base u.user_profile.save! end end + + if opts[:active] && opts[:password].present? + u.activate + end rescue # try based on email existing = User.find_by(email: opts[:email].downcase) diff --git a/script/import_scripts/phpbb3/database/database_3_0.rb b/script/import_scripts/phpbb3/database/database_3_0.rb index 35c5b15e50..e06d8f7ba5 100644 --- a/script/import_scripts/phpbb3/database/database_3_0.rb +++ b/script/import_scripts/phpbb3/database/database_3_0.rb @@ -14,7 +14,7 @@ module ImportScripts::PhpBB3 def fetch_users(offset) query(<<-SQL) - SELECT u.user_id, u.user_email, u.username, u.user_regdate, u.user_lastvisit, u.user_ip, + 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 diff --git a/script/import_scripts/phpbb3/database/database_3_1.rb b/script/import_scripts/phpbb3/database/database_3_1.rb index bf13546e2d..f51f1b5a4c 100644 --- a/script/import_scripts/phpbb3/database/database_3_1.rb +++ b/script/import_scripts/phpbb3/database/database_3_1.rb @@ -5,7 +5,7 @@ module ImportScripts::PhpBB3 class Database_3_1 < Database_3_0 def fetch_users(offset) query(<<-SQL) - SELECT u.user_id, u.user_email, u.username, u.user_regdate, u.user_lastvisit, u.user_ip, + 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, f.pf_phpbb_website AS user_website, f.pf_phpbb_location AS user_from, u.user_birthday, u.user_avatar_type, u.user_avatar diff --git a/script/import_scripts/phpbb3/importers/user_importer.rb b/script/import_scripts/phpbb3/importers/user_importer.rb index 0a9ba8c45b..be0321d48e 100644 --- a/script/import_scripts/phpbb3/importers/user_importer.rb +++ b/script/import_scripts/phpbb3/importers/user_importer.rb @@ -16,6 +16,7 @@ module ImportScripts::PhpBB3 id: row[:user_id], email: row[:user_email], username: row[:username], + password: @settings.import_passwords ? row[:user_password] : nil, name: @settings.username_as_name ? row[:username] : '', created_at: Time.zone.at(row[:user_regdate]), last_seen_at: row[:user_lastvisit] == 0 ? Time.zone.at(row[:user_regdate]) : Time.zone.at(row[:user_lastvisit]), diff --git a/script/import_scripts/phpbb3/settings.yml b/script/import_scripts/phpbb3/settings.yml index b591d39646..d7ee6174e2 100644 --- a/script/import_scripts/phpbb3/settings.yml +++ b/script/import_scripts/phpbb3/settings.yml @@ -33,6 +33,11 @@ import: # When false: The system user will be used for all anonymous users. anonymous_users: true + # Enable this, if you want import password hashes in order to use the "migratepassword" plugin. + # This will allow users to login with their current password. + # The plugin is available at: https://github.com/discoursehosting/discourse-migratepassword + passwords: false + # By default all the following things get imported. You can disable them by setting them to false. bookmarks: true attachments: true diff --git a/script/import_scripts/phpbb3/support/settings.rb b/script/import_scripts/phpbb3/support/settings.rb index 8a0c36ee19..1c68c5f8b7 100644 --- a/script/import_scripts/phpbb3/support/settings.rb +++ b/script/import_scripts/phpbb3/support/settings.rb @@ -12,6 +12,7 @@ module ImportScripts::PhpBB3 attr_reader :import_private_messages attr_reader :import_polls attr_reader :import_bookmarks + attr_reader :import_passwords attr_reader :import_uploaded_avatars attr_reader :import_remote_avatars @@ -36,6 +37,7 @@ module ImportScripts::PhpBB3 @import_private_messages = import_settings['private_messages'] @import_polls = import_settings['polls'] @import_bookmarks = import_settings['bookmarks'] + @import_passwords = import_settings['passwords'] avatar_settings = import_settings['avatars'] @import_uploaded_avatars = avatar_settings['uploaded'] From 3eb2668fcf617518baab17dd7d0078f0a4b2a530 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 13:30:34 -0700 Subject: [PATCH 171/237] Add color boxes on the theme chooser page TODO: filter down to the colors actually used --- .../admin/templates/customize_colors.hbs | 87 +++++++++++++++++++ .../stylesheets/common/admin/admin_base.scss | 54 ++++++++++++ 2 files changed, 141 insertions(+) diff --git a/app/assets/javascripts/admin/templates/customize_colors.hbs b/app/assets/javascripts/admin/templates/customize_colors.hbs index 85fc27abb8..439e819f4a 100644 --- a/app/assets/javascripts/admin/templates/customize_colors.hbs +++ b/app/assets/javascripts/admin/templates/customize_colors.hbs @@ -40,6 +40,93 @@
    +
    + Various greys used throught the UI. +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + choose-grey() +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + choose-grey() +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + dark-light-diff() +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + dark-light-diff() +
    +
    + {{#if colors.length}} diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index a988f54389..7043a13496 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1663,3 +1663,57 @@ table#user-badges { .mobile-view .full-width { margin: 0; } + +.cboxcontainer { + display: inline-block; + padding: 8px; + padding-bottom: 4px; + + * { + width: 20px; + height: 20px; + display: inline-block; + border: 1px solid $tertiary; + } + &.primary { + background: $primary; + } + &.secondary { + background: $secondary; + } +} +.cbox0 { background: choose-grey(0%); } +.cbox10 { background: choose-grey(10%); } +.cbox20 { background: choose-grey(20%); } +.cbox30 { background: choose-grey(30%); } +.cbox40 { background: choose-grey(40%); } +.cbox50 { background: choose-grey(50%); } +.cbox60 { background: choose-grey(60%); } +.cbox70 { background: choose-grey(70%); } +.cbox80 { background: choose-grey(80%); } +.cbox90 { background: choose-grey(90%); } +.cbox100 { background: choose-grey(100%); } +.cbox5 { background: choose-grey(5%); } +.cbox15 { background: choose-grey(15%); } +.cbox25 { background: choose-grey(25%); } +.cbox95 { background: choose-grey(95%); } +.cbox85 { background: choose-grey(85%); } +.cbox75 { background: choose-grey(75%); } + +.dbox0 { background: dark-light-diff($primary, $secondary, 0%, -0%); } +.dbox10 { background: dark-light-diff($primary, $secondary, 10%, -10%); } +.dbox20 { background: dark-light-diff($primary, $secondary, 20%, -20%); } +.dbox30 { background: dark-light-diff($primary, $secondary, 30%, -30%); } +.dbox40 { background: dark-light-diff($primary, $secondary, 40%, -40%); } +.dbox50 { background: dark-light-diff($primary, $secondary, 50%, -50%); } +.dbox60 { background: dark-light-diff($primary, $secondary, 60%, -60%); } +.dbox70 { background: dark-light-diff($primary, $secondary, 70%, -70%); } +.dbox80 { background: dark-light-diff($primary, $secondary, 80%, -80%); } +.dbox90 { background: dark-light-diff($primary, $secondary, 90%, -90%); } +.dbox100 { background: dark-light-diff($primary, $secondary, 100%, -100%); } +.dbox5 { background: dark-light-diff($primary, $secondary, 5%, -5%); } +.dbox15 { background: dark-light-diff($primary, $secondary, 15%, -15%); } +.dbox25 { background: dark-light-diff($primary, $secondary, 25%, -25%); } +.dbox95 { background: dark-light-diff($primary, $secondary, 95%, -95%); } +.dbox85 { background: dark-light-diff($primary, $secondary, 85%, -85%); } +.dbox75 { background: dark-light-diff($primary, $secondary, 75%, -75%); } From 26c3d7446072bfdfdae8c3774f9e7fb4474ed4b8 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 13:31:00 -0700 Subject: [PATCH 172/237] Split srgb-scale into its own function --- .../common/foundation/variables.scss | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index ae0b4e01bf..3acece6258 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -39,24 +39,32 @@ $base-font-family: Helvetica, Arial, sans-serif !default; @return ((red($color) * .299) + (green($color) * .587) + (blue($color) * .114)); } -// Replaces dark-light-diff($primary, $secondary, 50%, -50%) // Uses an approximation of sRGB blending, GAMMA=2 instead of GAMMA=2.2 -@function choose-grey($percent) { +@function srgb-scale($foreground, $background, $percent) { $ratio: ($percent / 100%); $iratio: 1 - $ratio; - $pr2: red($primary) * red($primary); - $pg2: green($primary) * green($primary); - $pb2: blue($primary) * blue($primary); - $sr2: red($secondary) * red($secondary); - $sg2: green($secondary) * green($secondary); - $sb2: blue($secondary) * blue($secondary); - $rr2: $pr2 * $ratio + $sr2 * $iratio; - $rg2: $pg2 * $ratio + $sg2 * $iratio; - $rb2: $pb2 * $ratio + $sb2 * $iratio; - $rr: sqrt($rr2); - $rg: sqrt($rg2); - $rb: sqrt($rb2); - @return rgb($rr, $rg, $rb); + $f_r2: red($foreground) * red($foreground); + $f_g2: green($foreground) * green($foreground); + $f_b2: blue($foreground) * blue($foreground); + $b_r2: red($background) * red($background); + $b_g2: green($background) * green($background); + $b_b2: blue($background) * blue($background); + $r_r2: $f_r2 * $ratio + $b_r2 * $iratio; + $r_g2: $f_g2 * $ratio + $b_g2 * $iratio; + $r_b2: $f_b2 * $ratio + $b_b2 * $iratio; + $r_r: sqrt($r_r2); + $r_g: sqrt($r_g2); + $r_b: sqrt($r_b2); + @return rgb($r_r, $r_g, $r_b); +} + +// Replaces dark-light-diff($primary, $secondary, 50%, -50%) +@function choose-grey($percent) { + @return srgb-scale($primary, $secondary, $percent); +} + +@function choose-gray($percent) { + @return choose-grey($percent); } @function dark-light-diff($adjusted-color, $comparison-color, $lightness, $darkness) { From 7e8ee8e7257a37626ca488bca39cc5331af6bd4f Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 13:52:37 -0700 Subject: [PATCH 173/237] FIX: mobile composer dark theme --- app/assets/stylesheets/mobile/compose.scss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index c850e804d0..91c2ef2ba9 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -128,6 +128,9 @@ display: none !important; // can be removed if inline JS CSS is removed from com margin-top: 6px; width: 99%; box-sizing: border-box; + background: $secondary; + color: $primary; + border-color: choose-grey(10%); } .wmd-controls { transition: top 0.3s ease; @@ -172,7 +175,7 @@ display: none !important; // can be removed if inline JS CSS is removed from com background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } .wmd-input { - color: darken($primary, 40%); + color: dark-light-choose(darken($primary, 40%), choose-grey(90%)); } .wmd-input { bottom: 35px; From 9c92a491b53da8b85763999c39c8d6c788d4849b Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 13:59:32 -0700 Subject: [PATCH 174/237] FIX: Tweaks to mobile select posts UI --- app/assets/stylesheets/mobile/compose.scss | 9 ++++++--- app/assets/stylesheets/mobile/topic-post.scss | 6 +++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index 91c2ef2ba9..4fb2c9c004 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -13,6 +13,12 @@ display: none !important; // can be removed if inline JS CSS is removed from com display: none !important; // can be removed if inline JS CSS is removed from composer-popup } +input { + background: $secondary; + color: $primary; + border-color: choose-grey(15%); +} + #reply-control { // used for upload link .composer-bottom-right { @@ -128,9 +134,6 @@ display: none !important; // can be removed if inline JS CSS is removed from com margin-top: 6px; width: 99%; box-sizing: border-box; - background: $secondary; - color: $primary; - border-color: choose-grey(10%); } .wmd-controls { transition: top 0.3s ease; diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index d69d3b1372..844b479d05 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -399,11 +399,11 @@ iframe { float: left; width: 97%; padding-left: 3%; - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: srgb-scale($tertiary, $secondary, 15%); .btn { margin-bottom: 10px; color: $secondary; - background: scale-color($tertiary, $lightness: 50%); + background: $tertiary; clear: both; } p { @@ -420,7 +420,7 @@ button.select-post { position: absolute; z-index: 401; // 400 is the reply-to tab left: 200px; - background-color: scale-color($tertiary, $lightness: 50%); + background-color: srgb-scale($tertiary, $secondary, 60%); color: $secondary; padding: 5px; } From 123f50cd7125d9960be04f75bd770d09f19d9453 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 14:05:04 -0700 Subject: [PATCH 175/237] FIX: Mobile user profile --- app/assets/stylesheets/mobile/user.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index f64f925616..da7ae77f4c 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -261,7 +261,7 @@ .details { padding: 15px 15px 4px 15px; - background-color: rgba($primary, .9); + background-color: dark-light-choose(rgba($primary, .9), rgba($secondary, .9)); h1 { font-size: 2.143em; @@ -306,7 +306,7 @@ width: 100%; position: relative; float: left; - color: dark-light-diff($secondary, $primary, 75%, 0%); + color: dark-light-choose(scale-color($secondary, $lightness: 75%), choose-grey(90%)); h1 {font-weight: bold;} @@ -362,14 +362,14 @@ } .secondary { display: none; } - .profile-image { - height: 0; - } + .profile-image { + height: 0; + } .details { padding: 12px 15px 2px 15px; margin-top: 0; - background: rgba($primary, 1); + background: dark-light-choose(rgba($primary, 1), choose-grey(5%)); .bio { display: none; } .primary { From 7083bfdf2716f695992fa66e82d018342ca4fee4 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 14:25:07 -0700 Subject: [PATCH 176/237] FIX: /user/x/notifications in mobile dark theme --- app/assets/stylesheets/mobile/user.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index da7ae77f4c..42032f3fec 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -468,7 +468,7 @@ } .notification { &.unread { - background-color: dark-light-diff($tertiary, $secondary, 85%, -65%); + background-color: dark-light-choose(scale-color($tertiary, $lightness: 85%), srgb-scale($tertiary, $secondary, 15%)); } li { display: inline-block; } From aaccb73a3bbaecfaa20488ef4371a8d93ea94974 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 14:35:28 -0700 Subject: [PATCH 177/237] Use choose-grey(5%) instead of diff(97%) --- app/assets/stylesheets/common/base/topic-post.scss | 2 +- app/assets/stylesheets/common/components/badges.css.scss | 4 ++-- app/assets/stylesheets/common/foundation/mixins.scss | 2 +- app/assets/stylesheets/desktop/topic-post.scss | 8 ++++---- app/assets/stylesheets/mobile/topic-post.scss | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index c655b6d055..1908afc4dc 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -169,7 +169,7 @@ pre { display: block; padding: 5px 10px; color: $primary; - background: dark-light-diff($primary, $secondary, 97%, -65%); + background: choose-grey(5%); max-height: 500px; } } diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index 0a03ef0aa0..d83ea438d9 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -265,9 +265,9 @@ &.clicks { font-weight: normal; - background-color: dark-light-diff($primary, $secondary, 85%, -65%); + background-color: dark-light-diff($primary, $secondary, 85%, -60%); top: -1px; - color: dark-light-diff($primary, $secondary, 50%, -45%); + color: dark-light-diff($primary, $secondary, 50%, -20%); position: relative; margin-left: 2px; border: none; diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index 4b560cc46f..25ec023f0a 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -98,5 +98,5 @@ // Stuff we repeat @mixin post-aside { border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -85%); - background-color: dark-light-diff($primary, $secondary, 97%, -75%); + background-color: choose-grey(5%); } diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 2a97d2dc9e..7c389b38f6 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -308,7 +308,7 @@ a.star { .topic-map { margin: 20px 0 0 0; - background: dark-light-diff($primary, $secondary, 97%, -75%); + background: choose-grey(5%); border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: none; // would cause double top border @@ -414,7 +414,7 @@ a.star { border: 0; padding: 0 23px; color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); - background: dark-light-diff($primary, $secondary, 97%, -75%); + background: choose-grey(5%); border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); &:hover { @@ -626,13 +626,13 @@ blockquote { .quote { &>blockquote { .onebox-result { - background-color: dark-light-diff($primary, $secondary, 97%, -45%); + background-color: choose-grey(5%); } } aside { .quote, .title, blockquote, .onebox, .onebox-result { - background: dark-light-diff($primary, $secondary, 97%, -45%); + background: choose-grey(5%); border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -65%); } diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 844b479d05..050d6d6d6f 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -168,7 +168,7 @@ a.star { .topic-map { margin: 10px 0; - background: dark-light-diff($primary, $secondary, 97%, -45%); + background: choose-grey(5%); border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: none; // would cause double top border @@ -287,7 +287,7 @@ a.star { border: 0; padding: 0 15px; color: $primary; - background: dark-light-diff($primary, $secondary, 97%, -75%); + background: choose-grey(5%); border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); .fa { From fad5af0f7d2f134237a0cd0b623c3d783954722e Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 15:10:09 -0700 Subject: [PATCH 178/237] FEATURE: Green/red background for ins/del elements --- app/assets/stylesheets/common/base/topic-post.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 1908afc4dc..167cbd4e45 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -31,6 +31,8 @@ h1, h2, h3, h4, h5, h6 { margin: 30px 0 10px; } h1 { line-height: 1em; } /* normalize.css sets h1 font size but not line height */ a { word-wrap: break-word; } + ins { background-color: dark-light-choose(scale-color($success, $lightness: 90%), scale-color($success, $lightness: -60%)); } + del { background-color: dark-light-choose(scale-color($danger, $lightness: 90%), scale-color($danger, $lightness: -60%)); } } From eb00a924521638ee7430d00d1fb9c33c7468e363 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 16:33:44 -0700 Subject: [PATCH 179/237] FIX: onebox links were too dark --- app/assets/stylesheets/common/base/onebox.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/onebox.scss b/app/assets/stylesheets/common/base/onebox.scss index 390ac83d52..1ca4e635ea 100644 --- a/app/assets/stylesheets/common/base/onebox.scss +++ b/app/assets/stylesheets/common/base/onebox.scss @@ -120,12 +120,12 @@ aside.onebox { } a[href] { - color: scale-color($tertiary, $lightness: -20%); + color: dark-light-choose(scale-color($tertiary, $lightness: -20%), $tertiary); text-decoration: none; } a[href]:visited { - color: scale-color($tertiary, $lightness: -20%); + color: dark-light-choose(scale-color($tertiary, $lightness: -20%), $tertiary); } img { From 1218d47eb50057d6a841c1136a584886938ac348 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 16:47:34 -0700 Subject: [PATCH 180/237] Rename choose-grey() to blend-primary-secondary() --- .../admin/templates/customize_colors.hbs | 4 +-- .../stylesheets/common/admin/admin_base.scss | 36 ++++++++++--------- .../stylesheets/common/base/topic-post.scss | 2 +- .../stylesheets/common/foundation/mixins.scss | 2 +- .../common/foundation/variables.scss | 6 +--- .../stylesheets/desktop/topic-post.scss | 8 ++--- app/assets/stylesheets/mobile/compose.scss | 4 +-- app/assets/stylesheets/mobile/topic-post.scss | 4 +-- app/assets/stylesheets/mobile/user.scss | 4 +-- 9 files changed, 34 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/admin/templates/customize_colors.hbs b/app/assets/javascripts/admin/templates/customize_colors.hbs index 439e819f4a..4e5dc4a5a2 100644 --- a/app/assets/javascripts/admin/templates/customize_colors.hbs +++ b/app/assets/javascripts/admin/templates/customize_colors.hbs @@ -60,7 +60,7 @@
    - choose-grey() + blend-primary-secondary()
    @@ -81,7 +81,7 @@
    - choose-grey() + blend-primary-secondary()
    diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 7043a13496..fcbe282575 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1682,23 +1682,25 @@ table#user-badges { background: $secondary; } } -.cbox0 { background: choose-grey(0%); } -.cbox10 { background: choose-grey(10%); } -.cbox20 { background: choose-grey(20%); } -.cbox30 { background: choose-grey(30%); } -.cbox40 { background: choose-grey(40%); } -.cbox50 { background: choose-grey(50%); } -.cbox60 { background: choose-grey(60%); } -.cbox70 { background: choose-grey(70%); } -.cbox80 { background: choose-grey(80%); } -.cbox90 { background: choose-grey(90%); } -.cbox100 { background: choose-grey(100%); } -.cbox5 { background: choose-grey(5%); } -.cbox15 { background: choose-grey(15%); } -.cbox25 { background: choose-grey(25%); } -.cbox95 { background: choose-grey(95%); } -.cbox85 { background: choose-grey(85%); } -.cbox75 { background: choose-grey(75%); } +.cbox0 { background: blend-primary-secondary(0%); } +.cbox10 { background: blend-primary-secondary(10%); } +.cbox20 { background: blend-primary-secondary(20%); } +.cbox30 { background: blend-primary-secondary(30%); } +.cbox40 { background: blend-primary-secondary(40%); } +.cbox50 { background: blend-primary-secondary(50%); } +.cbox60 { background: blend-primary-secondary(60%); } +.cbox70 { background: blend-primary-secondary(70%); } +.cbox80 { background: blend-primary-secondary(80%); } +.cbox90 { background: blend-primary-secondary(90%); } +.cbox100 { background: blend-primary-secondary(100%); } +.cbox5 { background: blend-primary-secondary(5%); } +.cbox7 { background: blend-primary-secondary(7%); } +.cbox15 { background: blend-primary-secondary(15%); } +.cbox17 { background: blend-primary-secondary(17%); } +.cbox25 { background: blend-primary-secondary(25%); } +.cbox95 { background: blend-primary-secondary(95%); } +.cbox85 { background: blend-primary-secondary(85%); } +.cbox75 { background: blend-primary-secondary(75%); } .dbox0 { background: dark-light-diff($primary, $secondary, 0%, -0%); } .dbox10 { background: dark-light-diff($primary, $secondary, 10%, -10%); } diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 167cbd4e45..ab1437c068 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -171,7 +171,7 @@ pre { display: block; padding: 5px 10px; color: $primary; - background: choose-grey(5%); + background: blend-primary-secondary(5%); max-height: 500px; } } diff --git a/app/assets/stylesheets/common/foundation/mixins.scss b/app/assets/stylesheets/common/foundation/mixins.scss index 25ec023f0a..1770d524d4 100644 --- a/app/assets/stylesheets/common/foundation/mixins.scss +++ b/app/assets/stylesheets/common/foundation/mixins.scss @@ -98,5 +98,5 @@ // Stuff we repeat @mixin post-aside { border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -85%); - background-color: choose-grey(5%); + background-color: blend-primary-secondary(5%); } diff --git a/app/assets/stylesheets/common/foundation/variables.scss b/app/assets/stylesheets/common/foundation/variables.scss index 3acece6258..b09dbc2893 100644 --- a/app/assets/stylesheets/common/foundation/variables.scss +++ b/app/assets/stylesheets/common/foundation/variables.scss @@ -59,14 +59,10 @@ $base-font-family: Helvetica, Arial, sans-serif !default; } // Replaces dark-light-diff($primary, $secondary, 50%, -50%) -@function choose-grey($percent) { +@function blend-primary-secondary($percent) { @return srgb-scale($primary, $secondary, $percent); } -@function choose-gray($percent) { - @return choose-grey($percent); -} - @function dark-light-diff($adjusted-color, $comparison-color, $lightness, $darkness) { @if brightness($adjusted-color) < brightness($comparison-color) { @return scale-color($adjusted-color, $lightness: $lightness); diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index 7c389b38f6..5d6195a4eb 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -308,7 +308,7 @@ a.star { .topic-map { margin: 20px 0 0 0; - background: choose-grey(5%); + background: blend-primary-secondary(5%); border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: none; // would cause double top border @@ -414,7 +414,7 @@ a.star { border: 0; padding: 0 23px; color: dark-light-choose(scale-color($primary, $lightness: 60%), scale-color($secondary, $lightness: 40%)); - background: choose-grey(5%); + background: blend-primary-secondary(5%); border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); &:hover { @@ -626,13 +626,13 @@ blockquote { .quote { &>blockquote { .onebox-result { - background-color: choose-grey(5%); + background-color: blend-primary-secondary(5%); } } aside { .quote, .title, blockquote, .onebox, .onebox-result { - background: choose-grey(5%); + background: blend-primary-secondary(5%); border-left: 5px solid dark-light-diff($primary, $secondary, 90%, -65%); } diff --git a/app/assets/stylesheets/mobile/compose.scss b/app/assets/stylesheets/mobile/compose.scss index 4fb2c9c004..cc0c26f0f1 100644 --- a/app/assets/stylesheets/mobile/compose.scss +++ b/app/assets/stylesheets/mobile/compose.scss @@ -16,7 +16,7 @@ display: none !important; // can be removed if inline JS CSS is removed from com input { background: $secondary; color: $primary; - border-color: choose-grey(15%); + border-color: blend-primary-secondary(15%); } #reply-control { @@ -178,7 +178,7 @@ input { background-color: dark-light-choose(scale-color($primary, $lightness: 75%), scale-color($secondary, $lightness: 25%)); } .wmd-input { - color: dark-light-choose(darken($primary, 40%), choose-grey(90%)); + color: dark-light-choose(darken($primary, 40%), blend-primary-secondary(90%)); } .wmd-input { bottom: 35px; diff --git a/app/assets/stylesheets/mobile/topic-post.scss b/app/assets/stylesheets/mobile/topic-post.scss index 050d6d6d6f..ef1bbbe8b3 100644 --- a/app/assets/stylesheets/mobile/topic-post.scss +++ b/app/assets/stylesheets/mobile/topic-post.scss @@ -168,7 +168,7 @@ a.star { .topic-map { margin: 10px 0; - background: choose-grey(5%); + background: blend-primary-secondary(5%); border: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: none; // would cause double top border @@ -287,7 +287,7 @@ a.star { border: 0; padding: 0 15px; color: $primary; - background: choose-grey(5%); + background: blend-primary-secondary(5%); border-left: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -65%); .fa { diff --git a/app/assets/stylesheets/mobile/user.scss b/app/assets/stylesheets/mobile/user.scss index 42032f3fec..13b7ebad09 100644 --- a/app/assets/stylesheets/mobile/user.scss +++ b/app/assets/stylesheets/mobile/user.scss @@ -306,7 +306,7 @@ width: 100%; position: relative; float: left; - color: dark-light-choose(scale-color($secondary, $lightness: 75%), choose-grey(90%)); + color: dark-light-choose(scale-color($secondary, $lightness: 75%), blend-primary-secondary(90%)); h1 {font-weight: bold;} @@ -369,7 +369,7 @@ .details { padding: 12px 15px 2px 15px; margin-top: 0; - background: dark-light-choose(rgba($primary, 1), choose-grey(5%)); + background: dark-light-choose(rgba($primary, 1), blend-primary-secondary(5%)); .bio { display: none; } .primary { From 7c7580d2267f63d9355c52348a7ef4e1ec43ee85 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 17:10:31 -0700 Subject: [PATCH 181/237] FIX: Remove mouseover listener --- .../javascripts/discourse/lib/desktop-notifications.js.es6 | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 index a0e2cc7d23..641b721cfe 100644 --- a/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 +++ b/app/assets/javascripts/discourse/lib/desktop-notifications.js.es6 @@ -80,7 +80,6 @@ function setupNotifications() { if (document) { document.addEventListener("scroll", resetIdle); } - window.addEventListener("mouseover", resetIdle); PageTracker.on("change", resetIdle); } From 2363897a258fdc886b3c55384d125696b6cd57e9 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 18:27:19 -0700 Subject: [PATCH 182/237] FEATURE: Arbitrary validations for site settings --- config/locales/server.en.yml | 11 +++++++---- lib/site_setting_extension.rb | 6 ++++++ lib/site_setting_validations.rb | 17 +++++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) create mode 100644 lib/site_setting_validations.rb diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 429fe03dfe..dba03554a7 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -90,6 +90,11 @@ en: other: ! '%{count} errors prohibited this %{model} from being saved' embed: load_from_remote: "There was an error loading that post." + site_settings: + min_username_length_exists: "You cannot set the minimum username length above the shortest username." + min_username_length_range: "You cannot set the minimum above the maximum." + max_username_length_exists: "You cannot set the maximum username length below the longest username." + max_username_length_range: "You cannot set the maximum above the minimum." activemodel: errors: @@ -892,14 +897,12 @@ en: invite_expiry_days: "How long user invitation keys are valid, in days" invite_passthrough_hours: "How long a user can use a previously redeemed invitation key to log in, in hours" - # TODO: perhaps we need a way of protecting these settings for hosted solution, global settings ... - invite_only: "Public registration is disabled, all new users must be explicitly invited by other members or staff." login_required: "Require authentication to read content on this site, disallow anonymous access." - min_username_length: "Minimum username length in characters. WARNING: ANY EXISTING USERS WITH NAMES SHORTER THAN THIS WILL BE UNABLE TO ACCESS THE SITE." - max_username_length: "Maximum username length in characters. WARNING: ANY EXISTING USERS WITH NAMES LONGER THAN THIS WILL BE UNABLE TO ACCESS THE SITE." + min_username_length: "Minimum username length in characters." + max_username_length: "Maximum username length in characters." reserved_usernames: "Usernames for which signup is not allowed." diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 228a7e6f39..f4c7c1df20 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -1,7 +1,9 @@ require_dependency 'enum' require_dependency 'site_settings/db_provider' +require 'site_setting_validations' module SiteSettingExtension + include SiteSettingValidations # For plugins, so they can tell if a feature is supported def supported_types @@ -303,6 +305,10 @@ module SiteSettingExtension end end + if self.respond_to? "validate_#{name}" + send("validate_#{name}", val) + end + provider.save(name, val, type) current[name] = convert(val, type) clear_cache! diff --git a/lib/site_setting_validations.rb b/lib/site_setting_validations.rb new file mode 100644 index 0000000000..6ed44df803 --- /dev/null +++ b/lib/site_setting_validations.rb @@ -0,0 +1,17 @@ + +module SiteSettingValidations + + def validate_error(key) + raise Discourse::InvalidParameters.new(I18n.t("errors.site_settings.#{key}")) + end + + def validate_min_username_length(new_val) + validate_error :min_username_length_range if new_val > SiteSetting.max_username_length + validate_error :min_username_length_exists if User.where('length(username) < ?', new_val).exists? + end + + def validate_max_username_length(new_val) + validate_error :min_username_length_range if new_val < SiteSetting.min_username_length + validate_error :max_username_length_exists if User.where('length(username) > ?', new_val).exists? + end +end From 7f4645820434d5e9b7f7747b10b3b9d3c4d983f0 Mon Sep 17 00:00:00 2001 From: Kane York Date: Thu, 20 Aug 2015 18:34:30 -0700 Subject: [PATCH 183/237] FIX: black-on-black queued posts --- app/assets/stylesheets/desktop/queued-posts.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/queued-posts.scss b/app/assets/stylesheets/desktop/queued-posts.scss index 688e85f3c8..06e24ab1e1 100644 --- a/app/assets/stylesheets/desktop/queued-posts.scss +++ b/app/assets/stylesheets/desktop/queued-posts.scss @@ -31,7 +31,7 @@ } } .post-title { - color: darken(dark-light-diff($primary, $secondary, 90%, -60%), 50%); + color: dark-light-diff($primary, $secondary, 20%, -60%); font-weight: bold; .badge-wrapper { From 07d6bb8d314e33bc97bc9d0d69caeea50c4a5c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 21 Aug 2015 12:19:35 +0200 Subject: [PATCH 184/237] FIX: remove client side maximum filesize check --- app/assets/javascripts/discourse/lib/utilities.js | 8 -------- config/site_settings.yml | 8 ++------ test/javascripts/helpers/site-settings.js | 2 -- test/javascripts/lib/utilities-test.js.es6 | 12 ------------ 4 files changed, 2 insertions(+), 28 deletions(-) diff --git a/app/assets/javascripts/discourse/lib/utilities.js b/app/assets/javascripts/discourse/lib/utilities.js index 92c66cd53c..ac55bdaf49 100644 --- a/app/assets/javascripts/discourse/lib/utilities.js +++ b/app/assets/javascripts/discourse/lib/utilities.js @@ -177,14 +177,6 @@ Discourse.Utilities = { } } - // check file size - var fileSizeKB = file.size / 1024; - var maxSizeKB = 10 * 1024; // 10MB - if (fileSizeKB > maxSizeKB) { - bootbox.alert(I18n.t('post.errors.file_too_large', { max_size_kb: maxSizeKB })); - return false; - } - // everything went fine return true; }, diff --git a/config/site_settings.yml b/config/site_settings.yml index c639543923..3dd9f03054 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -510,12 +510,8 @@ email: short_email_length: 2800 files: - max_image_size_kb: - client: true - default: 3072 - max_attachment_size_kb: - client: true - default: 3072 + max_image_size_kb: 3072 + max_attachment_size_kb: 3072 authorized_extensions: client: true default: 'jpg|jpeg|png|gif' diff --git a/test/javascripts/helpers/site-settings.js b/test/javascripts/helpers/site-settings.js index 68612e7530..6f43509e19 100644 --- a/test/javascripts/helpers/site-settings.js +++ b/test/javascripts/helpers/site-settings.js @@ -63,8 +63,6 @@ Discourse.SiteSettingsOriginal = { "default_code_lang":"lang-auto", "autohighlight_all_code":false, "email_in":false, - "max_image_size_kb":3072, - "max_attachment_size_kb":1024, "authorized_extensions":".jpg|.jpeg|.png|.gif|.svg|.txt|.ico|.yml", "max_image_width":690, "max_image_height":500, diff --git a/test/javascripts/lib/utilities-test.js.es6 b/test/javascripts/lib/utilities-test.js.es6 index 2f94980b31..8d0fd1c1d9 100644 --- a/test/javascripts/lib/utilities-test.js.es6 +++ b/test/javascripts/lib/utilities-test.js.es6 @@ -51,16 +51,6 @@ test("ensures an authorized upload", function() { ok(bootbox.alert.calledWith(I18n.t('post.errors.upload_not_authorized', { authorized_extensions: extensions }))); }); -test("prevents files that are too big from being uploaded", function() { - Discourse.User.resetCurrent(Discourse.User.create()); - var image = { name: "image.png", size: 11 * 1024 * 1024 }; - Discourse.User.currentProp("trust_level", 1); - sandbox.stub(bootbox, "alert"); - - not(validUpload([image])); - ok(bootbox.alert.calledWith(I18n.t('post.errors.file_too_large', { max_size_kb: 10 * 1024 }))); -}); - var imageSize = 10 * 1024; var dummyBlob = function() { @@ -77,8 +67,6 @@ var dummyBlob = function() { test("allows valid uploads to go through", function() { Discourse.User.resetCurrent(Discourse.User.create()); Discourse.User.currentProp("trust_level", 1); - Discourse.SiteSettings.max_image_size_kb = 15; - Discourse.SiteSettings.max_attachment_size_kb = 1; sandbox.stub(bootbox, "alert"); // image From 37f2d8c73cf9b8e09f94f750284e4c293d867581 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 21 Aug 2015 11:28:17 -0400 Subject: [PATCH 185/237] Adds more helpers for plugin authors `add_class_method` can be used to add a class method that only executes when the plugin is enabled. `add_model_callback` can be used to attach a callback to an ActiveRecord model such as `before_save` that will only execute when the plugin is enabled. --- lib/plugin/instance.rb | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index caa406ec24..3c2f35cd98 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -35,6 +35,7 @@ class Plugin::Instance def initialize(metadata=nil, path=nil) @metadata = metadata @path = path + @idx = 0 end def add_admin_route(label, location) @@ -62,6 +63,7 @@ class Plugin::Instance end # Extend a class but check that the plugin is enabled + # for class methods use `add_class_method` def add_to_class(klass, attr, &block) klass = klass.to_s.classify.constantize @@ -74,6 +76,34 @@ class Plugin::Instance end end + # Adds a class method to a class, respecting if plugin is enabled + def add_class_method(klass, attr, &block) + klass = klass.to_s.classify.constantize + + hidden_method_name = :"#{attr}_without_enable_check" + klass.send(:define_singleton_method, hidden_method_name, &block) + + plugin = self + klass.send(:define_singleton_method, attr) do |*args| + send(hidden_method_name, *args) if plugin.enabled? + end + end + + def add_model_callback(klass, callback, &block) + klass = klass.to_s.classify.constantize + plugin = self + + # generate a unique method name + method_name = "#{plugin.name}_#{klass.name}_#{callback}#{@idx}".underscore + hidden_method_name = :"#{method_name}_without_enable_check" + klass.send(:define_method, hidden_method_name, &block) + + klass.send(callback) do |*args| + send(hidden_method_name, *args) if plugin.enabled? + end + + end + # Add validation method but check that the plugin is enabled def validate(klass, name, &block) klass = klass.to_s.classify.constantize From 2b72bd35923396ae5054209fe597440d2474ecc5 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 21 Aug 2015 11:39:40 -0400 Subject: [PATCH 186/237] FIX: Missed incrementing `idx` --- lib/plugin/instance.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/plugin/instance.rb b/lib/plugin/instance.rb index 3c2f35cd98..1339a47d6b 100644 --- a/lib/plugin/instance.rb +++ b/lib/plugin/instance.rb @@ -95,6 +95,7 @@ class Plugin::Instance # generate a unique method name method_name = "#{plugin.name}_#{klass.name}_#{callback}#{@idx}".underscore + @idx += 1 hidden_method_name = :"#{method_name}_without_enable_check" klass.send(:define_method, hidden_method_name, &block) From 7ffdc43091faf388a5928e8ebf7ce01546b2276f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 21 Aug 2015 12:43:10 -0400 Subject: [PATCH 187/237] Include the error messages so we can debug this easier --- app/models/queued_post.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/queued_post.rb b/app/models/queued_post.rb index 6bbff2a053..e0199bd137 100644 --- a/app/models/queued_post.rb +++ b/app/models/queued_post.rb @@ -72,7 +72,7 @@ class QueuedPost < ActiveRecord::Base created_post = creator.create unless created_post && creator.errors.blank? - raise StandardError, "Failed to create post #{raw[0..100]} #{creator.errors}" + raise StandardError, "Failed to create post #{raw[0..100]} #{creator.errors.full_messages.inspect}" end end From 4f8542008832da7f9138020c4c92e31963557b42 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 21 Aug 2015 12:47:16 -0400 Subject: [PATCH 188/237] FIX: Fields should be ordered by position on preferences page --- app/assets/javascripts/discourse/controllers/preferences.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index 3a0bd90fe6..dd729eab29 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -30,7 +30,7 @@ export default Ember.Controller.extend(CanCheckEmails, { if (!this.get('currentUser.staff')) { siteUserFields = siteUserFields.filterProperty('editable', true); } - return siteUserFields.sortBy('field_type').map(function(field) { + return siteUserFields.sortBy('position').map(function(field) { const value = userFields ? userFields[field.get('id').toString()] : null; return Ember.Object.create({ value, field }); }); From 73264648f2a8eac0bd5f7b80cceb0a6f591d87fe Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 21 Aug 2015 13:13:15 -0400 Subject: [PATCH 189/237] FIX: emoji upload button always appeared disabled --- .../discourse/templates/components/emoji-uploader.hbs | 6 +++--- app/assets/stylesheets/common/components/buttons.css.scss | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs b/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs index 8d9b297cf9..a58498a91b 100644 --- a/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs +++ b/app/assets/javascripts/discourse/templates/components/emoji-uploader.hbs @@ -1,6 +1,6 @@ {{text-field name="name" placeholderKey="admin.emoji.name" value=name}} - \ No newline at end of file diff --git a/app/assets/stylesheets/common/components/buttons.css.scss b/app/assets/stylesheets/common/components/buttons.css.scss index af8f0df24a..e195a034f4 100644 --- a/app/assets/stylesheets/common/components/buttons.css.scss +++ b/app/assets/stylesheets/common/components/buttons.css.scss @@ -55,7 +55,7 @@ background: dark-light-diff($primary, $secondary, 65%, -75%); color: #fff; } - &[disabled] { + &[disabled], &.disabled { background: dark-light-diff($primary, $secondary, 90%, -60%); &:hover { color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); } cursor: not-allowed; @@ -88,7 +88,7 @@ @include linear-gradient(scale-color($tertiary, $lightness: -20%), scale-color($tertiary, $lightness: -10%)); color: #fff; } - &[disabled] { + &[disabled], &.disabled { background: $tertiary; } } @@ -109,7 +109,7 @@ &:active { @include linear-gradient(scale-color($danger, $lightness: -20%), $danger); } - &[disabled] { + &[disabled], &.disabled { background: $danger; } } From 6819c2d47c2acbd3027e4de92e891af25d3dc67c Mon Sep 17 00:00:00 2001 From: Kane York Date: Fri, 21 Aug 2015 11:14:50 -0700 Subject: [PATCH 190/237] FIX: Make small-actions stick out less dark theme --- app/assets/stylesheets/common/base/topic-post.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index ab1437c068..1ff8dc3d4b 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -276,7 +276,7 @@ table.md-table { font-size: 35px; width: 45px; text-align: center; - color: dark-light-diff($primary, $secondary, 75%, 20%); + color: dark-light-diff($primary, $secondary, 75%, -20%); } } @@ -286,7 +286,7 @@ table.md-table { text-transform: uppercase; font-weight: bold; font-size: 0.9em; - color: dark-light-diff($primary, $secondary, 50%, 0%); + color: dark-light-diff($primary, $secondary, 50%, -30%); .custom-message { text-transform: none; From aa0b2d74c124d4b1c373c70e5fccf32e88f18485 Mon Sep 17 00:00:00 2001 From: Kane York Date: Fri, 21 Aug 2015 11:15:04 -0700 Subject: [PATCH 191/237] Remove borders from composer resizing grip --- app/assets/stylesheets/desktop/discourse.scss | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/assets/stylesheets/desktop/discourse.scss b/app/assets/stylesheets/desktop/discourse.scss index e25653136a..5e250612c1 100644 --- a/app/assets/stylesheets/desktop/discourse.scss +++ b/app/assets/stylesheets/desktop/discourse.scss @@ -77,13 +77,11 @@ body { .grippie { width: 100%; - border: 1px solid; - border-right-width: 0; - border-left-width: 0; cursor: row-resize; height: 11px; overflow: hidden; display:block; + border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); background: image-url("grippie.png") dark-light-diff($primary, $secondary, 90%, -60%) no-repeat center 3px; } } From e2e7e6df442c2649ad2340a2fbccc36f6a3f2904 Mon Sep 17 00:00:00 2001 From: Kane York Date: Fri, 21 Aug 2015 11:19:02 -0700 Subject: [PATCH 192/237] FIX: Unread post circle colors in dark theme --- app/assets/stylesheets/common/components/badges.css.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/assets/stylesheets/common/components/badges.css.scss b/app/assets/stylesheets/common/components/badges.css.scss index d83ea438d9..67c361ff20 100644 --- a/app/assets/stylesheets/common/components/badges.css.scss +++ b/app/assets/stylesheets/common/components/badges.css.scss @@ -236,16 +236,16 @@ font-size: 11px; line-height: 1; text-align: center; - background-color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 30%)); + background-color: dark-light-choose(scale-color($primary, $lightness: 70%), scale-color($secondary, $lightness: 70%)); &[href] { - color: $secondary; + color: $secondary; } // New posts &.new-posts, &.unread-posts { - background-color: dark-light-choose(scale-color($tertiary, $lightness: 50%), scale-color($tertiary, $lightness: 20%)); - color: $secondary; + background-color: dark-light-choose(scale-color($tertiary, $lightness: 50%), $tertiary); + color: dark-light-choose($secondary, $secondary); font-weight: dark-light-choose(normal, bold); } From 9185cec1f359d906aadf625aeaca227507e7965f Mon Sep 17 00:00:00 2001 From: Kane York Date: Fri, 21 Aug 2015 11:23:06 -0700 Subject: [PATCH 193/237] FIX: Insert link dialog in dark theme --- app/assets/stylesheets/common/base/pagedown.scss | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/assets/stylesheets/common/base/pagedown.scss b/app/assets/stylesheets/common/base/pagedown.scss index 8cac61a14f..d7d411f631 100644 --- a/app/assets/stylesheets/common/base/pagedown.scss +++ b/app/assets/stylesheets/common/base/pagedown.scss @@ -133,6 +133,8 @@ .wmd-prompt-dialog > form > input[type="button"] { border: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); + background: dark-light-choose(initial, blend-primary-secondary(50%)); + color: dark-light-choose(inherit, $secondary); font-family: trebuchet MS, helvetica, sans-serif; font-size: 0.8em; font-weight: bold; From 36b5269d1947b621ac4b0f05c47969714a674b33 Mon Sep 17 00:00:00 2001 From: Kane York Date: Fri, 21 Aug 2015 11:35:19 -0700 Subject: [PATCH 194/237] FIX: Emoji modal in dark theme --- app/assets/stylesheets/common/base/emoji.scss | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/app/assets/stylesheets/common/base/emoji.scss b/app/assets/stylesheets/common/base/emoji.scss index 2d9061bffc..69e111f445 100644 --- a/app/assets/stylesheets/common/base/emoji.scss +++ b/app/assets/stylesheets/common/base/emoji.scss @@ -15,12 +15,12 @@ body img.emoji { margin-top: -100px; left: 50%; top: 50%; - background-color: #dadada; + background-color: dark-light-choose(#dadada, blend-primary-secondary(5%)); } .emoji-page td { border: 1px solid transparent; - background-color: white; + background-color: dark-light-choose(white, $secondary); } .emoji-page a { @@ -30,13 +30,13 @@ body img.emoji { } .emoji-page a:hover { - background-color: rgb(210, 236, 252); + background-color: dark-light-choose(rgb(210, 236, 252), rgb(45, 19, 3)); } .emoji-table-wrapper { min-width: 442px; min-height: 185px; - background-color: white; + background-color: $secondary; } .emoji-modal-wrapper { @@ -46,7 +46,7 @@ body img.emoji { top: 0; width: 100%; height: 100%; - opacity: 0.8; + opacity: dark-light-choose(0.8, 0.5); background-color: black; } @@ -61,11 +61,11 @@ body img.emoji { .emoji-modal .toolbar li a { padding: 8px; - background-color: #dadada; + background-color: dark-light-choose(#dadada, blend-primary-secondary(5%)); } .emoji-modal .toolbar li a.selected { - background-color: #fff; + background-color: $secondary; } .emoji-modal .info { @@ -78,6 +78,7 @@ body img.emoji { .emoji-modal .info span { margin-left: 5px; font-weight: bold; + color: $primary; } .emoji-modal .info { @@ -89,10 +90,10 @@ body img.emoji { } .emoji-modal .nav span { - color: #aaa; + color: dark-light-choose(#aaa, #555); margin-right: 10px; } .emoji-modal .nav a { - color: #333; + color: dark-light-choose(#333, #ccc); } From a275b0b8a3b08eed08b573155a1fcbca93353248 Mon Sep 17 00:00:00 2001 From: Kane York Date: Fri, 21 Aug 2015 11:37:19 -0700 Subject: [PATCH 195/237] FIX: Edit reasons on profile page --- app/assets/stylesheets/desktop/user.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/desktop/user.scss b/app/assets/stylesheets/desktop/user.scss index 4823de49ab..c8a39b3799 100644 --- a/app/assets/stylesheets/desktop/user.scss +++ b/app/assets/stylesheets/desktop/user.scss @@ -484,7 +484,7 @@ text-overflow: ellipsis; } .edit-reason { - background-color: scale-color($highlight, $lightness: 25%); + background-color: dark-light-choose(scale-color($highlight, $lightness: 25%), scale-color($highlight, $lightness: -50%)); padding: 3px 5px 5px 5px; } .remove-bookmark { From bef80633b1b58189a732a3959d3a59da44aa55e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 21 Aug 2015 20:39:21 +0200 Subject: [PATCH 196/237] FEATURE: global admin override of most of the user preferences --- .../admin/components/site-setting.js.es6 | 56 +++++------ .../components/site-settings/bool.js.es6 | 17 ++++ .../site-settings/category-list.js.es6 | 16 ++++ .../templates/components/site-setting.hbs | 2 +- .../{ => components}/site-settings/bool.hbs | 0 .../site-settings/category-list.hbs | 3 + .../{ => components}/site-settings/enum.hbs | 2 +- .../site-settings/host-list.hbs} | 2 +- .../{ => components}/site-settings/list.hbs | 2 +- .../{ => components}/site-settings/string.hbs | 2 +- .../site-settings/url-list.hbs} | 2 +- .../components/category-group.js.es6 | 4 +- .../discourse/controllers/preferences.js.es6 | 22 ++--- .../stylesheets/common/admin/admin_base.scss | 3 + .../auto_track_duration_site_setting.rb | 27 ++++++ app/models/new_topic_duration_site_setting.rb | 24 +++++ app/models/site_setting.rb | 8 ++ app/models/topic_tracking_state.rb | 2 +- app/models/topic_user.rb | 6 +- app/models/user.rb | 82 ++++++++++++---- app/serializers/user_serializer.rb | 4 +- config/locales/client.en.yml | 24 ++--- config/locales/server.bs_BA.yml | 4 +- config/locales/server.cs.yml | 4 +- config/locales/server.da.yml | 4 +- config/locales/server.de.yml | 4 +- config/locales/server.en.yml | 24 ++++- config/locales/server.es.yml | 4 +- config/locales/server.fa_IR.yml | 4 +- config/locales/server.fi.yml | 4 +- config/locales/server.fr.yml | 4 +- config/locales/server.he.yml | 4 +- config/locales/server.it.yml | 4 +- config/locales/server.ja.yml | 4 +- config/locales/server.ko.yml | 4 +- config/locales/server.nl.yml | 4 +- config/locales/server.pl_PL.yml | 4 +- config/locales/server.pt.yml | 4 +- config/locales/server.pt_BR.yml | 4 +- config/locales/server.ro.yml | 2 +- config/locales/server.ru.yml | 4 +- config/locales/server.sq.yml | 4 +- config/locales/server.sv.yml | 2 +- config/locales/server.tr_TR.yml | 4 +- config/locales/server.uk.yml | 4 +- config/locales/server.zh_CN.yml | 4 +- config/locales/server.zh_TW.yml | 4 +- config/site_settings.yml | 37 ++++++-- lib/site_setting_extension.rb | 2 +- lib/site_setting_validations.rb | 9 ++ .../site_settings/yaml_loader_spec.rb | 2 +- spec/fixtures/site_settings/enum.yml | 2 +- spec/jobs/enqueue_digest_emails_spec.rb | 4 +- .../notify_mailing_list_subscribers_spec.rb | 95 ++++++++++--------- spec/models/topic_user_spec.rb | 12 ++- spec/models/user_spec.rb | 64 +++++++++---- 56 files changed, 438 insertions(+), 215 deletions(-) create mode 100644 app/assets/javascripts/admin/components/site-settings/bool.js.es6 create mode 100644 app/assets/javascripts/admin/components/site-settings/category-list.js.es6 rename app/assets/javascripts/admin/templates/{ => components}/site-settings/bool.hbs (100%) create mode 100644 app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs rename app/assets/javascripts/admin/templates/{ => components}/site-settings/enum.hbs (80%) rename app/assets/javascripts/admin/templates/{site-settings/url_list.hbs => components/site-settings/host-list.hbs} (60%) rename app/assets/javascripts/admin/templates/{ => components}/site-settings/list.hbs (54%) rename app/assets/javascripts/admin/templates/{ => components}/site-settings/string.hbs (62%) rename app/assets/javascripts/admin/templates/{site-settings/host_list.hbs => components/site-settings/url-list.hbs} (60%) create mode 100644 app/models/auto_track_duration_site_setting.rb create mode 100644 app/models/new_topic_duration_site_setting.rb diff --git a/app/assets/javascripts/admin/components/site-setting.js.es6 b/app/assets/javascripts/admin/components/site-setting.js.es6 index f89996c70a..93e65743c7 100644 --- a/app/assets/javascripts/admin/components/site-setting.js.es6 +++ b/app/assets/javascripts/admin/components/site-setting.js.es6 @@ -4,7 +4,7 @@ import SiteSetting from 'admin/models/site-setting'; import { propertyNotEqual } from 'discourse/lib/computed'; import computed from 'ember-addons/ember-computed-decorators'; -const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list']; +const CustomTypes = ['bool', 'enum', 'list', 'url_list', 'host_list', 'category_list']; export default Ember.Component.extend(BufferedContent, ScrollTop, { classNameBindings: [':row', ':setting', 'setting.overridden', 'typeClass'], @@ -12,44 +12,32 @@ export default Ember.Component.extend(BufferedContent, ScrollTop, { dirty: propertyNotEqual('buffered.value', 'setting.value'), validationMessage: null, - preview: function() { - const preview = this.get('setting.preview'); + @computed("setting.preview", "buffered.value") + preview(preview, value) { if (preview) { - return new Handlebars.SafeString("
    " + - preview.replace(/\{\{value\}\}/g, this.get('buffered.value')) + - "
    "); - } - }.property('buffered.value'), - - @computed('partialType') - typeClass() { - return this.get('partialType').replace("_", "-"); - }, - - @computed('buffered.value') - enabled: { - get(bufferedValue) { - if (Ember.isEmpty(bufferedValue)) { return false; } - return bufferedValue === 'true'; - }, - set(value) { - this.set('buffered.value', value ? 'true' : 'false'); - return value; + return new Handlebars.SafeString("
    " + preview.replace(/\{\{value\}\}/g, value) + "
    "); } }, - settingName: function() { - return this.get('setting.setting').replace(/\_/g, ' '); - }.property('setting.setting'), + @computed('componentType') + typeClass(componentType) { + return componentType.replace("_", "-"); + }, - partialType: function() { - let type = this.get('setting.type'); + @computed("setting.setting") + settingName(setting) { + return setting.replace(/\_/g, ' '); + }, + + @computed("setting.type") + componentType(type) { return CustomTypes.indexOf(type) !== -1 ? type : 'string'; - }.property('setting.type'), + }, - partialName: function() { - return 'admin/templates/site-settings/' + this.get('partialType'); - }.property('partialType'), + @computed("typeClass") + componentName(typeClass) { + return "site-settings/" + typeClass; + }, _watchEnterKey: function() { const self = this; @@ -65,8 +53,8 @@ export default Ember.Component.extend(BufferedContent, ScrollTop, { }.on("willDestroyElement"), _save() { - const setting = this.get('buffered'); - const self = this; + const self = this, + setting = this.get('buffered'); SiteSetting.update(setting.get('setting'), setting.get('value')).then(function() { self.set('validationMessage', null); self.commitBuffer(); diff --git a/app/assets/javascripts/admin/components/site-settings/bool.js.es6 b/app/assets/javascripts/admin/components/site-settings/bool.js.es6 new file mode 100644 index 0000000000..40fcfb354b --- /dev/null +++ b/app/assets/javascripts/admin/components/site-settings/bool.js.es6 @@ -0,0 +1,17 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + + @computed("value") + enabled: { + get(value) { + if (Ember.isEmpty(value)) { return false; } + return value === "true"; + }, + set(value) { + this.set("value", value ? "true" : "false"); + return value; + } + }, + +}); diff --git a/app/assets/javascripts/admin/components/site-settings/category-list.js.es6 b/app/assets/javascripts/admin/components/site-settings/category-list.js.es6 new file mode 100644 index 0000000000..487239b78f --- /dev/null +++ b/app/assets/javascripts/admin/components/site-settings/category-list.js.es6 @@ -0,0 +1,16 @@ +import computed from "ember-addons/ember-computed-decorators"; + +export default Ember.Component.extend({ + + @computed("value") + selectedCategories: { + get(value) { + return Discourse.Category.findByIds(value.split("|")); + }, + set(value) { + this.set("value", value.mapBy("id").join("|")); + return value; + } + } + +}); diff --git a/app/assets/javascripts/admin/templates/components/site-setting.hbs b/app/assets/javascripts/admin/templates/components/site-setting.hbs index 15821600ee..be0d4d8a08 100644 --- a/app/assets/javascripts/admin/templates/components/site-setting.hbs +++ b/app/assets/javascripts/admin/templates/components/site-setting.hbs @@ -2,7 +2,7 @@

    {{unbound settingName}}

    - {{partial partialName}} +{{component componentName setting=setting value=buffered.value validationMessage=validationMessage}}
    {{#if dirty}}
    diff --git a/app/assets/javascripts/admin/templates/site-settings/bool.hbs b/app/assets/javascripts/admin/templates/components/site-settings/bool.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/site-settings/bool.hbs rename to app/assets/javascripts/admin/templates/components/site-settings/bool.hbs diff --git a/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs new file mode 100644 index 0000000000..621f3fa70e --- /dev/null +++ b/app/assets/javascripts/admin/templates/components/site-settings/category-list.hbs @@ -0,0 +1,3 @@ +{{category-group categories=selectedCategories blacklist=selectedCategories}} +
    {{{unbound setting.description}}}
    +{{setting-validation-message message=validationMessage}} diff --git a/app/assets/javascripts/admin/templates/site-settings/enum.hbs b/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs similarity index 80% rename from app/assets/javascripts/admin/templates/site-settings/enum.hbs rename to app/assets/javascripts/admin/templates/components/site-settings/enum.hbs index 67cbfc92ac..765a0e20d1 100644 --- a/app/assets/javascripts/admin/templates/site-settings/enum.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/enum.hbs @@ -1,4 +1,4 @@ -{{combo-box valueAttribute="value" content=setting.validValues value=buffered.value none=setting.allowsNone}} +{{combo-box valueAttribute="value" content=setting.validValues value=value none=setting.allowsNone}} {{preview}} {{setting-validation-message message=validationMessage}}
    {{{unbound setting.description}}}
    diff --git a/app/assets/javascripts/admin/templates/site-settings/url_list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/host-list.hbs similarity index 60% rename from app/assets/javascripts/admin/templates/site-settings/url_list.hbs rename to app/assets/javascripts/admin/templates/components/site-settings/host-list.hbs index ec8ecaf395..5107f5b4af 100644 --- a/app/assets/javascripts/admin/templates/site-settings/url_list.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/host-list.hbs @@ -1,3 +1,3 @@ -{{value-list values=buffered.value addKey="admin.site_settings.add_url"}} +{{value-list values=value addKey="admin.site_settings.add_host"}} {{setting-validation-message message=validationMessage}}
    {{{unbound setting.description}}}
    diff --git a/app/assets/javascripts/admin/templates/site-settings/list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/list.hbs similarity index 54% rename from app/assets/javascripts/admin/templates/site-settings/list.hbs rename to app/assets/javascripts/admin/templates/components/site-settings/list.hbs index bc1cf2b51e..e741bea5ed 100644 --- a/app/assets/javascripts/admin/templates/site-settings/list.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/list.hbs @@ -1,3 +1,3 @@ -{{list-setting settingValue=buffered.value choices=setting.choices settingName=setting.setting}} +{{list-setting settingValue=value choices=setting.choices settingName=setting.setting}} {{setting-validation-message message=validationMessage}}
    {{{unbound setting.description}}}
    diff --git a/app/assets/javascripts/admin/templates/site-settings/string.hbs b/app/assets/javascripts/admin/templates/components/site-settings/string.hbs similarity index 62% rename from app/assets/javascripts/admin/templates/site-settings/string.hbs rename to app/assets/javascripts/admin/templates/components/site-settings/string.hbs index f8427094ab..71d7216f27 100644 --- a/app/assets/javascripts/admin/templates/site-settings/string.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/string.hbs @@ -1,3 +1,3 @@ -{{text-field value=buffered.value classNames="input-setting-string"}} +{{text-field value=value classNames="input-setting-string"}} {{setting-validation-message message=validationMessage}}
    {{{unbound setting.description}}}
    diff --git a/app/assets/javascripts/admin/templates/site-settings/host_list.hbs b/app/assets/javascripts/admin/templates/components/site-settings/url-list.hbs similarity index 60% rename from app/assets/javascripts/admin/templates/site-settings/host_list.hbs rename to app/assets/javascripts/admin/templates/components/site-settings/url-list.hbs index 5f0c301d0d..41213777e3 100644 --- a/app/assets/javascripts/admin/templates/site-settings/host_list.hbs +++ b/app/assets/javascripts/admin/templates/components/site-settings/url-list.hbs @@ -1,3 +1,3 @@ -{{value-list values=buffered.value addKey="admin.site_settings.add_host"}} +{{value-list values=value addKey="admin.site_settings.add_url"}} {{setting-validation-message message=validationMessage}}
    {{{unbound setting.description}}}
    diff --git a/app/assets/javascripts/discourse/components/category-group.js.es6 b/app/assets/javascripts/discourse/components/category-group.js.es6 index 13906ed312..e81fa5d1d3 100644 --- a/app/assets/javascripts/discourse/components/category-group.js.es6 +++ b/app/assets/javascripts/discourse/components/category-group.js.es6 @@ -24,13 +24,13 @@ export default Ember.Component.extend({ const slug = link.match(regexp)[1]; return Discourse.Category.findSingleBySlug(slug); }); - self.set("categories", categories); + Em.run.next(() => self.set("categories", categories)); }, template, transformComplete(category) { return categoryBadgeHTML(category, {allowUncategorized: true}); } }); - }.on('didInsertElement') + }.on('didInsertElement'), }); diff --git a/app/assets/javascripts/discourse/controllers/preferences.js.es6 b/app/assets/javascripts/discourse/controllers/preferences.js.es6 index dd729eab29..ee3e86e9d8 100644 --- a/app/assets/javascripts/discourse/controllers/preferences.js.es6 +++ b/app/assets/javascripts/discourse/controllers/preferences.js.es6 @@ -71,19 +71,19 @@ export default Ember.Controller.extend(CanCheckEmails, { autoTrackDurations: [{ name: I18n.t('user.auto_track_options.never'), value: -1 }, { name: I18n.t('user.auto_track_options.immediately'), value: 0 }, - { name: I18n.t('user.auto_track_options.after_n_seconds', { count: 30 }), value: 30000 }, - { name: I18n.t('user.auto_track_options.after_n_minutes', { count: 1 }), value: 60000 }, - { name: I18n.t('user.auto_track_options.after_n_minutes', { count: 2 }), value: 120000 }, - { name: I18n.t('user.auto_track_options.after_n_minutes', { count: 3 }), value: 180000 }, - { name: I18n.t('user.auto_track_options.after_n_minutes', { count: 4 }), value: 240000 }, - { name: I18n.t('user.auto_track_options.after_n_minutes', { count: 5 }), value: 300000 }, - { name: I18n.t('user.auto_track_options.after_n_minutes', { count: 10 }), value: 600000 }], + { name: I18n.t('user.auto_track_options.after_30_seconds'), value: 30000 }, + { name: I18n.t('user.auto_track_options.after_1_minute'), value: 60000 }, + { name: I18n.t('user.auto_track_options.after_2_minutes'), value: 120000 }, + { name: I18n.t('user.auto_track_options.after_3_minutes'), value: 180000 }, + { name: I18n.t('user.auto_track_options.after_4_minutes'), value: 240000 }, + { name: I18n.t('user.auto_track_options.after_5_minutes'), value: 300000 }, + { name: I18n.t('user.auto_track_options.after_10_minutes'), value: 600000 }], considerNewTopicOptions: [{ name: I18n.t('user.new_topic_duration.not_viewed'), value: -1 }, - { name: I18n.t('user.new_topic_duration.after_n_days', { count: 1 }), value: 60 * 24 }, - { name: I18n.t('user.new_topic_duration.after_n_days', { count: 2 }), value: 60 * 48 }, - { name: I18n.t('user.new_topic_duration.after_n_weeks', { count: 1 }), value: 7 * 60 * 24 }, - { name: I18n.t('user.new_topic_duration.after_n_weeks', { count: 2 }), value: 2 * 7 * 60 * 24 }, + { name: I18n.t('user.new_topic_duration.after_1_day'), value: 60 * 24 }, + { name: I18n.t('user.new_topic_duration.after_2_days'), value: 60 * 48 }, + { name: I18n.t('user.new_topic_duration.after_1_week'), value: 7 * 60 * 24 }, + { name: I18n.t('user.new_topic_duration.after_2_weeks'), value: 2 * 7 * 60 * 24 }, { name: I18n.t('user.new_topic_duration.last_here'), value: -2 }], saveButtonText: function() { diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index 15875c0d2c..d7686805af 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -312,6 +312,9 @@ td.flaggers td { .setting-value { float: left; width: 53%; + .category-group { + width: 95%; + } @media (max-width: $mobile-breakpoint) { width: 100%; } diff --git a/app/models/auto_track_duration_site_setting.rb b/app/models/auto_track_duration_site_setting.rb new file mode 100644 index 0000000000..6b10f80a5f --- /dev/null +++ b/app/models/auto_track_duration_site_setting.rb @@ -0,0 +1,27 @@ +require_dependency 'enum_site_setting' + +class AutoTrackDurationSiteSetting < EnumSiteSetting + + def self.valid_value?(val) + values.any? { |v| v[:value].to_s == val.to_s } + end + + def self.values + @values ||= [ + { name: 'user.auto_track_options.never', value: -1 }, + { name: 'user.auto_track_options.immediately', value: 0 }, + { name: 'user.auto_track_options.after_30_seconds', value: 1000 * 30 }, + { name: 'user.auto_track_options.after_1_minute', value: 1000 * 60 }, + { name: 'user.auto_track_options.after_2_minutes', value: 1000 * 60 * 2 }, + { name: 'user.auto_track_options.after_3_minutes', value: 1000 * 60 * 3 }, + { name: 'user.auto_track_options.after_4_minutes', value: 1000 * 60 * 4 }, + { name: 'user.auto_track_options.after_5_minutes', value: 1000 * 60 * 5 }, + { name: 'user.auto_track_options.after_10_minutes', value: 1000 * 60 * 10 }, + ] + end + + def self.translate_names? + true + end + +end diff --git a/app/models/new_topic_duration_site_setting.rb b/app/models/new_topic_duration_site_setting.rb new file mode 100644 index 0000000000..92a00ea1c6 --- /dev/null +++ b/app/models/new_topic_duration_site_setting.rb @@ -0,0 +1,24 @@ +require_dependency 'enum_site_setting' + +class NewTopicDurationSiteSetting < EnumSiteSetting + + def self.valid_value?(val) + values.any? { |v| v[:value].to_s == val.to_s } + end + + def self.values + @values ||= [ + { name: 'user.new_topic_duration.not_viewed', value: -1 }, + { name: 'user.new_topic_duration.after_1_day', value: 60 * 24 }, + { name: 'user.new_topic_duration.after_2_days', value: 60 * 24 * 2 }, + { name: 'user.new_topic_duration.after_1_week', value: 60 * 24 * 7 }, + { name: 'user.new_topic_duration.after_2_weeks', value: 60 * 24 * 7 * 2 }, + { name: 'user.new_topic_duration.last_here', value: -2 }, + ] + end + + def self.translate_names? + true + end + +end diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index ff1d17c005..8dbcb329ce 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -99,6 +99,14 @@ class SiteSetting < ActiveRecord::Base false end + def self.default_categories_selected + [ + SiteSetting.default_categories_watching.split("|"), + SiteSetting.default_categories_tracking.split("|"), + SiteSetting.default_categories_muted.split("|"), + ].flatten.to_set + end + end # == Schema Information diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index e5272922f9..12370fa234 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -110,7 +110,7 @@ class TopicTrackingState now: DateTime.now, last_visit: User::NewTopicDuration::LAST_VISIT, always: User::NewTopicDuration::ALWAYS, - default_duration: SiteSetting.new_topic_duration_minutes + default_duration: SiteSetting.default_other_new_topic_duration_minutes ).where_values[0] end diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb index b900c9b0a6..01d57c5808 100644 --- a/app/models/topic_user.rb +++ b/app/models/topic_user.rb @@ -97,7 +97,7 @@ class TopicUser < ActiveRecord::Base if rows == 0 now = DateTime.now auto_track_after = User.select(:auto_track_topics_after_msecs).find_by(id: user_id).auto_track_topics_after_msecs - auto_track_after ||= SiteSetting.auto_track_topics_after + auto_track_after ||= SiteSetting.default_other_auto_track_topics_after_msecs if auto_track_after >= 0 && auto_track_after <= (attrs[:total_msecs_viewed] || 0) attrs[:notification_level] ||= notification_levels[:tracking] @@ -143,7 +143,7 @@ class TopicUser < ActiveRecord::Base now: DateTime.now, msecs: msecs, tracking: notification_levels[:tracking], - threshold: SiteSetting.auto_track_topics_after + threshold: SiteSetting.default_other_auto_track_topics_after_msecs } # In case anyone seens "highest_seen_post_number" and gets confused, like I do. @@ -198,7 +198,7 @@ class TopicUser < ActiveRecord::Base if rows.length == 0 # The user read at least one post in a topic that they haven't viewed before. args[:new_status] = notification_levels[:regular] - if (user.auto_track_topics_after_msecs || SiteSetting.auto_track_topics_after) == 0 + if (user.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs) == 0 args[:new_status] = notification_levels[:tracking] end TopicTrackingState.publish_read(topic_id, post_number, user.id, args[:new_status]) diff --git a/app/models/user.rb b/app/models/user.rb index 6b25f4f556..650d2b9ee1 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -75,14 +75,15 @@ class User < ActiveRecord::Base validates :ip_address, allowed_ip_address: {on: :create, message: :signup_not_allowed} after_initialize :add_trust_level - after_initialize :set_default_email_digest - after_initialize :set_default_external_links_in_new_tab + + before_create :set_default_user_preferences after_create :create_email_token after_create :create_user_stat after_create :create_user_profile after_create :ensure_in_trust_level_group after_create :automatic_group_membership + after_create :set_default_categories_preferences before_save :update_username_lower before_save :ensure_password_is_hashed @@ -578,7 +579,7 @@ class User < ActiveRecord::Base end def treat_as_new_topic_start_date - duration = new_topic_duration_minutes || SiteSetting.new_topic_duration_minutes + duration = new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes [case duration when User::NewTopicDuration::ALWAYS created_at @@ -901,26 +902,42 @@ class User < ActiveRecord::Base end end - def set_default_email_digest - if has_attribute?(:email_digests) && self.email_digests.nil? - if SiteSetting.default_digest_email_frequency.blank? - self.email_digests = false - else - self.email_digests = true - self.digest_after_days ||= SiteSetting.default_digest_email_frequency.to_i if has_attribute?(:digest_after_days) - end - end + def set_default_user_preferences + set_default_email_digest_frequency + set_default_email_private_messages + set_default_email_direct + set_default_email_mailing_list_mode + set_default_email_always + + set_default_other_new_topic_duration_minutes + set_default_other_auto_track_topics_after_msecs + set_default_other_external_links_in_new_tab + set_default_other_enable_quoting + set_default_other_dynamic_favicon + set_default_other_disable_jump_reply + set_default_other_edit_history_public + + # needed, otherwise the callback chain is broken... + true end - def set_default_external_links_in_new_tab - if has_attribute?(:external_links_in_new_tab) && self.external_links_in_new_tab.nil? - self.external_links_in_new_tab = !SiteSetting.default_external_links_in_new_tab.blank? + def set_default_categories_preferences + values = [] + + %w{watching tracking muted}.each do |s| + category_ids = SiteSetting.send("default_categories_#{s}").split("|") + category_ids.each do |category_id| + values << "(#{self.id}, #{category_id}, #{CategoryUser.notification_levels[s.to_sym]})" + end + end + + if values.present? + exec_sql("INSERT INTO category_users (user_id, category_id, notification_level) VALUES #{values.join(",")}") end end # Delete unactivated accounts (without verified email) that are over a week old def self.purge_unactivated - to_destroy = User.where(active: false) .joins('INNER JOIN user_stats AS us ON us.user_id = users.id') .where("created_at < ?", SiteSetting.purge_unactivated_users_grace_period_days.days.ago) @@ -950,6 +967,39 @@ class User < ActiveRecord::Base end end + def set_default_email_digest_frequency + if has_attribute?(:email_digests) + if SiteSetting.default_email_digest_frequency.blank? + self.email_digests = false + else + self.email_digests = true + self.digest_after_days ||= SiteSetting.default_email_digest_frequency.to_i if has_attribute?(:digest_after_days) + end + end + end + + def set_default_email_mailing_list_mode + self.mailing_list_mode = SiteSetting.default_email_mailing_list_mode if has_attribute?(:mailing_list_mode) + end + + %w{private_messages direct always}.each do |s| + define_method("set_default_email_#{s}") do + self.send("email_#{s}=", SiteSetting.send("default_email_#{s}")) if has_attribute?("email_#{s}") + end + end + + %w{new_topic_duration_minutes auto_track_topics_after_msecs}.each do |s| + define_method("set_default_other_#{s}") do + self.send("#{s}=", SiteSetting.send("default_other_#{s}").to_i) if has_attribute?(s) + end + end + + %w{external_links_in_new_tab enable_quoting dynamic_favicon disable_jump_reply edit_history_public}.each do |s| + define_method("set_default_other_#{s}") do + self.send("#{s}=", SiteSetting.send("default_other_#{s}")) if has_attribute?(s) + end + end + end # == Schema Information diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb index dd5132504a..a17dd36d2e 100644 --- a/app/serializers/user_serializer.rb +++ b/app/serializers/user_serializer.rb @@ -249,11 +249,11 @@ class UserSerializer < BasicUserSerializer ### def auto_track_topics_after_msecs - object.auto_track_topics_after_msecs || SiteSetting.auto_track_topics_after + object.auto_track_topics_after_msecs || SiteSetting.default_other_auto_track_topics_after_msecs end def new_topic_duration_minutes - object.new_topic_duration_minutes || SiteSetting.new_topic_duration_minutes + object.new_topic_duration_minutes || SiteSetting.default_other_new_topic_duration_minutes end def muted_category_ids diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d85e2a46d0..c00d476aff 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -583,23 +583,22 @@ en: label: "Consider topics new when" not_viewed: "I haven't viewed them yet" last_here: "created since I was here last" - after_n_days: - one: "created in the last day" - other: "created in the last {{count}} days" - after_n_weeks: - one: "created in the last week" - other: "created in the last {{count}} weeks" + after_1_day: "created in the last day" + after_2_days: "created in the last 2 days" + after_1_week: "created in the last week" + after_2_weeks: "created in the last 2 weeks" auto_track_topics: "Automatically track topics I enter" auto_track_options: never: "never" immediately: "immediately" - after_n_seconds: - one: "after 1 second" - other: "after {{count}} seconds" - after_n_minutes: - one: "after 1 minute" - other: "after {{count}} minutes" + after_30_seconds: "after 30 seconds" + after_1_minute: "after 1 minute" + after_2_minutes: "after 2 minutes" + after_3_minutes: "after 3 minutes" + after_4_minutes: "after 4 minutes" + after_5_minutes: "after 5 minutes" + after_10_minutes: "after 10 minutes" invited: search: "type to search invites..." @@ -2422,6 +2421,7 @@ en: backups: "Backups" login: "Login" plugins: "Plugins" + user_preferences: "User Preferences" badges: title: Badges diff --git a/config/locales/server.bs_BA.yml b/config/locales/server.bs_BA.yml index 6757d882ad..4b690b99df 100644 --- a/config/locales/server.bs_BA.yml +++ b/config/locales/server.bs_BA.yml @@ -698,8 +698,8 @@ bs_BA: automatically_download_gravatars: "Download Gravatars for users upon account creation or email change." digest_topics: "The maximum number of topics to display in the email digest." digest_min_excerpt_length: "Minimum post excerpt in the email digest, in characters." - default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." - default_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." + default_email_digest_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." + default_other_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." max_daily_gravatar_crawls: "Maximum number of times Discourse will check Gravatar for custom avatars in a day" public_user_custom_fields: "A whitelist of custom fields for a user that can be shown publically." allow_profile_backgrounds: "Allow users to upload profile backgrounds." diff --git a/config/locales/server.cs.yml b/config/locales/server.cs.yml index d185b59964..a80f177c7d 100644 --- a/config/locales/server.cs.yml +++ b/config/locales/server.cs.yml @@ -668,8 +668,8 @@ cs: delete_all_posts_max: "Maximální počet příspěvků, které mohou být smazány najednou tlačítkem 'Odstranit všechny příspěvky'. Pokud má uživatel více příspěvků než je zde nastaveno, nemohou být jeho příspěvky smazány najednou a uživatele nelze odstranit." username_change_period: "Počet dní od registrace za kolik si uživatel může změnit svoje uživatelské jméno (0 pokud chcete změnu uživatelského jména úplně zakázat)." email_editable: "Povolit uživatelům změnit si po registraci emailovou adresu." - default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." - default_external_links_in_new_tab: "Otevírat odkazy na externí weby v novém tabu. Uživatelé si toto můžou změnit v svém nastavení." + default_email_digest_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." + default_other_external_links_in_new_tab: "Otevírat odkazy na externí weby v novém tabu. Uživatelé si toto můžou změnit v svém nastavení." enable_mobile_theme: "Používat na mobilních zařízeních verzi přizpůsobenou pro mobily s možností přejít na plnou verzi. Zruště pokud chcete používat vlastní plně responzivní kaskádový styl." short_progress_text_threshold: "After the number of posts in a topic goes above this number, the progress bar will only show the current post number. If you change the progress bar's width, you may need to change this value." default_code_lang: "Default programming language syntax highlighting applied to GitHub code blocks (lang-auto, ruby, python etc.)" diff --git a/config/locales/server.da.yml b/config/locales/server.da.yml index 68addb92fa..ab8aa0037c 100644 --- a/config/locales/server.da.yml +++ b/config/locales/server.da.yml @@ -626,8 +626,8 @@ da: delete_all_posts_max: "Det maksimale antal indlæg der kan slettes på én gang med “Slet alle indlæg”-knappen. Hvis en bruger har mere end dette antal indlæg, kan indlæggene ikke slettes på én gang og brugeren kan ikke slettes." username_change_period: "Antal dage efter oprettelsen hvor brugere kan ændre deres brugernavn (0 for ikke at tillade skift af brugernavn)." email_editable: "Lad brugerne skifte deres e-mail-adresse efter oprettelsen." - default_digest_email_frequency: "Hvor ofte brugerne som standard modtager e-mail-sammendrag. De kan ændre indstillingen på deres profil." - default_external_links_in_new_tab: "Åbn eksterne links i en nu fane; brugerne kan ændre dette på deres profil" + default_email_digest_frequency: "Hvor ofte brugerne som standard modtager e-mail-sammendrag. De kan ændre indstillingen på deres profil." + default_other_external_links_in_new_tab: "Åbn eksterne links i en nu fane; brugerne kan ændre dette på deres profil" enable_mobile_theme: "Mobile enheder bruger et mobilvenligt tema, med mulighed for at skifte til det fulde site. Deaktivér dette hvis du ønsker at anvende et brugerdefineret stylesheet som er fuldstændigt responsivt." short_progress_text_threshold: "Når antallet af indlæg overstiger dette tal viser statuslinjen kun det aktuelle indlægsnummer. Hvis du ændrer bredden af statuslinjen kan det være nødvendigt at opdatere denne værdi." default_code_lang: "Standard syntax highlighting som bruges i GitHub kodeblokke (lang-auto, ruby, python etc.)." diff --git a/config/locales/server.de.yml b/config/locales/server.de.yml index 356ef447ba..02b0b32020 100644 --- a/config/locales/server.de.yml +++ b/config/locales/server.de.yml @@ -912,10 +912,10 @@ de: automatically_download_gravatars: "Avatare von Gravatar herunterladen, wenn ein Nutzer sich registriert oder seine E-Mail-Adresse ändert." digest_topics: "Maximale Anzahl von Themen, die in der E-Mail-Zusammenfassung angezeigt werden." digest_min_excerpt_length: "Minimale Länge des Auszugs aus einem Beitrag in der E-Mail-Zusammenfassung, in Zeichen." - default_digest_email_frequency: "Wie oft man Zusammenfassungen per Mail standardmässig erhält. Diese Einstellung kann von jedem geändert werden." + default_email_digest_frequency: "Wie oft man Zusammenfassungen per Mail standardmässig erhält. Diese Einstellung kann von jedem geändert werden." suppress_digest_email_after_days: "Sende keine E-Mail-Zusammenfassungen an Benutzer, die die Seite seit mehr als (n) Tagen nicht mehr besucht haben." disable_digest_emails: "E-Mail-Zusammenfassungen für alle Benutzer deaktivieren." - default_external_links_in_new_tab: "Öffne externe Links in einem neuen Tab. Benutzer können dies in ihren Einstellungen ändern." + default_other_external_links_in_new_tab: "Öffne externe Links in einem neuen Tab. Benutzer können dies in ihren Einstellungen ändern." max_daily_gravatar_crawls: "Wie oft pro Tag Discourse höchstens auf Gravatar nach benuterdefinierten Avataren suchen soll." public_user_custom_fields: "Liste selbst definierter Profil-Felder, die öffentlich angezeigt werden dürfen." staff_user_custom_fields: "Liste selbst definierter Profil-Felder, die Mitarbeitern angezeigt werden dürfen." diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index dba03554a7..31391bf511 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -95,6 +95,7 @@ en: min_username_length_range: "You cannot set the minimum above the maximum." max_username_length_exists: "You cannot set the maximum username length below the longest username." max_username_length_range: "You cannot set the maximum above the minimum." + default_categories_already_selected: "You cannot select a category used in another list." activemodel: errors: @@ -829,9 +830,6 @@ en: anon_polling_interval: "How often should anonymous clients poll in milliseconds" background_polling_interval: "How often should the clients poll in milliseconds (when the window is in the background)" - auto_track_topics_after: "Global default milliseconds before a topic is automatically tracked, users can override (0 for always, -1 for never)" - new_topic_duration_minutes: "Global default number of minutes a topic is considered new, users can override (-1 for always, -2 for last visit)" - flags_required_to_hide_post: "Number of flags that cause a post to be automatically hidden and PM sent to the user (0 for never)" cooldown_minutes_after_hiding_posts: "Number of minutes a user must wait before they can edit a post hidden via community flagging" @@ -1124,10 +1122,8 @@ en: automatically_download_gravatars: "Download Gravatars for users upon account creation or email change." digest_topics: "The maximum number of topics to display in the email digest." digest_min_excerpt_length: "Minimum post excerpt in the email digest, in characters." - default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." suppress_digest_email_after_days: "Suppress digest emails for users not seen on the site for more than (n) days." disable_digest_emails: "Disable digest emails for all users." - default_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." detect_custom_avatars: "Whether or not to check that users have uploaded custom profile pictures." max_daily_gravatar_crawls: "Maximum number of times Discourse will check Gravatar for custom avatars in a day" @@ -1195,6 +1191,24 @@ en: approve_unless_trust_level: "Posts for users below this trust level must be approved" notify_about_queued_posts_after: "If there are posts that have been waiting to be reviewed for more than this many hours, an email will be sent to the contact email. Set to 0 to disable these emails." + default_email_digest_frequency: "How often users receive digest emails by default." + default_email_private_messages: "Send an email when someone messages the user by default." + default_email_direct: "Send an email when someone quotes/replies to/mentions or invites the user by default." + default_email_mailing_list_mode: "Send an email for every new post by default." + default_email_always: "Send an email notification even when the user is active by default." + + default_other_new_topic_duration_minutes: "Global default number of minutes a topic is considered new, users can override (-1 for always, -2 for last visit)" + default_other_auto_track_topics_after_msecs: "Global default milliseconds before a topic is automatically tracked, users can override (0 for always, -1 for never)" + default_other_external_links_in_new_tab: "Open external links in a new tab by default." + default_other_enable_quoting: "Enable quote reply for highlighted text by default." + default_other_dynamic_favicon: "Show new/updated topic count on browser icon by default." + default_other_disable_jump_reply: "Don't jump to user's post after they reply by default." + default_other_edit_history_public: "Make the post revisions public by default." + + default_categories_watching: "List of categories that are watched by default." + default_categories_tracking: "List of categories that are tracked by default." + default_categories_muted: "List of categories that are muted by default." + errors: invalid_email: "Invalid email address." invalid_username: "There's no user with that username." diff --git a/config/locales/server.es.yml b/config/locales/server.es.yml index 825daf4a65..cd70349ba3 100644 --- a/config/locales/server.es.yml +++ b/config/locales/server.es.yml @@ -962,10 +962,10 @@ es: automatically_download_gravatars: "Descargar Gravatars para usuarios cuando se creen una cuenta o cambien el email." digest_topics: "El número máximo de temas a mostrar en el resumen por email." digest_min_excerpt_length: "La extensión mínima, en caracteres, del extracto de un post en el resumen por email." - default_digest_email_frequency: "Cada cuánto tiempo los usuarios recibirán emails con el resumen del sitio por defecto. Cada usuario puede cambiar esta opción para sí en sus preferencias." + default_email_digest_frequency: "Cada cuánto tiempo los usuarios recibirán emails con el resumen del sitio por defecto. Cada usuario puede cambiar esta opción para sí en sus preferencias." suppress_digest_email_after_days: "Suprimir los emails de resumen para aquellos usuarios que no han visto el sitio desde más de (n) días." disable_digest_emails: "Inhabilitar e-mails de resumen para todos los usuarios." - default_external_links_in_new_tab: "Abrir enlaces externos en una nueva pestaña. Los usuarios pueden cambiar esto en sus preferencias." + default_other_external_links_in_new_tab: "Abrir enlaces externos en una nueva pestaña. Los usuarios pueden cambiar esto en sus preferencias." detect_custom_avatars: "Verificar o no que los usuarios han subido una imagen de perfil." max_daily_gravatar_crawls: "Máximo número de veces que Discourse comprobará Gravatar en busca de avatares personalizados en un día" public_user_custom_fields: "Una lista con campos personalizados para el usuario que pueden ser mostrados públicamente." diff --git a/config/locales/server.fa_IR.yml b/config/locales/server.fa_IR.yml index 60c71fcf50..1920ee6b2d 100644 --- a/config/locales/server.fa_IR.yml +++ b/config/locales/server.fa_IR.yml @@ -903,10 +903,10 @@ fa_IR: automatically_download_gravatars: "آواتار را برای کاربران دریافت کن برای ساختن حساب کاربری یا ایمیل. " digest_topics: "حداکثر تعداد جستارهایی که در دایجست ایمیل نشان داده می شود " digest_min_excerpt_length: "حداقل نوشته های گزیده در ایمیل دایجست٬‌ در کاراکتر." - default_digest_email_frequency: "هر چند وقت یکبار کاربران ایمیل های دایجست دریافت کنند بطور پیش فرض. آنها می توانند آن را در بخش تنظیمات عوض کنند. " + default_email_digest_frequency: "هر چند وقت یکبار کاربران ایمیل های دایجست دریافت کنند بطور پیش فرض. آنها می توانند آن را در بخش تنظیمات عوض کنند. " suppress_digest_email_after_days: "ایمیل های خلاصه را مهار کن برای کاربرانی که در وب سایت دیده نشده اند بیشتر از (n) روز " disable_digest_emails: "ایمیل های دایجست را برای تمام کاربران غیر فعال کن. " - default_external_links_in_new_tab: "پیوند های خارجی را در یک تب جدید باز کن. کاربران می توانند این را در قسمت تنظیماتشان تغییر دهند." + default_other_external_links_in_new_tab: "پیوند های خارجی را در یک تب جدید باز کن. کاربران می توانند این را در قسمت تنظیماتشان تغییر دهند." max_daily_gravatar_crawls: "حداکثرتعداد زمانی که دیسکورس Gravatar را چک می کند برای آواتار سفارشی در هر روز. " public_user_custom_fields: "لیست مجاز فیلد سفارشی برای کاربر که می تواند به همه نشان داده شود." staff_user_custom_fields: "لیست مجاز فیلد سفارشی برای کاربر که می تواند به مدیران نشان داده شود." diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index fb87dbf2bb..91ca213c03 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -971,10 +971,10 @@ fi: automatically_download_gravatars: "Lataa käyttäjille Gravatarit automaattisesti tilin luonnin ja sähköpostin vaihdon yhteydessä." digest_topics: "Sähköpostitiivistelmässä näytettävien ketjujen maksimimäärä." digest_min_excerpt_length: "Viestin katkelman vähimmäispituus sähköpostitiivistelmässä, merkeissä" - default_digest_email_frequency: "Kuinka usein käyttäjän saavat sähköpostitiivistelmän oletuksena. He voivat muuttaa asetusta omista asetuksistaan." + default_email_digest_frequency: "Kuinka usein käyttäjän saavat sähköpostitiivistelmän oletuksena. He voivat muuttaa asetusta omista asetuksistaan." suppress_digest_email_after_days: "Jätä lähettämättä tiivistelmäsähköpostit käyttäjille, joita ei ole nähty (n) päivän aikana." disable_digest_emails: "Ota tiivistelmäsähköpostit pois käytöstä kaikilta käyttäjiltä." - default_external_links_in_new_tab: "Avaa ulkoiset linkit uudessa välilehdessä. Käyttäjät voivat muuttaa tämän asetuksistaan." + default_other_external_links_in_new_tab: "Avaa ulkoiset linkit uudessa välilehdessä. Käyttäjät voivat muuttaa tämän asetuksistaan." detect_custom_avatars: "Tarkistetaanko, ovatko käyttäjät ladanneet oman profiilikuvan." max_daily_gravatar_crawls: "Korkeintaan kuinka monta kertaa Discourse tarkistaa avatarit Gravatarista päivässä" public_user_custom_fields: "Whitelist käyttäjän mukautetuista kentistä, jotka voidaan näyttää julkisesti." diff --git a/config/locales/server.fr.yml b/config/locales/server.fr.yml index 137666d213..3d85c31a89 100644 --- a/config/locales/server.fr.yml +++ b/config/locales/server.fr.yml @@ -966,10 +966,10 @@ fr: automatically_download_gravatars: "Télécharger les gravatars pour les utilisateurs lors de la création de compte ou de la modification de courriel." digest_topics: "Nombre maximum de sujets à afficher dans le courriel de résumé." digest_min_excerpt_length: "Taille minimum du résumé des messages dans les courriels, en caractères." - default_digest_email_frequency: "A quelle fréquence les utilisateurs reçoivent-ils les courriels par défaut. Ils peuvent modifier ce paramétrage dans leur préférences." + default_email_digest_frequency: "A quelle fréquence les utilisateurs reçoivent-ils les courriels par défaut. Ils peuvent modifier ce paramétrage dans leur préférences." suppress_digest_email_after_days: "Ne pas envoyer de résumés courriel aux utilisateurs qui n'ont pas visité le site depuis (n) jours." disable_digest_emails: "Désactiver les résumés par courriels pour tous les utilisateurs." - default_external_links_in_new_tab: "Les liens externes s'ouvrent dans un nouvel onglet. Les utilisateurs peuvent modifier ceci dans leurs préférences." + default_other_external_links_in_new_tab: "Les liens externes s'ouvrent dans un nouvel onglet. Les utilisateurs peuvent modifier ceci dans leurs préférences." detect_custom_avatars: "Vérifier ou non si les utilisateurs ont envoyé une photo de profil personnalisée." max_daily_gravatar_crawls: "Nombre maximum de fois que Discourse vérifiera Gravatar pour des avatars personnalisés en une journée." public_user_custom_fields: "Une liste blanche des champs personnalisés pour un utilisateur qui peuvent être affichés publiquement." diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 5b5fc89d7b..86aa93036d 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -950,10 +950,10 @@ he: automatically_download_gravatars: "הורדת גראווטרים למשתמשים בעת יצירת החשבון או שינוי כתובת הדוא\"ל." digest_topics: "מספר הנושאים המקסימלי להצגה במייל סיכום." digest_min_excerpt_length: "מספר התווים המינימלי למובאות מתוך הפרסום במייל הסיכום." - default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." + default_email_digest_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." suppress_digest_email_after_days: "השהיית מיילים מסכמים עבור משתמשים שלא נראו באתר במשך יותר מ(n) ימים." disable_digest_emails: "נטרול דוא\"ל סיכום לכל המשתמשים." - default_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." + default_other_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." detect_custom_avatars: "Whether or not to check that users have uploaded custom profile pictures." max_daily_gravatar_crawls: "מספר הפעמים המקסימלי ש-Discourse יבדוק אווטרים ב-Gravatar ביום" public_user_custom_fields: "רשימה לבנה (whitelist) של שדות מותאמים למשתמש שיכולים להיות מוצגים באופן פומבי." diff --git a/config/locales/server.it.yml b/config/locales/server.it.yml index 0e72e1dead..bdd270d2e6 100644 --- a/config/locales/server.it.yml +++ b/config/locales/server.it.yml @@ -896,8 +896,8 @@ it: default_avatars: "URL degli avatar che verranno utilizzati come predefiniti per tutti i nuovi utenti, fintanto che non li cambieranno esplicitamente." automatically_download_gravatars: "Scarica i Gravatars per gli utenti quando viene creato l'account o quando viene modificata l'email" digest_topics: "Numero massimo di argomenti da mostrare nel riassunto email." - default_digest_email_frequency: "Quanto spesso gli utenti ricevono email di riepilogo. Gli utenti possono modificare questa impostazione nelle loro preferenze." - default_external_links_in_new_tab: "Apri i collegamenti esterni in una nuova scheda. Gli utenti possono modificare questa impostazione nelle loro preferenze." + default_email_digest_frequency: "Quanto spesso gli utenti ricevono email di riepilogo. Gli utenti possono modificare questa impostazione nelle loro preferenze." + default_other_external_links_in_new_tab: "Apri i collegamenti esterni in una nuova scheda. Gli utenti possono modificare questa impostazione nelle loro preferenze." allow_profile_backgrounds: "Permetti agli utenti di caricare immagini di sfondo per il profilo." enable_mobile_theme: "I dispositivi mobili usano un tema apposito, con possibilità di passare alla visualizzazione completa. Disabilita questa opzione se vuoi usare un foglio di stile personalizzato che sia completamente reattivo." suppress_uncategorized_badge: "Non mostrare la targhetta per gli argomenti senza categoria nell'elenco degli argomenti." diff --git a/config/locales/server.ja.yml b/config/locales/server.ja.yml index 2c997ccda0..7e797cde41 100644 --- a/config/locales/server.ja.yml +++ b/config/locales/server.ja.yml @@ -897,10 +897,10 @@ ja: automatically_download_gravatars: "アカウントの生成時、メールアドレスの変更時にGravatarをダウンロード" digest_topics: "ダイジェストメールに表示されるトピックの最大数" digest_min_excerpt_length: "ダイジェストメール内の投稿の抜粋の最小文字数" - default_digest_email_frequency: "ユーザがダイジェストメールを受け取る頻度のデフォルト値。ユーザは設定画面でこの値をカスタマイズできます。" + default_email_digest_frequency: "ユーザがダイジェストメールを受け取る頻度のデフォルト値。ユーザは設定画面でこの値をカスタマイズできます。" suppress_digest_email_after_days: "(n)日以上ユーザが参照していなければダイジェストメールを抑制します" disable_digest_emails: "全てのユーザのダイジェストメールを無効にする" - default_external_links_in_new_tab: "外部リンクは新しいタブで開きます。ユーザーはこの設定を変更する事が出来ます" + default_other_external_links_in_new_tab: "外部リンクは新しいタブで開きます。ユーザーはこの設定を変更する事が出来ます" detect_custom_avatars: "ユーザがプロフィール画像をアップロードしたか確認する" max_daily_gravatar_crawls: "Discourseがプロフィール画像の確認をgravastarに行う回数の上限" public_user_custom_fields: "パブリックに公開されるカスタムフィールドのホワイトリスト" diff --git a/config/locales/server.ko.yml b/config/locales/server.ko.yml index 0b2b79d333..d1e54c3b2b 100644 --- a/config/locales/server.ko.yml +++ b/config/locales/server.ko.yml @@ -886,10 +886,10 @@ ko: automatically_download_gravatars: "사용자가 계정을 만들거나 이메일을 변경하자마자 Gravatar를 다운로드합니다." digest_topics: "요약 이메일에서 보여질 최대 토픽 개수" digest_min_excerpt_length: "요약 이메일에서 최소 포스트 발췌 수" - default_digest_email_frequency: "사용자가 요약 이메일을 받는 횟수 기본값. 사용자는 그들의 환경설정에서 변경할 수 있음" + default_email_digest_frequency: "사용자가 요약 이메일을 받는 횟수 기본값. 사용자는 그들의 환경설정에서 변경할 수 있음" suppress_digest_email_after_days: "(n)일동안 사이트에서 보지 못한 사용자에게는 이메일 요약을 보내지 않습니다." disable_digest_emails: "모든 유저들 이메일 다이제스트 못하게 하기" - default_external_links_in_new_tab: "다른 싸이트의 링크는 새 탭으로 연다. 사용자는 자신의 설정에 따라 바꿀 수 있음" + default_other_external_links_in_new_tab: "다른 싸이트의 링크는 새 탭으로 연다. 사용자는 자신의 설정에 따라 바꿀 수 있음" max_daily_gravatar_crawls: "하루에 Discourse가 커스텀 아파타를 위해 Gravatar를 체크하는 최대 횟수" public_user_custom_fields: "유저가 쓸 수 있는 공개 커스텀 필드 목록" staff_user_custom_fields: "스태프가 쓸 수 있는 공개 커스텀 필드 목록" diff --git a/config/locales/server.nl.yml b/config/locales/server.nl.yml index 9ccb24406a..54fd386f94 100644 --- a/config/locales/server.nl.yml +++ b/config/locales/server.nl.yml @@ -840,8 +840,8 @@ nl: email_editable: "Gebruikers mogen hun e-mailadres na registratie nog wijzigen." digest_topics: "Het maximum aantal topics dat in de e-maildigest opgenomen wordt." digest_min_excerpt_length: "Hoeveel karakters er per bericht getoond worden in de mail digest" - default_digest_email_frequency: "Hoe vaak ontvangen gebruikers standaard de digestmails. Ze kunnen dit in hun eigen instellingen nog aanpassen." - default_external_links_in_new_tab: "Open externe links in a nieuwe tab. Gebruikers kunnen dit wijzigen in hun instellingen." + default_email_digest_frequency: "Hoe vaak ontvangen gebruikers standaard de digestmails. Ze kunnen dit in hun eigen instellingen nog aanpassen." + default_other_external_links_in_new_tab: "Open externe links in a nieuwe tab. Gebruikers kunnen dit wijzigen in hun instellingen." allow_profile_backgrounds: "Gebruikers mogen een profielachtergrond instellen." enable_mobile_theme: "Mobiele apparaten gebruiken een mobiel-vriendelijke theme met de mogelijkheid te schakelen naar de volledige site. Schakel deze optie uit als je een eigen stylesheet wil gebruiken die volledig responsive is." suppress_uncategorized_badge: "Laat de badge niet zien voor topics zonder categorie in de topiclijsten." diff --git a/config/locales/server.pl_PL.yml b/config/locales/server.pl_PL.yml index 1677c38ad6..d76b6f5085 100644 --- a/config/locales/server.pl_PL.yml +++ b/config/locales/server.pl_PL.yml @@ -796,10 +796,10 @@ pl_PL: email_editable: "Allow users to change their e-mail address after registration." digest_topics: "Maksymalna liczba tematów w podsumowaniu e-mail." digest_min_excerpt_length: "Minimalny wycinek wpisu (liczba znaków) w podsumowaniu e-mail." - default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." + default_email_digest_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." suppress_digest_email_after_days: "Nie wysyłaj podsumowań e-mail użytkownikom, którzy nie odwiedzili serwisu dłużej niż (n) dni." disable_digest_emails: "Wyłącz wysyłanie podsumowania e-mail wszystkim uzytkownikom. " - default_external_links_in_new_tab: "Otwieraj zewnętrzne odnośniki w nowej karcie. Użytkownicy mogą zmienić to ustawienie w swoich preferencjach." + default_other_external_links_in_new_tab: "Otwieraj zewnętrzne odnośniki w nowej karcie. Użytkownicy mogą zmienić to ustawienie w swoich preferencjach." allow_profile_backgrounds: "Zezwól użytkownikom na przesyłanie obrazu tła dla profilu." enable_mobile_theme: "Urządzenia mobilne używają dedykowanego mobilnego szablonu. Wyłącz to, jeśli chcesz użyć własnego, pojedynczego i responsywnego szablonu stylów. " suppress_uncategorized_badge: "Nie pokazuj etykiety z nazwą kategorii Inne na listach tematów." diff --git a/config/locales/server.pt.yml b/config/locales/server.pt.yml index d702d48251..a56c7fe213 100644 --- a/config/locales/server.pt.yml +++ b/config/locales/server.pt.yml @@ -975,10 +975,10 @@ pt: automatically_download_gravatars: "Descarregar Gravatars para os utilizadores após criação de conta ou mudança de email." digest_topics: "Número máximo de tópicos a serem apresentados no resumo do email." digest_min_excerpt_length: "Tamanho mínimo do excerto da mensagem no resumo do email, em caracteres." - default_digest_email_frequency: "Por defeito, quantas vezes os utilizadores recebem emails de resumo. Os utilizadores podem alterar esta configuração nas suas preferências." + default_email_digest_frequency: "Por defeito, quantas vezes os utilizadores recebem emails de resumo. Os utilizadores podem alterar esta configuração nas suas preferências." suppress_digest_email_after_days: "Suprimir emails de resumos para utilizadores não vistos no sítio por mais de (n) dias." disable_digest_emails: "Desativar os emails de resumo para todos os utilizadores." - default_external_links_in_new_tab: "Abrir hiperligações externas num novo separador. Os utilizadores podem alterar isto nas suas preferências." + default_other_external_links_in_new_tab: "Abrir hiperligações externas num novo separador. Os utilizadores podem alterar isto nas suas preferências." detect_custom_avatars: "Se deve ou não verificar que os utilizadores carregaram fotografias de perfil personalizadas." max_daily_gravatar_crawls: "Número máximo de vezes que o Discourse irá verificar o Gravatar para avatars personalizados, por dia" public_user_custom_fields: "Lista de campos personalizados para um utilizador e que podem ser exibidos publicamente." diff --git a/config/locales/server.pt_BR.yml b/config/locales/server.pt_BR.yml index 6ea4621962..c1d7dbd423 100644 --- a/config/locales/server.pt_BR.yml +++ b/config/locales/server.pt_BR.yml @@ -938,10 +938,10 @@ pt_BR: automatically_download_gravatars: "Fazer download de Gravatars dos usuários ao criar conta ou mudança de email." digest_topics: "O número máximo de tópicos a serem mostrados no resumo via email." digest_min_excerpt_length: "O excerto mínimo de post no resumo via email, em caracteres." - default_digest_email_frequency: "Quantas vezes os usuários recebem emails de resumo por padrão. Eles podem alterar essa configuração em suas preferências." + default_email_digest_frequency: "Quantas vezes os usuários recebem emails de resumo por padrão. Eles podem alterar essa configuração em suas preferências." suppress_digest_email_after_days: "Suprimir emails de resumo para usuários não vistos no site há mais do que (n) dias." disable_digest_emails: "Desabilitar emails de resumo para todos os usuários." - default_external_links_in_new_tab: "Abrir links externos em uma nova guia. Os usuários podem mudar isso em suas preferências." + default_other_external_links_in_new_tab: "Abrir links externos em uma nova guia. Os usuários podem mudar isso em suas preferências." max_daily_gravatar_crawls: "Número máximo de vezes que o Discourse irá checar o Gravatar por avatares personalizados em um dia" public_user_custom_fields: "Um conjunto de campos personalizados para um usuário que podem ser apresentados publicamente." staff_user_custom_fields: "Um conjunto de campos personalizados para um usuário que pode ser mostrado para membros da equipe." diff --git a/config/locales/server.ro.yml b/config/locales/server.ro.yml index fc589c87cf..d3bef4d66a 100644 --- a/config/locales/server.ro.yml +++ b/config/locales/server.ro.yml @@ -770,7 +770,7 @@ ro: automatically_download_gravatars: "Descarcă Gravatare pentru utilizatori la crearea contului sau schimbarea email-ului." digest_topics: "Numărul maxim de discuții arătate în email-ul rezumat." digest_min_excerpt_length: "Numărul minimum de extrase din postări din email-ul rezumat, în caractere." - default_digest_email_frequency: "Cat de des utliziatorii primesc emailuri rezumat din oficiu. Utilizatorii pot schimba această opțiune în preferințe." + default_email_digest_frequency: "Cat de des utliziatorii primesc emailuri rezumat din oficiu. Utilizatorii pot schimba această opțiune în preferințe." max_daily_gravatar_crawls: "Numărul maxim de verificări făcute de Discourse pentru existența unui gravatar preferențial într-o zi" allow_profile_backgrounds: "Permite utilizatorilor să încarce fundaluri de profil." sequential_replies_threshold: "Numărul de postări la rând într-o discuție până să-i fie amintit utilizatorului că sunt prea multe răspunsuri secvențiale. " diff --git a/config/locales/server.ru.yml b/config/locales/server.ru.yml index faade29e25..49f466e653 100644 --- a/config/locales/server.ru.yml +++ b/config/locales/server.ru.yml @@ -986,10 +986,10 @@ ru: automatically_download_gravatars: "Скачивать аватарку Gravatar пользователя во время создания учетной записи или изменения e-mail." digest_topics: "Максимальное количество тем в письме - сводке новостей." digest_min_excerpt_length: "Минимальная длина (в символах) вытяжки из сообщения в письме - сводке новостей." - default_digest_email_frequency: "Как часто пользователи получают дайджест по умолчанию. Возможно изменение этой настройки каждым пользователем." + default_email_digest_frequency: "Как часто пользователи получают дайджест по умолчанию. Возможно изменение этой настройки каждым пользователем." suppress_digest_email_after_days: "Не рассылать новости для пользователей, которые не заходили на сайт в течении (n) дней." disable_digest_emails: "Отключить рассылку новостей для всех пользователей." - default_external_links_in_new_tab: "Открывать внешние ссылки в новом окне. Пользователи могут изменить данное поведение в настройках." + default_other_external_links_in_new_tab: "Открывать внешние ссылки в новом окне. Пользователи могут изменить данное поведение в настройках." max_daily_gravatar_crawls: "Максимальное количество загрузок аватаорок с Gravatar за один день" public_user_custom_fields: "Список разрешенных дополнительных полей пользователей, которые могут быть отображены публично." staff_user_custom_fields: "Список разрешенных дополнительных полей пользователей, которые могут быть отображены для модераторов." diff --git a/config/locales/server.sq.yml b/config/locales/server.sq.yml index 3cf162ef81..b2639ae2fd 100644 --- a/config/locales/server.sq.yml +++ b/config/locales/server.sq.yml @@ -971,10 +971,10 @@ sq: automatically_download_gravatars: "Download Gravatars for users upon account creation or email change." digest_topics: "The maximum number of topics to display in the email digest." digest_min_excerpt_length: "Minimum post excerpt in the email digest, in characters." - default_digest_email_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." + default_email_digest_frequency: "How often users receive digest emails by default. They can change this setting in their preferences." suppress_digest_email_after_days: "Suppress digest emails for users not seen on the site for more than (n) days." disable_digest_emails: "Disable digest emails for all users." - default_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." + default_other_external_links_in_new_tab: "Open external links in a new tab. Users can change this in their preferences." detect_custom_avatars: "Whether or not to check that users have uploaded custom profile pictures." max_daily_gravatar_crawls: "Maximum number of times Discourse will check Gravatar for custom avatars in a day" public_user_custom_fields: "A whitelist of custom fields for a user that can be shown publicly." diff --git a/config/locales/server.sv.yml b/config/locales/server.sv.yml index 6e939fcfd3..2c7bcaaf39 100644 --- a/config/locales/server.sv.yml +++ b/config/locales/server.sv.yml @@ -719,7 +719,7 @@ sv: disable_emails: "Förhindra Discourse från att skicka någon form av e-post" pop3_polling_ssl: "Använd SSL vid anslutning till POP3-servern. (Rekommenderat)" email_editable: "Tillåt användare att ändra deras e-postadress efter registrering." - default_digest_email_frequency: "Hur ofta användare får emailutskick som standard. De kan ändra detta val under sina inställningar." + default_email_digest_frequency: "Hur ofta användare får emailutskick som standard. De kan ändra detta val under sina inställningar." enable_user_directory: "Tillhandahåll en bläddringsbar användarkatalog" allow_anonymous_posting: "Tillåt användare att växla till anonymt läge" anonymous_posting_min_trust_level: "Lägsta förtroendenivå som krävs för att aktivera anonyma inlägg" diff --git a/config/locales/server.tr_TR.yml b/config/locales/server.tr_TR.yml index 053737520b..d938e4cf61 100644 --- a/config/locales/server.tr_TR.yml +++ b/config/locales/server.tr_TR.yml @@ -889,10 +889,10 @@ tr_TR: automatically_download_gravatars: "Hesap oluşturma veya e-posta değişikliği esnasında kullanıcılar için Gravatarları indir" digest_topics: "Özet e-postalarda yer alacak en fazla konu sayısı. " digest_min_excerpt_length: "Özet e-postalarında, gönderi alıntılarında olması gereken en az karakter sayısı." - default_digest_email_frequency: "Varsayılan olarak, özet e-postalar hangi sıklıkta gönderilsin? Üyeler, ayarlar sayfasından bu değeri değiştirebilir." + default_email_digest_frequency: "Varsayılan olarak, özet e-postalar hangi sıklıkta gönderilsin? Üyeler, ayarlar sayfasından bu değeri değiştirebilir." suppress_digest_email_after_days: "Siteye (n) günden fazla süredir uğramayan kullanıcılar için özet e-posta gönderimini durdur" disable_digest_emails: "Tüm kullanıcılar için özet e-postalarını devre dışı bırak." - default_external_links_in_new_tab: "Dış bağlantıları yeni sekmede aç. Üyeler, ayarlar sayfasından bu ayarı değiştirebilir." + default_other_external_links_in_new_tab: "Dış bağlantıları yeni sekmede aç. Üyeler, ayarlar sayfasından bu ayarı değiştirebilir." max_daily_gravatar_crawls: "Discourse'un gün içinde özel avatarlar için Gravatar'ı en fazla kaç kere kontrol edeceği." public_user_custom_fields: "Kullanıcıların için, herkes tarafından görüntülenebilir özel alanların beyaz listesi." staff_user_custom_fields: "Kullanıcıların için, sadece görevlilere görüntülenebilir özel alanların beyaz listesi." diff --git a/config/locales/server.uk.yml b/config/locales/server.uk.yml index 9ad9d639e0..07262f5133 100644 --- a/config/locales/server.uk.yml +++ b/config/locales/server.uk.yml @@ -371,8 +371,8 @@ uk: delete_all_posts_max: "Максимальне число дописів, які можна видалити за один раз кнопкою \"Видалити всі дописи\". Якщо користувач має більше дописів, ніж це число, їх не можна буде видалити за один раз, і користувача також." username_change_period: "Кількість днів після реєстрації, протягом яких новим обліковим записам можна змінювати своє ім'я користувача (0 щоб заборонити зміну імені користувача)." email_editable: "Дозволити користувачам змінювати свою електронну скриньку після реєстрації." - default_digest_email_frequency: "Як часто користувачі отримують листи зі стислим викладом новин за замовчуванням. Вони можуть змінити це у своїх налаштуваннях." - default_external_links_in_new_tab: "Відкривати нові посилання у новій вкладці. Користувачі можуть змінити це у своїх налаштуваннях." + default_email_digest_frequency: "Як часто користувачі отримують листи зі стислим викладом новин за замовчуванням. Вони можуть змінити це у своїх налаштуваннях." + default_other_external_links_in_new_tab: "Відкривати нові посилання у новій вкладці. Користувачі можуть змінити це у своїх налаштуваннях." allow_profile_backgrounds: "Дозволити користувачам завантажувати фони профілю." enable_mobile_theme: "Мобільні пристрої використовуватимуть тему, дружню для них, з можливістю перемикатися на повний сайт. Відключіть це, якщо хочете використовувати власну, повністю чутливу, таблицю стилів." display_name_on_posts: "Показувати повні імена користувачів на їх дописах у додаток до їх @username." diff --git a/config/locales/server.zh_CN.yml b/config/locales/server.zh_CN.yml index 13821a1111..5c834650d0 100644 --- a/config/locales/server.zh_CN.yml +++ b/config/locales/server.zh_CN.yml @@ -937,10 +937,10 @@ zh_CN: automatically_download_gravatars: "为注册或更改邮箱的用户下载 Gravatar 头像。" digest_topics: "邮件摘要中显示的最大主题数目。" digest_min_excerpt_length: "在邮件摘要中每个帖子最少显示的字符数量。" - default_digest_email_frequency: "用户收到摘要邮件的默认间隔。用户可以在参数设置中更改这个设置。" + default_email_digest_frequency: "用户收到摘要邮件的默认间隔。用户可以在参数设置中更改这个设置。" suppress_digest_email_after_days: "不发送摘要邮件给超过 (n) 天未出现的用户。" disable_digest_emails: "为所有用户禁用摘要邮件。" - default_external_links_in_new_tab: "在新标签页中打开外部链接。用户可以在参数设置中更改这个设置。" + default_other_external_links_in_new_tab: "在新标签页中打开外部链接。用户可以在参数设置中更改这个设置。" detect_custom_avatars: "检测用户是否上传了自定义个人头像。" max_daily_gravatar_crawls: "一天内 Discourse 将自动检查 gravatar 自定义头像的次数" public_user_custom_fields: "可公开显示的用户自定义属性白名单" diff --git a/config/locales/server.zh_TW.yml b/config/locales/server.zh_TW.yml index 4c8efad4cf..dd0d0c9850 100644 --- a/config/locales/server.zh_TW.yml +++ b/config/locales/server.zh_TW.yml @@ -769,10 +769,10 @@ zh_TW: automatically_download_gravatars: "當用戶註冊或更改EMail時下載 Gravatars 圖片" digest_topics: "EMail 摘要中顯示的最大話題數量" digest_min_excerpt_length: "EMail 摘要中每篇文章最少顯示的字元數量" - default_digest_email_frequency: "用戶收到摘要郵件的默認間隔。用戶可以在設定中更改。" + default_email_digest_frequency: "用戶收到摘要郵件的默認間隔。用戶可以在設定中更改。" suppress_digest_email_after_days: "不發送摘要郵件給超過 (n) 天閒置的用戶。" disable_digest_emails: "禁用發送摘要郵件給所有用戶。" - default_external_links_in_new_tab: "以新分頁開啟所有外部連結,用戶可於個人偏好設定更改設定。" + default_other_external_links_in_new_tab: "以新分頁開啟所有外部連結,用戶可於個人偏好設定更改設定。" max_daily_gravatar_crawls: "一天內 Discourse 將自動檢查 Gravatar 自訂個人圖示的次數" public_user_custom_fields: "用戶可設定公開顯示的自定欄位白名單。" staff_user_custom_fields: "用戶可設定只給管理員顯示的自定欄位白名單。" diff --git a/config/site_settings.yml b/config/site_settings.yml index 3dd9f03054..7720cf8903 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -70,7 +70,6 @@ basic: min: 0 limit_suggested_to_category: default: false - default_external_links_in_new_tab: false track_external_right_clicks: client: true default: false @@ -282,7 +281,6 @@ users: default: '' hidden: true username_change_period: 3 - auto_track_topics_after: 240000 email_editable: true logout_redirect: client: true @@ -474,9 +472,6 @@ email: email_posts_context: 5 digest_min_excerpt_length: 100 digest_topics: 20 - default_digest_email_frequency: - default: 7 - enum: 'DigestEmailSiteSetting' suppress_digest_email_after_days: 365 disable_digest_emails: default: false @@ -841,7 +836,6 @@ uncategorized: max_similar_results: 5 minimum_topics_similar: 50 - new_topic_duration_minutes: 2880 previous_visit_timeout_hours: 1 staff_like_weight: 3 topic_view_duration_hours: 8 @@ -959,3 +953,34 @@ uncategorized: privacy_topic_id: default: -1 hidden: true + +user_preferences: + default_email_digest_frequency: + enum: 'DigestEmailSiteSetting' + default: 7 + default_email_private_messages: true + default_email_direct: true + default_email_mailing_list_mode: false + default_email_always: false + + default_other_new_topic_duration_minutes: + enum: 'NewTopicDurationSiteSetting' + default: 2880 + default_other_auto_track_topics_after_msecs: + enum: 'AutoTrackDurationSiteSetting' + default: 240000 + default_other_external_links_in_new_tab: false + default_other_enable_quoting: true + default_other_dynamic_favicon: false + default_other_disable_jump_reply: false + default_other_edit_history_public: false + + default_categories_watching: + type: category_list + default: '' + default_categories_tracking: + type: category_list + default: '' + default_categories_muted: + type: category_list + default: '' diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index f4c7c1df20..19ad366f06 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -21,7 +21,7 @@ module SiteSettingExtension end def types - @types ||= Enum.new(:string, :time, :fixnum, :float, :bool, :null, :enum, :list, :url_list, :host_list) + @types ||= Enum.new(:string, :time, :fixnum, :float, :bool, :null, :enum, :list, :url_list, :host_list, :category_list) end def mutex diff --git a/lib/site_setting_validations.rb b/lib/site_setting_validations.rb index 6ed44df803..5a9fbb2fb9 100644 --- a/lib/site_setting_validations.rb +++ b/lib/site_setting_validations.rb @@ -14,4 +14,13 @@ module SiteSettingValidations validate_error :min_username_length_range if new_val < SiteSetting.min_username_length validate_error :max_username_length_exists if User.where('length(username) > ?', new_val).exists? end + + def validate_default_categories(new_val) + validate_error :default_categories_already_selected if (new_val.split("|").to_set & SiteSetting.default_categories_selected).size > 0 + end + + alias_method :validate_default_categories_watching, :validate_default_categories + alias_method :validate_default_categories_tracking, :validate_default_categories + alias_method :validate_default_categories_muted, :validate_default_categories + end diff --git a/spec/components/site_settings/yaml_loader_spec.rb b/spec/components/site_settings/yaml_loader_spec.rb index a6800a1134..6fc59b623a 100644 --- a/spec/components/site_settings/yaml_loader_spec.rb +++ b/spec/components/site_settings/yaml_loader_spec.rb @@ -64,7 +64,7 @@ describe SiteSettings::YamlLoader do end it "can load enum settings" do - receiver.expects(:setting).with('email', 'default_digest_email_frequency', 7, {enum: 'DigestEmailSiteSetting'}) + receiver.expects(:setting).with('email', 'default_email_digest_frequency', 7, {enum: 'DigestEmailSiteSetting'}) receiver.load_yaml(enum) end diff --git a/spec/fixtures/site_settings/enum.yml b/spec/fixtures/site_settings/enum.yml index 999b30b5ee..638caeee20 100644 --- a/spec/fixtures/site_settings/enum.yml +++ b/spec/fixtures/site_settings/enum.yml @@ -1,4 +1,4 @@ email: - default_digest_email_frequency: + default_email_digest_frequency: default: 7 enum: 'DigestEmailSiteSetting' diff --git a/spec/jobs/enqueue_digest_emails_spec.rb b/spec/jobs/enqueue_digest_emails_spec.rb index 2c32d34318..459e087051 100644 --- a/spec/jobs/enqueue_digest_emails_spec.rb +++ b/spec/jobs/enqueue_digest_emails_spec.rb @@ -3,11 +3,11 @@ require_dependency 'jobs/base' describe Jobs::EnqueueDigestEmails do - describe '#target_users' do context 'disabled digests' do - let!(:user_no_digests) { Fabricate(:active_user, email_digests: false, last_emailed_at: 8.days.ago, last_seen_at: 10.days.ago) } + before { SiteSetting.stubs(:default_email_digest_frequency).returns("") } + let!(:user_no_digests) { Fabricate(:active_user, last_emailed_at: 8.days.ago, last_seen_at: 10.days.ago) } it "doesn't return users with email disabled" do expect(Jobs::EnqueueDigestEmails.new.target_user_ids.include?(user_no_digests.id)).to eq(false) diff --git a/spec/jobs/notify_mailing_list_subscribers_spec.rb b/spec/jobs/notify_mailing_list_subscribers_spec.rb index bd70b7d3d9..84e94dc58f 100644 --- a/spec/jobs/notify_mailing_list_subscribers_spec.rb +++ b/spec/jobs/notify_mailing_list_subscribers_spec.rb @@ -3,62 +3,69 @@ require "spec_helper" describe Jobs::NotifyMailingListSubscribers do context "with mailing list on" do - let(:user) { Fabricate(:user, mailing_list_mode: true) } + before { SiteSetting.stubs(:default_email_mailing_list_mode).returns(true) } - context "with a valid post" do + context "with mailing list on" do + let(:user) { Fabricate(:user) } + + context "with a valid post" do + let!(:post) { Fabricate(:post, user: user) } + + it "sends the email to the user" do + UserNotifications.expects(:mailing_list_notify).with(user, post).once + Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) + end + end + + context "with a deleted post" do + let!(:post) { Fabricate(:post, user: user, deleted_at: Time.now) } + + it "doesn't send the email to the user" do + UserNotifications.expects(:mailing_list_notify).with(user, post).never + Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) + end + end + + context "with a user_deleted post" do + let!(:post) { Fabricate(:post, user: user, user_deleted: true) } + + it "doesn't send the email to the user" do + UserNotifications.expects(:mailing_list_notify).with(user, post).never + Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) + end + end + + context "with a deleted topic" do + let!(:post) { Fabricate(:post, user: user) } + + before do + post.topic.update_column(:deleted_at, Time.now) + end + + it "doesn't send the email to the user" do + UserNotifications.expects(:mailing_list_notify).with(user, post).never + Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) + end + end + + end + + context "to an anonymous user with mailing list on" do + let(:user) { Fabricate(:anonymous) } let!(:post) { Fabricate(:post, user: user) } - it "sends the email to the user" do - UserNotifications.expects(:mailing_list_notify).with(user, post).once - Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) - end - end - - context "with a deleted post" do - let!(:post) { Fabricate(:post, user: user, deleted_at: Time.now) } - it "doesn't send the email to the user" do UserNotifications.expects(:mailing_list_notify).with(user, post).never Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) end end - context "with a user_deleted post" do - let!(:post) { Fabricate(:post, user: user, user_deleted: true) } - - it "doesn't send the email to the user" do - UserNotifications.expects(:mailing_list_notify).with(user, post).never - Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) - end - end - - context "with a deleted topic" do - let!(:post) { Fabricate(:post, user: user) } - - before do - post.topic.update_column(:deleted_at, Time.now) - end - - it "doesn't send the email to the user" do - UserNotifications.expects(:mailing_list_notify).with(user, post).never - Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) - end - end - - end - - context "to an anonymous user with mailing list on" do - let(:user) { Fabricate(:anonymous, mailing_list_mode: true) } - let!(:post) { Fabricate(:post, user: user) } - - it "doesn't send the email to the user" do - UserNotifications.expects(:mailing_list_notify).with(user, post).never - Jobs::NotifyMailingListSubscribers.new.execute(post_id: post.id) - end end context "with mailing list off" do - let(:user) { Fabricate(:user, mailing_list_mode: false) } + before { SiteSetting.stubs(:default_email_mailing_list_mode).returns(false) } + + let(:user) { Fabricate(:user) } let!(:post) { Fabricate(:post, user: user) } it "doesn't send the email to the user" do diff --git a/spec/models/topic_user_spec.rb b/spec/models/topic_user_spec.rb index ce8ec92a9e..4498d6e360 100644 --- a/spec/models/topic_user_spec.rb +++ b/spec/models/topic_user_spec.rb @@ -181,13 +181,13 @@ describe TopicUser do it 'should automatically track topics after they are read for long enough' do expect(topic_new_user.notification_level).to eq(TopicUser.notification_levels[:regular]) - TopicUser.update_last_read(new_user, topic.id, 2, 1001) + TopicUser.update_last_read(new_user, topic.id, 2, SiteSetting.default_other_auto_track_topics_after_msecs + 1) expect(TopicUser.get(topic, new_user).notification_level).to eq(TopicUser.notification_levels[:tracking]) end it 'should not automatically track topics after they are read for long enough if changed manually' do TopicUser.change(new_user, topic, notification_level: TopicUser.notification_levels[:regular]) - TopicUser.update_last_read(new_user, topic, 2, 1001) + TopicUser.update_last_read(new_user, topic, 2, SiteSetting.default_other_auto_track_topics_after_msecs + 1) expect(topic_new_user.notification_level).to eq(TopicUser.notification_levels[:regular]) end end @@ -256,9 +256,13 @@ describe TopicUser do it "will receive email notification for every topic" do user1 = Fabricate(:user) - user2 = Fabricate(:user, mailing_list_mode: true) + + SiteSetting.stubs(:default_email_mailing_list_mode).returns(true) + + user2 = Fabricate(:user) post = create_post - user3 = Fabricate(:user, mailing_list_mode: true) + + user3 = Fabricate(:user) create_post(topic_id: post.topic_id) # mails posts from earlier topics diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 7f3aeb6812..e2bd09f772 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -156,24 +156,6 @@ describe User do expect(subject.email_direct).to eq(true) end - context 'digest emails' do - it 'defaults to digests every week' do - expect(subject.email_digests).to eq(true) - expect(subject.digest_after_days).to eq(7) - end - - it 'uses default_digest_email_frequency' do - SiteSetting.stubs(:default_digest_email_frequency).returns(1) - expect(subject.email_digests).to eq(true) - expect(subject.digest_after_days).to eq(1) - end - - it 'disables digests by default if site setting says so' do - SiteSetting.stubs(:default_digest_email_frequency).returns('') - expect(subject.email_digests).to eq(false) - end - end - context 'after_save' do before { subject.save } @@ -1223,4 +1205,50 @@ describe User do end end + context "when user preferences are overriden" do + + before do + SiteSetting.stubs(:default_email_digest_frequency).returns(1) # daily + SiteSetting.stubs(:default_email_private_messages).returns(false) + SiteSetting.stubs(:default_email_direct).returns(false) + SiteSetting.stubs(:default_email_mailing_list_mode).returns(true) + SiteSetting.stubs(:default_email_always).returns(true) + + SiteSetting.stubs(:default_other_new_topic_duration_minutes).returns(-1) # not viewed + SiteSetting.stubs(:default_other_auto_track_topics_after_msecs).returns(0) # immediately + SiteSetting.stubs(:default_other_external_links_in_new_tab).returns(true) + SiteSetting.stubs(:default_other_enable_quoting).returns(false) + SiteSetting.stubs(:default_other_dynamic_favicon).returns(true) + SiteSetting.stubs(:default_other_disable_jump_reply).returns(true) + SiteSetting.stubs(:default_other_edit_history_public).returns(true) + + SiteSetting.stubs(:default_categories_watching).returns("1") + SiteSetting.stubs(:default_categories_tracking).returns("2") + SiteSetting.stubs(:default_categories_muted).returns("3") + end + + it "has overriden preferences" do + user = Fabricate(:user) + + expect(user.digest_after_days).to eq(1) + expect(user.email_private_messages).to eq(false) + expect(user.email_direct).to eq(false) + expect(user.mailing_list_mode).to eq(true) + expect(user.email_always).to eq(true) + + expect(user.new_topic_duration_minutes).to eq(-1) + expect(user.auto_track_topics_after_msecs).to eq(0) + expect(user.external_links_in_new_tab).to eq(true) + expect(user.enable_quoting).to eq(false) + expect(user.dynamic_favicon).to eq(true) + expect(user.disable_jump_reply).to eq(true) + expect(user.edit_history_public).to eq(true) + + expect(CategoryUser.lookup(user, :watching).pluck(:category_id)).to eq([1]) + expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to eq([2]) + expect(CategoryUser.lookup(user, :muted).pluck(:category_id)).to eq([3]) + end + + end + end From 4dd03ad6febd57c50f482f4c443d535b4f9ae33b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 21 Aug 2015 14:39:55 -0400 Subject: [PATCH 197/237] FIX: Couldn't restrict search to a category --- app/assets/javascripts/discourse/controllers/search.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/search.js.es6 b/app/assets/javascripts/discourse/controllers/search.js.es6 index 553543646f..4c39f5dbca 100644 --- a/app/assets/javascripts/discourse/controllers/search.js.es6 +++ b/app/assets/javascripts/discourse/controllers/search.js.es6 @@ -41,7 +41,7 @@ export default Em.Controller.extend({ const searchContext = this.get('searchContext'); if (this.get('searchContextEnabled')) { - if (searchContext.id.toLowerCase() === this.get('currentUser.username_lower') && + if (searchContext.id.toString().toLowerCase() === this.get('currentUser.username_lower') && searchContext.type === "private_messages" ) { url += ' in:private'; From 6d4c07385f6b1afd0c390f87b3172a941bec3e1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 21 Aug 2015 21:06:47 +0200 Subject: [PATCH 198/237] FIX: smoke test :fired: --- .../components/category-group.js.es6 | 2 +- .../controllers/upload-selector.js.es6 | 21 +++++------------- .../templates/modal/upload_selector.hbs | 22 +++++++++---------- spec/phantom_js/smoke_test.js | 10 ++++----- 4 files changed, 21 insertions(+), 34 deletions(-) diff --git a/app/assets/javascripts/discourse/components/category-group.js.es6 b/app/assets/javascripts/discourse/components/category-group.js.es6 index e81fa5d1d3..887e4ad7b2 100644 --- a/app/assets/javascripts/discourse/components/category-group.js.es6 +++ b/app/assets/javascripts/discourse/components/category-group.js.es6 @@ -31,6 +31,6 @@ export default Ember.Component.extend({ return categoryBadgeHTML(category, {allowUncategorized: true}); } }); - }.on('didInsertElement'), + }.on('didInsertElement') }); diff --git a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 index 2857b2a5c8..f0a7d67fcd 100644 --- a/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 +++ b/app/assets/javascripts/discourse/controllers/upload-selector.js.es6 @@ -1,25 +1,14 @@ import ModalFunctionality from 'discourse/mixins/modal-functionality'; -import { setting } from 'discourse/lib/computed'; export default Ember.Controller.extend(ModalFunctionality, { - remote: Em.computed.not("local"), - local: false, showMore: false, - - _initialize: function() { - this.setProperties({ - local: this.get("allowLocal"), - showMore: false - }); - }.on('init'), - - maxSize: setting('max_attachment_size_kb'), - allowLocal: Em.computed.gt('maxSize', 0), + local: true, + remote: Ember.computed.not("local"), actions: { - useLocal: function() { this.setProperties({ local: true, showMore: false}); }, - useRemote: function() { this.set("local", false); }, - toggleShowMore: function() { this.toggleProperty("showMore"); } + useLocal() { this.setProperties({ local: true, showMore: false}); }, + useRemote() { this.set("local", false); }, + toggleShowMore() { this.toggleProperty("showMore"); } } }); diff --git a/app/assets/javascripts/discourse/templates/modal/upload_selector.hbs b/app/assets/javascripts/discourse/templates/modal/upload_selector.hbs index 4423fe5f72..254e02648b 100644 --- a/app/assets/javascripts/discourse/templates/modal/upload_selector.hbs +++ b/app/assets/javascripts/discourse/templates/modal/upload_selector.hbs @@ -1,16 +1,14 @@
    From 4d72cb2851dd584d0bdf9cdc22d78afd432ff00f Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 24 Aug 2015 18:16:45 +0800 Subject: [PATCH 218/237] FIX: Title popup tip not positioned correctly. --- app/assets/stylesheets/desktop/compose.scss | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/assets/stylesheets/desktop/compose.scss b/app/assets/stylesheets/desktop/compose.scss index 829387d9d1..9ba434f904 100644 --- a/app/assets/stylesheets/desktop/compose.scss +++ b/app/assets/stylesheets/desktop/compose.scss @@ -220,6 +220,7 @@ padding: 10px; min-width: 1280px; .form-element { + position: relative; display: inline-block; .select2-container { width: 400px; @@ -323,8 +324,8 @@ } .title-input .popup-tip { width: 300px; - left: -8px; - margin-top: 8px; + left: 0px; + top: -30px; } .category-input .popup-tip { width: 240px; From f40f73326992d310d88ef1db88754a0785cf4362 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 24 Aug 2015 11:37:24 -0400 Subject: [PATCH 219/237] FIX: The digests aren't always weekly --- config/locales/client.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index c00d476aff..3eddbd92de 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2076,7 +2076,7 @@ en: sent_test: "sent!" delivery_method: "Delivery Method" preview_digest: "Preview Digest" - preview_digest_desc: "Preview the content of the weekly digest emails sent to inactive users." + preview_digest_desc: "Preview the content of the digest emails sent to inactive users." refresh: "Refresh" format: "Format" html: "html" From 99edcddafb518f26c90cf4fc59ed90692033c0db Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 25 Aug 2015 00:33:25 +0530 Subject: [PATCH 220/237] FEATURE: show pending/redeemed invite count in tabs --- .../controllers/user-invited-show.js.es6 | 17 +++++++++++++++++ .../javascripts/discourse/models/invite.js.es6 | 5 +++++ .../discourse/routes/user-invited-show.js.es6 | 11 ++++++++--- .../discourse/templates/components/nav-item.hbs | 6 +++++- .../discourse/templates/user-invited-show.hbs | 4 ++-- app/controllers/users_controller.rb | 10 ++++++++++ app/models/invite.rb | 12 ++++++++++-- config/locales/client.en.yml | 2 ++ config/routes.rb | 1 + 9 files changed, 60 insertions(+), 8 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 index 781d9be9ab..f03d7d0d8f 100644 --- a/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-invited-show.js.es6 @@ -7,6 +7,7 @@ export default Ember.Controller.extend({ model: null, filter: null, totalInvites: null, + invitesCount: null, canLoadMore: true, invitesLoading: false, @@ -58,6 +59,22 @@ export default Ember.Controller.extend({ return this.get('totalInvites') > 9; }.property('totalInvites'), + pendingLabel: function() { + if (this.get('invitesCount.total') > 50) { + return I18n.t('user.invited.pending_tab_with_count', {count: this.get('invitesCount.pending')}); + } else { + return I18n.t('user.invited.pending_tab'); + } + }.property('invitesCount'), + + redeemedLabel: function() { + if (this.get('invitesCount.total') > 50) { + return I18n.t('user.invited.redeemed_tab_with_count', {count: this.get('invitesCount.redeemed')}); + } else { + return I18n.t('user.invited.redeemed_tab'); + } + }.property('invitesCount'), + actions: { rescind(invite) { diff --git a/app/assets/javascripts/discourse/models/invite.js.es6 b/app/assets/javascripts/discourse/models/invite.js.es6 index 8f9bcd0bde..49a116bda6 100644 --- a/app/assets/javascripts/discourse/models/invite.js.es6 +++ b/app/assets/javascripts/discourse/models/invite.js.es6 @@ -43,6 +43,11 @@ Invite.reopenClass({ return Em.Object.create(result); }); + }, + + 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)); } }); diff --git a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 index 48e6d415cb..c9465fa7d1 100644 --- a/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 +++ b/app/assets/javascripts/discourse/routes/user-invited-show.js.es6 @@ -4,8 +4,12 @@ import showModal from "discourse/lib/show-modal"; export default Discourse.Route.extend({ model(params) { - this.inviteFilter = params.filter; - return Invite.findInvitedBy(this.modelFor("user"), params.filter); + const self = this; + Invite.findInvitedCount(self.modelFor("user")).then(function (result) { + self.set('invitesCount', result); + }); + self.inviteFilter = params.filter; + return Invite.findInvitedBy(self.modelFor("user"), params.filter); }, afterModel(model) { @@ -20,7 +24,8 @@ export default Discourse.Route.extend({ user: this.controllerFor("user").get("model"), filter: this.inviteFilter, searchTerm: "", - totalInvites: model.invites.length + totalInvites: model.invites.length, + invitesCount: this.get('invitesCount') }); }, diff --git a/app/assets/javascripts/discourse/templates/components/nav-item.hbs b/app/assets/javascripts/discourse/templates/components/nav-item.hbs index ec8e0a1e75..96c49a6fc3 100644 --- a/app/assets/javascripts/discourse/templates/components/nav-item.hbs +++ b/app/assets/javascripts/discourse/templates/components/nav-item.hbs @@ -1,5 +1,9 @@ {{#if routeParam}} - {{#link-to route routeParam}}{{i18n label}}{{/link-to}} + {{#if i18nLabel}} + {{#link-to route routeParam}}{{i18nLabel}}{{/link-to}} + {{else}} + {{#link-to route routeParam}}{{i18n label}}{{/link-to}} + {{/if}} {{else}} {{#if route}} {{#link-to route}}{{i18n label}}{{/link-to}} diff --git a/app/assets/javascripts/discourse/templates/user-invited-show.hbs b/app/assets/javascripts/discourse/templates/user-invited-show.hbs index a13bb5860c..29c37bcb99 100644 --- a/app/assets/javascripts/discourse/templates/user-invited-show.hbs +++ b/app/assets/javascripts/discourse/templates/user-invited-show.hbs @@ -6,8 +6,8 @@
    diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 83902f434b..bb550483d3 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -181,6 +181,16 @@ class UsersController < ApplicationController can_see_invite_details: guardian.can_see_invite_details?(inviter) end + def invited_count + inviter = fetch_user_from_params + + pending_count = Invite.find_pending_invites_count(inviter) + redeemed_count = Invite.find_redeemed_invites_count(inviter) + + render json: {counts: { pending: pending_count, redeemed: redeemed_count, + total: (pending_count.to_i + redeemed_count.to_i) } } + end + def is_local_username users = params[:usernames] users = [params[:username]] if users.blank? diff --git a/app/models/invite.rb b/app/models/invite.rb index d4a509ab12..6929084fca 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -151,13 +151,13 @@ class Invite < ActiveRecord::Base group_ids end - def self.find_all_invites_from(inviter, offset=0) + def self.find_all_invites_from(inviter, offset=0, limit=SiteSetting.invites_per_page) Invite.where(invited_by_id: inviter.id) .includes(:user => :user_stat) .order('CASE WHEN invites.user_id IS NOT NULL THEN 0 ELSE 1 END', 'user_stats.time_read DESC', 'invites.redeemed_at DESC') - .limit(SiteSetting.invites_per_page) + .limit(limit) .offset(offset) .references('user_stats') end @@ -170,6 +170,14 @@ class Invite < ActiveRecord::Base find_all_invites_from(inviter, offset).where('invites.user_id IS NOT NULL').order('invites.redeemed_at DESC') end + def self.find_pending_invites_count(inviter) + find_all_invites_from(inviter, 0, nil).where('invites.user_id IS NULL').count + end + + def self.find_redeemed_invites_count(inviter) + find_all_invites_from(inviter, 0, nil).where('invites.user_id IS NOT NULL').count + end + def self.filter_by(email_or_username) if email_or_username where( diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 3eddbd92de..472d780b43 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -609,9 +609,11 @@ en: truncated: "Showing the first {{count}} invites." redeemed: "Redeemed Invites" redeemed_tab: "Redeemed" + redeemed_tab_with_count: "Redeemed ({{count}})" redeemed_at: "Redeemed" pending: "Pending Invites" pending_tab: "Pending" + pending_tab_with_count: "Pending ({{count}})" topics_entered: "Topics Viewed" posts_read_count: "Posts Read" expired: "This invite has expired." diff --git a/config/routes.rb b/config/routes.rb index 235f824325..c68223269f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -275,6 +275,7 @@ Discourse::Application.routes.draw do get "users/:username/staff-info" => "users#staff_info", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/invited" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT} + get "users/:username/invited_count" => "users#invited_count", constraints: {username: USERNAME_ROUTE_FORMAT} get "users/:username/invited/:filter" => "users#invited", constraints: {username: USERNAME_ROUTE_FORMAT} post "users/action/send_activation_email" => "users#send_activation_email" get "users/:username/activity" => "users#show", constraints: {username: USERNAME_ROUTE_FORMAT} From f2db4bfcf3ee6dc024ef1bee91acf66049753501 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 24 Aug 2015 16:29:58 -0400 Subject: [PATCH 221/237] FIX: Conflict in dialect method names broke code formatting Sometimes newlines were being stripped from code contents due to the table formatting using the same method name. In the future we will be rewriting dialects to prevent this. --- app/assets/javascripts/discourse/dialects/code_dialect.js | 8 ++++---- .../javascripts/discourse/dialects/table_dialect.js | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/dialects/code_dialect.js b/app/assets/javascripts/discourse/dialects/code_dialect.js index f6260d8318..e7322a4abe 100644 --- a/app/assets/javascripts/discourse/dialects/code_dialect.js +++ b/app/assets/javascripts/discourse/dialects/code_dialect.js @@ -21,7 +21,7 @@ if (Discourse.SiteSettings && Discourse.SiteSettings.highlighted_languages) { var textCodeClasses = ["text", "pre", "plain"]; -function flattenBlocks(blocks) { +function codeFlattenBlocks(blocks) { var result = ""; blocks.forEach(function(b) { result += b; @@ -42,9 +42,9 @@ Discourse.Dialect.replaceBlock({ } if (textCodeClasses.indexOf(matches[1]) !== -1) { - return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, flattenBlocks(blockContents) ]]]; + return ['p', ['pre', ['code', {'class': 'lang-nohighlight'}, codeFlattenBlocks(blockContents) ]]]; } else { - return ['p', ['pre', ['code', {'class': 'lang-' + klass}, flattenBlocks(blockContents) ]]]; + return ['p', ['pre', ['code', {'class': 'lang-' + klass}, codeFlattenBlocks(blockContents) ]]]; } } }); @@ -56,7 +56,7 @@ Discourse.Dialect.replaceBlock({ skipIfTradtionalLinebreaks: true, emitter: function(blockContents) { - return ['p', ['pre', flattenBlocks(blockContents)]]; + return ['p', ['pre', codeFlattenBlocks(blockContents)]]; } }); diff --git a/app/assets/javascripts/discourse/dialects/table_dialect.js b/app/assets/javascripts/discourse/dialects/table_dialect.js index ad1bb375df..da9b2c2fcf 100644 --- a/app/assets/javascripts/discourse/dialects/table_dialect.js +++ b/app/assets/javascripts/discourse/dialects/table_dialect.js @@ -1,4 +1,4 @@ -var flattenBlocks = function(blocks) { +var tableFlattenBlocks = function(blocks) { var result = ""; blocks.forEach(function(b) { result += b; @@ -19,7 +19,7 @@ var emitter = function(contents) { window.html4.ELEMENTS.th = 1; window.html4.ELEMENTS.tr = 1; } - return ['table', {"class": "md-table"}, flattenBlocks.apply(this, [contents])]; + return ['table', {"class": "md-table"}, tableFlattenBlocks.apply(this, [contents])]; }; var tableBlock = { From 9c882795c3197542b1ec669c327f5cac77e99a68 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 24 Aug 2015 16:58:24 -0400 Subject: [PATCH 222/237] FIX: Weird double escaping of `<` and `>` in quotes --- app/assets/javascripts/discourse/views/post.js.es6 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/views/post.js.es6 b/app/assets/javascripts/discourse/views/post.js.es6 index ad9963aabf..fba794c605 100644 --- a/app/assets/javascripts/discourse/views/post.js.es6 +++ b/app/assets/javascripts/discourse/views/post.js.es6 @@ -146,7 +146,7 @@ const PostView = Discourse.GroupedView.extend(Ember.Evented, { Discourse.ajax("/posts/by_number/" + topicId + "/" + postId).then(function (result) { // slightly double escape the cooked html to prevent jQuery from unescaping it - const escaped = result.cooked.replace("&", "&"); + const escaped = result.cooked.replace(/&[^gla]/, "&"); const parsed = $(escaped); parsed.replaceText(originalText, "" + originalText + ""); $blockQuote.showHtml(parsed, 'fast', finished); From d74d5c47ad06fc954bd687aae5dd7bd09d5e7cc2 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 25 Aug 2015 09:25:39 +1000 Subject: [PATCH 223/237] FIX: admin not getting updates for topics in secure groups (only where admin is missing explicit permissions) --- config/initializers/04-message_bus.rb | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/config/initializers/04-message_bus.rb b/config/initializers/04-message_bus.rb index b8d67a0964..00bf806d41 100644 --- a/config/initializers/04-message_bus.rb +++ b/config/initializers/04-message_bus.rb @@ -17,7 +17,12 @@ end MessageBus.group_ids_lookup do |env| user = CurrentUser.lookup_from_env(env) - user.groups.select('groups.id').map{|g| g.id} if user + if user && user.admin? + # special rule, admin is allowed access to all groups + Group.pluck(:id) + elsif user + user.groups.pluck('groups.id') + end end MessageBus.on_connect do |site_id| From 2c59ad3dd3af0e64441ec90af32e8bb730f028fd Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 25 Aug 2015 11:54:23 +1000 Subject: [PATCH 224/237] FIX: favicon update broken when favicon lived on a CDN --- app/assets/javascripts/discourse.js | 6 +++++- app/controllers/static_controller.rb | 28 +++++++++++++++++++++++-- config/initializers/06-mini_profiler.rb | 3 ++- config/locales/server.en.yml | 2 +- config/nginx.sample.conf | 2 +- config/routes.rb | 2 ++ 6 files changed, 37 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index e12c8243d1..4e2c45d639 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -57,7 +57,11 @@ window.Discourse = Ember.Application.createWithMixins(Discourse.Ajax, { faviconChanged: function() { if(Discourse.User.currentProp('dynamic_favicon')) { - new Favcount(Discourse.SiteSettings.favicon_url).set( + var url = Discourse.SiteSettings.favicon_url; + if (/^http/.test(url)) { + url = Discourse.getURL("/favicon/proxied?" + encodeURIComponent(url)); + } + new Favcount(url).set( this.get('notifyCount') ); } diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index b65f52b085..507c22f6f9 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -1,7 +1,10 @@ +require_dependency 'distributed_memoizer' +require_dependency 'file_helper' + class StaticController < ApplicationController skip_before_filter :check_xhr, :redirect_to_login_if_required - skip_before_filter :verify_authenticity_token, only: [:enter] + skip_before_filter :verify_authenticity_token, only: [:cdn_asset, :enter, :favicon] def show return redirect_to(path '/') if current_user && params[:id] == 'login' @@ -82,7 +85,28 @@ class StaticController < ApplicationController redirect_to destination end - skip_before_filter :verify_authenticity_token, only: [:cdn_asset] + # We need to be able to draw our favicon on a canvas + # and pull it off the canvas into a data uri + # This can work by ensuring people set all the right CORS + # settings in the CDN asset, BUT its annoying and error prone + # instead we cache the favicon in redis and serve it out real quick with + # a huge expiry, we also cache these assets in nginx so it bypassed if needed + def favicon + + data = DistributedMemoizer.memoize('favicon' + SiteSetting.favicon_url, 60*60*24) do + file = FileHelper.download(SiteSetting.favicon_url, 50.kilobytes, "favicon.png") + data = file.read + file.unlink + data + end + + expires_in 1.year, public: true + response.headers["Expires"] = 1.year.from_now.httpdate + response.headers["Content-Length"] = data.bytesize.to_s + response.headers["Last-Modified"] = Time.new('2000-01-01').httpdate + render text: data, content_type: "image/png" + end + def cdn_asset path = File.expand_path(Rails.root + "public/assets/" + params[:path]) diff --git a/config/initializers/06-mini_profiler.rb b/config/initializers/06-mini_profiler.rb index 2b0eedb68b..3726cb173b 100644 --- a/config/initializers/06-mini_profiler.rb +++ b/config/initializers/06-mini_profiler.rb @@ -37,7 +37,8 @@ if defined?(Rack::MiniProfiler) /^\/uploads/, /^\/javascripts\//, /^\/images\//, - /^\/stylesheets\// + /^\/stylesheets\//, + /^\/favicon\/proxied/ ] # For our app, let's just show mini profiler always, polling is chatty so nuke that diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 31391bf511..57f4a7ef4f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -807,7 +807,7 @@ en: logo_url: "The logo image at the top left of your site, should be a wide rectangle shape. If left blank site title text will be shown." digest_logo_url: "The alternate logo image used at the top of your site's email digest. Should be a wide rectangle shape. If left blank `logo_url` will be used." logo_small_url: "The small logo image at the top left of your site, should be a square shape, seen when scrolling down. If left blank a home glyph will be shown." - favicon_url: "A favicon for your site, see http://en.wikipedia.org/wiki/Favicon" + favicon_url: "A favicon for your site, see http://en.wikipedia.org/wiki/Favicon, to work correctly over a CDN it must be a png" mobile_logo_url: "The fixed position logo image used at the top left of your mobile site. Should be a square shape. If left blank, `logo_url` will be used. eg: http://example.com/uploads/default/logo.png" apple_touch_icon_url: "Icon used for Apple touch devices. Recommended size is 144px by 144px." diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf index 021e6ea3da..cd103e493c 100644 --- a/config/nginx.sample.conf +++ b/config/nginx.sample.conf @@ -173,7 +173,7 @@ server { # This big block is needed so we can selectively enable # acceleration for backups and avatars # see note about repetition above - location ~ ^/(letter_avatar|user_avatar|highlight-js|stylesheets) { + location ~ ^/(letter_avatar|user_avatar|highlight-js|stylesheets|favicon/proxied) { proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/config/routes.rb b/config/routes.rb index c68223269f..67bfa0f551 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -520,6 +520,8 @@ Discourse::Application.routes.draw do get "cdn_asset/:site/*path" => "static#cdn_asset", format: false + get "favicon/proxied" => "static#favicon", format: false + get "robots.txt" => "robots_txt#index" Discourse.filters.each do |filter| From 4e37bcc3e2d951205f487218fd1bdf5b97b1ac61 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 25 Aug 2015 12:05:15 +1000 Subject: [PATCH 225/237] Add extra safety --- app/controllers/static_controller.rb | 30 ++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 507c22f6f9..0247fe328b 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -93,18 +93,28 @@ class StaticController < ApplicationController # a huge expiry, we also cache these assets in nginx so it bypassed if needed def favicon - data = DistributedMemoizer.memoize('favicon' + SiteSetting.favicon_url, 60*60*24) do - file = FileHelper.download(SiteSetting.favicon_url, 50.kilobytes, "favicon.png") - data = file.read - file.unlink - data + data = DistributedMemoizer.memoize('favicon' + SiteSetting.favicon_url, 60*30) do + begin + file = FileHelper.download(SiteSetting.favicon_url, 50.kilobytes, "favicon.png") + data = file.read + file.unlink + data + rescue => e + Rails.logger.warn("Invalid favicon_url #{SiteSetting.favicon_url}: #{e}\n#{e.backtrace}") + "" + end + end + + if data.bytesize == 0 + render text: UserAvatarsController::DOT, content_type: "image/gif" + else + expires_in 1.year, public: true + response.headers["Expires"] = 1.year.from_now.httpdate + response.headers["Content-Length"] = data.bytesize.to_s + response.headers["Last-Modified"] = Time.new('2000-01-01').httpdate + render text: data, content_type: "image/png" end - expires_in 1.year, public: true - response.headers["Expires"] = 1.year.from_now.httpdate - response.headers["Content-Length"] = data.bytesize.to_s - response.headers["Last-Modified"] = Time.new('2000-01-01').httpdate - render text: data, content_type: "image/png" end From 00e59bdc620bd4a41929fac7a1110860bcb1e411 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 25 Aug 2015 15:40:50 +1000 Subject: [PATCH 226/237] FIX: display warning when user is tracking too many topics FEATURE: allow admins to bump up number of tracked topics if needed using max_tracked_new_unread --- .../discourse/controllers/discovery/topics.js.es6 | 4 ++++ .../javascripts/discourse/models/topic-tracking-state.js.es6 | 5 +++++ .../javascripts/discourse/templates/discovery/topics.hbs | 4 ++++ app/models/topic_tracking_state.rb | 2 +- config/locales/client.en.yml | 1 + config/locales/server.en.yml | 1 + config/site_settings.yml | 3 +++ 7 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 index ac534b9618..772c8e4c5b 100644 --- a/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 +++ b/app/assets/javascripts/discourse/controllers/discovery/topics.js.es6 @@ -95,6 +95,10 @@ const controllerOpts = { return this.get('model.filter') === 'new' && this.get('model.topics.length') > 0; }.property('model.filter', 'model.topics.length'), + tooManyTracked: function(){ + return Discourse.TopicTrackingState.current().tooManyTracked(); + }.property(), + showDismissAtTop: function() { return (this.isFilterPage(this.get('model.filter'), 'new') || this.isFilterPage(this.get('model.filter'), 'unread')) && diff --git a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 index 051a7b0328..cf5f890375 100644 --- a/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 +++ b/app/assets/javascripts/discourse/models/topic-tracking-state.js.es6 @@ -237,6 +237,10 @@ const TopicTrackingState = Discourse.Model.extend({ .length; }, + tooManyTracked() { + return this.initialStatesLength >= Discourse.SiteSettings.max_tracked_new_unread; + }, + resetNew() { const self = this; Object.keys(this.states).forEach(function (id) { @@ -308,6 +312,7 @@ TopicTrackingState.reopenClass({ instance = Discourse.TopicTrackingState.create({ messageBus, currentUser }); instance.loadStates(data); + instance.initialStatesLength = data && data.length; instance.establishChannels(); return instance; }, diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index f3e452a4ab..e95136eaca 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -1,3 +1,7 @@ +{{#if tooManyTracked}} +
    {{i18n 'topics.too_many_tracked'}}
    +{{/if}} + {{#if redirectedReason}}
    {{redirectedReason}}
    {{/if}} diff --git a/app/models/topic_tracking_state.rb b/app/models/topic_tracking_state.rb index 12370fa234..b6fae15187 100644 --- a/app/models/topic_tracking_state.rb +++ b/app/models/topic_tracking_state.rb @@ -166,7 +166,7 @@ SQL sql << " AND topics.id = :topic_id" end - sql << " ORDER BY topics.bumped_at DESC ) SELECT * FROM x LIMIT 500" + sql << " ORDER BY topics.bumped_at DESC ) SELECT * FROM x LIMIT #{SiteSetting.max_tracked_new_unread.to_i}" SqlBuilder.new(sql) .map_exec(TopicTrackingState, user_id: user_id, topic_id: topic_id) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 472d780b43..86c437496b 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -936,6 +936,7 @@ en: current_user: 'go to your user page' topics: + too_many_tracked: "Warning: you have too many tracked new and unread topics, clear some using \"Dismiss New\" or \"Dismiss Posts\"" bulk: reset_read: "Reset Read" delete: "Delete Topics" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 57f4a7ef4f..8ea0218b32 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -946,6 +946,7 @@ en: active_user_rate_limit_secs: "How frequently we update the 'last_seen_at' field, in seconds" verbose_localization: "Show extended localization tips in the UI" + max_tracked_new_unread: "Cap the total of new + unread topics per user, this protects database and client from very large payloads for some users tracking thousands of topics" previous_visit_timeout_hours: "How long a visit lasts before we consider it the 'previous' visit, in hours" rate_limit_create_topic: "After creating a topic, users must wait (n) seconds before creating another topic." diff --git a/config/site_settings.yml b/config/site_settings.yml index 7720cf8903..d9c16cb10a 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -748,6 +748,9 @@ developer: migrate_to_new_scheme: hidden: true default: false + max_tracked_new_unread: + default: 1000 + client: true embedding: feed_polling_enabled: From 7df62023c7d09850344f725140c9c5e7728ed20a Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 25 Aug 2015 13:26:29 +0530 Subject: [PATCH 227/237] Update Translations --- config/locales/client.ar.yml | 26 +++++++++++++++ config/locales/client.es.yml | 19 +++++++++++ config/locales/client.he.yml | 32 ++++++++++++++++++ config/locales/client.it.yml | 36 +++++++++++++++----- config/locales/client.pl_PL.yml | 4 +++ config/locales/client.pt.yml | 31 +++++++++++++++++- config/locales/client.ru.yml | 2 +- config/locales/client.zh_CN.yml | 4 +++ config/locales/server.ar.yml | 58 ++++++++++++++++++++++++++++++++- 9 files changed, 201 insertions(+), 11 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index 45b20c5cec..9e2923cac6 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -189,6 +189,21 @@ ar: autoclosed: enabled: 'أغلق %{when}' disabled: 'مفتوح %{when}' + closed: + enabled: 'مغلق %{when}' + disabled: 'مفتوح %{when}' + archived: + enabled: 'مؤرشف %{when}' + disabled: 'غير مؤرشف %{when}' + pinned: + enabled: 'مثبت %{when}' + disabled: 'غير مثبت %{when}' + pinned_globally: + enabled: 'مثبت عالمياً %{when}' + disabled: 'غير مثبت %{when}' + visible: + enabled: 'مدرج %{when}' + disabled: 'غير مدرج %{when}' topic_admin_menu: "عمليات المدير" emails_are_disabled: "جميع الرسائل الالكترونية تم تعطيلها من قبل المدير , لن يتم ارسال اي بريد الكتروني " edit: 'عدّل العنوان و التصنيف على هذا الموضوع' @@ -204,6 +219,7 @@ ar: admin_title: "المدير" flags_title: "بلاغات" show_more: "أعرض المزيد" + show_help: "خيارات" links: "روابط" links_lowercase: zero: "رابط" @@ -663,6 +679,7 @@ ar: search: "نوع البحث عن الدعوات" title: "دعوة" user: "المستخدمين المدعويين" + sent: "تم الإرسال" none: ".لم تقم بدعوة أي شخص إلى هنا حتى الأن" truncated: "اظهار اوائل {{count}} المدعويين" redeemed: "دعوات مستخدمة" @@ -1013,6 +1030,9 @@ ar: bookmarks: "لايوجد المزيد من المواضيع في المفضلة" search: "لايوجد نتائج بحث أخرى يمكن عرضها" topic: + unsubscribe: + stop_notifications: "ستستقبل الأن إشعارات أقل لـ{{title}}" + change_notification_state: "حالة إشعارك الحالي هي " filter_to: "{{post_count}} مشاركات/مشاركة في الموضوع" create: 'موضوع جديد' create_long: 'كتابة موضوع جديد' @@ -1130,8 +1150,10 @@ ar: title: "تحت المتابعة" description: "سيتم عرض عدد الردود جديدة لهذا الموضوع. سيتم إعلامك إذا ذكر أحد name@ أو ردود لك." regular: + title: "منتظم" description: ".سيتم إشعارك إذا ذكر أحد ما @اسمك أو رد لك" regular_pm: + title: "منتظم" description: "سيتم تنبيهك إذا قام احدٌ بالاشارة إلى حسابك @name أو الرد عليك." muted_pm: title: "كتم" @@ -1271,6 +1293,10 @@ ar: many: "الرجاء اختيار المالك الجديد لـ {{count}} مشاركة نُشرت بواسطة {{old_user}}." other: "الرجاء اختيار المالك الجديد لـ {{count}} مشاركة نُشرت بواسطة {{old_user}}." instructions_warn: "ملاحطة لن يتم نقل الاشعارت القديمة للمشاركة للمسخدم الجديد
    تحذير: اي بيانات تتعلق بالمشاركة هذه لن يتم نقلها للمستخدم الجديد. استعملها بحذر." + change_timestamp: + title: "تغيير الطابع الزمني" + action: "تغيير الطابع الزمني" + invalid_timestamp: "الطابع الزمني لا يمكن أن يكون في المستقبل." multi_select: select: 'تحديد' selected: 'محدد ({{count}})' diff --git a/config/locales/client.es.yml b/config/locales/client.es.yml index 2f0abd235a..f3d3ff7e41 100644 --- a/config/locales/client.es.yml +++ b/config/locales/client.es.yml @@ -1109,6 +1109,12 @@ es: one: "Por favor escoge el nuevo dueño del {{count}} post de {{old_user}}." other: "Por favor escoge el nuevo dueño de los {{count}} posts de {{old_user}}." instructions_warn: "Ten en cuenta que las notificaciones sobre este post no serán transferidas al nuevo usuario de forma retroactiva.
    Aviso: actualmente, los datos que no dependen del post son transferidos al nuevo usuario. Usar con precaución." + change_timestamp: + title: "Cambiar Timestamp" + action: "cambiar timestamp" + invalid_timestamp: "El Timestamp no puede ser futuro" + error: "Hubo un error cambiando el timestamp de este tema." + instructions: "Por favor, señecciona el nuevo timestamp del tema. Los posts en el tema serán actualizados para mantener la diferencia de tiempo." multi_select: select: 'seleccionar' selected: 'seleccionado ({{count}})' @@ -2297,6 +2303,19 @@ es: name: "Nombre" image: "Imagen" delete_confirm: "¿Estás seguro de querer eliminar el emoji :%{name}:?" + embedding: + get_started: "Si quieres insertar Discourse en otro sitio web, empieza por añadir su host." + confirm_delete: "¿Seguro que quieres borrar ese host?" + sample: "Usa el siguiente código HTML en tu sitio para crear e insertar temas. Reempalza REPLACE_ME con la URL canónica de la página donde quieres insertar." + title: "Insertado" + host: "Hosts Permitidos" + edit: "editar" + category: "Publicar a Categoría" + add_host: "Añadir Host" + settings: "Ajustes de Insertado" + feed_settings: "Ajustes de Feed" + feed_description: "Discourse podrá importar tu contenido de forma más fácil si proporcionas un feed RSS/ATOM de tu sitio." + crawling_settings: "Ajustes de Crawlers" permalink: title: "Enlaces permanentes" url: "URL" diff --git a/config/locales/client.he.yml b/config/locales/client.he.yml index 7559c02fd6..86bd748645 100644 --- a/config/locales/client.he.yml +++ b/config/locales/client.he.yml @@ -9,6 +9,7 @@ he: js: number: format: + separator: " ." delimiter: "," human: storage_units: @@ -109,6 +110,24 @@ he: email: 'שלח קישור בדוא"ל' action_codes: split_topic: "פצל נושא זה" + autoclosed: + enabled: 'סגר %{when}' + disabled: 'פתח %{when}' + closed: + enabled: 'סגר %{when}' + disabled: 'פתח %{when}' + archived: + enabled: 'עבר לארכיון %{when}' + disabled: 'הוצא מהארכיון %{when}' + pinned: + enabled: 'ננעץ %{when}' + disabled: 'נעיצה בוטלה %{when}' + pinned_globally: + enabled: 'ננעץ גלובלית %{when}' + disabled: 'נעיצה בוטלה %{when}' + visible: + enabled: 'נכנס לרשימה %{when}' + disabled: 'הוצא מהרשימה %{when}' topic_admin_menu: "פעולות ניהול לנושא" emails_are_disabled: "כל הדוא\"ל היוצא נוטרל באופן גורף על ידי מנהל אתר. שום הודעת דוא\"ל, מכל סוג שהוא, תשלח." edit: 'ערוך את הכותרת והקטגוריה של הנושא' @@ -124,6 +143,7 @@ he: admin_title: "ניהול" flags_title: "סימוני הודעה" show_more: "הראה עוד" + show_help: "אפשרויות" links: "קישורים" links_lowercase: one: "קישור" @@ -523,6 +543,7 @@ he: search: "הקלידו כדי לחפש הזמנות..." title: "הזמנות" user: "משתמש/ת שהוזמנו" + sent: "נשלח" none: "עוד לא הזמנת לכאן אף אחד." truncated: "מראה את {{count}} ההזמנות הראשונות." redeemed: "הזמנות נוצלו" @@ -865,6 +886,9 @@ he: bookmarks: "אין עוד סימניות לנושאים." search: "אין עוד תוצאות חיפוש" topic: + unsubscribe: + stop_notifications: "תקבלו פחות התראות עבור {{title}}" + change_notification_state: "מצב ההתראות הנוכחי שלך הוא" filter_to: "{{post_count}} הודעות בנושא" create: 'נושא חדש' create_long: 'יצירת נושא חדש' @@ -958,8 +982,10 @@ he: title: "רגיל+" description: "כמו רגיל, בנוסף מספר התגובות שלא נקראו יוצג לנושא זה. " regular: + title: "רגיל" description: "תקבלו התראה אם מישהו יזכיר את @שם_המשתמש/ת שלך או ישיב לפרסום שלך." regular_pm: + title: "רגיל" description: "תקבלו התראה אם מישהו יזכיר את @שם_המשתמש/ת שלך או ישיב לפרסום שלך." muted_pm: title: "מושתק" @@ -1083,6 +1109,12 @@ he: one: "אנא בחר את הבעלים החדש של ההודעות מאת {{old_user}}." other: "אנא בחר את הבעלים החדש של {{count}} ההודעות מאת {{old_user}}." instructions_warn: "יש לשים לב שהתראות על הודעה זו יועברו למשתמש החדש רטרואקטיבית.
    זהירות: כרגע, שום מידע תלוי-הודעה אינו מועבר למשתמש החדש. השתמשו בזהירות." + change_timestamp: + title: "שנה חותמת זמן" + action: "זנה חותמת זמן" + invalid_timestamp: "חותמת זמן לא יכולה להיות בעתיד" + error: "הייתה שגיאה בשינוי חותמת הזמן של הנושא" + instructions: "אנא בחרו את חותמת הזמן החדשה של הנושא. פרסומים בנושא יועדכנו לאותם הפרשי זמנים." multi_select: select: 'בחירה' selected: 'נבחרו ({{count}})' diff --git a/config/locales/client.it.yml b/config/locales/client.it.yml index deb1fd61ef..9eb2b0a83c 100644 --- a/config/locales/client.it.yml +++ b/config/locales/client.it.yml @@ -110,6 +110,24 @@ it: email: 'invia questo collegamento via email' action_codes: split_topic: "dividi questo argomento" + autoclosed: + enabled: 'chiuso %{when}' + disabled: 'aperto %{when}' + closed: + enabled: 'chiuso %{when}' + disabled: 'aperto %{when}' + archived: + enabled: 'archiviato %{when}' + disabled: 'dearchiviato %{when}' + pinned: + enabled: 'appuntato %{when}' + disabled: 'spuntato %{when}' + pinned_globally: + enabled: 'appuntato globalmente %{when}' + disabled: 'spuntato %{when}' + visible: + enabled: 'listato %{when}' + disabled: 'delistato %{when}' topic_admin_menu: "azioni amministrative sull'argomento" emails_are_disabled: "Tutte le email in uscita sono state disabilitate a livello globale da un amministratore. Non sarà inviata nessun tipo di notifica via email." edit: 'modifica titolo e categoria dell''argomento' @@ -125,6 +143,7 @@ it: admin_title: "Amministrazione" flags_title: "Segnalazioni" show_more: "Altro" + show_help: "opzioni" links: "Link" links_lowercase: one: "collegamento" @@ -354,13 +373,13 @@ it: trust_level: "Livello Esperienza" notifications: "Notifiche" desktop_notifications: - label: "Notifiche sul desktop" - not_supported: "Spiacente, le notifiche non sono supportate su questo browser." - perm_default: "Attiva le notifiche" - perm_denied_btn: "Permesso negato" - perm_denied_expl: "Hai negato il permesso per le notifiche. Usa il browser per abilitare le notifiche e premi il bottone quando hai finito. (Per il desktop: l'icona più a sinistra sulla barra degli indirizzi. Mobile: 'Informazioni sul sito'.)" - disable: "Disabilita le notifiche" - currently_enabled: "(attualmente attivata)" + label: "Notifiche Desktop" + not_supported: "Spiacenti, le notifiche non sono supportate su questo browser." + perm_default: "Attiva Notifiche" + perm_denied_btn: "Permesso Negato" + perm_denied_expl: "Hai negato il permesso per le notifiche. Usa il browser per abilitare le notifiche, poi premi il bottone quando hai finito. (Per il desktop: l'icona più a sinistra sulla barra degli indirizzi. Mobile: 'Informazioni sul sito'.)" + disable: "Disabilita Notifiche" + currently_enabled: "(attualmente attivate)" enable: "Abilita le notifiche" currently_disabled: "(attualmente disabilitata)" each_browser_note: "Nota: devi modificare questa impostazione per ogni browser che utilizzi." @@ -524,6 +543,7 @@ it: search: "digita per cercare inviti..." title: "Inviti" user: "Utente Invitato" + sent: "Spedito" none: "Non hai ancora invitato nessuno qui." truncated: "Mostro i primi {{count}} inviti." redeemed: "Inviti Accettati" @@ -1968,7 +1988,7 @@ it: not_found: "Spiacenti, questo nome utente non esiste nel sistema." id_not_found: "Spiacenti, nel nostro sistema non esiste questo id utente." active: "Attivo" - show_emails: "Mostre Email" + show_emails: "Mostra email" nav: new: "Nuovi" active: "Attivi" diff --git a/config/locales/client.pl_PL.yml b/config/locales/client.pl_PL.yml index 9a2826624a..dc3153a889 100644 --- a/config/locales/client.pl_PL.yml +++ b/config/locales/client.pl_PL.yml @@ -1156,6 +1156,10 @@ pl_PL: few: "Wybierz nowego właściciela dla {{count}} wpisów autorstwa {{old_user}}." other: "Wybierz nowego właściciela dla {{count}} wpisów autorstwa {{old_user}}." instructions_warn: "Przeszłe powiadomienia dla tego wpisu nie zostaną przypisane do nowego użytkownika.
    Uwaga: Aktualnie, żadne dane uzależnione od wpisu nie są przenoszone do nowego użytkownika. Zachowaj ostrożność." + change_timestamp: + title: "Zmień znacznik czasu" + action: "zmień znacznik czasu" + invalid_timestamp: "Znacznik czasu nie może wskazywać na przyszłość." multi_select: select: 'wybierz' selected: 'wybrano ({{count}})' diff --git a/config/locales/client.pt.yml b/config/locales/client.pt.yml index a53e51f80e..1b7869e38c 100644 --- a/config/locales/client.pt.yml +++ b/config/locales/client.pt.yml @@ -1109,6 +1109,12 @@ pt: one: "Por favor seleccione o novo titular da mensagem de {{old_user}}." other: "Por favor selecione o novo titular das {{count}} mensagens de {{old_user}}." instructions_warn: "Note que quaisquer notificações relacionadas com esta mensagem serão transferidas retroativamente para o novo utilizador.
    Aviso: Atualmente nenhum dado dependente da mensagem é transferido para o novo utilizador. Usar com cautela." + change_timestamp: + title: "Alterar Selo Temporal" + action: "alterar selo temporal" + invalid_timestamp: "O selo temporal não pode ser no futuro." + error: "Ocorreu um erro ao alterar o selo temporal do tópico." + instructions: "Por favor selecione o novo selo temporal do tópico. Mensagens no tópico serão atualizadas para terem a mesma diferença temporal." multi_select: select: 'selecionar' selected: '({{count}}) selecionados' @@ -1795,7 +1801,7 @@ pt: header: "Cabeçalho" top: "Topo" footer: "Rodapé" - embedded_css: "CSS embebido" + embedded_css: "CSS incorporado" head_tag: text: "" title: "HTML que será introduzido antes da tag " @@ -2297,6 +2303,29 @@ pt: name: "Nome" image: "Imagem" delete_confirm: "Tem a certeza que deseja eliminar o emoji :%{name}:?" + embedding: + get_started: "Se deseja incorporar o Discourse noutro sítio, comece por adicionar o seu servidor." + confirm_delete: "Tem certeza que deseja eliminar este servidor?" + sample: "Utilize o seguinte código HTML no seu sítio para criar e incorporar tópicos do discourse. Substitua REPLACE_ME pelo URL canónico da página onde está a incorporá-los." + title: "Incorporação" + host: "Servidores Permitidos" + edit: "editar" + category: "Mensagem para Categoria" + add_host: "Adicionar Servidor" + settings: "Configurações de Incorporação" + feed_settings: "Configurações do Feed" + feed_description: "Fornecer um fed RSS/ATOM para o seu sítio pode melhorar a habilidade do Discourse de importar o seu conteúdo." + crawling_settings: "Configurações de Rastreio" + crawling_description: "Quando o Discourse cria tópicos para as suas mensagens, se nenhum feed RSS/ATOM está presente o Discourse irá tentar analisar o seu conteúdo fora do seu HTML. Algumas vezes pode ser um desafio extrair o seu conteúdo, por isso temos a habilidade de especificar regras CSS para tornar a extração mais fácil. " + embed_by_username: "Nome de uilizador para criação do tópico" + embed_post_limit: "Número máximo de mensagens a incorporar" + embed_username_key_from_feed: "Chave para puxar o nome de utilizador discouse do feed" + embed_truncate: "Truncar as mensagens incorporadas" + embed_whitelist_selector: "Seletor CSS para elementos que são permitidos nas incorporações" + embed_blacklist_selector: "Seletor CSS para elementos que são removidos das incorporações" + feed_polling_enabled: "Importar mensagens através de RSS/ATOM" + feed_polling_url: "URL do feed RSS/ATOM para rastreio" + save: "Guardar Configurações de Incorporação" permalink: title: "Hiperligações Permanentes" url: "URL" diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 83a2c06df4..2523549da3 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -759,7 +759,7 @@ ru: title_too_long: "Заголовок не может быть длиннее {{max}} символов" post_missing: "Сообщение не может быть пустым" post_length: "Сообщение должно содержать минимум {{min}} символов" - try_like: 'А вы пробовали лайкнуть собощение с помощью кнопки ?' + try_like: 'А вы пробовали лайкнуть сообщение с помощью кнопки ?' category_missing: "Нужно выбрать раздел" save_edit: "Сохранить" reply_original: "Ответ в первоначальной теме" diff --git a/config/locales/client.zh_CN.yml b/config/locales/client.zh_CN.yml index 8a27f72d5c..0625eabc3e 100644 --- a/config/locales/client.zh_CN.yml +++ b/config/locales/client.zh_CN.yml @@ -830,6 +830,8 @@ zh_CN: bookmarks: "没有更多书签主题了。" search: "没有更多搜索结果。" topic: + unsubscribe: + change_notification_state: "您现在的提醒状态是" filter_to: "本主题中的 {{post_count}} 帖" create: '新主题' create_long: '创建一个新主题' @@ -2185,6 +2187,8 @@ zh_CN: name: "姓名" image: "图片" delete_confirm: "你确定要删除 :%{name}: emoji 么?" + embedding: + feed_polling_enabled: "通过RSS/ATOM导入帖子" permalink: title: "永久链接" url: "URL" diff --git a/config/locales/server.ar.yml b/config/locales/server.ar.yml index cc247b3007..f3ef3bdb6e 100644 --- a/config/locales/server.ar.yml +++ b/config/locales/server.ar.yml @@ -770,17 +770,30 @@ ar: min_private_message_title_length: "الحد الأدنى المسموح به لطول عنوان لرسالة في الأحرف" min_search_term_length: "الحد الأدنى الصالح لطول مصطلح في الأحرف" uncategorized_description: "الوصف للفئة غير المصنفة. اتركه فارغا لعدم الوصف." + unique_posts_mins: "كمية الدقائق التي يمكن للعضو قبلها إنشاء مشاركة مع نفس المحتوى مجددا" + title: "الاسم لهذا الموقع، كأنه يستخدم علامة العنوان." + max_image_width: "أقصى عرض للصور المصغرة في مشاركة" + max_image_height: "أقصى ارتفاع للصور المصغرة في مشاركة" + category_featured_topics: "عدد المواضيع المعروضة لكل فئة من صفحة الفئات /categories .بعد تغير هذه القيمة, قد تستغرق صفحة الفئات 15 دقيقة لتُحَدّث." + show_subcategory_list: "اعرض قائمة الفئات الفرعية بدلاً من قائمة المواضيع عند ادخال فئة ما." + fixed_category_positions: "إذا تم التحقق, ستتمكن من ترتيب الفئات على شكل المطلوب. وإذا لم يتم التحقق, ستسرد الفئات على حسب الفعالية." favicon_url: "إيقونة لموقعك , شاهد http://en.wikipedia.org/wiki/Favicon" summary_max_results: "الحد الأقصى للمشاركات العائدة بـ 'ملخص هذا الموضوع'" enable_private_messages: "يسمح مستوى الثقة 1 للمستخدمين بإنشاء والرد على الرسائل" allow_moderators_to_create_categories: "السماح للمشرفين إنشاء قسم جديد" + send_welcome_message: "أرسل لكل الأعضاء الجدد رسالة ترحيب مع دليل البدء السريع." + enable_badges: "تفعيل نظام الشارات." min_password_length: "أقل طول لكلمة المرور" block_common_passwords: "لا تسمح لكلمات المرور المسجلة في قائمة كلمات المرور الشائعةز" sso_url: "نقطة نهاية URL الدخول الموحد" sso_not_approved_url: "إعادة التوجيه لم توافق على حسابات SSO لهذا URL" enable_yahoo_logins: "تفعيل مصادقة ياهو" google_oauth2_client_id: "التسجيل بحسابك الشخصي في جوجل" + google_oauth2_client_secret: "العميل السري لتطبيق قوقل الخاص بك." enable_twitter_logins: "تفعيل مصادقة تويتر , يطلب : twitter_consumer_key و twitter_consumer_secret" + automatic_backups_enabled: "فعل النسخ الإحتياطي التلقائي بشكل متكرر كما هو محدد." + backup_frequency: "كمية النسخ الإحتياطية المتكررة التي أنشأناها، في اليوم." + verbose_localization: "شاهد تلميحات الترجمة الممتدة في واجهة المستخدم." max_likes_per_day: "أقصى عدد للإعجابات لكل عضو باليوم." max_flags_per_day: "أقصى عدد للإعلامات لكل عضو باليوم." max_bookmarks_per_day: "أقصى عدد للتفضيلات لكل عضو باليوم." @@ -789,15 +802,31 @@ ar: max_private_messages_per_day: "أقصى عدد لرسائل الأعضاء التي يمكن إنشائها باليوم." max_invites_per_day: "أقصى عدد للدعوات التي يمكن للعضو إرسالها باليوم." max_topic_invitations_per_day: "أقصى عدد لدعوات الموضوع التي يمكن للعضو إرسالها باليوم." + suggested_topics: "عدد المواضيع المواضيع المقترحة يظهر في أسفل الموضوع." + limit_suggested_to_category: "أظهر فقط المواضيع من الفئات الحالية في المواضيع المقترحة." + s3_access_key_id: "معرف مفتاح دخول امازون S3 سيستخدم لرفع الصور." + s3_secret_access_key: "مفتاح دخول أمازون السري S3 سيستخدم لرفع الصور." + s3_region: "اسم منطقة أمازون S3 سيستخدم لرفع الصور." + default_invitee_trust_level: "مستوى الثقة الإفتراضي (0-4) للأعضاء المدعوين." + default_trust_level: "مستوى الثقة الإفتراضي (0-4) للأعضاء المدعوين.تحذير! التغيرات ستضعك تحت خطر البريد الغير هام." + tl1_requires_topics_entered: "كمية المواضيع التي يجب على العضو الجديد دخولها قبل ترقيته لمستوى الثقة 1." + tl1_requires_read_posts: "كمية المشاركات التي يجب على العضو الجديد قرائتها قبل ترقيته لمستوى الثقة 1." + tl1_requires_time_spent_mins: "كمية الدقائق التي يجب على العضو الجديد قراءة المشاركات فيها قبل ترقيته لمستوى الثقة 1." + tl2_requires_topics_entered: "كمية المواضيع التي يجب على العضو دخولها قبل ترقيته لمستوى الثقة 2." + tl2_requires_read_posts: "كمية المشاركات التي يجب على العضو قرائتها قبل ترقيته لمستوى الثقة 2." + tl2_requires_time_spent_mins: "كمية الدقائق التي يجب على العضو قراءة المشاركات فيها قبل ترقيته لمستوى الثقة 2." + tl2_requires_days_visited: "كمية الأيام التي يجب على العضو زيارة الموقع فيها قبل ترقيته لمستوى الثقة 2." newuser_max_links: "عدد الروابط التي يمكن للمستخدم الجديد إضافتها للمشاركة." newuser_max_images: "عدد الصور التي يمكن للمستخدم الجديد إضافتها للمشاركة." newuser_max_attachments: "عدد المرفقات التي يمكن للمستخدم الجديد إضافتها للمشاركة." title_max_word_length: "الحد الأقصى المسموح لطول كلمة، بالأحرف، في عنوان الموضوع." + category_style: "النمط المرئي لفئة الشارات." + auto_respond_to_flag_actions: "تمكين الرد التلقائي عند التخلص من التبليغ." full_name_required: "الإسم الكامل مطلوب وهو ضروري لإكمال الحساب " enable_names: "عرض الاسم الكامل للعضو , بطاقة العضو , ورسائل البريد الالكتروني , تعطيل عرض الاسم في اي مكان " display_name_on_posts: "عرض الاسم الكامل للعضو على التعليقات بالاضافة الى @username." invites_per_page: "الدعوات الافتراضية تظهر في صفحة العضو" - embed_category: "فئة مواضيع مضمنة." + embed_truncate: "اقتطاع الوظائف المدمجة." embed_post_limit: "أقصى عدد للمشاركات المضمنة." delete_drafts_older_than_n_days: حذف المسودات مضى عليها أكثر من (ن) يوما. enable_emoji: "تمكين الرموز التعبيرية " @@ -858,13 +887,19 @@ ar: closed_disabled: "تم فتح هذا الموضوع الان ويسمح باضافة ردود جديدة " autoclosed_disabled: "تم فتح هذا الموضوع الان ويسمح باضافة ردود جديدة " autoclosed_disabled_lastpost: "تم فتح هذا الموضوع الان ويسمح باضافة ردود جديدة " + pinned_enabled: "هذا الموضوع الآن مقيد. سوف تظهر في الجزء العلوي من فئة لها حتى أنها متغيرة من الموظفين لكل فرد، أو بواسطة المستخدمين انفسهم." + pinned_disabled: "هذا الموضوع يتم الآن إزالة. لن يظهر في الجزء العلوي من هذه الفئة." login: incorrect_username_email_or_password: "اسم المستخدم او كلمة المرور او البريد الالكتروني غير صحيح" not_allowed_from_ip_address: "ﻻ يمكنك تسجيل الدخول كـ %{username} من هذا الـIP" + admin_not_allowed_from_ip_address: "لا يمكنك تسجيل الدخول كمدير من خلال هذا العنوان الرقمي - IP." suspended: "ﻻ يمكنك تسجيل الدخول حتى %{date}." + suspended_with_reason: "الحساب موقوف حتى %{date}: %{reason}" errors: "%{errors}" not_available: "غير متاح. جرّب %{suggestion} ؟" + something_already_taken: "حدث خطأ ما, ربما اسم المستخدم و البريد الالكتروني مسجل مسبقا, جرب رابط نسيان كلمة المرور." omniauth_error: "نأسف, هناك خطأ في تصريح حسابك. ربما لم تعطي تصريح؟" + omniauth_error_unknown: "حدث خطأ ما في معالجة دخولك، الرجاء إعادة المحاولة." new_registrations_disabled: "لا يُسمح بتسجيل حساب جديد بهذا الوقت " password_too_long: "الحد الاقصى لكلمة المرور 200 حرف" reserved_username: "اسم المستخدم ذلك غير مسموح." @@ -884,6 +919,8 @@ ar: blocked: "غير مسموح" ip_address: blocked: "لا يُسمح بتسجيل جديد من عنوان ip الخاص بك " + invite_mailer: + subject_template: "%{invitee_name} دعاك إلى '%{topic_title}' على %{site_domain_name}" invite_forum_mailer: subject_template: "%{invitee_name} قام بدعوتك للإنضمام إلى %{site_domain_name}" invite_password_instructions: @@ -920,6 +957,11 @@ ar: text_body_template: "الاستعادة نجحت" restore_failed: subject_template: "فشل استعادة" + bulk_invite_succeeded: + subject_template: "دعوة العضو الجماعية تمت بنجاح" + text_body_template: "ملف دعوة العضو الجماعية الخاص بك تمت معالجته، %{sent} دعوات مرسلة." + bulk_invite_failed: + subject_template: "دعوة العضو الجماعية تمت مع وجود أخطاء" csv_export_succeeded: subject_template: "اكتمل تصدير البيانات" csv_export_failed: @@ -977,6 +1019,14 @@ ar: subject_template: "العضو الجديد %{username} تم حجب مشاركاته بسبب تكرار الروابط " unblocked: subject_template: "الحساب مُفعل " + pending_users_reminder: + subject_template: + zero: "لا يوجد أعضاء تنتظر الموافقة." + one: "عضوا واحد ينتظر الموافقة." + two: "عضوان ينتظران الموافقة." + few: "%{count} أعضاء تنتظر الموافقة." + many: "%{count} أعضاء تنتظر الموافقة." + other: "%{count} أعضاء تنتظر الموافقة." download_remote_images_disabled: subject_template: "الغاء تفعيل تحميل الصور عن بعد " subject_re: "اعادة " @@ -985,6 +1035,10 @@ ar: previous_discussion: "الردود السابقة " unsubscribe: title: "غير مشترك " + visit_link_to_respond: "للرد، قم بزيارة %{base_url}%{url} في متصفحك." + posted_by: "مشاركة بواسطة %{username} على %{post_date}" + user_invited_to_private_message_pm: + subject_template: "[%{site_name}] %{username} دعاك لرسالة '%{topic_title}'" user_replied: subject_template: "[%{site_name}] %{topic_title}" text_body_template: | @@ -1068,6 +1122,7 @@ ar: edit_reason: "تحميل نسخ محلية للصور" unauthorized: "المعذرة، الملف الذي تحاول رفعه غير مسموح به (الامتدادات المسموح بها هي : %{authorized_extensions})." pasted_image_filename: "لصق الصورة " + file_missing: "عذرا، يجب عليك توفير ملف للرفع." attachments: too_large: "نعتذر، الملف الذي تريد رفعه كبير جداً ( الحد الاقصى {max_size_kb} كيلوبايت )" images: @@ -1079,6 +1134,7 @@ ar: suspended_not_pm: "عضو موقوف، ليست رسالة" seen_recently: "تم رؤية هذا المستخدم مسبقاً" notification_already_read: "تم قراءة هذه الاشعارات " + topic_nil: "مشاركة.موضوع صفر" post_deleted: "تم حذف الموضوع من قبل كاتبه " user_suspended: "تم تعليق حساب المستخدم" already_read: "المستخدم قرأ هذه المشاركة " From 124fc4daf7b3961a049f17693a99fa94b791d4d2 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 25 Aug 2015 18:32:37 +1000 Subject: [PATCH 228/237] PERF: the 500 cap was sane, keep it capped at 500 --- config/site_settings.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/site_settings.yml b/config/site_settings.yml index d9c16cb10a..60c8df990c 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -749,7 +749,7 @@ developer: hidden: true default: false max_tracked_new_unread: - default: 1000 + default: 500 client: true embedding: From 294669c856167d9e68faf8067b3a00a7f75da1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 25 Aug 2015 10:42:19 +0200 Subject: [PATCH 229/237] FIX qunit test runner for phantomjs 2.0 --- lib/autospec/run-qunit.js | 12 +++++--- lib/tasks/qunit.rake | 2 +- .../acceptance/category-edit-test.js.es6 | 6 ++-- .../acceptance/topic-anonymous-test.js.es6 | 6 ++-- vendor/assets/javascripts/run-qunit.js | 28 +++++++++++-------- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/lib/autospec/run-qunit.js b/lib/autospec/run-qunit.js index 59d1524586..2200aac623 100644 --- a/lib/autospec/run-qunit.js +++ b/lib/autospec/run-qunit.js @@ -2,13 +2,17 @@ /*global QUnit, ANSI */ // THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC -if (phantom.args.length !== 1) { +var system = require('system'), + args = system.args; + +args.shift(); + +if (args.length !== 1) { console.log("Usage: " + phantom.scriptName + " "); phantom.exit(1); } -var system = require('system'), - fs = require('fs'), +var fs = require('fs'), page = require('webpage').create(), QUNIT_RESULT = "./tmp/qunit_result"; @@ -34,7 +38,7 @@ page.start = new Date(); // -----------------------------------WARNING -------------------------------------- // calling "console.log" BELOW this line will go through the "page.onConsoleMessage" // -----------------------------------WARNING -------------------------------------- -page.open(phantom.args[0], function (status) { +page.open(args[0], function (status) { if (status !== "success") { console.log("\nNO NETWORK :(\n"); phantom.exit(1); diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake index a35b41a8e9..061ae69ffb 100644 --- a/lib/tasks/qunit.rake +++ b/lib/tasks/qunit.rake @@ -35,7 +35,7 @@ task "qunit:test" => :environment do begin success = true test_path = "#{Rails.root}/vendor/assets/javascripts" - cmd = "phantomjs #{test_path}/run-qunit.js \"http://localhost:#{port}/qunit\"" + cmd = "phantomjs #{test_path}/run-qunit.js http://localhost:#{port}/qunit" # wait for server to respond, will exception out on failure tries = 0 diff --git a/test/javascripts/acceptance/category-edit-test.js.es6 b/test/javascripts/acceptance/category-edit-test.js.es6 index 46d3a2125e..fc13ab5376 100644 --- a/test/javascripts/acceptance/category-edit-test.js.es6 +++ b/test/javascripts/acceptance/category-edit-test.js.es6 @@ -3,7 +3,7 @@ import { acceptance } from "helpers/qunit-helpers"; acceptance("Category Edit", { loggedIn: true }); -test("Can open the category modal", (assert) => { +test("Can open the category modal", assert => { visit("/c/bug"); click('.edit-category'); @@ -17,7 +17,7 @@ test("Can open the category modal", (assert) => { }); }); -test("Change the category color", (assert) => { +test("Change the category color", assert => { visit("/c/bug"); click('.edit-category'); @@ -29,7 +29,7 @@ test("Change the category color", (assert) => { }); }); -test("Change the topic template", (assert) => { +test("Change the topic template", assert => { visit("/c/bug"); click('.edit-category'); diff --git a/test/javascripts/acceptance/topic-anonymous-test.js.es6 b/test/javascripts/acceptance/topic-anonymous-test.js.es6 index 8e5ddf45c2..831968a21c 100644 --- a/test/javascripts/acceptance/topic-anonymous-test.js.es6 +++ b/test/javascripts/acceptance/topic-anonymous-test.js.es6 @@ -16,7 +16,7 @@ test("Enter without an id", () => { }); }); -test("Enter a 404 topic", (assert) => { +test("Enter a 404 topic", assert => { visit("/t/not-found/404"); andThen(() => { assert.ok(!exists("#topic"), "The topic was not rendered"); @@ -24,7 +24,7 @@ test("Enter a 404 topic", (assert) => { }); }); -test("Enter without access", (assert) => { +test("Enter without access", assert => { visit("/t/i-dont-have-access/403"); andThen(() => { assert.ok(!exists("#topic"), "The topic was not rendered"); @@ -32,7 +32,7 @@ test("Enter without access", (assert) => { }); }); -test("Enter with 500 errors", (assert) => { +test("Enter with 500 errors", assert => { visit("/t/throws-error/500"); andThen(() => { assert.ok(!exists("#topic"), "The topic was not rendered"); diff --git a/vendor/assets/javascripts/run-qunit.js b/vendor/assets/javascripts/run-qunit.js index b650fa926c..2d70729486 100644 --- a/vendor/assets/javascripts/run-qunit.js +++ b/vendor/assets/javascripts/run-qunit.js @@ -2,18 +2,21 @@ /*globals QUnit phantom*/ -var args = phantom.args; +var system = require("system"), + args = system.args; + +args.shift(); + if (args.length < 1 || args.length > 2) { console.log("Usage: " + phantom.scriptName + " "); phantom.exit(1); } -var system = require("system"), - page = require('webpage').create(); +var page = require("webpage").create(); page.onConsoleMessage = function(msg) { - if (msg.slice(0,8) === 'WARNING:') { return; } - if (msg.slice(0,6) === 'DEBUG:') { return; } + if (msg.slice(0, 8) === "WARNING:") { return; } + if (msg.slice(0, 6) === "DEBUG:") { return; } console.log(msg); }; @@ -24,14 +27,15 @@ page.onCallback = function (message) { }; page.open(args[0], function(status) { - if (status !== 'success') { + if (status !== "success") { console.error("Unable to access network"); phantom.exit(1); } else { page.evaluate(logQUnit); - var timeout = parseInt(args[1] || 120000, 10); - var start = Date.now(); + var timeout = parseInt(args[1] || 120000, 10), + start = Date.now(); + var interval = setInterval(function() { if (Date.now() > start + timeout) { console.error("Tests timed out"); @@ -50,7 +54,7 @@ page.open(args[0], function(status) { } } } - }, 500); + }, 250); } }); @@ -74,9 +78,9 @@ function logQUnit() { var msg = " Test Failed: " + context.name + assertionErrors.join(" "); testErrors.push(msg); assertionErrors = []; - window.callPhantom('F'); + window.callPhantom("F"); } else { - window.callPhantom('.'); + window.callPhantom("."); } }); @@ -96,7 +100,7 @@ function logQUnit() { }); QUnit.done(function(context) { - console.log('\n'); + console.log("\n"); if (moduleErrors.length > 0) { for (var idx=0; idx Date: Tue, 25 Aug 2015 22:25:37 +0800 Subject: [PATCH 230/237] Extract logic for censored-words so that it can be reused. --- .../discourse/dialects/censored_dialect.js | 23 +---------------- .../discourse/lib/censored-words.js | 25 +++++++++++++++++++ lib/pretty_text.rb | 1 + 3 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 app/assets/javascripts/discourse/lib/censored-words.js diff --git a/app/assets/javascripts/discourse/dialects/censored_dialect.js b/app/assets/javascripts/discourse/dialects/censored_dialect.js index 1cb38a9115..c4faa57481 100644 --- a/app/assets/javascripts/discourse/dialects/censored_dialect.js +++ b/app/assets/javascripts/discourse/dialects/censored_dialect.js @@ -1,24 +1,3 @@ -var censorRegexp; - Discourse.Dialect.addPreProcessor(function(text) { - var censored = Discourse.SiteSettings.censored_words; - if (censored && censored.length) { - if (!censorRegexp) { - var split = censored.split("|"); - if (split && split.length) { - censorRegexp = new RegExp("\\b(?:" + split.map(function (t) { return "(" + t.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ")"; }).join("|") + ")\\b", "ig"); - } - } - - if (censorRegexp) { - var m = censorRegexp.exec(text); - while (m && m[0]) { - var replacement = new Array(m[0].length+1).join('■'); - text = text.replace(new RegExp("\\b" + m[0] + "\\b", "ig"), replacement); - m = censorRegexp.exec(text); - } - - } - } - return text; + return Discourse.CensoredWords.censor(text); }); diff --git a/app/assets/javascripts/discourse/lib/censored-words.js b/app/assets/javascripts/discourse/lib/censored-words.js new file mode 100644 index 0000000000..840c3093b0 --- /dev/null +++ b/app/assets/javascripts/discourse/lib/censored-words.js @@ -0,0 +1,25 @@ +Discourse.CensoredWords = { + censor: function(text) { + var censorRegexp, + censored = Discourse.SiteSettings.censored_words; + + if (censored && censored.length) { + if (!censorRegexp) { + var split = censored.split("|"); + if (split && split.length) { + censorRegexp = new RegExp("\\b(?:" + split.map(function (t) { return "(" + t.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ")"; }).join("|") + ")\\b", "ig"); + } + } + if (censorRegexp) { + var m = censorRegexp.exec(text); + while (m && m[0]) { + var replacement = new Array(m[0].length+1).join('■'); + text = text.replace(new RegExp("\\b" + m[0] + "\\b", "ig"), replacement); + m = censorRegexp.exec(text); + } + + } + } + return text; + } +} diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 116b83b600..1b43959482 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -79,6 +79,7 @@ module PrettyText "vendor/assets/javascripts/better_markdown.js", "app/assets/javascripts/defer/html-sanitizer-bundle.js", "app/assets/javascripts/discourse/dialects/dialect.js", + "app/assets/javascripts/discourse/lib/censored-words.js", "app/assets/javascripts/discourse/lib/utilities.js", "app/assets/javascripts/discourse/lib/markdown.js", ) From 23b4d2d7d7f14ed1feefca47b0bcb90e8d8c4024 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 25 Aug 2015 22:35:29 +0800 Subject: [PATCH 231/237] FIX: Censored words filter not applied to title. --- app/assets/javascripts/discourse/models/topic.js.es6 | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index 88d99fa3bb..2c14d86beb 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -2,14 +2,17 @@ import { flushMap } from 'discourse/models/store'; import RestModel from 'discourse/models/rest'; import { propertyEqual } from 'discourse/lib/computed'; import { longDate } from 'discourse/lib/formatter'; +import computed from 'ember-addons/ember-computed-decorators'; const Topic = RestModel.extend({ message: null, errorLoading: false, - fancyTitle: function() { - return Discourse.Emoji.unescape(this.get('fancy_title')); - }.property("fancy_title"), + @computed('fancy_title') + fancyTitle(title) { + title = Discourse.Emoji.unescape(title); + return Discourse.CensoredWords.censor(title); + }, // returns createdAt if there's no bumped date bumpedAt: function() { From d15b698ac90cfca0a459bdda83e6731a4db8be48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 25 Aug 2015 17:28:20 +0200 Subject: [PATCH 232/237] FIX: body class from category not loadeing on topics on first load --- .../javascripts/discourse/views/navigation-category.js.es6 | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 app/assets/javascripts/discourse/views/navigation-category.js.es6 diff --git a/app/assets/javascripts/discourse/views/navigation-category.js.es6 b/app/assets/javascripts/discourse/views/navigation-category.js.es6 deleted file mode 100644 index f5e5c3d970..0000000000 --- a/app/assets/javascripts/discourse/views/navigation-category.js.es6 +++ /dev/null @@ -1,5 +0,0 @@ -import AddCategoryClass from 'discourse/mixins/add-category-class'; - -export default Em.View.extend(AddCategoryClass, { - categoryFullSlug: Em.computed.alias('controller.category.fullSlug') -}); From 324c6551d36e5f5d96eca55e83289362c2f99e97 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 25 Aug 2015 11:33:57 -0400 Subject: [PATCH 233/237] FIX: Both rules are important --- app/assets/stylesheets/vendor/select2.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/assets/stylesheets/vendor/select2.scss b/app/assets/stylesheets/vendor/select2.scss index 828c718517..87080ec2f8 100644 --- a/app/assets/stylesheets/vendor/select2.scss +++ b/app/assets/stylesheets/vendor/select2.scss @@ -596,7 +596,7 @@ html[dir="rtl"] .select2-search-choice-close { .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice .select2-arrow b { - background: asset-url('select2x2.png') no-repeat !important; + background: asset-url('select2x2.png') !important no-repeat !important; background-size: 60px 40px !important; } From d81327f4acded5fb8cc5b0f2ba43a4ca03f3250f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 25 Aug 2015 17:35:49 +0200 Subject: [PATCH 234/237] add vendor folder in sublime project --- discourse.sublime-project | 1 + 1 file changed, 1 insertion(+) diff --git a/discourse.sublime-project b/discourse.sublime-project index 6ce1a8c6ea..20854867ba 100644 --- a/discourse.sublime-project +++ b/discourse.sublime-project @@ -15,6 +15,7 @@ { "path": "plugins" }, { "path": "script" }, { "path": "spec" }, + { "path": "vendor" }, { "path": "test", "folder_exclude_patterns": ["fixtures"] } From e7e96eb8afc196d608e0c4a8231f10a3047c49f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 25 Aug 2015 17:36:24 +0200 Subject: [PATCH 235/237] fix the run-qunit scripts with backward compatible syntax --- lib/autospec/run-qunit.js | 12 ++++++++---- vendor/assets/javascripts/run-qunit.js | 7 +++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/autospec/run-qunit.js b/lib/autospec/run-qunit.js index 2200aac623..a4a851822e 100644 --- a/lib/autospec/run-qunit.js +++ b/lib/autospec/run-qunit.js @@ -1,11 +1,15 @@ /*jshint devel:true, phantom:true */ -/*global QUnit, ANSI */ +/*globals QUnit ANSI */ + // THIS FILE IS CALLED BY "qunit_runner.rb" IN AUTOSPEC -var system = require('system'), - args = system.args; +var system = require("system"), + args = phantom.args; -args.shift(); +if (args === undefined) { + args = system.args; + args.shift(); +} if (args.length !== 1) { console.log("Usage: " + phantom.scriptName + " "); diff --git a/vendor/assets/javascripts/run-qunit.js b/vendor/assets/javascripts/run-qunit.js index 2d70729486..f2bca49532 100644 --- a/vendor/assets/javascripts/run-qunit.js +++ b/vendor/assets/javascripts/run-qunit.js @@ -3,9 +3,12 @@ /*globals QUnit phantom*/ var system = require("system"), - args = system.args; + args = phantom.args; -args.shift(); +if (args === undefined) { + args = system.args; + args.shift(); +} if (args.length < 1 || args.length > 2) { console.log("Usage: " + phantom.scriptName + " "); From d5adf61458b92f2bbd0227172cf5258d1a1c3677 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 25 Aug 2015 17:44:52 +0200 Subject: [PATCH 236/237] Revert "FIX: body class from category not loadeing on topics on first load" This reverts commit d15b698ac90cfca0a459bdda83e6731a4db8be48. --- .../javascripts/discourse/views/navigation-category.js.es6 | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 app/assets/javascripts/discourse/views/navigation-category.js.es6 diff --git a/app/assets/javascripts/discourse/views/navigation-category.js.es6 b/app/assets/javascripts/discourse/views/navigation-category.js.es6 new file mode 100644 index 0000000000..f5e5c3d970 --- /dev/null +++ b/app/assets/javascripts/discourse/views/navigation-category.js.es6 @@ -0,0 +1,5 @@ +import AddCategoryClass from 'discourse/mixins/add-category-class'; + +export default Em.View.extend(AddCategoryClass, { + categoryFullSlug: Em.computed.alias('controller.category.fullSlug') +}); From fd28d4c9784af70431c3a69d95f12b3b127ef977 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 25 Aug 2015 15:06:01 -0400 Subject: [PATCH 237/237] Version bump to v1.4.0.beta10 --- lib/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.rb b/lib/version.rb index 93448c54ab..aadc4d2233 100644 --- a/lib/version.rb +++ b/lib/version.rb @@ -5,7 +5,7 @@ module Discourse MAJOR = 1 MINOR = 4 TINY = 0 - PRE = 'beta9' + PRE = 'beta10' STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') end
    " method="post"> + <%= csrf_tag if respond_to?(:csrf_tag) %>