diff --git a/controllers/custom_wizard/admin/wizard.rb b/controllers/custom_wizard/admin/wizard.rb index ac5eeb8a..48beea67 100644 --- a/controllers/custom_wizard/admin/wizard.rb +++ b/controllers/custom_wizard/admin/wizard.rb @@ -4,7 +4,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController def index render_json_dump( wizard_list: ActiveModel::ArraySerializer.new( - CustomWizard::Wizard.list, + CustomWizard::Template.list, each_serializer: CustomWizard::BasicWizardSerializer ), field_types: CustomWizard::Field.types @@ -14,7 +14,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController def show params.require(:wizard_id) - if data = CustomWizard::Wizard.find(params[:wizard_id].underscore) + if data = CustomWizard::Template.find(params[:wizard_id].underscore) render json: data.as_json else render json: { none: true } @@ -22,8 +22,11 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController end def remove - CustomWizard::Wizard.remove(@wizard.id) - render json: success_json + if CustomWizard::Template.remove(@wizard.id) + render json: success_json + else + render json: failed_json + end end def save @@ -36,7 +39,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController if validation[:error] render json: { error: validation[:error] } else - if wizard_id = CustomWizard::Wizard.save(validation[:wizard]) + if wizard_id = CustomWizard::Template.save(validation[:wizard]) render json: success_json.merge(wizard_id: wizard_id) else render json: failed_json diff --git a/controllers/custom_wizard/steps.rb b/controllers/custom_wizard/steps.rb index 9318d16b..4d338ab8 100644 --- a/controllers/custom_wizard/steps.rb +++ b/controllers/custom_wizard/steps.rb @@ -1,19 +1,30 @@ class CustomWizard::StepsController < ::ApplicationController before_action :ensure_logged_in + before_action :ensure_can_update def update params.require(:step_id) params.require(:wizard_id) - field_ids = CustomWizard::Wizard.field_ids(params[:wizard_id], params[:step_id]) + + wizard = @builder.build + step = wizard.steps.select { |s| s.id == update_params[:step_id] }.first - permitted = params.permit(:wizard_id, :step_id) - if params[:fields] - permitted[:fields] = params[:fields].select { |k, v| field_ids.include? k } - permitted.permit! + if !step || step.fields.blank? + raise Discourse::InvalidParameters.new(:step_id) end - wizard = CustomWizard::Builder.new(permitted[:wizard_id].underscore, current_user).build - updater = wizard.create_updater(permitted[:step_id], permitted[:fields]) + field_ids = step.fields.map(&:id) + + if params[:fields] + permitted_fields = params[:fields].select { |k, v| field_ids.include? k } + update_params[:fields] = permitted_fields + update_params.permit! + end + + updater = wizard.create_updater( + update_params[:step_id], + update_params[:fields] + ) updater.update if updater.success? @@ -29,4 +40,25 @@ class CustomWizard::StepsController < ::ApplicationController render json: { errors: errors }, status: 422 end end + + private + + def ensure_can_update + @builder = CustomWizard::Builder.new( + update_params[:wizard_id].underscore, + current_user + ) + + if @builder.nil? + raise Discourse::InvalidParameters.new(:wizard_id) + end + + if !@builder.wizard || !@builder.wizard.can_access? + raise Discourse::InvalidAccess.new + end + end + + def update_params + params.permit(:wizard_id, :step_id) + end end diff --git a/controllers/custom_wizard/wizard.rb b/controllers/custom_wizard/wizard.rb index 914d3cea..bf2b3ed4 100644 --- a/controllers/custom_wizard/wizard.rb +++ b/controllers/custom_wizard/wizard.rb @@ -24,7 +24,7 @@ class CustomWizard::WizardController < ::ApplicationController if builder.wizard.present? builder_opts = {} - builder_opts[:reset] = params[:reset] || builder.wizard.restart_on_revisit + builder_opts[:reset] = params[:reset] built_wizard = builder.build(builder_opts, params) render_serialized(built_wizard, ::CustomWizard::WizardSerializer, root: false) diff --git a/lib/custom_wizard/builder.rb b/lib/custom_wizard/builder.rb index 728ed847..4e8ec8b5 100644 --- a/lib/custom_wizard/builder.rb +++ b/lib/custom_wizard/builder.rb @@ -2,13 +2,13 @@ class CustomWizard::Builder attr_accessor :wizard, :updater, :submissions def initialize(wizard_id, user=nil) - params = CustomWizard::Wizard.find(wizard_id) - return nil if params.blank? + template = CustomWizard::Template.find(wizard_id) + return nil if template.blank? - @wizard = CustomWizard::Wizard.new(params, user) - @steps = params['steps'] || [] - @actions = params['actions'] || [] - @submissions = @wizard.submissions if user && @wizard + @wizard = CustomWizard::Wizard.new(template, user) + @steps = template['steps'] || [] + @actions = template['actions'] || [] + @submissions = @wizard.submissions end def self.sorted_handlers @@ -48,12 +48,27 @@ class CustomWizard::Builder return nil if !SiteSetting.custom_wizard_enabled || !@wizard return @wizard if !@wizard.can_access? + build_opts[:reset] = build_opts[:reset] || @wizard.restart_on_revisit reset_submissions if build_opts[:reset] @steps.each do |step_template| @wizard.append_step(step_template['id']) do |step| + step.permitted = true + + if step_template['required_data'] + step = ensure_required_data(step, step_template) + end + + if !step.permitted + if step_template['required_data_message'] + step.permitted_message = step_template['required_data_message'] + end + next + end + step.title = step_template['title'] if step_template['title'] step.banner = step_template['banner'] if step_template['banner'] + step.key = step_template['key'] if step_template['key'] if step_template['description'] step.description = mapper.interpolate( @@ -63,59 +78,8 @@ class CustomWizard::Builder ) end - step.key = step_template['key'] if step_template['key'] - step.permitted = true - if permitted_params = step_template['permitted_params'] - permitted_data = {} - - permitted_params.each do |p| - pair = p['pairs'].first - params_key = pair['key'].to_sym - submission_key = pair['value'].to_sym - permitted_data[submission_key] = params[params_key] if params[params_key] - end - - if permitted_data.present? - current_data = @submissions.last || {} - save_submissions(current_data.merge(permitted_data), false) - end - end - - if (required_data = step_template['required_data']).present? - has_required_data = true - - required_data.each do |required| - required['pairs'].each do |pair| - if pair['key'].blank? || pair['value'].blank? - has_required_data = false - end - end - end - - if has_required_data - if !@submissions.last - step.permitted = false - else - required_data.each do |required| - pairs = required['pairs'].map do |p| - p['key'] = @submissions.last[p['key']] - end - - unless mapper.validate_pairs(pairs) - step.permitted = false - end - end - end - - if !step.permitted - if step_template['required_data_message'] - step.permitted_message = step_template['required_data_message'] - end - - next - end - end + save_permitted_params(permitted_params, params) end if step_template['fields'] && step_template['fields'].length @@ -211,14 +175,14 @@ class CustomWizard::Builder params[:description] = field_template['description'] if field_template['description'] params[:image] = field_template['image'] if field_template['image'] params[:key] = field_template['key'] if field_template['key'] + params[:min_length] = field_template['min_length'] if field_template['min_length'] + params[:value] = prefill_field(field_template, step_template) ## Load previously submitted values if !build_opts[:reset] && @submissions.last && !@submissions.last.key?("submitted_at") submission = @submissions.last params[:value] = submission[field_template['id']] if submission[field_template['id']] end - - params[:value] = prefill_field(field_template, step_template) || params[:value] if field_template['type'] === 'group' && params[:value].present? params[:value] = params[:value].first @@ -404,4 +368,44 @@ class CustomWizard::Builder PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, @submissions) @wizard.reset end + + def save_permitted_params(permitted_params, params) + permitted_data = {} + + permitted_params.each do |pp| + pair = pp['pairs'].first + params_key = pair['key'].to_sym + submission_key = pair['value'].to_sym + permitted_data[submission_key] = params[params_key] if params[params_key] + end + + if permitted_data.present? + current_data = @submissions.last || {} + save_submissions(current_data.merge(permitted_data), false) + end + end + + def ensure_required_data(step, step_template) + step_template['required_data'].each do |required| + pairs = required['pairs'].select do |pair| + pair['key'].present? && pair['value'].present? + end + + if pairs.any? && !@submissions.last + step.permitted = false + break + end + + pairs.each do |pair| + pair['key'] = @submissions.last[pair['key']] + end + + if !mapper.validate_pairs(pairs) + step.permitted = false + break + end + end + + step + end end diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb new file mode 100644 index 00000000..39e4b3e5 --- /dev/null +++ b/lib/custom_wizard/template.rb @@ -0,0 +1,81 @@ +class CustomWizard::Template + def self.add(obj) + wizard = obj.is_a?(String) ? ::JSON.parse(json) : obj + PluginStore.set('custom_wizard', wizard["id"], wizard) + end + + def self.find(wizard_id) + PluginStore.get('custom_wizard', wizard_id) + end + + def self.save(data) + data = data.with_indifferent_access + existing = self.find(data[:id]) + + data[:steps].each do |step| + if step[:raw_description] + step[:description] = PrettyText.cook(step[:raw_description]) + end + end + + data = data.slice!(:create) + + ActiveRecord::Base.transaction do + PluginStore.set('custom_wizard', data[:id], data) + + if data[:after_time] + Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: data[:id]) + enqueue_at = Time.parse(data[:after_time_scheduled]).utc + Jobs.enqueue_at(enqueue_at, :set_after_time_wizard, wizard_id: data[:id]) + end + + if existing && existing[:after_time] && !data[:after_time] + Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: data[:id]) + Jobs.enqueue(:clear_after_time_wizard, wizard_id: data[:id]) + end + end + + data[:id] + end + + def self.remove(wizard_id) + wizard = self.create(wizard_id) + + ActiveRecord::Base.transaction do + if wizard.after_time + Jobs.cancel_scheduled_job(:set_after_time_wizard) + Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard.id) + end + + PluginStore.remove('custom_wizard', wizard.id) + end + end + + def self.exists?(wizard_id) + PluginStoreRow.exists?(plugin_name: 'custom_wizard', key: wizard_id) + end + + def self.list(user=nil) + PluginStoreRow.where(plugin_name: 'custom_wizard').order(:id) + .reduce([]) do |result, record| + attrs = JSON.parse(record.value) + + if attrs.present? && + attrs.is_a?(Hash) && + attrs['id'].present? && + attrs['name'].present? + + result.push(attrs) + end + + result + end + end + + def self.setting_enabled(attr) + PluginStoreRow.where(" + plugin_name = 'custom_wizard' AND + (value::json ->> '#{attr}')::boolean IS TRUE + ") + end +end \ No newline at end of file diff --git a/lib/custom_wizard/validator.rb b/lib/custom_wizard/validator.rb index 5b377584..b75b295d 100644 --- a/lib/custom_wizard/validator.rb +++ b/lib/custom_wizard/validator.rb @@ -95,7 +95,7 @@ class CustomWizard::Validator end def check_id(object, type) - if type === :wizard && @opts[:create] && CustomWizard::Wizard.exists?(object[:id]) + if type === :wizard && @opts[:create] && CustomWizard::Template.exists?(object[:id]) @error = { type: 'conflict', params: { type: type, property: 'id', value: object[:id] } diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index 04308a7f..b4f41089 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -24,6 +24,7 @@ class CustomWizard::Wizard :needs_categories, :needs_groups, :steps, + :step_ids, :actions, :user @@ -52,6 +53,7 @@ class CustomWizard::Wizard end @first_step = nil + @step_ids = attrs['steps'].map { |s| s['id'] } @steps = [] @actions = [] end @@ -65,13 +67,11 @@ class CustomWizard::Wizard yield step if block_given? - last_step = @steps.last - - @steps << step + last_step = steps.last + steps << step - # If it's the first step - if @steps.size == 1 - @first_step = step + if steps.size == 1 + first_step = step step.index = 0 elsif last_step.present? last_step.next = step @@ -81,67 +81,62 @@ class CustomWizard::Wizard end def start - return nil if !@user + return nil if !user if unfinished? && last_completed_step = ::UserHistory.where( - acting_user_id: @user.id, + acting_user_id: user.id, action: ::UserHistory.actions[:custom_wizard_step], - context: @id, - subject: @steps.map(&:id) + context: id, + subject: steps.map(&:id) ).order("created_at").last step_id = last_completed_step.subject - last_index = @steps.index { |s| s.id == step_id } - @steps[last_index + 1] + last_index = steps.index { |s| s.id == step_id } + steps[last_index + 1] else - @first_step + first_step end end def create_updater(step_id, fields) step = @steps.find { |s| s.id == step_id } wizard = self - CustomWizard::StepUpdater.new(@user, wizard, step, fields) + CustomWizard::StepUpdater.new(user, wizard, step, fields) end def unfinished? - return nil if !@user + return nil if !user most_recent = ::UserHistory.where( - acting_user_id: @user.id, + acting_user_id: user.id, action: ::UserHistory.actions[:custom_wizard_step], - context: @id, + context: id, ).distinct.order('updated_at DESC').first if most_recent && most_recent.subject == "reset" false elsif most_recent - last_finished_step = most_recent.subject - last_step = CustomWizard::Wizard.step_ids(@id).last - last_finished_step != last_step + most_recent.subject != steps.last.id else true end end def completed? - return nil if !@user + return nil if !user - steps = CustomWizard::Wizard.step_ids(@id) - history = ::UserHistory.where( - acting_user_id: @user.id, + acting_user_id: user.id, action: ::UserHistory.actions[:custom_wizard_step], - context: @id + context: id ) - if @after_time - history = history.where("updated_at > ?", @after_time_scheduled) + if after_time + history = history.where("updated_at > ?", after_time_scheduled) end completed = history.distinct.order(:subject).pluck(:subject) - - (steps - completed).empty? + (step_ids - completed).empty? end def permitted? @@ -178,33 +173,38 @@ class CustomWizard::Wizard def reset ::UserHistory.create( action: ::UserHistory.actions[:custom_wizard_step], - acting_user_id: @user.id, - context: @id, + acting_user_id: user.id, + context: id, subject: "reset" ) end def categories - @categories ||= ::Site.new(Guardian.new(@user)).categories + @categories ||= ::Site.new(Guardian.new(user)).categories end def groups - @groups ||= ::Site.new(Guardian.new(@user)).groups + @groups ||= ::Site.new(Guardian.new(user)).groups end def submissions - Array.wrap(PluginStore.get("#{id}_submissions", @user.id)) + Array.wrap(PluginStore.get("#{id}_submissions", user.id)) end - def self.filter_records(filter) - PluginStoreRow.where(" - plugin_name = 'custom_wizard' AND - (value::json ->> '#{filter}')::boolean IS TRUE - ") + def self.create(wizard_id, user = nil) + if template = CustomWizard::Template.find(wizard_id) + self.new(template.to_h, user) + else + false + end + end + + def self.list(user=nil) + CustomWizard::Template.list.map { |template| self.new(template.to_h, user) } end def self.after_signup(user) - if (records = filter_records('after_signup')).any? + if (records = CustomWizard::Template.setting_enabled('after_signup')).any? result = false records @@ -225,9 +225,9 @@ class CustomWizard::Wizard end def self.prompt_completion(user) - if (records = filter_records('prompt_completion')).any? + if (records = CustomWizard::Template.setting_enabled('prompt_completion')).any? records.reduce([]) do |result, record| - wizard = CustomWizard::Wizard.new(::JSON.parse(record.value), user) + wizard = self.new(::JSON.parse(record.value), user) if wizard.permitted? && !wizard.completed? result.push(id: wizard.id, name: wizard.name) @@ -241,115 +241,13 @@ class CustomWizard::Wizard end def self.restart_on_revisit - if (records = filter_records('restart_on_revisit')).any? + if (records = CustomWizard::Template.setting_enabled('restart_on_revisit')).any? records.first.key else false end end - def self.steps(wizard_id) - wizard = PluginStore.get('custom_wizard', wizard_id) - wizard ? wizard['steps'] : nil - end - - def self.step_ids(wizard_id) - steps = self.steps(wizard_id) - return [] if !steps - steps.map { |s| s['id'] }.flatten.uniq - end - - def self.field_ids(wizard_id, step_id) - steps = self.steps(wizard_id) - return [] if !steps - step = steps.select { |s| s['id'] === step_id }.first - if step && fields = step['fields'] - fields.map { |f| f['id'] } - else - [] - end - end - - def self.add_wizard(obj) - wizard = obj.is_a?(String) ? ::JSON.parse(json) : obj - PluginStore.set('custom_wizard', wizard["id"], wizard) - end - - def self.find(wizard_id) - PluginStore.get('custom_wizard', wizard_id) - end - - def self.list(user=nil) - PluginStoreRow.where(plugin_name: 'custom_wizard').order(:id) - .reduce([]) do |result, record| - attrs = JSON.parse(record.value) - - if attrs.present? && - attrs.is_a?(Hash) && - attrs['id'].present? && - attrs['name'].present? - - result.push(self.new(attrs, user)) - end - - result - end - end - - def self.save(wizard) - existing_wizard = self.create(wizard[:id]) - - wizard[:steps].each do |step| - if step[:raw_description] - step[:description] = PrettyText.cook(step[:raw_description]) - end - end - - wizard = wizard.slice!(:create) - - ActiveRecord::Base.transaction do - PluginStore.set('custom_wizard', wizard[:id], wizard) - - if wizard[:after_time] - Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard[:id]) - enqueue_at = Time.parse(wizard[:after_time_scheduled]).utc - Jobs.enqueue_at(enqueue_at, :set_after_time_wizard, wizard_id: wizard[:id]) - end - - if existing_wizard && existing_wizard.after_time && !wizard[:after_time] - Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard[:id]) - Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard[:id]) - end - end - - wizard[:id] - end - - def self.remove(wizard_id) - wizard = self.create(wizard_id) - - ActiveRecord::Base.transaction do - if wizard.after_time - Jobs.cancel_scheduled_job(:set_after_time_wizard) - Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard.id) - end - - PluginStore.remove('custom_wizard', wizard.id) - end - end - - def self.exists?(wizard_id) - PluginStoreRow.exists?(plugin_name: 'custom_wizard', key: wizard_id) - end - - def self.create(wizard_id, user = nil) - if wizard = self.find(wizard_id) - self.new(wizard.to_h, user) - else - false - end - end - def self.set_submission_redirect(user, wizard_id, url) PluginStore.set("#{wizard_id.underscore}_submissions", user.id, [{ redirect_to: url }]) end @@ -364,12 +262,4 @@ class CustomWizard::Wizard false end end - - def self.register_styles - full_path = "#{Rails.root}/plugins/discourse-custom-wizard/assets/stylesheets/wizard/wizard_custom.scss" - DiscoursePluginRegistry.register_asset(full_path, {}, "wizard_custom") - Stylesheet::Importer.register_import("wizard_custom") do - import_files(DiscoursePluginRegistry.stylesheets["wizard_custom"]) - end - end end diff --git a/plugin.rb b/plugin.rb index dfe3d063..3a8d48e0 100644 --- a/plugin.rb +++ b/plugin.rb @@ -55,6 +55,7 @@ after_initialize do ../lib/custom_wizard/mapper.rb ../lib/custom_wizard/log.rb ../lib/custom_wizard/step_updater.rb + ../lib/custom_wizard/template.rb ../lib/custom_wizard/validator.rb ../lib/custom_wizard/wizard.rb ../lib/custom_wizard/api/api.rb @@ -158,7 +159,11 @@ after_initialize do ::Wizard::Field.prepend CustomWizardFieldExtension ::Wizard::Step.prepend CustomWizardStepExtension - CustomWizard::Wizard.register_styles + full_path = "#{Rails.root}/plugins/discourse-custom-wizard/assets/stylesheets/wizard/wizard_custom.scss" + DiscoursePluginRegistry.register_asset(full_path, {}, "wizard_custom") + Stylesheet::Importer.register_import("wizard_custom") do + import_files(DiscoursePluginRegistry.stylesheets["wizard_custom"]) + end DiscourseEvent.trigger(:custom_wizard_ready) end diff --git a/spec/components/custom_wizard/action_spec.rb b/spec/components/custom_wizard/action_spec.rb index 9ebfe779..12308ef2 100644 --- a/spec/components/custom_wizard/action_spec.rb +++ b/spec/components/custom_wizard/action_spec.rb @@ -7,20 +7,23 @@ describe CustomWizard::Action do before do Group.refresh_automatic_group!(:trust_level_2) - template = JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" - ).read) - CustomWizard::Wizard.add_wizard(template) - @wizard = CustomWizard::Wizard.create('super_mega_fun_wizard', user) + CustomWizard::Template.add( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + ) + @template = CustomWizard::Template.find('super_mega_fun_wizard') end it 'creates a topic' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - updater = built_wizard.create_updater(built_wizard.steps[0].id, + wizard = CustomWizard::Builder.new(@template[:id], user).build + + updater = wizard.create_updater( + wizard.steps[0].id, step_1_field_1: "Topic Title", step_1_field_2: "topic body" ).update - updater2 = built_wizard.create_updater(built_wizard.steps[1].id, {}).update + updater2 = wizard.create_updater(wizard.steps[1].id, {}).update topic = Topic.where(title: "Topic Title") @@ -34,9 +37,9 @@ describe CustomWizard::Action do it 'sends a message' do User.create(username: 'angus1', email: "angus1@email.com") - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - built_wizard.create_updater(built_wizard.steps[0].id, {}).update - built_wizard.create_updater(built_wizard.steps[1].id, {}).update + wizard = CustomWizard::Builder.new(@template[:id], user).build + wizard.create_updater(wizard.steps[0].id, {}).update + wizard.create_updater(wizard.steps[1].id, {}).update topic = Topic.where( archetype: Archetype.private_message, @@ -54,26 +57,26 @@ describe CustomWizard::Action do end it 'updates a profile' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build + wizard = CustomWizard::Builder.new(@template[:id], user).build upload = Upload.create!( url: '/images/image.png', original_filename: 'image.png', filesize: 100, user_id: -1, ) - steps = built_wizard.steps - built_wizard.create_updater(steps[0].id, {}).update - built_wizard.create_updater(steps[1].id, - step_2_field_7: upload.as_json, + steps = wizard.steps + wizard.create_updater(steps[0].id, {}).update + wizard.create_updater(steps[1].id, + step_2_field_7: upload.as_json ).update expect(user.profile_background_upload.id).to eq(upload.id) end it 'opens a composer' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - built_wizard.create_updater(built_wizard.steps[0].id, step_1_field_1: "Text input").update + wizard = CustomWizard::Builder.new(@template[:id], user).build + wizard.create_updater(wizard.steps[0].id, step_1_field_1: "Text input").update - updater = built_wizard.create_updater(built_wizard.steps[1].id, {}) + updater = wizard.create_updater(wizard.steps[1].id, {}) updater.update submissions = PluginStore.get("super_mega_fun_wizard_submissions", user.id) @@ -85,34 +88,34 @@ describe CustomWizard::Action do end it 'creates a category' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - built_wizard.create_updater(built_wizard.steps[0].id, step_1_field_1: "Text input").update - built_wizard.create_updater(built_wizard.steps[1].id, {}).update + wizard = CustomWizard::Builder.new(@template[:id], user).build + wizard.create_updater(wizard.steps[0].id, step_1_field_1: "Text input").update + wizard.create_updater(wizard.steps[1].id, {}).update submissions = PluginStore.get("super_mega_fun_wizard_submissions", user.id) expect(Category.where(id: submissions.first['action_8']).exists?).to eq(true) end it 'creates a group' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - step_id = built_wizard.steps[0].id - updater = built_wizard.create_updater(step_id, step_1_field_1: "Text input").update + wizard = CustomWizard::Builder.new(@template[:id], user).build + step_id = wizard.steps[0].id + updater = wizard.create_updater(step_id, step_1_field_1: "Text input").update submissions = PluginStore.get("super_mega_fun_wizard_submissions", user.id) expect(Group.where(name: submissions.first['action_9']).exists?).to eq(true) end it 'adds a user to a group' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - step_id = built_wizard.steps[0].id - updater = built_wizard.create_updater(step_id, step_1_field_1: "Text input").update + wizard = CustomWizard::Builder.new(@template[:id], user).build + step_id = wizard.steps[0].id + updater = wizard.create_updater(step_id, step_1_field_1: "Text input").update submissions = PluginStore.get("super_mega_fun_wizard_submissions", user.id) group = Group.find_by(name: submissions.first['action_9']) expect(group.users.first.username).to eq('angus') end it 'watches categories' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - built_wizard.create_updater(built_wizard.steps[0].id, step_1_field_1: "Text input").update - built_wizard.create_updater(built_wizard.steps[1].id, {}).update + wizard = CustomWizard::Builder.new(@template[:id], user).build + wizard.create_updater(wizard.steps[0].id, step_1_field_1: "Text input").update + wizard.create_updater(wizard.steps[1].id, {}).update submissions = PluginStore.get("super_mega_fun_wizard_submissions", user.id) expect(CategoryUser.where( category_id: submissions.first['action_8'], @@ -125,8 +128,8 @@ describe CustomWizard::Action do end it 're-routes a user' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - updater = built_wizard.create_updater(built_wizard.steps.last.id, {}) + wizard = CustomWizard::Builder.new(@template[:id], user).build + updater = wizard.create_updater(wizard.steps.last.id, {}) updater.update expect(updater.result[:redirect_on_complete]).to eq("https://google.com") end diff --git a/spec/components/custom_wizard/builder_spec.rb b/spec/components/custom_wizard/builder_spec.rb index 9a7c8b60..341cf045 100644 --- a/spec/components/custom_wizard/builder_spec.rb +++ b/spec/components/custom_wizard/builder_spec.rb @@ -3,19 +3,45 @@ require 'rails_helper' describe CustomWizard::Builder do - fab!(:user) { Fabricate(:user, username: 'angus', email: "angus@email.com", trust_level: TrustLevel[2]) } - fab!(:new_user) { Fabricate(:user, trust_level: 0) } + fab!(:trusted_user) { + Fabricate( + :user, + username: 'angus', + email: "angus@email.com", + trust_level: TrustLevel[3] + ) + } + fab!(:user) { Fabricate(:user) } fab!(:category1) { Fabricate(:category, name: 'cat1') } fab!(:category2) { Fabricate(:category, name: 'cat2') } fab!(:group) { Fabricate(:group) } - before do - Group.refresh_automatic_group!(:trust_level_2) - template = JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + let(:required_data_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/required_data.json" ).read) - CustomWizard::Wizard.add_wizard(template) - @wizard = CustomWizard::Wizard.create('super_mega_fun_wizard', user) + } + + let(:permitted_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read) + } + + let(:permitted_param_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/permitted_params.json" + ).read) + } + + before do + Group.refresh_automatic_group!(:trust_level_3) + CustomWizard::Template.add( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + ) + @template = CustomWizard::Template.find('super_mega_fun_wizard') end context 'disabled' do @@ -24,7 +50,9 @@ describe CustomWizard::Builder do end it "returns nil" do - expect(CustomWizard::Builder.new(@wizard.id, user).build).to eq(nil) + expect( + CustomWizard::Builder.new(@template[:id], user).build + ).to eq(nil) end end @@ -33,150 +61,276 @@ describe CustomWizard::Builder do SiteSetting.custom_wizard_enabled = true end + it "returns wizard metadata" do + wizard = CustomWizard::Builder.new(@template[:id], user).build + expect(wizard.id).to eq("super_mega_fun_wizard") + expect(wizard.name).to eq("Super Mega Fun Wizard") + expect(wizard.background).to eq("#333333") + end + it "returns steps" do expect( - CustomWizard::Builder.new(@wizard.id, user).build.steps.length - ).to eq(2) - end - - it 'returns no steps if multiple submissions are disabled and user has completed' do - wizard_template = CustomWizard::Wizard.find(@wizard.id) - wizard_template[:multiple_submissions] = false - CustomWizard::Wizard.save(wizard_template) - - history_params = { - action: UserHistory.actions[:custom_wizard_step], - acting_user_id: user.id, - context: @wizard.id - } - @wizard.steps.each do |step| - UserHistory.create!(history_params.merge(subject: step.id)) - end - - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - expect( - CustomWizard::Builder.new(@wizard.id, user).build.steps.length - ).to eq(0) - end - - it 'returns no steps if user is not permitted' do - expect( - CustomWizard::Builder.new(@wizard.id, new_user).build.steps.length - ).to eq(0) - end - - it 'returns steps if user is permitted' do - expect( - CustomWizard::Builder.new(@wizard.id, user).build.steps.length + CustomWizard::Builder.new(@template[:id], user).build + .steps.length ).to eq(3) end - it 'returns a wizard with prefilled data if user has partially completed it' do - expect( - CustomWizard::Builder.new(@wizard.id, user) - .build - .steps[0].fields[0].value - ).to eq('I am prefilled') - end - - it 'returns a wizard with no prefilled data if options include reset' do - PluginStore.set("super_mega_fun_wizard_submissions", user.id, { - text: 'Input into text', - }) - expect( - CustomWizard::Builder.new(@wizard.id, user) - .build(reset: true) - .steps[0].fields[0].value - ).to eq(nil) - end - - context 'building steps' do - it 'returns step metadata' do - expect( - CustomWizard::Builder.new(@wizard.id, user) - .build(reset: true) - .steps[0] - ).to eq('Super Mega Fun Wizard') + context "with multiple submissions disabled" do + before do + @template[:multiple_submissions] = false + CustomWizard::Template.save(@template.as_json) end - it 'saves permitted params' do - @wizard.steps[0].permitted_params = permitted_params - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build({}, param_key: 'param_value') - submissions = PluginStore.get("super_mega_fun_wizard_submissions", user.id) - expect(submissions[0]['submission_param_key']).to eq('param_value') + it 'returns steps if user has not completed it' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.length + ).to eq(3) end - it 'is not permitted if required data is not present' do - @wizard.steps[0].required_data = required_data + it 'returns no steps if user has completed it' do + @template[:steps].each do |step| + UserHistory.create!( + { + action: UserHistory.actions[:custom_wizard_step], + acting_user_id: user.id, + context: @template[:id] + }.merge( + subject: step[:id] + ) + ) + end + expect( - CustomWizard::Builder.new(@wizard.id, user).build.steps[0].permitted + CustomWizard::Builder.new(@template[:id], user).build + .steps.length + ).to eq(0) + end + end + + context "with restricted permissions" do + before do + @template[:permitted] = permitted_json["permitted"] + CustomWizard::Template.save(@template.as_json) + end + + it 'is not permitted if user is not in permitted group' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .permitted? ).to eq(false) end - it 'it shows required data message if required data has message' do - @wizard.steps[0].required_data = required_data - @wizard.steps[0].required_data_message = "Data is required" - PluginStore.set("super_mega_fun_wizard_submissions", user.id, - text: 'Input into text', - ) - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - expect(built_wizard.steps[0].permitted).to eq(false) - expect(built_wizard.steps[0].permitted_message).to eq("Data is required") + it 'user cannot access if not permitted' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .can_access? + ).to eq(false) end - it 'is permitted if required data is present' do - @wizard.steps[0].required_data = required_data - PluginStore.set('super_mega_fun_wizard_submissions', user.id, - text: "Input into text" - ) + it 'returns wizard metadata if user is not permitted' do expect( - CustomWizard::Builder.new(@wizard.id, user).build.steps[0].permitted + CustomWizard::Builder.new(@template[:id], user).build + .name + ).to eq("Super Mega Fun Wizard") + end + + it 'returns no steps if user is not permitted' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.length + ).to eq(0) + end + + it 'is permitted if user is in permitted group' do + expect( + CustomWizard::Builder.new(@template[:id], trusted_user).build + .permitted? ).to eq(true) end - it 'returns field metadata' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - expect(built_wizard.steps[0].fields[0].label).to eq("

Name

") - expect(built_wizard.steps[0].fields[0].type).to eq("text") + it 'user can access if permitted' do + expect( + CustomWizard::Builder.new(@template[:id], trusted_user).build + .can_access? + ).to eq(true) end - it 'returns fields' do - @wizard.steps[0].fields[1] = checkbox_field - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - expect(built_wizard.steps[0].fields.length).to eq(2) + it 'returns steps if user is permitted' do + expect( + CustomWizard::Builder.new(@template[:id], trusted_user).build + .steps.length + ).to eq(3) + end + end + + it 'returns prefilled data' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.first + .fields.first + .value + ).to eq('I am prefilled') + end + + context "user has partially completed" do + before do + PluginStore.set("super_mega_fun_wizard_submissions", user.id, + step_1_field_1: 'I am a user submission' + ) + end + + it 'returns saved submissions' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.first + .fields.first + .value + ).to eq('I am a user submission') + end + + context "restart is enabled" do + before do + @template[:restart_on_revisit] = true + CustomWizard::Template.save(@template.as_json) + end + + it 'does not return saved submissions' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.first + .fields.first + .value + ).to eq('I am prefilled') + end + end + end + + context 'building step' do + it 'returns step metadata' do + first_step = CustomWizard::Builder.new(@template[:id], user) + .build(reset: true) + .steps.first + + expect(first_step.id).to eq("step_1") + expect(first_step.title).to eq("Text") + expect(first_step.description).to eq("

Text inputs!

") + end + + context 'with required data' do + before do + @template[:steps][0][:required_data] = required_data_json['required_data'] + @template[:steps][0][:required_data_message] = required_data_json['required_data_message'] + CustomWizard::Template.save(@template.as_json) + end + + it 'is not permitted if required data is not present' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.first + .permitted + ).to eq(false) + end + + it 'it shows required data message' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.first + .permitted_message + ).to eq("Missing required data") + end + + it 'is permitted if required data is present' do + PluginStore.set('super_mega_fun_wizard_submissions', user.id, + required_data: "required_value" + ) + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.first + .permitted + ).to eq(true) + end + end + + context "with permitted params" do + before do + @template[:steps][0][:permitted_params] = permitted_param_json['permitted_params'] + CustomWizard::Template.save(@template.as_json) + end + + it 'saves permitted params' do + CustomWizard::Builder.new(@template[:id], user).build({}, + param: 'param_value' + ) + expect( + PluginStore.get("super_mega_fun_wizard_submissions", user.id) + .first['saved_param'] + ).to eq('param_value') + end + end + end + + context 'building field' do + it 'returns field metadata' do + wizard = CustomWizard::Builder.new(@template[:id], user).build + field = wizard.steps.first.fields.first + + expect(field.label).to eq("

Text

") + expect(field.type).to eq("text") + expect(field.id).to eq("step_1_field_1") + expect(field.min_length).to eq("3") + end + + it 'returns all step fields' do + expect( + CustomWizard::Builder.new(@template[:id], user) + .build + .steps.first + .fields.length + ).to eq(4) end end context 'on update' do + def perform_update(step_id, submission) + wizard = CustomWizard::Builder.new(@template[:id], user).build + updater = wizard.create_updater(step_id, submission) + updater.update + updater + end + it 'saves submissions' do - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - built_wizard.create_updater(built_wizard.steps[0].id, - step_1_field_1: 'Text input' - ).update + perform_update('step_1', step_1_field_1: 'Text input') expect( PluginStore.get("super_mega_fun_wizard_submissions", user.id) .first['step_1_field_1'] ).to eq('Text input') end + context 'save submissions disabled' do + before do + @template[:save_submissions] = false + CustomWizard::Template.save(@template.as_json) + end + + it "does not save submissions" do + perform_update('step_1', step_1_field_1: 'Text input') + expect( + PluginStore.get("super_mega_fun_wizard_submissions", user.id) + ).to eq(nil) + end + end + context 'validation' do it 'applies min length' do - @wizard.steps[0].fields[0].min_length = 10 - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - updater = built_wizard.create_updater(built_wizard.steps[0].id, - step_1_field_1: 'Te' - ).update - expect(updater.errors.messages[:text].first).to eq( - I18n.t('wizard.field.too_short', label: 'Text', min: 3) - ) + expect( + perform_update('step_1', step_1_field_1: 'Te') + .errors.messages[:step_1_field_1].first + ).to eq(I18n.t('wizard.field.too_short', label: 'Text', min: 3)) end it 'standardises boolean entries' do - @wizard.steps[0].fields[0] = checkbox_field - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - updater = built_wizard.create_updater(built_wizard.steps[1].id, - step_2_field_5: 'false' - ).update + perform_update('step_2', step_2_field_5: 'false') expect( PluginStore.get("super_mega_fun_wizard_submissions", user.id) .first['step_2_field_5'] @@ -184,12 +338,13 @@ describe CustomWizard::Builder do end it 'requires required fields' do - @wizard.steps[0].fields[0]['required'] = true - built_wizard = CustomWizard::Builder.new(@wizard.id, user).build - updater = built_wizard.create_updater(built_wizard.steps.second.id).update + @template[:steps][0][:fields][1][:required] = true + CustomWizard::Template.save(@template.as_json) + expect( - updater.errors.messages[:step_1_field_1].first - ).to eq(I18n.t('wizard.field.required', label: 'Text')) + perform_update('step_1', step_1_field_2: nil) + .errors.messages[:step_1_field_2].first + ).to eq(I18n.t('wizard.field.required', label: 'Textarea')) end end end diff --git a/spec/fixtures/step/permitted_params.json b/spec/fixtures/step/permitted_params.json new file mode 100644 index 00000000..d6b7e729 --- /dev/null +++ b/spec/fixtures/step/permitted_params.json @@ -0,0 +1,17 @@ +{ + "permitted_params": [ + { + "type": "association", + "pairs": [ + { + "index": 0, + "key": "param", + "key_type": "text", + "value": "saved_param", + "value_type": "text", + "connector": "association" + } + ] + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/wizard.json b/spec/fixtures/wizard.json index 09ffc95e..1d5e1bc7 100644 --- a/spec/fixtures/wizard.json +++ b/spec/fixtures/wizard.json @@ -7,16 +7,6 @@ "after_signup": false, "prompt_completion": true, "theme_id": 2, - "permitted": [ - { - "type": "assignment", - "output_type": "group", - "output_connector": "set", - "output": [ - 12 - ] - } - ], "steps": [ { "id": "step_1", diff --git a/spec/fixtures/wizard/permitted.json b/spec/fixtures/wizard/permitted.json new file mode 100644 index 00000000..91d8edf1 --- /dev/null +++ b/spec/fixtures/wizard/permitted.json @@ -0,0 +1,12 @@ +{ + "permitted": [ + { + "type": "assignment", + "output_type": "group", + "output_connector": "set", + "output": [ + 13 + ] + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/wizard/required_data.json b/spec/fixtures/wizard/required_data.json new file mode 100644 index 00000000..9f65d516 --- /dev/null +++ b/spec/fixtures/wizard/required_data.json @@ -0,0 +1,18 @@ +{ + "required_data_message": "Missing required data", + "required_data": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "required_data", + "key_type": "text", + "value": "required_value", + "value_type": "text", + "connector": "equal" + } + ] + } + ] +} \ No newline at end of file diff --git a/spec_offload/serializers/custom_wizard/wizard_serializer_spec.rb b/spec_offload/serializers/custom_wizard/wizard_serializer_spec.rb index c036e9fa..1498ede9 100644 --- a/spec_offload/serializers/custom_wizard/wizard_serializer_spec.rb +++ b/spec_offload/serializers/custom_wizard/wizard_serializer_spec.rb @@ -10,7 +10,7 @@ describe CustomWizard::WizardSerializer do template = JSON.parse(File.open( "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" ).read) - CustomWizard::Wizard.add_wizard(template) + CustomWizard::Template.add(template) @wizard = CustomWizard::Wizard.create('super_mega_fun_wizard', user) end