From 1e166d89ff236601329a45a92b8cd3c8a9edeb98 Mon Sep 17 00:00:00 2001 From: Erick Guan Date: Sat, 20 Dec 2014 22:07:29 +0800 Subject: [PATCH 01/17] support setting category slug --- .../controllers/edit-category.js.es6 | 4 +- .../javascripts/discourse/models/category.js | 1 + .../templates/modal/edit-category-general.hbs | 10 ++++- app/assets/stylesheets/common/base/modal.scss | 4 ++ app/controllers/categories_controller.rb | 13 +++++++ app/models/category.rb | 23 +++++------ config/locales/client.en.yml | 2 + config/routes.rb | 1 + .../controllers/categories_controller_spec.rb | 38 +++++++++++++++++++ spec/fabricators/category_fabricator.rb | 6 +++ spec/models/category_spec.rb | 5 +++ 11 files changed, 92 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 index 7f0f66549..443d3bcb7 100644 --- a/app/assets/javascripts/discourse/controllers/edit-category.js.es6 +++ b/app/assets/javascripts/discourse/controllers/edit-category.js.es6 @@ -146,9 +146,9 @@ export default ObjectController.extend(ModalFunctionality, { }).catch(function(error) { if (error && error.responseText) { - self.flash($.parseJSON(error.responseText).errors[0]); + self.flash($.parseJSON(error.responseText).errors[0], 'error'); } else { - self.flash(I18n.t('generic_error')); + self.flash(I18n.t('generic_error'), 'error'); } self.set('saving', false); }); diff --git a/app/assets/javascripts/discourse/models/category.js b/app/assets/javascripts/discourse/models/category.js index 3f0732328..3f8bb6edb 100644 --- a/app/assets/javascripts/discourse/models/category.js +++ b/app/assets/javascripts/discourse/models/category.js @@ -58,6 +58,7 @@ Discourse.Category = Discourse.Model.extend({ return Discourse.ajax(url, { data: { name: this.get('name'), + slug: this.get('slug'), color: this.get('color'), text_color: this.get('text_color'), secure: this.get('secure'), diff --git a/app/assets/javascripts/discourse/templates/modal/edit-category-general.hbs b/app/assets/javascripts/discourse/templates/modal/edit-category-general.hbs index 096441c01..7931c5ef5 100644 --- a/app/assets/javascripts/discourse/templates/modal/edit-category-general.hbs +++ b/app/assets/javascripts/discourse/templates/modal/edit-category-general.hbs @@ -1,7 +1,13 @@
- - {{text-field value=name placeholderKey="category.name_placeholder" maxlength="50"}} +
+ + {{text-field value=name placeholderKey="category.name_placeholder" maxlength="50"}} +
+
+ + {{text-field value=slug placeholderKey="category.slug_placeholder" maxlength="255"}} +
{{#if canSelectParentCategory}} diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss index 8e35ab13b..bc7728447 100644 --- a/app/assets/stylesheets/common/base/modal.scss +++ b/app/assets/stylesheets/common/base/modal.scss @@ -130,6 +130,10 @@ section.field { margin-bottom: 20px; } + section.field .field-item { + display: inline-block; + margin-right: 10px; + } } .reply-where-modal { diff --git a/app/controllers/categories_controller.rb b/app/controllers/categories_controller.rb index 3fb80f71a..a97f7ff77 100644 --- a/app/controllers/categories_controller.rb +++ b/app/controllers/categories_controller.rb @@ -95,6 +95,19 @@ class CategoriesController < ApplicationController end end + def update_slug + @category = Category.find(params[:category_id].to_i) + guardian.ensure_can_edit!(@category) + + custom_slug = params[:slug].to_s + + if custom_slug.present? && @category.update_attributes(slug: custom_slug) + render json: success_json + else + render_json_error(@category) + end + end + def set_notifications category_id = params[:category_id].to_i notification_level = params[:notification_level].to_i diff --git a/app/models/category.rb b/app/models/category.rb index 4103eb869..3535019b7 100644 --- a/app/models/category.rb +++ b/app/models/category.rb @@ -201,18 +201,19 @@ SQL end def ensure_slug - if name.present? - self.name.strip! + return unless name.present? - if slug.present? - # custom slug - errors.add(:slug, "is already in use") if duplicate_slug? - else - # auto slug - self.slug = Slug.for(name) - return if self.slug.blank? - self.slug = '' if duplicate_slug? - end + self.name.strip! + + if slug.present? + # santized custom slug + self.slug = Slug.for(slug) + errors.add(:slug, 'is already in use') if duplicate_slug? + else + # auto slug + self.slug = Slug.for(name) + return if self.slug.blank? + self.slug = '' if duplicate_slug? end end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 294010a1c..5186255fe 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1281,6 +1281,8 @@ en: delete: 'Delete Category' create: 'New Category' save: 'Save Category' + slug: 'Category Slug' + slug_placeholder: '(Optional) dashed-words for url' creation_error: There has been an error during the creation of the category. save_error: There was an error saving the category. name: "Category Name" diff --git a/config/routes.rb b/config/routes.rb index 81a8d29ef..0c883317b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -325,6 +325,7 @@ Discourse::Application.routes.draw do post "category/uploads" => "categories#upload" post "category/:category_id/move" => "categories#move" post "category/:category_id/notifications" => "categories#set_notifications" + put "category/:category_id/slug" => "categories#update_slug" get "c/:id/show" => "categories#show" get "c/:category.rss" => "list#category_feed", format: :rss diff --git a/spec/controllers/categories_controller_spec.rb b/spec/controllers/categories_controller_spec.rb index 9131980c1..0dd227c4b 100644 --- a/spec/controllers/categories_controller_spec.rb +++ b/spec/controllers/categories_controller_spec.rb @@ -203,4 +203,42 @@ describe CategoriesController do end + describe 'update_slug' do + it 'requires the user to be logged in' do + lambda { xhr :put, :update_slug, category_id: 'category'}.should raise_error(Discourse::NotLoggedIn) + end + + describe 'logged in' do + let(:valid_attrs) { {id: @category.id, slug: 'fff'} } + + before do + @user = log_in(:admin) + @category = Fabricate(:happy_category, user: @user) + end + + it 'rejects blank' do + xhr :put, :update_slug, category_id: @category.id, slug: nil + response.status.should == 422 + end + + it 'accepts valid custom slug' do + xhr :put, :update_slug, category_id: @category.id, slug: 'valid-slug' + response.should be_success + category = Category.find(@category.id) + category.slug.should == 'valid-slug' + end + + it 'accepts not well formed custom slug' do + xhr :put, :update_slug, category_id: @category.id, slug: ' valid slug' + response.should be_success + category = Category.find(@category.id) + category.slug.should == 'valid-slug' + end + + it 'rejects invalid custom slug' do + xhr :put, :update_slug, category_id: @category.id, slug: ' ' + response.status.should == 422 + end + end + end end diff --git a/spec/fabricators/category_fabricator.rb b/spec/fabricators/category_fabricator.rb index dae133774..1f237b5d6 100644 --- a/spec/fabricators/category_fabricator.rb +++ b/spec/fabricators/category_fabricator.rb @@ -7,3 +7,9 @@ Fabricator(:diff_category, from: :category) do name "Different Category" user end + +Fabricator(:happy_category, from: :category) do + name 'Happy Category' + slug 'happy' + user +end diff --git a/spec/models/category_spec.rb b/spec/models/category_spec.rb index 9e3cf3d0f..93b3cf5ec 100644 --- a/spec/models/category_spec.rb +++ b/spec/models/category_spec.rb @@ -198,6 +198,11 @@ describe Category do c.slug.should eq("cats-category") end + it 'and be sanitized' do + c = Fabricate(:category, name: 'Cats', slug: ' invalid slug') + c.slug.should == 'invalid-slug' + end + it 'fails if custom slug is duplicate with existing' do c1 = Fabricate(:category, name: "Cats", slug: "cats") c2 = Fabricate.build(:category, name: "More Cats", slug: "cats") From bae432b255a9fdbe2446c277358318dbd36ae08e Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 2 Jan 2015 11:03:14 +0530 Subject: [PATCH 02/17] FIX: show only staff actions in export and hide site setting logs from moderators --- app/jobs/regular/export_csv_file.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index c0b86a7f9..6c8c9e5d0 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -68,7 +68,13 @@ module Jobs end def staff_action_export - staff_action_data = UserHistory.order('id DESC').to_a + if @current_user.admin? + staff_action_data = UserHistory.only_staff_actions.order('id DESC').to_a + else + # moderator + staff_action_data = UserHistory.where(admin_only: false).only_staff_actions.order('id DESC').to_a + end + staff_action_data.map do |staff_action| get_staff_action_fields(staff_action) end From bfe95966b47b1829577acd474acf33a1edff98e5 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Fri, 2 Jan 2015 12:29:05 +0530 Subject: [PATCH 03/17] better filenames for export --- app/assets/javascripts/discourse/models/export_csv.js | 2 +- app/controllers/export_csv_controller.rb | 2 +- app/jobs/regular/export_csv_file.rb | 10 ++++++---- spec/controllers/export_csv_controller_spec.rb | 6 +++--- spec/jobs/export_csv_file_spec.rb | 11 +++++------ 5 files changed, 16 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/discourse/models/export_csv.js b/app/assets/javascripts/discourse/models/export_csv.js index be678c392..9d5c00d89 100644 --- a/app/assets/javascripts/discourse/models/export_csv.js +++ b/app/assets/javascripts/discourse/models/export_csv.js @@ -30,7 +30,7 @@ Discourse.ExportCsv.reopenClass({ @method export_user_list **/ exportUserList: function() { - return Discourse.ajax("/export_csv/export_entity.json", {data: {entity_type: 'admin', entity: 'user'}}); + return Discourse.ajax("/export_csv/export_entity.json", {data: {entity_type: 'admin', entity: 'user_list'}}); }, /** diff --git a/app/controllers/export_csv_controller.rb b/app/controllers/export_csv_controller.rb index c28ffe864..a8f875ede 100644 --- a/app/controllers/export_csv_controller.rb +++ b/app/controllers/export_csv_controller.rb @@ -15,7 +15,7 @@ class ExportCsvController < ApplicationController def show params.require(:id) filename = params.fetch(:id) - export_id = filename.split('_')[1].split('.')[0] + export_id = filename.split('-')[2].split('.')[0] export_initiated_by_user_id = 0 export_initiated_by_user_id = UserExport.where(id: export_id)[0].user_id unless UserExport.where(id: export_id).empty? export_csv_path = UserExport.get_download_path(filename) diff --git a/app/jobs/regular/export_csv_file.rb b/app/jobs/regular/export_csv_file.rb index 6c8c9e5d0..b0ae17c36 100644 --- a/app/jobs/regular/export_csv_file.rb +++ b/app/jobs/regular/export_csv_file.rb @@ -24,6 +24,7 @@ module Jobs def execute(args) entity = args[:entity] + @file_name = entity if entity == "user_archive" @entity_type = "user" @@ -56,12 +57,12 @@ module Jobs end end - def user_export + def user_list_export query = ::AdminUserIndexQuery.new user_data = query.find_users_query.to_a user_data.map do |user| group_names = get_group_names(user).join(';') - user_array = get_user_fields(user) + user_array = get_user_list_fields(user) user_array.push(group_names) if group_names != '' user_array end @@ -168,7 +169,7 @@ module Jobs user_archive_array end - def get_user_fields(user) + def get_user_list_fields(user) user_array = [] HEADER_ATTRS_FOR['user'].each do |attr| @@ -271,7 +272,8 @@ module Jobs def set_file_path @file = UserExport.create(export_type: @entity_type, user_id: @current_user.id) - @file_name = "export_#{@file.id}.csv" + file_name_prefix = @file_name.split('_').join('-') + @file_name = "#{file_name_prefix}-#{@file.id}.csv" # ensure directory exists dir = File.dirname("#{UserExport.base_directory}/#{@file_name}") diff --git a/spec/controllers/export_csv_controller_spec.rb b/spec/controllers/export_csv_controller_spec.rb index cb40652ee..1b9a7337a 100644 --- a/spec/controllers/export_csv_controller_spec.rb +++ b/spec/controllers/export_csv_controller_spec.rb @@ -1,7 +1,7 @@ require "spec_helper" describe ExportCsvController do - let(:export_filename) { "export_999.csv" } + let(:export_filename) { "user-archive-999.csv" } context "while logged in as normal user" do @@ -30,7 +30,7 @@ describe ExportCsvController do describe ".download" do it "uses send_file to transmit the export file" do file = UserExport.create(export_type: "user", user_id: @user.id) - file_name = "export_#{file.id}.csv" + file_name = "user-archive-#{file.id}.csv" controller.stubs(:render) export = UserExport.new() UserExport.expects(:get_download_path).with(file_name).returns(export) @@ -74,7 +74,7 @@ describe ExportCsvController do describe ".download" do it "uses send_file to transmit the export file" do file = UserExport.create(export_type: "admin", user_id: @admin.id) - file_name = "export_#{file.id}.csv" + file_name = "screened-email-#{file.id}.csv" controller.stubs(:render) export = UserExport.new() UserExport.expects(:get_download_path).with(file_name).returns(export) diff --git a/spec/jobs/export_csv_file_spec.rb b/spec/jobs/export_csv_file_spec.rb index 8c41b43bc..f0f05afc1 100644 --- a/spec/jobs/export_csv_file_spec.rb +++ b/spec/jobs/export_csv_file_spec.rb @@ -8,16 +8,16 @@ describe Jobs::ExportCsvFile do end end - let :user_header do + let :user_list_header do Jobs::ExportCsvFile.new.get_header('user') end - let :user_export do - Jobs::ExportCsvFile.new.user_export + let :user_list_export do + Jobs::ExportCsvFile.new.user_list_export end def to_hash(row) - Hash[*user_header.zip(row).flatten] + Hash[*user_list_header.zip(row).flatten] end it 'exports sso data' do @@ -25,10 +25,9 @@ describe Jobs::ExportCsvFile do user = Fabricate(:user) user.create_single_sign_on_record(external_id: "123", last_payload: "xxx", external_email: 'test@test.com') - user = to_hash(user_export.find{|u| u[0] == user.id}) + user = to_hash(user_list_export.find{|u| u[0] == user.id}) user["external_id"].should == "123" user["external_email"].should == "test@test.com" end end - From c57a1b393f0fefb9e3094514fa03c9c5046cc6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 2 Jan 2015 12:37:17 +0100 Subject: [PATCH 04/17] clean up 'checked_for_custom_avatars' user history entries --- app/models/user_history.rb | 2 +- .../20150102113309_clean_up_user_history.rb | 10 ++++ lib/composer_messages_finder.rb | 58 +++++++++++-------- .../composer_messages_finder_spec.rb | 53 +++++++---------- 4 files changed, 66 insertions(+), 57 deletions(-) create mode 100644 db/migrate/20150102113309_clean_up_user_history.rb diff --git a/app/models/user_history.rb b/app/models/user_history.rb index 90aae0c0b..b4bfcad6a 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -20,7 +20,7 @@ class UserHistory < ActiveRecord::Base :change_site_setting, :change_site_customization, :delete_site_customization, - :checked_for_custom_avatar, + :checked_for_custom_avatar, # not used anymore :notified_about_avatar, :notified_about_sequential_replies, :notified_about_dominating_topic, diff --git a/db/migrate/20150102113309_clean_up_user_history.rb b/db/migrate/20150102113309_clean_up_user_history.rb new file mode 100644 index 000000000..8ddbe9be0 --- /dev/null +++ b/db/migrate/20150102113309_clean_up_user_history.rb @@ -0,0 +1,10 @@ +class CleanUpUserHistory < ActiveRecord::Migration + def up + # 'checked_for_custom_avatar' is not used anymore + # was removed in https://github.com/discourse/discourse/commit/6c1c8be79433f87bef9d768da7b8fa4ec9bb18d7 + UserHistory.where(action: UserHistory.actions[:checked_for_custom_avatar]).delete_all + end + + def down + end +end diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index 8d2865ac2..e61d0729d 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -26,9 +26,11 @@ class ComposerMessagesFinder if count < SiteSetting.educate_until_posts education_posts_text = I18n.t('education.until_posts', count: SiteSetting.educate_until_posts) - return {templateName: 'composer/education', - wait_for_typing: true, - body: PrettyText.cook(SiteText.text_for(education_key, education_posts_text: education_posts_text)) } + return { + templateName: 'composer/education', + wait_for_typing: true, + body: PrettyText.cook(SiteText.text_for(education_key, education_posts_text: education_posts_text)) + } end nil @@ -37,7 +39,11 @@ class ComposerMessagesFinder # New users have a limited number of replies in a topic def check_new_user_many_replies return unless replying? && @user.posted_too_much_in_topic?(@details[:topic_id]) - {templateName: 'composer/education', body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic)) } + + { + templateName: 'composer/education', + body: PrettyText.cook(I18n.t('education.too_many_replies', newuser_max_replies_per_topic: SiteSetting.newuser_max_replies_per_topic)) + } end # Should a user be contacted to update their avatar? @@ -49,14 +55,14 @@ class ComposerMessagesFinder # We don't notify users who have avatars or who have been notified already. return if @user.uploaded_avatar_id || UserHistory.exists_for_user?(@user, :notified_about_avatar) - # Finally, we don't check users whose avatars haven't been examined - return unless UserHistory.exists_for_user?(@user, :checked_for_custom_avatar) - # If we got this far, log that we've nagged them about the avatar UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: @user.id ) # Return the message - {templateName: 'composer/education', body: PrettyText.cook(I18n.t('education.avatar', profile_path: "/users/#{@user.username_lower}")) } + { + templateName: 'composer/education', + body: PrettyText.cook(I18n.t('education.avatar', profile_path: "/users/#{@user.username_lower}")) + } end # Is a user replying too much in succession? @@ -87,10 +93,12 @@ class ComposerMessagesFinder target_user_id: @user.id, topic_id: @details[:topic_id] ) - {templateName: 'composer/education', - wait_for_typing: true, - extraClass: 'urgent', - body: PrettyText.cook(I18n.t('education.sequential_replies')) } + { + templateName: 'composer/education', + wait_for_typing: true, + extraClass: 'urgent', + body: PrettyText.cook(I18n.t('education.sequential_replies')) + } end def check_dominating_topic @@ -102,6 +110,7 @@ class ComposerMessagesFinder !UserHistory.exists_for_user?(@user, :notified_about_dominating_topic, topic_id: @details[:topic_id]) topic = Topic.find_by(id: @details[:topic_id]) + return if topic.blank? || topic.user_id == @user.id || topic.posts_count < SiteSetting.summary_posts_required || @@ -117,11 +126,12 @@ class ComposerMessagesFinder target_user_id: @user.id, topic_id: @details[:topic_id]) - - {templateName: 'composer/education', - wait_for_typing: true, - extraClass: 'urgent', - body: PrettyText.cook(I18n.t('education.dominating_topic', percent: (ratio * 100).round)) } + { + templateName: 'composer/education', + wait_for_typing: true, + extraClass: 'urgent', + body: PrettyText.cook(I18n.t('education.dominating_topic', percent: (ratio * 100).round)) + } end def check_reviving_old_topic @@ -136,20 +146,22 @@ class ComposerMessagesFinder topic.last_posted_at.nil? || topic.last_posted_at > SiteSetting.warn_reviving_old_topic_age.days.ago - {templateName: 'composer/education', - wait_for_typing: false, - extraClass: 'urgent', - body: PrettyText.cook(I18n.t('education.reviving_old_topic', days: (Time.zone.now - topic.last_posted_at).round / 1.day)) } + { + templateName: 'composer/education', + wait_for_typing: false, + extraClass: 'urgent', + body: PrettyText.cook(I18n.t('education.reviving_old_topic', days: (Time.zone.now - topic.last_posted_at).round / 1.day)) + } end private def creating_topic? - return @details[:composerAction] == "createTopic" + @details[:composerAction] == "createTopic" end def replying? - return @details[:composerAction] == "reply" + @details[:composerAction] == "reply" end end diff --git a/spec/components/composer_messages_finder_spec.rb b/spec/components/composer_messages_finder_spec.rb index f119bf4da..fdca2b683 100644 --- a/spec/components/composer_messages_finder_spec.rb +++ b/spec/components/composer_messages_finder_spec.rb @@ -83,44 +83,31 @@ describe ComposerMessagesFinder do let(:finder) { ComposerMessagesFinder.new(user, composerAction: 'createTopic') } let(:user) { Fabricate(:user) } - context "a user who we haven't checked for an avatar yet" do - it "returns no avatar message" do - finder.check_avatar_notification.should be_blank + context "success" do + let!(:message) { finder.check_avatar_notification } + + it "returns an avatar upgrade message" do + message.should be_present + end + + it "creates a notified_about_avatar log" do + UserHistory.exists_for_user?(user, :notified_about_avatar).should == true end end - context "a user who has been checked for a custom avatar" do - before do - UserHistory.create!(action: UserHistory.actions[:checked_for_custom_avatar], target_user_id: user.id ) - end + it "doesn't return notifications for new users" do + user.trust_level = TrustLevel[0] + finder.check_avatar_notification.should be_blank + end - context "success" do - let!(:message) { finder.check_avatar_notification } - - it "returns an avatar upgrade message" do - message.should be_present - end - - it "creates a notified_about_avatar log" do - UserHistory.exists_for_user?(user, :notified_about_avatar).should == true - end - end - - it "doesn't return notifications for new users" do - user.trust_level = TrustLevel[0] - finder.check_avatar_notification.should be_blank - end - - it "doesn't return notifications for users who have custom avatars" do - user.uploaded_avatar_id = 1 - finder.check_avatar_notification.should be_blank - end - - it "doesn't notify users who have been notified already" do - UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: user.id ) - finder.check_avatar_notification.should be_blank - end + it "doesn't return notifications for users who have custom avatars" do + user.uploaded_avatar_id = 1 + finder.check_avatar_notification.should be_blank + end + it "doesn't notify users who have been notified already" do + UserHistory.create!(action: UserHistory.actions[:notified_about_avatar], target_user_id: user.id ) + finder.check_avatar_notification.should be_blank end end From 853e2ee5356017d06eb860d80c6f2d90b5411f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9gis=20Hanol?= Date: Fri, 2 Jan 2015 13:06:57 +0100 Subject: [PATCH 05/17] FIX: rss link in category pages --- app/views/layouts/_head.html.erb | 17 ++++++++--------- app/views/layouts/application.html.erb | 4 ++-- app/views/list/list.erb | 4 ++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/app/views/layouts/_head.html.erb b/app/views/layouts/_head.html.erb index 334aff879..367f11086 100644 --- a/app/views/layouts/_head.html.erb +++ b/app/views/layouts/_head.html.erb @@ -1,14 +1,13 @@ - - - + + - - + + - <%= canonical_link_tag %> +<%= canonical_link_tag %> - <%= render partial: "common/special_font_face" %> - <%= render partial: "common/discourse_stylesheet" %> +<%= render partial: "common/special_font_face" %> +<%= render partial: "common/discourse_stylesheet" %> - <%= discourse_csrf_tags %> +<%= discourse_csrf_tags %> diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 111e92e81..cc71444dd 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -7,11 +7,11 @@ <%= render partial: "layouts/head" %> <%- if SiteSetting.enable_escaped_fragments? %> - + <%- end %> <%- if shared_session_key %> - + <%- end %> <%= script "preload_store" %> diff --git a/app/views/list/list.erb b/app/views/list/list.erb index 4091f0b01..669b8f9eb 100644 --- a/app/views/list/list.erb +++ b/app/views/list/list.erb @@ -20,13 +20,13 @@ <% if @rss %> <% content_for :head do %> - <%= auto_discovery_link_tag(:rss, {action: "#{@rss}_feed"}, title: I18n.t("rss_description.#{@rss}")) %> + <%= auto_discovery_link_tag(:rss, { action: "#{@rss}_feed" }, title: I18n.t("rss_description.#{@rss}")) %> <% end %> <% end %> <% if @category %> <% content_for :head do %> - <%= auto_discovery_link_tag(@category, {action: :category_feed, format: :rss}, title: t('rss_topics_in_category', category: @category.name), type: 'application/rss+xml') %> + <%= auto_discovery_link_tag(:rss, { action: :category_feed }, title: t('rss_topics_in_category', category: @category.name)) %> <% end %> <% end %> From 701d896912c479dd6c9c5e3235c95377ba6f1a36 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Jan 2015 10:46:04 -0500 Subject: [PATCH 06/17] FIX: Link domains were not showing up in the topic map --- app/assets/javascripts/discourse/helpers/link-domain.js.es6 | 5 +++-- .../javascripts/discourse/templates/components/topic-map.hbs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/discourse/helpers/link-domain.js.es6 b/app/assets/javascripts/discourse/helpers/link-domain.js.es6 index b16255d94..ba59b7f50 100644 --- a/app/assets/javascripts/discourse/helpers/link-domain.js.es6 +++ b/app/assets/javascripts/discourse/helpers/link-domain.js.es6 @@ -1,5 +1,6 @@ -Handlebars.registerHelper('link-domain', function(property, options) { - var link = Em.get(this, property, options); +import registerUnbound from 'discourse/helpers/register-unbound'; + +registerUnbound('link-domain', function(link) { if (link) { var internal = Em.get(link, 'internal'), hasTitle = (!Em.isEmpty(Em.get(link, 'title'))); diff --git a/app/assets/javascripts/discourse/templates/components/topic-map.hbs b/app/assets/javascripts/discourse/templates/components/topic-map.hbs index 733267eb0..18eeb0ffe 100644 --- a/app/assets/javascripts/discourse/templates/components/topic-map.hbs +++ b/app/assets/javascripts/discourse/templates/components/topic-map.hbs @@ -82,7 +82,7 @@ {{#if showAllLinksControls}} {{/if}} From 9523b26af227bd6bdcb5ec552aca0aa8e78f4035 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Jan 2015 11:57:48 -0500 Subject: [PATCH 07/17] Move emoji autocomplete into its own file, remove ceremony around decorating the `ComposerView` for it since it's part of core now. --- .../discourse/lib/emoji/emoji-autocomplete.js | 46 ------------------- .../emoji-selector-autocomplete.raw.hbs | 9 ++++ .../discourse/views/composer.js.es6 | 34 ++++++++++++++ 3 files changed, 43 insertions(+), 46 deletions(-) delete mode 100644 app/assets/javascripts/discourse/lib/emoji/emoji-autocomplete.js create mode 100644 app/assets/javascripts/discourse/templates/emoji-selector-autocomplete.raw.hbs diff --git a/app/assets/javascripts/discourse/lib/emoji/emoji-autocomplete.js b/app/assets/javascripts/discourse/lib/emoji/emoji-autocomplete.js deleted file mode 100644 index 48abf219f..000000000 --- a/app/assets/javascripts/discourse/lib/emoji/emoji-autocomplete.js +++ /dev/null @@ -1,46 +0,0 @@ -// TODO: Make this a proper ES6 import -var ComposerView = require('discourse/views/composer').default; - -ComposerView.on("initWmdEditor", function(){ - if (!Discourse.SiteSettings.enable_emoji) { return; } - - var template = Handlebars.compile( - "
" + - "
    " + - "{{#each options}}" + - "
  • " + - " {{code}}" + - "
  • " + - "{{/each}}" + - "
" + - "
" - ); - - $('#wmd-input').autocomplete({ - template: template, - key: ":", - transformComplete: function(v){ return v.code + ":"; }, - dataSource: function(term){ - return new Ember.RSVP.Promise(function(resolve) { - var full = ":" + term; - term = term.toLowerCase(); - - if (term === "") { - return resolve(["smile", "smiley", "wink", "sunny", "blush"]); - } - - if (Discourse.Emoji.translations[full]) { - return resolve([Discourse.Emoji.translations[full]]); - } - - var options = Discourse.Emoji.search(term, {maxResults: 5}); - - return resolve(options); - }).then(function(list) { - return list.map(function(i) { - return {code: i, src: Discourse.Emoji.urlFor(i)}; - }); - }); - } - }); -}); diff --git a/app/assets/javascripts/discourse/templates/emoji-selector-autocomplete.raw.hbs b/app/assets/javascripts/discourse/templates/emoji-selector-autocomplete.raw.hbs new file mode 100644 index 000000000..ed2e39c7a --- /dev/null +++ b/app/assets/javascripts/discourse/templates/emoji-selector-autocomplete.raw.hbs @@ -0,0 +1,9 @@ +
+
    + {{#each options}} +
  • + {{code}} +
  • + {{/each}} +
+
diff --git a/app/assets/javascripts/discourse/views/composer.js.es6 b/app/assets/javascripts/discourse/views/composer.js.es6 index f69462c71..325e23f13 100644 --- a/app/assets/javascripts/discourse/views/composer.js.es6 +++ b/app/assets/javascripts/discourse/views/composer.js.es6 @@ -163,6 +163,39 @@ var ComposerView = Discourse.View.extend(Ember.Evented, { this.trigger('previewRefreshed', $wmdPreview); }, + _applyEmojiAutocomplete: function() { + if (!this.siteSettings.enable_emoji) { return; } + + var template = this.container.lookup('template:emoji-selector-autocomplete.raw'); + $('#wmd-input').autocomplete({ + template: template, + key: ":", + transformComplete: function(v){ return v.code + ":"; }, + dataSource: function(term){ + return new Ember.RSVP.Promise(function(resolve) { + var full = ":" + term; + term = term.toLowerCase(); + + if (term === "") { + return resolve(["smile", "smiley", "wink", "sunny", "blush"]); + } + + if (Discourse.Emoji.translations[full]) { + return resolve([Discourse.Emoji.translations[full]]); + } + + var options = Discourse.Emoji.search(term, {maxResults: 5}); + + return resolve(options); + }).then(function(list) { + return list.map(function(i) { + return {code: i, src: Discourse.Emoji.urlFor(i)}; + }); + }); + } + }); + }, + initEditor: function() { // not quite right, need a callback to pass in, meaning this gets called once, // but if you start replying to another topic it will get the avatars wrong @@ -172,6 +205,7 @@ var ComposerView = Discourse.View.extend(Ember.Evented, { $LAB.script(assetPath('defer/html-sanitizer-bundle')); ComposerView.trigger("initWmdEditor"); + this._applyEmojiAutocomplete(); var template = this.container.lookup('template:user-selector-autocomplete.raw'); $wmdInput.data('init', true); From 7182767349cf12b8fe8b53ebd230e8939a3bd94e Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Jan 2015 13:47:07 -0500 Subject: [PATCH 08/17] Refactor topic title/category saving to support easier changing of attributes via plugins. --- .../discourse/controllers/topic.js.es6 | 62 ++++++------------- .../javascripts/discourse/models/composer.js | 7 +-- .../javascripts/discourse/models/model.js | 10 +-- .../javascripts/discourse/models/topic.js | 28 ++++----- .../javascripts/discourse/templates/topic.hbs | 12 ++-- .../discourse/views/category-chooser.js.es6 | 1 + .../discourse/views/combo-box.js.es6 | 11 +++- .../javascripts/controllers/topic-test.js.es6 | 4 +- 8 files changed, 49 insertions(+), 86 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index 92e06d8f8..f94571ce9 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -1,7 +1,8 @@ import ObjectController from 'discourse/controllers/object'; +import BufferedContent from 'discourse/mixins/buffered-content'; import { spinnerHTML } from 'discourse/helpers/loading-spinner'; -export default ObjectController.extend(Discourse.SelectedPostsCount, { +export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedContent, { multiSelect: false, needs: ['header', 'modal', 'composer', 'quote-button', 'search', 'topic-progress', 'application'], allPostsSelected: false, @@ -235,11 +236,6 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, { this.set('allPostsSelected', false); }, - /** - Toggle a participant for filtering - - @method toggleParticipant - **/ toggleParticipant: function(user) { this.get('postStream').toggleParticipant(Em.get(user, 'username')); }, @@ -247,17 +243,13 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, { editTopic: function() { if (!this.get('details.can_edit')) return false; - this.setProperties({ - editingTopic: true, - newTitle: this.get('title'), - newCategoryId: this.get('category_id') - }); + this.set('editingTopic', true); return false; }, - // close editing mode cancelEditingTopic: function() { this.set('editingTopic', false); + this.rollbackBuffer(); }, toggleMultiSelect: function() { @@ -265,39 +257,25 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, { }, finishedEditingTopic: function() { - if (this.get('editingTopic')) { + if (!this.get('editingTopic')) { return; } - var topic = this.get('model'); + // save the modifications + var self = this, + props = this.get('buffered.buffer'); - // Topic title hasn't been sanitized yet, so the template shouldn't trust it. - this.set('topicSaving', true); - - // manually update the titles & category - var backup = topic.setPropertiesBackup({ - title: this.get('newTitle'), - category_id: parseInt(this.get('newCategoryId'), 10), - fancy_title: this.get('newTitle') - }); - - // save the modifications - var self = this; - topic.save().then(function(result){ - // update the title if it has been changed (cleaned up) server-side - topic.setProperties(Em.getProperties(result.basic_topic, 'title', 'fancy_title')); - self.set('topicSaving', false); - }, function(error) { - self.setProperties({ editingTopic: true, topicSaving: false }); - topic.setProperties(backup); - if (error && error.responseText) { - bootbox.alert($.parseJSON(error.responseText).errors[0]); - } else { - bootbox.alert(I18n.t('generic_error')); - } - }); - - // close editing mode + Discourse.Topic.update(this.get('model'), props).then(function() { self.set('editingTopic', false); - } + }).catch(function(error) { + if (error && error.responseText) { + bootbox.alert($.parseJSON(error.responseText).errors[0]); + } else { + bootbox.alert(I18n.t('generic_error')); + } + }).finally(function() { + // Note we even roll back on success here because `update` saves + // the properties to the topic. + self.rollbackBuffer(); + }); }, toggledSelectedPost: function(post) { diff --git a/app/assets/javascripts/discourse/models/composer.js b/app/assets/javascripts/discourse/models/composer.js index 2d9523cac..6b166a8ab 100644 --- a/app/assets/javascripts/discourse/models/composer.js +++ b/app/assets/javascripts/discourse/models/composer.js @@ -463,13 +463,10 @@ Discourse.Composer = Discourse.Model.extend({ // Update the title if we've changed it if (this.get('title') && post.get('post_number') === 1) { - var topic = this.get('topic'); - topic.setProperties({ + Discourse.Topic.update(this.get('topic'), { title: this.get('title'), - fancy_title: Handlebars.Utils.escapeExpression(this.get('title')), - category_id: parseInt(this.get('categoryId'), 10) + category_id: this.get('categoryId') }); - topic.save(); } post.setProperties({ diff --git a/app/assets/javascripts/discourse/models/model.js b/app/assets/javascripts/discourse/models/model.js index 084c7c72a..0c2619c7c 100644 --- a/app/assets/javascripts/discourse/models/model.js +++ b/app/assets/javascripts/discourse/models/model.js @@ -1,12 +1,4 @@ -Discourse.Model = Ember.Object.extend(Discourse.Presence, { - // Like `setProperties` but returns the original values in case - // we want to roll back - setPropertiesBackup: function(obj) { - var backup = this.getProperties(Ember.keys(obj)); - this.setProperties(obj); - return backup; - } -}); +Discourse.Model = Ember.Object.extend(Discourse.Presence); Discourse.Model.reopenClass({ extractByKey: function(collection, klass) { diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js index 817376fbf..59cfbf9df 100644 --- a/app/assets/javascripts/discourse/models/topic.js +++ b/app/assets/javascripts/discourse/models/topic.js @@ -202,23 +202,6 @@ Discourse.Topic = Discourse.Model.extend({ }); }, - // Save any changes we've made to the model - save: function() { - // Don't save unless we can - if (!this.get('details.can_edit')) return; - - var data = { title: this.get('title') }; - - if(this.get('category')){ - data.category_id = this.get('category.id'); - } - - return Discourse.ajax(this.get('url'), { - type: 'PUT', - data: data - }); - }, - /** Invite a user to this topic @@ -373,6 +356,17 @@ Discourse.Topic.reopenClass({ } }, + update: function(topic, props) { + return Discourse.ajax(topic.get('url'), { type: 'PUT', data: props }).then(function(result) { + + // The title can be cleaned up server side + props.title = result.basic_topic.title; + props.fancy_title = result.basic_topic.fancy_title; + + topic.setProperties(props); + }); + }, + create: function() { var result = this._super.apply(this, arguments); this.createActionSummary(result); diff --git a/app/assets/javascripts/discourse/templates/topic.hbs b/app/assets/javascripts/discourse/templates/topic.hbs index f724e92ff..3deaa666d 100644 --- a/app/assets/javascripts/discourse/templates/topic.hbs +++ b/app/assets/javascripts/discourse/templates/topic.hbs @@ -16,11 +16,11 @@ {{#if editingTopic}} {{#if isPrivateMessage}} {{fa-icon envelope}} - {{autofocus-text-field id='edit-title' value=newTitle maxLength=maxTitleLength}} + {{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}} {{else}} - {{autofocus-text-field id='edit-title' value=newTitle maxLength=maxTitleLength}} + {{autofocus-text-field id='edit-title' value=buffered.title maxLength=maxTitleLength}}
- {{category-chooser valueAttribute="id" value=newCategoryId source=category_id}} + {{category-chooser valueAttribute="id" value=buffered.category_id source=buffered.category_id}} {{/if}} @@ -34,11 +34,7 @@ {{#if details.loaded}} {{topic-status topic=model}} - {{#if topicSaving}} - {{fancy_title}} - {{else}} - {{{fancy_title}}} - {{/if}} + {{{fancy_title}}} {{/if}} diff --git a/app/assets/javascripts/discourse/views/category-chooser.js.es6 b/app/assets/javascripts/discourse/views/category-chooser.js.es6 index b0bda8b33..46a82d012 100644 --- a/app/assets/javascripts/discourse/views/category-chooser.js.es6 +++ b/app/assets/javascripts/discourse/views/category-chooser.js.es6 @@ -7,6 +7,7 @@ export default ComboboxView.extend({ overrideWidths: true, dataAttributes: ['id', 'description_text'], valueBinding: Ember.Binding.oneWay('source'), + castInteger: true, content: function() { var scopedCategoryId = this.get('scopedCategoryId'); diff --git a/app/assets/javascripts/discourse/views/combo-box.js.es6 b/app/assets/javascripts/discourse/views/combo-box.js.es6 index 24d4b2d28..186683673 100644 --- a/app/assets/javascripts/discourse/views/combo-box.js.es6 +++ b/app/assets/javascripts/discourse/views/combo-box.js.es6 @@ -63,7 +63,7 @@ export default Discourse.View.extend({ this.rerender(); }.observes('content.@each'), - didInsertElement: function() { + _initializeCombo: function() { var $elem = this.$(), self = this; @@ -75,10 +75,15 @@ export default Discourse.View.extend({ $elem.select2({formatResult: this.template, minimumResultsForSearch: 5, width: 'resolve'}); + var castInteger = this.get('castInteger'); $elem.on("change", function (e) { - self.set('value', $(e.target).val()); + var val = $(e.target).val(); + if (val.length && castInteger) { + val = parseInt(val, 10); + } + self.set('value', val); }); - }, + }.on('didInsertElement'), willClearRender: function() { var elementId = "s2id_" + this.$().attr('id'); diff --git a/test/javascripts/controllers/topic-test.js.es6 b/test/javascripts/controllers/topic-test.js.es6 index cf853c0d3..5b9bb5135 100644 --- a/test/javascripts/controllers/topic-test.js.es6 +++ b/test/javascripts/controllers/topic-test.js.es6 @@ -28,8 +28,8 @@ test("editingMode", function() { topicController.set('model.details.can_edit', true); topicController.send('editTopic'); ok(topicController.get('editingTopic'), "calling editTopic enables editing if the user can edit"); - equal(topicController.get('newTitle'), topic.get('title')); - equal(topicController.get('newCategoryId'), topic.get('category_id')); + equal(topicController.get('buffered.title'), topic.get('title')); + equal(topicController.get('buffered.category_id'), topic.get('category_id')); topicController.send('cancelEditingTopic'); ok(!topicController.get('editingTopic'), "cancelling edit mode reverts the property value"); From 4c166942adde8734d03f39796852c97fd7e5bbee Mon Sep 17 00:00:00 2001 From: Neil Lalonde Date: Fri, 2 Jan 2015 15:48:34 -0500 Subject: [PATCH 09/17] FEATURE: Invite admin api has an optional param send_email which can prevent sending an email to the invited user. The api will return the password reset url so that the caller can send an email with it instead. --- app/controllers/admin/users_controller.rb | 7 +++++-- spec/controllers/admin/users_controller_spec.rb | 9 +++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 8197b89a4..e62999fe0 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -320,12 +320,15 @@ class Admin::UsersController < Admin::AdminController user.email_tokens.update_all confirmed: true email_token = user.email_tokens.create(email: user.email) - Jobs.enqueue(:user_email, + + unless params[:send_email] == '0' || params[:send_email] == 'false' + Jobs.enqueue( :user_email, type: :account_created, user_id: user.id, email_token: email_token.token) + end - render json: success_json + render json: success_json.merge!(password_url: "#{Discourse.base_url}/users/password-reset/#{email_token.token}") end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 31271da27..5ad68e9bb 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -460,6 +460,7 @@ describe Admin::UsersController do context ".invite_admin" do it 'should invite admin' do + Jobs.expects(:enqueue).with(:user_email, anything).returns(true) xhr :post, :invite_admin, name: 'Bill', username: 'bill22', email: 'bill@bill.com' response.should be_success @@ -468,6 +469,14 @@ describe Admin::UsersController do u.username.should == "bill22" u.admin.should == true end + + it "doesn't send the email with send_email falsy" do + Jobs.expects(:enqueue).with(:user_email, anything).never + xhr :post, :invite_admin, name: 'Bill', username: 'bill22', email: 'bill@bill.com', send_email: '0' + response.should be_success + json = ::JSON.parse(response.body) + json["password_url"].should be_present + end end end From 6f72f265cbb055136955d8aeeb4aadf1543a734c Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Jan 2015 15:56:44 -0500 Subject: [PATCH 10/17] A trigger when a topic is updated, adds a couple of custom field tests --- app/assets/javascripts/discourse/controllers/topic.js.es6 | 7 +++---- app/controllers/topics_controller.rb | 3 +++ spec/components/concern/has_custom_fields_spec.rb | 5 ++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/topic.js.es6 b/app/assets/javascripts/discourse/controllers/topic.js.es6 index f94571ce9..db94ea85b 100644 --- a/app/assets/javascripts/discourse/controllers/topic.js.es6 +++ b/app/assets/javascripts/discourse/controllers/topic.js.es6 @@ -264,6 +264,9 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon props = this.get('buffered.buffer'); Discourse.Topic.update(this.get('model'), props).then(function() { + // Note we roll back on success here because `update` saves + // the properties to the topic. + self.rollbackBuffer(); self.set('editingTopic', false); }).catch(function(error) { if (error && error.responseText) { @@ -271,10 +274,6 @@ export default ObjectController.extend(Discourse.SelectedPostsCount, BufferedCon } else { bootbox.alert(I18n.t('generic_error')); } - }).finally(function() { - // Note we even roll back on success here because `update` saves - // the properties to the topic. - self.rollbackBuffer(); }); }, diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb index 63b45f734..73bc006ae 100644 --- a/app/controllers/topics_controller.rb +++ b/app/controllers/topics_controller.rb @@ -2,6 +2,7 @@ require_dependency 'topic_view' require_dependency 'promotion' require_dependency 'url_helper' require_dependency 'topics_bulk_action' +require_dependency 'discourse_event' class TopicsController < ApplicationController include UrlHelper @@ -134,6 +135,8 @@ class TopicsController < ApplicationController success = PostRevisor.new(first_post, topic).revise!(current_user, changes, validate_post: false) end + DiscourseEvent.trigger(:topic_saved, topic, params) + # this is used to return the title to the client as it may have been changed by "TextCleaner" success ? render_serialized(topic, BasicTopicSerializer) : render_json_error(topic) end diff --git a/spec/components/concern/has_custom_fields_spec.rb b/spec/components/concern/has_custom_fields_spec.rb index c41f3ebc6..0b7cfb90b 100644 --- a/spec/components/concern/has_custom_fields_spec.rb +++ b/spec/components/concern/has_custom_fields_spec.rb @@ -111,10 +111,13 @@ describe HasCustomFields do db_item = CustomFieldsTestItem.find(test_item.id) db_item.custom_fields.should == {"a" => ["b", "c", "d"]} - db_item.custom_fields["a"] = ["c", "d"] + db_item.custom_fields.update('a' => ['c', 'd']) db_item.save db_item.custom_fields.should == {"a" => ["c", "d"]} + db_item.custom_fields.delete('a') + db_item.custom_fields.should == {} + end it "casts integers in arrays properly without error" do From 5beac447316d8768311669bab5628a8f21653715 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Jan 2015 16:12:06 -0500 Subject: [PATCH 11/17] If transmitting an empty array, mark it as such --- app/assets/javascripts/discourse/models/topic.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/assets/javascripts/discourse/models/topic.js b/app/assets/javascripts/discourse/models/topic.js index 59cfbf9df..6df0bbff5 100644 --- a/app/assets/javascripts/discourse/models/topic.js +++ b/app/assets/javascripts/discourse/models/topic.js @@ -357,6 +357,18 @@ Discourse.Topic.reopenClass({ }, update: function(topic, props) { + props = JSON.parse(JSON.stringify(props)) || {}; + + // Annoyingly, empty arrays are not sent across the wire. This + // allows us to make a distinction between arrays that were not + // sent and arrays that we specifically want to be empty. + Object.keys(props).forEach(function(k) { + var v = props[k]; + if (v instanceof Array && v.length === 0) { + props[k + '_empty_array'] = true; + } + }); + return Discourse.ajax(topic.get('url'), { type: 'PUT', data: props }).then(function(result) { // The title can be cleaned up server side From 030bab92a2215fb73e6ddedc7bf0e767a04d2a3d Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Jan 2015 16:29:08 -0500 Subject: [PATCH 12/17] Small optimization: remove a template with an `{{if}}` --- .../conditional-loading-spinner.js.es6 | 16 +++++++++++++--- .../components/conditional-loading-spinner.hbs | 5 ----- 2 files changed, 13 insertions(+), 8 deletions(-) delete mode 100644 app/assets/javascripts/discourse/templates/components/conditional-loading-spinner.hbs diff --git a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 index ca6d0386a..d76807628 100644 --- a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 +++ b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 @@ -1,8 +1,18 @@ -export default Ember.Component.extend({ +import StringBuffer from 'discourse/mixins/string-buffer'; + +export default Ember.Component.extend(StringBuffer, { classNameBindings: ['containerClass'], - layoutName: 'components/conditional-loading-spinner', + rerenderTriggers: ['condition'], containerClass: function() { return (this.get('size') === 'small') ? 'inline-spinner' : undefined; - }.property('size') + }.property('size'), + + renderString: function(buffer) { + if (this.get('condition')) { + buffer.push('
'); + } else { + return this._super(); + } + } }); diff --git a/app/assets/javascripts/discourse/templates/components/conditional-loading-spinner.hbs b/app/assets/javascripts/discourse/templates/components/conditional-loading-spinner.hbs deleted file mode 100644 index d1d27ae58..000000000 --- a/app/assets/javascripts/discourse/templates/components/conditional-loading-spinner.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#if condition}} -
-{{else}} - {{yield}} -{{/if}} From 1e441522c077ea2f8d2509524230024cc6fad5b0 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Fri, 2 Jan 2015 16:35:56 -0500 Subject: [PATCH 13/17] Don't use a string buffer to refactor loading outlet --- .../components/conditional-loading-spinner.js.es6 | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 index d76807628..19103360d 100644 --- a/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 +++ b/app/assets/javascripts/discourse/components/conditional-loading-spinner.js.es6 @@ -1,18 +1,19 @@ -import StringBuffer from 'discourse/mixins/string-buffer'; - -export default Ember.Component.extend(StringBuffer, { +export default Ember.Component.extend({ classNameBindings: ['containerClass'], - rerenderTriggers: ['condition'], containerClass: function() { return (this.get('size') === 'small') ? 'inline-spinner' : undefined; }.property('size'), - renderString: function(buffer) { + render: function(buffer) { if (this.get('condition')) { buffer.push('
'); } else { return this._super(); } - } + }, + + _conditionChanged: function() { + this.rerender(); + }.observes('condition') }); From 0affda48182fcccb9ebae2b59bb6c07bb986c6e4 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sun, 4 Jan 2015 00:16:10 -0800 Subject: [PATCH 14/17] improve new site nag copy --- config/locales/server.en.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2c028863e..1d7520407 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -598,14 +598,14 @@ en: s3_backup_config_warning: 'The server is configured to upload backups to s3, but at least one the following setting is not set: s3_access_key_id, s3_secret_access_key or s3_backup_bucket. Go to the Site Settings and update the settings. See "How to set up image uploads to S3?" to learn more.' image_magick_warning: 'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or download the latest release.' failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/discourse.conf file and ensure that the mail server settings are correct. See the failed jobs in Sidekiq.' - default_logo_warning: "You haven't customized the logo images for your site. Update logo_url, logo_small_url, and favicon_url in the Site Settings." - contact_email_missing: "You haven't provided a contact email for your site. Please update contact_email in the Site Settings." - contact_email_invalid: "The site contact email is invalid. Please update contact_email in the Site Settings." - title_nag: "The title Site Setting is still set to the default value. Please update it with your site's title in the Site Settings." - site_description_missing: "The site_description setting is blank. Write a brief description of this forum in the Site Settings." + default_logo_warning: "Set the graphic logos for your site. Update logo_url, logo_small_url, and favicon_url in Site Settings." + contact_email_missing: "Enter a site contact email address so new users or users who can't log in can reach you. Update it in Site Settings." + contact_email_invalid: "The site contact email is invalid. Update it in Site Settings." + title_nag: "Enter the name of your site. Update title in Site Settings." + site_description_missing: "Enter a one sentence description of your site that will appear in search results. Update site_description in Site Settings." consumer_email_warning: "Your site is configured to use Gmail (or another consumer email service) to send email. Gmail limits how many emails you can send. Consider using an email service provider like mandrill.com to ensure email deliverability." access_password_removal: "Your site was using the access_password setting, which has been removed. The login_required and must_approve_users settings have been enabled, which should be used instead. You can change them in the Site Settings. Be sure to approve users in the Pending Users list. (This message will go away after 2 days.)" - site_contact_username_warning: "The site_contact_username setting is blank. Please update it in the Site Settings. Set it to the username of an admin user who should be the sender of system messages." + site_contact_username_warning: "Enter the name of a friendly staff user account to send important automated private messages from, such as the new user welcome, flag warnings, etc. Update site_contact_username in Site Settings." notification_email_warning: "The notification_email setting is blank. Please update it in the Site Settings." content_types: From 1c709ea5a920b2b4f3c78ed69d97ff578ddde21e Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sun, 4 Jan 2015 00:19:56 -0800 Subject: [PATCH 15/17] clarify site nag on contact email --- 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 1d7520407..1221a0a63 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -599,7 +599,7 @@ en: image_magick_warning: 'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or download the latest release.' failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/discourse.conf file and ensure that the mail server settings are correct. See the failed jobs in Sidekiq.' default_logo_warning: "Set the graphic logos for your site. Update logo_url, logo_small_url, and favicon_url in Site Settings." - contact_email_missing: "Enter a site contact email address so new users or users who can't log in can reach you. Update it in Site Settings." + contact_email_missing: "Enter a site contact email address so new users or users who can't log in, as well as other webmasters and system administrators, can reach you. Update it in Site Settings." contact_email_invalid: "The site contact email is invalid. Update it in Site Settings." title_nag: "Enter the name of your site. Update title in Site Settings." site_description_missing: "Enter a one sentence description of your site that will appear in search results. Update site_description in Site Settings." From 46dcc9f25749d45847216e9148ec0664fb40b411 Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sun, 4 Jan 2015 00:28:25 -0800 Subject: [PATCH 16/17] improve essential site settings descriptions --- config/locales/server.en.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 1221a0a63..7e004f16d 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -657,9 +657,9 @@ en: allow_duplicate_topic_titles: "Allow topics with identical, duplicate titles." unique_posts_mins: "How many minutes before a user can make a post with the same content again" educate_until_posts: "When the user starts typing their first (n) new posts, show the pop-up new user education panel in the composer." - title: "Brief title of this site, used in the title tag." - site_description: "Describe this site in one sentence, used in the meta description tag." - contact_email: "Email address of key contact for site. Important notices from discourse.org regarding critical updates may be sent to this address." + title: "The name of this site, as used in the title tag." + site_description: "Describe this site in one sentence, as used in the meta description tag." + contact_email: "Email address of key contact responsible for this site. Used for critical notifications only, as well as on the /about contact form for urgent matters." queue_jobs: "DEVELOPER ONLY! WARNING! By default, queue jobs in sidekiq. If disabled, your site will be broken." crawl_images: "Retrieve images from remote URLs to insert the correct width and height dimensions." download_remote_images_to_local: "Convert remote images to local images by downloading them; this prevents broken images." @@ -737,7 +737,7 @@ en: post_menu_hidden_items: "The menu items to hide by default in the post menu unless an expansion ellipsis is clicked on." share_links: "Determine which items appear on the share dialog, and in what order." track_external_right_clicks: "Track external links that are right clicked (eg: open in new tab) disabled by default because it rewrites URLs" - site_contact_username: "All automated private messages will be from this user; if left blank the default System account will be used." + site_contact_username: "A valid staff username to send all automated private messages from. If left blank the default System account will be used." send_welcome_message: "Send all new users a welcome private message with a quick start guide." suppress_reply_directly_below: "Don't show the expandable reply count on a post when there is only a single reply directly below this post." suppress_reply_directly_above: "Don't show the expandable in-reply-to on a post when there is only a single reply directly above this post." From d2d8f1393278c666d7e8d04535b7754cb70a064c Mon Sep 17 00:00:00 2001 From: Jeff Atwood Date: Sun, 4 Jan 2015 00:29:51 -0800 Subject: [PATCH 17/17] improve new site nag copy --- 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 7e004f16d..712239e16 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -599,7 +599,7 @@ en: image_magick_warning: 'The server is configured to create thumbnails of large images, but ImageMagick is not installed. Install ImageMagick using your favorite package manager or download the latest release.' failing_emails_warning: 'There are %{num_failed_jobs} email jobs that failed. Check your config/discourse.conf file and ensure that the mail server settings are correct. See the failed jobs in Sidekiq.' default_logo_warning: "Set the graphic logos for your site. Update logo_url, logo_small_url, and favicon_url in Site Settings." - contact_email_missing: "Enter a site contact email address so new users or users who can't log in, as well as other webmasters and system administrators, can reach you. Update it in Site Settings." + contact_email_missing: "Enter a site contact email address so new users or users who can't log in, as well as other webmasters and system administrators, can reach you for urgent matters. Update it in Site Settings." contact_email_invalid: "The site contact email is invalid. Update it in Site Settings." title_nag: "Enter the name of your site. Update title in Site Settings." site_description_missing: "Enter a one sentence description of your site that will appear in search results. Update site_description in Site Settings."