diff --git a/.github/workflows/plugin-linting.yml b/.github/workflows/plugin-linting.yml index a121658d..a79c5462 100644 --- a/.github/workflows/plugin-linting.yml +++ b/.github/workflows/plugin-linting.yml @@ -6,6 +6,8 @@ on: - master - main pull_request: + schedule: + - cron: '0 0 * * *' jobs: build: diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml index ce6112af..84e055ca 100644 --- a/.github/workflows/plugin-tests.yml +++ b/.github/workflows/plugin-tests.yml @@ -6,6 +6,8 @@ on: - master - main pull_request: + schedule: + - cron: '0 0 * * *' jobs: build: @@ -51,23 +53,26 @@ jobs: repository: discourse/discourse fetch-depth: 1 + - run: echo "REPOSITORY_NAME=$(echo '${{ github.repository }}' | awk -F '/' '{print $2}')" >> $GITHUB_ENV + shell: bash + - name: Install plugin uses: actions/checkout@v2 with: - path: plugins/${{ github.event.repository.name }} + path: plugins/${{ env.REPOSITORY_NAME }} fetch-depth: 1 - name: Check spec existence id: check_spec uses: andstor/file-existence-action@v1 with: - files: "plugins/${{ github.event.repository.name }}/spec" + files: "plugins/${{ env.REPOSITORY_NAME }}/spec" - name: Check qunit existence id: check_qunit uses: andstor/file-existence-action@v1 with: - files: "plugins/${{ github.event.repository.name }}/test/javascripts" + files: "plugins/${{ env.REPOSITORY_NAME }}/test/javascripts" - name: Setup Git run: | @@ -100,7 +105,7 @@ jobs: - name: Lint English locale if: matrix.build_type == 'backend' - run: bundle exec ruby script/i18n_lint.rb "plugins/${{ github.event.repository.name }}/locales/{client,server}.en.yml" + run: bundle exec ruby script/i18n_lint.rb "plugins/${{ env.REPOSITORY_NAME }}/locales/{client,server}.en.yml" - name: Get yarn cache directory id: yarn-cache-dir @@ -123,15 +128,11 @@ jobs: bin/rake db:create bin/rake db:migrate - - name: Plugin RSpec + - name: Plugin RSpec with Coverage if: matrix.build_type == 'backend' && steps.check_spec.outputs.files_exists == 'true' - run: bin/rake plugin:spec[${{ github.event.repository.name }}] + run: SIMPLECOV=1 bin/rake plugin:spec[${{ env.REPOSITORY_NAME }}] - name: Plugin QUnit if: matrix.build_type == 'frontend' && steps.check_qunit.outputs.files_exists == 'true' - run: bundle exec rake plugin:qunit['${{ github.event.repository.name }}','1200000'] + run: bundle exec rake plugin:qunit['${{ env.REPOSITORY_NAME }}','1200000'] timeout-minutes: 30 - - - name: Simplecov Report - if: matrix.build_type == 'backend' - run: COVERAGE=1 bin/rake plugin:spec[${{ github.event.repository.name }}] diff --git a/assets/javascripts/discourse/components/custom-field-input.js.es6 b/assets/javascripts/discourse/components/custom-field-input.js.es6 index f2dca4c7..e49c6f1d 100644 --- a/assets/javascripts/discourse/components/custom-field-input.js.es6 +++ b/assets/javascripts/discourse/components/custom-field-input.js.es6 @@ -1,6 +1,6 @@ import Component from "@ember/component"; import discourseComputed, { observes } from "discourse-common/utils/decorators"; -import { alias, or } from "@ember/object/computed"; +import { alias, equal, or } from "@ember/object/computed"; import I18n from "I18n"; const generateContent = function (array, type) { @@ -29,6 +29,7 @@ export default Component.extend({ loading: or("saving", "destroying"), destroyDisabled: alias("loading"), closeDisabled: alias("loading"), + isExternal: equal("field.id", "external"), didInsertElement() { this.set("originalField", JSON.parse(JSON.stringify(this.field))); @@ -61,13 +62,14 @@ export default Component.extend({ @discourseComputed( "saving", + "isExternal", "field.name", "field.klass", "field.type", "field.serializers" ) - saveDisabled(saving) { - if (saving) { + saveDisabled(saving, isExternal) { + if (saving || isExternal) { return true; } diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index c8309f10..feb83754 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -62,6 +62,11 @@ export default Component.extend(UndoChanges, { return key; }, + @discourseComputed("action.type") + customFieldsContext(type) { + return `action.${type}`; + }, + @discourseComputed("wizard.steps") runAfterContent(steps) { let content = steps.map(function (step) { diff --git a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 index 6d65d782..7d9b0bbd 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 @@ -6,11 +6,24 @@ import { } from "discourse-common/utils/decorators"; import { getOwner } from "discourse-common/lib/get-owner"; import { defaultSelectionType, selectionTypes } from "../lib/wizard-mapper"; -import { generateName, snakeCase, userProperties } from "../lib/wizard"; +import { + generateName, + sentenceCase, + snakeCase, + userProperties, +} from "../lib/wizard"; import Component from "@ember/component"; import { bind, later } from "@ember/runloop"; import I18n from "I18n"; +const customFieldActionMap = { + topic: ["create_topic", "send_message"], + post: ["create_topic", "send_message"], + category: ["create_category"], + group: ["create_group"], + user: ["update_profile"], +}; + export default Component.extend({ classNameBindings: [":mapper-selector", "activeType"], @@ -188,11 +201,19 @@ export default Component.extend({ customFields ) { let content; + let context; + let contextType; + + if (this.options.context) { + let contextAttrs = this.options.context.split("."); + context = contextAttrs[0]; + contextType = contextAttrs[1]; + } if (activeType === "wizardField") { content = wizardFields; - if (this.options.context === "field") { + if (context === "field") { content = content.filter((field) => field.id !== currentFieldId); } } @@ -204,7 +225,7 @@ export default Component.extend({ type: a.type, })); - if (this.options.context === "action") { + if (context === "action") { content = content.filter((a) => a.id !== currentActionId); } } @@ -218,7 +239,7 @@ export default Component.extend({ .concat(userFields || []); if ( - this.options.context === "action" && + context === "action" && this.inputType === "association" && this.selectorType === "key" ) { @@ -234,7 +255,17 @@ export default Component.extend({ } if (activeType === "customField") { - content = customFields; + content = customFields + .filter((f) => { + return ( + f.type !== "json" && + customFieldActionMap[f.klass].includes(contextType) + ); + }) + .map((f) => ({ + id: f.name, + name: `${sentenceCase(f.klass)} ${f.name} (${f.type})`, + })); } return content; diff --git a/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 index 2081cfe3..404c6afd 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-custom-fields.js.es6 @@ -3,12 +3,12 @@ import CustomWizardCustomField from "../models/custom-wizard-custom-field"; export default Controller.extend({ messageKey: "create", - fieldKeys: ["klass", "type", "serializers", "name"], + fieldKeys: ["klass", "type", "name", "serializers"], documentationUrl: "https://thepavilion.io/t/3572", actions: { addField() { - this.get("customFields").pushObject( + this.get("customFields").unshiftObject( CustomWizardCustomField.create({ edit: true }) ); }, diff --git a/assets/javascripts/discourse/lib/wizard.js.es6 b/assets/javascripts/discourse/lib/wizard.js.es6 index 1896b1fe..98bdbfdd 100644 --- a/assets/javascripts/discourse/lib/wizard.js.es6 +++ b/assets/javascripts/discourse/lib/wizard.js.es6 @@ -120,4 +120,5 @@ export { listProperties, notificationLevels, wizardFieldList, + sentenceCase, }; diff --git a/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 index eaa6591c..cb2d54c3 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 @@ -2,7 +2,6 @@ import CustomWizard from "../models/custom-wizard"; import { ajax } from "discourse/lib/ajax"; import DiscourseRoute from "discourse/routes/discourse"; import I18n from "I18n"; -import { selectKitContent } from "../lib/wizard"; export default DiscourseRoute.extend({ model(params) { @@ -33,9 +32,7 @@ export default DiscourseRoute.extend({ wizardList: parentModel.wizard_list, fieldTypes, userFields: parentModel.userFields, - customFields: selectKitContent( - parentModel.custom_fields.map((f) => f.name) - ), + customFields: parentModel.custom_fields, apis: parentModel.apis, themes: parentModel.themes, wizard, diff --git a/assets/javascripts/discourse/templates/components/custom-field-input.hbs b/assets/javascripts/discourse/templates/components/custom-field-input.hbs index 205b1644..43a97be8 100644 --- a/assets/javascripts/discourse/templates/components/custom-field-input.hbs +++ b/assets/javascripts/discourse/templates/components/custom-field-input.hbs @@ -13,6 +13,11 @@ none="admin.wizard.custom_field.type.select" onChange=(action (mut field.type))}} + + {{input + value=field.name + placeholder=(i18n "admin.wizard.custom_field.name.select")}} + {{multi-select value=field.serializers @@ -20,11 +25,6 @@ none="admin.wizard.custom_field.serializers.select" onChange=(action (mut field.serializers))}} - - {{input - value=field.name - placeholder=(i18n "admin.wizard.custom_field.name.select")}} - {{#if loading}} {{loading-spinner size="small"}} @@ -51,13 +51,25 @@ {{else}} - - {{#each field.serializers as |serializer|}} - - {{/each}} - - - {{d-button action="edit" icon="pencil-alt"}} + + {{#if isExternal}} + — + {{else}} + {{#each field.serializers as |serializer|}} + + {{/each}} + {{/if}} + {{#if isExternal}} + + + + {{else}} + + {{d-button action="edit" icon="pencil-alt"}} + + {{/if}} {{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index f06e0d89..4c645cf7 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -738,7 +738,7 @@ wizardActionSelection="value" userFieldSelection="value" keyPlaceholder="admin.wizard.action.custom_fields.key" - context="action" + context=customFieldsContext )}} diff --git a/assets/javascripts/wizard/custom-wizard.js.es6 b/assets/javascripts/wizard/custom-wizard.js.es6 index 63a9ea10..8c0a473c 100644 --- a/assets/javascripts/wizard/custom-wizard.js.es6 +++ b/assets/javascripts/wizard/custom-wizard.js.es6 @@ -4,6 +4,10 @@ export default Ember.Application.extend({ rootElement: "#custom-wizard-main", Resolver: buildResolver("wizard"), + customEvents: { + paste: "paste", + }, + start() { Object.keys(requirejs._eak_seen).forEach((key) => { if (/\/pre\-initializers\//.test(key)) { diff --git a/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 b/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 index 3ede2b05..f5deb927 100644 --- a/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 +++ b/assets/javascripts/wizard/initializers/custom-wizard-field.js.es6 @@ -15,7 +15,7 @@ export default { ); const DEditor = requirejs("discourse/components/d-editor").default; const { clipboardHelpers } = requirejs("discourse/lib/utilities"); - const { toMarkdown } = requirejs("discourse/lib/to-markdown"); + const toMarkdown = requirejs("discourse/lib/to-markdown").default; FieldComponent.reopen({ classNameBindings: ["field.id"], @@ -181,7 +181,7 @@ export default { markdown = pre.match(/\S$/) ? ` ${markdown}` : markdown; } - this.appEvents.trigger("composer:insert-text", { + this.appEvents.trigger("wizard-editor:insert-text", { fieldId: this.fieldId, text: markdown, }); diff --git a/assets/stylesheets/common/wizard-admin.scss b/assets/stylesheets/common/wizard-admin.scss index 3c4b78da..9c2838eb 100644 --- a/assets/stylesheets/common/wizard-admin.scss +++ b/assets/stylesheets/common/wizard-admin.scss @@ -667,6 +667,10 @@ margin-left: 5px !important; } } + + td.external { + font-style: italic; + } } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0c364853..43b86698 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -75,7 +75,7 @@ en: edit: "You're editing an action" documentation: "Check out the action documentation" custom_fields: - create: "Create, edit or destroy a custom field record" + create: "View, create, edit and destroy custom fields" saved: "Saved custom field" error: "Failed to save: {{messages}}" documentation: Check out the custom field documentation @@ -322,6 +322,9 @@ en: custom_field: nav_label: "Custom Fields" add: "Add" + external: + label: "from another plugin" + title: "This custom field has been added by another plugin. You can use it in your wizards but you can't edit the field here." name: label: "Name" select: "underscored_name" diff --git a/controllers/custom_wizard/admin/admin.rb b/controllers/custom_wizard/admin/admin.rb index 8d5e3cad..c99954d6 100644 --- a/controllers/custom_wizard/admin/admin.rb +++ b/controllers/custom_wizard/admin/admin.rb @@ -14,7 +14,7 @@ class CustomWizard::AdminController < ::Admin::AdminController end def custom_field_list - serialize_data(CustomWizard::CustomField.list, CustomWizard::CustomFieldSerializer) + serialize_data(CustomWizard::CustomField.full_list, CustomWizard::CustomFieldSerializer) end def render_error(message) diff --git a/controllers/custom_wizard/steps.rb b/controllers/custom_wizard/steps.rb index 277b94b2..aa4fbd7f 100644 --- a/controllers/custom_wizard/steps.rb +++ b/controllers/custom_wizard/steps.rb @@ -23,12 +23,12 @@ class CustomWizard::StepsController < ::ApplicationController if updater.success? wizard_id = update_params[:wizard_id] builder = CustomWizard::Builder.new(wizard_id, current_user) - @wizard = builder.build + @wizard = builder.build(force: true) current_step = @wizard.find_step(update[:step_id]) current_submission = @wizard.current_submission result = {} - + @wizard.filter_conditional_fields if current_step.conditional_final_step && !current_step.last_step current_step.force_final = true end diff --git a/controllers/custom_wizard/wizard.rb b/controllers/custom_wizard/wizard.rb index 37728ecb..9670fd62 100644 --- a/controllers/custom_wizard/wizard.rb +++ b/controllers/custom_wizard/wizard.rb @@ -61,7 +61,7 @@ class CustomWizard::WizardController < ::ApplicationController result = success_json user = current_user - if user + if user && wizard.can_access? submission = wizard.current_submission if submission && submission['redirect_to'] result.merge!(redirect_to: submission['redirect_to']) diff --git a/extensions/custom_field/extension.rb b/extensions/custom_field/extension.rb new file mode 100644 index 00000000..876f56d4 --- /dev/null +++ b/extensions/custom_field/extension.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true +module CustomWizardCustomFieldExtension + def custom_field_types + @custom_field_types + end +end diff --git a/extensions/extra_locales_controller.rb b/extensions/extra_locales_controller.rb index e7c5a02e..6242f7ca 100644 --- a/extensions/extra_locales_controller.rb +++ b/extensions/extra_locales_controller.rb @@ -4,7 +4,8 @@ module ExtraLocalesControllerCustomWizard super || begin return false unless bundle =~ /wizard/ && request.referer =~ /\/w\// path = URI(request.referer).path - wizard_id = path.split('/w/').last + wizard_path = path.split('/w/').last + wizard_id = wizard_path.split('/').first CustomWizard::Template.exists?(wizard_id.underscore) end end diff --git a/jobs/clear_after_time_wizard.rb b/jobs/clear_after_time_wizard.rb deleted file mode 100644 index 37d997db..00000000 --- a/jobs/clear_after_time_wizard.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true -module Jobs - class ClearAfterTimeWizard < ::Jobs::Base - sidekiq_options queue: 'critical' - - def execute(args) - User.human_users.each do |u| - if u.custom_fields['redirect_to_wizard'] == args[:wizard_id] - u.custom_fields.delete('redirect_to_wizard') - u.save_custom_fields(true) - end - end - end - end -end diff --git a/lib/custom_wizard/action.rb b/lib/custom_wizard/action.rb index d68e978b..5388a326 100644 --- a/lib/custom_wizard/action.rb +++ b/lib/custom_wizard/action.rb @@ -454,32 +454,51 @@ class CustomWizard::Action data: data, user: user ).perform - - registered_fields = CustomWizard::CustomField.cached_list + registered_fields = CustomWizard::CustomField.full_list field_map.each do |field| keyArr = field[:key].split('.') value = field[:value] if keyArr.length > 1 - klass = keyArr.first - name = keyArr.last + klass = keyArr.first.to_sym + name = keyArr.second + + if keyArr.length === 3 && name.include?("{}") + name = name.gsub("{}", "") + json_attr = keyArr.last + type = :json + end else name = keyArr.first end - registered = registered_fields.select { |f| f[:name] == name } - if registered.first.present? - klass = registered.first[:klass] + registered = registered_fields.select { |f| f.name == name }.first + if registered.present? + klass = registered.klass + type = registered.type end - if klass === 'topic' + next if type === :json && json_attr.blank? + + if klass === :topic params[:topic_opts] ||= {} params[:topic_opts][:custom_fields] ||= {} - params[:topic_opts][:custom_fields][name] = value + + if type === :json + params[:topic_opts][:custom_fields][name] ||= {} + params[:topic_opts][:custom_fields][name][json_attr] = value + else + params[:topic_opts][:custom_fields][name] = value + end else - params[:custom_fields] ||= {} - params[:custom_fields][name] = value + if type === :json + params[:custom_fields][name] ||= {} + params[:custom_fields][name][json_attr] = value + else + params[:custom_fields] ||= {} + params[:custom_fields][name] = value + end end end end diff --git a/lib/custom_wizard/builder.rb b/lib/custom_wizard/builder.rb index 813680c6..a9fc6263 100644 --- a/lib/custom_wizard/builder.rb +++ b/lib/custom_wizard/builder.rb @@ -30,7 +30,7 @@ class CustomWizard::Builder def build(build_opts = {}, params = {}) return nil if !SiteSetting.custom_wizard_enabled || !@wizard - return @wizard if !@wizard.can_access? + return @wizard if !@wizard.can_access? && !build_opts[:force] build_opts[:reset] = build_opts[:reset] || @wizard.restart_on_revisit diff --git a/lib/custom_wizard/custom_field.rb b/lib/custom_wizard/custom_field.rb index e3f01a1a..9cc185ba 100644 --- a/lib/custom_wizard/custom_field.rb +++ b/lib/custom_wizard/custom_field.rb @@ -66,10 +66,12 @@ class ::CustomWizard::CustomField value = send(attr) i18n_key = "wizard.custom_field.error" - if value.blank? - if REQUIRED.include?(attr) - add_error(I18n.t("#{i18n_key}.required_attribute", attr: attr)) - end + if value.blank? && REQUIRED.include?(attr) + add_error(I18n.t("#{i18n_key}.required_attribute", attr: attr)) + break + end + + if attr == 'serializers' && !value.is_a?(Array) next end @@ -140,7 +142,7 @@ class ::CustomWizard::CustomField fields.select do |cf| if attr == :serializers - cf[attr].include?(value) + cf[attr] && cf[attr].include?(value) else cf[attr] == value end @@ -215,4 +217,32 @@ class ::CustomWizard::CustomField def self.enabled? any? end + + def self.external_list + external = [] + + CLASSES.keys.each do |klass| + field_types = klass.to_s.classify.constantize.custom_field_types + + if field_types.present? + field_types.each do |name, type| + unless list.any? { |field| field.name === name } + field = new( + 'external', + name: name, + klass: klass, + type: type + ) + external.push(field) + end + end + end + end + + external + end + + def self.full_list + (list + external_list).uniq + end end diff --git a/lib/custom_wizard/mapper.rb b/lib/custom_wizard/mapper.rb index c1187b0f..0c3543cf 100644 --- a/lib/custom_wizard/mapper.rb +++ b/lib/custom_wizard/mapper.rb @@ -5,20 +5,27 @@ class CustomWizard::Mapper USER_FIELDS = [ 'name', 'username', - 'email', 'date_of_birth', 'title', 'locale', 'trust_level', + 'email' + ] + + USER_OPTION_FIELDS = [ 'email_level', 'email_messages_level', 'email_digests' ] - PROFILE_FIELDS = ['location', 'website', 'bio_raw'] + PROFILE_FIELDS = [ + 'location', + 'website', + 'bio_raw' + ] def self.user_fields - USER_FIELDS + PROFILE_FIELDS + USER_FIELDS + USER_OPTION_FIELDS + PROFILE_FIELDS end OPERATORS = { @@ -197,11 +204,15 @@ class CustomWizard::Mapper def map_user_field(value) if value.include?(User::USER_FIELD_PREFIX) - UserCustomField.where(user_id: user.id, name: value).pluck(:value).first + user.custom_fields[value] elsif PROFILE_FIELDS.include?(value) - UserProfile.find_by(user_id: user.id).send(value) + user.user_profile.send(value) elsif USER_FIELDS.include?(value) - User.find(user.id).send(value) + user.send(value) + elsif USER_OPTION_FIELDS.include?(value) + user.user_option.send(value) + else + nil end end @@ -217,19 +228,11 @@ class CustomWizard::Mapper return string if string.blank? if opts[:user] - string.gsub!(/u\{(.*?)\}/) do |match| - result = '' - result = user.send($1) if USER_FIELDS.include?($1) - result = user.user_profile.send($1) if PROFILE_FIELDS.include?($1) - result - end + string.gsub!(/u\{(.*?)\}/) { |match| map_user_field($1) || '' } end if opts[:wizard] - string.gsub!(/w\{(.*?)\}/) do |match| - value = recurse(data, [*$1.split('.')]) - value.present? ? value : '' - end + string.gsub!(/w\{(.*?)\}/) { |match| recurse(data, [*$1.split('.')]) || '' } end if opts[:value] diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb index a1c0aad0..8e944dca 100644 --- a/lib/custom_wizard/template.rb +++ b/lib/custom_wizard/template.rb @@ -49,18 +49,15 @@ class CustomWizard::Template def self.remove(wizard_id) wizard = CustomWizard::Wizard.create(wizard_id) - return false if !wizard ActiveRecord::Base.transaction do PluginStore.remove(CustomWizard::PLUGIN_NAME, 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 + clear_user_wizard_redirect(wizard_id) end + Jobs.cancel_scheduled_job(:set_after_time_wizard) if wizard.after_time + true end @@ -88,6 +85,10 @@ class CustomWizard::Template end end + def self.clear_user_wizard_redirect(wizard_id) + UserCustomField.where(name: 'redirect_to_wizard', value: wizard_id).destroy_all + end + private def normalize_data @@ -132,7 +133,7 @@ class CustomWizard::Template 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) + self.class.clear_user_wizard_redirect(wizard_id) end end end diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index 82693eed..f92f3d61 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -247,6 +247,26 @@ class CustomWizard::Wizard set_submissions(submissions) end + def filter_conditional_fields + included_fields = steps.map { |s| s.fields.map { |f| f.id } }.flatten + filtered_submision = current_submission&.select do |key, _| + key = key.to_s + included_fields.include?(key) || + required_fields.include?(key) || + key.include?("action") + end + + save_submission(filtered_submision) + end + + def required_fields + %w{ + submitted_at + route_to + saved_param + } + end + def final_cleanup! if id == user.custom_fields['redirect_to_wizard'] user.custom_fields.delete('redirect_to_wizard') diff --git a/package.json b/package.json index c6692218..f06823c8 100644 --- a/package.json +++ b/package.json @@ -2,9 +2,9 @@ "name": "discourse-custom-wizard", "version": "1.0.0", "repository": "git@github.com:paviliondev/discourse-custom-wizard.git", - "author": "Discourse", - "license": "MIT", + "author": "Pavilion", + "license": "GPL V2", "devDependencies": { "eslint-config-discourse": "^1.1.8" } -} +} \ No newline at end of file diff --git a/plugin.rb b/plugin.rb index 6f5f203a..e6287a44 100644 --- a/plugin.rb +++ b/plugin.rb @@ -65,7 +65,6 @@ after_initialize do ../controllers/custom_wizard/wizard.rb ../controllers/custom_wizard/steps.rb ../controllers/custom_wizard/realtime_validations.rb - ../jobs/clear_after_time_wizard.rb ../jobs/refresh_api_access_token.rb ../jobs/set_after_time_wizard.rb ../lib/custom_wizard/validators/template.rb @@ -109,6 +108,7 @@ after_initialize do ../extensions/users_controller.rb ../extensions/custom_field/preloader.rb ../extensions/custom_field/serializer.rb + ../extensions/custom_field/extension.rb ].each do |path| load File.expand_path(path, __FILE__) end @@ -201,18 +201,18 @@ after_initialize do end CustomWizard::CustomField::CLASSES.keys.each do |klass| + class_constant = klass.to_s.classify.constantize + add_model_callback(klass, :after_initialize) do if CustomWizard::CustomField.enabled? CustomWizard::CustomField.list_by(:klass, klass.to_s).each do |field| - klass.to_s - .classify - .constantize - .register_custom_field_type(field[:name], field[:type].to_sym) + class_constant.register_custom_field_type(field[:name], field[:type].to_sym) end end end - klass.to_s.classify.constantize.singleton_class.prepend CustomWizardCustomFieldPreloader + class_constant.singleton_class.prepend CustomWizardCustomFieldPreloader + class_constant.singleton_class.prepend CustomWizardCustomFieldExtension end CustomWizard::CustomField.serializers.each do |serializer_klass| diff --git a/spec/components/custom_wizard/action_spec.rb b/spec/components/custom_wizard/action_spec.rb index 28f2cab8..9910c0bd 100644 --- a/spec/components/custom_wizard/action_spec.rb +++ b/spec/components/custom_wizard/action_spec.rb @@ -72,6 +72,42 @@ describe CustomWizard::Action do raw: "topic body" ).exists?).to eq(false) end + + it "adds custom fields" 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 + ).first + topic_custom_field = TopicCustomField.where( + name: "topic_field", + value: "Topic custom field value", + topic_id: topic.id + ) + topic_json_custom_field = TopicCustomField.where(" + name = 'topic_json_field' AND + (value::json->>'key_1') = 'Key 1 value' AND + (value::json->>'key_2') = 'Key 2 value' AND + topic_id = #{topic.id}" + ) + post_custom_field = PostCustomField.where( + name: "post_field", + value: "Post custom field value", + post_id: topic.first_post.id + ) + expect(topic_custom_field.exists?).to eq(true) + expect(topic_json_custom_field.exists?).to eq(true) + expect(post_custom_field.exists?).to eq(true) + end end context 'sending a message' do diff --git a/spec/components/custom_wizard/custom_field_spec.rb b/spec/components/custom_wizard/custom_field_spec.rb index 3c9f1706..b17e26c6 100644 --- a/spec/components/custom_wizard/custom_field_spec.rb +++ b/spec/components/custom_wizard/custom_field_spec.rb @@ -49,6 +49,40 @@ describe CustomWizard::CustomField do end context "validation" do + it "does not save without required attributes" do + invalid_field_json = custom_field_json['custom_fields'].first + invalid_field_json['klass'] = nil + + custom_field = CustomWizard::CustomField.new(nil, invalid_field_json) + expect(custom_field.save).to eq(false) + expect(custom_field.valid?).to eq(false) + expect(custom_field.errors.full_messages.first).to eq( + I18n.t("wizard.custom_field.error.required_attribute", attr: "klass") + ) + expect( + PluginStoreRow.where( + plugin_name: CustomWizard::CustomField::NAMESPACE, + key: custom_field.name + ).exists? + ).to eq(false) + end + + it "does save without optional attributes" do + field_json = custom_field_json['custom_fields'].first + field_json['serializers'] = nil + + custom_field = CustomWizard::CustomField.new(nil, field_json) + expect(custom_field.save).to eq(true) + expect(custom_field.valid?).to eq(true) + expect( + PluginStoreRow.where(" + plugin_name = '#{CustomWizard::CustomField::NAMESPACE}' AND + key = '#{custom_field.name}' AND + value::jsonb = '#{field_json.except('name').to_json}'::jsonb + ",).exists? + ).to eq(true) + end + it "does not save with an unsupported class" do invalid_field_json = custom_field_json['custom_fields'].first invalid_field_json['klass'] = 'user' @@ -178,6 +212,22 @@ describe CustomWizard::CustomField do it "lists saved custom field records by attribute value" do expect(CustomWizard::CustomField.list_by(:klass, 'topic').length).to eq(1) end + + it "lists saved custom field records by optional values" do + field_json = custom_field_json['custom_fields'].first + field_json['serializers'] = nil + + custom_field = CustomWizard::CustomField.new(nil, field_json) + expect(CustomWizard::CustomField.list_by(:serializers, ['post']).length).to eq(0) + end + + it "lists custom field records added by other plugins " do + expect(CustomWizard::CustomField.external_list.length).to eq(11) + end + + it "lists all custom field records" do + expect(CustomWizard::CustomField.full_list.length).to eq(15) + end end it "is enabled if there are custom fields" do diff --git a/spec/components/custom_wizard/mapper_spec.rb b/spec/components/custom_wizard/mapper_spec.rb index 434f0001..ed66d7c1 100644 --- a/spec/components/custom_wizard/mapper_spec.rb +++ b/spec/components/custom_wizard/mapper_spec.rb @@ -229,28 +229,40 @@ describe CustomWizard::Mapper do ).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 + context "interpolates" do + it "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 "user emails" do + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_user_email'], + data: data, + user: user1 + ).perform).to eq("Email: angus@email.com") + 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")}") + it "user options" do + user1.user_option.update_columns(email_level: UserOption.email_level_types[:never]) + + expect(CustomWizard::Mapper.new( + inputs: inputs['interpolate_user_option'], + data: data, + user: user1 + ).perform).to eq("Email Level: #{UserOption.email_level_types[:never]}") + end + + it "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 end it "handles greater than pairs" do diff --git a/spec/components/custom_wizard/template_spec.rb b/spec/components/custom_wizard/template_spec.rb index fb76e0c4..0e3dbdbe 100644 --- a/spec/components/custom_wizard/template_spec.rb +++ b/spec/components/custom_wizard/template_spec.rb @@ -41,6 +41,14 @@ describe CustomWizard::Template do ).to eq(nil) end + it "removes user wizard redirects if template is removed" do + user.custom_fields['redirect_to_wizard'] = 'super_mega_fun_wizard' + user.save_custom_fields(true) + + CustomWizard::Template.remove('super_mega_fun_wizard') + expect(user.reload.custom_fields['redirect_to_wizard']).to eq(nil) + end + it "checks for wizard template existence" do expect( CustomWizard::Template.exists?('super_mega_fun_wizard') diff --git a/spec/components/custom_wizard/wizard_spec.rb b/spec/components/custom_wizard/wizard_spec.rb index aed44fe6..9808f32f 100644 --- a/spec/components/custom_wizard/wizard_spec.rb +++ b/spec/components/custom_wizard/wizard_spec.rb @@ -173,6 +173,8 @@ describe CustomWizard::Wizard do progress_step("step_2", acting_user: trusted_user) progress_step("step_3", acting_user: trusted_user) + @permitted_template["multiple_submissions"] = true + expect( CustomWizard::Wizard.new(@permitted_template, trusted_user).can_access? ).to eq(true) diff --git a/spec/extensions/extra_locales_controller_spec.rb b/spec/extensions/extra_locales_controller_spec.rb index 91a4e8c3..a71e39c4 100644 --- a/spec/extensions/extra_locales_controller_spec.rb +++ b/spec/extensions/extra_locales_controller_spec.rb @@ -37,6 +37,13 @@ describe ExtraLocalesControllerCustomWizard, type: :request do expect(response.status).to eq(200) end + it "returns wizard locales when requested by user in a wizard step" do + sign_in(new_user) + + get @locale_url, headers: { 'REFERER' => "/w/super-mega-fun-wizard/steps/step_1" } + expect(response.status).to eq(200) + end + it "return wizard locales if user cant access wizard" do template[:permitted] = permitted["permitted"] CustomWizard::Template.save(template.as_json) diff --git a/spec/fixtures/mapper/inputs.json b/spec/fixtures/mapper/inputs.json index f7d98903..443f186b 100644 --- a/spec/fixtures/mapper/inputs.json +++ b/spec/fixtures/mapper/inputs.json @@ -57,6 +57,22 @@ "output": "Name: u{name}" } ], + "interpolate_user_email": [ + { + "type": "assignment", + "output_type": "text", + "output_connector": "set", + "output": "Email: u{email}" + } + ], + "interpolate_user_option": [ + { + "type": "assignment", + "output_type": "text", + "output_connector": "set", + "output": "Email Level: u{email_level}" + } + ], "interpolate_wizard_field": [ { "type": "assignment", diff --git a/spec/fixtures/wizard.json b/spec/fixtures/wizard.json index c21d445c..a505c0d3 100644 --- a/spec/fixtures/wizard.json +++ b/spec/fixtures/wizard.json @@ -3,7 +3,6 @@ "name": "Super Mega Fun Wizard", "background": "#333333", "save_submissions": true, - "multiple_submissions": true, "after_signup": false, "prompt_completion": false, "theme_id": 2, @@ -391,10 +390,34 @@ "pairs": [ { "index": 0, - "key": "custom_field_1", + "key": "post_field", "key_type": "text", - "value": "title", - "value_type": "user_field", + "value": "Post custom field value", + "value_type": "text", + "connector": "association" + }, + { + "index": 1, + "key": "topic.topic_field", + "key_type": "text", + "value": "Topic custom field value", + "value_type": "text", + "connector": "association" + }, + { + "index": 2, + "key": "topic.topic_json_field{}.key_1", + "key_type": "text", + "value": "Key 1 value", + "value_type": "text", + "connector": "association" + }, + { + "index": 3, + "key": "topic.topic_json_field{}.key_2", + "key_type": "text", + "value": "Key 2 value", + "value_type": "text", "connector": "association" } ] diff --git a/spec/jobs/clear_after_time_wizard_spec.rb b/spec/jobs/clear_after_time_wizard_spec.rb deleted file mode 100644 index 935036a3..00000000 --- a/spec/jobs/clear_after_time_wizard_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require_relative '../plugin_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 - } - - it "clears wizard redirect for all users " do - after_time_template = template.dup - after_time_template["after_time"] = true - after_time_template["after_time_scheduled"] = (Time.now + 3.hours).iso8601 - - CustomWizard::Template.save(after_time_template) - - Jobs::SetAfterTimeWizard.new.execute(wizard_id: 'super_mega_fun_wizard') - - expect( - UserCustomField.where( - name: 'redirect_to_wizard', - value: 'super_mega_fun_wizard' - ).length - ).to eq(3) - - 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 diff --git a/spec/plugin_helper.rb b/spec/plugin_helper.rb index 6680874f..93f33a81 100644 --- a/spec/plugin_helper.rb +++ b/spec/plugin_helper.rb @@ -6,7 +6,7 @@ if ENV['SIMPLECOV'] SimpleCov.start do root "plugins/discourse-custom-wizard" track_files "plugins/discourse-custom-wizard/**/*.rb" - add_filter { |src| src.filename =~ /(\/spec\/|\/db\/|plugin\.rb|api)/ } + add_filter { |src| src.filename =~ /(\/spec\/|\/db\/|plugin\.rb|api|gems)/ } SimpleCov.minimum_coverage 80 end end diff --git a/spec/requests/custom_wizard/admin/custom_fields_controller_spec.rb b/spec/requests/custom_wizard/admin/custom_fields_controller_spec.rb index e006e65a..8c1a8550 100644 --- a/spec/requests/custom_wizard/admin/custom_fields_controller_spec.rb +++ b/spec/requests/custom_wizard/admin/custom_fields_controller_spec.rb @@ -17,9 +17,9 @@ describe CustomWizard::AdminCustomFieldsController do sign_in(admin_user) end - it "returns the list of custom fields" do + it "returns the full list of custom fields" do get "/admin/wizards/custom-fields.json" - expect(response.parsed_body.length).to eq(4) + expect(response.parsed_body.length).to eq(15) end it "saves custom fields" do diff --git a/spec/requests/custom_wizard/application_controller_spec.rb b/spec/requests/custom_wizard/application_controller_spec.rb index f79db877..e8b45c48 100644 --- a/spec/requests/custom_wizard/application_controller_spec.rb +++ b/spec/requests/custom_wizard/application_controller_spec.rb @@ -43,6 +43,12 @@ describe ApplicationController do .first['redirect_to'] ).to eq("/t/2") end + + it "does not redirect if wizard does not exist" do + CustomWizard::Template.remove('super_mega_fun_wizard') + get "/" + expect(response.status).to eq(200) + end end context "who is not required to complete wizard" do diff --git a/spec/requests/custom_wizard/steps_controller_spec.rb b/spec/requests/custom_wizard/steps_controller_spec.rb index c58f13a2..2424274b 100644 --- a/spec/requests/custom_wizard/steps_controller_spec.rb +++ b/spec/requests/custom_wizard/steps_controller_spec.rb @@ -249,4 +249,33 @@ describe CustomWizard::StepsController do expect(response.status).to eq(200) expect(response.parsed_body['final']).to eq(true) end + + it "excludes the non-included conditional fields from the submissions" do + new_template = wizard_template.dup + new_template['steps'][1]['fields'][0]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will pass" + } + } + + put '/w/super-mega-fun-wizard/steps/step_2.json', params: { + fields: { + step_2_field_1: "1995-04-23" + } + } + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will not pass" + } + } + + wizard_id = response.parsed_body['wizard']['id'] + wizard = CustomWizard::Wizard.create(wizard_id, user) + submission = wizard.submissions.last + expect(submission.keys).not_to include("step_2_field_1") + end end diff --git a/spec/requests/custom_wizard/wizard_controller_spec.rb b/spec/requests/custom_wizard/wizard_controller_spec.rb index 3e7ddd3d..4380bc73 100644 --- a/spec/requests/custom_wizard/wizard_controller_spec.rb +++ b/spec/requests/custom_wizard/wizard_controller_spec.rb @@ -11,6 +11,14 @@ describe CustomWizard::WizardController do ) } + let(:permitted_json) { + JSON.parse( + File.open( + "#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard/permitted.json" + ).read + ) + } + before do CustomWizard::Template.save( JSON.parse(File.open( @@ -47,6 +55,14 @@ describe CustomWizard::WizardController do expect(response.status).to eq(200) end + it 'lets user skip if user cant access wizard' do + @template["permitted"] = permitted_json["permitted"] + CustomWizard::Template.save(@template, skip_jobs: true) + + 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)