From f59c11b4ab05fb374b9b33f30384d2cafc2f9e07 Mon Sep 17 00:00:00 2001 From: cpradio Date: Mon, 31 Oct 2016 17:27:57 -0400 Subject: [PATCH 001/266] FIX: Advanced Search to utilize Category Selector so it can distinguish uncategorized from no category selected --- .../components/search-advanced-options.js.es6 | 24 +++++++++---------- .../components/search-advanced-options.hbs | 2 +- .../acceptance/search-full-test.js.es6 | 10 ++++---- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 index b45f54b530..4d154491b5 100644 --- a/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 +++ b/app/assets/javascripts/discourse/components/search-advanced-options.js.es6 @@ -60,7 +60,7 @@ export default Em.Component.extend({ this.setProperties({ searchedTerms: { username: '', - category: null, + category: '', group: [], badge: [], tags: [], @@ -167,21 +167,21 @@ export default Em.Component.extend({ const userInput = Discourse.Category.findBySlug(subcategories[1], subcategories[0]); if ((!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id)) - this.set('searchedTerms.category', userInput.id); + this.set('searchedTerms.category', [userInput]); } else if (isNaN(subcategories)) { const userInput = Discourse.Category.findSingleBySlug(subcategories[0]); if ((!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id)) - this.set('searchedTerms.category', userInput.id); + this.set('searchedTerms.category', [userInput]); } else { const userInput = Discourse.Category.findById(subcategories[0]); if ((!existingInput && userInput) || (existingInput && userInput && existingInput.id !== userInput.id)) - this.set('searchedTerms.category', userInput.id); + this.set('searchedTerms.category', [userInput]); } } else - this.set('searchedTerms.category', null); + this.set('searchedTerms.category', ''); }, setSearchedTermValueForGroup() { @@ -278,16 +278,16 @@ export default Em.Component.extend({ @observes('searchedTerms.category') updateSearchTermForCategory() { const match = this.filterBlocks(REGEXP_CATEGORY_PREFIX); - const categoryFilter = Discourse.Category.findById(this.get('searchedTerms.category')); + const categoryFilter = this.get('searchedTerms.category'); let searchTerm = this.get('searchTerm') || ''; const slugCategoryMatches = (match.length !== 0) ? match[0].match(REGEXP_CATEGORY_SLUG) : null; const idCategoryMatches = (match.length !== 0) ? match[0].match(REGEXP_CATEGORY_ID) : null; - if (categoryFilter && categoryFilter.length !== 0) { - const id = categoryFilter.id; - const slug = categoryFilter.slug; - if (categoryFilter && categoryFilter.parentCategory) { - const parentSlug = categoryFilter.parentCategory.slug; + if (categoryFilter && categoryFilter[0]) { + const id = categoryFilter[0].id; + const slug = categoryFilter[0].slug; + if (categoryFilter[0].parentCategory) { + const parentSlug = categoryFilter[0].parentCategory.slug; if (slugCategoryMatches) searchTerm = searchTerm.replace(slugCategoryMatches[0], `#${parentSlug}:${slug}`); else if (idCategoryMatches) @@ -296,7 +296,7 @@ export default Em.Component.extend({ searchTerm += ` #${parentSlug}:${slug}`; this.set('searchTerm', searchTerm.trim()); - } else if (categoryFilter) { + } else { if (slugCategoryMatches) searchTerm = searchTerm.replace(slugCategoryMatches[0], `#${slug}`); else if (idCategoryMatches) diff --git a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs index 554a2d689c..21322e0f3d 100644 --- a/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs +++ b/app/assets/javascripts/discourse/templates/components/search-advanced-options.hbs @@ -13,7 +13,7 @@
- {{category-chooser value=searchedTerms.category}} + {{category-selector categories=searchedTerms.category single="true"}}
diff --git a/test/javascripts/acceptance/search-full-test.js.es6 b/test/javascripts/acceptance/search-full-test.js.es6 index 29d863912d..9804e6db07 100644 --- a/test/javascripts/acceptance/search-full-test.js.es6 +++ b/test/javascripts/acceptance/search-full-test.js.es6 @@ -76,7 +76,7 @@ test("validate population of advanced search", assert => { andThen(() => { assert.ok(exists('.search-advanced-options span:contains("admin")'), 'has "admin" pre-populated'); - assert.ok(exists('.search-advanced-options .category-combobox .select2-choice .select2-chosen:contains("bug")'), 'has "bug" pre-populated'); + assert.ok(exists('.search-advanced-options .badge-category:contains("bug")'), 'has "bug" pre-populated'); //assert.ok(exists('.search-advanced-options span:contains("moderators")'), 'has "moderators" pre-populated'); //assert.ok(exists('.search-advanced-options span:contains("Reader")'), 'has "Reader" pre-populated'); assert.ok(exists('.search-advanced-options .tag-chooser .tag-monkey'), 'has "monkey" pre-populated'); @@ -118,11 +118,13 @@ test("update category through advanced search ui", assert => { visit("/search"); fillIn('.search input.full-page-search', 'none'); click('.search-advanced-btn'); - selectDropdown('.search-advanced-options .category-combobox', 4); - click('.search-advanced-options'); // need to click off the combobox for the search-term to get updated + fillIn('.search-advanced-options .category-selector', 'faq'); + click('.search-advanced-options .category-selector'); + keyEvent('.search-advanced-options .category-selector', 'keydown', 8); + keyEvent('.search-advanced-options .category-selector', 'keydown', 9); andThen(() => { - assert.ok(exists('.search-advanced-options .category-combobox .select2-choice .select2-chosen:contains("faq")'), 'has "faq" populated'); + assert.ok(exists('.search-advanced-options .badge-category:contains("faq")'), 'has "faq" populated'); assert.equal(find('.search input.full-page-search').val(), "none #faq", 'has updated search term to "none #faq"'); }); }); From 764a572070aa065221a36c9fbb19ed3a9b483d09 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Wed, 2 Nov 2016 15:29:33 -0400 Subject: [PATCH 002/266] FIX: when subcategories with the same name exist, filtering by tags might use the wrong subcategory --- app/controllers/tags_controller.rb | 2 +- spec/controllers/tags_controller_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 43e6f8aee9..dad7ee9512 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -270,7 +270,7 @@ class TagsController < ::ApplicationController page: params[:page], topic_ids: param_to_integer_list(:topic_ids), exclude_category_ids: params[:exclude_category_ids], - category: params[:category], + category: @filter_on_category ? @filter_on_category.id : params[:category], order: params[:order], ascending: params[:ascending], min_posts: params[:min_posts], diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index cc0ad376cd..7c2e7aff06 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -68,6 +68,19 @@ describe TagsController do xhr :get, :show_latest, tag_id: tag.name, category: 'none', parent_category: category.slug expect(response).to be_success end + + it "can handle subcategories with the same name" do + category2 = Fabricate(:category) + subcategory2 = Fabricate(:category, + parent_category_id: category2.id, + name: subcategory.name, + slug: subcategory.slug + ) + t = Fabricate(:topic, category_id: subcategory2.id, tags: [other_tag]) + xhr :get, :show_latest, tag_id: other_tag.name, category: subcategory2.slug, parent_category: category2.slug + expect(response).to be_success + expect(assigns(:list).topics).to include(t) + end end end From 3255a215ec8b213aefe049137cf6f63c1ffd32b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 3 Nov 2016 01:46:46 +0100 Subject: [PATCH 003/266] bump onebox --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 48c8b97299..9361ef7f4e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -215,7 +215,7 @@ GEM omniauth-twitter (1.2.1) json (~> 1.3) omniauth-oauth (~> 1.1) - onebox (1.5.50) + onebox (1.5.60) htmlentities (~> 4.3.4) moneta (~> 0.8) multi_json (~> 1.11) From 17169b3037037103a54317c1f03b86f1359ed1a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 3 Nov 2016 02:26:12 +0100 Subject: [PATCH 004/266] FIX: issues with 'X-MSYS-API' custom header --- lib/email/sender.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/email/sender.rb b/lib/email/sender.rb index 6e58a70434..8cad0c8067 100644 --- a/lib/email/sender.rb +++ b/lib/email/sender.rb @@ -203,11 +203,16 @@ module Email end def merge_json_x_header(name, value) - mc_metadata = JSON.parse(@message.header[name].to_s) rescue nil - mc_metadata ||= {} - mc_metadata.merge!(value) + data = JSON.parse(@message.header[name].to_s) rescue nil + data ||= {} + data.merge!(value) + # /!\ @message.header is not a standard ruby hash. + # It can have multiple values attached to the same key... + # In order to remove all the previous keys, we have to "nil" it. + # But for "nil" to work, there must already be a key... + @message.header[name] = "" @message.header[name] = nil - @message.header[name] = mc_metadata.to_json + @message.header[name] = data.to_json end end From 4ce99c998b6b77fce4b2590c17b921b3b76faa38 Mon Sep 17 00:00:00 2001 From: rizka10 Date: Thu, 3 Nov 2016 11:11:17 +0200 Subject: [PATCH 005/266] Improve summing poll percentages to 100 My first JavaScript! I have little experience with C++ and even less with Java, but that was enough to figure out a way to solve the task. The solution is rather good, but there may be better ways. I'm going to start a pull request. Even if it gets rejected, an expert can use the idea. NOTE: The code needs some serious testing before potential merging. I did some testing and it worked, but don't trust in my skills. --- .../assets/javascripts/lib/even-round.js.es6 | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/plugins/poll/assets/javascripts/lib/even-round.js.es6 b/plugins/poll/assets/javascripts/lib/even-round.js.es6 index 0395f1f16a..5446c874c5 100644 --- a/plugins/poll/assets/javascripts/lib/even-round.js.es6 +++ b/plugins/poll/assets/javascripts/lib/even-round.js.es6 @@ -1,17 +1,29 @@ -// stolen from http://stackoverflow.com/a/13484088/11983 +// works as described on http://stackoverflow.com/a/13483710 function sumsUpTo100(percentages) { return percentages.map(p => Math.floor(p)).reduce((a, b) => a + b) === 100; } export default (percentages) => { - const sumOfDecimals = Math.ceil(percentages.map(a => a % 1).reduce((a, b) => a + b)); - // compensate error by adding 1 to the first n "non-zero" items - for (let i = 0, max = percentages.length; i < sumOfDecimals && i < max; i++) { - if (percentages[i] > 0) { - percentages[i] = ++percentages[i]; - // quit early when there is a rounding issue - if (sumsUpTo100(percentages)) break; + var decimals = percentages.map(a => a % 1); + const sumOfDecimals = Math.ceil(decimals.reduce((a, b) => a + b)); + // compensate error by adding 1 to n items with the greatest decimal part + for (let i = 0, max = decimals.length; i < sumOfDecimals && i < max; i++) { + // find the greatest item in the decimals array, set it to 0, + // and increase the corresponding item in the percentages array by 1 + let greatest = 0; + let index = 0; + for (let j=0; j < decimals.length; j++) { + if (decimals[j] > greatest) { + index = j; + greatest = decimals[j]; + } } + ++percentages[index]; + decimals[index] = 0; + // quit early when there is a rounding issue + if (sumsUpTo100(percentages)) break; } + return percentages.map(p => Math.floor(p)); }; + From b825a6bc7f0c3b93e84f433cc3ffba2f6b382a75 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 3 Nov 2016 17:17:45 +0800 Subject: [PATCH 006/266] Move timecop to test group. --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 9e5b727f9a..023ad9f5de 100644 --- a/Gemfile +++ b/Gemfile @@ -122,6 +122,7 @@ end group :test do gem 'fakeweb', '~> 1.3.0', require: false gem 'minitest', require: false + gem 'timecop' end group :test, :development do @@ -138,7 +139,6 @@ group :test, :development do gem 'rspec-rails', require: false gem 'shoulda', require: false gem 'simplecov', require: false - gem 'timecop' gem 'rspec-given' gem 'rspec-html-matchers' gem 'spork-rails' From 464f5099413de02ec59f9e9fda9aca4b128790d8 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Nov 2016 13:38:54 -0400 Subject: [PATCH 007/266] In Ember 2.0 `this.resource` is deprecated --- .../admin/routes/admin-route-map.js.es6 | 50 +++++++++---------- .../discourse/mapping-router.js.es6 | 4 +- .../discourse/routes/app-route-map.js.es6 | 32 ++++++------ 3 files changed, 43 insertions(+), 43 deletions(-) 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 69b51e5cab..9b1b2c72c2 100644 --- a/app/assets/javascripts/admin/routes/admin-route-map.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-route-map.js.es6 @@ -3,11 +3,11 @@ export default { map() { this.route('dashboard', { path: '/' }); - this.resource('adminSiteSettings', { path: '/site_settings' }, function() { - this.resource('adminSiteSettingsCategory', { path: 'category/:category_id'} ); + this.route('adminSiteSettings', { path: '/site_settings', resetNamespace: true }, function() { + this.route('adminSiteSettingsCategory', { path: 'category/:category_id', resetNamespace: true} ); }); - this.resource('adminEmail', { path: '/email'}, function() { + this.route('adminEmail', { path: '/email', resetNamespace: true}, function() { this.route('sent'); this.route('skipped'); this.route('bounced'); @@ -16,72 +16,72 @@ export default { this.route('previewDigest', { path: '/preview-digest' }); }); - this.resource('adminCustomize', { path: '/customize' } ,function() { + this.route('adminCustomize', { path: '/customize', resetNamespace: true } ,function() { this.route('colors'); - this.resource('adminCustomizeCssHtml', { path: 'css_html' }, function() { + this.route('adminCustomizeCssHtml', { path: 'css_html', resetNamespace: true }, function() { this.route('show', {path: '/:site_customization_id/:section'}); }); - this.resource('adminSiteText', { path: '/site_texts' }, function() { + this.route('adminSiteText', { path: '/site_texts', resetNamespace: true }, function() { this.route('edit', { path: '/:id' }); }); - this.resource('adminUserFields', { path: '/user_fields' }); - this.resource('adminEmojis', { path: '/emojis' }); - this.resource('adminPermalinks', { path: '/permalinks' }); - this.resource('adminEmbedding', { path: '/embedding' }); - this.resource('adminCustomizeEmailTemplates', { path: '/email_templates' }, function() { + this.route('adminUserFields', { path: '/user_fields', resetNamespace: true }); + this.route('adminEmojis', { path: '/emojis', resetNamespace: true }); + this.route('adminPermalinks', { path: '/permalinks', resetNamespace: true }); + this.route('adminEmbedding', { path: '/embedding', resetNamespace: true }); + this.route('adminCustomizeEmailTemplates', { path: '/email_templates', resetNamespace: true }, function() { this.route('edit', { path: '/:id' }); }); }); - this.resource('adminApi', { path: '/api' }, function() { - this.resource('adminApiKeys', { path: '/keys' }); + this.route('adminApi', { path: '/api', resetNamespace: true }, function() { + this.route('adminApiKeys', { path: '/keys', resetNamespace: true }); - this.resource('adminWebHooks', { path: '/web_hooks' }, function() { + this.route('adminWebHooks', { path: '/web_hooks', resetNamespace: true }, function() { this.route('show', { path: '/:web_hook_id' }); this.route('showEvents', { path: '/:web_hook_id/events' }); }); }); - this.resource('admin.backups', { path: '/backups' }, function() { + this.route('admin.backups', { path: '/backups', resetNamespace: true }, function() { this.route('logs'); }); - this.resource('adminReports', { path: '/reports/:type' }); + this.route('adminReports', { path: '/reports/:type', resetNamespace: true }); - this.resource('adminFlags', { path: '/flags' }, function() { + this.route('adminFlags', { path: '/flags', resetNamespace: true }, function() { this.route('list', { path: '/:filter' }); }); - this.resource('adminLogs', { path: '/logs' }, function() { + this.route('adminLogs', { path: '/logs', resetNamespace: true }, function() { this.route('staffActionLogs', { path: '/staff_action_logs' }); this.route('screenedEmails', { path: '/screened_emails' }); this.route('screenedIpAddresses', { path: '/screened_ip_addresses' }); this.route('screenedUrls', { path: '/screened_urls' }); }); - this.resource('adminGroups', { path: '/groups' }, function() { + this.route('adminGroups', { path: '/groups', resetNamespace: true }, function() { this.route('bulk'); this.route('bulkComplete', { path: 'bulk-complete' }); - this.resource('adminGroupsType', { path: '/:type' }, function() { - this.resource('adminGroup', { path: '/:name' }); + this.route('adminGroupsType', { path: '/:type', resetNamespace: true }, function() { + this.route('adminGroup', { path: '/:name', resetNamespace: true }); }); }); - this.resource('adminUsers', { path: '/users' }, function() { - this.resource('adminUser', { path: '/:user_id/:username' }, function() { + this.route('adminUsers', { path: '/users', resetNamespace: true }, function() { + this.route('adminUser', { path: '/:user_id/:username', resetNamespace: true }, function() { this.route('badges'); this.route('tl3Requirements', { path: '/tl3_requirements' }); }); - this.resource('adminUsersList', { path: '/list' }, function() { + this.route('adminUsersList', { path: '/list', resetNamespace: true }, function() { this.route('show', { path: '/:filter' }); }); }); - this.resource('adminBadges', { path: '/badges' }, function() { + this.route('adminBadges', { path: '/badges', resetNamespace: true }, function() { this.route('show', { path: '/:badge_id' }); }); } diff --git a/app/assets/javascripts/discourse/mapping-router.js.es6 b/app/assets/javascripts/discourse/mapping-router.js.es6 index 0223dfe41b..306910624c 100644 --- a/app/assets/javascripts/discourse/mapping-router.js.es6 +++ b/app/assets/javascripts/discourse/mapping-router.js.es6 @@ -61,14 +61,14 @@ export function mapRoutes() { // Apply other resources next. A little hacky but works! standalone.forEach(function(r) { - router.resource(r, {path: paths[r]}, function() { + router.route(r, {path: paths[r], resetNamespace: true}, function() { var res = this; resources[r].forEach(function(m) { m.call(res); }); var s = segments[r]; if (s) { var full = r + '.' + s; - res.resource(s, {path: paths[full]}, function() { + res.route(s, {path: paths[full], resetNamespace: true}, function() { var nestedRes = this; resources[full].forEach(function(m) { m.call(nestedRes); }); }); diff --git a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 index 002f998c35..76f3803c9f 100644 --- a/app/assets/javascripts/discourse/routes/app-route-map.js.es6 +++ b/app/assets/javascripts/discourse/routes/app-route-map.js.es6 @@ -4,17 +4,17 @@ export default function() { // Error page this.route('exception', { path: '/exception' }); - this.resource('about', { path: '/about' }); + this.route('about', { path: '/about', resetNamespace: true }); // Topic routes - this.resource('topic', { path: '/t/:slug/:id' }, function() { + this.route('topic', { path: '/t/:slug/:id', resetNamespace: true }, function() { this.route('fromParams', { path: '/' }); this.route('fromParamsNear', { path: '/:nearPost' }); }); - this.resource('topicBySlug', { path: '/t/:slug' }); + this.route('topicBySlug', { path: '/t/:slug', resetNamespace: true }); this.route('topicUnsubscribe', { path: '/t/:slug/:id/unsubscribe' }); - this.resource('discovery', { path: '/' }, function() { + this.route('discovery', { path: '/', resetNamespace: true }, function() { // top this.route('top'); this.route('topParentCategory', { path: '/c/:slug/l/top' }); @@ -50,7 +50,7 @@ export default function() { this.route(defaultHomepage(), { path: '/' }); }); - this.resource('group', { path: '/groups/:name' }, function() { + this.route('group', { path: '/groups/:name', resetNamespace: true }, function() { this.route('members'); this.route('posts'); this.route('topics'); @@ -59,10 +59,10 @@ export default function() { }); // User routes - this.resource('users'); - this.resource('user', { path: '/users/:username' }, function() { + this.route('users', { resetNamespace: true }); + this.route('user', { path: '/users/:username', resetNamespace: true }, function() { this.route('summary'); - this.resource('userActivity', { path: '/activity' }, function() { + this.route('userActivity', { path: '/activity', resetNamespace: true }, function() { this.route('topics'); this.route('replies'); this.route('likesGiven', {path: 'likes-given'}); @@ -70,7 +70,7 @@ export default function() { this.route('pending'); }); - this.resource('userNotifications', {path: '/notifications'}, function(){ + this.route('userNotifications', {path: '/notifications', resetNamespace: true}, function(){ this.route('responses'); this.route('likesReceived', { path: 'likes-received'}); this.route('mentions'); @@ -81,14 +81,14 @@ export default function() { this.route('flaggedPosts', { path: '/flagged-posts' }); this.route('deletedPosts', { path: '/deleted-posts' }); - this.resource('userPrivateMessages', { path: '/messages' }, function() { + this.route('userPrivateMessages', { path: '/messages', resetNamespace: true }, function() { this.route('sent'); this.route('archive'); this.route('group', { path: 'group/:name'}); this.route('groupArchive', { path: 'group/:name/archive'}); }); - this.resource('preferences', function() { + this.route('preferences', { resetNamespace: true }, function() { this.route('username'); this.route('email'); this.route('about', { path: '/about-me' }); @@ -96,7 +96,7 @@ export default function() { this.route('card-badge', { path: '/card-badge' }); }); - this.resource('userInvited', { path: '/invited' }, function() { + this.route('userInvited', { path: '/invited', resetNamespace: true }, function() { this.route('show', { path: '/:filter' }); }); @@ -114,15 +114,15 @@ export default function() { this.route('new-topic', {path: '/new-topic'}); this.route('new-message', {path: '/new-message'}); - this.resource('badges', function() { + this.route('badges', { resetNamespace: true }, function() { this.route('show', {path: '/:id/:slug'}); }); - this.resource('queued-posts', { path: '/queued-posts' }); + this.route('queued-posts', { path: '/queued-posts', resetNamespace: true }); this.route('full-page-search', {path: '/search'}); - this.resource('tags', function() { + this.route('tags', { resetNamespace: true }, function() { this.route('show', {path: '/:tag_id'}); this.route('showCategory', {path: '/c/:category/:tag_id'}); this.route('showParentCategory', {path: '/c/:parent_category/:category/:tag_id'}); @@ -135,7 +135,7 @@ export default function() { this.route('show', {path: 'intersection/:tag_id/*additional_tags'}); }); - this.resource('tagGroups', {path: '/tag_groups'}, function() { + this.route('tagGroups', {path: '/tag_groups', resetNamespace: true}, function() { this.route('show', {path: '/:id'}); }); } From 2d126cff8f138c55fcea6f6d24d13bf339592b03 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Nov 2016 15:37:26 -0400 Subject: [PATCH 008/266] `Ember.String.fmt` is deprecated --- .../javascripts/discourse/lib/computed.js.es6 | 31 ++++++------------- .../javascripts/ember-addons/fmt.js.es6 | 24 ++++++++++++++ app/assets/javascripts/main_include.js | 1 + 3 files changed, 35 insertions(+), 21 deletions(-) create mode 100644 app/assets/javascripts/ember-addons/fmt.js.es6 diff --git a/app/assets/javascripts/discourse/lib/computed.js.es6 b/app/assets/javascripts/discourse/lib/computed.js.es6 index 1fb7e745cb..02d38869fe 100644 --- a/app/assets/javascripts/discourse/lib/computed.js.es6 +++ b/app/assets/javascripts/discourse/lib/computed.js.es6 @@ -1,3 +1,4 @@ +import addonFmt from 'ember-addons/fmt'; /** Returns whether two properties are equal to each other. @@ -46,14 +47,10 @@ export function propertyLessThan(p1, p2) { @params {String} format the i18n format string @return {Function} computedProperty function **/ -export function i18n() { - const args = Array.prototype.slice.call(arguments, 0); +export function i18n(...args) { const format = args.pop(); - const computed = Em.computed(function() { - const self = this; - return I18n.t(format.fmt.apply(format, args.map(function (a) { - return self.get(a); - }))); + const computed = Ember.computed(function() { + return I18n.t(addonFmt(format, ...args.map(a => this.get(a)))); }); return computed.property.apply(computed, args); } @@ -67,14 +64,10 @@ export function i18n() { @params {String} format the format string @return {Function} computedProperty function **/ -export function fmt() { - const args = Array.prototype.slice.call(arguments, 0); +export function fmt(...args) { const format = args.pop(); - const computed = Em.computed(function() { - const self = this; - return format.fmt.apply(format, args.map(function (a) { - return self.get(a); - })); + const computed = Ember.computed(function() { + return addonFmt(format, ...args.map(a => this.get(a))); }); return computed.property.apply(computed, args); } @@ -88,14 +81,10 @@ export function fmt() { @params {String} format the format string for the URL @return {Function} computedProperty function returning a URL **/ -export function url() { - const args = Array.prototype.slice.call(arguments, 0); +export function url(...args) { const format = args.pop(); - const computed = Em.computed(function() { - const self = this; - return Discourse.getURL(format.fmt.apply(format, args.map(function (a) { - return self.get(a); - }))); + const computed = Ember.computed(function() { + return Discourse.getURL(addonFmt(format, ...args.map(a => this.get(a)))); }); return computed.property.apply(computed, args); } diff --git a/app/assets/javascripts/ember-addons/fmt.js.es6 b/app/assets/javascripts/ember-addons/fmt.js.es6 new file mode 100644 index 0000000000..191fbe20ac --- /dev/null +++ b/app/assets/javascripts/ember-addons/fmt.js.es6 @@ -0,0 +1,24 @@ +import Ember from 'ember'; + +const inspect = Ember.inspect; +const isArray = Ember.isArray; + +export default function(str, formats) { + let cachedFormats = formats; + + if (!isArray(cachedFormats) || arguments.length > 2) { + cachedFormats = new Array(arguments.length - 1); + + for (let i = 1, l = arguments.length; i < l; i++) { + cachedFormats[i - 1] = arguments[i]; + } + } + + // first, replace any ORDERED replacements. + let idx = 0; // the current index for non-numerical replacements + return str.replace(/%@([0-9]+)?/g, function(s, argIndex) { + argIndex = (argIndex) ? parseInt(argIndex, 10) - 1 : idx++; + s = cachedFormats[argIndex]; + return (s === null) ? '(null)' : (s === undefined) ? '' : inspect(s); + }); +} diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 440edeee65..96b3abcd9d 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -2,6 +2,7 @@ //= require ./ember-addons/decorator-alias //= require ./ember-addons/macro-alias //= require ./ember-addons/ember-computed-decorators +//= require ./ember-addons/fmt //= require_tree ./discourse-common //= require ./discourse //= require ./deprecated From 95c8d66fe09ecf3e77ca1ea70af1099740ea8616 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 2 Nov 2016 16:22:52 -0400 Subject: [PATCH 009/266] FIX: In Ember 2.0 you can't bind Query Parametrs like this --- .../controllers/admin-site-text-index.js.es6 | 35 ++++--------------- .../discourse/components/d-checkbox.js.es6 | 15 +++++--- .../templates/components/d-checkbox.hbs | 2 +- 3 files changed, 19 insertions(+), 33 deletions(-) diff --git a/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 b/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 index 0c770bd06b..bc2147e923 100644 --- a/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 +++ b/app/assets/javascripts/admin/controllers/admin-site-text-index.js.es6 @@ -1,38 +1,14 @@ -import { default as computed } from 'ember-addons/ember-computed-decorators'; - let lastSearch; +let lastOverridden; export default Ember.Controller.extend({ - _q: null, searching: false, siteTexts: null, preferred: false, - _overridden: null, queryParams: ['q', 'overridden'], - @computed - overridden: { - set(value) { - if (!value || value === "false") { value = false; } - this._overridden = value; - return value; - }, - get() { - return this._overridden; - } - }, - - @computed - q: { - set(value) { - if (Ember.isEmpty(value)) { value = null; } - this._q = value; - return value; - }, - get() { - return this._q; - } - }, + q: null, + overridden: null, _performSearch() { this.store.find('site-text', this.getProperties('q', 'overridden')).then(results => { @@ -46,11 +22,14 @@ export default Ember.Controller.extend({ }, search(overridden) { + this.set('overridden', overridden); + const q = this.get('q'); - if (q !== lastSearch || overridden) { + if (q !== lastSearch || overridden !== lastOverridden) { this.set('searching', true); Ember.run.debounce(this, this._performSearch, 400); lastSearch = q; + lastOverridden = overridden; } } } diff --git a/app/assets/javascripts/discourse/components/d-checkbox.js.es6 b/app/assets/javascripts/discourse/components/d-checkbox.js.es6 index bb15966c4d..5e50bf98ed 100644 --- a/app/assets/javascripts/discourse/components/d-checkbox.js.es6 +++ b/app/assets/javascripts/discourse/components/d-checkbox.js.es6 @@ -3,16 +3,23 @@ import { on } from "ember-addons/ember-computed-decorators"; export default Ember.Component.extend({ tagName: 'label', - @on('didInsertElement') - _watchChanges() { + didInsertElement() { + this._super(); + + const checked = this.get('checked'); + if (checked && checked !== "false") { + this.$('input').prop('checked', true); + } + // In Ember 13.3 we can use action on the checkbox `{{input}}` but not in 1.11 this.$('input').on('click.d-checkbox', () => { - Ember.run.scheduleOnce('afterRender', () => this.sendAction('change', true)); + Ember.run.scheduleOnce('afterRender', () => this.sendAction('change', this.$('input').prop('checked'))); }); }, @on('willDestroyElement') - _stopWatching() { + willDestroyElement() { + this._super(); this.$('input').off('click.d-checkbox'); } }); diff --git a/app/assets/javascripts/discourse/templates/components/d-checkbox.hbs b/app/assets/javascripts/discourse/templates/components/d-checkbox.hbs index b4bbae64a2..f39a68e79c 100644 --- a/app/assets/javascripts/discourse/templates/components/d-checkbox.hbs +++ b/app/assets/javascripts/discourse/templates/components/d-checkbox.hbs @@ -1,2 +1,2 @@ -{{input type="checkbox" checked=checked}} + {{i18n label}} From beac81d0ee67e08178d290eb1152611aa02df424 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Nov 2016 11:52:30 -0400 Subject: [PATCH 010/266] Support both `_actions` and `actions` for delegating --- .../components/topic-footer-buttons.js.es6 | 13 +++---------- .../discourse/mixins/delegated-actions.js.es6 | 16 ++++++++++++++++ .../javascripts/discourse/widgets/widget.js.es6 | 9 +++++++-- app/assets/javascripts/main_include.js | 1 + 4 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/discourse/mixins/delegated-actions.js.es6 diff --git a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 index d9aad34dcc..aeb9db8baa 100644 --- a/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-footer-buttons.js.es6 @@ -1,6 +1,7 @@ import computed from 'ember-addons/ember-computed-decorators'; +import DelegatedActions from 'discourse/mixins/delegated-actions'; -export default Ember.Component.extend({ +export default Ember.Component.extend(DelegatedActions, { elementId: 'topic-footer-buttons', // Allow us to extend it @@ -8,15 +9,7 @@ export default Ember.Component.extend({ init() { this._super(); - - this._actions = this._actions || {}; - - (this.get('topicDelegated') || []).forEach(m => { - this._actions[m] = function() { - this.sendAction(m); - }; - this.set(m, m); - }); + this.delegateAll(this.get('topicDelegated')); }, @computed('topic.details.can_invite_to') diff --git a/app/assets/javascripts/discourse/mixins/delegated-actions.js.es6 b/app/assets/javascripts/discourse/mixins/delegated-actions.js.es6 new file mode 100644 index 0000000000..8940c1d49b --- /dev/null +++ b/app/assets/javascripts/discourse/mixins/delegated-actions.js.es6 @@ -0,0 +1,16 @@ + +export const TARGET_NAME = (Ember.VERSION[0] === "2") ? 'actions' : '_actions'; + +export default Ember.Mixin.create({ + + delegateAll(actionNames) { + actionNames = actionNames || []; + + this[TARGET_NAME] = this[TARGET_NAME] || {}; + + actionNames.forEach(m => { + this[TARGET_NAME][m] = function() { this.sendAction(m); }; + this.set(m, m); + }); + } +}); diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index bef20bf04e..851ad09bd9 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -1,6 +1,11 @@ -import { WidgetClickHook, WidgetClickOutsideHook, WidgetKeyUpHook, WidgetKeyDownHook, WidgetDragHook } from 'discourse/widgets/hooks'; +import { WidgetClickHook, + WidgetClickOutsideHook, + WidgetKeyUpHook, + WidgetKeyDownHook, + WidgetDragHook } from 'discourse/widgets/hooks'; import { h } from 'virtual-dom'; import DecoratorHelper from 'discourse/widgets/decorator-helper'; +import { TARGET_NAME } from 'discourse/mixins/delegated-actions'; function emptyContent() { } @@ -266,7 +271,7 @@ export default class Widget { if (target) { // TODO: Use ember closure actions - const actions = target._actions || target.actionHooks || {}; + const actions = target[TARGET_NAME] || target.actionHooks || {}; const method = actions[actionName]; if (method) { promise = method.call(target, param); diff --git a/app/assets/javascripts/main_include.js b/app/assets/javascripts/main_include.js index 96b3abcd9d..c18248dffb 100644 --- a/app/assets/javascripts/main_include.js +++ b/app/assets/javascripts/main_include.js @@ -25,6 +25,7 @@ //= require ./discourse/lib/formatter //= require ./discourse/lib/eyeline //= require ./discourse/mixins/scrolling +//= require ./discourse/mixins/scrolling //= require ./discourse/models/model //= require ./discourse/models/rest //= require ./discourse/models/badge-grouping From a9d7569ddad9adac3b1f35fe2e6870451a3e4777 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Nov 2016 13:09:43 -0400 Subject: [PATCH 011/266] Replace computed properties for topic query params --- .../components/toggle-deleted.js.es6 | 12 ---- .../discourse/controllers/topic.js.es6 | 58 +++++-------------- .../discourse/controllers/user-card.js.es6 | 4 +- .../discourse/models/post-stream.js.es6 | 12 +--- .../javascripts/discourse/models/topic.js.es6 | 1 - .../javascripts/discourse/routes/topic.js.es6 | 2 - .../templates/components/toggle-deleted.hbs | 7 --- .../models/post-stream-test.js.es6 | 15 +---- 8 files changed, 21 insertions(+), 90 deletions(-) delete mode 100644 app/assets/javascripts/discourse/components/toggle-deleted.js.es6 delete mode 100644 app/assets/javascripts/discourse/templates/components/toggle-deleted.hbs diff --git a/app/assets/javascripts/discourse/components/toggle-deleted.js.es6 b/app/assets/javascripts/discourse/components/toggle-deleted.js.es6 deleted file mode 100644 index 72ad47d004..0000000000 --- a/app/assets/javascripts/discourse/components/toggle-deleted.js.es6 +++ /dev/null @@ -1,12 +0,0 @@ -export default Ember.Component.extend({ - layoutName: 'components/toggle-deleted', - tagName: 'section', - classNames: ['information'], - postStream: Em.computed.alias('topic.postStream'), - - actions: { - toggleDeleted: function() { - this.get('postStream').toggleDeleted(); - } - } -}); diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 787196c4a0..b64e00a3c1 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -20,7 +20,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { editingTopic: false, selectedPosts: null, selectedReplies: null, - queryParams: ['filter', 'username_filters', 'show_deleted'], + queryParams: ['filter', 'username_filters'], loadedAllPosts: Ember.computed.or('model.postStream.loadedAllPosts', 'model.postStream.loadingLastPost'), enteredAt: null, enteredIndex: null, @@ -29,6 +29,9 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { _progressIndex: null, hasScrolled: null, + username_filters: null, + filter: null, + topicDelegated: [ 'toggleMultiSelect', 'deleteTopic', @@ -53,6 +56,11 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { 'showFlagTopic' ], + updateQueryParams() { + const postStream = this.get('model.postStream'); + this.setProperties(postStream.get('streamFilters')); + }, + _titleChanged: function() { const title = this.get('model.title'); if (!Ember.isEmpty(title)) { @@ -79,32 +87,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { return this.capabilities.isAndroid && loading; }, - @computed('model.postStream.summary') - show_deleted: { - set(value) { - const postStream = this.get('model.postStream'); - if (!postStream) { return; } - postStream.set('show_deleted', value); - return postStream.get('show_deleted') ? true : undefined; - }, - get() { - return this.get('postStream.show_deleted') ? true : undefined; - } - }, - - @computed('model.postStream.summary') - filter: { - set(value) { - const postStream = this.get('model.postStream'); - if (!postStream) { return; } - postStream.set('summary', value === "summary"); - return postStream.get('summary') ? "summary" : undefined; - }, - get() { - return this.get('postStream.summary') ? "summary" : undefined; - } - }, - @computed('model', 'topicTrackingState.messageCount') browseMoreMessage(model) { @@ -157,19 +139,6 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { I18n.t("suggested_topics.title"); }, - @computed('model.postStream.streamFilters.username_filters') - username_filters: { - set(value) { - const postStream = this.get('model.postStream'); - if (!postStream) { return; } - postStream.set('streamFilters.username_filters', value); - return postStream.get('streamFilters.username_filters'); - }, - get() { - return this.get('postStream.streamFilters.username_filters'); - } - }, - _clearSelected: function() { this.set('selectedPosts', []); this.set('selectedReplies', []); @@ -259,7 +228,9 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { }, toggleSummary() { - return this.get('model.postStream').toggleSummary(); + return this.get('model.postStream').toggleSummary().then(() => { + this.updateQueryParams(); + }); }, removeAllowedUser(user) { @@ -464,7 +435,10 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { }, toggleParticipant(user) { - this.get('model.postStream').toggleParticipant(Em.get(user, 'username')); + const postStream = this.get('model.postStream'); + postStream.toggleParticipant(Ember.get(user, 'username')).then(() => { + this.updateQueryParams(); + }); }, editTopic() { diff --git a/app/assets/javascripts/discourse/controllers/user-card.js.es6 b/app/assets/javascripts/discourse/controllers/user-card.js.es6 index f7d7da11ad..985d977216 100644 --- a/app/assets/javascripts/discourse/controllers/user-card.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-card.js.es6 @@ -130,8 +130,8 @@ export default Ember.Controller.extend({ actions: { togglePosts(user) { - const postStream = this.get('postStream'); - postStream.toggleParticipant(user.get('username')); + const topicController = this.get('topic'); + topicController.send('toggleParticipant', user); this.close(); }, diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 2473b6660b..2ce7e1076d 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -80,11 +80,10 @@ export default RestModel.extend({ Returns a JS Object of current stream filter options. It should match the query params for the stream. **/ - @computed('summary', 'show_deleted', 'userFilters.[]') - streamFilters(summary, showDeleted) { + @computed('summary', 'userFilters.[]') + streamFilters(summary) { const result = {}; if (summary) { result.filter = "summary"; } - if (showDeleted) { result.show_deleted = true; } const userFilters = this.get('userFilters'); if (!Ember.isEmpty(userFilters)) { @@ -141,7 +140,6 @@ export default RestModel.extend({ cancelFilter() { this.set('summary', false); - this.set('show_deleted', false); this.get('userFilters').clear(); }, @@ -156,11 +154,6 @@ export default RestModel.extend({ }); }, - toggleDeleted() { - this.toggleProperty('show_deleted'); - return this.refresh(); - }, - jumpToSecondVisible() { const posts = this.get('posts'); if (posts.length > 1) { @@ -173,7 +166,6 @@ export default RestModel.extend({ toggleParticipant(username) { const userFilters = this.get('userFilters'); this.set('summary', false); - this.set('show_deleted', true); let jump = false; if (userFilters.contains(username)) { diff --git a/app/assets/javascripts/discourse/models/topic.js.es6 b/app/assets/javascripts/discourse/models/topic.js.es6 index e59c60029d..139b2ddcf7 100644 --- a/app/assets/javascripts/discourse/models/topic.js.es6 +++ b/app/assets/javascripts/discourse/models/topic.js.es6 @@ -556,7 +556,6 @@ Topic.reopenClass({ opts.userFilters.forEach(function(username) { data.username_filters.push(username); }); - data.show_deleted = true; } // Add the summary of filter if we have it diff --git a/app/assets/javascripts/discourse/routes/topic.js.es6 b/app/assets/javascripts/discourse/routes/topic.js.es6 index 0ee442dde5..989d100432 100644 --- a/app/assets/javascripts/discourse/routes/topic.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic.js.es6 @@ -14,7 +14,6 @@ const TopicRoute = Discourse.Route.extend({ queryParams: { filter: { replace: true }, username_filters: { replace: true }, - show_deleted: { replace: true } }, titleToken() { @@ -140,7 +139,6 @@ const TopicRoute = Discourse.Route.extend({ setupParams(topic, params) { const postStream = topic.get('postStream'); postStream.set('summary', Em.get(params, 'filter') === 'summary'); - postStream.set('show_deleted', !!Em.get(params, 'show_deleted')); const usernames = Em.get(params, 'username_filters'), userFilters = postStream.get('userFilters'); diff --git a/app/assets/javascripts/discourse/templates/components/toggle-deleted.hbs b/app/assets/javascripts/discourse/templates/components/toggle-deleted.hbs deleted file mode 100644 index 5475461580..0000000000 --- a/app/assets/javascripts/discourse/templates/components/toggle-deleted.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#if postStream.show_deleted}} -

{{i18n 'deleted_filter.disabled_description'}}

- -{{else}} -

{{i18n 'deleted_filter.enabled_description'}}

- -{{/if}} diff --git a/test/javascripts/models/post-stream-test.js.es6 b/test/javascripts/models/post-stream-test.js.es6 index 7fa49fea3c..5f825d9fbc 100644 --- a/test/javascripts/models/post-stream-test.js.es6 +++ b/test/javascripts/models/post-stream-test.js.es6 @@ -190,20 +190,7 @@ test("streamFilters", function() { postStream.toggleParticipant(participant.username); deepEqual(postStream.get('streamFilters'), { username_filters: 'eviltrout', - show_deleted: true - }, "streamFilters contains the username we filtered and show_deleted"); - - postStream.toggleDeleted(); - deepEqual(postStream.get('streamFilters'), { - username_filters: 'eviltrout' - }, "streamFilters contains the username we filtered without show_deleted"); - - postStream.cancelFilter(); - postStream.toggleDeleted(); - deepEqual(postStream.get('streamFilters'), { - show_deleted: true - }, "streamFilters show_deleted only"); - + }, "streamFilters contains the username we filtered"); }); test("loading", function() { From a655e4b09259fa5a908416b0add303d7c710ac1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Thu, 3 Nov 2016 22:48:32 +0100 Subject: [PATCH 012/266] ensure we allow self oneboxing of login required sites --- lib/cooked_post_processor.rb | 4 +- lib/onebox/engine/discourse_local_onebox.rb | 128 +++++++----------- .../templates/discourse_topic_onebox.hbs | 6 +- lib/oneboxer.rb | 4 +- .../engine/discourse_local_onebox_spec.rb | 80 ++++------- 5 files changed, 76 insertions(+), 146 deletions(-) diff --git a/lib/cooked_post_processor.rb b/lib/cooked_post_processor.rb index 5f06261c1b..cd563f12ed 100644 --- a/lib/cooked_post_processor.rb +++ b/lib/cooked_post_processor.rb @@ -300,10 +300,10 @@ class CookedPostProcessor } # apply oneboxes - Oneboxer.apply(@doc, topic_id: @post.topic_id) { |url| + Oneboxer.apply(@doc, topic_id: @post.topic_id) do |url| @has_oneboxes = true Oneboxer.onebox(url, args) - } + end # make sure we grab dimensions for oneboxed images oneboxed_images.each { |img| limit_size!(img) } diff --git a/lib/onebox/engine/discourse_local_onebox.rb b/lib/onebox/engine/discourse_local_onebox.rb index 3e022e0526..37f7a487d7 100644 --- a/lib/onebox/engine/discourse_local_onebox.rb +++ b/lib/onebox/engine/discourse_local_onebox.rb @@ -3,118 +3,84 @@ module Onebox class DiscourseLocalOnebox include Engine - # we need to allow for multisite here - def self.is_on_site?(url) - Regexp.new("^#{Discourse.base_url.gsub(".","\\.")}.*$", true) === url.to_s - end - # Use this onebox before others def self.priority 1 end def self.===(other) - if other.kind_of?(URI) - uri = other - begin - route = Rails.application.routes.recognize_path(uri.path.sub(Discourse.base_uri, "")) - case route[:controller] - when 'uploads' - is_on_site?(other) - when 'topics' - # super will use matches_regexp to match the domain name - is_on_site?(other) - else - false - end - rescue ActionController::RoutingError - false - end - else - is_on_site?(other) - end + url = other.to_s + return false unless url[Discourse.base_url] + + path = url.sub(Discourse.base_url, "") + route = Rails.application.routes.recognize_path(path) + + !!(route[:controller] =~ /topics|uploads/) + rescue ActionController::RoutingError + false end def to_html - uri = URI::parse(@url) - route = Rails.application.routes.recognize_path(uri.path.sub(Discourse.base_uri, "")) - url = @url.sub(/[&?]source_topic_id=(\d+)/, "") - source_topic_id = $1.to_i + path = @url.sub(Discourse.base_url, "") + route = Rails.application.routes.recognize_path(path) - # Figure out what kind of onebox to show based on the URL case route[:controller] - when 'uploads' + when "uploads" then upload_html(path) + when "topics" then topic_html(path, route) + end + end - url.gsub!("http:", "https:") if SiteSetting.force_https - if File.extname(uri.path) =~ /^.(mov|mp4|webm|ogv)$/ - return "" - elsif File.extname(uri.path) =~ /^.(mp3|ogg|wav)$/ - return "" - else - return false + private + + def upload_html(path) + case File.extname(path) + when /^\.(mov|mp4|webm|ogv)$/ + "" + when /^\.(mp3|ogg|wav)$/ + "" end - when 'topics' + end + + def topic_html(path, route) + link = "#{@url}" + source_topic_id = @url[/[&?]source_topic_id=(\d+)/, 1].to_i - linked = "#{url}" if route[:post_number].present? && route[:post_number].to_i > 1 - # Post Link - post = Post.find_by(topic_id: route[:topic_id], post_number: route[:post_number].to_i) - return linked unless post - return linked if post.hidden - return linked unless Guardian.new.can_see?(post) + post = Post.find_by(topic_id: route[:topic_id], post_number: route[:post_number]) + return link if post.nil? || post.hidden || !Guardian.new.can_see?(post) topic = post.topic slug = Slug.for(topic.title) - excerpt = post.excerpt(SiteSetting.post_onebox_maxlength) - excerpt.gsub!("\n"," ") - # hack to make it render for now - excerpt.gsub!("[/quote]", "[quote]") + excerpt.gsub!(/[\r\n]+/, " ") + excerpt.gsub!("[/quote]", "[quote]") # don't break my quote + quote = "[quote=\"#{post.user.username}, topic:#{topic.id}, slug:#{slug}, post:#{post.post_number}\"]#{excerpt}[/quote]" args = {} args[:topic_id] = source_topic_id if source_topic_id > 0 - cooked = PrettyText.cook(quote, args) - return cooked + PrettyText.cook(quote, args) else - # Topic Link - topic = Topic.where(id: route[:topic_id].to_i).includes(:user).first - return linked unless topic - return linked unless Guardian.new.can_see?(topic) + topic = Topic.find_by(id: route[:topic_id]) + return link if topic.nil? || !Guardian.new.can_see?(topic) - post = topic.posts.first + first_post = topic.ordered_posts.first - posters = topic.posters_summary.map do |p| - { - username: p[:user].username, - avatar: PrettyText.avatar_img(p[:user].avatar_template, 'tiny'), - description: p[:description], - extras: p[:extras] - } - end + args = { + topic: topic.id, + avatar: PrettyText.avatar_img(topic.user.avatar_template, "tiny"), + original_url: @url, + title: PrettyText.unescape_emoji(CGI::escapeHTML(topic.title)), + category_html: CategoryBadge.html_for(topic.category), + quote: first_post.excerpt(SiteSetting.post_onebox_maxlength), + } - quote = post.excerpt(SiteSetting.post_onebox_maxlength) - args = { original_url: url, - title: PrettyText.unescape_emoji(CGI::escapeHTML(topic.title)), - avatar: PrettyText.avatar_img(topic.user.avatar_template, 'tiny'), - posts_count: topic.posts_count, - last_post: FreedomPatches::Rails4.time_ago_in_words(topic.last_posted_at, false, scope: :'datetime.distance_in_words_verbose'), - age: FreedomPatches::Rails4.time_ago_in_words(topic.created_at, false, scope: :'datetime.distance_in_words_verbose'), - views: topic.views, - posters: posters, - quote: quote, - category_html: CategoryBadge.html_for(topic.category), - topic: topic.id } - - return Mustache.render(File.read("#{Rails.root}/lib/onebox/templates/discourse_topic_onebox.hbs"), args) + template = File.read("#{Rails.root}/lib/onebox/templates/discourse_topic_onebox.hbs") + Mustache.render(template, args) end end - rescue ActionController::RoutingError - nil - end - end end end diff --git a/lib/onebox/templates/discourse_topic_onebox.hbs b/lib/onebox/templates/discourse_topic_onebox.hbs index b9ca0aff51..41a075db7f 100644 --- a/lib/onebox/templates/discourse_topic_onebox.hbs +++ b/lib/onebox/templates/discourse_topic_onebox.hbs @@ -1,11 +1,9 @@ diff --git a/lib/oneboxer.rb b/lib/oneboxer.rb index db6f184901..13d8152202 100644 --- a/lib/oneboxer.rb +++ b/lib/oneboxer.rb @@ -62,9 +62,7 @@ module Oneboxer onebox_links = doc.search("a.onebox") if onebox_links.present? onebox_links.each do |link| - if link['href'].present? - yield link['href'], link - end + yield(link['href'], link) if link['href'].present? end end diff --git a/spec/components/onebox/engine/discourse_local_onebox_spec.rb b/spec/components/onebox/engine/discourse_local_onebox_spec.rb index 9cb7e84c0d..90f9bf4534 100644 --- a/spec/components/onebox/engine/discourse_local_onebox_spec.rb +++ b/spec/components/onebox/engine/discourse_local_onebox_spec.rb @@ -1,17 +1,8 @@ require 'rails_helper' describe Onebox::Engine::DiscourseLocalOnebox do - it "matches for a topic url" do - url = "#{Discourse.base_url}/t/hot-topic" - expect(Onebox.has_matcher?(url)).to eq(true) - expect(Onebox::Matcher.new(url).oneboxed).to eq(described_class) - end - it "matches for a post url" do - url = "#{Discourse.base_url}/t/hot-topic/23/2" - expect(Onebox.has_matcher?(url)).to eq(true) - expect(Onebox::Matcher.new(url).oneboxed).to eq(described_class) - end + before { SiteSetting.external_system_avatars_enabled = false } context "for a link to a post" do let(:post) { Fabricate(:post) } @@ -36,13 +27,11 @@ describe Onebox::Engine::DiscourseLocalOnebox do it "returns some onebox goodness if post exists and can be seen" do url = "#{Discourse.base_url}#{post2.url}?source_topic_id=#{post2.topic_id+1}" - Guardian.any_instance.stubs(:can_see?).returns(true) html = Onebox.preview(url).to_s expect(html).to include(post2.excerpt) expect(html).to include(post2.topic.title) - url = "#{Discourse.base_url}#{post2.url}" - html = Onebox.preview(url).to_s + html = Onebox.preview("#{Discourse.base_url}#{post2.url}").to_s expect(html).to include(post2.user.username) expect(html).to include(post2.excerpt) end @@ -52,8 +41,6 @@ describe Onebox::Engine::DiscourseLocalOnebox do let(:post) { Fabricate(:post) } let(:topic) { post.topic } - before { topic.last_posted_at = Time.zone.now; topic.save; } # otherwise errors - it "returns a link if topic isn't found" do url = "#{Discourse.base_url}/t/not-found/123" expect(Onebox.preview(url).to_s).to eq("#{url}") @@ -67,16 +54,14 @@ describe Onebox::Engine::DiscourseLocalOnebox do it "replaces emoji in the title" do topic.update_column(:title, "Who wants to eat a :hamburger:") - expect(Onebox.preview(topic.url).to_s).to match(/hamburger.png/) + expect(Onebox.preview(topic.url).to_s).to match(/hamburger\.png/) end it "returns some onebox goodness if post exists and can be seen" do - SiteSetting.external_system_avatars_enabled = false - url = "#{topic.url}" Guardian.any_instance.stubs(:can_see?).returns(true) - html = Onebox.preview(url).to_s - expect(html).to include(topic.posts.first.user.username) - expect(html).to include("topic-info") + html = Onebox.preview(topic.url).to_s + expect(html).to include(topic.ordered_posts.first.user.username) + expect(html).to include("
") end end @@ -102,59 +87,42 @@ describe Onebox::Engine::DiscourseLocalOnebox do end context "When deployed to a subfolder" do - let(:base_url) { "http://test.localhost/subfolder" } let(:base_uri) { "/subfolder" } + let(:base_url) { "http://test.localhost#{base_uri}" } before do Discourse.stubs(:base_url).returns(base_url) Discourse.stubs(:base_uri).returns(base_uri) end - it "matches for a topic url" do - url = "#{Discourse.base_url}/t/hot-topic" - expect(Onebox.has_matcher?(url)).to eq(true) - expect(Onebox::Matcher.new(url).oneboxed).to eq(described_class) - end - - it "matches for a post url" do - url = "#{Discourse.base_url}/t/hot-topic/23/2" - expect(Onebox.has_matcher?(url)).to eq(true) - expect(Onebox::Matcher.new(url).oneboxed).to eq(described_class) - end - context "for a link to a post" do let(:post) { Fabricate(:post) } let(:post2) { Fabricate(:post, topic: post.topic, post_number: 2) } - it "returns a link if post isn't found" do - url = "#{Discourse.base_url}/t/not-exist/3/2" - expect(Onebox.preview(url).to_s).to eq("#{url}") - end - - it "returns a link if not allowed to see the post" do - url = "#{Discourse.base_url}#{post2.url}" - Guardian.any_instance.stubs(:can_see?).returns(false) - expect(Onebox.preview(url).to_s).to eq("#{url}") - end - - it "returns a link if post is hidden" do - hidden_post = Fabricate(:post, topic: post.topic, post_number: 2, hidden: true, hidden_reason_id: Post.hidden_reasons[:flag_threshold_reached]) - url = "#{Discourse.base_url}#{hidden_post.url}" - expect(Onebox.preview(url).to_s).to eq("#{url}") - end - it "returns some onebox goodness if post exists and can be seen" do url = "#{Discourse.base_url}#{post2.url}?source_topic_id=#{post2.topic_id+1}" Guardian.any_instance.stubs(:can_see?).returns(true) html = Onebox.preview(url).to_s expect(html).to include(post2.excerpt) expect(html).to include(post2.topic.title) - - url = "#{Discourse.base_url}#{post2.url}" - html = Onebox.preview(url).to_s - expect(html).to include(post2.user.username) - expect(html).to include(post2.excerpt) end end end + + context "When login_required is enabled" do + before { SiteSetting.login_required = true } + + context "for a link to a topic" do + let(:post) { Fabricate(:post) } + let(:topic) { post.topic } + + it "returns some onebox goodness if post exists and can be seen" do + html = Onebox.preview(topic.url).to_s + expect(html).to include(topic.ordered_posts.first.user.username) + expect(html).to include("
") + end + end + + end + end From 3277fb1c785eadd3f506448ea368cf4275e1515c Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 4 Nov 2016 08:30:49 +0800 Subject: [PATCH 013/266] Remove unused gem. --- Gemfile | 1 - Gemfile.lock | 4 ---- 2 files changed, 5 deletions(-) diff --git a/Gemfile b/Gemfile index 023ad9f5de..766c948480 100644 --- a/Gemfile +++ b/Gemfile @@ -150,7 +150,6 @@ group :development do gem 'bullet', require: !!ENV['BULLET'] gem 'better_errors' gem 'binding_of_caller' - gem 'librarian', '>= 0.0.25', require: false gem 'annotate' gem 'foreman', require: false end diff --git a/Gemfile.lock b/Gemfile.lock index 9361ef7f4e..a61cc01697 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -144,9 +144,6 @@ GEM json (1.8.3) jwt (1.5.2) kgio (2.10.0) - librarian (0.1.2) - highline - thor (~> 0.15) libv8 (5.3.332.38.1) listen (0.7.3) logster (1.2.5) @@ -431,7 +428,6 @@ DEPENDENCIES htmlentities http_accept_language (~> 2.0.5) image_optim (= 0.20.2) - librarian (>= 0.0.25) listen (= 0.7.3) logster lru_redux From 83de81930f914cbe53cd478707cd4ca785cc5b52 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 4 Nov 2016 09:02:09 +0800 Subject: [PATCH 014/266] Update gems. --- Gemfile.lock | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a61cc01697..fe14e6040b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -183,7 +183,7 @@ GEM multi_json (~> 1.3) multi_xml (~> 0.5) rack (~> 1.2) - oj (2.14.3) + oj (2.17.5) omniauth (1.3.1) hashie (>= 1.2, < 4) rack (>= 1.0, < 3) @@ -221,7 +221,7 @@ GEM openid-redis-store (0.0.2) redis ruby-openid - pg (0.18.4) + pg (0.19.0) progress (3.1.1) pry (0.10.4) coderay (~> 1.1.0) @@ -270,7 +270,7 @@ GEM activesupport (= 4.2.7.1) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - raindrops (0.16.0) + raindrops (0.17.0) rake (11.2.2) rake-compiler (0.9.9) rake @@ -281,7 +281,7 @@ GEM ffi (>= 1.0.6) msgpack (>= 0.4.3) trollop (>= 1.16.2) - redis (3.3.0) + redis (3.3.1) redis-namespace (1.5.2) redis (~> 3.0, >= 3.0.4) rest-client (1.8.0) @@ -342,9 +342,10 @@ GEM shoulda-context (1.2.1) shoulda-matchers (2.8.0) activesupport (>= 3.0.0) - sidekiq (4.1.2) + sidekiq (4.2.4) concurrent-ruby (~> 1.0) connection_pool (~> 2.2, >= 2.2.0) + rack-protection (>= 1.5.0) redis (~> 3.2, >= 3.2.1) sidekiq-statistic (1.2.0) sidekiq (>= 3.3.4, < 5) @@ -388,7 +389,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.1) - unicorn (5.1.0) + unicorn (5.2.0) kgio (~> 2.6) raindrops (~> 0.7) uniform_notifier (1.10.0) From 5f98cc8c3e96375e6aee3da13d4b7790e9ff0fc0 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 4 Nov 2016 10:08:01 +0800 Subject: [PATCH 015/266] Fix typo. --- config/locales/server.en.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index d40510aea4..c726595eb3 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -3270,7 +3270,7 @@ en: contact_url: label: "Web Page" placeholder: "http://www.example.com/contact-us" - description: "General contact web page for you or your oganization. Will be displayed on your about page." + description: "General contact web page for you or your organization. Will be displayed on your about page." site_contact: label: "Automated Messages" description: "All automated Discourse personal messages will be sent from this user. Most importantly, this user will be the designated sender of every welcome message automatically sent to new users." From b26368709e89efaf53329d4e1c30414bc4e8cc72 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 4 Nov 2016 16:53:13 +0800 Subject: [PATCH 016/266] FIX: Unescape emojis in composer topic title. --- 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 3bfdcc904a..a3d8f84364 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -5,6 +5,7 @@ import Quote from 'discourse/lib/quote'; import Draft from 'discourse/models/draft'; import computed from 'ember-addons/ember-computed-decorators'; import { escapeExpression, tinyAvatar } from 'discourse/lib/utilities'; +import { emojiUnescape } from 'discourse/lib/text'; const CLOSED = 'closed', SAVING = 'saving', @@ -174,7 +175,7 @@ const Composer = RestModel.extend({ case REPLY: case EDIT: if (postDescription) return postDescription; - if (topic) return I18n.t('post.reply_topic', { link: topicLink }); + if (topic) return emojiUnescape(I18n.t('post.reply_topic', { link: topicLink })); } }.property('action', 'post', 'topic', 'topic.title'), From 9fd317306cee7ea877298db808d730272a349580 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Fri, 4 Nov 2016 17:06:53 +0800 Subject: [PATCH 017/266] FIX: Do not show educational message for PMs. --- lib/composer_messages_finder.rb | 4 +++- .../composer_messages_finder_spec.rb | 20 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index 86f03680d9..551222b131 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -17,6 +17,8 @@ class ComposerMessagesFinder # Determines whether to show the user education text def check_education_message + return '' if @topic && @topic.archetype == Archetype.private_message + if creating_topic? count = @user.created_topic_count education_key = 'education.new-topic' @@ -35,7 +37,7 @@ class ComposerMessagesFinder } end - nil + '' end # New users have a limited number of replies in a topic diff --git a/spec/components/composer_messages_finder_spec.rb b/spec/components/composer_messages_finder_spec.rb index d4e0aaedc6..706c8f7f0c 100644 --- a/spec/components/composer_messages_finder_spec.rb +++ b/spec/components/composer_messages_finder_spec.rb @@ -41,6 +41,26 @@ describe ComposerMessagesFinder do end end + context 'private message' do + let(:topic) { Fabricate(:private_message_topic) } + + context 'starting a new private message' do + let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'createTopic', topic_id: topic.id) } + + it 'should return an empty string' do + expect(finder.check_education_message).to eq('') + end + end + + context 'replying to a private message' do + let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply', topic_id: topic.id) } + + it 'should return an empty string' do + expect(finder.check_education_message).to eq('') + end + end + end + context 'creating reply' do let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply') } From dd58c006990f99c81beace2dac3a8f9a3e3d29a0 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Fri, 4 Nov 2016 15:18:18 +0100 Subject: [PATCH 018/266] FEATURE: Add instance id in the webhook payload --- app/jobs/regular/emit_web_hook_event.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index 87e273376f..72e5f5923a 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -46,6 +46,7 @@ module Jobs 'Content-Type' => content_type, 'Host' => uri.host, 'User-Agent' => "Discourse/" + Discourse::VERSION::STRING, + 'X-Discourse-Instance' => Discourse.base_url, 'X-Discourse-Event-Id' => web_hook_event.id, 'X-Discourse-Event-Type' => @opts[:event_type] } From c8282e4ca1f106487d028e43355163d95a4e835b Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Nov 2016 13:51:16 -0400 Subject: [PATCH 019/266] FIX: We're running code outside of the runloop --- .../javascripts/discourse/components/d-editor.js.es6 | 8 +++++++- .../discourse/components/topic-navigation.js.es6 | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index 322959422f..e6dd2804dd 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -278,7 +278,13 @@ export default Ember.Component.extend({ @observes('ready', 'value') _watchForChanges() { if (!this.get('ready')) { return; } - Ember.run.debounce(this, this._updatePreview, 30); + + // Debouncing in test mode is complicated + if (Ember.testing) { + this._updatePreview(); + } else { + Ember.run.debounce(this, this._updatePreview, 30); + } }, _applyCategoryHashtagAutocomplete(container) { diff --git a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 b/app/assets/javascripts/discourse/components/topic-navigation.js.es6 index 1dd1e81216..579f9959f1 100644 --- a/app/assets/javascripts/discourse/components/topic-navigation.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-navigation.js.es6 @@ -4,7 +4,7 @@ export default Ember.Component.extend({ composerOpen: null, info: Em.Object.create(), - _checkSize() { + _performCheckSize() { if (!this.element || this.isDestroying || this.isDestroyed) { return; } let info = this.get('info'); @@ -39,6 +39,10 @@ export default Ember.Component.extend({ } }, + _checkSize() { + Ember.run.scheduleOnce('afterRender', this, this._performCheckSize); + }, + // we need to store this so topic progress has something to init with _topicScrolled(event) { this.set('info.prevEvent', event); @@ -62,7 +66,7 @@ export default Ember.Component.extend({ composerOpened() { this.set('composerOpen', true); // we need to do the check after animation is done - setTimeout(()=>this._checkSize(), 500); + Ember.run.later(() => this._checkSize(), 500); }, composerClosed() { @@ -112,7 +116,7 @@ export default Ember.Component.extend({ $('#reply-control').on('div-resized.discourse-topic-navigation', () => this._checkSize()); } - Ember.run.scheduleOnce('afterRender', this, this._checkSize); + this._checkSize(); }, willDestroyElement() { From e18ae3449398cd2553fd2c81c033b7230481718f Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Nov 2016 14:15:26 -0400 Subject: [PATCH 020/266] Don't try injecting into the Resolver. Use options instead. --- .../javascripts/discourse-common/resolver.js.es6 | 11 +++++++++-- .../javascripts/discourse/initializers/mobile.js.es6 | 6 +++--- test/javascripts/ember/resolver-test.js.es6 | 4 ++-- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse-common/resolver.js.es6 b/app/assets/javascripts/discourse-common/resolver.js.es6 index 15c099cc28..a3068b91b1 100644 --- a/app/assets/javascripts/discourse-common/resolver.js.es6 +++ b/app/assets/javascripts/discourse-common/resolver.js.es6 @@ -9,6 +9,13 @@ var LOADING_WHITELIST = ['badges', 'userActivity', 'userPrivateMessages', 'admin var _dummyRoute; var _loadingView; + +const _options = {}; + +export function setResolverOption(name, value) { + _options[name] = value; +} + function loadingResolver(cb) { return function(parsedName) { var fullNameWithoutType = parsedName.fullNameWithoutType; @@ -158,14 +165,14 @@ export function buildResolver(baseName) { }, findPluginMobileTemplate(parsedName) { - if (this.mobileView) { + if (_options.mobileView) { var pluginParsedName = this.parseName(parsedName.fullName.replace("template:", "template:javascripts/mobile/")); return this.findTemplate(pluginParsedName); } }, findMobileTemplate(parsedName) { - if (this.mobileView) { + if (_options.mobileView) { var mobileParsedName = this.parseName(parsedName.fullName.replace("template:", "template:mobile/")); return this.findTemplate(mobileParsedName); } diff --git a/app/assets/javascripts/discourse/initializers/mobile.js.es6 b/app/assets/javascripts/discourse/initializers/mobile.js.es6 index 2ac54497b2..8c057a425f 100644 --- a/app/assets/javascripts/discourse/initializers/mobile.js.es6 +++ b/app/assets/javascripts/discourse/initializers/mobile.js.es6 @@ -1,18 +1,18 @@ import Mobile from 'discourse/lib/mobile'; +import { setResolverOption } from 'discourse-common/resolver'; // Initializes the `Mobile` helper object. export default { name: 'mobile', after: 'inject-objects', - initialize(container, app) { + initialize(container) { Mobile.init(); const site = container.lookup('site:main'); site.set('mobileView', Mobile.mobileView); site.set('isMobileDevice', Mobile.isMobileDevice); - // This is a bit weird but you can't seem to inject into the resolver? - app.registry.resolver.__resolver__.mobileView = Mobile.mobileView; + setResolverOption('mobileView', Mobile.mobileView); } }; diff --git a/test/javascripts/ember/resolver-test.js.es6 b/test/javascripts/ember/resolver-test.js.es6 index 1c81f96df5..648729d6bd 100644 --- a/test/javascripts/ember/resolver-test.js.es6 +++ b/test/javascripts/ember/resolver-test.js.es6 @@ -1,4 +1,4 @@ -import { buildResolver } from 'discourse-common/resolver'; +import { setResolverOption, buildResolver } from 'discourse-common/resolver'; let originalTemplates; let resolver; @@ -92,7 +92,7 @@ test("resolves mobile templates to 'mobile/' namespace", function() { "baz" ]); - resolver.mobileView = true; + setResolverOption('mobileView', true); lookupTemplate("template:foo", "mobile/foo", "finding mobile version even if normal one is not present"); lookupTemplate("template:bar", "mobile/bar", "preferring mobile version when both mobile and normal versions are present"); From 5febbd58cca02e7627b117d5f29c444996035786 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 3 Nov 2016 14:52:14 -0400 Subject: [PATCH 021/266] The initializer arguments changed in future Ember releases --- app/assets/javascripts/discourse.js.es6 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse.js.es6 b/app/assets/javascripts/discourse.js.es6 index cbbc0bf9cf..9012e8301b 100644 --- a/app/assets/javascripts/discourse.js.es6 +++ b/app/assets/javascripts/discourse.js.es6 @@ -104,7 +104,14 @@ const Discourse = Ember.Application.extend({ if (/\/pre\-initializers\//.test(key)) { const module = require(key, null, null, true); if (!module) { throw new Error(key + ' must export an initializer.'); } - Discourse.initializer(module.default); + + const init = module.default; + const oldInitialize = init.initialize; + init.initialize = function() { + oldInitialize.call(this, Discourse.__container__, Discourse); + }; + + Discourse.initializer(init); } }); @@ -115,8 +122,8 @@ const Discourse = Ember.Application.extend({ const init = module.default; const oldInitialize = init.initialize; - init.initialize = function(app) { - oldInitialize.call(this, app.container, Discourse); + init.initialize = function() { + oldInitialize.call(this, Discourse.__container__, Discourse); }; Discourse.instanceInitializer(init); From fe07200dbf78965595df01374987285c1fd8f018 Mon Sep 17 00:00:00 2001 From: Mohamad Abras Date: Fri, 4 Nov 2016 23:12:03 +0200 Subject: [PATCH 022/266] refinement Arabic translation of NGINX plugin --- .../config/locales/server.ar.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/discourse-nginx-performance-report/config/locales/server.ar.yml b/plugins/discourse-nginx-performance-report/config/locales/server.ar.yml index 0c401c64f6..65b70e00fb 100644 --- a/plugins/discourse-nginx-performance-report/config/locales/server.ar.yml +++ b/plugins/discourse-nginx-performance-report/config/locales/server.ar.yml @@ -1,3 +1,3 @@ ar: site_settings: - daily_performance_report: "تحليل سجلات NGINX يومي ونشر موضوع \"قاقم فقط\" مع التفاصيل" + daily_performance_report: "تحليل سجلّات الخادم NGINX يوميًا ونشر موضوع مفصّل مع طاقم الموقع" From 36127b6ecad4ab3cd1be7dedaf690702057abc43 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Sun, 6 Nov 2016 15:23:18 +0530 Subject: [PATCH 023/266] update onebox gem --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index fe14e6040b..10a495336a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -212,7 +212,7 @@ GEM omniauth-twitter (1.2.1) json (~> 1.3) omniauth-oauth (~> 1.1) - onebox (1.5.60) + onebox (1.5.61) htmlentities (~> 4.3.4) moneta (~> 0.8) multi_json (~> 1.11) From 3aa22715af388bc6ed76305a4799566f6980c057 Mon Sep 17 00:00:00 2001 From: Kiffin Gish Date: Sun, 6 Nov 2016 20:14:09 +0100 Subject: [PATCH 024/266] A new guard for changing post timestamps called can_change_post_timestamps? --- app/controllers/topics_controller.rb | 2 +- lib/guardian/post_guardian.rb | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 1a15adc5b5..404495c06f 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -492,7 +492,7 @@ class TopicsController < ApplicationController params.require(:topic_id) params.require(:timestamp) - guardian.ensure_can_change_post_owner! + guardian.ensure_can_change_post_timestamps! begin PostTimestampChanger.new( topic_id: params[:topic_id].to_i, diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 6dc74106d9..c928290efe 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -185,6 +185,10 @@ module PostGuardian is_admin? end + def can_change_post_timestamps? + is_admin? + end + def can_wiki?(post) return false unless authenticated? return true if is_staff? || @user.has_trust_level?(TrustLevel[4]) From 2ddabc3928e377ee0757c603c66c4ca767350c40 Mon Sep 17 00:00:00 2001 From: Sam Date: Mon, 7 Nov 2016 12:48:00 +1100 Subject: [PATCH 025/266] FIX: protect against future regressions of google omniauth --- lib/auth/google_oauth2_authenticator.rb | 7 +++-- .../auth/google_oauth2_authenticator_spec.rb | 31 +++++++++++++++++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/lib/auth/google_oauth2_authenticator.rb b/lib/auth/google_oauth2_authenticator.rb index c51e5a01d0..ec1f386ea7 100644 --- a/lib/auth/google_oauth2_authenticator.rb +++ b/lib/auth/google_oauth2_authenticator.rb @@ -18,8 +18,11 @@ class Auth::GoogleOAuth2Authenticator < Auth::Authenticator user_info = GoogleUserInfo.find_by(google_user_id: google_hash[:google_user_id]) result.user = user_info.try(:user) - if !result.user && !result.email.blank? && result.user = User.find_by_email(result.email) - GoogleUserInfo.create({user_id: result.user.id}.merge(google_hash)) + if !result.user && !result.email.blank? && result.email_valid + result.user = User.find_by_email(result.email) + if result.user + GoogleUserInfo.create({user_id: result.user.id}.merge(google_hash)) + end end result diff --git a/spec/components/auth/google_oauth2_authenticator_spec.rb b/spec/components/auth/google_oauth2_authenticator_spec.rb index ecf5230d50..db28abfd22 100644 --- a/spec/components/auth/google_oauth2_authenticator_spec.rb +++ b/spec/components/auth/google_oauth2_authenticator_spec.rb @@ -6,9 +6,36 @@ load 'auth/google_oauth2_authenticator.rb' describe Auth::GoogleOAuth2Authenticator do + it 'does not look up user unless email is verified' do + # note, emails that come back from google via omniauth are always valid + # this protects against future regressions + + authenticator = Auth::GoogleOAuth2Authenticator.new + user = Fabricate(:user) + + hash = { + :uid => "123456789", + :info => { + :name => "John Doe", + :email => user.email + }, + :extra => { + :raw_info => { + :email => user.email, + :email_verified => false, + :name => "John Doe" + } + } + } + + result = authenticator.after_authenticate(hash) + + expect(result.user).to eq(nil) + end + context 'after_authenticate' do it 'can authenticate and create a user record for already existing users' do - authenticator = described_class.new + authenticator = Auth::GoogleOAuth2Authenticator.new user = Fabricate(:user) hash = { @@ -19,7 +46,7 @@ describe Auth::GoogleOAuth2Authenticator do }, :extra => { :raw_info => { - :email => "user@domain.example.com", + :email => user.email, :email_verified => true, :name => "John Doe" } From 9375dcb6fe8ac5576f3a865496aadc5065a24641 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 7 Nov 2016 14:54:39 +0800 Subject: [PATCH 026/266] PERF: Spawn a seperate timer task to check if Redis master is up. --- lib/discourse_redis.rb | 50 +++++++++++-------------- spec/components/discourse_redis_spec.rb | 41 +++++++++----------- 2 files changed, 38 insertions(+), 53 deletions(-) diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index f3f4993c81..b90aa0c079 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -14,18 +14,20 @@ class DiscourseRedis @running = false @mutex = Mutex.new @slave_config = DiscourseRedis.slave_config + @timer_task = init_timer_task end def verify_master synchronize do - return if @running || recently_checked? - @running = true + return if @timer_task.running? end - Thread.new { initiate_fallback_to_master } + @timer_task.execute end def initiate_fallback_to_master + success = false + begin slave_client = ::Redis::Client.new(@slave_config) logger.info "#{log_prefix}: Checking connection to master server..." @@ -39,13 +41,14 @@ class DiscourseRedis Discourse.clear_readonly! Discourse.request_refresh! - @master = true + self.master = true + success = true end ensure - @running = false - @last_checked = Time.zone.now slave_client.disconnect end + + success end def master @@ -56,23 +59,18 @@ class DiscourseRedis synchronize { @master = args } end - def recently_checked? - if @last_checked - Time.zone.now <= (@last_checked + 5.seconds) - else - false - end - end - - # Used for testing - def reset! - @master = true - @last_checked = nil - @running = false + def running? + synchronize { @timer_task.running? } end private + def init_timer_task + Concurrent::TimerTask.new(execution_interval: 10) do |task| + task.shutdown if initiate_fallback_to_master + end + end + def synchronize @mutex.synchronize { yield } end @@ -87,9 +85,6 @@ class DiscourseRedis end class Connector < Redis::Client::Connector - MASTER = 'master'.freeze - SLAVE = 'slave'.freeze - def initialize(options) super(options) @slave_options = DiscourseRedis.slave_config(options) @@ -97,21 +92,18 @@ class DiscourseRedis end def resolve - - return @options unless @slave_options[:host] + return @slave_options if !@fallback_handler.master begin options = @options.dup options.delete(:connector) - client = ::Redis::Client.new(options) - client.call([:role])[0] + client = Redis::Client.new(options) + client.call([:role]) @options rescue Redis::ConnectionError, Redis::CannotConnectError, RuntimeError => ex - # A consul service name may be deregistered for a redis container setup raise ex if ex.class == RuntimeError && ex.message != "Name or service not known" - - return @slave_options if !@fallback_handler.master @fallback_handler.master = false + @fallback_handler.verify_master unless @fallback_handler.running? raise ex ensure client.disconnect diff --git a/spec/components/discourse_redis_spec.rb b/spec/components/discourse_redis_spec.rb index 0535d8fc37..730db985b5 100644 --- a/spec/components/discourse_redis_spec.rb +++ b/spec/components/discourse_redis_spec.rb @@ -42,7 +42,7 @@ describe DiscourseRedis do it 'should return the slave config when master is down' do begin - Redis::Client.any_instance.expects(:call).raises(Redis::CannotConnectError).twice + Redis::Client.any_instance.expects(:call).raises(Redis::CannotConnectError).once expect { connector.resolve }.to raise_error(Redis::CannotConnectError) config = connector.resolve @@ -58,7 +58,7 @@ describe DiscourseRedis do begin error = RuntimeError.new('Name or service not known') - Redis::Client.any_instance.expects(:call).raises(error).twice + Redis::Client.any_instance.expects(:call).raises(error).once expect { connector.resolve }.to raise_error(error) config = connector.resolve @@ -79,34 +79,27 @@ describe DiscourseRedis do describe DiscourseRedis::FallbackHandler do after do - fallback_handler.reset! + fallback_handler.master = true end describe '#initiate_fallback_to_master' do - it 'should fallback to the master server once it is up' do - begin - fallback_handler.master = false - Redis::Client.any_instance.expects(:call).with([:info]).returns(DiscourseRedis::FallbackHandler::MASTER_LINK_STATUS) - - DiscourseRedis::FallbackHandler::CONNECTION_TYPES.each do |connection_type| - Redis::Client.any_instance.expects(:call).with([:client, [:kill, 'type', connection_type]]) - end - - fallback_handler.initiate_fallback_to_master - - expect(fallback_handler.master).to eq(true) - expect(Discourse.recently_readonly?).to eq(false) - ensure - fallback_handler.master = true - end + it 'should return the right value if the master server is still down' do + fallback_handler.master = false + Redis::Client.any_instance.expects(:call).with([:info]).returns("Some other stuff") + expect(fallback_handler.initiate_fallback_to_master).to eq(false) end - it "should restrict the number of checks" do - expect { fallback_handler.verify_master }.to change { Thread.list.count }.by(1) - expect(fallback_handler.master).to eq(true) - + it 'should fallback to the master server once it is up' do fallback_handler.master = false - expect { fallback_handler.verify_master }.to_not change { Thread.list.count } + Redis::Client.any_instance.expects(:call).with([:info]).returns(DiscourseRedis::FallbackHandler::MASTER_LINK_STATUS) + + DiscourseRedis::FallbackHandler::CONNECTION_TYPES.each do |connection_type| + Redis::Client.any_instance.expects(:call).with([:client, [:kill, 'type', connection_type]]) + end + + expect(fallback_handler.initiate_fallback_to_master).to eq(true) + expect(fallback_handler.master).to eq(true) + expect(Discourse.recently_readonly?).to eq(false) end end end From fbbcde1230f220c0e359598187ca66630e462bf2 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Mon, 7 Nov 2016 15:28:10 +0800 Subject: [PATCH 027/266] FIX: Don't treat master as up if it is still loading data. --- lib/discourse_redis.rb | 4 ++-- spec/components/discourse_redis_spec.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/lib/discourse_redis.rb b/lib/discourse_redis.rb index b90aa0c079..9ed43c4e3a 100644 --- a/lib/discourse_redis.rb +++ b/lib/discourse_redis.rb @@ -98,8 +98,8 @@ class DiscourseRedis options = @options.dup options.delete(:connector) client = Redis::Client.new(options) - client.call([:role]) - @options + loading = client.call([:info]).split("\r\n").include?("loading:1") + loading ? @slave_options : @options rescue Redis::ConnectionError, Redis::CannotConnectError, RuntimeError => ex raise ex if ex.class == RuntimeError && ex.message != "Name or service not known" @fallback_handler.master = false diff --git a/spec/components/discourse_redis_spec.rb b/spec/components/discourse_redis_spec.rb index 730db985b5..8cc8c76d47 100644 --- a/spec/components/discourse_redis_spec.rb +++ b/spec/components/discourse_redis_spec.rb @@ -70,6 +70,18 @@ describe DiscourseRedis do end end + it "should return the slave config when master is still loading data" do + begin + Redis::Client.any_instance.expects(:call).with([:info]).returns("someconfig:haha\r\nloading:1") + config = connector.resolve + + expect(config[:host]).to eq(slave_host) + expect(config[:port]).to eq(slave_port) + ensure + fallback_handler.master = true + end + end + it "should raise the right error" do error = RuntimeError.new('test error') Redis::Client.any_instance.expects(:call).raises(error).twice From aa80c8cbdd0bc4b68a35a3c0b01857c7669c3dbd Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 7 Nov 2016 11:06:32 -0500 Subject: [PATCH 028/266] FIX: Support optional logos when previewing the wizard --- app/assets/javascripts/wizard/lib/preview.js.es6 | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/wizard/lib/preview.js.es6 b/app/assets/javascripts/wizard/lib/preview.js.es6 index a5e8371eb2..17cfe560ff 100644 --- a/app/assets/javascripts/wizard/lib/preview.js.es6 +++ b/app/assets/javascripts/wizard/lib/preview.js.es6 @@ -126,8 +126,11 @@ export function createPreviewComponent(width, height, obj) { // Logo const headerMargin = headerHeight * 0.2; const logoHeight = headerHeight - (headerMargin * 2); - const logoWidth = (logoHeight / this.logo.height) * this.logo.width; - this.scaleImage(this.logo, headerMargin, headerMargin, logoWidth, logoHeight); + + if (this.logo) { + const logoWidth = (logoHeight / this.logo.height) * this.logo.width; + this.scaleImage(this.logo, headerMargin, headerMargin, logoWidth, logoHeight); + } // Top right menu this.scaleImage(this.avatar, width - avatarSize - headerMargin, headerMargin, avatarSize, avatarSize); From bc2c6b0918c10467d40aa690919917d9090c7b84 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 7 Nov 2016 12:11:52 -0500 Subject: [PATCH 029/266] FIX: Allow arrays of links for decorators --- .../javascripts/discourse/widgets/hamburger-menu.js.es6 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index c944c40203..f0e5319eb6 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -3,6 +3,8 @@ import { h } from 'virtual-dom'; import DiscourseURL from 'discourse/lib/url'; import { ajax } from 'discourse/lib/ajax'; +const flatten = array => [].concat.apply([], array); + createWidget('priority-faq-link', { tagName: 'a.faq-priority.widget-link', @@ -105,8 +107,7 @@ export default createWidget('hamburger-menu', { links.push({ route: 'tags', label: 'tagging.tags' }); } - const extraLinks = applyDecorators(this, 'generalLinks', this.attrs, this.state); - + const extraLinks = flatten(applyDecorators(this, 'generalLinks', this.attrs, this.state)); return links.concat(extraLinks).map(l => this.attach('link', l)); }, @@ -165,7 +166,7 @@ export default createWidget('hamburger-menu', { if (currentUser && currentUser.staff) { results.push(this.attach('menu-links', { contents: () => { - const extraLinks = applyDecorators(this, 'admin-links', this.attrs, this.state) || []; + const extraLinks = flatten(applyDecorators(this, 'admin-links', this.attrs, this.state)); return this.adminLinks().concat(extraLinks); }})); } From 9ef724a065e81e9b22c76642b221dac33d6cdeea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 7 Nov 2016 18:14:28 +0100 Subject: [PATCH 030/266] FIX: self-onebox in read protected categories --- lib/onebox/engine/discourse_local_onebox.rb | 23 +++++++++++++++---- .../engine/discourse_local_onebox_spec.rb | 10 ++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/onebox/engine/discourse_local_onebox.rb b/lib/onebox/engine/discourse_local_onebox.rb index 37f7a487d7..34bc278961 100644 --- a/lib/onebox/engine/discourse_local_onebox.rb +++ b/lib/onebox/engine/discourse_local_onebox.rb @@ -26,7 +26,7 @@ module Onebox case route[:controller] when "uploads" then upload_html(path) - when "topics" then topic_html(path, route) + when "topics" then topic_html(route) end end @@ -41,13 +41,14 @@ module Onebox end end - def topic_html(path, route) + def topic_html(route) link = "#{@url}" source_topic_id = @url[/[&?]source_topic_id=(\d+)/, 1].to_i + source_topic = Topic.find_by(id: source_topic_id) if source_topic_id > 0 if route[:post_number].present? && route[:post_number].to_i > 1 post = Post.find_by(topic_id: route[:topic_id], post_number: route[:post_number]) - return link if post.nil? || post.hidden || !Guardian.new.can_see?(post) + return link unless can_see_post?(post, source_topic) topic = post.topic slug = Slug.for(topic.title) @@ -63,7 +64,7 @@ module Onebox PrettyText.cook(quote, args) else topic = Topic.find_by(id: route[:topic_id]) - return link if topic.nil? || !Guardian.new.can_see?(topic) + return link unless can_see_topic?(topic, source_topic) first_post = topic.ordered_posts.first @@ -81,6 +82,20 @@ module Onebox end end + def can_see_post?(post, source_topic) + return false if post.nil? || post.hidden || post.trashed? || post.topic.nil? + Guardian.new.can_see_post?(post) || same_category?(post.topic.category, source_topic) + end + + def can_see_topic?(topic, source_topic) + return false if topic.nil? || topic.trashed? || topic.private_message? + Guardian.new.can_see_topic?(topic) || same_category?(topic.category, source_topic) + end + + def same_category?(category, source_topic) + source_topic.try(:category_id) == category.try(:id) + end + end end end diff --git a/spec/components/onebox/engine/discourse_local_onebox_spec.rb b/spec/components/onebox/engine/discourse_local_onebox_spec.rb index 90f9bf4534..c3b015befc 100644 --- a/spec/components/onebox/engine/discourse_local_onebox_spec.rb +++ b/spec/components/onebox/engine/discourse_local_onebox_spec.rb @@ -15,7 +15,7 @@ describe Onebox::Engine::DiscourseLocalOnebox do it "returns a link if not allowed to see the post" do url = "#{Discourse.base_url}#{post2.url}" - Guardian.any_instance.stubs(:can_see?).returns(false) + Guardian.any_instance.expects(:can_see_post?).returns(false) expect(Onebox.preview(url).to_s).to eq("#{url}") end @@ -46,9 +46,9 @@ describe Onebox::Engine::DiscourseLocalOnebox do expect(Onebox.preview(url).to_s).to eq("#{url}") end - it "returns a link if not allowed to see the post" do + it "returns a link if not allowed to see the topic" do url = topic.url - Guardian.any_instance.stubs(:can_see?).returns(false) + Guardian.any_instance.expects(:can_see_topic?).returns(false) expect(Onebox.preview(url).to_s).to eq("#{url}") end @@ -57,8 +57,7 @@ describe Onebox::Engine::DiscourseLocalOnebox do expect(Onebox.preview(topic.url).to_s).to match(/hamburger\.png/) end - it "returns some onebox goodness if post exists and can be seen" do - Guardian.any_instance.stubs(:can_see?).returns(true) + it "returns some onebox goodness if topic exists and can be seen" do html = Onebox.preview(topic.url).to_s expect(html).to include(topic.ordered_posts.first.user.username) expect(html).to include("
") @@ -101,7 +100,6 @@ describe Onebox::Engine::DiscourseLocalOnebox do it "returns some onebox goodness if post exists and can be seen" do url = "#{Discourse.base_url}#{post2.url}?source_topic_id=#{post2.topic_id+1}" - Guardian.any_instance.stubs(:can_see?).returns(true) html = Onebox.preview(url).to_s expect(html).to include(post2.excerpt) expect(html).to include(post2.topic.title) From 24ad68e7653b722c92785b6e0e44fb22c07bc0c3 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Mon, 7 Nov 2016 15:11:56 -0500 Subject: [PATCH 031/266] Use `this.registry` in `component-test` --- .../javascripts/helpers/component-test.js.es6 | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/javascripts/helpers/component-test.js.es6 b/test/javascripts/helpers/component-test.js.es6 index f671adb8e4..69b6b67ab0 100644 --- a/test/javascripts/helpers/component-test.js.es6 +++ b/test/javascripts/helpers/component-test.js.es6 @@ -10,30 +10,30 @@ export default function(name, opts) { const appEvents = AppEvents.create(); this.site = Discourse.Site.current(); - this.container.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); - this.container.register('app-events:main', appEvents, { instantiate: false }); - this.container.register('capabilities:main', Ember.Object); - this.container.register('site:main', this.site, { instantiate: false }); - this.container.injection('component', 'siteSettings', 'site-settings:main'); - this.container.injection('component', 'appEvents', 'app-events:main'); - this.container.injection('component', 'capabilities', 'capabilities:main'); - this.container.injection('component', 'site', 'site:main'); + this.registry.register('site-settings:main', Discourse.SiteSettings, { instantiate: false }); + this.registry.register('app-events:main', appEvents, { instantiate: false }); + this.registry.register('capabilities:main', Ember.Object); + this.registry.register('site:main', this.site, { instantiate: false }); + this.registry.injection('component', 'siteSettings', 'site-settings:main'); + this.registry.injection('component', 'appEvents', 'app-events:main'); + this.registry.injection('component', 'capabilities', 'capabilities:main'); + this.registry.injection('component', 'site', 'site:main'); this.siteSettings = Discourse.SiteSettings; - autoLoadModules(this.container, this.registry); + autoLoadModules(this.registry, this.registry); const store = createStore(); if (!opts.anonymous) { const currentUser = Discourse.User.create({ username: 'eviltrout' }); this.currentUser = currentUser; - this.container.register('current-user:main', this.currentUser, { instantiate: false }); - this.container.register('topic-tracking-state:main', + this.registry.register('current-user:main', this.currentUser, { instantiate: false }); + this.registry.register('topic-tracking-state:main', TopicTrackingState.create({ currentUser }), { instantiate: false }); } - this.container.register('store:main', store, { instantiate: false }); + this.registry.register('store:main', store, { instantiate: false }); if (opts.setup) { opts.setup.call(this, store); From 70fb2431a1b82b20f1a0b6e09db30f98331fb8cf Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 4 Nov 2016 11:32:12 -0400 Subject: [PATCH 032/266] Migrate `this.container` to `getOwner(this)` --- .../admin/components/customize-link.js.es6 | 4 ++- .../discourse-common/lib/deprecated.js.es6 | 3 ++ .../discourse-common/lib/get-owner.js.es6 | 32 +++++++++++++++++++ .../components/badge-selector.js.es6 | 3 +- .../components/category-selector.js.es6 | 3 +- .../components/composer-editor.js.es6 | 4 +-- .../components/composer-message.js.es6 | 3 +- .../discourse/components/d-editor.js.es6 | 30 ++++++++++------- .../components/group-selector.js.es6 | 3 +- .../components/header-extra-info.js.es6 | 4 ++- .../discourse/components/mount-widget.js.es6 | 9 ++++-- .../discourse/components/nav-item.js.es6 | 3 +- .../components/topic-list-item.js.es6 | 5 +-- .../discourse/components/user-selector.js.es6 | 3 +- .../discourse/controllers/composer.js.es6 | 7 ++-- .../discourse/controllers/header.js.es6 | 3 +- .../discourse/lib/emoji/toolbar.js.es6 | 2 +- .../javascripts/discourse/lib/mobile.js.es6 | 7 ++-- .../discourse/lib/page-tracker.js.es6 | 4 ++- .../javascripts/discourse/lib/url.js.es6 | 3 +- .../javascripts/discourse/models/store.js.es6 | 18 +++++++---- .../pre-initializers/map-routes.js.es6 | 5 ++- .../discourse/routes/application.js.es6 | 7 ++-- .../discourse/routes/full-page-search.js.es6 | 3 +- .../discourse/routes/topic-from-params.js.es6 | 6 ++-- .../discourse/templates/composer.hbs | 2 +- .../discourse/widgets/connector.js.es6 | 2 +- .../discourse/widgets/decorator-helper.js.es6 | 3 +- .../discourse/widgets/hamburger-menu.js.es6 | 2 +- .../discourse/widgets/header.js.es6 | 8 ++--- .../javascripts/discourse/widgets/link.js.es6 | 2 +- .../widgets/search-menu-controls.js.es6 | 2 +- .../discourse/widgets/search-menu.js.es6 | 2 +- .../discourse/widgets/widget.js.es6 | 28 ++++++++-------- .../components/wizard-field-image.js.es6 | 3 +- 35 files changed, 154 insertions(+), 74 deletions(-) create mode 100644 app/assets/javascripts/discourse-common/lib/deprecated.js.es6 create mode 100644 app/assets/javascripts/discourse-common/lib/get-owner.js.es6 diff --git a/app/assets/javascripts/admin/components/customize-link.js.es6 b/app/assets/javascripts/admin/components/customize-link.js.es6 index 79d8cc26f4..0600f6b5cd 100644 --- a/app/assets/javascripts/admin/components/customize-link.js.es6 +++ b/app/assets/javascripts/admin/components/customize-link.js.es6 @@ -1,6 +1,8 @@ +import { getOwner } from 'discourse-common/lib/get-owner'; + export default Ember.Component.extend({ router: function() { - return this.container.lookup('router:main'); + return getOwner(this).lookup('router:main'); }.property(), active: function() { diff --git a/app/assets/javascripts/discourse-common/lib/deprecated.js.es6 b/app/assets/javascripts/discourse-common/lib/deprecated.js.es6 new file mode 100644 index 0000000000..02daf6ebda --- /dev/null +++ b/app/assets/javascripts/discourse-common/lib/deprecated.js.es6 @@ -0,0 +1,3 @@ +export default function deprecated(msg) { + console.warn(`DEPRECATION: ${msg}`); +} diff --git a/app/assets/javascripts/discourse-common/lib/get-owner.js.es6 b/app/assets/javascripts/discourse-common/lib/get-owner.js.es6 new file mode 100644 index 0000000000..ada334f075 --- /dev/null +++ b/app/assets/javascripts/discourse-common/lib/get-owner.js.es6 @@ -0,0 +1,32 @@ +import deprecated from 'discourse-common/lib/deprecated'; + +export function getOwner(obj) { + if (Ember.getOwner) { + return Ember.getOwner(obj) || Discourse.__container__; + } + + return obj.container; +} + +// `this.container` is deprecated, but we can still build a container-like +// object for components to use +export function getRegister(obj) { + const owner = getOwner(obj); + const register = { + lookup: (...args) => owner.lookup(...args), + lookupFactory: (...args) => { + return owner.lookupFactory ? owner.lookupFactory(...args) : owner._lookupFactory(...args); + }, + + deprecateContainer(target) { + Object.defineProperty(target, 'container', { + get() { + deprecated("Use `this.register` or `getOwner` instead of `this.container`"); + return register; + } + }); + } + }; + + return register; +} diff --git a/app/assets/javascripts/discourse/components/badge-selector.js.es6 b/app/assets/javascripts/discourse/components/badge-selector.js.es6 index 14b1e5349e..315f7398a7 100644 --- a/app/assets/javascripts/discourse/components/badge-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/badge-selector.js.es6 @@ -1,4 +1,5 @@ import { on, observes, default as computed } from 'ember-addons/ember-computed-decorators'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Component.extend({ @computed('placeholderKey') @@ -17,7 +18,7 @@ export default Ember.Component.extend({ var self = this; var selectedBadges; - var template = this.container.lookup('template:badge-selector-autocomplete.raw'); + var template = getOwner(this).lookup('template:badge-selector-autocomplete.raw'); self.$('input').autocomplete({ allowAny: false, items: _.isArray(this.get('badgeNames')) ? this.get('badgeNames') : [this.get('badgeNames')], diff --git a/app/assets/javascripts/discourse/components/category-selector.js.es6 b/app/assets/javascripts/discourse/components/category-selector.js.es6 index 10a7c454eb..460a1dfdd6 100644 --- a/app/assets/javascripts/discourse/components/category-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/category-selector.js.es6 @@ -1,6 +1,7 @@ import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import Category from 'discourse/models/category'; import { on, observes } from 'ember-addons/ember-computed-decorators'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Component.extend({ @observes('categories') @@ -12,7 +13,7 @@ export default Ember.Component.extend({ @on('didInsertElement') _initializeAutocomplete(opts) { const self = this, - template = this.container.lookup('template:category-selector-autocomplete.raw'), + template = getOwner(this).lookup('template:category-selector-autocomplete.raw'), regexp = new RegExp(`href=['\"]${Discourse.getURL('/c/')}([^'\"]+)`); this.$('input').autocomplete({ diff --git a/app/assets/javascripts/discourse/components/composer-editor.js.es6 b/app/assets/javascripts/discourse/components/composer-editor.js.es6 index 6503e051c2..4ba0c20057 100644 --- a/app/assets/javascripts/discourse/components/composer-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-editor.js.es6 @@ -6,7 +6,7 @@ import { linkSeenTagHashtags, fetchUnseenTagHashtags } from 'discourse/lib/link- import { load } from 'pretty-text/oneboxer'; import { ajax } from 'discourse/lib/ajax'; import InputValidation from 'discourse/models/input-validation'; - +import { getOwner } from 'discourse-common/lib/get-owner'; import { tinyAvatar, displayErrorForUpload, getUploadMarkdown, @@ -62,7 +62,7 @@ export default Ember.Component.extend({ @on('didInsertElement') _composerEditorInit() { const topicId = this.get('topic.id'); - const template = this.container.lookup('template:user-selector-autocomplete.raw'); + const template = getOwner(this).lookup('template:user-selector-autocomplete.raw'); const $input = this.$('.d-editor-input'); $input.autocomplete({ template, diff --git a/app/assets/javascripts/discourse/components/composer-message.js.es6 b/app/assets/javascripts/discourse/components/composer-message.js.es6 index 39307dfccc..715afce98b 100644 --- a/app/assets/javascripts/discourse/components/composer-message.js.es6 +++ b/app/assets/javascripts/discourse/components/composer-message.js.es6 @@ -1,11 +1,12 @@ import computed from 'ember-addons/ember-computed-decorators'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Component.extend({ classNameBindings: [':composer-popup', ':hidden', 'message.extraClass'], @computed('message.templateName') defaultLayout(templateName) { - return this.container.lookup(`template:composer/${templateName}`); + return getOwner(this).lookup(`template:composer/${templateName}`); }, didInsertElement() { diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6 index e6dd2804dd..11feda8e3f 100644 --- a/app/assets/javascripts/discourse/components/d-editor.js.es6 +++ b/app/assets/javascripts/discourse/components/d-editor.js.es6 @@ -10,6 +10,8 @@ import { cook } from 'discourse/lib/text'; import { translations } from 'pretty-text/emoji/data'; import { emojiSearch } from 'pretty-text/emoji'; import { emojiUrlFor } from 'discourse/lib/text'; +import { getRegister } from 'discourse-common/lib/get-owner'; +import deprecated from 'discourse-common/lib/deprecated'; // Our head can be a static string or a function that returns a string // based on input (like for numbered lists). @@ -182,7 +184,7 @@ export function addToolbarCallback(func) { } export function onToolbarCreate(func) { - console.warn('`onToolbarCreate` is deprecated, use the plugin api instead.'); + deprecated('`onToolbarCreate` is deprecated, use the plugin api instead.'); addToolbarCallback(func); }; @@ -205,15 +207,18 @@ export default Ember.Component.extend({ this.set('ready', true); }, + init() { + this._super(); + this.register = getRegister(this); + }, + didInsertElement() { this._super(); - const container = this.get('container'), - $editorInput = this.$('.d-editor-input'); - - this._applyEmojiAutocomplete(container, $editorInput); - this._applyCategoryHashtagAutocomplete(container, $editorInput); + const $editorInput = this.$('.d-editor-input'); + this._applyEmojiAutocomplete($editorInput); + this._applyCategoryHashtagAutocomplete($editorInput); Ember.run.scheduleOnce('afterRender', this, this._readyNow); @@ -287,8 +292,8 @@ export default Ember.Component.extend({ } }, - _applyCategoryHashtagAutocomplete(container) { - const template = container.lookup('template:category-tag-autocomplete.raw'); + _applyCategoryHashtagAutocomplete() { + const template = this.register.lookup('template:category-tag-autocomplete.raw'); const siteSettings = this.siteSettings; this.$('.d-editor-input').autocomplete({ @@ -310,10 +315,11 @@ export default Ember.Component.extend({ }); }, - _applyEmojiAutocomplete(container, $editorInput) { + _applyEmojiAutocomplete($editorInput) { if (!this.siteSettings.enable_emoji) { return; } - const template = container.lookup('template:emoji-selector-autocomplete.raw'); + const register = this.register; + const template = this.register.lookup('template:emoji-selector-autocomplete.raw'); const self = this; $editorInput.autocomplete({ @@ -329,7 +335,7 @@ export default Ember.Component.extend({ } else { showSelector({ appendTo: self.$(), - container, + register, onSelect: title => { // Remove the previously type characters when a new emoji is selected from the selector. let selected = self._getSelected(); @@ -614,7 +620,7 @@ export default Ember.Component.extend({ emoji() { showSelector({ appendTo: this.$(), - container: this.container, + register: this.register, onSelect: title => this._addText(this._getSelected(), `:${title}:`) }); } diff --git a/app/assets/javascripts/discourse/components/group-selector.js.es6 b/app/assets/javascripts/discourse/components/group-selector.js.es6 index a2080ecda2..4f4fa0f6a5 100644 --- a/app/assets/javascripts/discourse/components/group-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/group-selector.js.es6 @@ -1,4 +1,5 @@ import { on, observes, default as computed } from 'ember-addons/ember-computed-decorators'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Component.extend({ @computed('placeholderKey') @@ -18,7 +19,7 @@ export default Ember.Component.extend({ var selectedGroups; var groupNames = this.get('groupNames'); - var template = this.container.lookup('template:group-selector-autocomplete.raw'); + var template = getOwner(this).lookup('template:group-selector-autocomplete.raw'); self.$('input').autocomplete({ allowAny: false, items: _.isArray(groupNames) ? groupNames : (Ember.isEmpty(groupNames)) ? [] : [groupNames], 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 66aaf82cb9..1d769faa09 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 deprecated from 'discourse-common/lib/deprecated'; + export function needsSecondRowIf() { - Ember.warn("DEPRECATION: `needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`"); + deprecated("`needsSecondRowIf` is deprecated. Use widget hooks on `header-second-row`"); } diff --git a/app/assets/javascripts/discourse/components/mount-widget.js.es6 b/app/assets/javascripts/discourse/components/mount-widget.js.es6 index e62390e694..3a4fb8bce5 100644 --- a/app/assets/javascripts/discourse/components/mount-widget.js.es6 +++ b/app/assets/javascripts/discourse/components/mount-widget.js.es6 @@ -2,6 +2,7 @@ import { keyDirty } from 'discourse/widgets/widget'; import { diff, patch } from 'virtual-dom'; import { WidgetClickHook } from 'discourse/widgets/hooks'; import { renderedKey, queryRegistry } from 'discourse/widgets/widget'; +import { getRegister } from 'discourse-common/lib/get-owner'; const _cleanCallbacks = {}; export function addWidgetCleanCallback(widgetName, fn) { @@ -24,12 +25,15 @@ export default Ember.Component.extend({ (this.get('delegated') || []).forEach(m => this.set(m, m)); - this._widgetClass = queryRegistry(name) || this.container.lookupFactory(`widget:${name}`); + this.register = getRegister(this); + + this._widgetClass = queryRegistry(name) || this.register.lookupFactory(`widget:${name}`); if (!this._widgetClass) { console.error(`Error: Could not find widget: ${name}`); } + this._childEvents = []; this._connected = []; this._dispatched = []; @@ -97,13 +101,14 @@ export default Ember.Component.extend({ rerenderWidget() { Ember.run.cancel(this._timeout); + if (this._rootNode) { if (!this._widgetClass) { return; } const t0 = new Date().getTime(); const args = this.get('args') || this.buildArgs(); const opts = { model: this.get('model') }; - const newTree = new this._widgetClass(args, this.container, opts); + const newTree = new this._widgetClass(args, this.register, opts); newTree._emberView = this; const patches = diff(this._tree || this._rootNode, newTree); diff --git a/app/assets/javascripts/discourse/components/nav-item.js.es6 b/app/assets/javascripts/discourse/components/nav-item.js.es6 index 69c797bd30..3a4b40dddf 100644 --- a/app/assets/javascripts/discourse/components/nav-item.js.es6 +++ b/app/assets/javascripts/discourse/components/nav-item.js.es6 @@ -1,6 +1,7 @@ /* You might be looking for navigation-item. */ import computed from "ember-addons/ember-computed-decorators"; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Component.extend({ tagName: 'li', @@ -8,7 +9,7 @@ export default Ember.Component.extend({ @computed() router() { - return this.container.lookup('router:main'); + return getOwner(this).lookup('router:main'); }, @computed("path") diff --git a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 index 1fc7cf53a6..d9b8569bb8 100644 --- a/app/assets/javascripts/discourse/components/topic-list-item.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list-item.js.es6 @@ -1,5 +1,6 @@ import computed from 'ember-addons/ember-computed-decorators'; import { bufferedRender } from 'discourse-common/lib/buffered-render'; +import { getOwner } from 'discourse-common/lib/get-owner'; export function showEntrance(e) { let target = $(e.target); @@ -11,7 +12,7 @@ export function showEntrance(e) { target = target.end(); } } - this.container.lookup('controller:application').send("showTopicEntrance", {topic: this.get('topic'), position: target.offset()}); + getOwner(this).lookup('controller:application').send("showTopicEntrance", {topic: this.get('topic'), position: target.offset()}); return false; } } @@ -30,7 +31,7 @@ export default Ember.Component.extend(bufferedRender({ }, buildBuffer(buffer) { - const template = Discourse.__container__.lookup('template:list/topic-list-item.raw'); + const template = getOwner(this).lookup('template:list/topic-list-item.raw'); if (template) { buffer.push(template(this)); } diff --git a/app/assets/javascripts/discourse/components/user-selector.js.es6 b/app/assets/javascripts/discourse/components/user-selector.js.es6 index e124b43fb2..94d6db9bfe 100644 --- a/app/assets/javascripts/discourse/components/user-selector.js.es6 +++ b/app/assets/javascripts/discourse/components/user-selector.js.es6 @@ -1,6 +1,7 @@ import { observes } from 'ember-addons/ember-computed-decorators'; import TextField from 'discourse/components/text-field'; import userSearch from 'discourse/lib/user-search'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default TextField.extend({ @observes('usernames') @@ -30,7 +31,7 @@ export default TextField.extend({ } this.$().val(this.get('usernames')).autocomplete({ - template: this.container.lookup('template:user-selector-autocomplete.raw'), + template: getOwner(this).lookup('template:user-selector-autocomplete.raw'), disabled: this.get('disabled'), single: this.get('single'), allowAny: this.get('allowAny'), diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index 30a5c7af93..f5b506a902 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -6,6 +6,7 @@ import { default as computed, observes } from 'ember-addons/ember-computed-decor import { relativeAge } from 'discourse/lib/formatter'; import { escapeExpression } from 'discourse/lib/utilities'; import InputValidation from 'discourse/models/input-validation'; +import { getOwner } from 'discourse-common/lib/get-owner'; function loadDraft(store, opts) { opts = opts || {}; @@ -85,8 +86,8 @@ export default Ember.Controller.extend({ }, showToolbar: Em.computed({ - get(){ - const keyValueStore = this.container.lookup('key-value-store:main'); + get() { + const keyValueStore = getOwner(this).lookup('key-value-store:main'); const storedVal = keyValueStore.get("toolbar-enabled"); if (this._toolbarEnabled === undefined && storedVal === undefined) { // iPhone 6 is 375, anything narrower and toolbar should @@ -97,7 +98,7 @@ export default Ember.Controller.extend({ return this._toolbarEnabled || storedVal === "true"; }, set(key, val){ - const keyValueStore = this.container.lookup('key-value-store:main'); + const keyValueStore = getOwner(this).lookup('key-value-store:main'); this._toolbarEnabled = val; keyValueStore.set({key: "toolbar-enabled", value: val ? "true" : "false"}); return val; diff --git a/app/assets/javascripts/discourse/controllers/header.js.es6 b/app/assets/javascripts/discourse/controllers/header.js.es6 index 2665f2afbd..f5979bb36a 100644 --- a/app/assets/javascripts/discourse/controllers/header.js.es6 +++ b/app/assets/javascripts/discourse/controllers/header.js.es6 @@ -1,6 +1,7 @@ import { addFlagProperty as realAddFlagProperty } from 'discourse/components/site-header'; +import deprecated from 'discourse-common/lib/deprecated'; export function addFlagProperty(prop) { - Ember.warn("importing `addFlagProperty` is deprecated. Use the PluginAPI instead"); + deprecated("importing `addFlagProperty` is deprecated. Use the PluginAPI instead"); realAddFlagProperty(prop); } diff --git a/app/assets/javascripts/discourse/lib/emoji/toolbar.js.es6 b/app/assets/javascripts/discourse/lib/emoji/toolbar.js.es6 index 68e9528184..d3b6e12f12 100644 --- a/app/assets/javascripts/discourse/lib/emoji/toolbar.js.es6 +++ b/app/assets/javascripts/discourse/lib/emoji/toolbar.js.es6 @@ -151,7 +151,7 @@ function render(page, offset, options) { }; $('.emoji-modal', options.appendTo).remove(); - const template = options.container.lookup('template:emoji-toolbar.raw'); + const template = options.register.lookup('template:emoji-toolbar.raw'); options.appendTo.append(template(model)); bindEvents(page, offset, options); diff --git a/app/assets/javascripts/discourse/lib/mobile.js.es6 b/app/assets/javascripts/discourse/lib/mobile.js.es6 index 940e18c44b..2ceb13ba51 100644 --- a/app/assets/javascripts/discourse/lib/mobile.js.es6 +++ b/app/assets/javascripts/discourse/lib/mobile.js.es6 @@ -1,3 +1,5 @@ +import deprecated from 'discourse-common/lib/deprecated'; + let mobileForced = false; // An object that is responsible for logic related to mobile devices. @@ -67,10 +69,9 @@ export function resetMobile() { mobileForced = false; } -// Backwards compatibiltity, deprecated Object.defineProperty(Discourse, 'Mobile', { - get: function() { - Ember.warn("DEPRECATION: `Discourse.Mobile` is deprecated, use `this.site.mobileView` instead"); + get() { + deprecated("`Discourse.Mobile` is deprecated, use `this.site.mobileView` instead"); return Mobile; } }); diff --git a/app/assets/javascripts/discourse/lib/page-tracker.js.es6 b/app/assets/javascripts/discourse/lib/page-tracker.js.es6 index 89deaafe03..ddef479268 100644 --- a/app/assets/javascripts/discourse/lib/page-tracker.js.es6 +++ b/app/assets/javascripts/discourse/lib/page-tracker.js.es6 @@ -1,3 +1,5 @@ +import deprecated from 'discourse-common/lib/deprecated'; + const PageTracker = Ember.Object.extend(Ember.Evented); let _pageTracker = PageTracker.create(); @@ -42,7 +44,7 @@ export function onPageChange(fn) { // backwards compatibility const BackwardsCompat = { current() { - console.warn(`Using PageTracker.current() is deprecated. Your plugin should use the PluginAPI`); + deprecated(`Using PageTracker.current() is deprecated. Your plugin should use the PluginAPI`); return _pageTracker; } }; diff --git a/app/assets/javascripts/discourse/lib/url.js.es6 b/app/assets/javascripts/discourse/lib/url.js.es6 index 7621ae4ce9..6aca579c13 100644 --- a/app/assets/javascripts/discourse/lib/url.js.es6 +++ b/app/assets/javascripts/discourse/lib/url.js.es6 @@ -332,7 +332,8 @@ const DiscourseURL = Ember.Object.extend({ const transition = router.handleURL(path); transition._discourse_intercepted = true; - transition.promise.then(() => jumpToElement(elementId)); + const promise = transition.promise || transition; + promise.then(() => jumpToElement(elementId)); } }).create(); diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index 87a589a5a3..b2f0225409 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -1,6 +1,7 @@ import { ajax } from 'discourse/lib/ajax'; import RestModel from 'discourse/models/rest'; import ResultSet from 'discourse/models/result-set'; +import { getRegister } from 'discourse-common/lib/get-owner'; let _identityMap; @@ -41,6 +42,11 @@ export default Ember.Object.extend({ _plurals: {'post-reply': 'post-replies', 'post-reply-history': 'post_reply_histories'}, + init() { + this._super(); + this.register = getRegister(this); + }, + pluralize(thing) { return this._plurals[thing] || thing + "s"; }, @@ -196,11 +202,11 @@ export default Ember.Object.extend({ obj.__state = obj.id ? "created" : "new"; // TODO: Have injections be automatic - obj.topicTrackingState = this.container.lookup('topic-tracking-state:main'); - obj.keyValueStore = this.container.lookup('key-value-store:main'); - obj.siteSettings = this.container.lookup('site-settings:main'); + obj.topicTrackingState = this.register.lookup('topic-tracking-state:main'); + obj.keyValueStore = this.register.lookup('key-value-store:main'); + obj.siteSettings = this.register.lookup('site-settings:main'); - const klass = this.container.lookupFactory('model:' + type) || RestModel; + const klass = this.register.lookupFactory('model:' + type) || RestModel; const model = klass.create(obj); storeMap(type, obj.id, model); @@ -208,7 +214,7 @@ export default Ember.Object.extend({ }, adapterFor(type) { - return this.container.lookup('adapter:' + type) || this.container.lookup('adapter:rest'); + return this.register.lookup('adapter:' + type) || this.register.lookup('adapter:rest'); }, _lookupSubType(subType, type, id, root) { @@ -287,7 +293,7 @@ export default Ember.Object.extend({ if (existing) { delete obj.id; - const klass = this.container.lookupFactory('model:' + type) || RestModel; + const klass = this.register.lookupFactory('model:' + type) || RestModel; existing.setProperties(klass.munge(obj)); obj.id = id; return existing; diff --git a/app/assets/javascripts/discourse/pre-initializers/map-routes.js.es6 b/app/assets/javascripts/discourse/pre-initializers/map-routes.js.es6 index 94629c17ba..0a593c11bd 100644 --- a/app/assets/javascripts/discourse/pre-initializers/map-routes.js.es6 +++ b/app/assets/javascripts/discourse/pre-initializers/map-routes.js.es6 @@ -10,7 +10,10 @@ export default { // HACK to fix: https://github.com/emberjs/ember.js/issues/10310 const originalBuildInstance = originalBuildInstance || Ember.Application.prototype.buildInstance; Ember.Application.prototype.buildInstance = function() { - this.registry = this.buildRegistry(); + const registry = this.buildRegistry(); + if (Ember.VERSION[0] === "1") { + this.registry = registry; + } return originalBuildInstance.apply(this); }; } diff --git a/app/assets/javascripts/discourse/routes/application.js.es6 b/app/assets/javascripts/discourse/routes/application.js.es6 index d506852e36..fab8595cd3 100644 --- a/app/assets/javascripts/discourse/routes/application.js.es6 +++ b/app/assets/javascripts/discourse/routes/application.js.es6 @@ -6,6 +6,7 @@ import OpenComposer from "discourse/mixins/open-composer"; import Category from 'discourse/models/category'; import mobile from 'discourse/lib/mobile'; import { findAll } from 'discourse/models/login-method'; +import { getOwner } from 'discourse-common/lib/get-owner'; function unlessReadOnly(method, message) { return function() { @@ -40,7 +41,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { // Ember doesn't provider a router `willTransition` event so let's make one willTransition() { - var router = this.container.lookup('router:main'); + var router = getOwner(this).lookup('router:main'); Ember.run.once(router, router.trigger, 'willTransition'); return this._super(); }, @@ -150,7 +151,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { changeBulkTemplate(w) { const controllerName = w.replace('modal/', ''), - factory = this.container.lookupFactory('controller:' + controllerName); + factory = getOwner(this).lookupFactory('controller:' + controllerName); this.render(w, {into: 'modal/topic-bulk-actions', outlet: 'bulkOutlet', controller: factory ? controllerName : 'topic-bulk-actions'}); }, @@ -200,7 +201,7 @@ const ApplicationRoute = Discourse.Route.extend(OpenComposer, { _autoLogin(modal, modalClass, notAuto) { const methods = findAll(this.siteSettings, - this.container.lookup('capabilities:main'), + getOwner(this).lookup('capabilities:main'), this.site.isMobileDevice); if (!this.siteSettings.enable_local_logins && methods.length === 1) { diff --git a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 index 805f1e1b91..eb0cc01092 100644 --- a/app/assets/javascripts/discourse/routes/full-page-search.js.es6 +++ b/app/assets/javascripts/discourse/routes/full-page-search.js.es6 @@ -3,6 +3,7 @@ import { translateResults, getSearchKey, isValidSearchTerm } from "discourse/lib import Composer from 'discourse/models/composer'; import PreloadStore from 'preload-store'; import { getTransient, setTransient } from 'discourse/lib/page-tracker'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Discourse.Route.extend({ queryParams: { q: {}, expanded: false, context_id: {}, context: {}, skip_context: {} }, @@ -52,7 +53,7 @@ export default Discourse.Route.extend({ category = match[1]; } } - this.container.lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC, topicCategory: category}); + getOwner(this).lookup('controller:composer').open({action: Composer.CREATE_TOPIC, draftKey: Composer.CREATE_TOPIC, topicCategory: category}); } } 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 7c96c1a264..2212da06b9 100644 --- a/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 +++ b/app/assets/javascripts/discourse/routes/topic-from-params.js.es6 @@ -56,8 +56,10 @@ export default Discourse.Route.extend({ ignoreIfChanged: true }); } - }).catch(function(e) { - Ember.warn('Could not view topic', e); + }).catch(e => { + if (!Ember.testing) { + console.log('Could not view topic', e); + } }); } diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index 2551444d59..2c47fc8fcd 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -17,7 +17,7 @@
{{#if site.mobileView}} - + {{/if}} diff --git a/app/assets/javascripts/discourse/widgets/connector.js.es6 b/app/assets/javascripts/discourse/widgets/connector.js.es6 index c132666a41..d197914af2 100644 --- a/app/assets/javascripts/discourse/widgets/connector.js.es6 +++ b/app/assets/javascripts/discourse/widgets/connector.js.es6 @@ -21,7 +21,7 @@ export default class Connector { } const view = Ember.View.create({ - container: widget.container, + container: widget.register, templateName: opts.templateName, context }); diff --git a/app/assets/javascripts/discourse/widgets/decorator-helper.js.es6 b/app/assets/javascripts/discourse/widgets/decorator-helper.js.es6 index a949d0a628..2ee8df8723 100644 --- a/app/assets/javascripts/discourse/widgets/decorator-helper.js.es6 +++ b/app/assets/javascripts/discourse/widgets/decorator-helper.js.es6 @@ -8,7 +8,8 @@ class DecoratorHelper { this.widget = widget; this.attrs = attrs; this.state = state; - this.container = widget.container; + this.register = widget.register; + this.register.deprecateContainer(this); } /** diff --git a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 index f0e5319eb6..5fa48a8413 100644 --- a/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/hamburger-menu.js.es6 @@ -63,7 +63,7 @@ export default createWidget('hamburger-menu', { }, lookupCount(type) { - const tts = this.container.lookup('topic-tracking-state:main'); + const tts = this.register.lookup('topic-tracking-state:main'); return tts ? tts.lookupCount(type) : 0; }, diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 index 80dc5b0f92..5f1f1e2133 100644 --- a/app/assets/javascripts/discourse/widgets/header.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -190,7 +190,7 @@ export default createWidget('header', { updateHighlight() { if (!this.state.searchVisible) { - const service = this.container.lookup('search-service:main'); + const service = this.register.lookup('search-service:main'); service.set('highlightTerm', ''); } }, @@ -208,7 +208,7 @@ export default createWidget('header', { toggleSearchMenu() { if (this.site.mobileView) { - const searchService = this.container.lookup('search-service:main'); + const searchService = this.register.lookup('search-service:main'); const context = searchService.get('searchContext'); var params = ""; @@ -240,7 +240,7 @@ export default createWidget('header', { state.contextEnabled = false; - const currentPath = this.container.lookup('controller:application').get('currentPath'); + const currentPath = this.register.lookup('controller:application').get('currentPath'); const blacklist = [ /^discovery\.categories/ ]; const whitelist = [ /^topic\./ ]; const check = function(regex) { return !!currentPath.match(regex); }; @@ -249,7 +249,7 @@ export default createWidget('header', { // If we're viewing a topic, only intercept search if there are cloaked posts if (showSearch && currentPath.match(/^topic\./)) { showSearch = ($('.topic-post .cooked, .small-action:not(.time-gap)').length < - this.container.lookup('controller:topic').get('model.postStream.stream.length')); + this.register.lookup('controller:topic').get('model.postStream.stream.length')); } if (state.searchVisible) { diff --git a/app/assets/javascripts/discourse/widgets/link.js.es6 b/app/assets/javascripts/discourse/widgets/link.js.es6 index 7eabce3975..9ba519fff4 100644 --- a/app/assets/javascripts/discourse/widgets/link.js.es6 +++ b/app/assets/javascripts/discourse/widgets/link.js.es6 @@ -10,7 +10,7 @@ export default createWidget('link', { href(attrs) { const route = attrs.route; if (route) { - const router = this.container.lookup('router:main'); + const router = this.register.lookup('router:main'); if (router && router.router) { const params = [route]; if (attrs.model) { diff --git a/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 index 5be3ce3543..70d046f7d5 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu-controls.js.es6 @@ -30,7 +30,7 @@ createWidget('search-context', { tagName: 'div.search-context', html(attrs) { - const service = this.container.lookup('search-service:main'); + const service = this.register.lookup('search-service:main'); const ctx = service.get('searchContext'); const result = []; diff --git a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 index 9ed3b8c7d4..f8d626f456 100644 --- a/app/assets/javascripts/discourse/widgets/search-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/search-menu.js.es6 @@ -134,7 +134,7 @@ export default createWidget('search-menu', { searchService() { if (!this._searchService) { - this._searchService = this.container.lookup('search-service:main'); + this._searchService = this.register.lookup('search-service:main'); } return this._searchService; }, diff --git a/app/assets/javascripts/discourse/widgets/widget.js.es6 b/app/assets/javascripts/discourse/widgets/widget.js.es6 index 851ad09bd9..76f8c60fba 100644 --- a/app/assets/javascripts/discourse/widgets/widget.js.es6 +++ b/app/assets/javascripts/discourse/widgets/widget.js.es6 @@ -127,12 +127,14 @@ export function createWidget(name, opts) { } export default class Widget { - constructor(attrs, container, opts) { + constructor(attrs, register, opts) { opts = opts || {}; this.attrs = attrs || {}; this.mergeState = opts.state; - this.container = container; this.model = opts.model; + this.register = register; + + register.deprecateContainer(this); this.key = this.buildKey ? this.buildKey(attrs) : null; @@ -146,13 +148,13 @@ export default class Widget { } } - this.site = container.lookup('site:main'); - this.siteSettings = container.lookup('site-settings:main'); - this.currentUser = container.lookup('current-user:main'); - this.capabilities = container.lookup('capabilities:main'); - this.store = container.lookup('store:main'); - this.appEvents = container.lookup('app-events:main'); - this.keyValueStore = container.lookup('key-value-store:main'); + this.site = register.lookup('site:main'); + this.siteSettings = register.lookup('site-settings:main'); + this.currentUser = register.lookup('current-user:main'); + this.capabilities = register.lookup('capabilities:main'); + this.store = register.lookup('store:main'); + this.appEvents = register.lookup('app-events:main'); + this.keyValueStore = register.lookup('key-value-store:main'); if (this.name) { const custom = _customSettings[this.name]; @@ -223,15 +225,15 @@ export default class Widget { let WidgetClass = _registry[widgetName]; if (!WidgetClass) { - if (!this.container) { - console.error("couldn't find container"); + if (!this.register) { + console.error("couldn't find register"); return; } - WidgetClass = this.container.lookupFactory(`widget:${widgetName}`); + WidgetClass = this.register.lookupFactory(`widget:${widgetName}`); } if (WidgetClass) { - const result = new WidgetClass(attrs, this.container, opts); + const result = new WidgetClass(attrs, this.register, opts); result.parentWidget = this; return result; } else { diff --git a/app/assets/javascripts/wizard/components/wizard-field-image.js.es6 b/app/assets/javascripts/wizard/components/wizard-field-image.js.es6 index bb5f796ced..370cf12c75 100644 --- a/app/assets/javascripts/wizard/components/wizard-field-image.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-field-image.js.es6 @@ -1,6 +1,7 @@ import getUrl from 'discourse-common/lib/get-url'; import computed from 'ember-addons/ember-computed-decorators'; import { getToken } from 'wizard/lib/ajax'; +import { getOwner } from 'discourse-common/lib/get-owner'; export default Ember.Component.extend({ classNames: ['wizard-image-row'], @@ -9,7 +10,7 @@ export default Ember.Component.extend({ @computed('field.id') previewComponent(id) { const componentName = `image-preview-${Ember.String.dasherize(id)}`; - const exists = this.container.lookup(`component:${componentName}`); + const exists = getOwner(this).lookup(`component:${componentName}`); return exists ? componentName : 'wizard-image-preview'; }, From 6b65c009e120c346f160048454202f0b8e4d4d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Mon, 7 Nov 2016 22:22:25 +0100 Subject: [PATCH 033/266] bump onebox --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 10a495336a..d1dfeace9c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -212,7 +212,7 @@ GEM omniauth-twitter (1.2.1) json (~> 1.3) omniauth-oauth (~> 1.1) - onebox (1.5.61) + onebox (1.5.62) htmlentities (~> 4.3.4) moneta (~> 0.8) multi_json (~> 1.11) From c8ac8d02f25bed294e1251ee3d369b88ebdacd30 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Mon, 7 Nov 2016 17:02:33 -0800 Subject: [PATCH 034/266] better copy for entropy / gibberish / allcaps err --- config/locales/server.en.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index c726595eb3..e693cdf043 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -94,7 +94,7 @@ en: has_already_been_used: "has already been used" inclusion: is not included in the list invalid: is invalid - is_invalid: "is invalid; try to be a little more descriptive" + is_invalid: "seems unclear, is it a complete sentence?" less_than: must be less than %{count} less_than_or_equal_to: must be less than or equal to %{count} not_a_number: is not a number @@ -196,7 +196,7 @@ en: just_posted_that: "is too similar to what you recently posted" invalid_characters: "contains invalid characters" - is_invalid: "is invalid; try to be a little more descriptive" + is_invalid: "seems unclear, is it a complete sentence?" next_page: "next page →" prev_page: "← previous page" page_num: "Page %{num}" From ac2c035856ad0cdb2ca7a3b5ccf7df18e15a2914 Mon Sep 17 00:00:00 2001 From: Sam Date: Tue, 8 Nov 2016 14:43:54 +1100 Subject: [PATCH 035/266] FIX: stop raising exceptions when a post goes missing --- app/jobs/regular/emit_web_hook_event.rb | 72 ++++++++++++++----------- spec/jobs/emit_web_hook_event_spec.rb | 6 +++ 2 files changed, 47 insertions(+), 31 deletions(-) diff --git a/app/jobs/regular/emit_web_hook_event.rb b/app/jobs/regular/emit_web_hook_event.rb index 72e5f5923a..2186ca9baa 100644 --- a/app/jobs/regular/emit_web_hook_event.rb +++ b/app/jobs/regular/emit_web_hook_event.rb @@ -6,34 +6,48 @@ module Jobs raise Discourse::InvalidParameters.new(:web_hook_id) unless args[:web_hook_id].present? raise Discourse::InvalidParameters.new(:event_type) unless args[:event_type].present? - @web_hook = WebHook.find(args[:web_hook_id]) + args = args.dup - unless args[:event_type] == 'ping' - return unless @web_hook.active? - return if @web_hook.group_ids.present? && (args[:group_id].present? || - !@web_hook.group_ids.include?(args[:group_id])) - return if @web_hook.category_ids.present? && (!args[:category_id].present? || - !@web_hook.category_ids.include?(args[:category_id])) + if args[:topic_id] + args[:topic_view] = TopicView.new(args[:topic_id], Discourse.system_user) end - @opts = args + if args[:post_id] + # deleted post so skip + return unless args[:post] = Post.find_by(id: args[:post_id]) + end - web_hook_request + if args[:user_id] + return unless args[:user] = User.find_by(id: args[:user_id]) + end + + web_hook = WebHook.find(args[:web_hook_id]) + + unless args[:event_type] == 'ping' + return unless web_hook.active? + return if web_hook.group_ids.present? && (args[:group_id].present? || + !web_hook.group_ids.include?(args[:group_id])) + return if web_hook.category_ids.present? && (!args[:category_id].present? || + !web_hook.category_ids.include?(args[:category_id])) + end + + web_hook_request(args, web_hook) end private - def web_hook_request - uri = URI(@web_hook.payload_url) + def web_hook_request(args, web_hook) + + uri = URI(web_hook.payload_url) conn = Excon.new(uri.to_s, - ssl_verify_peer: @web_hook.verify_certificate, + ssl_verify_peer: web_hook.verify_certificate, retry_limit: 0) - body = build_web_hook_body - web_hook_event = WebHookEvent.create!(web_hook_id: @web_hook.id) + body = build_web_hook_body(args, web_hook) + web_hook_event = WebHookEvent.create!(web_hook_id: web_hook.id) begin - content_type = case @web_hook.content_type + content_type = case web_hook.content_type when WebHook.content_types['application/x-www-form-urlencoded'] 'application/x-www-form-urlencoded' else @@ -48,12 +62,12 @@ module Jobs 'User-Agent' => "Discourse/" + Discourse::VERSION::STRING, 'X-Discourse-Instance' => Discourse.base_url, 'X-Discourse-Event-Id' => web_hook_event.id, - 'X-Discourse-Event-Type' => @opts[:event_type] + 'X-Discourse-Event-Type' => args[:event_type] } - headers['X-Discourse-Event'] = @opts[:event_name].to_s if @opts[:event_name].present? + headers['X-Discourse-Event'] = args[:event_name].to_s if args[:event_name].present? - if @web_hook.secret.present? - headers['X-Discourse-Event-Signature'] = "sha256=" + OpenSSL::HMAC.hexdigest("sha256", @web_hook.secret, body) + if web_hook.secret.present? + headers['X-Discourse-Event-Signature'] = "sha256=" + OpenSSL::HMAC.hexdigest("sha256", web_hook.secret, body) end now = Time.zone.now @@ -68,33 +82,29 @@ module Jobs response_headers: MultiJson.dump(response.headers), response_body: response.body, duration: ((Time.zone.now - now) * 1000).to_i) - MessageBus.publish("/web_hook_events/#{@web_hook.id}", { + MessageBus.publish("/web_hook_events/#{web_hook.id}", { web_hook_event_id: web_hook_event.id, - event_type: @opts[:event_type] + event_type: args[:event_type] }, user_ids: User.staff.pluck(:id)) end - def build_web_hook_body + def build_web_hook_body(args, web_hook) body = {} - web_hook_user = Discourse.system_user - guardian = Guardian.new(web_hook_user) + guardian = Guardian.new(Discourse.system_user) - if @opts[:topic_id] - topic_view = TopicView.new(@opts[:topic_id], web_hook_user) + if topic_view = args[:topic_view] body[:topic] = TopicViewSerializer.new(topic_view, scope: guardian, root: false).as_json end - if @opts[:post_id] - post = Post.find(@opts[:post_id]) + if post = args[:post] body[:post] = PostSerializer.new(post, scope: guardian, root: false).as_json end - if @opts[:user_id] - user = User.find(@opts[:user_id]) + if user = args[:user] body[:user] = UserSerializer.new(user, scope: guardian, root: false).as_json end - body[:ping] = 'OK' if @opts[:event_type] == 'ping' + body[:ping] = 'OK' if args[:event_type] == 'ping' raise Discourse::InvalidParameters.new if body.empty? diff --git a/spec/jobs/emit_web_hook_event_spec.rb b/spec/jobs/emit_web_hook_event_spec.rb index e7ba30fb02..c01ecb6569 100644 --- a/spec/jobs/emit_web_hook_event_spec.rb +++ b/spec/jobs/emit_web_hook_event_spec.rb @@ -69,6 +69,12 @@ describe Jobs::EmitWebHookEvent do end.to change(WebHookEvent, :count).by(1) end + it 'skips silently on missing post' do + expect do + subject.execute(web_hook_id: post_hook.id, event_type: 'post', post_id: (Post.maximum(:id).to_i + 1)) + end.not_to raise_error + end + it 'sets up proper request headers' do Excon.stub({ url: "https://meta.discourse.org/webhook_listener" }, { headers: { test: 'string' }, body: 'OK', status: 200 }) From a8b7599d4a4647b24b375ab1c42f0885379cd731 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 8 Nov 2016 16:12:40 +0800 Subject: [PATCH 036/266] FEATURE: Add a radial ping when user's first notification has not been read. --- .../subscribe-user-notifications.js.es6 | 1 + .../discourse/widgets/header.js.es6 | 2 + .../stylesheets/common/base/discourse.scss | 37 +++++++++++++++++++ .../stylesheets/common/base/header.scss | 4 +- app/models/user.rb | 5 +++ app/serializers/current_user_serializer.rb | 1 + spec/models/user_spec.rb | 23 ++++++++++++ 7 files changed, 71 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 index 7583982d28..8061b579b9 100644 --- a/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 +++ b/app/assets/javascripts/discourse/initializers/subscribe-user-notifications.js.es6 @@ -41,6 +41,7 @@ export default { user.set('unread_notifications', data.unread_notifications); user.set('unread_private_messages', data.unread_private_messages); + user.set('read_first_notification', data.read_first_notification); if (oldUnread !== data.unread_notifications || oldPM !== data.unread_private_messages) { appEvents.trigger('notifications:changed'); diff --git a/app/assets/javascripts/discourse/widgets/header.js.es6 b/app/assets/javascripts/discourse/widgets/header.js.es6 index 5f1f1e2133..1e01d0263d 100644 --- a/app/assets/javascripts/discourse/widgets/header.js.es6 +++ b/app/assets/javascripts/discourse/widgets/header.js.es6 @@ -42,6 +42,8 @@ createWidget('header-notifications', { const unreadPMs = currentUser.get('unread_private_messages'); if (!!unreadPMs) { + if (!currentUser.get('read_first_notification')) contents.push(h('span.ring')); + contents.push(this.attach('link', { action: attrs.action, className: 'badge-notification unread-private-messages', rawLabel: unreadPMs })); diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 0d68b07b9a..772ad99e89 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -166,6 +166,43 @@ body { &.badge-notification[href] {color: $secondary;} } +.ring { + top: -13.4px !important; + right: 22.4px !important; + border-radius: 15px; + width: 20px; + height: 20px; + border: solid #FF0000 1px; + -moz-animation-iteration-count: infinite; + -webkit-animation-iteration-count: infinite; + -webkit-transform-origin: center; + -moz-animation-duration: 3s; + -webkit-animation-duration: 3s; + -moz-animation-name: ping; + -webkit-animation-name: ping; +} + +@-webkit-keyframes ping { + from { + $scale: 0.25; + transform: scale($scale); + -ms-transform: scale($scale); + -webkit-transform: scale($scale); + -o-transform: scale($scale); + -moz-transform: scale($scale); + opacity: 1; + } + to { + $scale: 2.5; + transform: scale($scale); + -ms-transform: scale($scale); + -webkit-transform: scale($scale); + -o-transform: scale($scale); + -moz-transform: scale($scale); + opacity: 0; + } +} + .fade { opacity: 0; transition: opacity 0.15s linear; diff --git a/app/assets/stylesheets/common/base/header.scss b/app/assets/stylesheets/common/base/header.scss index e1f2408f45..1991799767 100644 --- a/app/assets/stylesheets/common/base/header.scss +++ b/app/assets/stylesheets/common/base/header.scss @@ -123,7 +123,7 @@ .notifications { position: relative; } - .badge-notification { + .badge-notification, .ring { position: absolute; top: -9px; z-index: 1; @@ -133,7 +133,7 @@ right: 0; background-color: scale-color($tertiary, $lightness: 50%); } - .unread-private-messages { + .unread-private-messages, .ring { right: 25px; } .flagged-posts { diff --git a/app/models/user.rb b/app/models/user.rb index aacae0c970..062bc47363 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -343,6 +343,10 @@ class User < ActiveRecord::Base end end + def read_first_notification? + notifications.order(created_at: :asc).first.read + end + def publish_notifications_state # publish last notification json with the message so we # can apply an update @@ -384,6 +388,7 @@ class User < ActiveRecord::Base {unread_notifications: unread_notifications, unread_private_messages: unread_private_messages, total_unread_notifications: total_unread_notifications, + read_first_notification: read_first_notification?, last_notification: json, recent: recent, seen_notification_id: seen_notification_id diff --git a/app/serializers/current_user_serializer.rb b/app/serializers/current_user_serializer.rb index 498632e4a7..f574ef3ccb 100644 --- a/app/serializers/current_user_serializer.rb +++ b/app/serializers/current_user_serializer.rb @@ -6,6 +6,7 @@ class CurrentUserSerializer < BasicUserSerializer :total_unread_notifications, :unread_notifications, :unread_private_messages, + :read_first_notification?, :admin?, :notification_channel_position, :site_flagged_posts_count, diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index f0bea747de..8e8218110c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1314,4 +1314,27 @@ describe User do end end + describe '#read_first_notification?' do + let(:user) { Fabricate(:user) } + let(:notification) { Fabricate(:private_message_notification, user: user) } + let(:other_notification) { Fabricate(:private_message_notification, user: user) } + + describe 'when first notification has not been read' do + it 'should return the right value' do + notification.update_attributes!(read: false) + other_notification.update_attributes!(read: true) + + expect(user.read_first_notification?).to eq(false) + end + end + + describe 'when first notification has been read' do + it 'should return the right value' do + notification.update_attributes!(read: true) + other_notification.update_attributes!(read: false) + + expect(user.read_first_notification?).to eq(true) + end + end + end end From b18439a1e217ebd8172f2e3bde57e951983e3309 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Tue, 8 Nov 2016 17:00:44 +0800 Subject: [PATCH 037/266] Fix build. --- app/models/user.rb | 2 +- spec/models/user_spec.rb | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/models/user.rb b/app/models/user.rb index 062bc47363..3de3020d17 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -344,7 +344,7 @@ class User < ActiveRecord::Base end def read_first_notification? - notifications.order(created_at: :asc).first.read + notifications.order(created_at: :asc).first&.read || false end def publish_notifications_state diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 8e8218110c..cddc4b50a8 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1336,5 +1336,11 @@ describe User do expect(user.read_first_notification?).to eq(true) end end + + describe 'when user does not have any notifications' do + it 'should return the right value' do + expect(user.read_first_notification?).to eq(false) + end + end end end From b7532b5247ff2a97ce917539bfc07c49d826361d Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Tue, 8 Nov 2016 15:03:22 +0530 Subject: [PATCH 038/266] FIX: download archive confirm message was broken for non-staff users --- .../javascripts/discourse/controllers/user-activity.js.es6 | 2 +- config/locales/client.en.yml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 index b899408f42..4e9a063060 100644 --- a/app/assets/javascripts/discourse/controllers/user-activity.js.es6 +++ b/app/assets/javascripts/discourse/controllers/user-activity.js.es6 @@ -23,7 +23,7 @@ export default Ember.Controller.extend({ actions: { exportUserArchive() { bootbox.confirm( - I18n.t("admin.export_csv.user_archive_confirm"), + I18n.t("user.download_archive_confirm"), I18n.t("no_value"), I18n.t("yes_value"), function(confirmed) { diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index d6a10c0d73..3a00814e54 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -488,6 +488,7 @@ en: mute: "Mute" edit: "Edit Preferences" download_archive: "Download My Posts" + download_archive_confirm: "Are you sure you want to download your posts?" new_private_message: "New Message" private_message: "Message" private_messages: "Messages" @@ -2621,7 +2622,6 @@ en: confirm: "Are you sure you want to rollback the database to the previous working state?" export_csv: - user_archive_confirm: "Are you sure you want to download your posts?" success: "Export initiated, you will be notified via message when the process is complete." failed: "Export failed. Please check the logs." rate_limit_error: "Posts can be downloaded once per day, please try again tomorrow." @@ -3302,5 +3302,3 @@ en: admin: "Admin" moderator: "Moderator" regular: "Regular User" - - From ba4ef3424fbacb035cdd71cffb0738adaa211f61 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Tue, 8 Nov 2016 03:57:32 -0800 Subject: [PATCH 039/266] minor tweak to ping effect --- app/assets/stylesheets/common/base/discourse.scss | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 772ad99e89..987c304206 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -167,12 +167,12 @@ body { } .ring { - top: -13.4px !important; - right: 22.4px !important; - border-radius: 15px; + top: -13px !important; + right: 21px !important; + border-radius: 100%; width: 20px; height: 20px; - border: solid #FF0000 1px; + border: solid #090 3px; -moz-animation-iteration-count: infinite; -webkit-animation-iteration-count: infinite; -webkit-transform-origin: center; @@ -193,7 +193,7 @@ body { opacity: 1; } to { - $scale: 2.5; + $scale: 2; transform: scale($scale); -ms-transform: scale($scale); -webkit-transform: scale($scale); From 78cd42943ffea0c7d525209be8c510f9a38cca87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Tue, 8 Nov 2016 16:36:09 +0100 Subject: [PATCH 040/266] FEATURE: add 'emoji-custom' class to custom emojis --- app/assets/javascripts/pretty-text/emoji.js.es6 | 7 ++++++- .../pretty-text/engines/discourse-markdown/emoji.js.es6 | 8 +++++--- lib/pretty_text.rb | 5 ++++- lib/pretty_text/shims.js | 1 + 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/pretty-text/emoji.js.es6 b/app/assets/javascripts/pretty-text/emoji.js.es6 index 786c31d031..fe68c16422 100644 --- a/app/assets/javascripts/pretty-text/emoji.js.es6 +++ b/app/assets/javascripts/pretty-text/emoji.js.es6 @@ -35,15 +35,20 @@ export function performEmojiUnescape(string, opts) { const emojiVal = isEmoticon ? translations[m] : m.slice(1, m.length - 1); const hasEndingColon = m.lastIndexOf(":") === m.length - 1; const url = buildEmojiUrl(emojiVal, opts); + const classes = isCustomEmoji(emojiVal) ? "emoji emoji-custom" : "emoji"; return url && (isEmoticon || hasEndingColon) ? - `${emojiVal}` : m; + `${emojiVal}` : m; }); } return string; } +export function isCustomEmoji(code) { + return extendedEmoji.hasOwnProperty(code.toLowerCase()); +} + export function buildEmojiUrl(code, opts) { let url; code = code.toLowerCase(); diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 index 167209cd47..37d518928b 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/emoji.js.es6 @@ -1,5 +1,5 @@ import { registerOption } from 'pretty-text/pretty-text'; -import { buildEmojiUrl } from 'pretty-text/emoji'; +import { buildEmojiUrl, isCustomEmoji } from 'pretty-text/emoji'; import { translations } from 'pretty-text/emoji/data'; let _unicodeReplacements; @@ -38,10 +38,12 @@ export function setup(helper) { function imageFor(code) { code = code.toLowerCase(); - const url = buildEmojiUrl(code, helper.getOptions()); + const options = helper.getOptions(); + const url = buildEmojiUrl(code, options); if (url) { const title = `:${code}:`; - return ['img', { href: url, title, 'class': 'emoji', alt: title }]; + const classes = isCustomEmoji(code) ? "emoji emoji-custom" : "emoji"; + return ['img', { href: url, title, 'class': classes, alt: title }]; } } diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index 691ff158b5..d3d6d4ed42 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -154,7 +154,10 @@ module PrettyText context.eval("__optInput.mentionLookup = __mentionLookup;") custom_emoji = {} - Emoji.custom.map {|e| custom_emoji[e.name] = e.url} + Emoji.custom.map do |e| + context.eval("__registerEmoji('#{e.name}', '#{e.url}')") + custom_emoji[e.name] = e.url + end context.eval("__optInput.customEmoji = #{custom_emoji.to_json};") context.eval('__textOptions = __buildOptions(__optInput);') diff --git a/lib/pretty_text/shims.js b/lib/pretty_text/shims.js index c156fdb0cd..ae4803534c 100644 --- a/lib/pretty_text/shims.js +++ b/lib/pretty_text/shims.js @@ -1,6 +1,7 @@ __PrettyText = require('pretty-text/pretty-text').default; __buildOptions = require('pretty-text/pretty-text').buildOptions; __performEmojiUnescape = require('pretty-text/emoji').performEmojiUnescape; +__registerEmoji = require('pretty-text/emoji').registerEmoji; __utils = require('discourse/lib/utilities'); __setUnicode = require('pretty-text/engines/discourse-markdown/emoji').setUnicodeReplacements; From 151597bf0fbcdca82b129b6a59cdb8529c34d7ba Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 8 Nov 2016 13:40:35 -0500 Subject: [PATCH 041/266] Update code so Ember 2.3 can have more tests passing --- .../javascripts/discourse/models/store.js.es6 | 2 +- .../discourse/widgets/post-menu.js.es6 | 16 ++++++---- .../components/text-field-test.js.es6 | 29 ++++++++++++------- .../javascripts/helpers/component-test.js.es6 | 16 +++++++++- test/javascripts/helpers/create-store.js.es6 | 3 +- 5 files changed, 46 insertions(+), 20 deletions(-) diff --git a/app/assets/javascripts/discourse/models/store.js.es6 b/app/assets/javascripts/discourse/models/store.js.es6 index b2f0225409..753f2dd5ab 100644 --- a/app/assets/javascripts/discourse/models/store.js.es6 +++ b/app/assets/javascripts/discourse/models/store.js.es6 @@ -44,7 +44,7 @@ export default Ember.Object.extend({ init() { this._super(); - this.register = getRegister(this); + this.register = this.register || getRegister(this); }, pluralize(thing) { diff --git a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 index bbffb5be37..4c39f4a542 100644 --- a/app/assets/javascripts/discourse/widgets/post-menu.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post-menu.js.es6 @@ -318,14 +318,18 @@ export default createWidget('post-menu', { const $heart = $(`[data-post-id=${attrs.id}] .fa-heart`); $heart.closest('button').addClass('has-like'); - const scale = [1.0, 1.5]; - return new Ember.RSVP.Promise(resolve => { - animateHeart($heart, scale[0], scale[1], () => { - animateHeart($heart, scale[1], scale[0], () => { - this.sendWidgetAction('toggleLike').then(() => resolve()); + if (!Ember.testing) { + const scale = [1.0, 1.5]; + return new Ember.RSVP.Promise(resolve => { + animateHeart($heart, scale[0], scale[1], () => { + animateHeart($heart, scale[1], scale[0], () => { + this.sendWidgetAction('toggleLike').then(() => resolve()); + }); }); }); - }); + } else { + this.sendWidgetAction('toggleLike'); + } }, refreshLikes() { diff --git a/test/javascripts/components/text-field-test.js.es6 b/test/javascripts/components/text-field-test.js.es6 index 3b93991342..81b0660052 100644 --- a/test/javascripts/components/text-field-test.js.es6 +++ b/test/javascripts/components/text-field-test.js.es6 @@ -1,17 +1,24 @@ -moduleForComponent("text-field", {needs: []}); +import componentTest from 'helpers/component-test'; -test("renders correctly with no properties set", function() { - var component = this.subject(); - equal(component.get('type'), "text"); +moduleForComponent("text-field", { integration: true }); + +componentTest("renders correctly with no properties set", { + template: `{{text-field}}`, + + test(assert) { + assert.ok(this.$('input[type=text]').length); + } }); -test("support a placeholder", function() { - sandbox.stub(I18n, "t").returnsArg(0); +componentTest("support a placeholder", { + template: `{{text-field placeholderKey="placeholder.i18n.key"}}`, - var component = this.subject({ - placeholderKey: "placeholder.i18n.key" - }); + setup() { + sandbox.stub(I18n, "t").returnsArg(0); + }, - equal(component.get('type'), "text"); - equal(component.get('placeholder'), "placeholder.i18n.key"); + test(assert) { + assert.ok(this.$('input[type=text]').length); + assert.equal(this.$('input').prop('placeholder'), 'placeholder.i18n.key'); + } }); diff --git a/test/javascripts/helpers/component-test.js.es6 b/test/javascripts/helpers/component-test.js.es6 index 69b6b67ab0..be0be14a73 100644 --- a/test/javascripts/helpers/component-test.js.es6 +++ b/test/javascripts/helpers/component-test.js.es6 @@ -36,10 +36,24 @@ export default function(name, opts) { this.registry.register('store:main', store, { instantiate: false }); if (opts.setup) { + if (Ember.VERSION[0] === "2") { + // use closure actions instead + this.on = (actionName, fn) => this.set(actionName, fn); + } + opts.setup.call(this, store); } - andThen(() => this.render(opts.template)); + andThen(() => { + // TODO: This shouldn't be necessary + this.owner = { + hasRegistration: (...args) => this.registry.has(...args), + lookup: (...args) => this.container.lookup(...args), + _lookupFactory: (...args) => this.container.lookupFactory(...args) + }; + + return this.render(opts.template); + }); andThen(() => opts.test.call(this, assert)); }); } diff --git a/test/javascripts/helpers/create-store.js.es6 b/test/javascripts/helpers/create-store.js.es6 index 37df69372f..9ad8d169c5 100644 --- a/test/javascripts/helpers/create-store.js.es6 +++ b/test/javascripts/helpers/create-store.js.es6 @@ -6,8 +6,9 @@ import { buildResolver } from 'discourse-common/resolver'; export default function() { const resolver = buildResolver('discourse').create(); + return Store.create({ - container: { + register: { lookup(type) { if (type === "adapter:rest") { this._restAdapter = this._restAdapter || RestAdapter.create({ container: this }); From 6a1c05a2680082f2895b4813a2083cfbe8943633 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 8 Nov 2016 14:29:50 -0500 Subject: [PATCH 042/266] Upgrade ember-qunit --- .../controllers/poll-ui-builder-test.js.es6 | 5 + .../controllers/admin-user-badges-test.js.es6 | 4 + .../admin/models/admin-user-test.js.es6 | 15 +- .../components/ace-editor-test.js.es6 | 2 + .../controllers/avatar-selector-test.js.es6 | 5 + .../controllers/create-account-test.js.es6 | 5 + test/javascripts/controllers/flag-test.js.es6 | 6 +- .../javascripts/controllers/topic-test.js.es6 | 6 +- vendor/assets/javascripts/ember-qunit.js | 368 ++++++++++++++---- 9 files changed, 341 insertions(+), 75 deletions(-) diff --git a/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 b/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 index 59e8d9266d..1e9446d90e 100644 --- a/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 +++ b/plugins/poll/test/javascripts/controllers/poll-ui-builder-test.js.es6 @@ -1,4 +1,9 @@ +import { mapRoutes } from 'discourse/mapping-router'; + moduleFor("controller:poll-ui-builder", "controller:poll-ui-builder", { + setup() { + this.registry.register('router:main', mapRoutes()); + }, needs: ['controller:modal'] }); 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 3d46614fad..e4a7aaceb4 100644 --- a/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 +++ b/test/javascripts/admin/controllers/admin-user-badges-test.js.es6 @@ -1,6 +1,10 @@ import Badge from 'discourse/models/badge'; +import { mapRoutes } from 'discourse/mapping-router'; moduleFor('controller:admin-user-badges', { + setup() { + this.registry.register('router:main', mapRoutes()); + }, needs: ['controller:adminUser'] }); diff --git a/test/javascripts/admin/models/admin-user-test.js.es6 b/test/javascripts/admin/models/admin-user-test.js.es6 index eb3a4ed7cb..e735ec9456 100644 --- a/test/javascripts/admin/models/admin-user-test.js.es6 +++ b/test/javascripts/admin/models/admin-user-test.js.es6 @@ -4,21 +4,24 @@ import ApiKey from 'admin/models/api-key'; module("model:admin-user"); -test('generate key', function() { +test('generate key', function(assert) { + assert.expect(2); + var adminUser = AdminUser.create({id: 333}); - blank(adminUser.get('api_key'), 'it has no api key by default'); - adminUser.generateApiKey().then(function() { + assert.ok(!adminUser.get('api_key'), 'it has no api key by default'); + return adminUser.generateApiKey().then(function() { present(adminUser.get('api_key'), 'it has an api_key now'); }); }); -test('revoke key', function() { +test('revoke key', function(assert) { + assert.expect(2); var apiKey = ApiKey.create({id: 1234, key: 'asdfasdf'}), adminUser = AdminUser.create({id: 333, api_key: apiKey}); - equal(adminUser.get('api_key'), apiKey, 'it has the api key in the beginning'); - adminUser.revokeApiKey().then(function() { + assert.equal(adminUser.get('api_key'), apiKey, 'it has the api key in the beginning'); + return adminUser.revokeApiKey().then(function() { blank(adminUser.get('api_key'), 'it cleared the api_key'); }); }); diff --git a/test/javascripts/components/ace-editor-test.js.es6 b/test/javascripts/components/ace-editor-test.js.es6 index 6fa9a533d4..76aa436ff3 100644 --- a/test/javascripts/components/ace-editor-test.js.es6 +++ b/test/javascripts/components/ace-editor-test.js.es6 @@ -5,6 +5,7 @@ moduleForComponent('ace-editor', {integration: true}); componentTest('css editor', { template: '{{ace-editor mode="css"}}', test(assert) { + expect(1); assert.ok(this.$('.ace_editor').length, 'it renders the ace editor'); } }); @@ -12,6 +13,7 @@ componentTest('css editor', { componentTest('html editor', { template: '{{ace-editor mode="html" content="wat"}}', test(assert) { + expect(1); assert.ok(this.$('.ace_editor').length, 'it renders the ace editor'); } }); diff --git a/test/javascripts/controllers/avatar-selector-test.js.es6 b/test/javascripts/controllers/avatar-selector-test.js.es6 index 3f2f0af811..00514303f4 100644 --- a/test/javascripts/controllers/avatar-selector-test.js.es6 +++ b/test/javascripts/controllers/avatar-selector-test.js.es6 @@ -1,4 +1,9 @@ +import { mapRoutes } from 'discourse/mapping-router'; + moduleFor("controller:avatar-selector", "controller:avatar-selector", { + setup() { + this.registry.register('router:main', mapRoutes()); + }, needs: ['controller:modal'] }); diff --git a/test/javascripts/controllers/create-account-test.js.es6 b/test/javascripts/controllers/create-account-test.js.es6 index 0f7284be6f..91156d4e52 100644 --- a/test/javascripts/controllers/create-account-test.js.es6 +++ b/test/javascripts/controllers/create-account-test.js.es6 @@ -1,4 +1,9 @@ +import { mapRoutes } from 'discourse/mapping-router'; + moduleFor("controller:create-account", "controller:create-account", { + setup() { + this.registry.register('router:main', mapRoutes()); + }, needs: ['controller:modal', 'controller:login'] }); diff --git a/test/javascripts/controllers/flag-test.js.es6 b/test/javascripts/controllers/flag-test.js.es6 index 4d4775065d..9036808245 100644 --- a/test/javascripts/controllers/flag-test.js.es6 +++ b/test/javascripts/controllers/flag-test.js.es6 @@ -1,5 +1,6 @@ import createStore from 'helpers/create-store'; import AdminUser from 'admin/models/admin-user'; +import { mapRoutes } from 'discourse/mapping-router'; var buildPost = function(args) { return Discourse.Post.create(_.merge({ @@ -17,6 +18,9 @@ var buildAdminUser = function(args) { }; moduleFor("controller:flag", "controller:flag", { + setup() { + this.registry.register('router:main', mapRoutes()); + }, needs: ['controller:modal'] }); @@ -97,4 +101,4 @@ test("canSendWarning notify_user selected", function(){ sandbox.stub(Discourse.User, 'currentProp').withArgs('staff').returns(true); var flagController = this.subject({ model: buildPost() }); canSendWarning(flagController, 'notify_user', true, 'true if current user is staff, selected is notify_user'); -}); \ No newline at end of file +}); diff --git a/test/javascripts/controllers/topic-test.js.es6 b/test/javascripts/controllers/topic-test.js.es6 index 342553f230..74bb33dabc 100644 --- a/test/javascripts/controllers/topic-test.js.es6 +++ b/test/javascripts/controllers/topic-test.js.es6 @@ -1,8 +1,12 @@ import { blank, present } from 'helpers/qunit-helpers'; +import { mapRoutes } from 'discourse/mapping-router'; moduleFor('controller:topic', 'controller:topic', { needs: ['controller:modal', 'controller:composer', 'controller:quote-button', - 'controller:application'] + 'controller:application'], + setup() { + this.registry.register('router:main', mapRoutes()); + }, }); import Topic from 'discourse/models/topic'; diff --git a/vendor/assets/javascripts/ember-qunit.js b/vendor/assets/javascripts/ember-qunit.js index ce4a591162..43957ae82c 100644 --- a/vendor/assets/javascripts/ember-qunit.js +++ b/vendor/assets/javascripts/ember-qunit.js @@ -111,7 +111,7 @@ var define, requireModule, require, requirejs; }; })(); -define('ember-qunit', ['exports', 'ember-qunit/module-for', 'ember-qunit/module-for-component', 'ember-qunit/module-for-model', 'ember-qunit/test', 'ember-test-helpers'], function (exports, moduleFor, moduleForComponent, moduleForModel, test, ember_test_helpers) { +define('ember-qunit', ['exports', 'ember-qunit/module-for', 'ember-qunit/module-for-component', 'ember-qunit/module-for-model', 'ember-qunit/test', 'ember-qunit/only', 'ember-test-helpers'], function (exports, moduleFor, moduleForComponent, moduleForModel, test, only, ember_test_helpers) { 'use strict'; @@ -121,6 +121,7 @@ define('ember-qunit', ['exports', 'ember-qunit/module-for', 'ember-qunit/module- exports.moduleForComponent = moduleForComponent['default']; exports.moduleForModel = moduleForModel['default']; exports.test = test['default']; + exports.only = only['default']; exports.setResolver = ember_test_helpers.setResolver; }); @@ -153,6 +154,16 @@ define('ember-qunit/module-for', ['exports', 'ember-qunit/qunit-module', 'ember- } exports['default'] = moduleFor; +}); +define('ember-qunit/only', ['exports', 'ember-qunit/test-wrapper', 'qunit'], function (exports, testWrapper, qunit) { + + 'use strict'; + + function only(testName, callback) { + testWrapper['default'](testName, callback, qunit.only); + } + exports['default'] = only; + }); define('ember-qunit/qunit-module', ['exports', 'qunit'], function (exports, qunit) { @@ -165,7 +176,7 @@ define('ember-qunit/qunit-module', ['exports', 'qunit'], function (exports, quni if (!callbacks) { return; } var beforeEach; - + if (callbacks.setup) { beforeEach = callbacks.setup; delete callbacks.setup; @@ -207,7 +218,7 @@ define('ember-qunit/qunit-module', ['exports', 'qunit'], function (exports, quni qunit.module(module.name, { setup: function(assert) { var done = assert.async(); - module.setup().then(function() { + return module.setup().then(function() { if (beforeEach) { beforeEach.call(module.context, assert); } @@ -219,26 +230,31 @@ define('ember-qunit/qunit-module', ['exports', 'qunit'], function (exports, quni afterEach.call(module.context, assert); } var done = assert.async(); - module.teardown()['finally'](done); + return module.teardown()['finally'](done); } }); } }); -define('ember-qunit/test', ['exports', 'ember', 'ember-test-helpers', 'qunit'], function (exports, Ember, ember_test_helpers, qunit) { +define('ember-qunit/test-wrapper', ['exports', 'ember', 'ember-test-helpers'], function (exports, Ember, ember_test_helpers) { 'use strict'; - function test(testName, callback) { - function wrapper(assert) { + function testWrapper(testName, callback, qunit) { + function wrapper() { var context = ember_test_helpers.getContext(); - var result = callback.call(context, assert); + var result = callback.apply(context, arguments); function failTestOnPromiseRejection(reason) { var message; if (reason instanceof Error) { message = reason.stack; + if (reason.message && message.indexOf(reason.message) < 0) { + // PhantomJS has a `stack` that does not contain the actual + // exception message. + message = Ember['default'].inspect(reason) + "\n" + message; + } } else { message = Ember['default'].inspect(reason); } @@ -251,7 +267,17 @@ define('ember-qunit/test', ['exports', 'ember', 'ember-test-helpers', 'qunit'], }); } - qunit.test(testName, wrapper); + qunit(testName, wrapper); + } + exports['default'] = testWrapper; + +}); +define('ember-qunit/test', ['exports', 'ember-qunit/test-wrapper', 'qunit'], function (exports, testWrapper, qunit) { + + 'use strict'; + + function test(testName, callback) { + testWrapper['default'](testName, callback, qunit.test); } exports['default'] = test; @@ -270,7 +296,7 @@ define('ember-test-helpers', ['exports', 'ember', 'ember-test-helpers/test-modul exports.setResolver = test_resolver.setResolver; }); -define('ember-test-helpers/build-registry', ['exports'], function (exports) { +define('ember-test-helpers/build-registry', ['exports', 'ember'], function (exports, Ember) { 'use strict'; @@ -290,9 +316,11 @@ define('ember-test-helpers/build-registry', ['exports'], function (exports) { ]; function exposeRegistryMethod(container, method) { - container[method] = function() { - return container._registry[method].apply(container._registry, arguments); - }; + if (method in container) { + container[method] = function() { + return container._registry[method].apply(container._registry, arguments); + }; + } } for (var i = 0, l = methods.length; i < l; i++) { @@ -300,9 +328,17 @@ define('ember-test-helpers/build-registry', ['exports'], function (exports) { } } + var Owner = (function() { + if (Ember['default']._RegistryProxyMixin && Ember['default']._ContainerProxyMixin) { + return Ember['default'].Object.extend(Ember['default']._RegistryProxyMixin, Ember['default']._ContainerProxyMixin); + } + + return Ember['default'].Object.extend(); + })(); + exports['default'] = function(resolver) { - var registry, container; - var namespace = Ember.Object.create({ + var fallbackRegistry, registry, container; + var namespace = Ember['default'].Object.create({ Resolver: { create: function() { return resolver; } } }); @@ -314,22 +350,50 @@ define('ember-test-helpers/build-registry', ['exports'], function (exports) { } } - if (Ember.Application.buildRegistry) { - registry = Ember.Application.buildRegistry(namespace); - registry.register('component-lookup:main', Ember.ComponentLookup); + if (Ember['default'].Application.buildRegistry) { + fallbackRegistry = Ember['default'].Application.buildRegistry(namespace); + fallbackRegistry.register('component-lookup:main', Ember['default'].ComponentLookup); + + registry = new Ember['default'].Registry({ + fallback: fallbackRegistry + }); + + // these properties are set on the fallback registry by `buildRegistry` + // and on the primary registry within the ApplicationInstance constructor + // but we need to manually recreate them since ApplicationInstance's are not + // exposed externally + registry.normalizeFullName = fallbackRegistry.normalizeFullName; + registry.makeToString = fallbackRegistry.makeToString; + registry.describe = fallbackRegistry.describe; + + var owner = Owner.create({ + __registry__: registry, + __container__: null + }); + + container = registry.container({ owner: owner }); + owner.__container__ = container; - registry = registry; - container = registry.container(); exposeRegistryMethodsWithoutDeprecations(container); } else { - container = Ember.Application.buildContainer(namespace); - container.register('component-lookup:main', Ember.ComponentLookup); + container = Ember['default'].Application.buildContainer(namespace); + container.register('component-lookup:main', Ember['default'].ComponentLookup); } // Ember 1.10.0 did not properly add `view:toplevel` or `view:default` // to the registry in Ember.Application.buildRegistry :( - register('view:toplevel', Ember.View.extend()); - register('view:default', Ember._MetamorphView); + // + // Ember 2.0.0 removed Ember.View as public API, so only do this when + // Ember.View is present + if (Ember['default'].View) { + register('view:toplevel', Ember['default'].View.extend()); + } + + // Ember 2.0.0 removed Ember._MetamorphView from the Ember global, so only + // do this when present + if (Ember['default']._MetamorphView) { + register('view:default', Ember['default']._MetamorphView); + } var globalContext = typeof global === 'object' && global || self; if (globalContext.DS) { @@ -354,9 +418,17 @@ define('ember-test-helpers/build-registry', ['exports'], function (exports) { } }); -define('ember-test-helpers/isolated-container', function () { +define('ember-test-helpers/has-ember-version', ['exports', 'ember'], function (exports, Ember) { - 'use strict'; + 'use strict'; + + function hasEmberVersion(major, minor) { + var numbers = Ember['default'].VERSION.split('-')[0].split('.'); + var actualMajor = parseInt(numbers[0], 10); + var actualMinor = parseInt(numbers[1], 10); + return actualMajor > major || (actualMajor === major && actualMinor >= minor); + } + exports['default'] = hasEmberVersion; }); define('ember-test-helpers/test-context', ['exports'], function (exports) { @@ -365,6 +437,7 @@ define('ember-test-helpers/test-context', ['exports'], function (exports) { exports.setContext = setContext; exports.getContext = getContext; + exports.unsetContext = unsetContext; var __test_context__; @@ -376,6 +449,10 @@ define('ember-test-helpers/test-context', ['exports'], function (exports) { return __test_context__; } + function unsetContext() { + __test_context__ = undefined; + } + }); define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-helpers/test-module', 'ember', 'ember-test-helpers/test-resolver'], function (exports, TestModule, Ember, test_resolver) { @@ -398,7 +475,14 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h } else if (callbacks.integration) { this.isUnitTest = false; } else { - Ember['default'].deprecate("the component:" + componentName + " test module is implicitly running in unit test mode, which will change to integration test mode by default in an upcoming version of ember-test-helpers. Add `unit: true` or a `needs:[]` list to explicitly opt in to unit test mode."); + Ember['default'].deprecate( + "the component:" + componentName + " test module is implicitly running in unit test mode, " + + "which will change to integration test mode by default in an upcoming version of " + + "ember-test-helpers. Add `unit: true` or a `needs:[]` list to explicitly opt in to unit " + + "test mode.", + false, + { id: 'ember-test-helpers.test-module-for-component.test-type', until: '0.6.0' } + ); this.isUnitTest = true; } @@ -419,45 +503,67 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h throw new Error("component integration tests do not support `subject()`."); }; this.setupSteps.push(this.setupComponentIntegrationTest); - this.teardownSteps.push(this.teardownComponent); + this.teardownSteps.unshift(this.teardownComponent); } + + if (Ember['default'].View && Ember['default'].View.views) { + this.setupSteps.push(this._aliasViewRegistry); + this.teardownSteps.unshift(this._resetViewRegistry); + } + }, + + _aliasViewRegistry: function() { + this._originalGlobalViewRegistry = Ember['default'].View.views; + var viewRegistry = this.container.lookup('-view-registry:main'); + + if (viewRegistry) { + Ember['default'].View.views = viewRegistry; + } + }, + + _resetViewRegistry: function() { + Ember['default'].View.views = this._originalGlobalViewRegistry; }, setupComponentUnitTest: function() { var _this = this; var resolver = test_resolver.getResolver(); - var container = this.container; var context = this.context; var layoutName = 'template:components/' + this.componentName; var layout = resolver.resolve(layoutName); + var thingToRegisterWith = this.registry || this.container; if (layout) { - container.register(layoutName, layout); - container.injection(this.subjectName, 'layout', layoutName); + thingToRegisterWith.register(layoutName, layout); + thingToRegisterWith.injection(this.subjectName, 'layout', layoutName); } - context.dispatcher = Ember['default'].EventDispatcher.create(); + context.dispatcher = this.container.lookup('event_dispatcher:main') || Ember['default'].EventDispatcher.create(); context.dispatcher.setup({}, '#ember-testing'); this.callbacks.render = function() { - var containerView = Ember['default'].ContainerView.create({container: container}); + var subject; + Ember['default'].run(function(){ - var subject = context.subject(); - containerView.pushObject(subject); - containerView.appendTo('#ember-testing'); + subject = context.subject(); + subject.appendTo('#ember-testing'); }); _this.teardownSteps.unshift(function() { Ember['default'].run(function() { - Ember['default'].tryInvoke(containerView, 'destroy'); + Ember['default'].tryInvoke(subject, 'destroy'); }); }); }; this.callbacks.append = function() { - Ember['default'].deprecate('this.append() is deprecated. Please use this.render() or this.$() instead.'); + Ember['default'].deprecate( + 'this.append() is deprecated. Please use this.render() or this.$() instead.', + false, + { id: 'ember-test-helpers.test-module-for-component.append', until: '0.6.0' } + ); return context.$(); }; @@ -472,10 +578,15 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h setupComponentIntegrationTest: function() { var module = this; var context = this.context; - context.dispatcher = Ember['default'].EventDispatcher.create(); - context.dispatcher.setup({}, '#ember-testing'); + this.actionHooks = {}; + context.dispatcher = this.container.lookup('event_dispatcher:main') || Ember['default'].EventDispatcher.create(); + context.dispatcher.setup({}, '#ember-testing'); + context.actions = module.actionHooks; + + (this.registry || this.container).register('component:-test-holder', Ember['default'].Component.extend()); + context.render = function(template) { if (!template) { throw new Error("in a component integration test you must pass a template to `render()`"); @@ -486,12 +597,12 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h if (typeof template === 'string') { template = Ember['default'].Handlebars.compile(template); } - module.component = Ember['default'].Component.create({ - layout: template, - container: module.container + module.component = module.container.lookupFactory('component:-test-holder').create({ + layout: template }); + module.component.set('context' ,context); - module.component.set('controller', module); + module.component.set('controller', context); Ember['default'].run(function() { module.component.appendTo('#ember-testing'); @@ -508,32 +619,47 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h }); }; + context.setProperties = function(hash) { + Ember['default'].run(function() { + Ember['default'].setProperties(context, hash); + }); + }; + context.get = function(key) { return Ember['default'].get(context, key); }; + context.getProperties = function() { + var args = Array.prototype.slice.call(arguments); + return Ember['default'].getProperties(context, args); + }; + context.on = function(actionName, handler) { module.actionHooks[actionName] = handler; }; - + context.send = function(actionName) { + var hook = module.actionHooks[actionName]; + if (!hook) { + throw new Error("integration testing template received unexpected action " + actionName); + } + hook.apply(module, Array.prototype.slice.call(arguments, 1)); + }; }, setupContext: function() { this._super.call(this); + + // only setup the injection if we are running against a version + // of Ember that has `-view-registry:main` (Ember >= 1.12) + if (this.container.lookupFactory('-view-registry:main')) { + (this.registry || this.container).injection('component', '_viewRegistry', '-view-registry:main'); + } + if (!this.isUnitTest) { this.context.factory = function() {}; } }, - - send: function(actionName) { - var hook = this.actionHooks[actionName]; - if (!hook) { - throw new Error("integration testing template received unexpected action " + actionName); - } - hook.apply(this, Array.prototype.slice.call(arguments, 1)); - }, - teardownComponent: function() { var component = this.component; if (component) { @@ -542,8 +668,6 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h }); } } - - }); }); @@ -568,7 +692,10 @@ define('ember-test-helpers/test-module-for-model', ['exports', 'ember-test-helpe var adapterFactory = container.lookupFactory('adapter:application'); if (!adapterFactory) { - container.register('adapter:application', DS.FixtureAdapter); + adapterFactory = DS.JSONAPIAdapter || DS.FixtureAdapter; + + var thingToRegisterWith = this.registry || this.container; + thingToRegisterWith.register('adapter:application', adapterFactory); } callbacks.store = function(){ @@ -591,7 +718,7 @@ define('ember-test-helpers/test-module-for-model', ['exports', 'ember-test-helpe }); }); -define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helpers/test-context', 'klassy', 'ember-test-helpers/test-resolver', 'ember-test-helpers/build-registry'], function (exports, Ember, test_context, klassy, test_resolver, buildRegistry) { +define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helpers/test-context', 'klassy', 'ember-test-helpers/test-resolver', 'ember-test-helpers/build-registry', 'ember-test-helpers/has-ember-version', 'ember-test-helpers/wait'], function (exports, Ember, test_context, klassy, test_resolver, buildRegistry, hasEmberVersion, wait) { 'use strict'; @@ -609,6 +736,10 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper this.name = description || subjectName; this.callbacks = callbacks || {}; + if (this.callbacks.integration && this.callbacks.needs) { + throw new Error("cannot declare 'integration: true' and 'needs' in the same module"); + } + if (this.callbacks.integration) { this.isIntegration = callbacks.integration; delete callbacks.integration; @@ -644,6 +775,7 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper this.setupSteps.push(this.setupContainer); this.setupSteps.push(this.setupContext); this.setupSteps.push(this.setupTestElements); + this.setupSteps.push(this.setupAJAXListeners); if (this.callbacks.setup) { this.contextualizedSetupSteps.push( this.callbacks.setup ); @@ -664,6 +796,7 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper this.teardownSteps.push(this.teardownContainer); this.teardownSteps.push(this.teardownContext); this.teardownSteps.push(this.teardownTestElements); + this.teardownSteps.push(this.teardownAJAXListeners); if (this.callbacks.afterTeardown) { this.teardownSteps.push( this.callbacks.afterTeardown ); @@ -729,10 +862,29 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper container: this.container, registry: this.registry, factory: factory, - dispatcher: null + dispatcher: null, + register: function() { + var target = this.registry || this.container; + return target.register.apply(target, arguments); + }, + inject: {} }); - this.context = test_context.getContext(); + var context = this.context = test_context.getContext(); + + if (Ember['default'].setOwner) { + Ember['default'].setOwner(context, this.container.owner); + } + + if (Ember['default'].inject) { + var keys = (Object.keys || Ember['default'].keys)(Ember['default'].inject); + keys.forEach(function(typeName) { + context.inject[typeName] = function(name, opts) { + var alias = (opts && opts.as) || name; + Ember['default'].set(context, alias, context.container.lookup(typeName + ':' + name)); + }; + }); + } }, setupTestElements: function() { @@ -741,6 +893,10 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper } }, + setupAJAXListeners: function() { + wait._setupAJAXHooks(); + }, + teardownSubject: function() { var subject = this.cache.subject; @@ -760,7 +916,10 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper teardownContext: function() { var context = this.context; - if (context.dispatcher) { + this.context = undefined; + test_context.unsetContext(); + + if (context.dispatcher && !context.dispatcher.isDestroyed) { Ember['default'].run(function() { context.dispatcher.destroy(); }); @@ -769,7 +928,16 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper teardownTestElements: function() { Ember['default'].$('#ember-testing').empty(); - Ember['default'].View.views = {}; + + // Ember 2.0.0 removed Ember.View as public API, so only do this when + // Ember.View is present + if (Ember['default'].View && Ember['default'].View.views) { + Ember['default'].View.views = {}; + } + }, + + teardownAJAXListeners: function() { + wait._teardownAJAXHooks(); }, defaultSubject: function(options, factory) { @@ -813,10 +981,12 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper this.container = items.container; this.registry = items.registry; - var thingToRegisterWith = this.registry || this.container; - var router = resolver.resolve('router:main'); - router = router || Ember['default'].Router.extend(); - thingToRegisterWith.register('router:main', router); + if (hasEmberVersion['default'](1, 13)) { + var thingToRegisterWith = this.registry || this.container; + var router = resolver.resolve('router:main'); + router = router || Ember['default'].Router.extend(); + thingToRegisterWith.register('router:main', router); + } }, _setupIsolatedContainer: function() { @@ -831,7 +1001,11 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper thingToRegisterWith.register(fullName, resolver.resolve(normalizedFullName)); } - thingToRegisterWith.resolver = function() { }; + if (this.registry) { + this.registry.fallback.resolver = function() {}; + } else { + this.container.resolver = function() {}; + } }, _setupIntegratedContainer: function() { @@ -859,6 +1033,64 @@ define('ember-test-helpers/test-resolver', ['exports'], function (exports) { return __resolver__; } +}); +define('ember-test-helpers/wait', ['exports', 'ember'], function (exports, Ember) { + + 'use strict'; + + exports._teardownAJAXHooks = _teardownAJAXHooks; + exports._setupAJAXHooks = _setupAJAXHooks; + + var requests; + function incrementAjaxPendingRequests(_, xhr) { + requests.push(xhr); + } + + function decrementAjaxPendingRequests(_, xhr) { + for (var i = 0;i < requests.length;i++) { + if (xhr === requests[i]) { + requests.splice(i, 1); + } + } + } + + function _teardownAJAXHooks() { + jQuery(document).off('ajaxSend', incrementAjaxPendingRequests); + jQuery(document).off('ajaxComplete', decrementAjaxPendingRequests); + } + + function _setupAJAXHooks() { + requests = []; + + jQuery(document).on('ajaxSend', incrementAjaxPendingRequests); + jQuery(document).on('ajaxComplete', decrementAjaxPendingRequests); + } + + function wait(_options) { + var options = _options || {}; + var waitForTimers = options.hasOwnProperty('waitForTimers') ? options.waitForTimers : true; + var waitForAJAX = options.hasOwnProperty('waitForAJAX') ? options.waitForAJAX : true; + + return new Ember['default'].RSVP.Promise(function(resolve) { + var watcher = self.setInterval(function() { + if (waitForTimers && (Ember['default'].run.hasScheduledTimers() || Ember['default'].run.currentRunLoop)) { + return; + } + + if (waitForAJAX && requests && requests.length > 0) { + return; + } + + // Stop polling + self.clearInterval(watcher); + + // Synchronously resolve the promise + Ember['default'].run(null, resolve); + }, 10); + }); + } + exports['default'] = wait; + }); define('klassy', ['exports'], function (exports) { @@ -1022,12 +1254,14 @@ define('qunit', ['exports'], function (exports) { var module = QUnit.module; var test = QUnit.test; var skip = QUnit.skip; + var only = QUnit.only; exports['default'] = QUnit; exports.module = module; exports.test = test; exports.skip = skip; + exports.only = only; }); define("ember", ["exports"], function(__exports__) { From 7d560ea3d564d87198ea911ae92093041e467ab7 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Tue, 8 Nov 2016 14:42:10 -0500 Subject: [PATCH 043/266] More ember-qunit fixes --- .../javascripts/helpers/component-test.js.es6 | 12 --- vendor/assets/javascripts/ember-qunit.js | 75 +++++++++++++------ 2 files changed, 54 insertions(+), 33 deletions(-) diff --git a/test/javascripts/helpers/component-test.js.es6 b/test/javascripts/helpers/component-test.js.es6 index be0be14a73..341c106b33 100644 --- a/test/javascripts/helpers/component-test.js.es6 +++ b/test/javascripts/helpers/component-test.js.es6 @@ -36,22 +36,10 @@ export default function(name, opts) { this.registry.register('store:main', store, { instantiate: false }); if (opts.setup) { - if (Ember.VERSION[0] === "2") { - // use closure actions instead - this.on = (actionName, fn) => this.set(actionName, fn); - } - opts.setup.call(this, store); } andThen(() => { - // TODO: This shouldn't be necessary - this.owner = { - hasRegistration: (...args) => this.registry.has(...args), - lookup: (...args) => this.container.lookup(...args), - _lookupFactory: (...args) => this.container.lookupFactory(...args) - }; - return this.render(opts.template); }); andThen(() => opts.test.call(this, assert)); diff --git a/vendor/assets/javascripts/ember-qunit.js b/vendor/assets/javascripts/ember-qunit.js index 43957ae82c..759edca9be 100644 --- a/vendor/assets/javascripts/ember-qunit.js +++ b/vendor/assets/javascripts/ember-qunit.js @@ -159,8 +159,12 @@ define('ember-qunit/only', ['exports', 'ember-qunit/test-wrapper', 'qunit'], fun 'use strict'; - function only(testName, callback) { - testWrapper['default'](testName, callback, qunit.only); + function only(/* testName, expected, callback, async */) { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; ++_key) { + args[_key] = arguments[_key]; + } + args.unshift(qunit.only); + testWrapper['default'].apply(null, args); } exports['default'] = only; @@ -240,7 +244,12 @@ define('ember-qunit/test-wrapper', ['exports', 'ember', 'ember-test-helpers'], f 'use strict'; - function testWrapper(testName, callback, qunit) { + function testWrapper(qunit /*, testName, expected, callback, async */) { + var callback; + for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; ++_key) { + args[_key - 1] = arguments[_key]; + } + function wrapper() { var context = ember_test_helpers.getContext(); @@ -267,7 +276,13 @@ define('ember-qunit/test-wrapper', ['exports', 'ember', 'ember-test-helpers'], f }); } - qunit(testName, wrapper); + if (args.length === 2) { + callback = args.splice(1, 1, wrapper)[0]; + } else { + callback = args.splice(2, 1, wrapper)[0]; + } + + qunit.apply(null, args); } exports['default'] = testWrapper; @@ -276,8 +291,12 @@ define('ember-qunit/test', ['exports', 'ember-qunit/test-wrapper', 'qunit'], fun 'use strict'; - function test(testName, callback) { - testWrapper['default'](testName, callback, qunit.test); + function test(/* testName, expected, callback, async */) { + for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; ++_key) { + args[_key] = arguments[_key]; + } + args.unshift(qunit.test); + testWrapper['default'].apply(null, args); } exports['default'] = test; @@ -459,6 +478,8 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h 'use strict'; exports['default'] = TestModule['default'].extend({ + isComponentTestModule: true, + init: function(componentName, description, callbacks) { // Allow `description` to be omitted if (!callbacks && typeof description === 'object') { @@ -486,17 +507,17 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h this.isUnitTest = true; } - if (!this.isUnitTest) { - callbacks.integration = true; - } - if (description) { this._super.call(this, 'component:' + componentName, description, callbacks); } else { this._super.call(this, 'component:' + componentName, callbacks); } - if (this.isUnitTest) { + if (!this.isUnitTest && !this.isLegacy) { + callbacks.integration = true; + } + + if (this.isUnitTest || this.isLegacy) { this.setupSteps.push(this.setupComponentUnitTest); } else { this.callbacks.subject = function() { @@ -655,7 +676,7 @@ define('ember-test-helpers/test-module-for-component', ['exports', 'ember-test-h (this.registry || this.container).injection('component', '_viewRegistry', '-view-registry:main'); } - if (!this.isUnitTest) { + if (!this.isUnitTest && !this.isLegacy) { this.context.factory = function() {}; } }, @@ -741,7 +762,16 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper } if (this.callbacks.integration) { - this.isIntegration = callbacks.integration; + if (this.isComponentTestModule) { + this.isLegacy = (callbacks.integration === 'legacy'); + this.isIntegration = (callbacks.integration !== 'legacy'); + } else { + if (callbacks.integration === 'legacy') { + throw new Error('`integration: \'legacy\'` is only valid for component tests.'); + } + this.isIntegration = true; + } + delete callbacks.integration; } @@ -843,7 +873,7 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper }, setupContainer: function() { - if (this.isIntegration) { + if (this.isIntegration || this.isLegacy) { this._setupIntegratedContainer(); } else { this._setupIsolatedContainer(); @@ -974,9 +1004,14 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper } }, - _setupContainer: function() { + _setupContainer: function(isolated) { var resolver = test_resolver.getResolver(); - var items = buildRegistry['default'](resolver); + + var items = buildRegistry['default'](!isolated ? resolver : Object.create(resolver, { + resolve: { + value: function() {} + } + })); this.container = items.container; this.registry = items.registry; @@ -991,7 +1026,7 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper _setupIsolatedContainer: function() { var resolver = test_resolver.getResolver(); - this._setupContainer(); + this._setupContainer(true); var thingToRegisterWith = this.registry || this.container; @@ -1001,9 +1036,7 @@ define('ember-test-helpers/test-module', ['exports', 'ember', 'ember-test-helper thingToRegisterWith.register(fullName, resolver.resolve(normalizedFullName)); } - if (this.registry) { - this.registry.fallback.resolver = function() {}; - } else { + if (!this.registry) { this.container.resolver = function() {}; } }, @@ -1277,4 +1310,4 @@ window.test = emberQunit.test; window.setResolver = emberQunit.setResolver; })(); -//# sourceMappingURL=ember-qunit.map +//# sourceMappingURL=ember-qunit.map \ No newline at end of file From 86522a52b7d12eb2379535aac6041b2165403243 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 8 Nov 2016 16:36:34 -0500 Subject: [PATCH 044/266] FEATURE: add censored_pattern setting to censor posts using regex --- .../pretty-text/censored-words.js.es6 | 45 +++++++++++++------ .../discourse-markdown/censored.js.es6 | 4 +- config/locales/server.en.yml | 2 + config/site_settings.yml | 5 +++ lib/pretty_text.rb | 13 +++++- lib/site_setting_extension.rb | 6 ++- lib/validators/regex_setting_validator.rb | 25 +++++++++++ spec/components/pretty_text_spec.rb | 9 ++++ .../regex_setting_validator_spec.rb | 24 ++++++++++ test/javascripts/lib/pretty-text-test.js.es6 | 6 ++- 10 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 lib/validators/regex_setting_validator.rb create mode 100644 spec/components/validators/regex_setting_validator_spec.rb diff --git a/app/assets/javascripts/pretty-text/censored-words.js.es6 b/app/assets/javascripts/pretty-text/censored-words.js.es6 index a715b757ec..e81032393f 100644 --- a/app/assets/javascripts/pretty-text/censored-words.js.es6 +++ b/app/assets/javascripts/pretty-text/censored-words.js.es6 @@ -1,19 +1,38 @@ -export function censor(text, censoredWords) { - if (censoredWords && censoredWords.length) { - const split = censoredWords.split("|"); - let censorRegexp; - if (split && split.length) { - censorRegexp = new RegExp("(\\b(?:" + split.map(function (t) { return "(" + t.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ")"; }).join("|") + ")\\b)(?![^\\(]*\\))", "ig"); - } +export function censor(text, censoredWords, censoredPattern) { + let patterns = [], + originalText = text; - if (censorRegexp) { - let m = censorRegexp.exec(text); - while (m && m[0]) { - const replacement = new Array(m[0].length+1).join('■'); - text = text.replace(new RegExp("(\\b" + m[0] + "\\b)(?![^\\(]*\\))", "ig"), replacement); - m = censorRegexp.exec(text); + if (censoredWords && censoredWords.length) { + patterns = censoredWords.split("|").map(t => { return "(" + t.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&') + ")"; }); + } + + if (censoredPattern && censoredPattern.length > 0) { + try { + new RegExp(censoredPattern); // exception if invalid + patterns.push("(" + censoredPattern + ")"); + } catch(e) {} + } + + if (patterns.length) { + let censorRegexp; + + try { + censorRegexp = new RegExp("(\\b(?:" + patterns.join("|") + ")\\b)(?![^\\(]*\\))", "ig"); + + if (censorRegexp) { + let m = censorRegexp.exec(text); + + while (m && m[0]) { + if (m[0].length > originalText.length) { return originalText; } // regex is dangerous + const replacement = new Array(m[0].length+1).join('■'); + text = text.replace(new RegExp("(\\b" + m[0] + "\\b)(?![^\\(]*\\))", "ig"), replacement); + m = censorRegexp.exec(text); + } } + } catch(e) { + return originalText; } } + return text; } diff --git a/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 b/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 index ecb463f8c3..b0ac2e5acc 100644 --- a/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 +++ b/app/assets/javascripts/pretty-text/engines/discourse-markdown/censored.js.es6 @@ -4,10 +4,12 @@ import { registerOption } from 'pretty-text/pretty-text'; registerOption((siteSettings, opts) => { opts.features.censored = true; opts.censoredWords = siteSettings.censored_words; + opts.censoredPattern = siteSettings.censored_pattern; }); export function setup(helper) { helper.addPreProcessor(text => { - return censor(text, helper.getOptions().censoredWords); + const options = helper.getOptions(); + return censor(text, options.censoredWords, options.censoredPattern); }); } diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index e693cdf043..04b2e4d94e 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -828,6 +828,7 @@ en: site_settings: censored_words: "Words that will be automatically replaced with ■■■■" + censored_pattern: "Regex pattern that will be automatically replaced with ■■■■" delete_old_hidden_posts: "Auto-delete any hidden posts that stay hidden for more than 30 days." default_locale: "The default language of this Discourse instance (ISO 639-1 Code)" allow_user_locale: "Allow users to choose their own language interface preference" @@ -1442,6 +1443,7 @@ en: reply_by_email_address_is_empty: "You must set a 'reply by email address' before enabling reply by email." email_polling_disabled: "You must enable either manual or POP3 polling before enabling reply by email." user_locale_not_enabled: "You must first enable 'allow user locale' before enabling this setting." + invalid_regex: "Regex is invalid or not allowed." search: within_post: "#%{post_number} by %{username}" diff --git a/config/site_settings.yml b/config/site_settings.yml index f93bc56709..92776b8eea 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -536,6 +536,11 @@ posting: default: '' refresh: true type: list + censored_pattern: + client: true + default: '' + refresh: true + type: regex enable_emoji: default: true client: true diff --git a/lib/pretty_text.rb b/lib/pretty_text.rb index d3d6d4ed42..412ca02d3a 100644 --- a/lib/pretty_text.rb +++ b/lib/pretty_text.rb @@ -212,7 +212,18 @@ module PrettyText options[:topicId] = opts[:topic_id] working_text = text.dup - sanitized = markdown(working_text, options) + + begin + sanitized = markdown(working_text, options) + rescue MiniRacer::ScriptTerminatedError => e + if SiteSetting.censored_pattern.present? + Rails.logger.warn "Post cooking timed out. Clearing the censored_pattern setting and retrying." + SiteSetting.censored_pattern = nil + sanitized = markdown(working_text, options) + else + raise e + end + end doc = Nokogiri::HTML.fragment(sanitized) diff --git a/lib/site_setting_extension.rb b/lib/site_setting_extension.rb index 3fb3b01550..70e576f08d 100644 --- a/lib/site_setting_extension.rb +++ b/lib/site_setting_extension.rb @@ -32,7 +32,8 @@ module SiteSettingExtension url_list: 9, host_list: 10, category_list: 11, - value_list: 12) + value_list: 12, + regex: 13) end def mutex @@ -443,7 +444,8 @@ module SiteSettingExtension types[:fixnum] => IntegerSettingValidator, types[:string] => StringSettingValidator, 'list' => StringSettingValidator, - 'enum' => StringSettingValidator + 'enum' => StringSettingValidator, + 'regex' => RegexSettingValidator } @validator_mapping[type_name] end diff --git a/lib/validators/regex_setting_validator.rb b/lib/validators/regex_setting_validator.rb new file mode 100644 index 0000000000..e1aa9900b6 --- /dev/null +++ b/lib/validators/regex_setting_validator.rb @@ -0,0 +1,25 @@ +class RegexSettingValidator + + LOREM = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam eget sem non elit tincidunt rhoncus.'.freeze + + def initialize(opts={}) + @opts = opts + end + + def valid_value?(val) + !val.present? || valid_regex?(val) + end + + # Check that string is a valid regex, and that it doesn't match most of the lorem string. + def valid_regex?(val) + r = Regexp.new(val) + matches = r.match(LOREM) + matches.nil? || matches[0].length < (LOREM.length - 10) + rescue + false + end + + def error_message + I18n.t('site_settings.errors.invalid_regex') + end +end diff --git a/spec/components/pretty_text_spec.rb b/spec/components/pretty_text_spec.rb index 109181333f..63577b90e9 100644 --- a/spec/components/pretty_text_spec.rb +++ b/spec/components/pretty_text_spec.rb @@ -438,4 +438,13 @@ HTML end end + describe "censored_pattern site setting" do + it "can be cleared if it causes cooking to timeout" do + SiteSetting.censored_pattern = "evilregex" + described_class.stubs(:markdown).raises(MiniRacer::ScriptTerminatedError) + PrettyText.cook("Protect against it plz.") rescue nil + expect(SiteSetting.censored_pattern).to be_blank + end + end + end diff --git a/spec/components/validators/regex_setting_validator_spec.rb b/spec/components/validators/regex_setting_validator_spec.rb new file mode 100644 index 0000000000..b7bb82f24e --- /dev/null +++ b/spec/components/validators/regex_setting_validator_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe RegexSettingValidator do + describe '#valid_value?' do + subject(:validator) { described_class.new } + + it "returns true for blank values" do + expect(validator.valid_value?('')).to eq(true) + expect(validator.valid_value?(nil)).to eq(true) + end + + it "return false for invalid regex" do + expect(validator.valid_value?('(()')).to eq(false) + end + + it "returns false for regex with dangerous matches" do + expect(validator.valid_value?('(.)*')).to eq(false) + end + + it "returns true for safe regex" do + expect(validator.valid_value?('\d{3}-\d{4}')).to eq(true) + end + end +end diff --git a/test/javascripts/lib/pretty-text-test.js.es6 b/test/javascripts/lib/pretty-text-test.js.es6 index fb9c04f0ed..790967119a 100644 --- a/test/javascripts/lib/pretty-text-test.js.es6 +++ b/test/javascripts/lib/pretty-text-test.js.es6 @@ -11,7 +11,8 @@ const defaultOpts = buildOptions({ emoji_set: 'emoji_one', highlighted_languages: 'json|ruby|javascript', default_code_lang: 'auto', - censored_words: 'shucks|whiz|whizzer' + censored_words: 'shucks|whiz|whizzer', + censored_pattern: '\\d{3}-\\d{4}|tech\\w*' }, getURL: url => url }); @@ -532,6 +533,9 @@ test("censoring", function() { cooked("The link still works. [whiz](http://www.whiz.com)", "

The link still works. ■■■■

", "it won't break links by censoring them."); + cooked("Call techapj the computer whiz at 555-555-1234 for free help.", + "

Call ■■■■■■■ the computer ■■■■ at 555-■■■■■■■■ for free help.

", + "uses both censored words and patterns from site settings"); }); test("code blocks/spans hoisting", function() { From bd77f5cb7239fe21cb549ae45b88636d6995ec77 Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Tue, 8 Nov 2016 19:56:13 -0200 Subject: [PATCH 045/266] FIX: Last Visit line shouldn't appear on /top --- .../javascripts/discourse/components/topic-list.js.es6 | 10 +++++++--- .../discourse/templates/discovery/topics.hbs | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/components/topic-list.js.es6 b/app/assets/javascripts/discourse/components/topic-list.js.es6 index 522efc4346..260bddb756 100644 --- a/app/assets/javascripts/discourse/components/topic-list.js.es6 +++ b/app/assets/javascripts/discourse/components/topic-list.js.es6 @@ -40,12 +40,12 @@ export default Ember.Component.extend({ } }, - @observes('topics', 'order', 'ascending', 'category') + @observes('topics', 'order', 'ascending', 'category', 'top') lastVisitedTopicChanged() { this.refreshLastVisited(); }, - _updateLastVisitedTopic(topics, order, ascending) { + _updateLastVisitedTopic(topics, order, ascending, top) { this.set('lastVisitedTopic', null); @@ -57,6 +57,10 @@ export default Ember.Component.extend({ return; } + if (top) { + return; + } + if (!topics || topics.length === 1) { return; } @@ -103,7 +107,7 @@ export default Ember.Component.extend({ }, refreshLastVisited() { - this._updateLastVisitedTopic(this.get('topics'), this.get('order'), this.get('ascending')); + this._updateLastVisitedTopic(this.get('topics'), this.get('order'), this.get('ascending'), this.get('top')); }, click(e) { diff --git a/app/assets/javascripts/discourse/templates/discovery/topics.hbs b/app/assets/javascripts/discourse/templates/discovery/topics.hbs index 2f9ec77044..8d0ab34d9e 100644 --- a/app/assets/javascripts/discourse/templates/discovery/topics.hbs +++ b/app/assets/javascripts/discourse/templates/discovery/topics.hbs @@ -34,6 +34,7 @@ {{#if hasTopics}} {{topic-list highlightLastVisited=true + top=top showTopicPostBadges=showTopicPostBadges showPosters=true currentUser=currentUser From f10520a5f2c6e72f3e032aa64aea62a60e6674d2 Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Tue, 8 Nov 2016 17:59:51 -0500 Subject: [PATCH 046/266] fix broken js lint --- app/assets/javascripts/pretty-text/censored-words.js.es6 | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/app/assets/javascripts/pretty-text/censored-words.js.es6 b/app/assets/javascripts/pretty-text/censored-words.js.es6 index e81032393f..bc9aa59ee0 100644 --- a/app/assets/javascripts/pretty-text/censored-words.js.es6 +++ b/app/assets/javascripts/pretty-text/censored-words.js.es6 @@ -7,10 +7,7 @@ export function censor(text, censoredWords, censoredPattern) { } if (censoredPattern && censoredPattern.length > 0) { - try { - new RegExp(censoredPattern); // exception if invalid - patterns.push("(" + censoredPattern + ")"); - } catch(e) {} + patterns.push("(" + censoredPattern + ")"); } if (patterns.length) { From 263a43bcfdb998506751cf5ac1659653087f10c5 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 9 Nov 2016 09:56:36 +0800 Subject: [PATCH 047/266] UX: Use a gradient instead of a border. --- app/assets/stylesheets/common/base/discourse.scss | 11 ++++++++--- app/assets/stylesheets/mobile.scss | 1 + app/assets/stylesheets/mobile/ring.scss | 3 +++ 3 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 app/assets/stylesheets/mobile/ring.scss diff --git a/app/assets/stylesheets/common/base/discourse.scss b/app/assets/stylesheets/common/base/discourse.scss index 987c304206..4362f4779e 100644 --- a/app/assets/stylesheets/common/base/discourse.scss +++ b/app/assets/stylesheets/common/base/discourse.scss @@ -167,12 +167,17 @@ body { } .ring { - top: -13px !important; - right: 21px !important; + $gradient-start: transparent; + $gradient-end: #090; + background: -webkit-radial-gradient($gradient-start, $gradient-end); + background: -o-radial-gradient($gradient-start, $gradient-end); + background: -moz-radial-gradient($gradient-start, $gradient-end); + background: radial-gradient($gradient-start, $gradient-end); + top: -11px !important; + right: 24px !important; border-radius: 100%; width: 20px; height: 20px; - border: solid #090 3px; -moz-animation-iteration-count: infinite; -webkit-animation-iteration-count: infinite; -webkit-transform-origin: center; diff --git a/app/assets/stylesheets/mobile.scss b/app/assets/stylesheets/mobile.scss index 47dbba7ef2..4152083286 100644 --- a/app/assets/stylesheets/mobile.scss +++ b/app/assets/stylesheets/mobile.scss @@ -21,6 +21,7 @@ @import "mobile/menu-panel"; @import "mobile/search"; @import "mobile/emoji"; +@import "mobile/ring"; /* These files doesn't actually exist, they are injected by DiscourseSassImporter. */ diff --git a/app/assets/stylesheets/mobile/ring.scss b/app/assets/stylesheets/mobile/ring.scss new file mode 100644 index 0000000000..2da82b54c9 --- /dev/null +++ b/app/assets/stylesheets/mobile/ring.scss @@ -0,0 +1,3 @@ +.ring { + top: -7px !important; +} From 857986e2c04919ae0bda98b7929a8afbd298ac28 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 9 Nov 2016 11:13:34 +0800 Subject: [PATCH 048/266] FIX: Don't change return value to blank string. --- lib/composer_messages_finder.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index 551222b131..347ca208e8 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -17,7 +17,7 @@ class ComposerMessagesFinder # Determines whether to show the user education text def check_education_message - return '' if @topic && @topic.archetype == Archetype.private_message + return if @topic && @topic.archetype == Archetype.private_message if creating_topic? count = @user.created_topic_count @@ -37,7 +37,7 @@ class ComposerMessagesFinder } end - '' + nil end # New users have a limited number of replies in a topic From 2d2998f5e0b3ec343f0f4c469f03196bbbbe175d Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 9 Nov 2016 11:31:53 +0800 Subject: [PATCH 049/266] Fix specs. --- spec/components/composer_messages_finder_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/components/composer_messages_finder_spec.rb b/spec/components/composer_messages_finder_spec.rb index 706c8f7f0c..8bc709e58a 100644 --- a/spec/components/composer_messages_finder_spec.rb +++ b/spec/components/composer_messages_finder_spec.rb @@ -48,7 +48,7 @@ describe ComposerMessagesFinder do let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'createTopic', topic_id: topic.id) } it 'should return an empty string' do - expect(finder.check_education_message).to eq('') + expect(finder.check_education_message).to eq(nil) end end @@ -56,7 +56,7 @@ describe ComposerMessagesFinder do let(:finder) { ComposerMessagesFinder.new(user, composer_action: 'reply', topic_id: topic.id) } it 'should return an empty string' do - expect(finder.check_education_message).to eq('') + expect(finder.check_education_message).to eq(nil) end end end From 74e188992483e8d55376a98b33a440c022c098de Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 9 Nov 2016 13:37:13 +0800 Subject: [PATCH 050/266] FEATURE: Scroll to new posts when user is near bottom of PM. --- .../discourse/controllers/topic.js.es6 | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index b64e00a3c1..1f182103a7 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -9,6 +9,7 @@ import Composer from 'discourse/models/composer'; import DiscourseURL from 'discourse/lib/url'; import { categoryBadgeHTML } from 'discourse/helpers/category-link'; import Post from 'discourse/models/post'; +import debounce from 'discourse/lib/debounce'; export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { composer: Ember.inject.controller(), @@ -668,6 +669,7 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { const topic = this.get('model'); const postStream = topic.get('postStream'); const post = postStream.findLoadedPost(postId); + if (post) { DiscourseURL.routeTo(topic.urlForPostNumber(post.get('post_number'))); } else { @@ -838,10 +840,30 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { topic.reload().then(() => { this.send('postChangedRoute', topic.get('post_number') || 1); }); + } else { + const postNumber = data.post_number; + const notInPostStream = topic.get('highest_post_number') <= postNumber; + const postNumberDifference = postNumber - topic.get('currentPost'); + + if (notInPostStream && + topic.get('isPrivateMessage') && + postNumberDifference > 0 && + postNumberDifference < 7) { + + this._scrollToPost(data.post_number); + } } }); }, + _scrollToPost: debounce(function(postNumber) { + const $post = $(`.topic-post article#post_${postNumber}`); + + if ($post.length === 0) return; + + $('body').animate({ scrollTop: $post.offset().top }, 1000); + }, 500), + unsubscribe() { const topicId = this.get('content.id'); if (!topicId) return; From 7a1400cc4b819b02b7ccf13eba9d6a0d974cc079 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Thu, 3 Nov 2016 04:46:37 +0800 Subject: [PATCH 051/266] Remove undefined variable. --- lib/tasks/qunit.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/qunit.rake b/lib/tasks/qunit.rake index 5aa1d79012..f3ec75b8db 100644 --- a/lib/tasks/qunit.rake +++ b/lib/tasks/qunit.rake @@ -60,7 +60,7 @@ task "qunit:test" => :environment do # A bit of a hack until we can figure this out on Travis tries = 0 - while tries < 3 && $?.exitstatus == 124 && !quit + while tries < 3 && $?.exitstatus == 124 tries += 1 puts "\nTimed Out. Trying again...\n" rake_system(cmd) From 4a2656192733ca84de8a702cbaf2c36651bf2368 Mon Sep 17 00:00:00 2001 From: Guo Xiang Tan Date: Wed, 9 Nov 2016 21:34:32 +0800 Subject: [PATCH 052/266] Only scroll to posts that are not your own in PMs. --- .../discourse/controllers/topic.js.es6 | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 1f182103a7..d32993ac75 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -841,16 +841,20 @@ export default Ember.Controller.extend(SelectedPostsCount, BufferedContent, { this.send('postChangedRoute', topic.get('post_number') || 1); }); } else { - const postNumber = data.post_number; - const notInPostStream = topic.get('highest_post_number') <= postNumber; - const postNumberDifference = postNumber - topic.get('currentPost'); + if (topic.get('isPrivateMessage') && + this.currentUser && + this.currentUser.get('id') !== data.user_id) { - if (notInPostStream && - topic.get('isPrivateMessage') && + const postNumber = data.post_number; + const notInPostStream = topic.get('highest_post_number') <= postNumber; + const postNumberDifference = postNumber - topic.get('currentPost'); + + if (notInPostStream && postNumberDifference > 0 && postNumberDifference < 7) { - this._scrollToPost(data.post_number); + this._scrollToPost(data.post_number); + } } } }); From 541c29ff4d7b1738c3c98911d9891e2fa00b2568 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 9 Nov 2016 20:16:29 +0530 Subject: [PATCH 053/266] Update Translations --- config/locales/client.ar.yml | 34 ++++++++++++++++++ config/locales/client.et.yml | 4 +++ config/locales/client.fi.yml | 1 + config/locales/client.fr.yml | 2 +- config/locales/client.ru.yml | 36 ++++++++++++++++++-- config/locales/client.zh_TW.yml | 1 + config/locales/server.fi.yml | 10 ++++++ config/locales/server.he.yml | 2 +- config/locales/server.nb_NO.yml | 10 ++++++ config/locales/server.sk.yml | 2 +- plugins/poll/config/locales/client.nb_NO.yml | 7 ++++ plugins/poll/config/locales/server.nb_NO.yml | 3 ++ 12 files changed, 107 insertions(+), 5 deletions(-) diff --git a/config/locales/client.ar.yml b/config/locales/client.ar.yml index d0fae2ed46..5e8a83e146 100644 --- a/config/locales/client.ar.yml +++ b/config/locales/client.ar.yml @@ -1166,12 +1166,46 @@ ar: category: "ابحث في فئة #{{category}}" topic: "ابحث في هذا الموضوع" private_messages: "البحث في الرسائل الخاصة" + advanced: + title: البحث التفصيلي + posted_by: + label: المشارك + in_category: + label: في الفئة + in_group: + label: في المجموعة + with_badge: + label: مع الشعارات + filters: + label: استرجع فقط المواضيع/المشاركات التي... + likes: أعجبني + posted: شاركت في + watching: أنا اراقب + tracking: أنا اتابع + private: من ضمن رسائلي + bookmarks: أضفت للمفضلات + first: تلك المشاركة الاولى + statuses: + label: عندما المواضيع + open: مفتوحة + closed: مغلقة + archived: محفوظة + noreplies: لا تحتوي على رد + single_user: تحتوي على مستخدم واحد + post: + count: + label: عدد المشاركات الأدنى + time: + label: ا’رْسِلت + before: قبل + after: بعد hamburger_menu: "انتقل إلى قائمة مواضيع أو فئة أخرى" new_item: "جديد" go_back: 'الرجوع' not_logged_in_user: 'صفحة المستخدم مع ملخص عن نشاطه و إعداداته' current_user: 'الذهاب إلى صفحتك الشخصية' topics: + new_messages_marker: "آخر مشاهدة" bulk: unlist_topics: "ازالة المواضيع من القائمة" reset_read: "تصفير القراءات" diff --git a/config/locales/client.et.yml b/config/locales/client.et.yml index 3375bbe531..acf464a3d0 100644 --- a/config/locales/client.et.yml +++ b/config/locales/client.et.yml @@ -2831,6 +2831,7 @@ et: developer: 'Arendaja' embedding: "Manustamised" legal: "Õiguslik" + user_api: 'Kasutaja API' uncategorized: 'Muu' backups: "Varukoopiad" login: "Sisselogimine" @@ -2919,6 +2920,7 @@ et: sample: "Kasuta järgnevat HTML koodi oma saidil, et luua ja lisada/sängitada discourse teemad oma saidile. Asenda REPLACE_ME kanoonilise lehe URL-viitega, kuhu Sa seda lisad." title: "Sängitamine" host: "Lubatud hostid" + path_whitelist: "Lubatud aadressiosad" edit: "muuda" category: "Postita foorumisse" add_host: "Lisa host" @@ -2958,7 +2960,9 @@ et: done: "Tehtud" back: "Tagasi" next: "Järgmine" + step: "%{current} %{total}st" upload: "Lae üles" + uploading: "Laen üles..." quit: "Võib-olla hiljem" staff_count: one: "Sinu kommuunis on 1 tiimi liige." diff --git a/config/locales/client.fi.yml b/config/locales/client.fi.yml index 97aeb320e3..d7a70bcbe8 100644 --- a/config/locales/client.fi.yml +++ b/config/locales/client.fi.yml @@ -475,6 +475,7 @@ fi: daily: "Lähetä päivittäin" individual: "Lähetä sähköpostia jokaisesta uudesta viestistä" + individual_no_echo: "Lähetä sähköposti jokaisesta uudesta viestistä lukuun ottamatta omiani" many_per_day: "Lähetä minulle sähköpostia jokaisesta uudesta viestistä (noin {{dailyEmailEstimate}} päivässä)" few_per_day: "Lähetä minulle sähköpostia jokaisesta uudesta viestistä (noin 2 päivässä)" tag_settings: "Tunnisteet" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 290aaf3284..6c1079dc3e 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -953,7 +953,7 @@ fr: create_pm: "Message privé" title: "ou appuyez sur Ctrl+Entrée" users_placeholder: "Ajouter un utilisateur" - title_placeholder: "Quel est votre sujet en une phrase descriptive ?" + title_placeholder: "Quel est le sujet en une courte phrase ?" edit_reason_placeholder: "pourquoi modifiez-vous le message ?" show_edit_reason: "(ajouter la raison de la modification)" reply_placeholder: "Écrivez ici. Utilisez Markdown, BBCode, ou HTML pour mettre en forme. Glissez ou collez des images." diff --git a/config/locales/client.ru.yml b/config/locales/client.ru.yml index 6f336f57cb..567c982b0b 100644 --- a/config/locales/client.ru.yml +++ b/config/locales/client.ru.yml @@ -505,7 +505,7 @@ ru: dismiss_notifications: "Отложить все" dismiss_notifications_tooltip: "Пометить все непрочитанные уведомления прочитанными" disable_jump_reply: "Не переходить к вашему новому сообщению после ответа" - dynamic_favicon: "Показывать колличество новых / обновленных тем на иконке сообщений" + dynamic_favicon: "Показывать количество новых / обновленных тем на иконке сообщений" external_links_in_new_tab: "Открывать все внешние ссылки в новой вкладке" enable_quoting: "Позволить отвечать с цитированием выделенного текста" change: "изменить" @@ -684,7 +684,7 @@ ru: weekly: "еженедельно" every_two_weeks: "каждые 2 недели" include_tl0_in_digests: "Включить контент от новых пользователей в сводки, отправляемые по электронной почте" - email_in_reply_to: "Добавить отрывок \"ответил на сообщение\" в электронных писмах" + email_in_reply_to: "Добавить предыдущие ответы к концу электронных писем" email_direct: "Присылать почтовое уведомление, когда кто-то цитирует меня, отвечает на мой пост, упоминает мой @псевдоним или приглашает меня в тему" email_private_messages: "Присылать почтовое уведомление, когда кто-то оставляет мне сообщение" email_always: "Присылать почтовое уведомление, даже если я присутствую на сайте" @@ -1076,6 +1076,7 @@ ru: notifications: title: "уведомления об упоминании @псевдонима, ответах на ваши посты и темы, сообщения и т.д." none: "Уведомления не могут быть загружены." + empty: "Уведомления не найдены." more: "посмотреть более ранние уведомления" total_flagged: "всего сообщений с жалобами" mentioned: "

{{username}} {{description}}

" @@ -1196,6 +1197,7 @@ ru: not_logged_in_user: 'страница пользователя с историей его последней активности и настроек' current_user: 'перейти на вашу страницу пользователя' topics: + new_messages_marker: "последний визит" bulk: unlist_topics: "Исключить из списков" reset_read: "Сбросить прочтённые" @@ -1321,18 +1323,23 @@ ru: auto_close_remove: "Не закрывать тему автоматически" timeline: back: "Вернуться" + back_description: "Перейти к последнему непрочитанному сообщению" + replies_short: "%{current} / %{total}" progress: title: текущее местоположение в теме go_top: "перейти наверх" go_bottom: "перейти вниз" go: "=>" jump_bottom: "перейти к последнему сообщению" + jump_prompt: "перейти к сообщению" + jump_prompt_long: "К какому сообщению вы хотите перейти?" jump_bottom_with_number: "перейти к сообщению %{post_number}" total: всего сообщений current: текущее сообщение notifications: title: изменить частоту уведомлений об этой теме reasons: + '3_10': 'Вы будете получать уведомления, т.к. наблюдаете за тэгом этой темы.' '3_6': 'Вы будете получать уведомления, т.к. наблюдаете за этим разделом.' '3_5': 'Вы будете получать уведомления, т.к. наблюдение темы началось автоматически.' '3_2': 'Вы будете получать уведомления, т.к. наблюдаете за этой темой.' @@ -1402,6 +1409,9 @@ ru: share: title: 'Поделиться' help: 'Поделиться ссылкой на тему' + print: + title: 'Печать' + help: 'Открыть версию для печати' flag_topic: title: 'Жалоба' help: 'пожаловаться на сообщение' @@ -1437,6 +1447,7 @@ ru: no_banner_exists: "Нет текущих тем-объявлений." banner_exists: "На данный момент уже есть тема-объявление." inviting: "Высылаю приглашение..." + automatically_add_to_groups: "Это приглашение предоставит доступ к следующим группам:" invite_private: title: 'Пригласить в беседу' email_or_username: "Адрес электронной почты или псевдоним того, кого вы хотите пригласить" @@ -1564,6 +1575,11 @@ ru: many: "Это сообщение понравилось {{count}} людям" other: "Это сообщение понравилось {{count}} людям" has_likes_title_only_you: "Вам понравилось это сообщение" + has_likes_title_you: + one: "Вам и еще 1 человеку понравилось это сообщение" + few: "Вам и еще {{count}} людям понравилось это сообщение" + many: "Вам и еще {{count}} людям понравилось это сообщение" + other: "Вам и еще {{count}} людям понравилось это сообщение" errors: create: "К сожалению, не удалось создать сообщение из-за ошибки. Попробуйте еще раз." edit: "К сожалению, не удалось изменить сообщение. Попробуйте еще раз." @@ -1628,6 +1644,7 @@ ru: inappropriate: "отмеченно как неуместное" notify_moderators: "уведомлёные модераторы" notify_user: "отправил сообщение" + bookmark: "добавить закладку" like: "понравилось это" vote: "проголосовал за это" by_you: @@ -1756,6 +1773,7 @@ ru: general: 'Общие' settings: 'Настройки' topic_template: "Шаблон темы" + tags: "Тэги" delete: 'Удалить раздел' create: 'Создать Раздел' create_long: 'Создать новый раздел' @@ -1789,6 +1807,7 @@ ru: email_in_allow_strangers: "Принимать письма от анонимных пользователей без учетных записей" email_in_disabled: "Создание новых тем через электронную почту отключено в настройках сайта. Чтобы разрешить создание новых тем через электронную почту," email_in_disabled_click: 'активируйте настройку "email in".' + sort_order: "Порядок сортировки:" allow_badges_label: "Разрешить вручение наград в этом разделе" edit_permissions: "Изменить права доступа" add_permission: "Добавить права" @@ -1809,6 +1828,18 @@ ru: muted: title: "Без уведомлений" description: "Не уведомлять о новых темах в этом разделе и скрыть их из последних." + sort_options: + default: "По умолчанию" + likes: "Количество симпатий" + op_likes: "Количество симпатий у первого сообщения" + views: "Количество просмотров" + posts: "Количество сообщений" + activity: "Последняя активность" + posters: "Количество участников" + category: "Раздел" + created: "Дата создания" + sort_ascending: 'По возрастанию' + sort_descending: 'По убыванию' flagging: title: 'Спасибо за вашу помощь в поддержании порядка!' action: 'Пожаловаться на сообщение' @@ -1853,6 +1884,7 @@ ru: title: "Сводка по теме" participants_title: "Частые авторы" links_title: "Популярные ссылки" + links_shown: "показать больше ссылок..." clicks: one: "1 клик" few: "%{count} клика" diff --git a/config/locales/client.zh_TW.yml b/config/locales/client.zh_TW.yml index 07adb33be1..d6132b8068 100644 --- a/config/locales/client.zh_TW.yml +++ b/config/locales/client.zh_TW.yml @@ -880,6 +880,7 @@ zh_TW: toggle_information: "切換討論話題詳情" read_more_in_category: "要閱讀更多文章嗎? 瀏覽 {{catLink}} 裡的討論話題或 {{latestLink}}。" read_more: "要閱讀更多文章嗎? 請按 {{catLink}} 或 {{latestLink}}。" + read_more_MF: "還有 { UNREAD, plural, =0 {} one { 1 個未讀的討論話題} other { # 個未讀的討論話題 } } { NEW, plural, =0 {} one { {BOTH, select, true{和 } false {} other{}} 1 個新的討論話題} other { {BOTH, select, true{和 } false {} other{}} # 個最近的討論話題} }可以閱讀,或者{CATEGORY, select, true {瀏覽{catLink}中的其他討論話題} false {{latestLink}} other {}}" browse_all_categories: 瀏覽所有分類 view_latest_topics: 檢視最近的文章 suggest_create_topic: 建立一個新討論話題吧? diff --git a/config/locales/server.fi.yml b/config/locales/server.fi.yml index c5d94389ee..36c4b128da 100644 --- a/config/locales/server.fi.yml +++ b/config/locales/server.fi.yml @@ -574,6 +574,16 @@ fi: different_user_description: "Olet kirjautunut sisään eri käyttäjänä kuin jolle sähköposti lähetettiin. Ole hyvä ja kirjaudu ulos tai siirry anonyymitilaan, ja yritä sitten uudelleen." not_found_description: "Pahoittelut, tätä tilauksen perumista ei löytynyt. Kenties sinulle lähetetty linkki on vanhentunut?" log_out: "Kirjaudu ulos" + user_api_key: + title: "Anna sovellukselle käyttöoikeus" + authorize: "Anna käyttöoikeus" + read: "luku" + read_write: "luku/kirjoitus" + description: "\"%{application_name}\" pyytää seuraavaa käyttöoikeutta tunnukseesi:" + no_trust_level: "Pahoittelut, luottamustasosi ei ole riittävä käyttäjärajapinnan käyttämiseen" + generic_error: "Pahoittelut, API-salausavainta ei muodostettu; sivuston ylläpitäjä on saattanut ottaa ominaisuuden pois käytöstä" + scopes: + message_bus: "Reaaliaikaiset päivitykset" reports: visits: title: "Vierailut" diff --git a/config/locales/server.he.yml b/config/locales/server.he.yml index 19dffb38ba..5028bc4800 100644 --- a/config/locales/server.he.yml +++ b/config/locales/server.he.yml @@ -226,7 +226,7 @@ he: - ביקורת מבנה היא רצויה, אבל אנא בקרו *רעיונות*, לא אנשים. - למידע נוסף [ראו את הכללים המנחים שלנו]/guidelines). פאנל זה יופיע רק ב-%{education_posts_text} התגובות הראשונות שלך. + למידע נוסף [ראו את הכללים המנחים שלנו](/guidelines). פאנל זה יופיע רק ב-%{education_posts_text} התגובות הראשונות שלך. avatar: | ### מה דעתך להוסיף תמונה לחשבון שלך? diff --git a/config/locales/server.nb_NO.yml b/config/locales/server.nb_NO.yml index c97670c01d..2e905aee89 100644 --- a/config/locales/server.nb_NO.yml +++ b/config/locales/server.nb_NO.yml @@ -10,6 +10,14 @@ nb_NO: short_date_no_year: "D MMM" short_date: "D MMM, YYYY" long_date: "MMMM D, YYYY h:mma" + datetime_formats: &datetime_formats + formats: + short: "%m-%d-%Y" + short_no_year: "%B %-d" + long: "%B %-d, %Y, %l:%M%P" + time: + am: "am" + <<: *datetime_formats title: "Discourse" topics: "Emner" posts: "innlegg" @@ -745,6 +753,8 @@ nb_NO: csv_export: boolean_yes: "Ja" boolean_no: "Nei" + date: + <<: *datetime_formats activemodel: errors: <<: *errors diff --git a/config/locales/server.sk.yml b/config/locales/server.sk.yml index 562687b5aa..55e2011e3a 100644 --- a/config/locales/server.sk.yml +++ b/config/locales/server.sk.yml @@ -862,7 +862,7 @@ sk: sso_overrides_name: "Nahrádzať lokálne plné mená pomocou plných mien z externej stránky pomocou dát z SSO dotazu pri každom logine a zamedziť lokálnym zmenám." sso_overrides_avatar: "Nahrádzať používateľský avatar pomocou avatara z externej stránky pomocou dát z SSO dotazu. V prípade zapnutia veľmi doporučujeme vypnúť allow_uploaded_avatars" sso_not_approved_url: "Presmeruj nepovolené SSO účty na URL" - enable_local_logins: "Povoliť pouťívanie lokálnych účtov a hesiel. (Poznámka: musí to byť zapnuté pre fungovanie pozvánok)" + enable_local_logins: "Povoliť používanie lokálnych účtov a hesiel. (Poznámka: musí to byť zapnuté pre fungovanie pozvánok)" allow_new_registrations: "Povoliť registráciu nových používateľov. Odznačte to ak chcete zabrániť vytváraniu nových účtov. " enable_signup_cta: "Zobraz oznámenie pre navrátilých anonymných používateľov s výzvou na registráciu účtu. " enable_yahoo_logins: "Povoliť autentifikáciu pomocou Yahoo." diff --git a/plugins/poll/config/locales/client.nb_NO.yml b/plugins/poll/config/locales/client.nb_NO.yml index f987ea6693..b0793b45be 100644 --- a/plugins/poll/config/locales/client.nb_NO.yml +++ b/plugins/poll/config/locales/client.nb_NO.yml @@ -15,6 +15,13 @@ nb_NO: one: "antall stemmer" other: "antall stemmer" average_rating: "Gjennomsnitt: %{average}." + public: + title: "Stemmer er offentlig." + multiple: + help: + at_least_min_options: + one: "Velg minst 1 alternativ" + other: "Velg minst %{count} alternativ" cast-votes: title: "Stem nå" label: "Stem!" diff --git a/plugins/poll/config/locales/server.nb_NO.yml b/plugins/poll/config/locales/server.nb_NO.yml index d5aee2590c..044f6060a6 100644 --- a/plugins/poll/config/locales/server.nb_NO.yml +++ b/plugins/poll/config/locales/server.nb_NO.yml @@ -14,6 +14,9 @@ nb_NO: multiple_polls_with_same_name: "Det er flere spørreundersøkelser med samme navn: %{name}. Bruk 'name' attributten til å identifisere spørreundersøkelsene dine." default_poll_must_have_at_least_2_options: "Spørrsundersøkelser må inneholde minst 2 alternativer." named_poll_must_have_at_least_2_options: "Spørresundersøkelsen %{name} må ha minst 2 svaralternativer." + default_poll_must_have_less_options: + one: "Spørreundersøkelsen må ha mindre enn 1 svaralternativ" + other: "Spørreundersøkelsen må ha mindre enn %{max} svaralternativer." default_poll_must_have_different_options: "Spørreundersøkelsen må ha ulike alternativer." named_poll_must_have_different_options: "Spørresundersøkelsen %{name} må ha ulike svaralternativer." default_poll_with_multiple_choices_has_invalid_parameters: "Spørreundersøkelsen med flere svaralternativer har ugyldige parametere." From 875703a750f11e1006d1966e443926987b68ab95 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 9 Nov 2016 11:46:11 -0500 Subject: [PATCH 054/266] A couple of small but important fixes for later Ember releases --- app/assets/javascripts/admin/templates/plugins-index.hbs | 2 +- .../discourse-common/lib/buffered-render.js.es6 | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/admin/templates/plugins-index.hbs b/app/assets/javascripts/admin/templates/plugins-index.hbs index 34bf63c756..b7d64f2a34 100644 --- a/app/assets/javascripts/admin/templates/plugins-index.hbs +++ b/app/assets/javascripts/admin/templates/plugins-index.hbs @@ -1,4 +1,4 @@ -{{#if length}} +{{#if model.length}} {{#if currentUser.admin}} {{d-button label="admin.plugins.change_settings" diff --git a/app/assets/javascripts/discourse-common/lib/buffered-render.js.es6 b/app/assets/javascripts/discourse-common/lib/buffered-render.js.es6 index 4873bfc778..c1eebb7768 100644 --- a/app/assets/javascripts/discourse-common/lib/buffered-render.js.es6 +++ b/app/assets/javascripts/discourse-common/lib/buffered-render.js.es6 @@ -25,13 +25,18 @@ export function bufferedRender(obj) { return obj; } - const caller = {}; + const caller = { + _didRender: false + }; // True in 1.13 or greater if (Ember.Helper) { caller.didRender = function() { this._super(); - this._customRender(); + if (!this._didRender) { + this._customRender(); + } + this._didRender = true; }; } else { caller.didInsertElement = function() { From b117a9994b86d225c545cff02eda823c2921dc93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 9 Nov 2016 18:06:19 +0100 Subject: [PATCH 055/266] FIX: escape emojis in topic selection modal --- app/assets/javascripts/discourse/controllers/composer.js.es6 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index f5b506a902..dfc59be1bd 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -407,7 +407,7 @@ export default Ember.Controller.extend({ if (currentTopic) { buttons.push({ - "label": I18n.t("composer.reply_here") + "
" + escapeExpression(currentTopic.get('title')) + "
", + "label": I18n.t("composer.reply_here") + "
" + currentTopic.get('fancyTitle') + "
", "class": "btn btn-reply-here", "callback": function() { composer.set('topic', currentTopic); @@ -418,7 +418,7 @@ export default Ember.Controller.extend({ } buttons.push({ - "label": I18n.t("composer.reply_original") + "
" + escapeExpression(this.get('model.topic.title')) + "
", + "label": I18n.t("composer.reply_original") + "
" + this.get('model.topic.fancyTitle') + "
", "class": "btn-primary btn-reply-on-original", "callback": function() { self.save(true); From b0c6cd8afdb800508387a160c046779880c8f024 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Wed, 9 Nov 2016 18:09:26 +0100 Subject: [PATCH 056/266] make jslint happy --- app/assets/javascripts/discourse/controllers/composer.js.es6 | 1 - 1 file changed, 1 deletion(-) diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index dfc59be1bd..da85c4a66d 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -4,7 +4,6 @@ import Draft from 'discourse/models/draft'; import Composer from 'discourse/models/composer'; import { default as computed, observes } from 'ember-addons/ember-computed-decorators'; import { relativeAge } from 'discourse/lib/formatter'; -import { escapeExpression } from 'discourse/lib/utilities'; import InputValidation from 'discourse/models/input-validation'; import { getOwner } from 'discourse-common/lib/get-owner'; From 150cb6659fc0de89238c52710420c092b489275e Mon Sep 17 00:00:00 2001 From: Rafael dos Santos Silva Date: Wed, 9 Nov 2016 16:38:07 -0200 Subject: [PATCH 057/266] FEATURE: Clinking on stats in user summary take you to the respective activity page --- .../discourse/templates/user/summary.hbs | 36 +++++++++++++++---- app/assets/stylesheets/common/base/user.scss | 11 ++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/app/assets/javascripts/discourse/templates/user/summary.hbs b/app/assets/javascripts/discourse/templates/user/summary.hbs index 065455510b..0042f4ba92 100644 --- a/app/assets/javascripts/discourse/templates/user/summary.hbs +++ b/app/assets/javascripts/discourse/templates/user/summary.hbs @@ -1,19 +1,41 @@

{{i18n "user.summary.stats"}}

    -
  • {{user-stat value=model.days_visited label="user.summary.days_visited"}}
  • +
  • + {{user-stat value=model.days_visited label="user.summary.days_visited"}} +
  • {{model.time_read}} {{{i18n "user.summary.time_read"}}}
  • -
  • {{user-stat value=model.posts_read_count label="user.summary.posts_read"}}
  • -
  • {{user-stat value=model.likes_given label="user.summary.likes_given"}}
  • +
  • + {{user-stat value=model.posts_read_count label="user.summary.posts_read"}} +
  • +
  • + {{#link-to 'userActivity.likesGiven'}} + {{user-stat value=model.likes_given label="user.summary.likes_given"}} + {{/link-to}} +
  • {{#if model.bookmark_count}} -
  • {{user-stat value=model.bookmark_count label="user.summary.bookmark_count"}}
  • +
  • + {{#link-to 'userActivity.bookmarks'}} + {{user-stat value=model.bookmark_count label="user.summary.bookmark_count"}} + {{/link-to}} +
  • {{/if}} -
  • {{user-stat value=model.topic_count label="user.summary.topic_count"}}
  • -
  • {{user-stat value=model.post_count label="user.summary.post_count"}}
  • -
  • {{user-stat value=model.likes_received label="user.summary.likes_received"}}
  • +
  • + {{#link-to 'userActivity.topics'}} + {{user-stat value=model.topic_count label="user.summary.topic_count"}} + {{/link-to}} +
  • +
  • + {{#link-to 'userActivity.replies'}} + {{user-stat value=model.post_count label="user.summary.post_count"}} + {{/link-to}} +
  • +
  • + {{user-stat value=model.likes_received label="user.summary.likes_received"}} +
diff --git a/app/assets/stylesheets/common/base/user.scss b/app/assets/stylesheets/common/base/user.scss index 91316490ca..0cdad17992 100644 --- a/app/assets/stylesheets/common/base/user.scss +++ b/app/assets/stylesheets/common/base/user.scss @@ -213,6 +213,17 @@ padding: 10px 14px; margin: 0 5px 10px 0; background: dark-light-diff($primary, $secondary, 90%, -65%); + + &.linked-stat { // This makes the entire "box" (the li) clickable instead of a narrow area. + padding: 0; + a { + padding: 10px 14px; + width: 100%; + height: 100%; + display: block; + color: black; + } + } } li:last-of-type { From 9e69798285078e017501973872f1bf1c2beddd9b Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 10 Nov 2016 00:07:54 +0530 Subject: [PATCH 058/266] FEATURE: watch first post default site setting --- app/models/site_setting.rb | 1 + app/models/user.rb | 2 +- config/locales/server.en.yml | 1 + config/site_settings.yml | 3 +++ lib/site_setting_validations.rb | 13 +++++++++++++ spec/models/user_spec.rb | 3 +++ 6 files changed, 22 insertions(+), 1 deletion(-) diff --git a/app/models/site_setting.rb b/app/models/site_setting.rb index fbe1b782d5..70b62602ee 100644 --- a/app/models/site_setting.rb +++ b/app/models/site_setting.rb @@ -94,6 +94,7 @@ class SiteSetting < ActiveRecord::Base SiteSetting.default_categories_watching.split("|"), SiteSetting.default_categories_tracking.split("|"), SiteSetting.default_categories_muted.split("|"), + SiteSetting.default_categories_watching_first_post.split("|") ].flatten.to_set end diff --git a/app/models/user.rb b/app/models/user.rb index 3de3020d17..2d623817cc 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -988,7 +988,7 @@ class User < ActiveRecord::Base values = [] - %w{watching tracking muted}.each do |s| + %w{watching watching_first_post 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]})" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 04b2e4d94e..fb314c4dd0 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1393,6 +1393,7 @@ en: 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." + default_categories_watching_first_post: "List of first post in each new topic in these categories that are watched by default." max_user_api_reqs_per_day: "Maximum number of user API requests per key per day" max_user_api_reqs_per_minute: "Maximum number of user API requests per key per minute" diff --git a/config/site_settings.yml b/config/site_settings.yml index 92776b8eea..57661444a8 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -1293,6 +1293,9 @@ user_preferences: default_categories_muted: type: category_list default: '' + default_categories_watching_first_post: + type: category_list + default: '' user_api: max_user_api_reqs_per_day: diff --git a/lib/site_setting_validations.rb b/lib/site_setting_validations.rb index e69bfc2f95..0380c457a0 100644 --- a/lib/site_setting_validations.rb +++ b/lib/site_setting_validations.rb @@ -21,6 +21,7 @@ module SiteSettingValidations def validate_default_categories_watching(new_val) default_categories_selected = [ + SiteSetting.default_categories_watching_first_post.split("|"), SiteSetting.default_categories_tracking.split("|"), SiteSetting.default_categories_muted.split("|"), ].flatten.to_set @@ -31,6 +32,7 @@ module SiteSettingValidations def validate_default_categories_tracking(new_val) default_categories_selected = [ SiteSetting.default_categories_watching.split("|"), + SiteSetting.default_categories_watching_first_post.split("|"), SiteSetting.default_categories_muted.split("|"), ].flatten.to_set @@ -40,12 +42,23 @@ module SiteSettingValidations def validate_default_categories_muted(new_val) default_categories_selected = [ SiteSetting.default_categories_watching.split("|"), + SiteSetting.default_categories_watching_first_post.split("|"), SiteSetting.default_categories_tracking.split("|"), ].flatten.to_set validate_default_categories(new_val, default_categories_selected) end + def validate_default_categories_watching_first_post(new_val) + default_categories_selected = [ + SiteSetting.default_categories_watching.split("|"), + SiteSetting.default_categories_tracking.split("|"), + SiteSetting.default_categories_muted.split("|"), + ].flatten.to_set + + validate_default_categories(new_val, default_categories_selected) + end + def validate_enable_s3_uploads(new_val) validate_error :s3_upload_bucket_is_required if new_val == "t" && SiteSetting.s3_upload_bucket.blank? end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index cddc4b50a8..3a32c5e485 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1257,6 +1257,7 @@ describe User do SiteSetting.default_categories_watching = "1" SiteSetting.default_categories_tracking = "2" SiteSetting.default_categories_muted = "3" + SiteSetting.default_categories_watching_first_post = "4" end it "has overriden preferences" do @@ -1279,6 +1280,7 @@ describe User do 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]) + expect(CategoryUser.lookup(user, :watching_first_post).pluck(:category_id)).to eq([4]) end it "does not set category preferences for staged users" do @@ -1286,6 +1288,7 @@ describe User do expect(CategoryUser.lookup(user, :watching).pluck(:category_id)).to eq([]) expect(CategoryUser.lookup(user, :tracking).pluck(:category_id)).to eq([]) expect(CategoryUser.lookup(user, :muted).pluck(:category_id)).to eq([]) + expect(CategoryUser.lookup(user, :watching_first_post).pluck(:category_id)).to eq([]) end end From 96f50790069d74255349d6caa2e943754119a35e Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Thu, 10 Nov 2016 00:15:14 +0530 Subject: [PATCH 059/266] improve default_categories_watching_first_post copy --- config/locales/server.en.yml | 2 +- lib/site_setting_validations.rb | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index fb314c4dd0..0cbdeec8fe 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1393,7 +1393,7 @@ en: 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." - default_categories_watching_first_post: "List of first post in each new topic in these categories that are watched by default." + default_categories_watching_first_post: "List of categories in which first post in each new topic will be watched by default." max_user_api_reqs_per_day: "Maximum number of user API requests per key per day" max_user_api_reqs_per_minute: "Maximum number of user API requests per key per minute" diff --git a/lib/site_setting_validations.rb b/lib/site_setting_validations.rb index 0380c457a0..7858c68f0e 100644 --- a/lib/site_setting_validations.rb +++ b/lib/site_setting_validations.rb @@ -21,9 +21,9 @@ module SiteSettingValidations def validate_default_categories_watching(new_val) default_categories_selected = [ - SiteSetting.default_categories_watching_first_post.split("|"), SiteSetting.default_categories_tracking.split("|"), SiteSetting.default_categories_muted.split("|"), + SiteSetting.default_categories_watching_first_post.split("|") ].flatten.to_set validate_default_categories(new_val, default_categories_selected) @@ -32,8 +32,8 @@ module SiteSettingValidations def validate_default_categories_tracking(new_val) default_categories_selected = [ SiteSetting.default_categories_watching.split("|"), - SiteSetting.default_categories_watching_first_post.split("|"), SiteSetting.default_categories_muted.split("|"), + SiteSetting.default_categories_watching_first_post.split("|") ].flatten.to_set validate_default_categories(new_val, default_categories_selected) @@ -42,8 +42,8 @@ module SiteSettingValidations def validate_default_categories_muted(new_val) default_categories_selected = [ SiteSetting.default_categories_watching.split("|"), - SiteSetting.default_categories_watching_first_post.split("|"), SiteSetting.default_categories_tracking.split("|"), + SiteSetting.default_categories_watching_first_post.split("|") ].flatten.to_set validate_default_categories(new_val, default_categories_selected) @@ -53,7 +53,7 @@ module SiteSettingValidations default_categories_selected = [ SiteSetting.default_categories_watching.split("|"), SiteSetting.default_categories_tracking.split("|"), - SiteSetting.default_categories_muted.split("|"), + SiteSetting.default_categories_muted.split("|") ].flatten.to_set validate_default_categories(new_val, default_categories_selected) From 1d784f5758c355b67b79460b09fa3c8d5f0ef614 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 9 Nov 2016 13:57:43 -0500 Subject: [PATCH 060/266] Allow Ember 2.4 to connect views to widgets --- app/assets/javascripts/discourse/widgets/connector.js.es6 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/discourse/widgets/connector.js.es6 b/app/assets/javascripts/discourse/widgets/connector.js.es6 index d197914af2..cc133942d3 100644 --- a/app/assets/javascripts/discourse/widgets/connector.js.es6 +++ b/app/assets/javascripts/discourse/widgets/connector.js.es6 @@ -25,6 +25,9 @@ export default class Connector { templateName: opts.templateName, context }); + if (Ember.setOwner) { + Ember.setOwner(view, Ember.getOwner(mounted)); + } mounted._connected.push(view); view.renderer.replaceIn(view, $elem[0]); From 985b855a5b584fb6f3944d901d887bce3ccf330e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Wed, 9 Nov 2016 14:47:58 -0500 Subject: [PATCH 061/266] FIX: Later versions of ember require dashes in template names --- .../javascripts/admin/components/permalinks-list.js.es6 | 2 +- .../admin/components/screened-emails-list.js.es6 | 2 +- .../admin/components/screened-ip-addresses-list.js.es6 | 2 +- .../javascripts/admin/components/screened-urls-list.js.es6 | 2 +- .../admin/components/staff-action-logs-list.js.es6 | 2 +- .../javascripts/admin/routes/admin-email-index.js.es6 | 4 ---- .../admin/routes/admin-logs-screened-emails.js.es6 | 2 +- .../admin/routes/admin-logs-screened-ip-addresses.js.es6 | 2 +- .../admin/routes/admin-logs-screened-urls.js.es6 | 2 +- .../admin/routes/admin-logs-staff-action-logs.js.es6 | 2 +- .../templates/{backups_index.hbs => backups-index.hbs} | 0 .../{customize_colors.hbs => customize-colors.hbs} | 0 .../admin/templates/{email_index.hbs => email-index.hbs} | 0 .../{email_preview_digest.hbs => email-preview-digest.hbs} | 0 .../admin/templates/{groups_type.hbs => groups-type.hbs} | 0 .../templates/logs/{details_modal.hbs => details-modal.hbs} | 0 ...d_emails_list_item.hbs => screened-emails-list-item.hbs} | 0 .../logs/{screened_emails.hbs => screened-emails.hbs} | 0 ...es_list_item.hbs => screened-ip-addresses-list-item.hbs} | 0 ...{screened_ip_addresses.hbs => screened-ip-addresses.hbs} | 0 ...eened_urls_list_item.hbs => screened-urls-list-item.hbs} | 0 .../templates/logs/{screened_urls.hbs => screened-urls.hbs} | 0 ...ge_details.hbs => site-customization-change-details.hbs} | 0 ...change_modal.hbs => site-customization-change-modal.hbs} | 4 ++-- ...n_logs_list_item.hbs => staff-action-logs-list-item.hbs} | 0 .../logs/{staff_action_logs.hbs => staff-action-logs.hbs} | 0 .../modal/{admin_agree_flag.hbs => admin-agree-flag.hbs} | 0 .../{admin_badge_preview.hbs => admin-badge-preview.hbs} | 0 .../modal/{admin_delete_flag.hbs => admin-delete-flag.hbs} | 0 ...t_badge_groupings.hbs => admin-edit-badge-groupings.hbs} | 0 .../{admin_incoming_email.hbs => admin-incoming-email.hbs} | 0 .../{admin_start_backup.hbs => admin-start-backup.hbs} | 0 .../{admin_suspend_user.hbs => admin-suspend-user.hbs} | 0 .../{permalinks_list_item.hbs => permalinks-list-item.hbs} | 0 .../admin/templates/{user_badges.hbs => user-badges.hbs} | 0 .../admin/templates/{users_list.hbs => users-list.hbs} | 0 .../discourse/controllers/topic-bulk-actions.js.es6 | 6 +++--- .../{topic_list_item.raw.hbs => topic-list-item.raw.hbs} | 0 .../modal/{avatar_selector.hbs => avatar-selector.hbs} | 0 .../{bulk_actions_buttons.hbs => bulk-actions-buttons.hbs} | 0 .../{bulk_change_category.hbs => bulk-change-category.hbs} | 0 ...k_notification_level.hbs => bulk-notification-level.hbs} | 0 .../modal/{option_boolean.hbs => option-boolean.hbs} | 0 .../templates/modal/{raw_email.hbs => raw-email.hbs} | 0 .../templates/modal/{search_help.hbs => search-help.hbs} | 0 app/assets/javascripts/discourse/views/raw-email.es6 | 2 +- 46 files changed, 15 insertions(+), 19 deletions(-) rename app/assets/javascripts/admin/templates/{backups_index.hbs => backups-index.hbs} (100%) rename app/assets/javascripts/admin/templates/{customize_colors.hbs => customize-colors.hbs} (100%) rename app/assets/javascripts/admin/templates/{email_index.hbs => email-index.hbs} (100%) rename app/assets/javascripts/admin/templates/{email_preview_digest.hbs => email-preview-digest.hbs} (100%) rename app/assets/javascripts/admin/templates/{groups_type.hbs => groups-type.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{details_modal.hbs => details-modal.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{screened_emails_list_item.hbs => screened-emails-list-item.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{screened_emails.hbs => screened-emails.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{screened_ip_addresses_list_item.hbs => screened-ip-addresses-list-item.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{screened_ip_addresses.hbs => screened-ip-addresses.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{screened_urls_list_item.hbs => screened-urls-list-item.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{screened_urls.hbs => screened-urls.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{_site_customization_change_details.hbs => site-customization-change-details.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{site_customization_change_modal.hbs => site-customization-change-modal.hbs} (86%) rename app/assets/javascripts/admin/templates/logs/{staff_action_logs_list_item.hbs => staff-action-logs-list-item.hbs} (100%) rename app/assets/javascripts/admin/templates/logs/{staff_action_logs.hbs => staff-action-logs.hbs} (100%) rename app/assets/javascripts/admin/templates/modal/{admin_agree_flag.hbs => admin-agree-flag.hbs} (100%) rename app/assets/javascripts/admin/templates/modal/{admin_badge_preview.hbs => admin-badge-preview.hbs} (100%) rename app/assets/javascripts/admin/templates/modal/{admin_delete_flag.hbs => admin-delete-flag.hbs} (100%) rename app/assets/javascripts/admin/templates/modal/{admin_edit_badge_groupings.hbs => admin-edit-badge-groupings.hbs} (100%) rename app/assets/javascripts/admin/templates/modal/{admin_incoming_email.hbs => admin-incoming-email.hbs} (100%) rename app/assets/javascripts/admin/templates/modal/{admin_start_backup.hbs => admin-start-backup.hbs} (100%) rename app/assets/javascripts/admin/templates/modal/{admin_suspend_user.hbs => admin-suspend-user.hbs} (100%) rename app/assets/javascripts/admin/templates/{permalinks_list_item.hbs => permalinks-list-item.hbs} (100%) rename app/assets/javascripts/admin/templates/{user_badges.hbs => user-badges.hbs} (100%) rename app/assets/javascripts/admin/templates/{users_list.hbs => users-list.hbs} (100%) rename app/assets/javascripts/discourse/templates/mobile/list/{topic_list_item.raw.hbs => topic-list-item.raw.hbs} (100%) rename app/assets/javascripts/discourse/templates/modal/{avatar_selector.hbs => avatar-selector.hbs} (100%) rename app/assets/javascripts/discourse/templates/modal/{bulk_actions_buttons.hbs => bulk-actions-buttons.hbs} (100%) rename app/assets/javascripts/discourse/templates/modal/{bulk_change_category.hbs => bulk-change-category.hbs} (100%) rename app/assets/javascripts/discourse/templates/modal/{bulk_notification_level.hbs => bulk-notification-level.hbs} (100%) rename app/assets/javascripts/discourse/templates/modal/{option_boolean.hbs => option-boolean.hbs} (100%) rename app/assets/javascripts/discourse/templates/modal/{raw_email.hbs => raw-email.hbs} (100%) rename app/assets/javascripts/discourse/templates/modal/{search_help.hbs => search-help.hbs} (100%) diff --git a/app/assets/javascripts/admin/components/permalinks-list.js.es6 b/app/assets/javascripts/admin/components/permalinks-list.js.es6 index c425437686..c5027fe8e7 100644 --- a/app/assets/javascripts/admin/components/permalinks-list.js.es6 +++ b/app/assets/javascripts/admin/components/permalinks-list.js.es6 @@ -4,5 +4,5 @@ import ListItemView from 'ember-addons/list-item-view'; export default ListView.extend({ height: 700, rowHeight: 32, - itemViewClass: ListItemView.extend({templateName: "admin/templates/permalinks_list_item"}) + itemViewClass: ListItemView.extend({templateName: "admin/templates/permalinks-list-item"}) }); diff --git a/app/assets/javascripts/admin/components/screened-emails-list.js.es6 b/app/assets/javascripts/admin/components/screened-emails-list.js.es6 index 1b32fb36fe..aaf883a3c9 100644 --- a/app/assets/javascripts/admin/components/screened-emails-list.js.es6 +++ b/app/assets/javascripts/admin/components/screened-emails-list.js.es6 @@ -4,5 +4,5 @@ import ListItemView from 'ember-addons/list-item-view'; export default ListView.extend({ height: 700, rowHeight: 32, - itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_emails_list_item"}) + itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened-emails-list-item"}) }); diff --git a/app/assets/javascripts/admin/components/screened-ip-addresses-list.js.es6 b/app/assets/javascripts/admin/components/screened-ip-addresses-list.js.es6 index 0d30fc6d48..83abab4b8c 100644 --- a/app/assets/javascripts/admin/components/screened-ip-addresses-list.js.es6 +++ b/app/assets/javascripts/admin/components/screened-ip-addresses-list.js.es6 @@ -4,5 +4,5 @@ import ListItemView from 'ember-addons/list-item-view'; export default ListView.extend({ height: 700, rowHeight: 32, - itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_ip_addresses_list_item"}) + itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened-ip-addresses-list-item"}) }); diff --git a/app/assets/javascripts/admin/components/screened-urls-list.js.es6 b/app/assets/javascripts/admin/components/screened-urls-list.js.es6 index b9d8b76667..b34bc3fc51 100644 --- a/app/assets/javascripts/admin/components/screened-urls-list.js.es6 +++ b/app/assets/javascripts/admin/components/screened-urls-list.js.es6 @@ -4,5 +4,5 @@ import ListItemView from 'ember-addons/list-item-view'; export default ListView.extend({ height: 700, rowHeight: 32, - itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened_urls_list_item"}) + itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/screened-urls-list-item"}) }); diff --git a/app/assets/javascripts/admin/components/staff-action-logs-list.js.es6 b/app/assets/javascripts/admin/components/staff-action-logs-list.js.es6 index cec82dba43..bf68175563 100644 --- a/app/assets/javascripts/admin/components/staff-action-logs-list.js.es6 +++ b/app/assets/javascripts/admin/components/staff-action-logs-list.js.es6 @@ -4,5 +4,5 @@ import ListItemView from 'ember-addons/list-item-view'; export default ListView.extend({ height: 700, rowHeight: 75, - itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/staff_action_logs_list_item"}) + itemViewClass: ListItemView.extend({templateName: "admin/templates/logs/staff-action-logs-list-item"}) }); diff --git a/app/assets/javascripts/admin/routes/admin-email-index.js.es6 b/app/assets/javascripts/admin/routes/admin-email-index.js.es6 index 1b75e39f6f..ae1272c0b9 100644 --- a/app/assets/javascripts/admin/routes/admin-email-index.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-email-index.js.es6 @@ -3,9 +3,5 @@ import EmailSettings from 'admin/models/email-settings'; export default Discourse.Route.extend({ model() { return EmailSettings.find(); - }, - - renderTemplate() { - this.render('admin/templates/email_index', { into: 'adminEmail' }); } }); diff --git a/app/assets/javascripts/admin/routes/admin-logs-screened-emails.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-screened-emails.js.es6 index 1008dc1c1d..d31e521924 100644 --- a/app/assets/javascripts/admin/routes/admin-logs-screened-emails.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-logs-screened-emails.js.es6 @@ -1,6 +1,6 @@ export default Discourse.Route.extend({ renderTemplate: function() { - this.render('admin/templates/logs/screened_emails', {into: 'adminLogs'}); + this.render('admin/templates/logs/screened-emails', {into: 'adminLogs'}); }, setupController: function() { diff --git a/app/assets/javascripts/admin/routes/admin-logs-screened-ip-addresses.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-screened-ip-addresses.js.es6 index ca914fe1d2..c6518c244c 100644 --- a/app/assets/javascripts/admin/routes/admin-logs-screened-ip-addresses.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-logs-screened-ip-addresses.js.es6 @@ -1,6 +1,6 @@ export default Discourse.Route.extend({ renderTemplate() { - this.render('admin/templates/logs/screened_ip_addresses', {into: 'adminLogs'}); + this.render('admin/templates/logs/screened-ip-addresses', {into: 'adminLogs'}); }, setupController() { diff --git a/app/assets/javascripts/admin/routes/admin-logs-screened-urls.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-screened-urls.js.es6 index 093dd26331..ee65fda090 100644 --- a/app/assets/javascripts/admin/routes/admin-logs-screened-urls.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-logs-screened-urls.js.es6 @@ -1,6 +1,6 @@ export default Discourse.Route.extend({ renderTemplate: function() { - this.render('admin/templates/logs/screened_urls', {into: 'adminLogs'}); + this.render('admin/templates/logs/screened-urls', {into: 'adminLogs'}); }, setupController: function() { diff --git a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 index 6ef900e843..8aaa771881 100644 --- a/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 +++ b/app/assets/javascripts/admin/routes/admin-logs-staff-action-logs.js.es6 @@ -3,7 +3,7 @@ import showModal from 'discourse/lib/show-modal'; export default Discourse.Route.extend({ // TODO: make this automatic using an `{{outlet}}` renderTemplate: function() { - this.render('admin/templates/logs/staff_action_logs', {into: 'adminLogs'}); + this.render('admin/templates/logs/staff-action-logs', {into: 'adminLogs'}); }, setupController: function(controller) { diff --git a/app/assets/javascripts/admin/templates/backups_index.hbs b/app/assets/javascripts/admin/templates/backups-index.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/backups_index.hbs rename to app/assets/javascripts/admin/templates/backups-index.hbs diff --git a/app/assets/javascripts/admin/templates/customize_colors.hbs b/app/assets/javascripts/admin/templates/customize-colors.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/customize_colors.hbs rename to app/assets/javascripts/admin/templates/customize-colors.hbs diff --git a/app/assets/javascripts/admin/templates/email_index.hbs b/app/assets/javascripts/admin/templates/email-index.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/email_index.hbs rename to app/assets/javascripts/admin/templates/email-index.hbs diff --git a/app/assets/javascripts/admin/templates/email_preview_digest.hbs b/app/assets/javascripts/admin/templates/email-preview-digest.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/email_preview_digest.hbs rename to app/assets/javascripts/admin/templates/email-preview-digest.hbs diff --git a/app/assets/javascripts/admin/templates/groups_type.hbs b/app/assets/javascripts/admin/templates/groups-type.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/groups_type.hbs rename to app/assets/javascripts/admin/templates/groups-type.hbs diff --git a/app/assets/javascripts/admin/templates/logs/details_modal.hbs b/app/assets/javascripts/admin/templates/logs/details-modal.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/logs/details_modal.hbs rename to app/assets/javascripts/admin/templates/logs/details-modal.hbs diff --git a/app/assets/javascripts/admin/templates/logs/screened_emails_list_item.hbs b/app/assets/javascripts/admin/templates/logs/screened-emails-list-item.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/logs/screened_emails_list_item.hbs rename to app/assets/javascripts/admin/templates/logs/screened-emails-list-item.hbs diff --git a/app/assets/javascripts/admin/templates/logs/screened_emails.hbs b/app/assets/javascripts/admin/templates/logs/screened-emails.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/logs/screened_emails.hbs rename to app/assets/javascripts/admin/templates/logs/screened-emails.hbs diff --git a/app/assets/javascripts/admin/templates/logs/screened_ip_addresses_list_item.hbs b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses-list-item.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/logs/screened_ip_addresses_list_item.hbs rename to app/assets/javascripts/admin/templates/logs/screened-ip-addresses-list-item.hbs diff --git a/app/assets/javascripts/admin/templates/logs/screened_ip_addresses.hbs b/app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/logs/screened_ip_addresses.hbs rename to app/assets/javascripts/admin/templates/logs/screened-ip-addresses.hbs diff --git a/app/assets/javascripts/admin/templates/logs/screened_urls_list_item.hbs b/app/assets/javascripts/admin/templates/logs/screened-urls-list-item.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/logs/screened_urls_list_item.hbs rename to app/assets/javascripts/admin/templates/logs/screened-urls-list-item.hbs diff --git a/app/assets/javascripts/admin/templates/logs/screened_urls.hbs b/app/assets/javascripts/admin/templates/logs/screened-urls.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/logs/screened_urls.hbs rename to app/assets/javascripts/admin/templates/logs/screened-urls.hbs diff --git a/app/assets/javascripts/admin/templates/logs/_site_customization_change_details.hbs b/app/assets/javascripts/admin/templates/logs/site-customization-change-details.hbs similarity index 100% rename from app/assets/javascripts/admin/templates/logs/_site_customization_change_details.hbs rename to app/assets/javascripts/admin/templates/logs/site-customization-change-details.hbs diff --git a/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs b/app/assets/javascripts/admin/templates/logs/site-customization-change-modal.hbs similarity index 86% rename from app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs rename to app/assets/javascripts/admin/templates/logs/site-customization-change-modal.hbs index 8d36dd964c..24f1e18912 100644 --- a/app/assets/javascripts/admin/templates/logs/site_customization_change_modal.hbs +++ b/app/assets/javascripts/admin/templates/logs/site-customization-change-modal.hbs @@ -11,7 +11,7 @@