From ee61c1deb3f8a4c2d1a987b5c8af6ab0317d9c5e Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Mon, 30 Mar 2020 17:16:03 +1100 Subject: [PATCH] add permitted setting --- .../components/wizard-custom-field.js.es6 | 6 +- .../components/wizard-custom-input.js.es6 | 11 +- .../components/wizard-field-mapper.js.es6 | 6 + .../discourse/lib/custom-wizard.js.es6 | 12 +- .../discourse/models/custom-wizard.js.es6 | 6 +- .../discourse/templates/admin-wizard.hbs | 36 +- .../components/wizard-custom-field.hbs | 8 +- .../components/wizard-field-mapper.hbs | 8 +- assets/javascripts/wizard-custom.js | 1 + .../components/wizard-field-group.js.es6 | 5 - .../components/wizard-group-selector.js.es6 | 11 +- .../wizard/routes/custom-index.js.es6 | 1 - .../components/wizard-field-category.hbs | 2 +- .../components/wizard-field-group.hbs | 3 +- .../wizard/templates/custom.index.hbs | 2 +- assets/stylesheets/wizard_custom_admin.scss | 4 - config/locales/client.en.yml | 9 +- config/locales/client.fr.yml | 2 - controllers/custom_wizard/admin.rb | 3 - lib/custom_wizard/actions.rb | 289 ++++++++++++ lib/custom_wizard/builder.rb | 429 ++---------------- lib/custom_wizard/mapper.rb | 114 +++++ lib/custom_wizard/template.rb | 6 +- lib/custom_wizard/wizard.rb | 46 +- lib/wizard/field.rb | 4 +- plugin.rb | 82 ++-- .../custom_wizard/wizard_field_serializer.rb | 6 +- .../custom_wizard/wizard_serializer.rb | 1 - 28 files changed, 589 insertions(+), 524 deletions(-) delete mode 100644 assets/javascripts/wizard/components/wizard-field-group.js.es6 create mode 100644 lib/custom_wizard/actions.rb create mode 100644 lib/custom_wizard/mapper.rb diff --git a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 index f5d85a48..93d5143a 100644 --- a/assets/javascripts/discourse/components/wizard-custom-field.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-field.js.es6 @@ -46,11 +46,11 @@ export default Ember.Component.extend({ return options; }, - canFilter: or('isCategory', 'isTag', 'isGroup'), + contentEnabled: or('isCategory', 'isTag', 'isGroup'), @computed('field.type') - filterOptions(fieldType) { - if (!this.canFilter) return {}; + contentOptions(fieldType) { + if (!this.contentEnabled) return {}; let options = { hasOutput: true, diff --git a/assets/javascripts/discourse/components/wizard-custom-input.js.es6 b/assets/javascripts/discourse/components/wizard-custom-input.js.es6 index 1dc54a8e..349fee7d 100644 --- a/assets/javascripts/discourse/components/wizard-custom-input.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-input.js.es6 @@ -23,9 +23,14 @@ export default Ember.Component.extend({ if (!this.type) this.set('type', defaultInputType(this.options)); }, - @discourseComputed - inputTypes() { - return ['conditional', 'assignment'].map((type) => { + @discourseComputed('options.allowedInputs') + allowedInputs(option) { + return option || 'conditional,assignment'; + }, + + @discourseComputed('allowedInputs') + inputTypes(allowedInputs) { + return allowedInputs.split(',').map((type) => { return { id: type, name: I18n.t(`admin.wizard.input.${type}.prefix`) diff --git a/assets/javascripts/discourse/components/wizard-field-mapper.js.es6 b/assets/javascripts/discourse/components/wizard-field-mapper.js.es6 index a0b352f0..aac69c5b 100644 --- a/assets/javascripts/discourse/components/wizard-field-mapper.js.es6 +++ b/assets/javascripts/discourse/components/wizard-field-mapper.js.es6 @@ -1,9 +1,15 @@ import { getOwner } from 'discourse-common/lib/get-owner'; import { on } from 'discourse-common/utils/decorators'; import { newInput } from '../lib/custom-wizard'; +import { default as discourseComputed } from 'discourse-common/utils/decorators'; export default Ember.Component.extend({ classNames: 'field-mapper', + + @discourseComputed('inputs.[]', 'options.singular') + canAdd(inputs, singular) { + return !singular || !inputs || inputs.length < 1; + }, actions: { add() { diff --git a/assets/javascripts/discourse/lib/custom-wizard.js.es6 b/assets/javascripts/discourse/lib/custom-wizard.js.es6 index 8ffdcd97..2c5b373e 100644 --- a/assets/javascripts/discourse/lib/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/lib/custom-wizard.js.es6 @@ -10,16 +10,15 @@ function generateName(id) { const profileFields = [ 'name', - 'user_avatar', + 'username', + 'email', 'date_of_birth', 'title', 'locale', 'location', 'website', 'bio_raw', - 'profile_background', - 'card_background', - 'theme_id' + 'trust_level' ]; const connectors = [ @@ -69,7 +68,10 @@ const inputTypes = [ ] function defaultInputType(options = {}) { - return options.hasOutput ? 'conditional' : 'pair'; + if (!options.hasOutput) return 'pair'; + const allowedInputs = options.allowedInputs; + if (!allowedInputs) return 'conditional'; + return allowedInputs.split(',')[0]; } function defaultSelectionType(inputType, options = {}) { diff --git a/assets/javascripts/discourse/models/custom-wizard.js.es6 b/assets/javascripts/discourse/models/custom-wizard.js.es6 index 2706acea..fdcc045d 100644 --- a/assets/javascripts/discourse/models/custom-wizard.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard.js.es6 @@ -12,8 +12,8 @@ const wizardProperties = [ 'required', 'prompt_completion', 'restart_on_revisit', - 'min_trust', - 'theme_id' + 'theme_id', + 'permitted' ]; const CustomWizard = EmberObject.extend({ @@ -267,7 +267,7 @@ CustomWizard.reopenClass({ props['required'] = false; props['prompt_completion'] = false; props['restart_on_revisit'] = false; - props['min_trust'] = 0; + props['permitted'] = null; props['steps'] = Ember.A(); }; diff --git a/assets/javascripts/discourse/templates/admin-wizard.hbs b/assets/javascripts/discourse/templates/admin-wizard.hbs index 31064780..40bd6be3 100644 --- a/assets/javascripts/discourse/templates/admin-wizard.hbs +++ b/assets/javascripts/discourse/templates/admin-wizard.hbs @@ -22,13 +22,7 @@ placeholderKey="admin.wizard.name_placeholder"}} - - -
- {{i18n 'admin.wizard.label'}} -
- -
+
@@ -40,7 +34,13 @@ placeholderKey="admin.wizard.background_placeholder"}}
- +
+ +
+ {{i18n 'admin.wizard.label'}} +
+ +
@@ -106,16 +106,6 @@
-
-
- -
-
- {{i18n 'admin.wizard.min_trust_label'}} - {{input type='number' value=model.min_trust class='input-small'}} -
-
-
@@ -144,17 +134,17 @@
- +
{{wizard-field-mapper - inputs=model.group + inputs=model.permitted options=(hash hasOutput=true - enableConnectors=true - userFieldSelection='key,value' - groupSelection=true + groupSelection='output' textDisabled='output' + allowedInputs='assignment' + singular=true )}}
diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs index d4251c2e..0ebb3229 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-field.hbs @@ -170,17 +170,17 @@
-{{#if canFilter}} +{{#if contentEnabled}}
- +
{{wizard-field-mapper - inputs=field.filters + inputs=field.content wizardFields=wizardFields - options=filterOptions}} + options=contentOptions}}
{{/if}} diff --git a/assets/javascripts/discourse/templates/components/wizard-field-mapper.hbs b/assets/javascripts/discourse/templates/components/wizard-field-mapper.hbs index a7cb5375..322a8a77 100644 --- a/assets/javascripts/discourse/templates/components/wizard-field-mapper.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-field-mapper.hbs @@ -10,6 +10,8 @@ remove=(action 'remove')}} {{/each}} -
- {{d-button action='add' label='admin.wizard.add' icon='plus'}} -
\ No newline at end of file +{{#if canAdd}} +
+ {{d-button action='add' label='admin.wizard.add' icon='plus'}} +
+{{/if}} \ No newline at end of file diff --git a/assets/javascripts/wizard-custom.js b/assets/javascripts/wizard-custom.js index acc1cb3d..e03cf6a8 100644 --- a/assets/javascripts/wizard-custom.js +++ b/assets/javascripts/wizard-custom.js @@ -30,6 +30,7 @@ //= require discourse/lib/show-modal //= require discourse/lib/key-value-store //= require discourse/lib/settings +//= require discourse/lib/user-presence //= require discourse/mixins/singleton diff --git a/assets/javascripts/wizard/components/wizard-field-group.js.es6 b/assets/javascripts/wizard/components/wizard-field-group.js.es6 deleted file mode 100644 index 3f94b6ae..00000000 --- a/assets/javascripts/wizard/components/wizard-field-group.js.es6 +++ /dev/null @@ -1,5 +0,0 @@ -export default Ember.Component.extend({ - didInsertElement() { - console.log(this.field) - } -}) \ No newline at end of file diff --git a/assets/javascripts/wizard/components/wizard-group-selector.js.es6 b/assets/javascripts/wizard/components/wizard-group-selector.js.es6 index af21db23..0029bae2 100644 --- a/assets/javascripts/wizard/components/wizard-group-selector.js.es6 +++ b/assets/javascripts/wizard/components/wizard-group-selector.js.es6 @@ -2,11 +2,16 @@ import ComboBox from 'select-kit/components/combo-box'; import { computed } from "@ember/object"; import { makeArray } from "discourse-common/lib/helpers"; -export default ComboBox.extend({ - content: computed("groups.[]", "whitelist.[]", function() { - const whitelist = makeArray(this.whitelist); +export default ComboBox.extend({ + content: computed("groups.[]", "field.content.[]", function() { + const whitelist = makeArray(this.field.content); return this.groups.filter(group => { return !whitelist.length || whitelist.indexOf(group.id) > -1; + }).map(g => { + return { + id: g.id, + name: g.name + } }); }) }) \ No newline at end of file diff --git a/assets/javascripts/wizard/routes/custom-index.js.es6 b/assets/javascripts/wizard/routes/custom-index.js.es6 index c857753d..c15dfb98 100644 --- a/assets/javascripts/wizard/routes/custom-index.js.es6 +++ b/assets/javascripts/wizard/routes/custom-index.js.es6 @@ -14,7 +14,6 @@ export default Ember.Route.extend({ if (model) { const completed = model.get('completed'); const permitted = model.get('permitted'); - const minTrust = model.get('min_trust'); const wizardId = model.get('id'); const user = model.get('user'); const name = model.get('name'); diff --git a/assets/javascripts/wizard/templates/components/wizard-field-category.hbs b/assets/javascripts/wizard/templates/components/wizard-field-category.hbs index 5ea598a3..1843a277 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-category.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-category.hbs @@ -1,6 +1,6 @@ {{wizard-category-selector categories=categories - whitelist=field.filter + whitelist=field.content maximum=field.limit onChange=(action (mut categories))}} diff --git a/assets/javascripts/wizard/templates/components/wizard-field-group.hbs b/assets/javascripts/wizard/templates/components/wizard-field-group.hbs index dc7be340..f10aae2e 100644 --- a/assets/javascripts/wizard/templates/components/wizard-field-group.hbs +++ b/assets/javascripts/wizard/templates/components/wizard-field-group.hbs @@ -1,6 +1,7 @@ {{wizard-group-selector groups=wizard.groups - whitelist=field.filter + field=field + whitelist=field.content value=field.value onChange=(action (mut field.value)) options=(hash diff --git a/assets/javascripts/wizard/templates/custom.index.hbs b/assets/javascripts/wizard/templates/custom.index.hbs index f6ac1df5..c8e8966c 100644 --- a/assets/javascripts/wizard/templates/custom.index.hbs +++ b/assets/javascripts/wizard/templates/custom.index.hbs @@ -5,7 +5,7 @@ {{wizard-no-access text=(i18n 'wizard.requires_login' name=name) wizardId=wizardId}} {{else}} {{#if notPermitted}} - {{wizard-no-access text=(i18n 'wizard.not_permitted' name=name level=minTrust) wizardId=wizardId}} + {{wizard-no-access text=(i18n 'wizard.not_permitted' name=name) wizardId=wizardId}} {{else}} {{#if completed}} {{wizard-no-access text=(i18n 'wizard.completed' name=name) wizardId=wizardId}} diff --git a/assets/stylesheets/wizard_custom_admin.scss b/assets/stylesheets/wizard_custom_admin.scss index 71814605..4bc608ff 100644 --- a/assets/stylesheets/wizard_custom_admin.scss +++ b/assets/stylesheets/wizard_custom_admin.scss @@ -61,10 +61,6 @@ $setting-background: dark-light-diff($primary, $secondary, 96%, -65%); } } -.wizard-basic-details { - margin-bottom: 10px; -} - .content-list + .content { overflow: hidden; } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 6d713193..2cc266f6 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -36,8 +36,6 @@ en: prompt_completion_label: "Prompt user to complete wizard." restart_on_revisit: "Restart" restart_on_revisit_label: "Restart the the wizard whenever the user revisits, regardless of prior progress." - min_trust: "Trust" - min_trust_label: "Trust level required to access wizard." theme_id: "Theme" no_theme: "Select a Theme (optional)" save: "Save Changes" @@ -61,6 +59,7 @@ en: submission_key: 'submission key' param_key: 'param' group: "Group" + permitted: "Permitted" editor: show: "Show" @@ -125,7 +124,7 @@ en: limit: "Limit" property: "Property" prefill: "Prefill" - filter: "Content" + content: "Content" action: header: "Actions" @@ -306,12 +305,12 @@ en: wizard: completed: "You have completed the {{name}} wizard." - not_permitted: "You need to be trust level {{level}} or higher to access the {{name}} wizard." + not_permitted: "You are not permitted to access the {{name}} wizard." none: "There is no wizard here." return_to_site: "Return to {{siteName}}" requires_login: "You need to be logged in to access the {{name}} wizard." reset: "Reset this wizard." - step_not_permitted: "You're not allowed to view this step." + step_not_permitted: "You're not permitted to view this step." wizard_composer: show_preview: "Preview Post" diff --git a/config/locales/client.fr.yml b/config/locales/client.fr.yml index 65e5335e..45169ac4 100644 --- a/config/locales/client.fr.yml +++ b/config/locales/client.fr.yml @@ -33,8 +33,6 @@ fr: required_label: "Les utilisatrices doivent compléter l'assistant." prompt_completion: "Invitation" prompt_completion_label: "Inviter l'utilisatrice à compléter l'assistant." - min_trust: "Confiance" - min_trust_label: "Niveau de confiance requis pour accéder à l'assistant." theme_id: "Thème" no_theme: "Choisir un thème (optionnel)" save: "Sauvegarder" diff --git a/controllers/custom_wizard/admin.rb b/controllers/custom_wizard/admin.rb index 2b09fcf2..23944c1e 100644 --- a/controllers/custom_wizard/admin.rb +++ b/controllers/custom_wizard/admin.rb @@ -14,11 +14,8 @@ class CustomWizard::AdminController < ::ApplicationController params.require(:wizard) wizard = ::JSON.parse(params[:wizard]) - existing = PluginStore.get('custom_wizard', wizard['id']) || {} - new_time = false - error = nil if wizard["id"].blank? diff --git a/lib/custom_wizard/actions.rb b/lib/custom_wizard/actions.rb new file mode 100644 index 00000000..995281c7 --- /dev/null +++ b/lib/custom_wizard/actions.rb @@ -0,0 +1,289 @@ +class CustomWizard::Action + attr_accessor :data, + :action, + :user, + :updater + + def initialize(params) + @action = params[:action] + @user = params[:user] + @data = params[:data] + @updater = params[:updater] + end + + def perform + ActiveRecord::Base.transaction { self.send(action['type'].to_sym) } + end + + def mapper + @mapper ||= CustomWizard::Mapper.new(user: user, data: data) + end + + def create_topic + if action['custom_title_enabled'] + title = mapper.interpolate(action['custom_title']) + else + title = data[action['title']] + end + + if action['post_builder'] + post = mapper.interpolate(action['post_template']) + else + post = data[action['post']] + end + + if title + params = { + title: title, + raw: post, + skip_validations: true + } + + params[:category] = action_category_id(action, data) + tags = action_tags(action, data) + params[:tags] = tags + + topic_custom_fields = {} + + if action['add_fields'] + action['add_fields'].each do |field| + value = field['value_custom'].present? ? field['value_custom'] : data[field['value']] + key = field['key'] + + if key && (value.present? || value === false) + if key.include?('custom_fields') + keyArr = key.split('.') + + if keyArr.length === 3 + custom_key = keyArr.last + type = keyArr.first + + if type === 'topic' + topic_custom_fields[custom_key] = value + elsif type === 'post' + params[:custom_fields] ||= {} + params[:custom_fields][custom_key.to_sym] = value + end + end + else + value = [*value] + tags if key === 'tags' + params[key.to_sym] = value + end + end + end + end + + creator = PostCreator.new(user, params) + post = creator.create + + if creator.errors.present? + updater.errors.add(:create_topic, creator.errors.full_messages.join(" ")) + else + if topic_custom_fields.present? + topic_custom_fields.each do |k, v| + post.topic.custom_fields[k] = v + end + post.topic.save_custom_fields(true) + end + + unless action['skip_redirect'] + data['redirect_on_complete'] = post.topic.url + end + end + end + end + + def send_message + if action['required'].present? && data[action['required']].blank? + return + end + + if action['custom_title_enabled'] + title = mapper.interpolate(action['custom_title']) + else + title = data[action['title']] + end + + if action['post_builder'] + post = mapper.interpolate(action['post_template']) + else + post = data[action['post']] + end + + if title && post + creator = PostCreator.new(user, + title: title, + raw: post, + archetype: Archetype.private_message, + target_usernames: action['username'] + ) + + post = creator.create + + if creator.errors.present? + updater.errors.add(:send_message, creator.errors.full_messages.join(" ")) + else + unless action['skip_redirect'] + data['redirect_on_complete'] = post.topic.url + end + end + end + end + + def update_profile + return unless action['profile_updates'].length + + attributes = {} + custom_fields = {} + + action['profile_updates'].each do |pu| + value = pu['value'] + key = pu['key'] + + return if data[key].blank? + + if user_field || custom_field + custom_fields[user_field || custom_field] = data[key] + else + updater_key = value + if ['profile_background', 'card_background'].include?(value) + updater_key = "#{value}_upload_url" + end + attributes[updater_key.to_sym] = data[key] if updater_key + end + + if ['user_avatar'].include?(value) + this_upload_id = data[key][:id] + user.create_user_avatar unless user.user_avatar + user.user_avatar.custom_upload_id = this_upload_id + user.uploaded_avatar_id = this_upload_id + user.save! + user.user_avatar.save! + end + end + + if custom_fields.present? + attributes[:custom_fields] = custom_fields + end + + if attributes.present? + user_updater = UserUpdater.new(user, user) + user_updater.update(attributes) + end + end + + def send_to_api + api_body = nil + + if action['api_body'] != "" + begin + api_body_parsed = JSON.parse(action['api_body']) + rescue JSON::ParserError + raise Discourse::InvalidParameters, "Invalid API body definition: #{action['api_body']} for #{action['title']}" + end + api_body = JSON.parse(mapper.interpolate(JSON.generate(api_body_parsed))) + end + + result = CustomWizard::Api::Endpoint.request(user, action['api'], action['api_endpoint'], api_body) + + if error = result['error'] || (result[0] && result[0]['error']) + error = error['message'] || error + updater.errors.add(:send_to_api, error) + else + ## add validation callback + end + end + + def open_composer + if action['custom_title_enabled'] + title = mapper.interpolate(action['custom_title']) + else + title = data[action['title']] + end + + url = "/new-topic?title=#{title}" + + if action['post_builder'] + post = mapper.interpolate(action['post_template']) + else + post = data[action['post']] + end + + url += "&body=#{post}" + + if category_id = action_category_id(action, data) + if category = Category.find(category_id) + url += "&category=#{category.full_slug('/')}" + end + end + + if tags = action_tags(action, data) + url += "&tags=#{tags.join(',')}" + end + + data['redirect_on_complete'] = Discourse.base_uri + URI.encode(url) + end + + def add_to_group + groups = CustomWizard::Mapper.new( + inputs: action['inputs'], + data: data, + user: user, + opts: { + multiple: true + } + ).output + + groups = groups.flatten.reduce([]) do |result, g| + begin + result.push(Integer(g)) + rescue ArgumentError + group = Group.find_by(name: g) + result.push(group.id) if group + end + + result + end + + if groups.present? + groups.each do |group_id| + group = Group.find(group_id) if group_id + group.add(user) if group + end + end + end + + def route_to + url = mapper.interpolate(action['url']) + if action['code'] + data[action['code']] = SecureRandom.hex(8) + url += "&#{action['code']}=#{data[action['code']]}" + end + data['route_to'] = URI.encode(url) + end + + def action_category_id + if action['custom_category_enabled'] + if action['custom_category_wizard_field'] + data[action['category_id']] + elsif action['custom_category_user_field_key'] + if action['custom_category_user_field_key'].include?('custom_fields') + field = action['custom_category_user_field_key'].split('.').last + user.custom_fields[field] + else + user.send(action['custom_category_user_field_key']) + end + end + else + action['category_id'] + end + end + + def action_tags + if action['custom_tag_enabled'] + data[action['custom_tag_field']] + else + action['tags'] + end + end +end \ No newline at end of file diff --git a/lib/custom_wizard/builder.rb b/lib/custom_wizard/builder.rb index 4c746166..af07de50 100644 --- a/lib/custom_wizard/builder.rb +++ b/lib/custom_wizard/builder.rb @@ -1,7 +1,4 @@ -TagStruct = Struct.new(:id, :name) - class CustomWizard::Builder - attr_accessor :wizard, :updater, :submissions def initialize(user=nil, wizard_id) @@ -10,10 +7,7 @@ class CustomWizard::Builder @steps = data['steps'] @wizard = CustomWizard::Wizard.new(user, data) - - if user - @submissions = Array.wrap(PluginStore.get("#{wizard_id}_submissions", user.id)) - end + @submissions = Array.wrap(PluginStore.get("#{wizard_id}_submissions", user.id)) if user end def self.sorted_handlers @@ -42,47 +36,6 @@ class CustomWizard::Builder @sorted_field_validators.sort_by! { |h| -h[:priority] } end - USER_FIELDS = ['name', 'username', 'email', 'date_of_birth', 'title', 'locale'] - PROFILE_FIELDS = ['location', 'website', 'bio_raw', 'profile_background', 'card_background'] - OPERATORS = { - 'eq': '==', - 'gt': '>', - 'lt': '<', - 'gte': '>=', - 'lte': '<=' - } - - def self.fill_placeholders(string, user, data) - result = 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 - - result = result.gsub(/w\{(.*?)\}/) { |match| recurse(data, [*$1.split('.')]) } - - result.gsub(/v\{(.*?)\}/) do |match| - attrs = $1.split(':') - key = attrs.first - format = attrs.length > 1 ? attrs.last : nil - v = nil - - if key == 'time' - time_format = format.present? ? format : "%B %-d, %Y" - v = Time.now.strftime(time_format) - end - - v - end - end - - def self.recurse(data, keys) - k = keys.shift - result = data[k] - keys.empty? ? result : self.recurse(result, keys) - end - def build(build_opts = {}, params = {}) return @wizard if !SiteSetting.custom_wizard_enabled || @@ -135,13 +88,24 @@ class CustomWizard::Builder step.permitted = false else required_data.each do |required| - pairs = required['pairs'].map { |p| p['value'] = @submissions.last[p['value']] } - step.permitted = false unless validate_pairs(pairs) + pairs = required['pairs'].map do |p| + p['key'] = @submissions.last[p['key']] + end + + unless CustomWizard::Mapper.new( + user: @wizard.user, + data: @submissions.last + ).validate_pairs(pairs) + step.permitted = false + end end end if !step.permitted - step.permitted_message = step_template['required_data_message'] if step_template['required_data_message'] + if step_template['required_data_message'] + step.permitted_message = step_template['required_data_message'] + end + next end end @@ -183,7 +147,12 @@ class CustomWizard::Builder if step_template['actions'] && step_template['actions'].length && data step_template['actions'].each do |action| - self.send(action['type'].to_sym, user, action, data) + CustomWizard::Action.new( + action: action, + user: user, + data: data, + updater: updater + ).perform end end @@ -237,6 +206,10 @@ class CustomWizard::Builder end params[:value] = prefill_field(field_template, step_template) || params[:value] + + if field_template['type'] === 'group' + params[:value] = params[:value].first + end if field_template['type'] === 'checkbox' params[:value] = standardise_boolean(params[:value]) @@ -262,8 +235,12 @@ class CustomWizard::Builder @wizard.needs_groups = true end - if (prefill = field_template['filters']).present? - params[:filter] = get_output(field_template['filters']) + if (content = field_template['content']).present? + params[:content] = CustomWizard::Mapper.new( + inputs: content, + user: @wizard.user, + data: @submissions.last + ).output end field = step.add_field(params) @@ -275,76 +252,11 @@ class CustomWizard::Builder def prefill_field(field_template, step_template) if (prefill = field_template['prefill']).present? - get_output(prefill) - end - end - - def get_output(inputs, opts = {}) - output = opts[:multiple] ? [] : nil - - inputs.each do |input| - if input['type'] === 'conditional' && validate_pairs(input['pairs']) - if opts[:multiple] - output.push(get_field(input['output'], input['output_type'], opts)) - else - output = get_field(input['output'], input['output_type'], opts) - break - end - end - - if input['type'] === 'assignment' - value = get_field(input['output'], input['output_type'], opts) - - if opts[:multiple] - output.push(value) - else - output = value - break - end - end - end - - output - end - - def validate_pairs(pairs) - failed = false - - pairs.each do |pair| - key = get_field(pair['key'], pair['key_type']) - value = get_field(pair['value'], pair['value_type']) - failed = true unless key.public_send(get_operator(pair['connector']), value) - end - - !failed - end - - def get_operator(connector) - OPERATORS[connector] || '==' - end - - def get_field(value, type, opts = {}) - method = "get_#{type}_field" - - if self.respond_to?(method) - self.send(method, value, opts) - else - value - end - end - - def get_wizard_field(value, opts = {}) - data = opts[:data] || @submissions.last - data && !data.key?("submitted_at") && data[value] - end - - def get_user_field(value, opts = {}) - if value.include?('user_field_') - UserCustomField.where(user_id: @wizard.user.id, name: value).pluck(:value).first - elsif UserProfile.column_names.include? value - UserProfile.find_by(user_id: @wizard.user.id).send(value) - elsif User.column_names.include? value - User.find(@wizard.user.id).send(value) + CustomWizard::Mapper.new( + inputs: prefill, + user: @wizard.user, + data: @submissions.last + ).output end end @@ -382,7 +294,10 @@ class CustomWizard::Builder end if min_length && value.is_a?(String) && value.strip.length < min_length.to_i - updater.errors.add(field['id'].to_s, I18n.t('wizard.field.too_short', label: label, min: min_length.to_i)) + updater.errors.add( + field['id'].to_s, + I18n.t('wizard.field.too_short', label: label, min: min_length.to_i) + ) end ## ensure all checkboxes are booleans @@ -405,245 +320,6 @@ class CustomWizard::Builder ActiveRecord::Type::Boolean.new.cast(value) end - def create_topic(user, action, data) - if action['custom_title_enabled'] - title = CustomWizard::Builder.fill_placeholders(action['custom_title'], user, data) - else - title = data[action['title']] - end - - if action['post_builder'] - post = CustomWizard::Builder.fill_placeholders(action['post_template'], user, data) - else - post = data[action['post']] - end - - if title - params = { - title: title, - raw: post, - skip_validations: true - } - - params[:category] = action_category_id(action, data) - - tags = action_tags(action, data) - - params[:tags] = tags - - topic_custom_fields = {} - - if action['add_fields'] - action['add_fields'].each do |field| - value = field['value_custom'].present? ? field['value_custom'] : data[field['value']] - key = field['key'] - - if key && (value.present? || value === false) - if key.include?('custom_fields') - keyArr = key.split('.') - - if keyArr.length === 3 - custom_key = keyArr.last - type = keyArr.first - - if type === 'topic' - topic_custom_fields[custom_key] = value - elsif type === 'post' - params[:custom_fields] ||= {} - params[:custom_fields][custom_key.to_sym] = value - end - end - else - value = [*value] + tags if key === 'tags' - params[key.to_sym] = value - end - end - end - end - - creator = PostCreator.new(user, params) - post = creator.create - - if creator.errors.present? - updater.errors.add(:create_topic, creator.errors.full_messages.join(" ")) - else - if topic_custom_fields.present? - topic_custom_fields.each do |k, v| - post.topic.custom_fields[k] = v - end - post.topic.save_custom_fields(true) - end - - unless action['skip_redirect'] - data['redirect_on_complete'] = post.topic.url - end - end - end - end - - def send_message(user, action, data) - - if action['required'].present? && data[action['required']].blank? - return - end - - if action['custom_title_enabled'] - title = CustomWizard::Builder.fill_placeholders(action['custom_title'], user, data) - else - title = data[action['title']] - end - - if action['post_builder'] - post = CustomWizard::Builder.fill_placeholders(action['post_template'], user, data) - else - post = data[action['post']] - end - - if title && post - creator = PostCreator.new(user, - title: title, - raw: post, - archetype: Archetype.private_message, - target_usernames: action['username'] - ) - - post = creator.create - - if creator.errors.present? - updater.errors.add(:send_message, creator.errors.full_messages.join(" ")) - else - unless action['skip_redirect'] - data['redirect_on_complete'] = post.topic.url - end - end - end - end - - def update_profile(user, action, data) - return unless action['profile_updates'].length - - attributes = {} - custom_fields = {} - - action['profile_updates'].each do |pu| - value = pu['value'] - key = pu['key'] - - return if data[key].blank? - - if user_field || custom_field - custom_fields[user_field || custom_field] = data[key] - else - updater_key = value - if ['profile_background', 'card_background'].include?(value) - updater_key = "#{value}_upload_url" - end - attributes[updater_key.to_sym] = data[key] if updater_key - end - - if ['user_avatar'].include?(value) - this_upload_id = data[key][:id] - user.create_user_avatar unless user.user_avatar - user.user_avatar.custom_upload_id = this_upload_id - user.uploaded_avatar_id = this_upload_id - user.save! - user.user_avatar.save! - end - end - - if custom_fields.present? - attributes[:custom_fields] = custom_fields - end - - if attributes.present? - user_updater = UserUpdater.new(user, user) - user_updater.update(attributes) - end - end - - def send_to_api(user, action, data) - api_body = nil - - if action['api_body'] != "" - begin - api_body_parsed = JSON.parse(action['api_body']) - rescue JSON::ParserError - raise Discourse::InvalidParameters, "Invalid API body definition: #{action['api_body']} for #{action['title']}" - end - api_body = JSON.parse(CustomWizard::Builder.fill_placeholders(JSON.generate(api_body_parsed), user, data)) - end - - result = CustomWizard::Api::Endpoint.request(user, action['api'], action['api_endpoint'], api_body) - - if error = result['error'] || (result[0] && result[0]['error']) - error = error['message'] || error - updater.errors.add(:send_to_api, error) - else - ## add validation callback - end - end - - def open_composer(user, action, data) - if action['custom_title_enabled'] - title = CustomWizard::Builder.fill_placeholders(action['custom_title'], user, data) - else - title = data[action['title']] - end - - url = "/new-topic?title=#{title}" - - if action['post_builder'] - post = CustomWizard::Builder.fill_placeholders(action['post_template'], user, data) - else - post = data[action['post']] - end - - url += "&body=#{post}" - - if category_id = action_category_id(action, data) - if category = Category.find(category_id) - url += "&category=#{category.full_slug('/')}" - end - end - - if tags = action_tags(action, data) - url += "&tags=#{tags.join(',')}" - end - - data['redirect_on_complete'] = Discourse.base_uri + URI.encode(url) - end - - def add_to_group(user, action, data) - groups = get_output(action['inputs'], multiple: true, data: data) - - groups = groups.flatten.reduce([]) do |result, g| - begin - result.push(Integer(g)) - rescue ArgumentError - group = Group.find_by(name: g) - result.push(group.id) if group - end - - result - end - - if groups.present? - groups.each do |group_id| - group = Group.find(group_id) if group_id - group.add(user) if group - end - end - end - - def route_to(user, action, data) - url = CustomWizard::Builder.fill_placeholders(action['url'], user, data) - if action['code'] - data[action['code']] = SecureRandom.hex(8) - url += "&#{action['code']}=#{data[action['code']]}" - end - data['route_to'] = URI.encode(url) - end - def save_submissions(data, final_step) if final_step data['submitted_at'] = Time.now.iso8601 @@ -661,29 +337,4 @@ class CustomWizard::Builder PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, @submissions) @wizard.reset end - - def action_category_id(action, data) - if action['custom_category_enabled'] - if action['custom_category_wizard_field'] - data[action['category_id']] - elsif action['custom_category_user_field_key'] - if action['custom_category_user_field_key'].include?('custom_fields') - field = action['custom_category_user_field_key'].split('.').last - user.custom_fields[field] - else - user.send(action['custom_category_user_field_key']) - end - end - else - action['category_id'] - end - end - - def action_tags(action, data) - if action['custom_tag_enabled'] - data[action['custom_tag_field']] - else - action['tags'] - end - end end diff --git a/lib/custom_wizard/mapper.rb b/lib/custom_wizard/mapper.rb new file mode 100644 index 00000000..e94f2b65 --- /dev/null +++ b/lib/custom_wizard/mapper.rb @@ -0,0 +1,114 @@ +class CustomWizard::Mapper + attr_accessor :inputs, :data, :user + + USER_FIELDS = ['name', 'username', 'email', 'date_of_birth', 'title', 'locale', 'trust_level'] + PROFILE_FIELDS = ['location', 'website', 'bio_raw'] + OPERATORS = { 'eq': '==', 'gt': '>', 'lt': '<', 'gte': '>=', 'lte': '<=' } + + def initialize(params) + @inputs = params[:inputs] || {} + @data = params[:data] || {} + @user = params[:user] + @opts = params[:opts] || {} + end + + def output + multiple = @opts[:multiple] + output = multiple ? [] : nil + + inputs.each do |input| + if input['type'] === 'conditional' && validate_pairs(input['pairs']) + if multiple + output.push(map_field(input['output'], input['output_type'])) + else + output = map_field(input['output'], input['output_type']) + break + end + end + + if input['type'] === 'assignment' + value = map_field(input['output'], input['output_type']) + + if @opts[:multiple] + output.push(value) + else + output = value + break + end + end + end + + output + end + + def validate_pairs(pairs) + failed = false + + pairs.each do |pair| + key = map_field(pair['key'], pair['key_type']) + value = map_field(pair['value'], pair['value_type']) + failed = true unless key.public_send(operator(pair['connector']), value) + end + + !failed + end + + def operator(connector) + OPERATORS[connector] || '==' + end + + def map_field(value, type) + method = "#{type}_field" + + if self.respond_to?(method) + self.send(method, value) + else + value + end + end + + def wizard_field(value) + data && !data.key?("submitted_at") && data[value] + end + + def user_field(value) + if value.include?('user_field_') + UserCustomField.where(user_id: user.id, name: value).pluck(:value).first + elsif PROFILE_FIELDS.include?(value) + UserProfile.find_by(user_id: user.id).send(value) + elsif USER_FIELDS.include?(value) + User.find(user.id).send(value) + end + end + + def interpolate(string) + result = 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 + + result = result.gsub(/w\{(.*?)\}/) { |match| recurse(data, [*$1.split('.')]) } + + result.gsub(/v\{(.*?)\}/) do |match| + attrs = $1.split(':') + key = attrs.first + format = attrs.length > 1 ? attrs.last : nil + val = nil + + if key == 'time' + time_format = format.present? ? format : "%B %-d, %Y" + val = Time.now.strftime(time_format) + end + + val + end + end + + def recurse(data, keys) + k = keys.shift + result = data[k] + keys.empty? ? result : self.recurse(result, keys) + end +end \ No newline at end of file diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb index 2cc348df..f236fdcf 100644 --- a/lib/custom_wizard/template.rb +++ b/lib/custom_wizard/template.rb @@ -8,12 +8,12 @@ class CustomWizard::Template :multiple_submissions, :prompt_completion, :restart_on_revisit, - :min_trust, :after_signup, :after_time, :after_time_scheduled, :required, - :theme_id + :theme_id, + :permitted def initialize(data) data = data.is_a?(String) ? ::JSON.parse(data) : data @@ -28,12 +28,12 @@ class CustomWizard::Template @multiple_submissions = data['multiple_submissions'] || false @prompt_completion = data['prompt_completion'] || false @restart_on_revisit = data['restart_on_revisit'] || false - @min_trust = data['min_trust'] || 0 @after_signup = data['after_signup'] @after_time = data['after_time'] @after_time_scheduled = data['after_time_scheduled'] @required = data['required'] || false @theme_id = data['theme_id'] + @permitted = data['permitted'] || nil if data['theme'] theme = Theme.find_by(name: data['theme']) diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index c65f0b4d..79c40574 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -14,13 +14,13 @@ class CustomWizard::Wizard :background, :save_submissions, :multiple_submissions, - :min_trust, :after_time, :after_time_scheduled, :after_signup, :required, :prompt_completion, :restart_on_revisit, + :permitted, :needs_categories, :needs_groups @@ -127,7 +127,10 @@ class CustomWizard::Wizard end def permitted? - user && (user.staff? || user.trust_level.to_i >= min_trust.to_i) + return false unless user + return true if user.admin? || permitted.blank? + group_ids = permitted.first['output'] + return GroupUser.exists?(group_id: group_ids, user_id: user.id) end def reset @@ -146,22 +149,37 @@ class CustomWizard::Wizard def groups @groups ||= ::Site.new(Guardian.new(@user)).groups end + + def self.templates(filter = nil) + rows = [*PluginStoreRow.where(plugin_name: 'custom_wizard')] + rows = rows.select { |r| r.value[filter] } if filter + rows + end - def self.after_signup - rows = PluginStoreRow.where(plugin_name: 'custom_wizard') - wizards = [*rows].select { |r| r.value['after_signup'] } - if wizards.any? - wizards.first.key + def self.after_signup(user) + if (temps = templates('after_signup')).any? + wizard = nil + + temps + .sort_by { |t| template.value['permitted'].present? } + .each do |template| + wizard = CustomWizard::Wizard.new(user, template) + + if wizard.permitted? + wizard = wizard + break + end + end + + wizard else false end end def self.prompt_completion(user) - rows = PluginStoreRow.where(plugin_name: 'custom_wizard') - wizards = [*rows].select { |r| r.value['prompt_completion'] } - if wizards.any? - wizards.reduce([]) do |result, w| + if (temps = templates('prompt_completion')).any? + temps.reduce([]) do |result, w| data = ::JSON.parse(w.value) id = data['id'] name = data['name'] @@ -175,10 +193,8 @@ class CustomWizard::Wizard end def self.restart_on_revisit - rows = PluginStoreRow.where(plugin_name: 'custom_wizard') - wizards = [*rows].select { |r| r.value['restart_on_revisit'] } - if wizards.any? - wizards.first.key + if (temps = templates('restart_on_revisit')).any? + temps.first.key else false end diff --git a/lib/wizard/field.rb b/lib/wizard/field.rb index 7de580bc..eefaa063 100644 --- a/lib/wizard/field.rb +++ b/lib/wizard/field.rb @@ -7,7 +7,7 @@ module CustomWizardFieldExtension :file_types, :limit, :property, - :filter + :content attr_accessor :dropdown_none @@ -26,7 +26,7 @@ module CustomWizardFieldExtension @file_types = attrs[:file_types] @limit = attrs[:limit] @property = attrs[:property] - @filter = attrs[:filter] + @content = attrs[:content] end def label diff --git a/plugin.rb b/plugin.rb index c3d928b3..93cb13a1 100644 --- a/plugin.rb +++ b/plugin.rb @@ -37,42 +37,44 @@ if respond_to?(:register_svg_icon) end after_initialize do - [ - '../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/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/custom_wizard/api_serializer.rb', - '../serializers/custom_wizard/basic_api_serializer.rb', - '../serializers/custom_wizard/wizard_field_serializer.rb', - '../serializers/custom_wizard/wizard_step_serializer.rb', - '../serializers/custom_wizard/wizard_serializer.rb', - '../serializers/site_serializer.rb' + %w[ + ../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/actions.rb + ../lib/custom_wizard/builder.rb + ../lib/custom_wizard/field.rb + ../lib/custom_wizard/mapper.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/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/custom_wizard/api_serializer.rb + ../serializers/custom_wizard/basic_api_serializer.rb + ../serializers/custom_wizard/wizard_field_serializer.rb + ../serializers/custom_wizard/wizard_step_serializer.rb + ../serializers/custom_wizard/wizard_serializer.rb + ../serializers/site_serializer.rb ].each do |path| load File.expand_path(path, __FILE__) end @@ -85,13 +87,11 @@ after_initialize do if user && user.first_seen_at.blank? && - wizard_id = CustomWizard::Wizard.after_signup - - wizard = CustomWizard::Wizard.create(user, wizard_id) + wizard = CustomWizard::Wizard.after_signup(user) - if !wizard.completed? && wizard.permitted? + if !wizard.completed? custom_redirect = true - CustomWizard::Wizard.set_wizard_redirect(user, wizard_id) + CustomWizard::Wizard.set_wizard_redirect(user, wizard.id) end end diff --git a/serializers/custom_wizard/wizard_field_serializer.rb b/serializers/custom_wizard/wizard_field_serializer.rb index 820a1740..11bad2e9 100644 --- a/serializers/custom_wizard/wizard_field_serializer.rb +++ b/serializers/custom_wizard/wizard_field_serializer.rb @@ -7,7 +7,7 @@ class CustomWizardFieldSerializer < ::WizardFieldSerializer :file_types, :limit, :property, - :filter + :content has_many :choices, serializer: WizardFieldChoiceSerializer, embed: :objects @@ -49,7 +49,7 @@ class CustomWizardFieldSerializer < ::WizardFieldSerializer object.property end - def filter - object.filter + def content + object.content end end \ No newline at end of file diff --git a/serializers/custom_wizard/wizard_serializer.rb b/serializers/custom_wizard/wizard_serializer.rb index 9ab3fcc6..afb7a2c7 100644 --- a/serializers/custom_wizard/wizard_serializer.rb +++ b/serializers/custom_wizard/wizard_serializer.rb @@ -7,7 +7,6 @@ class CustomWizardSerializer < ::WizardSerializer :background, :completed, :required, - :min_trust, :permitted, :uncategorized_category_id