Track steps the user has completed, nag them to finish it.

This commit is contained in:
Robin Ward 2016-09-14 16:36:08 -04:00
parent ef84981e38
commit 29cf47cfb2
21 changed files with 290 additions and 19 deletions

View File

@ -36,7 +36,10 @@ export default Ember.Component.extend({
@computed() @computed()
shouldSee() { 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') @computed('enabled', 'shouldSee', 'publicTopicCount', 'publicPostCount')

View File

@ -17,6 +17,10 @@ export default Ember.Component.extend(StringBuffer, {
notices.push([I18n.t("emails_are_disabled"), 'alert-emails-disabled']); 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.currentUser && this.currentUser.get('staff') && this.siteSettings.bootstrap_mode_enabled) {
if (this.siteSettings.bootstrap_mode_min_users > 0) { 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']); notices.push([I18n.t("bootstrap_mode_enabled", {min_users: this.siteSettings.bootstrap_mode_min_users}), 'alert-bootstrap-mode']);

View File

@ -9,6 +9,9 @@ export default Ember.Component.extend({
this.autoFocus(); this.autoFocus();
}, },
@computed('step.index')
showQuitButton: index => index === 0,
@computed('step.displayIndex', 'wizard.totalSteps') @computed('step.displayIndex', 'wizard.totalSteps')
showNextButton: (current, total) => current < total, showNextButton: (current, total) => current < total,
@ -49,6 +52,10 @@ export default Ember.Component.extend({
}, },
actions: { actions: {
quit() {
document.location = "/";
},
backStep() { backStep() {
if (this.get('saving')) { return; } if (this.get('saving')) { return; }
this.sendAction('goBack'); this.sendAction('goBack');

View File

@ -21,6 +21,13 @@
</div> </div>
<div class='wizard-buttons'> <div class='wizard-buttons'>
{{#if showQuitButton}}
<button class='wizard-btn danger' {{action "quit"}} disabled={{saving}}>
{{fa-icon "chevron-left"}}
{{i18n "wizard.quit"}}
</button>
{{/if}}
{{#if showBackButton}} {{#if showBackButton}}
<button class='wizard-btn back' {{action "backStep"}} disabled={{saving}}> <button class='wizard-btn back' {{action "backStep"}} disabled={{saving}}>
{{fa-icon "chevron-left"}} {{fa-icon "chevron-left"}}
@ -36,7 +43,7 @@
{{/if}} {{/if}}
{{#if showDoneButton}} {{#if showDoneButton}}
<button class='wizard-btn done' {{action "finished"}} disabled={{saving}}> <button class='wizard-btn done' {{action "quit"}} disabled={{saving}}>
{{fa-icon "check"}} {{fa-icon "check"}}
{{i18n "wizard.done"}} {{i18n "wizard.done"}}
</button> </button>

View File

@ -461,6 +461,10 @@ class ApplicationController < ActionController::Base
raise Discourse::InvalidAccess.new unless current_user && current_user.staff? raise Discourse::InvalidAccess.new unless current_user && current_user.staff?
end end
def ensure_wizard_enabled
raise Discourse::InvalidAccess.new unless SiteSetting.wizard_enabled?
end
def destination_url def destination_url
request.original_url unless request.original_url =~ /uploads/ request.original_url unless request.original_url =~ /uploads/
end end

View File

@ -4,6 +4,7 @@ require_dependency 'wizard/step_updater'
class StepsController < ApplicationController class StepsController < ApplicationController
before_filter :ensure_wizard_enabled
before_filter :ensure_logged_in before_filter :ensure_logged_in
before_filter :ensure_staff before_filter :ensure_staff

View File

@ -2,7 +2,7 @@ require_dependency 'wizard'
require_dependency 'wizard/builder' require_dependency 'wizard/builder'
class WizardController < ApplicationController class WizardController < ApplicationController
before_filter :ensure_wizard_enabled, only: [:index]
before_filter :ensure_logged_in before_filter :ensure_logged_in
before_filter :ensure_staff before_filter :ensure_staff

View File

@ -55,6 +55,7 @@ class UserHistory < ActiveRecord::Base
rate_limited_like: 37, # not used anymore rate_limited_like: 37, # not used anymore
revoke_email: 38, revoke_email: 38,
deactivate_user: 39, deactivate_user: 39,
wizard_step: 40
) )
end end

View File

@ -1,4 +1,6 @@
require_dependency 'discourse_tagging' require_dependency 'discourse_tagging'
require_dependency 'wizard'
require_dependency 'wizard/builder'
class SiteSerializer < ApplicationSerializer class SiteSerializer < ApplicationSerializer
@ -20,7 +22,8 @@ class SiteSerializer < ApplicationSerializer
:can_create_tag, :can_create_tag,
:can_tag_topics, :can_tag_topics,
:tags_filter_regexp, :tags_filter_regexp,
:top_tags :top_tags,
:wizard_required
has_many :categories, serializer: BasicCategorySerializer, embed: :objects has_many :categories, serializer: BasicCategorySerializer, embed: :objects
has_many :trust_levels, embed: :objects has_many :trust_levels, embed: :objects
@ -110,4 +113,12 @@ class SiteSerializer < ApplicationSerializer
def top_tags def top_tags
Tag.top_tags Tag.top_tags
end end
def wizard_required
true
end
def include_wizard_required?
Wizard::Builder.new(scope.user).build.requires_completion?
end
end end

View File

@ -353,6 +353,15 @@ class StaffActionLogger
})) }))
end 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 private
def params(opts=nil) def params(opts=nil)

View File

@ -165,6 +165,7 @@ en:
topic_admin_menu: "topic admin actions" topic_admin_menu: "topic admin actions"
wizard_required: "It's time to configure your forum! <a href='/wizard/' data-auto-route='true'>Start the Setup Wizard</a>!"
emails_are_disabled: "All outgoing email has been globally disabled by an administrator. No email notifications of any kind will be sent." 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." 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}" step: "Step %{current} of %{total}"
upload: "Upload" upload: "Upload"
uploading: "Uploading..." uploading: "Uploading..."
quit: "Perform Setup Later"
invites: invites:
add_user: "add" add_user: "add"

View File

@ -972,6 +972,9 @@ developer:
default: 500 default: 500
client: true client: true
hidden: true hidden: true
wizard_enabled:
default: false
hidden: true
embedding: embedding:
feed_polling_enabled: feed_polling_enabled:

View File

@ -1,12 +1,14 @@
require_dependency 'wizard/step' require_dependency 'wizard/step'
require_dependency 'wizard/field' require_dependency 'wizard/field'
require_dependency 'wizard/step_updater'
class Wizard class Wizard
attr_reader :start, :steps, :user attr_reader :steps, :user
def initialize(user) def initialize(user)
@steps = [] @steps = []
@user = user @user = user
@first_step = nil
end end
def create_step(step_name) def create_step(step_name)
@ -24,7 +26,7 @@ class Wizard
# If it's the first step # If it's the first step
if @steps.size == 1 if @steps.size == 1
@start = step @first_step = step
step.index = 0 step.index = 0
elsif last_step.present? elsif last_step.present?
last_step.next = step last_step.next = step
@ -33,9 +35,55 @@ class Wizard
end end
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) def create_updater(step_id, fields)
step = @steps.find {|s| s.id == step_id.dasherize} step = @steps.find {|s| s.id == step_id.dasherize}
Wizard::StepUpdater.new(@user, step, fields) Wizard::StepUpdater.new(@user, step, fields)
end 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 end

View File

@ -6,6 +6,8 @@ class Wizard
end end
def build def build
return @wizard unless SiteSetting.wizard_enabled? && @wizard.user.try(:staff?)
@wizard.append_step('locale') do |step| @wizard.append_step('locale') do |step|
languages = step.add_field(id: 'default_locale', languages = step.add_field(id: 'default_locale',
type: 'dropdown', type: 'dropdown',

View File

@ -15,6 +15,10 @@ class Wizard
field field
end end
def has_fields?
@fields.present?
end
def on_update(&block) def on_update(&block)
@updater = block @updater = block
end end

View File

@ -12,7 +12,12 @@ class Wizard
end end
def update 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 end
def success? def success?

View File

@ -4,15 +4,19 @@ require_dependency 'wizard/builder'
require_dependency 'wizard/step_updater' require_dependency 'wizard/step_updater'
describe Wizard::StepUpdater do describe Wizard::StepUpdater do
before do
SiteSetting.wizard_enabled = true
end
let(:user) { Fabricate(:admin) } let(:user) { Fabricate(:admin) }
let(:wizard) { Wizard::Builder.new(user).build } let(:wizard) { Wizard::Builder.new(user).build }
context "locale" do context "locale" do
it "does not require refresh when the language stays the same" do it "does not require refresh when the language stays the same" do
updater = wizard.create_updater('locale', default_locale: 'en') updater = wizard.create_updater('locale', default_locale: 'en')
updater.update updater.update
expect(updater.refresh_required?).to eq(false) expect(updater.refresh_required?).to eq(false)
expect(wizard.completed_steps?('locale')).to eq(true)
end end
it "updates the locale and requires refresh when it does change" do it "updates the locale and requires refresh when it does change" do
@ -20,6 +24,7 @@ describe Wizard::StepUpdater do
updater.update updater.update
expect(SiteSetting.default_locale).to eq('ru') expect(SiteSetting.default_locale).to eq('ru')
expect(updater.refresh_required?).to eq(true) expect(updater.refresh_required?).to eq(true)
expect(wizard.completed_steps?('locale')).to eq(true)
end end
end end
@ -30,6 +35,7 @@ describe Wizard::StepUpdater do
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)
expect(SiteSetting.title).to eq("new forum title") expect(SiteSetting.title).to eq("new forum title")
expect(SiteSetting.site_description).to eq("neat place") expect(SiteSetting.site_description).to eq("neat place")
expect(wizard.completed_steps?('forum-title')).to eq(true)
end end
context "privacy settings" do context "privacy settings" do
@ -39,6 +45,7 @@ describe Wizard::StepUpdater do
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)
expect(SiteSetting.login_required?).to eq(false) expect(SiteSetting.login_required?).to eq(false)
expect(SiteSetting.invite_only?).to eq(false) expect(SiteSetting.invite_only?).to eq(false)
expect(wizard.completed_steps?('privacy')).to eq(true)
end end
it "updates to private correctly" do it "updates to private correctly" do
@ -47,6 +54,7 @@ describe Wizard::StepUpdater do
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)
expect(SiteSetting.login_required?).to eq(true) expect(SiteSetting.login_required?).to eq(true)
expect(SiteSetting.invite_only?).to eq(true) expect(SiteSetting.invite_only?).to eq(true)
expect(wizard.completed_steps?('privacy')).to eq(true)
end end
end end
@ -62,6 +70,7 @@ describe Wizard::StepUpdater do
expect(SiteSetting.contact_email).to eq("eviltrout@example.com") expect(SiteSetting.contact_email).to eq("eviltrout@example.com")
expect(SiteSetting.contact_url).to eq("http://example.com/custom-contact-url") expect(SiteSetting.contact_url).to eq("http://example.com/custom-contact-url")
expect(SiteSetting.site_contact_username).to eq(user.username) expect(SiteSetting.site_contact_username).to eq(user.username)
expect(wizard.completed_steps?('contact')).to eq(true)
end end
it "doesn't update when there are errors" do it "doesn't update when there are errors" do
@ -71,6 +80,7 @@ describe Wizard::StepUpdater do
updater.update updater.update
expect(updater).to_not be_success expect(updater).to_not be_success
expect(updater.errors).to be_present expect(updater.errors).to be_present
expect(wizard.completed_steps?('contact')).to eq(false)
end end
end end
@ -109,6 +119,8 @@ describe Wizard::StepUpdater do
updater.update updater.update
raw = Post.where(topic_id: SiteSetting.tos_topic_id, post_number: 1).pluck(:raw).first 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(raw).to eq("company_domain - company_full_name - company_short_name template")
expect(wizard.completed_steps?('corporate')).to eq(true)
end end
end end
@ -120,6 +132,7 @@ describe Wizard::StepUpdater do
updater = wizard.create_updater('colors', theme_id: 'dark') updater = wizard.create_updater('colors', theme_id: 'dark')
updater.update updater.update
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)
expect(wizard.completed_steps?('colors')).to eq(true)
color_scheme.reload color_scheme.reload
expect(color_scheme).to be_enabled expect(color_scheme).to be_enabled
@ -131,6 +144,7 @@ describe Wizard::StepUpdater do
updater = wizard.create_updater('colors', theme_id: 'dark') updater = wizard.create_updater('colors', theme_id: 'dark')
updater.update updater.update
expect(updater.success?).to eq(true) expect(updater.success?).to eq(true)
expect(wizard.completed_steps?('colors')).to eq(true)
color_scheme = ColorScheme.where(via_wizard: true).first color_scheme = ColorScheme.where(via_wizard: true).first
expect(color_scheme).to be_present expect(color_scheme).to be_present
@ -150,6 +164,7 @@ describe Wizard::StepUpdater do
updater.update updater.update
expect(updater).to be_success 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_url).to eq('/uploads/logo.png')
expect(SiteSetting.logo_small_url).to eq('/uploads/logo-small.png') expect(SiteSetting.logo_small_url).to eq('/uploads/logo-small.png')
expect(SiteSetting.favicon_url).to eq('/uploads/favicon.png') expect(SiteSetting.favicon_url).to eq('/uploads/favicon.png')
@ -158,7 +173,6 @@ describe Wizard::StepUpdater do
end end
context "invites step" do context "invites step" do
let(:invites) { let(:invites) {
return [{ email: 'regular@example.com', role: 'regular'}, return [{ email: 'regular@example.com', role: 'regular'},
{ email: 'moderator@example.com', role: 'moderator'}] { email: 'moderator@example.com', role: 'moderator'}]
@ -169,6 +183,7 @@ describe Wizard::StepUpdater do
updater.update updater.update
expect(updater).to be_success 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: 'regular@example.com')).to be_present
expect(Invite.where(email: 'moderator@example.com')).to be_present expect(Invite.where(email: 'moderator@example.com')).to be_present

View File

@ -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

View File

@ -2,18 +2,21 @@ require 'rails_helper'
require 'wizard' require 'wizard'
describe Wizard do describe Wizard do
before do
SiteSetting.wizard_enabled = true
end
let(:user) { Fabricate.build(:user) } context "defaults" do
let(:wizard) { Wizard.new(user) } it "has default values" do
wizard = Wizard.new(Fabricate.build(:moderator))
it "has default values" do expect(wizard.steps).to be_empty
expect(wizard.start).to be_blank expect(wizard.user).to be_present
expect(wizard.steps).to be_empty end
expect(wizard.user).to be_present
end end
describe "append_step" do describe "append_step" do
let(:user) { Fabricate.build(:moderator) }
let(:wizard) { Wizard.new(user) }
let(:step1) { wizard.create_step('first-step') } let(:step1) { wizard.create_step('first-step') }
let(:step2) { wizard.create_step('second-step') } let(:step2) { wizard.create_step('second-step') }
@ -54,4 +57,96 @@ describe Wizard do
end end
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 end

View File

@ -2,6 +2,10 @@ require 'rails_helper'
describe StepsController do describe StepsController do
before do
SiteSetting.wizard_enabled = true
end
it 'needs you to be logged in' do it 'needs you to be logged in' do
expect { expect {
xhr :put, :update, id: 'made-up-id', fields: { forum_title: "updated title" } xhr :put, :update, id: 'made-up-id', fields: { forum_title: "updated title" }
@ -19,6 +23,12 @@ describe StepsController do
log_in(:admin) log_in(:admin)
end 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 it "updates properly if you are staff" do
xhr :put, :update, id: 'contact', fields: { contact_email: "eviltrout@example.com" } xhr :put, :update, id: 'contact', fields: { contact_email: "eviltrout@example.com" }
expect(response).to be_success expect(response).to be_success

View File

@ -2,9 +2,13 @@ require 'rails_helper'
describe WizardController do describe WizardController do
context 'index' do context 'wizard enabled' do
render_views render_views
before do
SiteSetting.wizard_enabled = true
end
it 'needs you to be logged in' do it 'needs you to be logged in' do
expect { xhr :get, :index }.to raise_error(Discourse::NotLoggedIn) expect { xhr :get, :index }.to raise_error(Discourse::NotLoggedIn)
end end
@ -15,6 +19,13 @@ describe WizardController do
expect(response).to be_forbidden expect(response).to be_forbidden
end 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 it "renders the wizard if you are an admin" do
log_in(:admin) log_in(:admin)
xhr :get, :index xhr :get, :index
@ -27,7 +38,6 @@ describe WizardController do
expect(response).to be_success expect(response).to be_success
expect(::JSON.parse(response.body).has_key?('wizard')).to eq(true) expect(::JSON.parse(response.body).has_key?('wizard')).to eq(true)
end end
end end
end end