diff --git a/.travis.yml b/.travis.yml index 9a695031..a2048719 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,10 @@ -# We want to use the KVM-based system, so require sudo -sudo: required services: - docker before_install: - git clone --depth=1 https://github.com/discourse/discourse-plugin-ci -install: true # Prevent travis doing bundle install +install: true script: - discourse-plugin-ci/script.sh diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index 137cf0f8..265da2a3 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -113,6 +113,7 @@ wizardFieldSelection=true userFieldSelection='key,value' categorySelection='output' + wizardActionSelection='output' outputDefaultSelection='category' context='action' )}} @@ -198,6 +199,7 @@ textSelection='value' userFieldSelection='key' wizardFieldSelection='value' + wizardActionSelection='value' keyDefaultSelection='userField' context='action' )}} @@ -270,6 +272,7 @@ textSelection='value,output' wizardFieldSelection='key,value,assignment' userFieldSelection='key,value,assignment' + wizardActionSelection=true groupSelection='value,output' outputDefaultSelection='group' context='action' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 2fcfe687..4f9ef820 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -26,6 +26,11 @@ en: file_large: "File too large" invalid_json: "File is not a valid json file" no_valid_wizards: "File doesn't contain any valid wizards" + + validation: + required: "%{property} is required" + conflict: "Wizard with %{wizard_id} already exists" + after_time: "After time setting is invalid" site_settings: custom_wizard_enabled: "Enable custom wizards." diff --git a/config/routes.rb b/config/routes.rb index 4050d810..a765a809 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -36,8 +36,8 @@ Discourse::Application.routes.append do get 'admin/wizards/logs' => 'admin_logs#index' - get 'admin/wizards/transfer' => 'transfer#index' - get 'admin/wizards/transfer/export' => 'transfer#export' - post 'admin/wizards/transfer/import' => 'transfer#import' + get 'admin/wizards/transfer' => 'admin_transfer#index' + get 'admin/wizards/transfer/export' => 'admin_transfer#export' + post 'admin/wizards/transfer/import' => 'admin_transfer#import' end end \ No newline at end of file diff --git a/controllers/custom_wizard/admin/admin.rb b/controllers/custom_wizard/admin/admin.rb index 3f70b542..5da337a4 100644 --- a/controllers/custom_wizard/admin/admin.rb +++ b/controllers/custom_wizard/admin/admin.rb @@ -9,6 +9,7 @@ class CustomWizard::AdminController < ::Admin::AdminController def find_wizard params.require(:wizard_id) @wizard = CustomWizard::Wizard.create(params[:wizard_id].underscore) + raise Discourse::InvalidParameters.new(:wizard_id) unless @wizard end def custom_field_list diff --git a/controllers/custom_wizard/admin/logs.rb b/controllers/custom_wizard/admin/logs.rb index 60303af7..005de8dc 100644 --- a/controllers/custom_wizard/admin/logs.rb +++ b/controllers/custom_wizard/admin/logs.rb @@ -1,7 +1,7 @@ class CustomWizard::AdminLogsController < CustomWizard::AdminController def index render_serialized( - CustomWizard::Log.list(params[:page].to_i), + CustomWizard::Log.list(params[:page].to_i, params[:limit].to_i), CustomWizard::LogSerializer ) end diff --git a/controllers/custom_wizard/admin/submissions.rb b/controllers/custom_wizard/admin/submissions.rb index b7d45e44..69088492 100644 --- a/controllers/custom_wizard/admin/submissions.rb +++ b/controllers/custom_wizard/admin/submissions.rb @@ -1,29 +1,23 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController skip_before_action :preload_json, :check_xhr, only: [:download] - - before_action :find_wizard + before_action :find_wizard, except: [:index] def index render json: ActiveModel::ArraySerializer.new( - CustomWizard::Wizard.list, + CustomWizard::Wizard.list(current_user), each_serializer: CustomWizard::BasicWizardSerializer ) end def show - result = {} - - if wizard = @wizard - submissions = build_submissions(wizard.id) - result[:wizard] = CustomWizard::BasicWizardSerializer.new(wizard, root: false) - result[:submissions] = submissions.as_json - end - - render_json_dump(result) + render_json_dump( + wizard: CustomWizard::BasicWizardSerializer.new(@wizard, root: false), + submissions: build_submissions.as_json + ) end def download - send_data build_submissions(@wizard.id).to_json, + send_data build_submissions.to_json, filename: "#{Discourse.current_hostname}-wizard-submissions-#{@wizard.name}.json", content_type: "application/json", disposition: "attachment" @@ -31,23 +25,21 @@ class CustomWizard::AdminSubmissionsController < CustomWizard::AdminController private - def build_submissions(wizard_id) - rows = PluginStoreRow.where(plugin_name: "#{wizard_id}_submissions").order('id DESC') - - submissions = [*rows].map do |row| - value = ::JSON.parse(row.value) - - if user = User.find_by(id: row.key) - username = user.username - else - username = I18n.t('admin.wizard.submissions.no_user', id: row.key) - end - - value.map do |submission| - { - username: username - }.merge!(submission.except("redirect_to")) - end - end.flatten + def build_submissions + PluginStoreRow.where(plugin_name: "#{@wizard.id}_submissions") + .order('id DESC') + .map do |row| + value = ::JSON.parse(row.value) + + if user = User.find_by(id: row.key) + username = user.username + else + username = I18n.t('admin.wizard.submissions.no_user', id: row.key) + end + + value.map do |v| + { username: username }.merge!(v.except("redirect_to")) + end + end.flatten end end \ No newline at end of file diff --git a/controllers/custom_wizard/transfer.rb b/controllers/custom_wizard/admin/transfer.rb similarity index 50% rename from controllers/custom_wizard/transfer.rb rename to controllers/custom_wizard/admin/transfer.rb index f1bf0bba..b55fe872 100644 --- a/controllers/custom_wizard/transfer.rb +++ b/controllers/custom_wizard/admin/transfer.rb @@ -1,25 +1,22 @@ -class CustomWizard::TransferController < ::ApplicationController - before_action :ensure_logged_in - before_action :ensure_admin +class CustomWizard::AdminTransferController < CustomWizard::AdminController skip_before_action :check_xhr, :only => [:export] - def index - end - def export - wizards = params['wizards'] - wizard_objects = [] + wizard_ids = params['wizards'] + templates = [] - if wizards.nil? + if wizard_ids.nil? render json: { error: I18n.t('wizard.export.error.select_one') } return end - wizards.each do |w| - wizard_objects.push(PluginStore.get('custom_wizard', w.tr('-', '_'))) + wizard_ids.each do |wizard_id| + if template = CustomWizard::Template.find(wizard_id) + templates.push(template) + end end - send_data wizard_objects.to_json, + send_data templates.to_json, type: "application/json", disposition: 'attachment', filename: 'wizards.json' @@ -27,45 +24,42 @@ class CustomWizard::TransferController < ::ApplicationController def import file = File.read(params['file'].tempfile) - + if file.nil? render json: { error: I18n.t('wizard.import.error.no_file') } return end - fileSize = file.size - maxFileSize = 512 * 1024 - - if maxFileSize < fileSize + file_size = file.size + max_file_size = 512 * 1024 + + if max_file_size < file_size render json: { error: I18n.t('wizard.import.error.file_large') } return end - + begin - jsonObject = JSON.parse file + template_json = JSON.parse file rescue JSON::ParserError render json: { error: I18n.t('wizard.import.error.invalid_json') } return end - countValid = 0 success_ids = [] failed_ids = [] - jsonObject.each do |o| - if !CustomWizard::Wizard.new(o) - failed_ids.push o['id'] - next + template_json.each do |t_json| + template = CustomWizard::Template.new(t_json) + template.save(skip_jobs: true) + + if template.errors.any? + failed_ids.push t_json['id'] + else + success_ids.push t_json['id'] end - - countValid += 1 - pluginStoreEntry = PluginStore.new 'custom_wizard' - saved = pluginStoreEntry.set(o['id'], o) unless pluginStoreEntry.get(o['id']) - success_ids.push o['id'] if !!saved - failed_ids.push o['id'] if !saved end - if countValid == 0 + if success_ids.length == 0 render json: { error: I18n.t('wizard.import.error.no_valid_wizards') } else render json: { success: success_ids, failed: failed_ids } diff --git a/controllers/custom_wizard/admin/wizard.rb b/controllers/custom_wizard/admin/wizard.rb index a4515008..9a0dc4cf 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::Wizard.list(current_user), each_serializer: CustomWizard::BasicWizardSerializer ), field_types: CustomWizard::Field.types, @@ -15,7 +15,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 } @@ -23,25 +23,21 @@ 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 - opts = {} - opts[:create] = params[:create] if params[:create] + template = CustomWizard::Template.new(save_wizard_params.to_h) + wizard_id = template.save(create: params[:create]) - validator = CustomWizard::Validator.new(save_wizard_params.to_h, opts) - validation = validator.perform - - if validation[:error] - render json: { error: validation[:error] } + if template.errors.any? + render json: failed_json.merge(errors: result.errors.full_messages) else - if wizard_id = CustomWizard::Wizard.save(validation[:wizard]) - render json: success_json.merge(wizard_id: wizard_id) - else - render json: failed_json - end + render json: success_json.merge(wizard_id: wizard_id) end end diff --git a/controllers/custom_wizard/steps.rb b/controllers/custom_wizard/steps.rb index e370a1dd..778707a6 100644 --- a/controllers/custom_wizard/steps.rb +++ b/controllers/custom_wizard/steps.rb @@ -1,25 +1,38 @@ 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) + + update = update_params.to_h + + if params[:fields] + update[:fields] = {} + + params[:fields].each do |k, v| + update[:fields][k] = v if field_ids.include? k + end + end + + updater = wizard.create_updater(update[:step_id], update[:fields]) updater.update - + if updater.success? result = success_json result.merge!(updater.result) if updater.result result[:refresh_required] = true if updater.refresh_required? + render json: result else errors = [] @@ -29,4 +42,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..aa786510 100644 --- a/controllers/custom_wizard/wizard.rb +++ b/controllers/custom_wizard/wizard.rb @@ -1,7 +1,8 @@ class CustomWizard::WizardController < ::ApplicationController prepend_view_path(Rails.root.join('plugins', 'discourse-custom-wizard', 'views')) layout 'wizard' - + + before_action :ensure_plugin_enabled helper_method :wizard_page_title helper_method :theme_ids @@ -24,7 +25,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) @@ -38,7 +39,7 @@ class CustomWizard::WizardController < ::ApplicationController def skip params.require(:wizard_id) - + if wizard.required && !wizard.completed? && wizard.permitted? return render json: { error: I18n.t('wizard.no_skip') } end @@ -47,16 +48,11 @@ class CustomWizard::WizardController < ::ApplicationController user = current_user if user - submission = wizard.submissions.last - + submission = wizard.current_submission if submission && submission['redirect_to'] result.merge!(redirect_to: submission['redirect_to']) end - if submission && !wizard.save_submissions - PluginStore.remove("#{wizard.id}_submissions", user.id) - end - if user.custom_fields['redirect_to_wizard'] === wizard.id user.custom_fields.delete('redirect_to_wizard') user.save_custom_fields(true) @@ -65,4 +61,12 @@ class CustomWizard::WizardController < ::ApplicationController render json: result end + + private + + def ensure_plugin_enabled + unless SiteSetting.custom_wizard_enabled + redirect_to path("/") + end + end end diff --git a/jobs/set_after_time_wizard.rb b/jobs/set_after_time_wizard.rb index 80f8f5e5..38d0eebe 100644 --- a/jobs/set_after_time_wizard.rb +++ b/jobs/set_after_time_wizard.rb @@ -3,7 +3,7 @@ module Jobs def execute(args) if SiteSetting.custom_wizard_enabled wizard = CustomWizard::Wizard.create(args[:wizard_id]) - + if wizard && wizard.after_time user_ids = [] diff --git a/lib/custom_wizard/action.rb b/lib/custom_wizard/action.rb index 5f657dc7..b99466ae 100644 --- a/lib/custom_wizard/action.rb +++ b/lib/custom_wizard/action.rb @@ -83,6 +83,11 @@ class CustomWizard::Action multiple: true ).perform + if targets.blank? + log_error("no recipients", "send_message has no recipients") + return + end + targets.each do |target| if Group.find_by(name: target) params[:target_group_names] = target @@ -125,12 +130,18 @@ class CustomWizard::Action def update_profile params = {} - + if (profile_updates = action['profile_updates']) profile_updates.first[:pairs].each do |pair| if allowed_profile_field?(pair['key']) key = cast_profile_key(pair['key']) - value = cast_profile_value(mapper.map_field(pair['value'], pair['value_type']), pair['key']) + value = cast_profile_value( + mapper.map_field( + pair['value'], + pair['value_type'] + ), + pair['key'] + ) if user_field?(pair['key']) params[:custom_fields] ||= {} @@ -143,10 +154,10 @@ class CustomWizard::Action end params = add_custom_fields(params) - + if params.present? result = UserUpdater.new(Discourse.system_user, user).update(params) - + if params[:avatar].present? result = update_avatar(params[:avatar]) end @@ -256,13 +267,13 @@ class CustomWizard::Action def open_composer params = basic_topic_params - + if params[:title].present? && params[:raw].present? url = "/new-topic?title=#{params[:title]}" url += "&body=#{params[:raw]}" - + if category_id = action_category - if category_id && category = Category.find(category_id) + if category = Category.find_by(id: category_id) url += "&category=#{category.full_slug('/')}" end end @@ -272,7 +283,7 @@ class CustomWizard::Action end route_to = Discourse.base_uri + URI.encode(url) - data['redirect_on_complete'] = route_to + data['route_to'] = route_to log_info("route: #{route_to}") else @@ -289,8 +300,15 @@ class CustomWizard::Action multiple: true } ).perform + + group_map = group_map.flatten.compact + + unless group_map.present? + log_error("invalid group map") + return + end - groups = group_map.flatten.reduce([]) do |groups, g| + groups = group_map.reduce([]) do |groups, g| begin groups.push(Integer(g)) rescue ArgumentError @@ -330,7 +348,7 @@ class CustomWizard::Action user: user ).perform end - + if action['code'] data[action['code']] = SecureRandom.hex(8) url += "{action['code']}=#{data[action['code']]}" @@ -342,20 +360,36 @@ class CustomWizard::Action log_info("route: #{route_to}") end - def create_group + def create_group group = begin - Group.new(new_group_params) + Group.new(new_group_params.except(:usernames, :owner_usernames)) rescue ArgumentError => e raise Discourse::InvalidParameters, "Invalid group params" end if group.save - GroupActionLogger.new(user, group).log_change_group_settings + def get_user_ids(username_string) + User.where(username: username_string.split(",")).pluck(:id) + end + + if new_group_params[:owner_usernames].present? + owner_ids = get_user_ids(new_group_params[:owner_usernames]) + owner_ids.each { |user_id| group.group_users.build(user_id: user_id, owner: true) } + end + + if new_group_params[:usernames].present? + user_ids = get_user_ids(new_group_params[:usernames]) + user_ids -= owner_ids if owner_ids + user_ids.each { |user_id| group.group_users.build(user_id: user_id) } + end + + GroupActionLogger.new(user, group, skip_guardian: true).log_change_group_settings log_success("Group created", group.name) + result.output = group.name else - log_error("Group creation failed") + log_error("Group creation failed", group.errors.messages) end end @@ -372,7 +406,7 @@ class CustomWizard::Action log_success("Category created", category.name) result.output = category.id else - log_error("Category creation failed") + log_error("Category creation failed", category.errors.messages) end end @@ -385,6 +419,8 @@ class CustomWizard::Action user: user ).perform + return false unless output.present? + if output.is_a?(Array) output.first elsif output.is_a?(Integer) @@ -400,6 +436,8 @@ class CustomWizard::Action data: data, user: user, ).perform + + return false unless output.present? if output.is_a?(Array) output.flatten @@ -472,11 +510,11 @@ class CustomWizard::Action def public_topic_params params = {} - if (category = action_category) + if category = action_category params[:category] = category end - if (tags = action_tags) + if tags = action_tags params[:tags] = tags end @@ -528,10 +566,12 @@ class CustomWizard::Action user: user ).perform - value = value.parameterize(separator: '_') if attr === "name" - value = value.to_i if attr.include?("_level") - - params[attr.to_sym] = value + if value + value = value.parameterize(separator: '_') if attr === "name" + value = value.to_i if attr.include?("_level") + + params[attr.to_sym] = value + end end end @@ -556,30 +596,36 @@ class CustomWizard::Action user: user ).perform - if attr === "parent_category_id" && value.is_a?(Array) - value = value[0] - end - - if attr === "permissions" && value.is_a?(Array) - permissions = value - value = {} - - permissions.each do |p| - k = p[:key] - v = p[:value].to_i - - if k.is_a?(Array) - group = Group.find_by(id: k[0]) - k = group.name - else - k = k.parameterize(separator: '_') - end - - value[k] = v + if value + if attr === "parent_category_id" && value.is_a?(Array) + value = value[0] end + + if attr === "permissions" && value.is_a?(Array) + permissions = value + value = {} + + permissions.each do |p| + k = p[:key] + v = p[:value].to_i + + if k.is_a?(Array) + group = Group.find_by(id: k[0]) + k = group.name + else + k = k.parameterize(separator: '_') + end + + value[k] = v + end + end + + if attr === 'slug' + value = value.parameterize(separator: '-') + end + + params[attr.to_sym] = value end - - params[attr.to_sym] = value end end @@ -607,6 +653,8 @@ class CustomWizard::Action end def cast_profile_value(value, key) + return value if value.nil? + if profile_url_fields.include?(key) value['url'] elsif key === 'avatar' diff --git a/lib/custom_wizard/builder.rb b/lib/custom_wizard/builder.rb index 0af53245..66d2f5df 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,26 @@ class CustomWizard::Builder return nil if !SiteSetting.custom_wizard_enabled || !@wizard return @wizard if !@wizard.can_access? - reset_submissions if build_opts[:reset] + build_opts[:reset] = build_opts[:reset] || @wizard.restart_on_revisit @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 +77,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 @@ -133,7 +96,7 @@ class CustomWizard::Builder validate_field(field, updater, step_template) if field['type'] != 'text_only' end end - + next if updater.errors.any? CustomWizard::Builder.step_handlers.each do |handler| @@ -146,9 +109,7 @@ class CustomWizard::Builder data = updater.fields - ## if the wizard has data from the previous steps make that accessible to the actions. - if @submissions && @submissions.last && !@submissions.last.key?("submitted_at") - submission = @submissions.last + if submission = @wizard.current_submission data = submission.merge(data) end @@ -156,10 +117,10 @@ class CustomWizard::Builder if @actions.present? @actions.each do |action| - + if (action['run_after'] === updater.step.id) || (final_step && (!action['run_after'] || (action['run_after'] === 'wizard_completion'))) - + CustomWizard::Action.new( wizard: @wizard, action: action, @@ -169,29 +130,31 @@ class CustomWizard::Builder end end end - - if route_to = data['route_to'] - data.delete('route_to') - end - - if @wizard.save_submissions && updater.errors.empty? - save_submissions(data, final_step) - elsif final_step - PluginStore.remove("#{@wizard.id}_submissions", @wizard.user.id) - end - - if final_step && @wizard.id === @wizard.user.custom_fields['redirect_to_wizard'] - @wizard.user.custom_fields.delete('redirect_to_wizard'); - @wizard.user.save_custom_fields(true) - end - + if updater.errors.empty? + if route_to = data['route_to'] + data.delete('route_to') + end + + if @wizard.save_submissions + save_submissions(data, final_step) + end + if final_step + if @wizard.id == @wizard.user.custom_fields['redirect_to_wizard'] + @wizard.user.custom_fields.delete('redirect_to_wizard'); + @wizard.user.save_custom_fields(true) + end + redirect_url = route_to || data['redirect_on_complete'] || data["redirect_to"] updater.result[:redirect_on_complete] = redirect_url elsif route_to updater.result[:redirect_on_next] = route_to end + + true + else + false end end end @@ -211,15 +174,13 @@ 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'] - - ## Load previously submitted values - if !build_opts[:reset] && @submissions.last && !@submissions.last.key?("submitted_at") - submission = @submissions.last + params[:min_length] = field_template['min_length'] if field_template['min_length'] + params[:value] = prefill_field(field_template, step_template) + + if !build_opts[:reset] && (submission = @wizard.current_submission) 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 end @@ -339,7 +300,7 @@ class CustomWizard::Builder if type === 'time' && value.present? && !validate_time(value) updater.errors.add(id, I18n.t('wizard.field.invalid_time')) end - + CustomWizard::Builder.field_validators.each do |validator| if type === validator[:type] validator[:block].call(field, updater, step_template) @@ -395,13 +356,47 @@ class CustomWizard::Builder if data.present? @submissions.pop(1) if @wizard.unfinished? @submissions.push(data) - PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, @submissions) + @wizard.set_submissions(@submissions) end end + + def save_permitted_params(permitted_params, params) + permitted_data = {} - def reset_submissions - @submissions.pop(1) if @wizard.unfinished? - PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, @submissions) - @wizard.reset + 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/field.rb b/lib/custom_wizard/field.rb index 534e4824..cbb08e9c 100644 --- a/lib/custom_wizard/field.rb +++ b/lib/custom_wizard/field.rb @@ -57,10 +57,10 @@ class CustomWizard::Field @require_assets ||= {} end - def self.add_assets(type, plugin = nil, asset_paths = [], opts={}) + def self.register(type, plugin = nil, asset_paths = [], opts={}) if type - types[type] ||= {} - types[type] = opts[:type_opts] if opts[:type_opts].present? + types[type.to_sym] ||= {} + types[type.to_sym] = opts[:type_opts] if opts[:type_opts].present? end if plugin && asset_paths diff --git a/lib/custom_wizard/log.rb b/lib/custom_wizard/log.rb index 56430949..7dca2093 100644 --- a/lib/custom_wizard/log.rb +++ b/lib/custom_wizard/log.rb @@ -29,9 +29,12 @@ class CustomWizard::Log ").order("value::json->>'date' DESC") end - def self.list(page = 0) - self.list_query.limit(PAGE_LIMIT) - .offset(page * PAGE_LIMIT) + def self.list(page = 0, limit = nil) + limit = limit.to_i > 0 ? limit.to_i : PAGE_LIMIT + page = page.to_i + + self.list_query.limit(limit) + .offset(page * limit) .map { |r| self.new(JSON.parse(r.value)) } end end \ No newline at end of file diff --git a/lib/custom_wizard/mapper.rb b/lib/custom_wizard/mapper.rb index 72bf8396..d28daed9 100644 --- a/lib/custom_wizard/mapper.rb +++ b/lib/custom_wizard/mapper.rb @@ -48,7 +48,7 @@ class CustomWizard::Mapper inputs.each do |input| input_type = input['type'] pairs = input['pairs'] - + if (input_type === 'conditional' && validate_pairs(pairs)) || input_type === 'assignment' output = input['output'] output_type = input['output_type'] @@ -144,7 +144,7 @@ class CustomWizard::Mapper end if operator == '=~' - result == 0 ? true : false + result.nil? ? false : true else result end diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb new file mode 100644 index 00000000..fc79e91d --- /dev/null +++ b/lib/custom_wizard/template.rb @@ -0,0 +1,115 @@ +class CustomWizard::Template + include HasErrors + + attr_reader :data, + :opts + + def initialize(data) + @data = data + end + + def save(opts={}) + @opts = opts + + normalize_data + validate_data + prepare_data + + return false if errors.any? + + ActiveRecord::Base.transaction do + schedule_save_jobs unless opts[:skip_jobs] + PluginStore.set('custom_wizard', @data[:id], @data) + end + + @data[:id] + end + + def self.save(data, opts={}) + new(data).save(opts) + end + + def self.find(wizard_id) + PluginStore.get('custom_wizard', wizard_id) + end + + def self.remove(wizard_id) + wizard = CustomWizard::Wizard.create(wizard_id) + + ActiveRecord::Base.transaction do + PluginStore.remove('custom_wizard', wizard.id) + + if wizard.after_time + Jobs.cancel_scheduled_job(:set_after_time_wizard) + Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard_id) + end + end + end + + def self.exists?(wizard_id) + PluginStoreRow.exists?(plugin_name: 'custom_wizard', key: wizard_id) + end + + def self.list(setting: nil, order: :id) + query = "plugin_name = 'custom_wizard'" + query += "AND (value::json ->> '#{setting}')::boolean IS TRUE" if setting + + PluginStoreRow.where(query).order(order) + .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 + + private + + def normalize_data + @data = ::JSON.parse(@data) if @data.is_a?(String) + @data = @data.with_indifferent_access + end + + def prepare_data + @data[:steps].each do |step| + if step[:raw_description] + step[:description] = PrettyText.cook(step[:raw_description]) + end + end + end + + def validate_data + validator = CustomWizard::Validator.new(@data, @opts) + validator.perform + add_errors_from(validator) + end + + def schedule_save_jobs + if @data[:after_time] && @data[:after_time_scheduled] + wizard_id = @data[:id] + old_data = CustomWizard::Template.find(data[:id]) + + begin + enqueue_wizard_at = Time.parse(@data[:after_time_scheduled]).utc + rescue ArgumentError + errors.add :validation, I18n.t("wizard.validation.after_time") + raise ActiveRecord::Rollback.new + end + + if enqueue_wizard_at + Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard_id) + Jobs.enqueue_at(enqueue_wizard_at, :set_after_time_wizard, wizard_id: wizard_id) + elsif old_data && old_data[: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 + end +end \ No newline at end of file diff --git a/lib/custom_wizard/validator.rb b/lib/custom_wizard/validator.rb index 5b377584..e98023d0 100644 --- a/lib/custom_wizard/validator.rb +++ b/lib/custom_wizard/validator.rb @@ -1,52 +1,38 @@ class CustomWizard::Validator + include HasErrors - def initialize(params, opts={}) - @params = params + def initialize(data, opts={}) + @data = data @opts = opts - @error = nil end def perform - params = @params + data = @data - check_id(params, :wizard) - check_required(params, :wizard) - check_depdendent(params, :wizard) - - after_time = nil - - if !@error && @params[:after_time] - validate_after_time - end + check_id(data, :wizard) + check_required(data, :wizard) + validate_after_time - if !@error - params[:steps].each do |step| - check_required(step, :step) - check_depdendent(step, :step) - break if @error.present? - - if params[:fields].present? - params[:fields].each do |field| - check_required(field, :field) - check_depdendent(field, :field) - break if @error.present? - end - end - end + data[:steps].each do |step| + check_required(step, :step) - if params[:actions].present? - params[:actions].each do |action| - check_required(action, :action) - check_depdendent(action, :action) - break if @error.present? + if data[:fields].present? + data[:fields].each do |field| + check_required(field, :field) end end end + + if data[:actions].present? + data[:actions].each do |action| + check_required(action, :action) + end + end - if @error - { error: @error } + if errors.any? + false else - { wizard: params } + true end end @@ -59,54 +45,28 @@ class CustomWizard::Validator } end - def self.dependent - { - wizard: { - after_time: 'after_time_scheduled' - }, - step: {}, - field: {}, - action: {} - } - end - private def check_required(object, type) CustomWizard::Validator.required[type].each do |property| if object[property].blank? - @error = { - type: 'required', - params: { type: type, property: property } - } + errors.add :validation, I18n.t("wizard.validation.required", property: property) end end end - - def check_depdendent(object, type) - CustomWizard::Validator.dependent[type].each do |property, dependent| - if object[property] && object[dependent].blank? - @error = { - type: 'dependent', - params: { property: property, dependent: dependent } - } - end - end - end - + def check_id(object, type) - if type === :wizard && @opts[:create] && CustomWizard::Wizard.exists?(object[:id]) - @error = { - type: 'conflict', - params: { type: type, property: 'id', value: object[:id] } - } + if type === :wizard && @opts[:create] && CustomWizard::Template.exists?(object[:id]) + errors.add :validation, I18n.t("wizard.validation.conflict", id: object[:id]) end end - def validate_after_time - wizard = CustomWizard::Wizard.create(@params[:id]) if !@opts[:create] + def validate_after_time + return unless @data[:after_time] + + wizard = CustomWizard::Wizard.create(@data[:id]) if !@opts[:create] current_time = wizard.present? ? wizard.after_time_scheduled : nil - new_time = @params[:after_time_scheduled] + new_time = @data[:after_time_scheduled] begin active_time = Time.parse(new_time.present? ? new_time : current_time).utc @@ -115,7 +75,7 @@ class CustomWizard::Validator end if invalid_time || active_time.blank? || active_time < Time.now.utc - @error = { type: 'after_time' } + errors.add :validation, I18n.t("wizard.validation.after_time") end end end \ No newline at end of file diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index 04308a7f..96ae4e0e 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -24,23 +24,26 @@ class CustomWizard::Wizard :needs_categories, :needs_groups, :steps, + :step_ids, :actions, - :user + :user, + :first_step def initialize(attrs = {}, user=nil) @user = user + attrs = attrs.with_indifferent_access @id = attrs['id'] @name = attrs['name'] @background = attrs['background'] - @save_submissions = attrs['save_submissions'] || false - @multiple_submissions = attrs['multiple_submissions'] || false - @prompt_completion = attrs['prompt_completion'] || false - @restart_on_revisit = attrs['restart_on_revisit'] || false - @after_signup = attrs['after_signup'] - @after_time = attrs['after_time'] + @save_submissions = cast_bool(attrs['save_submissions']) + @multiple_submissions = cast_bool(attrs['multiple_submissions']) + @prompt_completion = cast_bool(attrs['prompt_completion']) + @restart_on_revisit = cast_bool(attrs['restart_on_revisit']) + @after_signup = cast_bool(attrs['after_signup']) + @after_time = cast_bool(attrs['after_time']) @after_time_scheduled = attrs['after_time_scheduled'] - @required = attrs['required'] || false + @required = cast_bool(attrs['required']) @permitted = attrs['permitted'] || nil @needs_categories = false @needs_groups = false @@ -53,8 +56,16 @@ class CustomWizard::Wizard @first_step = nil @steps = [] + if attrs['steps'].present? + @step_ids = attrs['steps'].map { |s| s['id'] } + end + @actions = [] end + + def cast_bool(val) + val.nil? ? false : ActiveRecord::Type::Boolean.new.cast(val) + end def create_step(step_name) ::Wizard::Step.new(step_name) @@ -65,12 +76,10 @@ class CustomWizard::Wizard yield step if block_given? - last_step = @steps.last - - @steps << step - - # If it's the first step - if @steps.size == 1 + last_step = steps.last + steps << step + + if steps.size == 1 @first_step = step step.index = 0 elsif last_step.present? @@ -81,67 +90,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 end end - def create_updater(step_id, fields) + def create_updater(step_id, inputs) step = @steps.find { |s| s.id == step_id } wizard = self - CustomWizard::StepUpdater.new(@user, wizard, step, fields) + CustomWizard::StepUpdater.new(user, wizard, step, inputs) 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? @@ -171,6 +175,7 @@ class CustomWizard::Wizard end def can_access? + return false unless user return true if user.admin return permitted? && (multiple_submissions || !completed?) end @@ -178,173 +183,89 @@ 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 current_submission + if submissions.present? && !submissions.last.key?("submitted_at") + submissions.last + else + nil + end + end + + def set_submissions(submissions) + PluginStore.set("#{id}_submissions", user.id, Array.wrap(submissions)) + end + + def self.submissions(wizard_id, user) + new({ id: wizard_id }, user).submissions + end + + def self.set_submissions(wizard_id, user, submissions) + new({ id: wizard_id }, user).set_submissions(submissions) + end + + def self.create(wizard_id, user = nil) + if template = CustomWizard::Template.find(wizard_id) + new(template.to_h, user) + else + false + end + end + + def self.list(user, template_opts: {}) + return [] unless user + + CustomWizard::Template.list(template_opts).reduce([]) do |result, template| + wizard = new(template, user) + result.push(wizard) if wizard.can_access? + result + end end def self.after_signup(user) - if (records = filter_records('after_signup')).any? - result = false - - records - .sort_by { |record| record.value['permitted'].present? ? 0 : 1 } - .each do |record| - wizard = self.new(JSON.parse(record.value), user) - - if wizard.permitted? - result = wizard - break - end - end - - result - else - false - end + wizards = list( + user, + template_opts: { + setting: 'after_signup', + order: "(value::json ->> 'permitted') IS NOT NULL DESC" + } + ) + wizards.any? ? wizards.first : false end def self.prompt_completion(user) - if (records = filter_records('prompt_completion')).any? - records.reduce([]) do |result, record| - wizard = CustomWizard::Wizard.new(::JSON.parse(record.value), user) - - if wizard.permitted? && !wizard.completed? - result.push(id: wizard.id, name: wizard.name) - end - - result - end - else - false - end - end - - def self.restart_on_revisit - if (records = filter_records('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]) + wizards = list( + user, + template_opts: { + setting: 'prompt_completion', + order: "(value::json ->> 'permitted') IS NOT NULL DESC" + } + ) - wizard[:steps].each do |step| - if step[:raw_description] - step[:description] = PrettyText.cook(step[:raw_description]) + if wizards.any? + wizards.map do |w| + { + id: w.id, + name: w.name + } 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 @@ -364,12 +285,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 82ee9855..48537e28 100644 --- a/plugin.rb +++ b/plugin.rb @@ -42,10 +42,10 @@ after_initialize do ../controllers/custom_wizard/admin/submissions.rb ../controllers/custom_wizard/admin/api.rb ../controllers/custom_wizard/admin/logs.rb + ../controllers/custom_wizard/admin/transfer.rb ../controllers/custom_wizard/admin/custom_fields.rb ../controllers/custom_wizard/wizard.rb ../controllers/custom_wizard/steps.rb - ../controllers/custom_wizard/transfer.rb ../jobs/clear_after_time_wizard.rb ../jobs/refresh_api_access_token.rb ../jobs/set_after_time_wizard.rb @@ -57,6 +57,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 @@ -130,8 +131,7 @@ after_initialize do if request.referer !~ /\/w\// && request.referer !~ /\/invites\// CustomWizard::Wizard.set_submission_redirect(current_user, wizard_id, request.referer) end - - if CustomWizard::Wizard.exists?(wizard_id) + if CustomWizard::Template.exists?(wizard_id) redirect_to "/w/#{wizard_id.dasherize}" end end @@ -161,8 +161,12 @@ 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 + CustomWizard::CustomField::CLASSES.each do |klass| add_model_callback(klass.to_sym, :after_initialize) do CustomWizard::CustomField.list_by('klass', klass).each do |field| diff --git a/spec/components/custom_wizard/action_spec.rb b/spec/components/custom_wizard/action_spec.rb index 6437c8c4..890dde12 100644 --- a/spec/components/custom_wizard/action_spec.rb +++ b/spec/components/custom_wizard/action_spec.rb @@ -1,92 +1,162 @@ +require 'rails_helper' + describe CustomWizard::Action do - let(:create_topic_action) {{"id":"create_topic","type":"create_topic","title":"text","post":"textarea"}} - let(:send_message_action) {{"id":"send_message","type":"send_message","title":"text","post":"textarea","username":"angus"}} - let(:route_to_action) {{"id":"route_to","type":"route_to","url":"https://google.com"}} - let(:open_composer_action) {{"id":"open_composer","type":"open_composer","title":"text","post":"textarea"}} - let(:add_to_group_action) {{"id":"add_to_group","type":"add_to_group","group_id":"dropdown_groups"}} + fab!(:user) { Fabricate(:user, name: "Angus", username: 'angus', email: "angus@email.com", trust_level: TrustLevel[2]) } + fab!(:category) { Fabricate(:category, name: 'cat1', slug: 'cat-slug') } + fab!(:group) { Fabricate(:group) } - it 'creates a topic' do - template['steps'][0]['fields'] = [text_field, textarea_field] - template['steps'][0]["actions"] = [create_topic_action] - updater = run_update(template, nil, - text: "Topic Title", - textarea: "topic body" - ) - topic = Topic.where(title: "Topic Title") + before do + Group.refresh_automatic_group!(:trust_level_2) + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + @template = CustomWizard::Template.find('super_mega_fun_wizard') + end + + context "creating a topic" do + + end + + context 'creating a topic' do + it "works" do + wizard = CustomWizard::Builder.new(@template[:id], user).build + wizard.create_updater( + wizard.steps.first.id, + step_1_field_1: "Topic Title", + step_1_field_2: "topic body" + ).update + wizard.create_updater(wizard.steps.second.id, {}).update + wizard.create_updater(wizard.steps.last.id, + step_3_field_3: category.id + ).update + + topic = Topic.where( + title: "Topic Title", + category_id: category.id + ) + expect(topic.exists?).to eq(true) + expect(Post.where( + topic_id: topic.pluck(:id), + raw: "topic body" + ).exists?).to eq(true) + end - expect(topic.exists?).to eq(true) - expect(Post.where( - topic_id: topic.pluck(:id), - raw: "topic body" - ).exists?).to eq(true) + it "fails silently without basic topic inputs" do + wizard = CustomWizard::Builder.new(@template[:id], user).build + wizard.create_updater( + wizard.steps.first.id, + step_1_field_2: "topic body" + ).update + wizard.create_updater(wizard.steps.second.id, {}).update + updater = wizard.create_updater(wizard.steps.last.id, {}) + updater.update + + expect(updater.success?).to eq(true) + expect(UserHistory.where( + acting_user_id: user.id, + context: "super_mega_fun_wizard", + subject: "step_3" + ).exists?).to eq(true) + expect(Post.where( + raw: "topic body" + ).exists?).to eq(false) + end end it 'sends a message' do - fields = [text_field, textarea_field] + User.create(username: 'angus1', email: "angus1@email.com") - if extra_field - fields.push(extra_field) - end - - template['steps'][0]['fields'] = fields - template['steps'][0]["actions"] = [send_message_action.merge(extra_action_opts)] - - run_update(template, nil, - text: "Message Title", - textarea: "message body" - ) + 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, - title: "Message Title" + title: "Message title" ) - expect(topic.exists?).to eq(true) - expect( - topic.first.topic_allowed_users.first.user.username - ).to eq('angus') - expect(Post.where( + post = Post.where( topic_id: topic.pluck(:id), - raw: "message body" - ).exists?).to eq(true) + raw: "I will interpolate some wizard fields" + ) + + expect(topic.exists?).to eq(true) + expect(topic.first.topic_allowed_users.first.user.username).to eq('angus1') + expect(post.exists?).to eq(true) end it 'updates a profile' do - run_update(template, template['steps'][1]['id'], name: "Sally") - expect(user.name).to eq('Sally') + 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 = 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 - template['steps'][0]['fields'] = [text_field, textarea_field] - template['steps'][0]["actions"] = [open_composer_action] + wizard = CustomWizard::Builder.new(@template[:id], user).build + wizard.create_updater(wizard.steps[0].id, step_1_field_1: "Text input").update - updater = run_update(template, nil, - text: "Topic Title", - textarea: "topic body" - ) + updater = wizard.create_updater(wizard.steps[1].id, {}) + updater.update - expect(updater.result.blank?).to eq(true) + category = Category.find_by(id: wizard.current_submission['action_8']) - updater = run_update(template, template['steps'][1]['id']) - - expect(updater.result[:redirect_on_complete]).to eq( - "/new-topic?title=Topic%20Title&body=topic%20body" + expect(updater.result[:redirect_on_next]).to eq( + "/new-topic?title=Title%20of%20the%20composer%20topic&body=I%20am%20interpolating%20some%20user%20fields%20Angus%20angus%20angus@email.com&category=#{category.slug}/#{category.id}&tags=tag1" ) end - it 'adds a user to a group' do - template['steps'][0]['fields'] = [dropdown_groups_field] - template['steps'][0]["actions"] = [add_to_group_action] - - updater = run_update(template, nil, dropdown_groups: group.id) + it 'creates a category' do + 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 + expect(Category.where(id: wizard.current_submission['action_8']).exists?).to eq(true) + end + + it 'creates a group' do + 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 + expect(Group.where(name: wizard.current_submission['action_9']).exists?).to eq(true) + end + + it 'adds a user to a group' do + 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 + group = Group.find_by(name: wizard.current_submission['action_9']) expect(group.users.first.username).to eq('angus') end + it 'watches categories' do + 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 + expect(CategoryUser.where( + category_id: wizard.current_submission['action_8'], + user_id: user.id + ).first.notification_level).to eq(2) + expect(CategoryUser.where( + category_id: category.id, + user_id: user.id + ).first.notification_level).to eq(0) + end + it 're-routes a user' do - template['steps'][0]["actions"] = [route_to_action] - updater = run_update(template, nil, {}) - expect(updater.result[:redirect_on_next]).to eq( - "https://google.com" - ) + 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 end diff --git a/spec/components/custom_wizard/api_spec.rb b/spec/components/custom_wizard/api_spec.rb deleted file mode 100644 index a7a8ba1b..00000000 --- a/spec/components/custom_wizard/api_spec.rb +++ /dev/null @@ -1,21 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -describe CustomWizard::Api do - context 'authorization' do - it 'authorizes with an oauth2 api' do - - end - - it 'refreshes the api access token' do - - end - end - - context 'endpoint' do - it 'requests an api endpoint' do - - end - end -end diff --git a/spec/components/custom_wizard/builder_spec.rb b/spec/components/custom_wizard/builder_spec.rb index ba8da001..699f5ce9 100644 --- a/spec/components/custom_wizard/builder_spec.rb +++ b/spec/components/custom_wizard/builder_spec.rb @@ -3,39 +3,45 @@ require 'rails_helper' describe CustomWizard::Builder do - fab!(:user) { Fabricate(:user, username: 'angus') } - fab!(:trusted_user) { Fabricate(:user, trust_level: 3) } + 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) } - let!(:template) do + let(:required_data_json) { JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/required_data.json" ).read) - end + } - def build_wizard(t = template, u = user, build_opts = {}, params = {}) - CustomWizard::Wizard.add_wizard(t) - CustomWizard::Builder.new('welcome', u).build(build_opts, params) - end + let(:permitted_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read) + } - def add_submission_data(data = {}) - PluginStore.set("welcome_submissions", user.id, { - name: 'Angus', - website: 'https://thepavilion.io' - }.merge(data)) - end + let(:permitted_param_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/permitted_params.json" + ).read) + } - def get_submission_data - PluginStore.get("welcome_submissions", user.id) - end - - def run_update(t = template, step_id = nil, data = {}) - wizard = build_wizard(t) - updater = wizard.create_updater(step_id || t['steps'][0]['id'], data) - updater.update - updater + before do + Group.refresh_automatic_group!(:trust_level_3) + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + @template = CustomWizard::Template.find('super_mega_fun_wizard') end context 'disabled' do @@ -43,15 +49,10 @@ describe CustomWizard::Builder do SiteSetting.custom_wizard_enabled = false end - it "returns no steps" do - wizard = build_wizard - expect(wizard.steps.length).to eq(0) - expect(wizard.name).to eq('Welcome') - end - - it "doesn't save submissions" do - run_update(template, nil, name: 'Angus') - expect(get_submission_data.blank?).to eq(true) + it "returns nil" do + expect( + CustomWizard::Builder.new(@template[:id], user).build + ).to eq(nil) end end @@ -60,124 +61,288 @@ 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(build_wizard.steps.length).to eq(2) + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.length + ).to eq(3) end - it 'returns no steps if multiple submissions are disabled and user has completed' do - history_params = { - action: UserHistory.actions[:custom_wizard_step], - acting_user_id: user.id, - context: template['id'] - } - UserHistory.create!(history_params.merge(subject: template['steps'][0]['id'])) - UserHistory.create!(history_params.merge(subject: template['steps'][1]['id'])) + context "with multiple submissions disabled" do + before do + @template[:multiple_submissions] = false + CustomWizard::Template.save(@template.as_json) + end - template["multiple_submissions"] = false - expect(build_wizard(template).steps.length).to eq(0) + 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 '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(@template[:id], user).build + .steps.length + ).to eq(0) + end end - it 'returns no steps if user is not permitted' do - template["min_trust"] = 3 - expect(build_wizard(template).steps.length).to eq(0) + 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 'user cannot access if not permitted' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .can_access? + ).to eq(false) + end + + it 'returns wizard metadata if user is not permitted' do + expect( + 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 'user can access if permitted' do + expect( + CustomWizard::Builder.new(@template[:id], trusted_user).build + .can_access? + ).to eq(true) + end + + 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 steps if user is permitted' do - template["min_trust"] = 3 - expect(build_wizard(template, trusted_user).steps.length).to eq(2) + it 'returns prefilled data' do + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.first + .fields.first + .value + ).to eq('I am prefilled') end - it 'returns a wizard with prefilled data if user has partially completed it' do - add_submission_data - wizard = build_wizard - expect(wizard.steps[0].fields.first.value).to eq('Angus') - expect(wizard.steps[1].fields.first.value).to eq('https://thepavilion.io') + context "user has partially completed" do + before do + wizard = CustomWizard::Wizard.new(@template, user) + wizard.set_submissions(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 - - it 'returns a wizard with no prefilled data if options include reset' do - add_submission_data - wizard = build_wizard(template, user, reset: true) - expect(wizard.steps[0].fields.first.value).to eq(nil) - expect(wizard.steps[1].fields.first.value).to eq(nil) - end - - context 'building steps' do + + context 'building step' do it 'returns step metadata' do - expect(build_wizard.steps[0].title).to eq('Welcome to Pavilion') - expect(build_wizard.steps[1].title).to eq('Tell us about you') + 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 - it 'saves permitted params' do - template['steps'][0]['permitted_params'] = permitted_params - wizard = build_wizard(template, user, {}, param_key: 'param_value') - submissions = get_submission_data - expect(submissions.first['submission_param_key']).to eq('param_value') + 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 + CustomWizard::Wizard.set_submissions('super_mega_fun_wizard', user, + required_data: "required_value" + ) + expect( + CustomWizard::Builder.new(@template[:id], user).build + .steps.first + .permitted + ).to eq(true) + end end - it 'is not permitted if required data is not present' do - template['steps'][0]['required_data'] = required_data - expect(build_wizard(template, user).steps[0].permitted).to eq(false) + 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 + wizard = CustomWizard::Builder.new(@template[:id], user).build({}, + param: 'param_value' + ) + expect(wizard.current_submission['saved_param']).to eq('param_value') + end end - - it 'it shows required data message if required data has message' do - template['steps'][0]['required_data'] = required_data - template['steps'][0]['required_data_message'] = required_data_message - add_submission_data(nickname: "John") - wizard = build_wizard(template, user) - expect(wizard.steps[0].permitted).to eq(false) - expect(wizard.steps[0].permitted_message).to eq(required_data_message) - end - - it 'is permitted if required data is present' do - template['steps'][0]['required_data'] = required_data - PluginStore.set('welcome_submissions', user.id, nickname: "Angus", name: "Angus") - expect(build_wizard(template, user).steps[0].permitted).to eq(true) - end - + end + + context 'building field' do it 'returns field metadata' do - expect(build_wizard(template, user).steps[0].fields[0].label).to eq("Name
") - expect(build_wizard(template, user).steps[0].fields[0].type).to eq("text") + 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 fields' do - template['steps'][0]['fields'][1] = checkbox_field - expect(build_wizard(template, user).steps[0].fields.length).to eq(2) + 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 - run_update(template, nil, name: 'Angus') - expect(get_submission_data.first['name']).to eq('Angus') + perform_update('step_1', step_1_field_1: 'Text input') + expect( + CustomWizard::Wizard.submissions(@template[:id], user) + .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( + CustomWizard::Wizard.submissions(@template[:id], user).first + ).to eq(nil) + end end context 'validation' do it 'applies min length' do - template['steps'][0]['fields'][0]['min_length'] = 10 - updater = run_update(template, nil, name: 'short') - expect(updater.errors.messages[:name].first).to eq( - I18n.t('wizard.field.too_short', label: 'Name', min: 10) - ) + 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 - template['steps'][0]['fields'][0] = checkbox_field - run_update(template, nil, checkbox: 'false') - expect(get_submission_data.first['checkbox']).to eq(false) + perform_update('step_2', step_2_field_5: 'false') + expect( + CustomWizard::Wizard.submissions(@template[:id], user) + .first['step_2_field_5'] + ).to eq(false) end it 'requires required fields' do - template['steps'][0]['fields'][0]['required'] = true - expect(run_update(template).errors.messages[:name].first).to eq( - I18n.t('wizard.field.required', label: 'Name') - ) + @template[:steps][0][:fields][1][:required] = true + CustomWizard::Template.save(@template.as_json) + + expect( + 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 - - it 'runs actions attached to a step' do - run_update(template, template['steps'][1]['id'], name: "Gus") - expect(user.name).to eq('Gus') - end end end end \ No newline at end of file diff --git a/spec/components/custom_wizard/field_spec.rb b/spec/components/custom_wizard/field_spec.rb new file mode 100644 index 00000000..d6d6e61a --- /dev/null +++ b/spec/components/custom_wizard/field_spec.rb @@ -0,0 +1,30 @@ +require 'rails_helper' + +describe CustomWizard::Field do + before do + CustomWizard::Field.register( + 'location', + 'discourse-locations', + ['components', 'helpers', 'lib', 'stylesheets', 'templates'], + type_opts: { + prefill: { "coordinates": [35.3082, 149.1244] } + } + ) + end + + it "registers custom field types" do + expect(CustomWizard::Field.types[:location].present?).to eq(true) + end + + it "allows custom field types to set default attributes" do + expect( + CustomWizard::Field.types[:location][:prefill] + ).to eq({ "coordinates": [35.3082, 149.1244] }) + end + + it "registers custom field assets" do + expect( + CustomWizard::Field.require_assets['discourse-locations'] + ).to eq(['components', 'helpers', 'lib', 'stylesheets', 'templates']) + end +end \ No newline at end of file diff --git a/spec/components/custom_wizard/log_spec.rb b/spec/components/custom_wizard/log_spec.rb new file mode 100644 index 00000000..9c8b570d --- /dev/null +++ b/spec/components/custom_wizard/log_spec.rb @@ -0,0 +1,27 @@ +require 'rails_helper' + +describe CustomWizard::Log do + before do + CustomWizard::Log.create("First log message") + CustomWizard::Log.create("Second log message") + CustomWizard::Log.create("Third log message") + end + + it "creates logs" do + expect( + CustomWizard::Log.list.length + ).to eq(3) + end + + it "lists logs by time created" do + expect( + CustomWizard::Log.list.first.message + ).to eq("Third log message") + end + + it "paginates logs" do + expect( + CustomWizard::Log.list(0, 2).length + ).to eq(2) + end +end \ No newline at end of file diff --git a/spec/components/custom_wizard/mapper_spec.rb b/spec/components/custom_wizard/mapper_spec.rb index 4fb8a10e..4ddbee57 100644 --- a/spec/components/custom_wizard/mapper_spec.rb +++ b/spec/components/custom_wizard/mapper_spec.rb @@ -1,14 +1,250 @@ +require 'rails_helper' + describe CustomWizard::Mapper do - -it 'interpolates user data' do - user.name = "Angus" - user.save! - - expect( - CustomWizard::Builder.fill_placeholders( - "My name is u{name}", - user, - {} + fab!(:user1) { + Fabricate(:user, + name: "Angus", + username: "angus", + email: "angus@email.com", + trust_level: TrustLevel[3] ) - ).to eq('My name is Angus') + } + fab!(:user2) { + Fabricate(:user, + name: "Patrick", + username: "patrick", + email: "patrick@email2.com", + trust_level: TrustLevel[1] + ) + } + fab!(:user_field) { + field = Fabricate(:user_field, + id: 3, + name: 'dropdown_field', + description: 'field desc', + field_type: 'dropdown', + user_field_options_attributes: [ + { value: "a" }, + { value: "b" }, + { value: "c" } + ] + ) + } + let(:inputs) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/mapper/inputs.json" + ).read) + } + let(:data) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/mapper/data.json" + ).read) + } + + it "maps values" do + expect(CustomWizard::Mapper.new( + inputs: inputs['assignment'], + data: data, + user: user1 + ).perform).to eq([13]) + end + + it "maps associations" do + association = CustomWizard::Mapper.new( + inputs: inputs['association'], + data: data, + user: user1 + ).perform + expect(association.length).to eq(3) + expect(association.first[:value]).to eq("Choice 1") + end + + context "conditional mapping" do + it "maps when the condition is met" do + expect(CustomWizard::Mapper.new( + inputs: inputs['conditional'], + data: data, + user: user1 + ).perform).to eq("true") + end + + it "does not map when the condition is not met" do + expect(CustomWizard::Mapper.new( + inputs: inputs['conditional'], + data: data, + user: user2 + ).perform).to eq(nil) + end + + it "maps when multiple conditions are met" do + expect(CustomWizard::Mapper.new( + inputs: inputs['conditional_multiple_pairs'], + data: data, + user: user1 + ).perform).to eq("true") + end + + it "does not map when one of multiple conditions are not met" do + user1.email = "angus@other-email.com" + expect(CustomWizard::Mapper.new( + inputs: inputs['conditional_multiple_pairs'], + data: data, + user: user1 + ).perform).to eq(nil) + end + end + + it "validates valid data" do + expect(CustomWizard::Mapper.new( + inputs: inputs['validation'], + data: data, + user: user1 + ).perform).to eq(true) + end + + it "does not validate invalid data" do + data["input_2"] = "value 3" + expect(CustomWizard::Mapper.new( + inputs: inputs['validation'], + data: data, + user: user1 + ).perform).to eq(false) + end + + it "maps text fields" do + expect(CustomWizard::Mapper.new( + inputs: inputs['assignment_text'], + data: data, + user: user1 + ).perform).to eq("Value") + end + + it "maps user fields" do + expect(CustomWizard::Mapper.new( + inputs: inputs['assignment_user_field'], + data: data, + user: user1 + ).perform).to eq("Angus") + end + + it "maps user field options" do + expect(CustomWizard::Mapper.new( + inputs: inputs['assignment_user_field_options'], + data: data, + user: user1 + ).perform).to eq(["a", "b", "c"]) + end + + it "maps wizard fields" do + expect(CustomWizard::Mapper.new( + inputs: inputs['assignment_wizard_field'], + data: data, + user: user1 + ).perform).to eq("value 1") + end + + it "maps wizard actions" do + expect(CustomWizard::Mapper.new( + inputs: inputs['assignment_wizard_action'], + data: data, + user: user1 + ).perform).to eq("value 2") + end + + it "interpolates user fields" do + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_user_field'], + data: data, + user: user1 + ).perform).to eq("Name: Angus") + end + + it "interpolates wizard fields" do + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_wizard_field'], + data: data, + user: user1 + ).perform).to eq("Input 1: value 1") + end + + it "interpolates date" do + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_timestamp'], + data: data, + user: user1 + ).perform).to eq("Time: #{Time.now.strftime("%B %-d, %Y")}") + end + + it "handles greater than pairs" do + expect(CustomWizard::Mapper.new( + inputs: inputs['greater_than_pair'], + data: data, + user: user1 + ).perform).to eq(true) + expect(CustomWizard::Mapper.new( + inputs: inputs['greater_than_pair'], + data: data, + user: user2 + ).perform).to eq(false) + end + + it "handles less than pairs" do + expect(CustomWizard::Mapper.new( + inputs: inputs['less_than_pair'], + data: data, + user: user1 + ).perform).to eq(false) + expect(CustomWizard::Mapper.new( + inputs: inputs['less_than_pair'], + data: data, + user: user2 + ).perform).to eq(true) + end + + it "handles greater than or equal pairs" do + expect(CustomWizard::Mapper.new( + inputs: inputs['greater_than_or_equal_pair'], + data: data, + user: user1 + ).perform).to eq(true) + expect(CustomWizard::Mapper.new( + inputs: inputs['greater_than_or_equal_pair'], + data: data, + user: user2 + ).perform).to eq(true) + end + + it "handles less than or equal pairs" do + expect(CustomWizard::Mapper.new( + inputs: inputs['less_than_or_equal_pair'], + data: data, + user: user1 + ).perform).to eq(true) + expect(CustomWizard::Mapper.new( + inputs: inputs['less_than_or_equal_pair'], + data: data, + user: user2 + ).perform).to eq(true) + end + + it "handles regex pairs" do + expect(CustomWizard::Mapper.new( + inputs: inputs['regex_pair'], + data: data, + user: user1 + ).perform).to eq(true) + expect(CustomWizard::Mapper.new( + inputs: inputs['regex_pair'], + data: data, + user: user2 + ).perform).to eq(false) + end + + it "handles shorthand pairs" do + expect(CustomWizard::Mapper.new( + inputs: inputs['shorthand_pair'], + data: data, + user: user1 + ).perform).to eq(false) + end end \ No newline at end of file diff --git a/spec/components/custom_wizard/template_spec.rb b/spec/components/custom_wizard/template_spec.rb new file mode 100644 index 00000000..594009a8 --- /dev/null +++ b/spec/components/custom_wizard/template_spec.rb @@ -0,0 +1,111 @@ +require 'rails_helper' + +describe CustomWizard::Template do + fab!(:user) { Fabricate(:user) } + + let(:template_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + } + let(:permitted_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read) + } + let(:after_time) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/after_time.json" + ).read).with_indifferent_access + } + + before do + CustomWizard::Template.save(template_json, skip_jobs: true) + end + + it "saves wizard templates" do + expect( + PluginStoreRow.exists?( + plugin_name: 'custom_wizard', + key: 'super_mega_fun_wizard' + ) + ).to eq(true) + end + + it "finds wizard templates" do + expect( + CustomWizard::Template.find('super_mega_fun_wizard')['id'] + ).to eq('super_mega_fun_wizard') + end + + it "removes wizard templates" do + CustomWizard::Template.remove('super_mega_fun_wizard') + expect( + CustomWizard::Template.find('super_mega_fun_wizard') + ).to eq(nil) + end + + it "checks for wizard template existence" do + expect( + CustomWizard::Template.exists?('super_mega_fun_wizard') + ).to eq(true) + end + + context "wizard template list" do + before do + template_json_2 = template_json.dup + template_json_2["id"] = 'super_mega_fun_wizard_2' + template_json_2["permitted"] = permitted_json['permitted'] + CustomWizard::Template.save(template_json_2, skip_jobs: true) + + template_json_3 = template_json.dup + template_json_3["id"] = 'super_mega_fun_wizard_3' + template_json_3["after_signup"] = true + CustomWizard::Template.save(template_json_3, skip_jobs: true) + end + + it "works" do + expect( + CustomWizard::Template.list.length + ).to eq(3) + end + + it "can be filtered by wizard settings" do + expect( + CustomWizard::Template.list(setting: "after_signup").length + ).to eq(1) + end + + it "can be ordered" do + expect( + CustomWizard::Template.list( + order: "(value::json ->> 'permitted') IS NOT NULL DESC" + ).first['id'] + ).to eq('super_mega_fun_wizard_2') + end + end + + context "after time setting" do + before do + freeze_time Time.now + @after_time_template = template_json.dup + @after_time_template["after_time"] = after_time['after_time'] + @after_time_template["after_time_scheduled"] = after_time['after_time_scheduled'] + end + + it 'if enabled queues jobs after wizard is saved' do + expect_enqueued_with(job: :set_after_time_wizard, at: Time.parse(after_time['after_time_scheduled']).utc) do + CustomWizard::Template.save(@after_time_template) + end + end + + it 'if disabled clears jobs after wizard is saved' do + CustomWizard::Template.save(@after_time_template) + @after_time_template['after_time'] = false + + expect_not_enqueued_with(job: :set_after_time_wizard) do + CustomWizard::Template.save(@after_time_template) + end + end + end +end \ No newline at end of file diff --git a/spec/components/custom_wizard/validator_spec.rb b/spec/components/custom_wizard/validator_spec.rb new file mode 100644 index 00000000..5833127e --- /dev/null +++ b/spec/components/custom_wizard/validator_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe CustomWizard::Validator do + fab!(:user) { Fabricate(:user) } + + let(:template) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read).with_indifferent_access + } + + let(:after_time) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/after_time.json" + ).read).with_indifferent_access + } + + it "validates valid templates" do + expect( + CustomWizard::Validator.new(template).perform + ).to eq(true) + end + + it "invalidates templates without required attributes" do + template.delete(:id) + expect( + CustomWizard::Validator.new(template).perform + ).to eq(false) + end + + it "invalidates templates with duplicate ids if creating a new template" do + CustomWizard::Template.save(template) + expect( + CustomWizard::Validator.new(template, create: true).perform + ).to eq(false) + end + + it "validates after time settings" do + template[:after_time] = after_time[:after_time] + template[:after_time_scheduled] = after_time[:after_time_scheduled] + expect( + CustomWizard::Validator.new(template).perform + ).to eq(true) + end + + it "invalidates invalid after time settings" do + template[:after_time] = after_time[:after_time] + template[:after_time_scheduled] = "not a time" + expect( + CustomWizard::Validator.new(template).perform + ).to eq(false) + end +end \ No newline at end of file diff --git a/spec/components/custom_wizard/wizard_spec.rb b/spec/components/custom_wizard/wizard_spec.rb new file mode 100644 index 00000000..7724cc64 --- /dev/null +++ b/spec/components/custom_wizard/wizard_spec.rb @@ -0,0 +1,230 @@ +require 'rails_helper' + +describe CustomWizard::Wizard do + fab!(:user) { Fabricate(:user) } + fab!(:trusted_user) { Fabricate(:user, trust_level: TrustLevel[3])} + fab!(:admin_user) { Fabricate(:user, admin: true)} + + let(:template_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + } + + let(:after_time) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/after_time.json" + ).read) + } + + let(:permitted_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read) + } + + before do + Group.refresh_automatic_group!(:trust_level_3) + + @permitted_template = template_json.dup + @permitted_template["permitted"] = permitted_json["permitted"] + + @wizard = CustomWizard::Wizard.new(template_json, user) + template_json['steps'].each do |step_template| + @wizard.append_step(step_template['id']) + end + end + + def progress_step(step_id, acting_user = user) + UserHistory.create( + action: UserHistory.actions[:custom_wizard_step], + acting_user_id: acting_user.id, + context: @wizard.id, + subject: step_id + ) + end + + it "appends steps from a template" do + expect(@wizard.steps.length).to eq(3) + end + + it "determines the user's current step" do + expect(@wizard.start.id).to eq('step_1') + progress_step('step_1') + expect(@wizard.start.id).to eq('step_2') + end + + it "creates a step updater" do + expect( + @wizard.create_updater('step_1', step_1_field_1: "Text input") + .class + ).to eq(CustomWizard::StepUpdater) + end + + it "determines whether a wizard is unfinished" do + expect(@wizard.unfinished?).to eq(true) + progress_step("step_1") + expect(@wizard.unfinished?).to eq(true) + progress_step("step_2") + expect(@wizard.unfinished?).to eq(true) + progress_step("step_3") + expect(@wizard.unfinished?).to eq(false) + end + + it "determines whether a wizard has been completed by a user" do + expect(@wizard.completed?).to eq(false) + progress_step("step_1") + progress_step("step_2") + progress_step("step_3") + expect(@wizard.completed?).to eq(true) + end + + it "is not completed if steps submitted before after time" do + progress_step("step_1") + progress_step("step_2") + progress_step("step_3") + + template_json['after_time'] = after_time['after_time'] + template_json['after_time_scheduled'] = after_time['after_time_scheduled'] + + wizard = CustomWizard::Wizard.new(template_json, user) + + expect(wizard.completed?).to eq(false) + end + + it "permits admins" do + expect( + CustomWizard::Wizard.new(@permitted_template, admin_user).permitted? + ).to eq(true) + end + + it "permits permitted users" do + expect( + CustomWizard::Wizard.new(@permitted_template, trusted_user).permitted? + ).to eq(true) + end + + it "does not permit unpermitted users" do + expect( + CustomWizard::Wizard.new(@permitted_template, user).permitted? + ).to eq(false) + end + + it "does not let an unpermitted user access a wizard" do + expect( + CustomWizard::Wizard.new(@permitted_template, user).can_access? + ).to eq(false) + end + + it "lets a permitted user access an incomplete wizard" do + expect( + CustomWizard::Wizard.new(@permitted_template, trusted_user).can_access? + ).to eq(true) + end + + it "lets a permitted user access a complete wizard with multiple submissions" do + progress_step("step_1", trusted_user) + progress_step("step_2", trusted_user) + progress_step("step_3", trusted_user) + + expect( + CustomWizard::Wizard.new(@permitted_template, trusted_user).can_access? + ).to eq(true) + end + + it "does not let an unpermitted user access a complete wizard without multiple submissions" do + progress_step("step_1", trusted_user) + progress_step("step_2", trusted_user) + progress_step("step_3", trusted_user) + + @permitted_template['multiple_submissions'] = false + + expect( + CustomWizard::Wizard.new(@permitted_template, trusted_user).can_access? + ).to eq(false) + end + + it "lists the site groups" do + expect(@wizard.groups.length).to eq(8) + end + + it "lists the site categories" do + expect(@wizard.categories.length).to eq(1) + end + + context "submissions" do + before do + @wizard.set_submissions(step_1_field_1: 'I am a user submission') + end + + it "sets the user's submission" do + expect( + PluginStore.get("#{template_json['id']}_submissions", user.id) + .first['step_1_field_1'] + ).to eq('I am a user submission') + end + + it "lists the user's submissions" do + expect(@wizard.submissions.length).to eq(1) + end + + it "returns the user's current submission" do + expect(@wizard.current_submission['step_1_field_1']).to eq('I am a user submission') + end + end + + it "provides class methods to set and list submissions" do + CustomWizard::Wizard.set_submissions(template_json['id'], user, + step_1_field_1: 'I am a user submission' + ) + expect( + CustomWizard::Wizard.submissions(template_json['id'], user) + .first['step_1_field_1'] + ).to eq('I am a user submission') + end + + context do + before do + CustomWizard::Template.save(@permitted_template, skip_jobs: true) + + template_json_2 = template_json.dup + template_json_2["id"] = 'super_mega_fun_wizard_2' + template_json_2["prompt_completion"] = true + CustomWizard::Template.save(template_json_2, skip_jobs: true) + + template_json_3 = template_json.dup + template_json_3["id"] = 'super_mega_fun_wizard_3' + template_json_3["after_signup"] = true + CustomWizard::Template.save(template_json_3, skip_jobs: true) + end + + it "lists wizards the user can see" do + expect(CustomWizard::Wizard.list(user).length).to eq(2) + expect(CustomWizard::Wizard.list(trusted_user).length).to eq(3) + end + + it "returns the first after signup wizard" do + expect(CustomWizard::Wizard.after_signup(user).id).to eq('super_mega_fun_wizard_3') + end + + it "lists prompt completion wizards" do + expect(CustomWizard::Wizard.prompt_completion(user).length).to eq(2) + end + end + + it "sets wizard redirects if user is permitted" do + CustomWizard::Template.save(@permitted_template, skip_jobs: true) + CustomWizard::Wizard.set_wizard_redirect('super_mega_fun_wizard', trusted_user) + expect( + trusted_user.custom_fields['redirect_to_wizard'] + ).to eq("super_mega_fun_wizard") + end + + it "does not set a wizard redirect if user is not permitted" do + CustomWizard::Template.save(@permitted_template, skip_jobs: true) + CustomWizard::Wizard.set_wizard_redirect('super_mega_fun_wizard', user) + expect( + trusted_user.custom_fields['redirect_to_wizard'] + ).to eq(nil) + end +end \ No newline at end of file diff --git a/spec/extensions/extra_locales_controller_spec.rb b/spec/extensions/extra_locales_controller_spec.rb new file mode 100644 index 00000000..bac0f2c6 --- /dev/null +++ b/spec/extensions/extra_locales_controller_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe ExtraLocalesControllerCustomWizard, type: :request do + before do + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + end + + before do + @controller = ExtraLocalesController.new + end + + it "returns locales when requested by wizard" do + expect( + ExtraLocalesController.url("wizard") + ).to eq( + "#{Discourse.base_path}/extra-locales/wizard?v=#{ExtraLocalesController.bundle_js_hash("wizard")}" + ) + end +end \ No newline at end of file diff --git a/spec/extensions/invites_controller_spec.rb b/spec/extensions/invites_controller_spec.rb new file mode 100644 index 00000000..05fa58a5 --- /dev/null +++ b/spec/extensions/invites_controller_spec.rb @@ -0,0 +1,24 @@ +require 'rails_helper' + +describe InvitesControllerCustomWizard, type: :request do + fab!(:topic) { Fabricate(:topic) } + let(:invite) do + Invite.invite_by_email("angus@email.com", topic.user, topic) + end + let(:template) do + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + end + + before do + @controller = InvitesController.new + end + + it "redirects a user to wizard after invite if after signup is enabled" do + template['after_signup'] = true + CustomWizard::Template.save(template, skip_jobs: true) + put "/invites/show/#{invite.invite_key}.json" + expect(response.parsed_body["redirect_to"]).to eq("/w/super-mega-fun-wizard") + end +end \ No newline at end of file diff --git a/spec/extensions/users_controller_spec.rb b/spec/extensions/users_controller_spec.rb new file mode 100644 index 00000000..5a219dee --- /dev/null +++ b/spec/extensions/users_controller_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe CustomWizardUsersController, type: :request do + let(:template) do + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + end + + before do + @controller = UsersController.new + end + + it "redirects a user to wizard after sign up if after signup is enabled" do + template['after_signup'] = true + CustomWizard::Template.save(template, skip_jobs: true) + sign_in(Fabricate(:user)) + get "/u/account-created" + expect(response).to redirect_to("/w/super-mega-fun-wizard") + end +end \ No newline at end of file diff --git a/spec/extensions/wizard_field_spec.rb b/spec/extensions/wizard_field_spec.rb new file mode 100644 index 00000000..d13b3c80 --- /dev/null +++ b/spec/extensions/wizard_field_spec.rb @@ -0,0 +1,21 @@ +require 'rails_helper' + +describe CustomWizardFieldExtension do + let(:field_hash) do + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/field/field.json" + ).read).with_indifferent_access + end + + it "adds custom field attributes" do + field = Wizard::Field.new(field_hash) + expect(field.id).to eq("field_id") + expect(field.label).to eq("Field Label
") + expect(field.image).to eq("field_image_url.png") + expect(field.description).to eq("Field description") + expect(field.required).to eq(true) + expect(field.key).to eq("field.locale.key") + expect(field.type).to eq("field_type") + expect(field.content).to eq([]) + end +end \ No newline at end of file diff --git a/spec/extensions/wizard_step_spec.rb b/spec/extensions/wizard_step_spec.rb new file mode 100644 index 00000000..7d1fbb68 --- /dev/null +++ b/spec/extensions/wizard_step_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe CustomWizardStepExtension do + let(:step_hash) do + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/step.json" + ).read).with_indifferent_access + end + + it "adds custom step attributes" do + step = Wizard::Step.new(step_hash[:id]) + [ + :title, + :description, + :key, + :permitted, + :permitted_message + ].each do |attr| + step.send("#{attr.to_s}=", step_hash[attr]) + expect(step.send(attr)).to eq(step_hash[attr]) + end + end +end \ No newline at end of file diff --git a/spec/fixtures/field/field.json b/spec/fixtures/field/field.json new file mode 100644 index 00000000..c2de266d --- /dev/null +++ b/spec/fixtures/field/field.json @@ -0,0 +1,10 @@ +{ + "id": "field_id", + "label": "Field Label", + "image": "field_image_url.png", + "description": "Field description", + "required": true, + "key": "field.locale.key", + "type": "field_type", + "content": [] +} \ No newline at end of file diff --git a/spec/fixtures/mapper/data.json b/spec/fixtures/mapper/data.json new file mode 100644 index 00000000..eca75a1a --- /dev/null +++ b/spec/fixtures/mapper/data.json @@ -0,0 +1,4 @@ +{ + "input_1": "value 1", + "input_2": "value 2" +} \ No newline at end of file diff --git a/spec/fixtures/mapper/inputs.json b/spec/fixtures/mapper/inputs.json new file mode 100644 index 00000000..8faf2435 --- /dev/null +++ b/spec/fixtures/mapper/inputs.json @@ -0,0 +1,264 @@ +{ + "assignment": [ + { + "type": "assignment", + "output_type": "group", + "output_connector": "set", + "output": [ + 13 + ] + } + ], + "assignment_text": [ + { + "type": "assignment", + "output_type": "text", + "output_connector": "set", + "output": "Value" + } + ], + "assignment_user_field": [ + { + "type": "assignment", + "output_type": "user_field", + "output_connector": "set", + "output": "name" + } + ], + "assignment_user_field_options": [ + { + "type": "assignment", + "output_type": "user_field_options", + "output_connector": "set", + "output": "user_field_3" + } + ], + "assignment_wizard_field": [ + { + "type": "assignment", + "output_type": "wizard_field", + "output_connector": "set", + "output": "input_1" + } + ], + "assignment_wizard_action": [ + { + "type": "assignment", + "output_type": "wizard_action", + "output_connector": "set", + "output": "input_2" + } + ], + "interpolate_user_field": [ + { + "type": "assignment", + "output_type": "text", + "output_connector": "set", + "output": "Name: u{name}" + } + ], + "interpolate_wizard_field": [ + { + "type": "assignment", + "output_type": "text", + "output_connector": "set", + "output": "Input 1: w{input_1}" + } + ], + "interpolate_timestamp": [ + { + "type": "assignment", + "output_type": "text", + "output_connector": "set", + "output": "Time: v{time}" + } + ], + "validation": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "input_1", + "key_type": "wizard_field", + "value": "value 1", + "value_type": "text", + "connector": "equal" + }, + { + "index": 1, + "key": "input_2", + "key_type": "wizard_field", + "value": "value 2", + "value_type": "text", + "connector": "equal" + } + ] + } + ], + "association": [ + { + "type": "association", + "pairs": [ + { + "index": 0, + "key": "choice1", + "key_type": "text", + "value": "Choice 1", + "value_type": "text", + "connector": "equal" + }, + { + "index": 1, + "key": "choice2", + "key_type": "text", + "value": "Choice 2", + "value_type": "text", + "connector": "association" + }, + { + "index": 2, + "key": "choice3", + "key_type": "text", + "value": "Choice 3", + "value_type": "text", + "connector": "association" + } + ] + } + ], + "conditional": [ + { + "type": "conditional", + "output": "true", + "output_type": "text", + "output_connector": "then", + "pairs": [ + { + "index": 0, + "key": "name", + "key_type": "user_field", + "value": "Angus", + "value_type": "text", + "connector": "equal" + } + ] + } + ], + "conditional_multiple_pairs": [ + { + "type": "conditional", + "output": "true", + "output_type": "text", + "output_connector": "then", + "pairs": [ + { + "index": 0, + "key": "name", + "key_type": "user_field", + "value": "Angus", + "value_type": "text", + "connector": "equal" + }, + { + "index": 1, + "key": "email", + "key_type": "user_field", + "value": "angus@email.com", + "value_type": "text", + "connector": "equal" + } + ] + } + ], + "greater_than_pair": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "trust_level", + "key_type": "user_field", + "value": "2", + "value_type": "text", + "connector": "greater" + } + ] + } + ], + "less_than_pair": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "trust_level", + "key_type": "user_field", + "value": "2", + "value_type": "text", + "connector": "less" + } + ] + } + ], + "greater_than_or_equal_pair": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "trust_level", + "key_type": "user_field", + "value": "1", + "value_type": "text", + "connector": "greater_or_equal" + } + ] + } + ], + "less_than_or_equal_pair": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "trust_level", + "key_type": "user_field", + "value": "3", + "value_type": "text", + "connector": "less_or_equal" + } + ] + } + ], + "regex_pair": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "email", + "key_type": "user_field", + "value": "@email.com", + "value_type": "text", + "connector": "regex" + } + ] + } + ], + "shorthand_pair": [ + { + "type": "validation", + "pairs": [ + { + "index": 0, + "key": "bio_raw", + "key_type": "user_field", + "value": "present", + "value_type": "text", + "connector": "is" + } + ] + } + ] +} \ No newline at end of file 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/step/required_data.json b/spec/fixtures/step/required_data.json new file mode 100644 index 00000000..9f65d516 --- /dev/null +++ b/spec/fixtures/step/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/fixtures/step/step.json b/spec/fixtures/step/step.json new file mode 100644 index 00000000..5c6c5f58 --- /dev/null +++ b/spec/fixtures/step/step.json @@ -0,0 +1,10 @@ +{ + "id": "step_1", + "title": "Text", + "description": "Step description", + "image": "step_image_url.png", + "key": "step.locale.key", + "fields": [], + "required_data": [], + "permitted": [] +} \ No newline at end of file diff --git a/spec/fixtures/wizard.json b/spec/fixtures/wizard.json index a9251f65..287f3b60 100644 --- a/spec/fixtures/wizard.json +++ b/spec/fixtures/wizard.json @@ -1,49 +1,516 @@ { - "id": "welcome", - "name": "Welcome", - "background": "#006da3", + "id": "super_mega_fun_wizard", + "name": "Super Mega Fun Wizard", + "background": "#333333", "save_submissions": true, "multiple_submissions": true, - "after_signup": true, - "min_trust": 1, - "theme_id": 4, + "after_signup": false, + "prompt_completion": true, + "theme_id": 2, "steps": [ { - "id": "welcome", - "title": "Welcome to Pavilion", - "raw_description": "Hey there, thanks for signing up.\n\nWe're Pavilion, an international freelancer cooperative that specialises in online communities.\n\nThis site is our own community, where we work with our clients, users of our open source work and our broader community.\n\n", - "description": "Hey there, thanks for signing up.
\nWe’re Pavilion, an international freelancer cooperative that specialises in online communities.
\nThis site is our own community, where we work with our clients, users of our open source work and our broader community.
", + "id": "step_1", + "title": "Text", + "raw_description": "Text inputs!", "fields": [ { - "id": "name", + "id": "step_1_field_1", + "label": "Text", + "description": "Text field description.", "type": "text", - "label": "Name" + "min_length": "3", + "prefill": [ + { + "type": "assignment", + "output": "I am prefilled", + "output_type": "text", + "output_connector": "set" + } + ] + }, + { + "id": "step_1_field_2", + "label": "Textarea", + "type": "textarea", + "min_length": "" + }, + { + "id": "step_1_field_3", + "label": "Composer", + "type": "composer" + }, + { + "id": "step_1_field_4", + "label": "I'm only text", + "description": "", + "type": "text_only" + } + ], + "description": "Text inputs!
" + }, + { + "id": "step_2", + "title": "Values", + "raw_description": "Because I couldn't think of another name for this step :)", + "fields": [ + { + "id": "step_2_field_1", + "label": "Date", + "type": "date", + "format": "YYYY-MM-DD" + }, + { + "id": "step_2_field_2", + "label": "Time", + "type": "time", + "format": "HH:mm" + }, + { + "id": "step_2_field_3", + "label": "Date & Time", + "type": "date_time", + "format": "" + }, + { + "id": "step_2_field_4", + "label": "Number", + "type": "number" + }, + { + "id": "step_2_field_5", + "label": "Checkbox", + "type": "checkbox" + }, + { + "id": "step_2_field_7", + "label": "Upload", + "type": "upload", + "file_types": ".jpg,.png" + } + ], + "description": "Because I couldn’t think of another name for this step
" + }, + { + "id": "step_3", + "title": "Combo-boxes", + "raw_description": "Unfortunately not the edible type :sushi: ", + "fields": [ + { + "id": "step_3_field_1", + "label": "Custom Dropdown", + "type": "dropdown", + "content": [ + { + "type": "association", + "pairs": [ + { + "index": 0, + "key": "choice1", + "key_type": "text", + "value": "Choice 1", + "value_type": "text", + "connector": "equal" + }, + { + "index": 1, + "key": "choice2", + "key_type": "text", + "value": "Choice 2", + "value_type": "text", + "connector": "association" + }, + { + "index": 2, + "key": "choice3", + "key_type": "text", + "value": "Choice 3", + "value_type": "text", + "connector": "association" + } + ] + } + ] + }, + { + "id": "step_3_field_2", + "label": "Tag", + "type": "tag" + }, + { + "id": "step_3_field_3", + "label": "Category", + "type": "category", + "limit": 1, + "property": "id" + }, + { + "id": "step_3_field_4", + "label": "Group", + "type": "group" + }, + { + "id": "step_3_field_5", + "label": "User Selector", + "description": "", + "type": "user_selector" + } + ], + "description": "Unfortunately not the edible type
" + } + ], + "actions": [ + { + "id": "action_9", + "run_after": "step_1", + "type": "create_group", + "title": [ + { + "type": "assignment", + "output": "New Group Member", + "output_type": "text", + "output_connector": "set" + } + ], + "custom_fields": [ + { + "type": "association", + "pairs": [ + { + "index": 0, + "key": "group_custom_field", + "key_type": "text", + "value": "step_3_field_1", + "value_type": "wizard_field", + "connector": "association" + } + ] + } + ], + "name": [ + { + "type": "assignment", + "output": "step_1_field_1", + "output_type": "wizard_field", + "output_connector": "set" + } + ], + "full_name": [ + { + "type": "assignment", + "output": "step_1_field_1", + "output_type": "wizard_field", + "output_connector": "set" + } + ], + "usernames": [ + { + "type": "assignment", + "output_type": "user", + "output_connector": "set", + "output": [ + "angus1" + ] + } + ], + "owner_usernames": [ + { + "type": "assignment", + "output_type": "user", + "output_connector": "set", + "output": [ + "angus" + ] + } + ], + "grant_trust_level": [ + { + "type": "assignment", + "output": "3", + "output_type": "text", + "output_connector": "set" + } + ], + "mentionable_level": [ + { + "type": "assignment", + "output": "1", + "output_type": "text", + "output_connector": "set" + } + ], + "messageable_level": [ + { + "type": "assignment", + "output": "2", + "output_type": "text", + "output_connector": "set" + } + ], + "visibility_level": [ + { + "type": "assignment", + "output": "3", + "output_type": "text", + "output_connector": "set" + } + ], + "members_visibility_level": [ + { + "type": "assignment", + "output": "99", + "output_type": "text", + "output_connector": "set" } ] }, { - "id": "about_you", - "title": "Tell us about you", - "raw_description": "We'd like to know a little more about you. Add a your name and your website below. This will update your user profile.", - "description": "We’d like to know a little more about you. Add a your name and your website below. This will update your user profile.
", - "fields": [ + "id": "action_6", + "run_after": "step_1", + "type": "add_to_group", + "group": [ { - "id": "website", - "label": "Website", - "type": "text" + "type": "assignment", + "output": "action_9", + "output_type": "wizard_action", + "output_connector": "set" } - ], - "actions": [ + ] + }, + { + "id": "action_8", + "run_after": "step_1", + "type": "create_category", + "custom_fields": [ { - "id": "update_profile", - "type": "update_profile", - "profile_updates": [ + "type": "association", + "pairs": [ { - "key": "name", - "value": "name" + "index": 0, + "key": "category_custom_field", + "key_type": "text", + "value": "CC Val", + "value_type": "text", + "connector": "association" } ] } + ], + "name": [ + { + "type": "assignment", + "output": "step_1_field_1", + "output_type": "wizard_field", + "output_connector": "set" + } + ], + "slug": [ + { + "type": "assignment", + "output": "step_1_field_1", + "output_type": "wizard_field", + "output_connector": "set" + } + ], + "permissions": [ + { + "type": "association", + "pairs": [ + { + "index": 0, + "key": "action_9", + "key_type": "wizard_action", + "value": "2", + "value_type": "text", + "connector": "association" + } + ] + } + ] + }, + { + "id": "action_5", + "run_after": "step_1", + "type": "watch_categories", + "notification_level": "tracking", + "wizard_user": true, + "categories": [ + { + "type": "assignment", + "output": "action_8", + "output_type": "wizard_action", + "output_connector": "set" + } + ], + "mute_remainder": [ + { + "type": "assignment", + "output": "true", + "output_type": "text", + "output_connector": "set" + } + ] + }, + { + "id": "action_1", + "run_after": "step_3", + "type": "create_topic", + "skip_redirect": true, + "post": "step_1_field_2", + "title": [ + { + "type": "assignment", + "output": "step_1_field_1", + "output_type": "wizard_field", + "output_connector": "set" + } + ], + "category": [ + { + "type": "assignment", + "output": "step_3_field_3", + "output_type": "wizard_field", + "output_connector": "set" + } + ], + "tags": [ + { + "type": "assignment", + "output": "step_3_field_2", + "output_type": "wizard_field", + "output_connector": "set" + } + ], + "custom_fields": [ + { + "type": "association", + "pairs": [ + { + "index": 0, + "key": "custom_field_1", + "key_type": "text", + "value": "title", + "value_type": "user_field", + "connector": "association" + } + ] + } + ], + "visible": [ + { + "type": "conditional", + "output": "true", + "output_type": "text", + "output_connector": "then", + "pairs": [ + { + "index": 0, + "key": "name", + "key_type": "user_field", + "value": "Angus", + "value_type": "text", + "connector": "equal" + } + ] + } + ] + }, + { + "id": "action_4", + "run_after": "step_2", + "type": "update_profile", + "profile_updates": [ + { + "type": "association", + "pairs": [ + { + "index": 0, + "key": "profile_background", + "key_type": "user_field", + "value": "step_2_field_7", + "value_type": "wizard_field", + "connector": "association" + } + ] + } + ] + }, + { + "id": "action_2", + "run_after": "step_2", + "type": "send_message", + "post_builder": true, + "post_template": "I will interpolate some wizard fields w{step_1_field_1} w{step_1_field_2}", + "title": [ + { + "type": "assignment", + "output": "Message title", + "output_type": "text", + "output_connector": "set" + } + ], + "recipient": [ + { + "type": "assignment", + "output_type": "user", + "output_connector": "set", + "output": [ + "angus1" + ] + } + ] + }, + { + "id": "action_3", + "run_after": "step_2", + "type": "open_composer", + "post_builder": true, + "post_template": "I am interpolating some user fields u{name} u{username} u{email}", + "title": [ + { + "type": "assignment", + "output": "Title of the composer topic", + "output_type": "text", + "output_connector": "set" + } + ], + "category": [ + { + "type": "assignment", + "output": "action_8", + "output_type": "wizard_action", + "output_connector": "set", + "pairs": [ + { + "index": 0, + "key": "step_2_field_5", + "key_type": "wizard_field", + "value": "true", + "value_type": "text", + "connector": "is" + } + ] + } + ], + "tags": [ + { + "type": "assignment", + "output": "tag1", + "output_type": "text", + "output_connector": "set" + } + ] + }, + { + "id": "action_10", + "run_after": "wizard_completion", + "type": "route_to", + "url": [ + { + "type": "assignment", + "output": "https://google.com", + "output_type": "text", + "output_connector": "set" + } ] } ] diff --git a/spec/fixtures/wizard/after_time.json b/spec/fixtures/wizard/after_time.json new file mode 100644 index 00000000..691af470 --- /dev/null +++ b/spec/fixtures/wizard/after_time.json @@ -0,0 +1,4 @@ +{ + "after_time": true, + "after_time_scheduled": "2020-11-20T00:59:00.000Z" +} \ No newline at end of file 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/jobs/clear_after_time_wizard_spec.rb b/spec/jobs/clear_after_time_wizard_spec.rb new file mode 100644 index 00000000..b6d675fe --- /dev/null +++ b/spec/jobs/clear_after_time_wizard_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Jobs::ClearAfterTimeWizard do + fab!(:user1) { Fabricate(:user) } + fab!(:user2) { Fabricate(:user) } + fab!(:user3) { Fabricate(:user) } + + let(:template) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read).with_indifferent_access + } + let(:after_time) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/after_time.json" + ).read).with_indifferent_access + } + + it "clears wizard redirect for all users " do + after_time_template = template.dup + after_time_template["after_time"] = after_time['after_time'] + after_time_template["after_time_scheduled"] = after_time['after_time_scheduled'] + + CustomWizard::Template.save(after_time_template) + + described_class.new.execute(wizard_id: 'super_mega_fun_wizard') + + expect( + UserCustomField.where(" + name = 'redirect_to_wizard' AND + value = 'super_mega_fun_wizard' + ").exists? + ).to eq(false) + end +end \ No newline at end of file diff --git a/spec/jobs/set_after_time_wizard_spec.rb b/spec/jobs/set_after_time_wizard_spec.rb new file mode 100644 index 00000000..49ec211f --- /dev/null +++ b/spec/jobs/set_after_time_wizard_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe Jobs::SetAfterTimeWizard do + fab!(:user1) { Fabricate(:user) } + fab!(:user2) { Fabricate(:user) } + fab!(:user3) { Fabricate(:user) } + + let(:template) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read).with_indifferent_access + } + let(:after_time) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/after_time.json" + ).read).with_indifferent_access + } + + it "sets wizard redirect for all users " do + after_time_template = template.dup + after_time_template["after_time"] = after_time['after_time'] + after_time_template["after_time_scheduled"] = after_time['after_time_scheduled'] + + CustomWizard::Template.save(after_time_template) + + messages = MessageBus.track_publish("/redirect_to_wizard") do + described_class.new.execute(wizard_id: 'super_mega_fun_wizard') + end + + expect( + UserCustomField.where( + name: 'redirect_to_wizard', + value: 'super_mega_fun_wizard' + ).length + ).to eq(3) + + expect(messages.first.data).to eq("super_mega_fun_wizard") + expect(messages.first.user_ids).to match_array([user1.id,user2.id,user3.id]) + end +end \ No newline at end of file diff --git a/spec/plugin_helper.rb b/spec/plugin_helper.rb index 47368da5..55deb6a2 100644 --- a/spec/plugin_helper.rb +++ b/spec/plugin_helper.rb @@ -3,6 +3,8 @@ require 'simplecov' SimpleCov.configure do add_filter do |src| src.filename !~ /discourse-custom-wizard/ || - src.filename =~ /spec/ + src.filename =~ /spec/ || + src.filename =~ /db/ || + src.filename =~ /api/ ## API features are currently experimental end end \ No newline at end of file diff --git a/spec/requests/custom_wizard/admin/logs_controller_spec.rb b/spec/requests/custom_wizard/admin/logs_controller_spec.rb new file mode 100644 index 00000000..37a83e90 --- /dev/null +++ b/spec/requests/custom_wizard/admin/logs_controller_spec.rb @@ -0,0 +1,22 @@ +require 'rails_helper' + +describe CustomWizard::AdminLogsController do + fab!(:admin_user) { Fabricate(:user, admin: true) } + + before do + CustomWizard::Log.create("First log message") + CustomWizard::Log.create("Second log message") + CustomWizard::Log.create("Third log message") + sign_in(admin_user) + end + + it "returns a list of logs" do + get "/admin/wizards/logs.json" + expect(response.parsed_body.length).to eq(3) + end + + it "paginates" do + get "/admin/wizards/logs.json", params: { page: 1, limit: 2 } + expect(response.parsed_body.length).to eq(1) + end +end \ No newline at end of file diff --git a/spec/requests/custom_wizard/admin/submissions_controller_spec.rb b/spec/requests/custom_wizard/admin/submissions_controller_spec.rb new file mode 100644 index 00000000..51628ea2 --- /dev/null +++ b/spec/requests/custom_wizard/admin/submissions_controller_spec.rb @@ -0,0 +1,45 @@ +require 'rails_helper' + +describe CustomWizard::AdminSubmissionsController do + fab!(:admin_user) {Fabricate(:user, admin: true)} + fab!(:user1) {Fabricate(:user)} + fab!(:user2) {Fabricate(:user)} + + let(:template) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + } + + before do + CustomWizard::Template.save(template, skip_jobs: true) + CustomWizard::Wizard.set_submissions(template['id'], user1, + step_1_field_1: "I am a user1's submission" + ) + CustomWizard::Wizard.set_submissions(template['id'], user2, + step_1_field_1: "I am a user2's submission" + ) + sign_in(admin_user) + end + + it "returns a basic list of wizards" do + get "/admin/wizards/submissions.json" + expect(response.parsed_body.length).to eq(1) + expect(response.parsed_body.first['id']).to eq(template['id']) + end + + it "returns the all user's submissions for a wizard" do + get "/admin/wizards/submissions/#{template['id']}.json" + expect(response.parsed_body['submissions'].length).to eq(2) + end + + it "returns the all user's submissions for a wizard" do + get "/admin/wizards/submissions/#{template['id']}.json" + expect(response.parsed_body['submissions'].length).to eq(2) + end + + it "downloads all user submissions" do + get "/admin/wizards/submissions/#{template['id']}/download" + expect(response.parsed_body.length).to eq(2) + end +end \ No newline at end of file diff --git a/spec/requests/custom_wizard/admin/transfer_controller_spec.rb b/spec/requests/custom_wizard/admin/transfer_controller_spec.rb new file mode 100644 index 00000000..217311c2 --- /dev/null +++ b/spec/requests/custom_wizard/admin/transfer_controller_spec.rb @@ -0,0 +1,52 @@ +require 'rails_helper' + +describe CustomWizard::AdminTransferController do + fab!(:admin_user) { Fabricate(:user, admin: true) } + + let(:template) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + } + + before do + sign_in(admin_user) + + CustomWizard::Template.save(template, skip_jobs: true) + + template_2 = template.dup + template_2["id"] = 'super_mega_fun_wizard_2' + CustomWizard::Template.save(template_2, skip_jobs: true) + + template_3 = template.dup + template_3["id"] = 'super_mega_fun_wizard_3' + template_3["after_signup"] = true + CustomWizard::Template.save(template_3, skip_jobs: true) + + @template_array = [template, template_2, template_3] + + FileUtils.mkdir_p(file_from_fixtures_tmp_folder) unless Dir.exists?(file_from_fixtures_tmp_folder) + @tmp_file_path = File.join(file_from_fixtures_tmp_folder, SecureRandom.hex << 'wizards.json') + File.write(@tmp_file_path, @template_array.to_json) + end + + it 'exports all the wizard templates' do + get '/admin/wizards/transfer/export.json', params: { + wizards: [ + 'super_mega_fun_wizard', + 'super_mega_fun_wizard_2', + 'super_mega_fun_wizard_3' + ] + } + expect(response.status).to eq(200) + expect(response.parsed_body).to match_array(@template_array) + end + + it 'imports wizard a template' do + post '/admin/wizards/transfer/import.json', params: { + file: fixture_file_upload(File.open(@tmp_file_path)) + } + expect(response.status).to eq(200) + expect(response.parsed_body['success']).to eq(@template_array.map { |t| t['id'] }) + end +end \ No newline at end of file diff --git a/spec/requests/custom_wizard/admin/wizard_controller_spec.rb b/spec/requests/custom_wizard/admin/wizard_controller_spec.rb new file mode 100644 index 00000000..d33762e4 --- /dev/null +++ b/spec/requests/custom_wizard/admin/wizard_controller_spec.rb @@ -0,0 +1,66 @@ +require 'rails_helper' + +describe CustomWizard::AdminWizardController do + fab!(:admin_user) {Fabricate(:user, admin: true)} + fab!(:user1) {Fabricate(:user)} + fab!(:user2) {Fabricate(:user)} + + let(:template) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + } + + before do + CustomWizard::Template.save(template, skip_jobs: true) + + template_2 = template.dup + template_2["id"] = 'super_mega_fun_wizard_2' + template_2["permitted"] = template_2['permitted'] + CustomWizard::Template.save(template_2, skip_jobs: true) + + template_3 = template.dup + template_3["id"] = 'super_mega_fun_wizard_3' + template_3["after_signup"] = true + CustomWizard::Template.save(template_3, skip_jobs: true) + + sign_in(admin_user) + end + + it "returns a basic list of wizard templates and wizard field types" do + get "/admin/wizards/wizard.json" + expect( + response.parsed_body['wizard_list'].map { |w| w['id'] } + ).to match_array(['super_mega_fun_wizard', 'super_mega_fun_wizard_2', 'super_mega_fun_wizard_3']) + expect( + response.parsed_body['field_types'].keys + ).to eq(CustomWizard::Field.types.keys.map(&:to_s)) + end + + it "returns a wizard template" do + get "/admin/wizards/wizard/#{template['id']}.json" + expect(response.parsed_body['id']).to eq(template['id']) + expect(response.parsed_body['steps'].length).to eq(3) + end + + it "removes wizard templates" do + delete "/admin/wizards/wizard/#{template['id']}.json" + expect(response.status).to eq(200) + expect(CustomWizard::Template.exists?(template['id'])).to eq(false) + end + + it "saves wizard templates" do + template_updated = template.dup + template_updated['name'] = "Super Mega Fun Wizard 2" + template_updated['multiple_submissions'] = false + template_updated['steps'][0]['fields'][0]['label'] = "Text 2" + + put "/admin/wizards/wizard/#{template['id']}.json", params: { wizard: template_updated } + expect(response.status).to eq(200) + + updated_template = CustomWizard::Template.find('super_mega_fun_wizard') + expect(updated_template['name']).to eq("Super Mega Fun Wizard 2") + expect(updated_template['multiple_submissions']).to eq("false") + expect(updated_template['steps'][0]['fields'][0]['label']).to eq("Text 2") + end +end \ No newline at end of file diff --git a/spec/requests/custom_wizard/admin_controller_spec.rb b/spec/requests/custom_wizard/admin_controller_spec.rb deleted file mode 100644 index 731d0de5..00000000 --- a/spec/requests/custom_wizard/admin_controller_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -describe CustomWizard::AdminController do - -end \ No newline at end of file diff --git a/spec/requests/custom_wizard/application_controller_spec.rb b/spec/requests/custom_wizard/application_controller_spec.rb index 1315748d..4c67cc41 100644 --- a/spec/requests/custom_wizard/application_controller_spec.rb +++ b/spec/requests/custom_wizard/application_controller_spec.rb @@ -1,5 +1,61 @@ require 'rails_helper' describe ApplicationController do + fab!(:user) { + Fabricate( + :user, + username: 'angus', + email: "angus@email.com", + trust_level: TrustLevel[3] + ) + } + before do + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + @template = CustomWizard::Template.find('super_mega_fun_wizard') + end + + context "with signed in user" do + before do + sign_in(user) + end + + context "who is required to complete wizard" do + before do + user.custom_fields['redirect_to_wizard'] = 'super_mega_fun_wizard' + user.save_custom_fields(true) + end + + it "redirects if user is required to complete a wizard" do + get "/" + expect(response).to redirect_to("/w/super-mega-fun-wizard") + end + + it "saves original destination of user" do + get '/', headers: { 'REFERER' => "/t/2" } + expect( + CustomWizard::Wizard.submissions(@template['id'], user) + .first['redirect_to'] + ).to eq("/t/2") + end + end + + context "who is not required to complete wizard" do + it "does nothing" do + get "/" + expect(response.status).to eq(200) + end + end + end + + context "with guest" do + it "does nothing" do + get "/" + expect(response.status).to eq(200) + end + end end \ No newline at end of file diff --git a/spec/requests/custom_wizard/steps_controller_spec.rb b/spec/requests/custom_wizard/steps_controller_spec.rb new file mode 100644 index 00000000..eeea6c17 --- /dev/null +++ b/spec/requests/custom_wizard/steps_controller_spec.rb @@ -0,0 +1,34 @@ +require 'rails_helper' + +describe CustomWizard::StepsController do + fab!(:user) { + Fabricate( + :user, + username: 'angus', + email: "angus@email.com", + trust_level: TrustLevel[3] + ) + } + + before do + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + sign_in(user) + end + + it 'performs a step update' do + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Text input" + } + } + expect(response.status).to eq(200) + + wizard = CustomWizard::Builder.new("super_mega_fun_wizard", user).build + expect(wizard.current_submission['step_1_field_1']).to eq("Text input") + expect(wizard.start.id).to eq("step_2") + end +end \ No newline at end of file diff --git a/spec/requests/custom_wizard/wizard_controller_spec.rb b/spec/requests/custom_wizard/wizard_controller_spec.rb index b457eba7..a9833381 100644 --- a/spec/requests/custom_wizard/wizard_controller_spec.rb +++ b/spec/requests/custom_wizard/wizard_controller_spec.rb @@ -1,31 +1,63 @@ require 'rails_helper' describe CustomWizard::WizardController do - it 'returns a wizard if enabled' do - + fab!(:user) { + Fabricate( + :user, + username: 'angus', + email: "angus@email.com", + trust_level: TrustLevel[3] + ) + } + + before do + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + @template = CustomWizard::Template.find("super_mega_fun_wizard") + sign_in(user) end - it 'returns a disabled message if disabled' do - - end - - it 'returns a missing message if no wizard exists' do - - end - - it 'returns a custom wizard theme' do - - end - - it 'updates the page title' do - - end - - it 'skips a wizard if user is allowed to skip' do + context 'plugin disabled' do + before do + SiteSetting.custom_wizard_enabled = false + end + it 'redirects to root' do + get '/w/super-mega-fun-wizard', xhr: true + expect(response).to redirect_to("/") + end + end + + it 'returns wizard' do + get '/w/super-mega-fun-wizard.json' + expect(response.parsed_body["id"]).to eq("super_mega_fun_wizard") + end + + it 'returns missing message if no wizard exists' do + get '/w/super-mega-fun-wizards.json' + expect(response.parsed_body["error"]).to eq("We couldn't find a wizard at that address.") + end + + it 'skips a wizard if user is allowed to skip' do + put '/w/super-mega-fun-wizard/skip.json' + expect(response.status).to eq(200) end it 'returns a no skip message if user is not allowed to skip' do + @template['required'] = 'true' + CustomWizard::Template.save(@template) + put '/w/super-mega-fun-wizard/skip.json' + expect(response.parsed_body['error']).to eq("Wizard can't be skipped") + end + it 'skip response contains a redirect_to if in users submissions' do + CustomWizard::Wizard.set_submissions(@template['id'], user, + redirect_to: '/t/2' + ) + put '/w/super-mega-fun-wizard/skip.json' + expect(response.parsed_body['redirect_to']).to eq('/t/2') end end \ No newline at end of file diff --git a/spec/serializers/custom_wizard/basic_wizard_serializer_spec.rb b/spec/serializers/custom_wizard/basic_wizard_serializer_spec.rb new file mode 100644 index 00000000..0e088e2d --- /dev/null +++ b/spec/serializers/custom_wizard/basic_wizard_serializer_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe CustomWizard::BasicWizardSerializer do + fab!(:user) { Fabricate(:user) } + + it 'should return basic wizard attributes' do + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + json = CustomWizard::BasicWizardSerializer.new( + CustomWizard::Builder.new("super_mega_fun_wizard", user).build, + scope: Guardian.new(user) + ).as_json + expect(json[:basic_wizard][:id]).to eq("super_mega_fun_wizard") + expect(json[:basic_wizard][:name]).to eq("Super Mega Fun Wizard") + end +end \ No newline at end of file diff --git a/spec/serializers/custom_wizard/log_serializer_spec.rb b/spec/serializers/custom_wizard/log_serializer_spec.rb new file mode 100644 index 00000000..2ea39871 --- /dev/null +++ b/spec/serializers/custom_wizard/log_serializer_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe CustomWizard::LogSerializer do + fab!(:user) { Fabricate(:user) } + + it 'should return log attributes' do + CustomWizard::Log.create("First log message") + CustomWizard::Log.create("Second log message") + + json_array = ActiveModel::ArraySerializer.new( + CustomWizard::Log.list(0), + each_serializer: CustomWizard::LogSerializer + ).as_json + expect(json_array.length).to eq(2) + expect(json_array[0][:message]).to eq("Second log message") + end +end \ No newline at end of file diff --git a/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb b/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb new file mode 100644 index 00000000..02069a9b --- /dev/null +++ b/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe CustomWizard::FieldSerializer do + fab!(:user) { Fabricate(:user) } + + before do + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + @wizard = CustomWizard::Builder.new("super_mega_fun_wizard", user).build + end + + it "should return basic field attributes" do + json_array = ActiveModel::ArraySerializer.new( + @wizard.steps.first.fields, + each_serializer: CustomWizard::FieldSerializer, + scope: Guardian.new(user) + ).as_json + expect(json_array.length).to eq(4) + expect(json_array[0][:label]).to eq("Text
") + expect(json_array[0][:description]).to eq("Text field description.") + end + + it "should return optional field attributes" do + json_array = ActiveModel::ArraySerializer.new( + @wizard.steps.second.fields, + each_serializer: CustomWizard::FieldSerializer, + scope: Guardian.new(user) + ).as_json + expect(json_array[0][:format]).to eq("YYYY-MM-DD") + expect(json_array[5][:file_types]).to eq(".jpg,.png") + end +end \ No newline at end of file diff --git a/spec/serializers/custom_wizard/wizard_serializer_spec.rb b/spec/serializers/custom_wizard/wizard_serializer_spec.rb index a470bccd..16734f83 100644 --- a/spec/serializers/custom_wizard/wizard_serializer_spec.rb +++ b/spec/serializers/custom_wizard/wizard_serializer_spec.rb @@ -2,48 +2,85 @@ require 'rails_helper' -describe CustomWizardSerializer do +describe CustomWizard::WizardSerializer do fab!(:user) { Fabricate(:user) } fab!(:category) { Fabricate(:category) } - let!(:template) do - JSON.parse(File.open( - "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" - ).read) + before do + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + @template = CustomWizard::Template.find('super_mega_fun_wizard') end - let(:category_field) {{"id": "category","type": "category","limit": "1","label": "Category"}} - - def build_wizard(t = template, u = user, build_opts = {}, params = {}) - CustomWizard::Wizard.add_wizard(t) - CustomWizard::Builder.new('welcome', u).build(build_opts, params) - end - it 'should return the wizard attributes' do - json = CustomWizardSerializer.new(build_wizard, scope: Guardian.new(user)).as_json - expect(json[:custom_wizard][:id]).to eq("welcome") - expect(json[:custom_wizard][:name]).to eq("Welcome") - expect(json[:custom_wizard][:background]).to eq("#006da3") - expect(json[:custom_wizard][:required]).to eq(false) - expect(json[:custom_wizard][:min_trust]).to eq(1) + json = CustomWizard::WizardSerializer.new( + CustomWizard::Builder.new(@template[:id], user).build, + scope: Guardian.new(user) + ).as_json + expect(json[:wizard][:id]).to eq("super_mega_fun_wizard") + expect(json[:wizard][:name]).to eq("Super Mega Fun Wizard") + expect(json[:wizard][:background]).to eq("#333333") + expect(json[:wizard][:required]).to eq(false) + end + + it 'should return the wizard steps' do + json = CustomWizard::WizardSerializer.new( + CustomWizard::Builder.new(@template[:id], user).build, + scope: Guardian.new(user) + ).as_json + expect(json[:wizard][:steps].length).to eq(3) end it "should return the wizard user attributes" do - json = CustomWizardSerializer.new(build_wizard, scope: Guardian.new(user)).as_json - expect(json[:custom_wizard][:permitted]).to eq(true) - expect(json[:custom_wizard][:user]).to eq(BasicUserSerializer.new(user, root: false).as_json) + json = CustomWizard::WizardSerializer.new( + CustomWizard::Builder.new(@template[:id], user).build, + scope: Guardian.new(user) + ).as_json + expect( + json[:wizard][:user] + ).to eq(BasicUserSerializer.new(user, root: false).as_json) end - it "should not return category attributes if there are no category fields" do - json = CustomWizardSerializer.new(build_wizard, scope: Guardian.new(user)).as_json - expect(json[:custom_wizard][:categories].present?).to eq(false) - expect(json[:custom_wizard][:uncategorized_category_id].present?).to eq(false) + it "should not return categories if there are no category fields" do + @template[:steps][2][:fields].delete_at(2) + CustomWizard::Template.save(@template) + + json = CustomWizard::WizardSerializer.new( + CustomWizard::Builder.new(@template[:id], user).build, + scope: Guardian.new(user) + ).as_json + expect(json[:wizard][:categories].present?).to eq(false) + expect(json[:wizard][:uncategorized_category_id].present?).to eq(false) end - it "should return category attributes if there is a category selector field" do - template['steps'][0]['fields'][0] = category_field - json = CustomWizardSerializer.new(build_wizard(template), scope: Guardian.new(user)).as_json - expect(json[:custom_wizard][:categories].present?).to eq(true) - expect(json[:custom_wizard][:uncategorized_category_id].present?).to eq(true) + it "should return categories if there is a category selector field" do + json = CustomWizard::WizardSerializer.new( + CustomWizard::Builder.new(@template[:id], user).build, + scope: Guardian.new(user) + ).as_json + expect(json[:wizard][:categories].present?).to eq(true) + expect(json[:wizard][:uncategorized_category_id].present?).to eq(true) + end + + it 'should return groups if there is a group selector field' do + json = CustomWizard::WizardSerializer.new( + CustomWizard::Builder.new(@template[:id], user).build, + scope: Guardian.new(user) + ).as_json + expect(json[:wizard][:groups].length).to eq(8) + end + + it 'should not return groups if there is not a group selector field' do + @template[:steps][2][:fields].delete_at(3) + CustomWizard::Template.save(@template) + + json = CustomWizard::WizardSerializer.new( + CustomWizard::Builder.new(@template[:id], user).build, + scope: Guardian.new(user) + ).as_json + expect(json[:wizard][:groups].present?).to eq(false) end end \ No newline at end of file diff --git a/spec/serializers/custom_wizard/wizard_step_serializer.rb b/spec/serializers/custom_wizard/wizard_step_serializer.rb new file mode 100644 index 00000000..30e8cc5f --- /dev/null +++ b/spec/serializers/custom_wizard/wizard_step_serializer.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe CustomWizard::StepSerializer do + fab!(:user) { Fabricate(:user) } + + let(:required_data_json) { + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/step/required_data.json" + ).read) + } + + before do + CustomWizard::Template.save( + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read), + skip_jobs: true) + @wizard = CustomWizard::Builder.new("super_mega_fun_wizard", user).build + end + + it 'should return basic step attributes' do + json_array = ActiveModel::ArraySerializer.new( + @wizard.steps, + each_serializer: CustomWizard::StepSerializer, + scope: Guardian.new(user) + ).as_json + expect(json_array[0][:wizard_step][:title]).to eq("Text") + expect(json_array[0][:wizard_step][:description]).to eq("Text inputs!") + end + + it 'should return fields' do + json_array = ActiveModel::ArraySerializer.new( + @wizard.steps, + each_serializer: CustomWizard::StepSerializer, + scope: Guardian.new(user) + ).as_json + expect(json_array[0][:wizard_step][:fields].length).to eq(4) + 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 'should return permitted attributes' do + json_array = ActiveModel::ArraySerializer.new( + @wizard.steps, + each_serializer: CustomWizard::StepSerializer, + scope: Guardian.new(user) + ).as_json + expect(json_array[0][:wizard_step][:permitted]).to eq(false) + expect(json_array[0][:wizard_step][:permitted_message]).to eq("Missing required data") + end + end +end \ No newline at end of file