diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..ed9f9cc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b48ce894..9a695031 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,12 @@ -# Uncomment tests runner when tests are added. - +# We want to use the KVM-based system, so require sudo sudo: required -#names: - #- docker +services: + - docker before_install: - - plugin_name=${PWD##*/} && echo $plugin_name - - chmod -R 777 . + - git clone --depth=1 https://github.com/discourse/discourse-plugin-ci -#script: - #- > - #docker run - #-e "COMMIT_HASH=origin/tests-passed" - #-e "SKIP_LINT=1" - #-e "RUBY_ONLY=1" - #-e SINGLE_PLUGIN=$plugin_name - #-v $(pwd):/var/www/discourse/plugins/$plugin_name - #discourse/discourse_test:release +install: true # Prevent travis doing bundle install -after_success: - - pip install virtualenv - - virtualenv ~/env - - source ~/env/bin/activate - - pip install transifex-client - - sudo echo $'[https://www.transifex.com]\nhostname = https://www.transifex.com\nusername = '"$TRANSIFEX_USER"$'\npassword = '"$TRANSIFEX_PASSWORD"$'\ntoken = '"$TRANSIFEX_API_TOKEN"$'\n' > ~/.transifexrc - - tx push -s +script: + - discourse-plugin-ci/script.sh diff --git a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 index 172f0f34..1e4436cb 100644 --- a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 +++ b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 @@ -3,7 +3,11 @@ import DiscourseURL from 'discourse/lib/url'; export default { name: 'custom-wizard-edits', - initialize() { + initialize(container) { + const siteSettings = container.lookup('site-settings:main'); + + if (!siteSettings.custom_wizard_enabled) return; + withPluginApi('0.8.12', api => { api.modifyClass('component:global-notice', { buildBuffer(buffer) { diff --git a/assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6 b/assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6 index 4f60b57a..32827b9b 100644 --- a/assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6 +++ b/assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6 @@ -6,8 +6,9 @@ export default { initialize: function (container) { const messageBus = container.lookup('message-bus:main'); - - if (!messageBus) { return; } + const siteSettings = container.lookup('site-settings:main'); + + if (!siteSettings.custom_wizard_enabled || !messageBus) return; messageBus.subscribe("/redirect_to_wizard", function (wizardId) { const wizardUrl = window.location.origin + '/w/' + wizardId; diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index a0063e3b..8033e4a7 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -8,6 +8,7 @@ en: custom_title: "Wizard" field: too_short: "%{label} must be at least %{min} characters" + required: "%{label} is required." none: "We couldn't find a wizard at that address." no_skip: "Wizard can't be skipped" export: @@ -21,5 +22,6 @@ en: no_valid_wizards: "File doesn't contain any valid wizards" site_settings: + custom_wizard_enabled: "Enable custom wizards." wizard_redirect_exclude_paths: "Routes excluded from wizard redirects." wizard_recognised_image_upload_formats: "File types which will result in upload displaying an image preview" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 00000000..764c4e42 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,36 @@ +CustomWizard::Engine.routes.draw do + get ':wizard_id' => 'wizard#index' + put ':wizard_id/skip' => 'wizard#skip' + get ':wizard_id/steps' => 'wizard#index' + get ':wizard_id/steps/:step_id' => 'wizard#index' + put ':wizard_id/steps/:step_id' => 'steps#update' +end + +Discourse::Application.routes.append do + mount ::CustomWizard::Engine, at: 'w' + post 'wizard/authorization/callback' => "custom_wizard/authorization#callback" + + scope module: 'custom_wizard', constraints: AdminConstraint.new do + get 'admin/wizards' => 'admin#index' + get 'admin/wizards/field-types' => 'admin#field_types' + get 'admin/wizards/custom' => 'admin#index' + get 'admin/wizards/custom/new' => 'admin#index' + get 'admin/wizards/custom/all' => 'admin#custom_wizards' + get 'admin/wizards/custom/:wizard_id' => 'admin#find_wizard' + put 'admin/wizards/custom/save' => 'admin#save' + delete 'admin/wizards/custom/remove' => 'admin#remove' + get 'admin/wizards/submissions' => 'admin#index' + get 'admin/wizards/submissions/:wizard_id' => 'admin#submissions' + get 'admin/wizards/apis' => 'api#list' + get 'admin/wizards/apis/new' => 'api#index' + get 'admin/wizards/apis/:name' => 'api#find' + put 'admin/wizards/apis/:name' => 'api#save' + delete 'admin/wizards/apis/:name' => 'api#remove' + delete 'admin/wizards/apis/logs/:name' => 'api#clearlogs' + get 'admin/wizards/apis/:name/redirect' => 'api#redirect' + get 'admin/wizards/apis/:name/authorize' => 'api#authorize' + get 'admin/wizards/transfer' => 'transfer#index' + get 'admin/wizards/transfer/export' => 'transfer#export' + post 'admin/wizards/transfer/import' => 'transfer#import' + end +end \ No newline at end of file diff --git a/config/settings.yml b/config/settings.yml index 552f0ce8..a5ab7759 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -1,4 +1,5 @@ plugins: + custom_wizard_enabled: true wizard_redirect_exclude_paths: client: true type: list diff --git a/controllers/application_controller.rb b/controllers/application_controller.rb new file mode 100644 index 00000000..5e6358d2 --- /dev/null +++ b/controllers/application_controller.rb @@ -0,0 +1,27 @@ +module ApplicationControllerCWExtension + extend ActiveSupport::Concern + + included do + before_action :redirect_to_wizard_if_required, if: :current_user + end + + def redirect_to_wizard_if_required + wizard_id = current_user.custom_fields['redirect_to_wizard'] + @excluded_routes ||= SiteSetting.wizard_redirect_exclude_paths.split('|') + ['/w/'] + url = request.referer || request.original_url + + if request.format === 'text/html' && !@excluded_routes.any? {|str| /#{str}/ =~ url} && wizard_id + 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) + redirect_to "/w/#{wizard_id.dasherize}" + end + end + end +end + +class ApplicationController + prepend ApplicationControllerCWExtension if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/controllers/admin.rb b/controllers/custom_wizard/admin.rb similarity index 100% rename from controllers/admin.rb rename to controllers/custom_wizard/admin.rb diff --git a/controllers/api.rb b/controllers/custom_wizard/api.rb similarity index 100% rename from controllers/api.rb rename to controllers/custom_wizard/api.rb diff --git a/controllers/steps.rb b/controllers/custom_wizard/steps.rb similarity index 100% rename from controllers/steps.rb rename to controllers/custom_wizard/steps.rb diff --git a/controllers/transfer.rb b/controllers/custom_wizard/transfer.rb similarity index 100% rename from controllers/transfer.rb rename to controllers/custom_wizard/transfer.rb diff --git a/controllers/wizard.rb b/controllers/custom_wizard/wizard.rb similarity index 100% rename from controllers/wizard.rb rename to controllers/custom_wizard/wizard.rb diff --git a/controllers/extra_locales_controller.rb b/controllers/extra_locales_controller.rb new file mode 100644 index 00000000..e1da1803 --- /dev/null +++ b/controllers/extra_locales_controller.rb @@ -0,0 +1,20 @@ +module CustomWizardExtraLocalesController + def show + if request.referer && URI(request.referer).path.include?('/w/') + bundle = params[:bundle] + + if params[:v]&.size == 32 + hash = ExtraLocalesController.bundle_js_hash(bundle) + immutable_for(1.year) if hash == params[:v] + end + + render plain: ExtraLocalesController.bundle_js(bundle), content_type: "application/javascript" + else + super + end + end +end + +class ExtraLocalesController + prepend CustomWizardExtraLocalesController if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/controllers/invites_controller.rb b/controllers/invites_controller.rb new file mode 100644 index 00000000..760100f2 --- /dev/null +++ b/controllers/invites_controller.rb @@ -0,0 +1,22 @@ +module InvitesControllerCustomWizard + def path(url) + if Wizard.user_requires_completion?(@user) + wizard_id = @user.custom_fields['custom_wizard_redirect'] + + if wizard_id && url != '/' + CustomWizard::Wizard.set_submission_redirect(@user, wizard_id, url) + url = "/w/#{wizard_id.dasherize}" + end + end + super(url) + end + + private def post_process_invite(user) + super(user) + @user = user + end +end + +class InvitesController + prepend InvitesControllerCustomWizard if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/lib/api/api.rb b/lib/custom_wizard/api/api.rb similarity index 100% rename from lib/api/api.rb rename to lib/custom_wizard/api/api.rb diff --git a/lib/api/authorization.rb b/lib/custom_wizard/api/authorization.rb similarity index 100% rename from lib/api/authorization.rb rename to lib/custom_wizard/api/authorization.rb diff --git a/lib/api/endpoint.rb b/lib/custom_wizard/api/endpoint.rb similarity index 100% rename from lib/api/endpoint.rb rename to lib/custom_wizard/api/endpoint.rb diff --git a/lib/api/log_entry.rb b/lib/custom_wizard/api/log_entry.rb similarity index 100% rename from lib/api/log_entry.rb rename to lib/custom_wizard/api/log_entry.rb diff --git a/lib/builder.rb b/lib/custom_wizard/builder.rb similarity index 78% rename from lib/builder.rb rename to lib/custom_wizard/builder.rb index b59bc4b9..403fcc13 100644 --- a/lib/builder.rb +++ b/lib/custom_wizard/builder.rb @@ -77,112 +77,119 @@ class CustomWizard::Builder end def build(build_opts = {}, params = {}) - unless (@wizard.completed? && !@wizard.multiple_submissions && !@wizard.user.admin) || !@steps || !@wizard.permitted? - reset_submissions if build_opts[:reset] + + return @wizard if !SiteSetting.custom_wizard_enabled || + (!@wizard.multiple_submissions && + @wizard.completed? && + !@wizard.user.admin) || + !@steps || + !@wizard.permitted? + + reset_submissions if build_opts[:reset] - @steps.each do |step_template| - @wizard.append_step(step_template['id']) do |step| - step.title = step_template['title'] if step_template['title'] - step.description = step_template['description'] if step_template['description'] - step.banner = step_template['banner'] if step_template['banner'] - step.key = step_template['key'] if step_template['key'] - step.permitted = true + @steps.each do |step_template| + @wizard.append_step(step_template['id']) do |step| + step.title = step_template['title'] if step_template['title'] + step.description = step_template['description'] if step_template['description'] + step.banner = step_template['banner'] if step_template['banner'] + step.key = step_template['key'] if step_template['key'] + step.permitted = true - if permitted_params = step_template['permitted_params'] - permitted_data = {} + if permitted_params = step_template['permitted_params'] + permitted_data = {} - permitted_params.each do |param| - key = param['key'].to_sym - permitted_data[key] = params[key] if params[key] - end - - if permitted_data.present? - current_data = @submissions.last || {} - save_submissions(current_data.merge(permitted_data), false) - end + permitted_params.each do |p| + params_key = p['key'].to_sym + submission_key = p['value'].to_sym + permitted_data[submission_key] = params[params_key] if params[params_key] end - if required_data = step_template['required_data'] - if !@submissions.last && required_data.present? - step.permitted = false - next - end + if permitted_data.present? + current_data = @submissions.last || {} + save_submissions(current_data.merge(permitted_data), false) + end + end - required_data.each do |rd| - if rd['connector'] === 'equals' - step.permitted = @submissions.last[rd['key']] == @submissions.last[rd['value']] - end - end - - if !step.permitted - step.permitted_message = step_template['required_data_message'] if step_template['required_data_message'] - next - end + if required_data = step_template['required_data'] + if !@submissions.last && required_data.present? + step.permitted = false + next end + required_data.each do |rd| + if rd['connector'] === 'equals' + step.permitted = @submissions.last[rd['key']] == @submissions.last[rd['value']] + end + end + + if !step.permitted + step.permitted_message = step_template['required_data_message'] if step_template['required_data_message'] + next + end + end + + if step_template['fields'] && step_template['fields'].length + step_template['fields'].each do |field_template| + append_field(step, step_template, field_template, build_opts) + end + end + + step.on_update do |updater| + @updater = updater + user = @wizard.user + if step_template['fields'] && step_template['fields'].length - step_template['fields'].each do |field_template| - append_field(step, step_template, field_template, build_opts) + step_template['fields'].each do |field| + 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| + if handler[:wizard_id] == @wizard.id + handler[:block].call(self) end end - step.on_update do |updater| - @updater = updater - user = @wizard.user - - if step_template['fields'] && step_template['fields'].length - step_template['fields'].each do |field| - validate_field(field, updater, step_template) if field['type'] != 'text-only' - end + next if updater.errors.any? + + 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 + data = submission.merge(data) + end + + if step_template['actions'] && step_template['actions'].length && data + step_template['actions'].each do |action| + self.send(action['type'].to_sym, user, action, data) end - - next if updater.errors.any? + end - CustomWizard::Builder.step_handlers.each do |handler| - if handler[:wizard_id] == @wizard.id - handler[:block].call(self) - end - end + final_step = updater.step.next.nil? + + if route_to = data['route_to'] + data.delete('route_to') + end - next if updater.errors.any? + if @wizard.save_submissions && updater.errors.empty? + save_submissions(data, final_step) + elsif final_step + PluginStore.remove("#{@wizard.id}_submissions", @wizard.user.id) + end - data = updater.fields.to_h + 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 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 - data = submission.merge(data) - end - - if step_template['actions'] && step_template['actions'].length && data - step_template['actions'].each do |action| - self.send(action['type'].to_sym, user, action, data) - end - end - - final_step = updater.step.next.nil? - - 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 final_step - updater.result[:redirect_on_complete] = route_to || data['redirect_on_complete'] - elsif route_to - updater.result[:redirect_on_next] = route_to - end + if updater.errors.empty? + if final_step + updater.result[:redirect_on_complete] = route_to || data['redirect_on_complete'] + elsif route_to + updater.result[:redirect_on_next] = route_to end end end @@ -209,7 +216,7 @@ class CustomWizard::Builder submission = @submissions.last params[:value] = submission[field_template['id']] if submission[field_template['id']] end - + ## If a field updates a profile field, load the current value if step_template['actions'] && step_template['actions'].any? profile_actions = step_template['actions'].select { |a| a['type'] === 'update_profile' } @@ -217,7 +224,7 @@ class CustomWizard::Builder if profile_actions.any? profile_actions.each do |action| if update = action['profile_updates'].select { |u| u['key'] === field_template['id'] }.first - params[:value] = prefill_profile_field(update) + params[:value] = prefill_profile_field(update) || params[:value] end end end @@ -313,13 +320,17 @@ class CustomWizard::Builder def validate_field(field, updater, step_template) value = updater.fields[field['id']] min_length = false + label = field['label'] || I18n.t("#{field['key']}.label") + + if field['required'] && !value + updater.errors.add(field['id'].to_s, I18n.t('wizard.field.required', label: label)) + end if is_text_type(field) min_length = field['min_length'] end if min_length && value.is_a?(String) && value.strip.length < min_length.to_i - label = field['label'] || I18n.t("#{field['key']}.label") updater.errors.add(field['id'].to_s, I18n.t('wizard.field.too_short', label: label, min: min_length.to_i)) end @@ -349,7 +360,7 @@ class CustomWizard::Builder else title = data[action['title']] end - + if action['post_builder'] post = CustomWizard::Builder.fill_placeholders(action['post_template'], user, data) else diff --git a/lib/custom_wizard/engine.rb b/lib/custom_wizard/engine.rb new file mode 100644 index 00000000..80681bee --- /dev/null +++ b/lib/custom_wizard/engine.rb @@ -0,0 +1,6 @@ +module ::CustomWizard + class Engine < ::Rails::Engine + engine_name 'custom_wizard' + isolate_namespace CustomWizard + end +end \ No newline at end of file diff --git a/lib/field.rb b/lib/custom_wizard/field.rb similarity index 100% rename from lib/field.rb rename to lib/custom_wizard/field.rb diff --git a/lib/step_updater.rb b/lib/custom_wizard/step_updater.rb similarity index 87% rename from lib/step_updater.rb rename to lib/custom_wizard/step_updater.rb index c521924c..b5f9bf6c 100644 --- a/lib/step_updater.rb +++ b/lib/custom_wizard/step_updater.rb @@ -8,11 +8,13 @@ class CustomWizard::StepUpdater @wizard = wizard @step = step @refresh_required = false - @fields = fields + @fields = fields.to_h.with_indifferent_access @result = {} end def update + return false if !SiteSetting.custom_wizard_enabled + @step.updater.call(self) if @step.present? && @step.updater.present? if success? diff --git a/lib/template.rb b/lib/custom_wizard/template.rb similarity index 100% rename from lib/template.rb rename to lib/custom_wizard/template.rb diff --git a/lib/wizard.rb b/lib/custom_wizard/wizard.rb similarity index 97% rename from lib/wizard.rb rename to lib/custom_wizard/wizard.rb index 8fe8a690..26f1c79a 100644 --- a/lib/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -3,6 +3,8 @@ require_dependency 'wizard/field' require_dependency 'wizard/step_updater' require_dependency 'wizard/builder' +UserHistory.actions[:custom_wizard_step] = 1000 + class CustomWizard::Wizard attr_reader :steps, :user @@ -190,8 +192,8 @@ class CustomWizard::Wizard end end - def self.add_wizard(json) - wizard = ::JSON.parse(json) + def self.add_wizard(obj) + wizard = obj.is_a?(String) ? ::JSON.parse(json) : obj PluginStore.set('custom_wizard', wizard["id"], wizard) end diff --git a/lib/wizard/choice.rb b/lib/wizard/choice.rb new file mode 100644 index 00000000..f0aa6bf3 --- /dev/null +++ b/lib/wizard/choice.rb @@ -0,0 +1,17 @@ +module CustomWizardChoiceExtension + def initialize(id, opts) + @id = id + @opts = opts + @data = opts[:data] + @extra_label = opts[:extra_label] + @icon = opts[:icon] + end + + def label + @label ||= PrettyText.cook(@opts[:label]) + end +end + +class Wizard::Choice + prepend CustomWizardChoiceExtension if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/lib/wizard/field.rb b/lib/wizard/field.rb new file mode 100644 index 00000000..038fdbbc --- /dev/null +++ b/lib/wizard/field.rb @@ -0,0 +1,37 @@ +module CustomWizardFieldExtension + attr_reader :label, + :description, + :image, + :key, + :min_length, + :file_types, + :limit, + :property + + attr_accessor :dropdown_none + + def initialize(attrs) + @attrs = attrs || {} + @id = attrs[:id] + @type = attrs[:type] + @required = !!attrs[:required] + @description = attrs[:description] + @image = attrs[:image] + @key = attrs[:key] + @min_length = attrs[:min_length] + @value = attrs[:value] + @choices = [] + @dropdown_none = attrs[:dropdown_none] + @file_types = attrs[:file_types] + @limit = attrs[:limit] + @property = attrs[:property] + end + + def label + @label ||= PrettyText.cook(@attrs[:label]) + end +end + +class Wizard::Field + prepend CustomWizardFieldExtension if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/lib/wizard/step.rb b/lib/wizard/step.rb new file mode 100644 index 00000000..b58bc837 --- /dev/null +++ b/lib/wizard/step.rb @@ -0,0 +1,7 @@ +module CustomWizardStepExtension + attr_accessor :title, :description, :key, :permitted, :permitted_message +end + +class Wizard::Step + prepend CustomWizardStepExtension if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/lib/wizard_edits.rb b/lib/wizard_edits.rb deleted file mode 100644 index 9c834808..00000000 --- a/lib/wizard_edits.rb +++ /dev/null @@ -1,227 +0,0 @@ -require_dependency 'wizard' -require_dependency 'wizard/field' -require_dependency 'wizard/step' - -::Wizard.class_eval do - def self.user_requires_completion?(user) - wizard_result = self.new(user).requires_completion? - return wizard_result if wizard_result - - custom_redirect = false - - if user && user.first_seen_at.blank? && wizard_id = CustomWizard::Wizard.after_signup - wizard = CustomWizard::Wizard.create(user, wizard_id) - - if !wizard.completed? && wizard.permitted? - custom_redirect = true - CustomWizard::Wizard.set_wizard_redirect(user, wizard_id) - end - end - - !!custom_redirect - end -end - -::Wizard::Field.class_eval do - attr_reader :label, :description, :image, :key, :min_length, :file_types, :limit, :property - attr_accessor :dropdown_none - - def initialize(attrs) - @attrs = attrs || {} - @id = attrs[:id] - @type = attrs[:type] - @required = !!attrs[:required] - @description = attrs[:description] - @image = attrs[:image] - @key = attrs[:key] - @min_length = attrs[:min_length] - @value = attrs[:value] - @choices = [] - @dropdown_none = attrs[:dropdown_none] - @file_types = attrs[:file_types] - @limit = attrs[:limit] - @property = attrs[:property] - end - - def label - @label ||= PrettyText.cook(@attrs[:label]) - end -end - -::Wizard::Choice.class_eval do - def initialize(id, opts) - @id = id - @opts = opts - @data = opts[:data] - @extra_label = opts[:extra_label] - @icon = opts[:icon] - end - - def label - @label ||= PrettyText.cook(@opts[:label]) - end -end - -class ::Wizard::Step - attr_accessor :title, :description, :key, :permitted, :permitted_message -end - -::WizardSerializer.class_eval do - attributes :id, - :name, - :background, - :completed, - :required, - :min_trust, - :permitted, - :user, - :categories, - :uncategorized_category_id - - def id - object.id - end - - def include_id? - object.respond_to?(:id) - end - - def name - object.name - end - - def include_name? - object.respond_to?(:name) - end - - def background - object.background - end - - def include_background? - object.respond_to?(:background) - end - - def completed - object.completed? - end - - def include_completed? - object.completed? && - (!object.respond_to?(:multiple_submissions) || !object.multiple_submissions) && - !scope.is_admin? - end - - def min_trust - object.min_trust - end - - def include_min_trust? - object.respond_to?(:min_trust) - end - - def permitted - object.permitted? - end - - def include_permitted? - object.respond_to?(:permitted?) - end - - def include_start? - object.start && include_steps? - end - - def include_steps? - !include_completed? - end - - def required - object.required - end - - def include_required? - object.respond_to?(:required) - end - - def user - object.user - end - - def categories - begin - site = ::Site.new(scope) - ::ActiveModel::ArraySerializer.new(site.categories, each_serializer: BasicCategorySerializer) - rescue => e - puts "HERE IS THE ERROR: #{e.inspect}" - end - end - - def uncategorized_category_id - SiteSetting.uncategorized_category_id - end -end - -::WizardStepSerializer.class_eval do - attributes :permitted, :permitted_message - - def title - return PrettyText.cook(object.title) if object.title - PrettyText.cook(I18n.t("#{object.key || i18n_key}.title", default: '')) - end - - def description - return object.description if object.description - PrettyText.cook(I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url)) - end - - def permitted - object.permitted - end - - def permitted_message - object.permitted_message - end -end - -::WizardFieldSerializer.class_eval do - attributes :dropdown_none, :image, :file_types, :limit, :property - - def label - return object.label if object.label.present? - I18n.t("#{object.key || i18n_key}.label", default: '') - end - - def description - return object.description if object.description.present? - I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url) - end - - def image - object.image - end - - def include_image? - object.image.present? - end - - def placeholder - I18n.t("#{object.key || i18n_key}.placeholder", default: '') - end - - def dropdown_none - object.dropdown_none - end - - def file_types - object.file_types - end - - def limit - object.limit - end - - def property - object.property - end -end diff --git a/plugin.rb b/plugin.rb index f203125c..c56942f7 100644 --- a/plugin.rb +++ b/plugin.rb @@ -8,10 +8,12 @@ register_asset 'stylesheets/wizard_custom_admin.scss' register_asset 'lib/jquery.timepicker.min.js' register_asset 'lib/jquery.timepicker.scss' -config = Rails.application.config -config.assets.paths << Rails.root.join('plugins', 'discourse-custom-wizard', 'assets', 'javascripts') -config.assets.paths << Rails.root.join('plugins', 'discourse-custom-wizard', 'assets', 'stylesheets', 'wizard') +enabled_site_setting :custom_wizard_enabled +config = Rails.application.config +plugin_asset_path = "#{Rails.root}/plugins/discourse-custom-wizard/assets" +config.assets.paths << "#{plugin_asset_path}/javascripts" +config.assets.paths << "#{plugin_asset_path}/stylesheets/wizard" if Rails.env.production? config.assets.precompile += %w{ @@ -35,181 +37,84 @@ if respond_to?(:register_svg_icon) end after_initialize do - UserHistory.actions[:custom_wizard_step] = 1000 - - module ::CustomWizard - class Engine < ::Rails::Engine - engine_name 'custom_wizard' - isolate_namespace CustomWizard - end - end - - CustomWizard::Engine.routes.draw do - get ':wizard_id' => 'wizard#index' - put ':wizard_id/skip' => 'wizard#skip' - get ':wizard_id/steps' => 'wizard#index' - get ':wizard_id/steps/:step_id' => 'wizard#index' - put ':wizard_id/steps/:step_id' => 'steps#update' - end - - Discourse::Application.routes.append do - mount ::CustomWizard::Engine, at: 'w' - post 'wizard/authorization/callback' => "custom_wizard/authorization#callback" - - scope module: 'custom_wizard', constraints: AdminConstraint.new do - get 'admin/wizards' => 'admin#index' - get 'admin/wizards/field-types' => 'admin#field_types' - get 'admin/wizards/custom' => 'admin#index' - get 'admin/wizards/custom/new' => 'admin#index' - get 'admin/wizards/custom/all' => 'admin#custom_wizards' - get 'admin/wizards/custom/:wizard_id' => 'admin#find_wizard' - put 'admin/wizards/custom/save' => 'admin#save' - delete 'admin/wizards/custom/remove' => 'admin#remove' - get 'admin/wizards/submissions' => 'admin#index' - get 'admin/wizards/submissions/:wizard_id' => 'admin#submissions' - get 'admin/wizards/apis' => 'api#list' - get 'admin/wizards/apis/new' => 'api#index' - get 'admin/wizards/apis/:name' => 'api#find' - put 'admin/wizards/apis/:name' => 'api#save' - delete 'admin/wizards/apis/:name' => 'api#remove' - delete 'admin/wizards/apis/logs/:name' => 'api#clearlogs' - get 'admin/wizards/apis/:name/redirect' => 'api#redirect' - get 'admin/wizards/apis/:name/authorize' => 'api#authorize' - get 'admin/wizards/transfer' => 'transfer#index' - get 'admin/wizards/transfer/export' => 'transfer#export' - post 'admin/wizards/transfer/import' => 'transfer#import' - end - end - - load File.expand_path('../jobs/clear_after_time_wizard.rb', __FILE__) - load File.expand_path('../jobs/set_after_time_wizard.rb', __FILE__) - load File.expand_path('../lib/builder.rb', __FILE__) - load File.expand_path('../lib/field.rb', __FILE__) - load File.expand_path('../lib/step_updater.rb', __FILE__) - load File.expand_path('../lib/template.rb', __FILE__) - load File.expand_path('../lib/wizard.rb', __FILE__) - load File.expand_path('../lib/wizard_edits.rb', __FILE__) - load File.expand_path('../controllers/wizard.rb', __FILE__) - load File.expand_path('../controllers/steps.rb', __FILE__) - load File.expand_path('../controllers/admin.rb', __FILE__) - #transfer code - load File.expand_path('../controllers/transfer.rb', __FILE__) - - load File.expand_path('../jobs/refresh_api_access_token.rb', __FILE__) - load File.expand_path('../lib/api/api.rb', __FILE__) - load File.expand_path('../lib/api/authorization.rb', __FILE__) - load File.expand_path('../lib/api/endpoint.rb', __FILE__) - load File.expand_path('../lib/api/log_entry.rb', __FILE__) - load File.expand_path('../controllers/api.rb', __FILE__) - load File.expand_path('../serializers/api/api_serializer.rb', __FILE__) - load File.expand_path('../serializers/api/authorization_serializer.rb', __FILE__) - load File.expand_path('../serializers/api/basic_api_serializer.rb', __FILE__) - load File.expand_path('../serializers/api/endpoint_serializer.rb', __FILE__) - load File.expand_path('../serializers/api/basic_endpoint_serializer.rb', __FILE__) - load File.expand_path('../serializers/api/log_serializer.rb', __FILE__) - - ::UsersController.class_eval do - def wizard_path - if custom_wizard_redirect = current_user.custom_fields['redirect_to_wizard'] - "#{Discourse.base_url}/w/#{custom_wizard_redirect.dasherize}" - else - "#{Discourse.base_url}/wizard" - end - end - end - - module InvitesControllerCustomWizard - def path(url) - if Wizard.user_requires_completion?(@user) - wizard_id = @user.custom_fields['custom_wizard_redirect'] - - if wizard_id && url != '/' - CustomWizard::Wizard.set_submission_redirect(@user, wizard_id, url) - url = "/w/#{wizard_id.dasherize}" - end - end - super(url) - end - - private def post_process_invite(user) - super(user) - @user = user - end + [ + '../lib/custom_wizard/engine.rb', + '../config/routes.rb', + '../controllers/custom_wizard/wizard.rb', + '../controllers/custom_wizard/steps.rb', + '../controllers/custom_wizard/admin.rb', + '../controllers/custom_wizard/transfer.rb', + '../controllers/custom_wizard/api.rb', + '../controllers/application_controller.rb', + '../controllers/extra_locales_controller.rb', + '../controllers/invites_controller.rb', + '../jobs/clear_after_time_wizard.rb', + '../jobs/refresh_api_access_token.rb', + '../jobs/set_after_time_wizard.rb', + '../lib/custom_wizard/builder.rb', + '../lib/custom_wizard/field.rb', + '../lib/custom_wizard/step_updater.rb', + '../lib/custom_wizard/template.rb', + '../lib/custom_wizard/wizard.rb', + '../lib/custom_wizard/api/api.rb', + '../lib/custom_wizard/api/authorization.rb', + '../lib/custom_wizard/api/endpoint.rb', + '../lib/custom_wizard/api/log_entry.rb', + '../lib/wizard/choice.rb', + '../lib/wizard/field.rb', + '../lib/wizard/step.rb', + '../serializers/custom_wizard/api_serializer.rb', + '../serializers/custom_wizard/basic_api_serializer.rb', + '../serializers/custom_wizard/api/authorization_serializer.rb', + '../serializers/custom_wizard/api/basic_endpoint_serializer.rb', + '../serializers/custom_wizard/api/endpoint_serializer.rb', + '../serializers/custom_wizard/api/log_serializer.rb', + '../serializers/site_serializer.rb', + '../serializers/wizard_serializer.rb', + '../serializers/wizard_step_serializer.rb', + '../serializers/wizard_field_serializer.rb' + ].each do |path| + load File.expand_path(path, __FILE__) end - require_dependency 'invites_controller' - class ::InvitesController - prepend InvitesControllerCustomWizard - end - - require_dependency 'application_controller' - class ::ApplicationController - before_action :redirect_to_wizard_if_required, if: :current_user + add_class_method(:wizard, :user_requires_completion?) do |user| + wizard_result = self.new(user).requires_completion? + return wizard_result if wizard_result - def redirect_to_wizard_if_required - wizard_id = current_user.custom_fields['redirect_to_wizard'] - @excluded_routes ||= SiteSetting.wizard_redirect_exclude_paths.split('|') + ['/w/'] - url = request.referer || request.original_url + custom_redirect = false - if request.format === 'text/html' && !@excluded_routes.any? {|str| /#{str}/ =~ url} && wizard_id - if request.referer !~ /\/w\// && request.referer !~ /\/invites\// - CustomWizard::Wizard.set_submission_redirect(current_user, wizard_id, request.referer) - end + if user && + user.first_seen_at.blank? && + wizard_id = CustomWizard::Wizard.after_signup + + wizard = CustomWizard::Wizard.create(user, wizard_id) - if CustomWizard::Wizard.exists?(wizard_id) - redirect_to "/w/#{wizard_id.dasherize}" - end - end - end - end - - add_to_serializer(:current_user, :redirect_to_wizard) {object.custom_fields['redirect_to_wizard']} - - ## TODO limit this to the first admin - SiteSerializer.class_eval do - attributes :complete_custom_wizard - - def include_wizard_required? - scope.is_admin? && Wizard.new(scope.user).requires_completion? - end - - def complete_custom_wizard - if scope.user && requires_completion = CustomWizard::Wizard.prompt_completion(scope.user) - requires_completion.map {|w| {name: w[:name], url: "/w/#{w[:id]}"}} + if !wizard.completed? && wizard.permitted? + custom_redirect = true + CustomWizard::Wizard.set_wizard_redirect(user, wizard_id) end end - def include_complete_custom_wizard? - complete_custom_wizard.present? + !!custom_redirect + end + + add_to_class(:users_controller, :wizard_path) do + if custom_wizard_redirect = current_user.custom_fields['redirect_to_wizard'] + "#{Discourse.base_url}/w/#{custom_wizard_redirect.dasherize}" + else + "#{Discourse.base_url}/wizard" end end + add_to_serializer(:current_user, :redirect_to_wizard) do + object.custom_fields['redirect_to_wizard'] + end + DiscourseEvent.on(:user_approved) do |user| if wizard_id = CustomWizard::Wizard.after_signup CustomWizard::Wizard.set_wizard_redirect(user, wizard_id) end end - - module CustomWizardExtraLocalesController - def show - if request.referer && URI(request.referer).path.include?('/w/') - bundle = params[:bundle] - - if params[:v]&.size == 32 - hash = ExtraLocalesController.bundle_js_hash(bundle) - immutable_for(1.year) if hash == params[:v] - end - - render plain: ExtraLocalesController.bundle_js(bundle), content_type: "application/javascript" - else - super - end - end - end - - class ::ExtraLocalesController - prepend CustomWizardExtraLocalesController - end DiscourseEvent.trigger(:custom_wizard_ready) end diff --git a/serializers/api/authorization_serializer.rb b/serializers/custom_wizard/api/authorization_serializer.rb similarity index 100% rename from serializers/api/authorization_serializer.rb rename to serializers/custom_wizard/api/authorization_serializer.rb diff --git a/serializers/api/basic_endpoint_serializer.rb b/serializers/custom_wizard/api/basic_endpoint_serializer.rb similarity index 100% rename from serializers/api/basic_endpoint_serializer.rb rename to serializers/custom_wizard/api/basic_endpoint_serializer.rb diff --git a/serializers/api/endpoint_serializer.rb b/serializers/custom_wizard/api/endpoint_serializer.rb similarity index 100% rename from serializers/api/endpoint_serializer.rb rename to serializers/custom_wizard/api/endpoint_serializer.rb diff --git a/serializers/api/log_serializer.rb b/serializers/custom_wizard/api/log_serializer.rb similarity index 100% rename from serializers/api/log_serializer.rb rename to serializers/custom_wizard/api/log_serializer.rb diff --git a/serializers/api/api_serializer.rb b/serializers/custom_wizard/api_serializer.rb similarity index 100% rename from serializers/api/api_serializer.rb rename to serializers/custom_wizard/api_serializer.rb diff --git a/serializers/api/basic_api_serializer.rb b/serializers/custom_wizard/basic_api_serializer.rb similarity index 100% rename from serializers/api/basic_api_serializer.rb rename to serializers/custom_wizard/basic_api_serializer.rb diff --git a/serializers/site_serializer.rb b/serializers/site_serializer.rb new file mode 100644 index 00000000..faff37c9 --- /dev/null +++ b/serializers/site_serializer.rb @@ -0,0 +1,27 @@ +## TODO limit this to the first admin +module SiteSerializerCWX + extend ActiveSupport::Concern + + included do + attributes :complete_custom_wizard + end + + + def include_wizard_required? + scope.is_admin? && Wizard.new(scope.user).requires_completion? + end + + def complete_custom_wizard + if scope.user && requires_completion = CustomWizard::Wizard.prompt_completion(scope.user) + requires_completion.map {|w| {name: w[:name], url: "/w/#{w[:id]}"}} + end + end + + def include_complete_custom_wizard? + complete_custom_wizard.present? + end +end + +class SiteSerializer + prepend SiteSerializerCWX if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/serializers/wizard_field_serializer.rb b/serializers/wizard_field_serializer.rb new file mode 100644 index 00000000..c1ef779f --- /dev/null +++ b/serializers/wizard_field_serializer.rb @@ -0,0 +1,49 @@ +module CustomWizardWizardFieldSerializerExtension + extend ActiveSupport::Concern + + included do + attributes :dropdown_none, :image, :file_types, :limit, :property + end + + def label + return object.label if object.label.present? + I18n.t("#{object.key || i18n_key}.label", default: '') + end + + def description + return object.description if object.description.present? + I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url) + end + + def image + object.image + end + + def include_image? + object.image.present? + end + + def placeholder + I18n.t("#{object.key || i18n_key}.placeholder", default: '') + end + + def dropdown_none + object.dropdown_none + end + + def file_types + object.file_types + end + + def limit + object.limit + end + + def property + object.property + end +end + +class WizardFieldSerializer + prepend CustomWizardWizardFieldSerializerExtension if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/serializers/wizard_serializer.rb b/serializers/wizard_serializer.rb new file mode 100644 index 00000000..9cd97f26 --- /dev/null +++ b/serializers/wizard_serializer.rb @@ -0,0 +1,103 @@ +module CustomWizardWizardSerializerExtension + extend ActiveSupport::Concern + + included do + attributes :id, + :name, + :background, + :completed, + :required, + :min_trust, + :permitted, + :user, + :categories, + :uncategorized_category_id + end + + def id + object.id + end + + def include_id? + object.respond_to?(:id) + end + + def name + object.name + end + + def include_name? + object.respond_to?(:name) + end + + def background + object.background + end + + def include_background? + object.respond_to?(:background) + end + + def completed + object.completed? + end + + def include_completed? + object.completed? && + (!object.respond_to?(:multiple_submissions) || !object.multiple_submissions) && + !scope.is_admin? + end + + def min_trust + object.min_trust + end + + def include_min_trust? + object.respond_to?(:min_trust) + end + + def permitted + object.permitted? + end + + def include_permitted? + object.respond_to?(:permitted?) + end + + def include_start? + object.start && include_steps? + end + + def include_steps? + !include_completed? + end + + def required + object.required + end + + def include_required? + object.respond_to?(:required) + end + + def user + object.user + end + + def categories + begin + site = ::Site.new(scope) + ::ActiveModel::ArraySerializer.new(site.categories, each_serializer: BasicCategorySerializer) + rescue => e + puts "HERE IS THE ERROR: #{e.inspect}" + end + end + + def uncategorized_category_id + SiteSetting.uncategorized_category_id + end +end + +class WizardSerializer + prepend CustomWizardWizardSerializerExtension if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/serializers/wizard_step_serializer.rb b/serializers/wizard_step_serializer.rb new file mode 100644 index 00000000..e3774069 --- /dev/null +++ b/serializers/wizard_step_serializer.rb @@ -0,0 +1,29 @@ +module CustomWizardWizardStepSerializerExtension + extend ActiveSupport::Concern + + included do + attributes :permitted, :permitted_message + end + + def title + return PrettyText.cook(object.title) if object.title + PrettyText.cook(I18n.t("#{object.key || i18n_key}.title", default: '')) + end + + def description + return object.description if object.description + PrettyText.cook(I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url)) + end + + def permitted + object.permitted + end + + def permitted_message + object.permitted_message + end +end + +class WizardStepSerializer + prepend CustomWizardWizardStepSerializerExtension if SiteSetting.custom_wizard_enabled +end \ No newline at end of file diff --git a/spec/components/custom_wizard/api_spec.rb b/spec/components/custom_wizard/api_spec.rb new file mode 100644 index 00000000..a7a8ba1b --- /dev/null +++ b/spec/components/custom_wizard/api_spec.rb @@ -0,0 +1,21 @@ +# 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 new file mode 100644 index 00000000..4dc13b64 --- /dev/null +++ b/spec/components/custom_wizard/builder_spec.rb @@ -0,0 +1,370 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe CustomWizard::Builder do + fab!(:user) { Fabricate(:user, username: 'angus') } + fab!(:trusted_user) { Fabricate(:user, trust_level: 3) } + fab!(:group) { Fabricate(:group) } + + let!(:template) do + JSON.parse(File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json" + ).read) + end + + let(:permitted_params) {[{"key":"param_key","value":"submission_param_key"}]} + let(:required_data) {[{"key":"nickname","connector":"equals","value":"name"}]} + let(:required_data_message) {"Nickname is required to match your name"} + let(:checkbox_field) {{"id":"checkbox","type":"checkbox","label":"Checkbox"}} + let(:composer_field) {{"id": "composer","label":"Composer","type":"composer"}} + let(:dropdown_categories_field) {{"id": "dropdown_categories","type": "dropdown","label": "Dropdown Categories","choices_type": "preset","choices_preset": "categories"}} + let(:tag_field) {{"id": "tag","type": "tag","label": "Tag","limit": "2"}} + let(:category_field) {{"id": "category","type": "category","limit": "1","label": "Category"}} + let(:image_field) {{"id": "image","type": "image","label": "Image"}} + let(:text_field) {{"id": "text","type": "text","label": "Text"}} + let(:textarea_field) {{"id": "textarea","type": "textarea","label": "Textarea"}} + let(:text_only_field) {{"id": "text_only","type": "text-only","label": "Text only"}} + let(:upload_field) {{"id": "upload","type": "upload","file_types": ".jpg,.png,.pdf","label": "Upload"}} + let(:user_selector_field) {{"id": "user_selector","type": "user-selector","label": "User selector"}} + let(:dropdown_groups_field) {{"id": "dropdown_groups","type": "dropdown","choices_type": "preset","choices_preset": "groups","label": "Dropdown Groups"}} + let(:dropdown_tags_field) {{"id": "dropdown_tags","type": "dropdown","choices_type": "preset","choices_preset": "tags","label": "Dropdown Tags"}} + let(:dropdown_custom_field) {{"id": "dropdown_custom","type": "dropdown","choices_type": "custom","choices": [{"key": "option_1","value": "Option 1"},{"key": "option_2","value": "Option 2"}]}} + let(:dropdown_translation_field) {{"id": "dropdown_translation","type": "dropdown","choices_type": "translation","choices_key": "key1.key2"}} + let(:dropdown_categories_filtered_field) {{"id": "dropdown_categories_filtered_field","type": "dropdown","choices_type": "preset","choices_preset": "categories","choices_filters": [{"key": "slug","value": "staff"}]}} + 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"}} + + def build_wizard(t = template, u = user, build_opts = {}, params = {}) + CustomWizard::Wizard.add_wizard(t) + CustomWizard::Builder.new(u, 'welcome').build(build_opts, params) + end + + def add_submission_data(data = {}) + PluginStore.set("welcome_submissions", user.id, { + name: 'Angus', + website: 'https://thepavilion.io' + }.merge(data)) + end + + 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 + end + + def send_message(extra_field = nil, extra_action_opts = {}) + fields = [text_field, textarea_field] + + 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" + ) + end + + context 'disabled' do + before 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) + end + end + + context 'enabled' do + before do + SiteSetting.custom_wizard_enabled = true + end + + it "returns steps" do + expect(build_wizard.steps.length).to eq(2) + end + + it 'returns no steps if the multiple submissions are disabled and user has completed it' 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'])) + + template["multiple_submissions"] = false + expect(build_wizard(template).steps.length).to eq(0) + end + + it 'returns no steps if has min trust and user does not meet it' do + template["min_trust"] = 3 + expect(build_wizard(template).steps.length).to eq(0) + end + + it 'returns steps if it has min trust and user meets it' do + template["min_trust"] = 3 + expect(build_wizard(template, trusted_user).steps.length).to eq(2) + 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') + 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 + 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') + 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') + 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) + end + + it "is not permitted if required data is not present" do + template['steps'][0]['required_data'] = required_data + add_submission_data(nickname: "John") + expect(build_wizard(template, user).steps[0].permitted).to eq(false) + 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 + + 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") + end + + it 'returns fields' do + template['steps'][0]['fields'][1] = checkbox_field + expect(build_wizard(template, user).steps[0].fields.length).to eq(2) + end + end + + context 'on update' do + it 'saves submissions' do + run_update(template, nil, name: 'Angus') + expect(get_submission_data.first['name']).to eq('Angus') + 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) + ) + 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) + 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') + ) + end + end + + context 'actions' do + it 'runs actions attached to a step' do + run_update(template, template['steps'][1]['id'], name: "Gus") + expect(user.name).to eq('Gus') + end + + it 'interpolates user data correctly' do + user.name = "Angus" + user.save! + + expect( + CustomWizard::Builder.fill_placeholders( + "My name is u{name}", + user, + {} + ) + ).to eq('My name is Angus') + end + + 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") + + expect(topic.exists?).to eq(true) + expect(Post.where( + topic_id: topic.pluck(:id), + raw: "topic body" + ).exists?).to eq(true) + end + + it 'creates a topic with a custom title' do + user.name = "Angus" + user.save! + + template['steps'][0]['fields'] = [text_field, textarea_field] + + create_topic_action['custom_title_enabled'] = true + create_topic_action['custom_title'] = "u{name}' Topic Title" + template['steps'][0]["actions"] = [create_topic_action] + + run_update(template, nil, textarea: "topic body") + + topic = Topic.where(title: "Angus' Topic Title") + + expect(topic.exists?).to eq(true) + expect(Post.where( + topic_id: topic.pluck(:id), + raw: "topic body" + ).exists?).to eq(true) + end + + it 'creates a topic with a custom post' do + user.name = "Angus" + user.save! + + template['steps'][0]['fields'] = [text_field, textarea_field] + + create_topic_action['post_builder'] = true + create_topic_action['post_template'] = "u{name}' w{textarea}" + template['steps'][0]["actions"] = [create_topic_action] + + run_update(template, nil, + text: "Topic Title", + textarea: "topic body" + ) + + topic = Topic.where(title: "Topic Title") + + expect(topic.exists?).to eq(true) + expect(Post.where( + topic_id: topic.pluck(:id), + raw: "Angus' topic body" + ).exists?).to eq(true) + end + + it 'sends a message' do + send_message + + topic = Topic.where( + archetype: Archetype.private_message, + title: "Message Title" + ) + + expect(topic.exists?).to eq(true) + expect( + topic.first.topic_allowed_users.first.user.username + ).to eq('angus') + expect(Post.where( + topic_id: topic.pluck(:id), + raw: "message body" + ).exists?).to eq(true) + end + + it 'doesnt sent a message if the required data is not present' do + send_message(user_selector_field, required: "user_selector") + topic = Topic.where( + archetype: Archetype.private_message, + title: "Message Title" + ) + expect(topic.exists?).to eq(false) + end + + it 'updates a profile' do + run_update(template, template['steps'][1]['id'], name: "Sally") + expect(user.name).to eq('Sally') + end + + it 'opens a composer' do + template['steps'][0]['fields'] = [text_field, textarea_field] + template['steps'][0]["actions"] = [open_composer_action] + + updater = run_update(template, nil, + text: "Topic Title", + textarea: "topic body" + ) + + expect(updater.result.blank?).to eq(true) + + updater = run_update(template, template['steps'][1]['id']) + + expect(updater.result[:redirect_on_complete]).to eq( + "/new-topic?title=Topic%20Title&body=topic%20body" + ) + 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) + expect(group.users.first.username).to eq('angus') + 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" + ) + end + end + end + end +end \ No newline at end of file diff --git a/spec/fixtures/wizard.json b/spec/fixtures/wizard.json new file mode 100644 index 00000000..04c16fea --- /dev/null +++ b/spec/fixtures/wizard.json @@ -0,0 +1,49 @@ +{ + "id": "welcome", + "name": "Welcome", + "background": "#006da3", + "save_submissions": true, + "multiple_submissions": true, + "after_signup": true, + "theme_id": 4, + "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.

\n

We’re Pavilion, an international freelancer cooperative that specialises in online communities.

\n

This site is our own community, where we work with our clients, users of our open source work and our broader community.

", + "fields": [ + { + "id": "name", + "type": "text", + "label": "Name" + } + ] + }, + { + "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": "website", + "label": "Website", + "type": "text" + } + ], + "actions": [ + { + "id": "update_profile", + "type": "update_profile", + "profile_updates": [ + { + "key": "name", + "value": "name" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/spec/plugin_helper.rb b/spec/plugin_helper.rb new file mode 100644 index 00000000..47368da5 --- /dev/null +++ b/spec/plugin_helper.rb @@ -0,0 +1,8 @@ +require 'simplecov' + +SimpleCov.configure do + add_filter do |src| + src.filename !~ /discourse-custom-wizard/ || + src.filename =~ /spec/ + 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 new file mode 100644 index 00000000..731d0de5 --- /dev/null +++ b/spec/requests/custom_wizard/admin_controller_spec.rb @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000..1315748d --- /dev/null +++ b/spec/requests/custom_wizard/application_controller_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +describe ApplicationController do + +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 new file mode 100644 index 00000000..b457eba7 --- /dev/null +++ b/spec/requests/custom_wizard/wizard_controller_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +describe CustomWizard::WizardController do + it 'returns a wizard if enabled' do + + 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 + + end + + it 'returns a no skip message if user is not allowed to skip' do + + end +end \ No newline at end of file