diff --git a/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 b/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 index fd4e09bf49a..5a7aaf1a747 100644 --- a/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 +++ b/app/assets/javascripts/discourse/components/create-topics-notice.js.es6 @@ -36,7 +36,10 @@ export default Ember.Component.extend({ @computed() shouldSee() { - return Discourse.User.currentProp('admin') && this.siteSettings.show_create_topics_notice; + const user = this.currentUser; + return user && user.get('admin') && + this.siteSettings.show_create_topics_notice && + !this.site.get('wizard_required'); }, @computed('enabled', 'shouldSee', 'publicTopicCount', 'publicPostCount') diff --git a/app/assets/javascripts/discourse/components/global-notice.js.es6 b/app/assets/javascripts/discourse/components/global-notice.js.es6 index 0e76a27e32a..50132cf5cab 100644 --- a/app/assets/javascripts/discourse/components/global-notice.js.es6 +++ b/app/assets/javascripts/discourse/components/global-notice.js.es6 @@ -17,6 +17,10 @@ export default Ember.Component.extend(StringBuffer, { notices.push([I18n.t("emails_are_disabled"), 'alert-emails-disabled']); } + if (this.site.get('wizard_required')) { + notices.push([I18n.t('wizard_required'), 'alert-wizard']); + } + if (this.currentUser && this.currentUser.get('staff') && this.siteSettings.bootstrap_mode_enabled) { if (this.siteSettings.bootstrap_mode_min_users > 0) { notices.push([I18n.t("bootstrap_mode_enabled", {min_users: this.siteSettings.bootstrap_mode_min_users}), 'alert-bootstrap-mode']); diff --git a/app/assets/javascripts/wizard/components/wizard-step.js.es6 b/app/assets/javascripts/wizard/components/wizard-step.js.es6 index 079b333cff7..39af7c43aa9 100644 --- a/app/assets/javascripts/wizard/components/wizard-step.js.es6 +++ b/app/assets/javascripts/wizard/components/wizard-step.js.es6 @@ -9,6 +9,9 @@ export default Ember.Component.extend({ this.autoFocus(); }, + @computed('step.index') + showQuitButton: index => index === 0, + @computed('step.displayIndex', 'wizard.totalSteps') showNextButton: (current, total) => current < total, @@ -49,6 +52,10 @@ export default Ember.Component.extend({ }, actions: { + quit() { + document.location = "/"; + }, + backStep() { if (this.get('saving')) { return; } this.sendAction('goBack'); diff --git a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs index 7fe656e48c4..52a9b5fbfda 100644 --- a/app/assets/javascripts/wizard/templates/components/wizard-step.hbs +++ b/app/assets/javascripts/wizard/templates/components/wizard-step.hbs @@ -21,6 +21,13 @@
+ {{#if showQuitButton}} + + {{/if}} + {{#if showBackButton}} diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 263377d8a97..c015c06ad8f 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -461,6 +461,10 @@ class ApplicationController < ActionController::Base raise Discourse::InvalidAccess.new unless current_user && current_user.staff? end + def ensure_wizard_enabled + raise Discourse::InvalidAccess.new unless SiteSetting.wizard_enabled? + end + def destination_url request.original_url unless request.original_url =~ /uploads/ end diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index c65ff16cf95..6f9df1d65c1 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -4,6 +4,7 @@ require_dependency 'wizard/step_updater' class StepsController < ApplicationController + before_filter :ensure_wizard_enabled before_filter :ensure_logged_in before_filter :ensure_staff diff --git a/app/controllers/wizard_controller.rb b/app/controllers/wizard_controller.rb index eb91d822585..951ad1d9ed8 100644 --- a/app/controllers/wizard_controller.rb +++ b/app/controllers/wizard_controller.rb @@ -2,7 +2,7 @@ require_dependency 'wizard' require_dependency 'wizard/builder' class WizardController < ApplicationController - + before_filter :ensure_wizard_enabled, only: [:index] before_filter :ensure_logged_in before_filter :ensure_staff diff --git a/app/models/user_history.rb b/app/models/user_history.rb index d7a99bdbc9c..635014b1d08 100644 --- a/app/models/user_history.rb +++ b/app/models/user_history.rb @@ -55,6 +55,7 @@ class UserHistory < ActiveRecord::Base rate_limited_like: 37, # not used anymore revoke_email: 38, deactivate_user: 39, + wizard_step: 40 ) end diff --git a/app/serializers/site_serializer.rb b/app/serializers/site_serializer.rb index cd931952351..c9e3b87cee1 100644 --- a/app/serializers/site_serializer.rb +++ b/app/serializers/site_serializer.rb @@ -1,4 +1,6 @@ require_dependency 'discourse_tagging' +require_dependency 'wizard' +require_dependency 'wizard/builder' class SiteSerializer < ApplicationSerializer @@ -20,7 +22,8 @@ class SiteSerializer < ApplicationSerializer :can_create_tag, :can_tag_topics, :tags_filter_regexp, - :top_tags + :top_tags, + :wizard_required has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :trust_levels, embed: :objects @@ -110,4 +113,12 @@ class SiteSerializer < ApplicationSerializer def top_tags Tag.top_tags end + + def wizard_required + true + end + + def include_wizard_required? + Wizard::Builder.new(scope.user).build.requires_completion? + end end diff --git a/app/services/staff_action_logger.rb b/app/services/staff_action_logger.rb index 3beaf9bcae1..57c213b4e2f 100644 --- a/app/services/staff_action_logger.rb +++ b/app/services/staff_action_logger.rb @@ -353,6 +353,15 @@ class StaffActionLogger })) end + def log_wizard_step(step, opts={}) + raise Discourse::InvalidParameters.new(:step) unless step + UserHistory.create(params(opts).merge({ + action: UserHistory.actions[:wizard_step], + acting_user_id: @admin.id, + context: step.id + })) + end + private def params(opts=nil) diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 719043c29d0..c60f0fc1013 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -165,6 +165,7 @@ en: topic_admin_menu: "topic admin actions" + wizard_required: "It's time to configure your forum! Start the Setup Wizard!" emails_are_disabled: "All outgoing email has been globally disabled by an administrator. No email notifications of any kind will be sent." bootstrap_mode_enabled: "To make launching your new site easier, you are in bootstrap mode. All new users will be granted trust level 1 and have daily email digest updates enabled. This will be automatically turned off when total user count exceeds %{min_users} users." @@ -3233,6 +3234,7 @@ en: step: "Step %{current} of %{total}" upload: "Upload" uploading: "Uploading..." + quit: "Perform Setup Later" invites: add_user: "add" diff --git a/config/site_settings.yml b/config/site_settings.yml index 9e19d79b358..b4f4c547a67 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -972,6 +972,9 @@ developer: default: 500 client: true hidden: true + wizard_enabled: + default: false + hidden: true embedding: feed_polling_enabled: diff --git a/lib/wizard.rb b/lib/wizard.rb index 5413263b7a1..d30917f1931 100644 --- a/lib/wizard.rb +++ b/lib/wizard.rb @@ -1,12 +1,14 @@ require_dependency 'wizard/step' require_dependency 'wizard/field' +require_dependency 'wizard/step_updater' class Wizard - attr_reader :start, :steps, :user + attr_reader :steps, :user def initialize(user) @steps = [] @user = user + @first_step = nil end def create_step(step_name) @@ -24,7 +26,7 @@ class Wizard # If it's the first step if @steps.size == 1 - @start = step + @first_step = step step.index = 0 elsif last_step.present? last_step.next = step @@ -33,9 +35,55 @@ class Wizard end end + def steps_with_fields + @steps_with_fields ||= @steps.select {|s| s.has_fields? } + end + + def start + completed = UserHistory.where( + action: UserHistory.actions[:wizard_step], + context: steps_with_fields.map(&:id) + ).uniq.pluck(:context) + + # First uncompleted step + steps_with_fields.each do |s| + return s unless completed.include?(s.id) + end + + @first_step + end + def create_updater(step_id, fields) step = @steps.find {|s| s.id == step_id.dasherize} Wizard::StepUpdater.new(@user, step, fields) end + def completed? + completed_steps?(steps_with_fields.map(&:id)) + end + + def completed_steps?(steps) + steps = [steps].flatten.uniq + + completed = UserHistory.where( + action: UserHistory.actions[:wizard_step], + context: steps + ).distinct.order(:context).pluck(:context) + + steps.sort == completed + end + + def requires_completion? + return false unless SiteSetting.wizard_enabled? + + admins = User.where("admin = true and id <> ?", Discourse.system_user.id).order(:created_at) + + # In development mode all admins are developers, so the logic is a bit screwy: + unless Rails.env.development? + admins = admins.select {|a| !Guardian.new(a).is_developer? } + end + + admins.present? && admins.first == @user && !completed? && (Topic.count < 15) + end + end diff --git a/lib/wizard/builder.rb b/lib/wizard/builder.rb index 4353e65c209..e6f421fb1ff 100644 --- a/lib/wizard/builder.rb +++ b/lib/wizard/builder.rb @@ -6,6 +6,8 @@ class Wizard end def build + return @wizard unless SiteSetting.wizard_enabled? && @wizard.user.try(:staff?) + @wizard.append_step('locale') do |step| languages = step.add_field(id: 'default_locale', type: 'dropdown', diff --git a/lib/wizard/step.rb b/lib/wizard/step.rb index 4d422841959..5586a1eb35c 100644 --- a/lib/wizard/step.rb +++ b/lib/wizard/step.rb @@ -15,6 +15,10 @@ class Wizard field end + def has_fields? + @fields.present? + end + def on_update(&block) @updater = block end diff --git a/lib/wizard/step_updater.rb b/lib/wizard/step_updater.rb index 4cd4e6a7fdd..f8ef9538c8f 100644 --- a/lib/wizard/step_updater.rb +++ b/lib/wizard/step_updater.rb @@ -12,7 +12,12 @@ class Wizard end def update - @step.updater.call(self) if @step.updater.present? + @step.updater.call(self) if @step.present? && @step.updater.present? + + if success? + logger = StaffActionLogger.new(@current_user) + logger.log_wizard_step(@step) + end end def success? diff --git a/spec/components/step_updater_spec.rb b/spec/components/step_updater_spec.rb index 28feab921d8..45c4c6f94bc 100644 --- a/spec/components/step_updater_spec.rb +++ b/spec/components/step_updater_spec.rb @@ -4,15 +4,19 @@ require_dependency 'wizard/builder' require_dependency 'wizard/step_updater' describe Wizard::StepUpdater do + before do + SiteSetting.wizard_enabled = true + end + let(:user) { Fabricate(:admin) } let(:wizard) { Wizard::Builder.new(user).build } context "locale" do - it "does not require refresh when the language stays the same" do updater = wizard.create_updater('locale', default_locale: 'en') updater.update expect(updater.refresh_required?).to eq(false) + expect(wizard.completed_steps?('locale')).to eq(true) end it "updates the locale and requires refresh when it does change" do @@ -20,6 +24,7 @@ describe Wizard::StepUpdater do updater.update expect(SiteSetting.default_locale).to eq('ru') expect(updater.refresh_required?).to eq(true) + expect(wizard.completed_steps?('locale')).to eq(true) end end @@ -30,6 +35,7 @@ describe Wizard::StepUpdater do expect(updater.success?).to eq(true) expect(SiteSetting.title).to eq("new forum title") expect(SiteSetting.site_description).to eq("neat place") + expect(wizard.completed_steps?('forum-title')).to eq(true) end context "privacy settings" do @@ -39,6 +45,7 @@ describe Wizard::StepUpdater do expect(updater.success?).to eq(true) expect(SiteSetting.login_required?).to eq(false) expect(SiteSetting.invite_only?).to eq(false) + expect(wizard.completed_steps?('privacy')).to eq(true) end it "updates to private correctly" do @@ -47,6 +54,7 @@ describe Wizard::StepUpdater do expect(updater.success?).to eq(true) expect(SiteSetting.login_required?).to eq(true) expect(SiteSetting.invite_only?).to eq(true) + expect(wizard.completed_steps?('privacy')).to eq(true) end end @@ -62,6 +70,7 @@ describe Wizard::StepUpdater do expect(SiteSetting.contact_email).to eq("eviltrout@example.com") expect(SiteSetting.contact_url).to eq("http://example.com/custom-contact-url") expect(SiteSetting.site_contact_username).to eq(user.username) + expect(wizard.completed_steps?('contact')).to eq(true) end it "doesn't update when there are errors" do @@ -71,6 +80,7 @@ describe Wizard::StepUpdater do updater.update expect(updater).to_not be_success expect(updater.errors).to be_present + expect(wizard.completed_steps?('contact')).to eq(false) end end @@ -109,6 +119,8 @@ describe Wizard::StepUpdater do updater.update raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck(:raw).first expect(raw).to eq("company_domain - company_full_name - company_short_name template") + + expect(wizard.completed_steps?('corporate')).to eq(true) end end @@ -120,6 +132,7 @@ describe Wizard::StepUpdater do updater = wizard.create_updater('colors', theme_id: 'dark') updater.update expect(updater.success?).to eq(true) + expect(wizard.completed_steps?('colors')).to eq(true) color_scheme.reload expect(color_scheme).to be_enabled @@ -131,6 +144,7 @@ describe Wizard::StepUpdater do updater = wizard.create_updater('colors', theme_id: 'dark') updater.update expect(updater.success?).to eq(true) + expect(wizard.completed_steps?('colors')).to eq(true) color_scheme = ColorScheme.where(via_wizard: true).first expect(color_scheme).to be_present @@ -150,6 +164,7 @@ describe Wizard::StepUpdater do updater.update expect(updater).to be_success + expect(wizard.completed_steps?('logos')).to eq(true) expect(SiteSetting.logo_url).to eq('/uploads/logo.png') expect(SiteSetting.logo_small_url).to eq('/uploads/logo-small.png') expect(SiteSetting.favicon_url).to eq('/uploads/favicon.png') @@ -158,7 +173,6 @@ describe Wizard::StepUpdater do end context "invites step" do - let(:invites) { return [{ email: 'regular@example.com', role: 'regular'}, { email: 'moderator@example.com', role: 'moderator'}] @@ -169,6 +183,7 @@ describe Wizard::StepUpdater do updater.update expect(updater).to be_success + expect(wizard.completed_steps?('invites')).to eq(true) expect(Invite.where(email: 'regular@example.com')).to be_present expect(Invite.where(email: 'moderator@example.com')).to be_present diff --git a/spec/components/wizard_builder_spec.rb b/spec/components/wizard_builder_spec.rb new file mode 100644 index 00000000000..53052fbf674 --- /dev/null +++ b/spec/components/wizard_builder_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' +require 'wizard' +require 'wizard/builder' + +describe Wizard::Builder do + let(:moderator) { Fabricate.build(:moderator) } + + it "returns a wizard with steps when enabled" do + SiteSetting.wizard_enabled = true + + wizard = Wizard::Builder.new(moderator).build + expect(wizard).to be_present + expect(wizard.steps).to be_present + end + + it "returns a wizard without steps when enabled, but not staff" do + wizard = Wizard::Builder.new(Fabricate.build(:user)).build + expect(wizard).to be_present + expect(wizard.steps).to be_blank + end + + it "returns a wizard without steps when disabled" do + SiteSetting.wizard_enabled = false + + wizard = Wizard::Builder.new(moderator).build + expect(wizard).to be_present + expect(wizard.steps).to be_blank + end + +end diff --git a/spec/components/wizard_spec.rb b/spec/components/wizard_spec.rb index 527191b1482..067e2bd143b 100644 --- a/spec/components/wizard_spec.rb +++ b/spec/components/wizard_spec.rb @@ -2,18 +2,21 @@ require 'rails_helper' require 'wizard' describe Wizard do + before do + SiteSetting.wizard_enabled = true + end - let(:user) { Fabricate.build(:user) } - let(:wizard) { Wizard.new(user) } - - it "has default values" do - expect(wizard.start).to be_blank - expect(wizard.steps).to be_empty - expect(wizard.user).to be_present + context "defaults" do + it "has default values" do + wizard = Wizard.new(Fabricate.build(:moderator)) + expect(wizard.steps).to be_empty + expect(wizard.user).to be_present + end end describe "append_step" do - + let(:user) { Fabricate.build(:moderator) } + let(:wizard) { Wizard.new(user) } let(:step1) { wizard.create_step('first-step') } let(:step2) { wizard.create_step('second-step') } @@ -54,4 +57,96 @@ describe Wizard do end end + describe "completed?" do + let(:user) { Fabricate.build(:moderator) } + let(:wizard) { Wizard.new(user) } + + it "is complete when all steps with fields have logs" do + wizard.append_step('first') do |step| + step.add_field(id: 'element', type: 'text') + end + + wizard.append_step('second') do |step| + step.add_field(id: 'another_element', type: 'text') + end + + wizard.append_step('finished') + + expect(wizard.start.id).to eq('first') + expect(wizard.completed_steps?('first')).to eq(false) + expect(wizard.completed_steps?('second')).to eq(false) + expect(wizard.completed?).to eq(false) + + updater = wizard.create_updater('first', element: 'test') + updater.update + expect(wizard.start.id).to eq('second') + expect(wizard.completed_steps?('first')).to eq(true) + expect(wizard.completed?).to eq(false) + + updater = wizard.create_updater('second', element: 'test') + updater.update + + expect(wizard.completed_steps?('first')).to eq(true) + expect(wizard.completed_steps?('second')).to eq(true) + expect(wizard.completed_steps?('finished')).to eq(false) + expect(wizard.completed?).to eq(true) + + # Once you've completed the wizard start at the beginning + expect(wizard.start.id).to eq('first') + end + end + + describe "#requires_completion?" do + + def build_simple(user) + wizard = Wizard.new(user) + wizard.append_step('simple') do |step| + step.add_field(id: 'name', type: 'text') + end + wizard + end + + it "is false for anonymous" do + expect(build_simple(nil).requires_completion?).to eq(false) + end + + it "is false for regular users" do + expect(build_simple(Fabricate.build(:user)).requires_completion?).to eq(false) + end + + it "is false for a developer" do + developer = Fabricate(:admin) + Developer.create!(user_id: developer.id) + + expect(build_simple(developer).requires_completion?).to eq(false) + end + + it "it's false when the wizard is disabled" do + SiteSetting.wizard_enabled = false + admin = Fabricate(:admin) + expect(build_simple(admin).requires_completion?).to eq(false) + end + + it "it's true for the first admin" do + admin = Fabricate(:admin) + expect(build_simple(admin).requires_completion?).to eq(true) + + second_admin = Fabricate(:admin) + expect(build_simple(second_admin).requires_completion?).to eq(false) + end + + it "is false for staff when complete" do + wizard = build_simple(Fabricate(:admin)) + updater = wizard.create_updater('simple', name: 'Evil Trout') + updater.update + + expect(wizard.requires_completion?).to eq(false) + + # It's also false for another user + wizard = build_simple(Fabricate(:admin)) + expect(wizard.requires_completion?).to eq(false) + end + + end + end diff --git a/spec/controllers/steps_controller_spec.rb b/spec/controllers/steps_controller_spec.rb index 5356477ef6a..950005b2027 100644 --- a/spec/controllers/steps_controller_spec.rb +++ b/spec/controllers/steps_controller_spec.rb @@ -2,6 +2,10 @@ require 'rails_helper' describe StepsController do + before do + SiteSetting.wizard_enabled = true + end + it 'needs you to be logged in' do expect { xhr :put, :update, id: 'made-up-id', fields: { forum_title: "updated title" } @@ -19,6 +23,12 @@ describe StepsController do log_in(:admin) end + it "raises an error if the wizard is disabled" do + SiteSetting.wizard_enabled = false + xhr :put, :update, id: 'contact', fields: { contact_email: "eviltrout@example.com" } + expect(response).to be_forbidden + end + it "updates properly if you are staff" do xhr :put, :update, id: 'contact', fields: { contact_email: "eviltrout@example.com" } expect(response).to be_success diff --git a/spec/controllers/wizard_controller_spec.rb b/spec/controllers/wizard_controller_spec.rb index 87fd198b8e0..ffc1ed64bd0 100644 --- a/spec/controllers/wizard_controller_spec.rb +++ b/spec/controllers/wizard_controller_spec.rb @@ -2,9 +2,13 @@ require 'rails_helper' describe WizardController do - context 'index' do + context 'wizard enabled' do render_views + before do + SiteSetting.wizard_enabled = true + end + it 'needs you to be logged in' do expect { xhr :get, :index }.to raise_error(Discourse::NotLoggedIn) end @@ -15,6 +19,13 @@ describe WizardController do expect(response).to be_forbidden end + it "raises an error if the wizard is disabled" do + SiteSetting.wizard_enabled = false + log_in(:admin) + xhr :get, :index + expect(response).to be_forbidden + end + it "renders the wizard if you are an admin" do log_in(:admin) xhr :get, :index @@ -27,7 +38,6 @@ describe WizardController do expect(response).to be_success expect(::JSON.parse(response.body).has_key?('wizard')).to eq(true) end - end end