0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-11-26 02:50:28 +01:00

add permitted setting

Dieser Commit ist enthalten in:
Angus McLeod 2020-03-30 17:16:03 +11:00
Ursprung b212eaa2f3
Commit ee61c1deb3
28 geänderte Dateien mit 589 neuen und 524 gelöschten Zeilen

Datei anzeigen

@ -46,11 +46,11 @@ export default Ember.Component.extend({
return options; return options;
}, },
canFilter: or('isCategory', 'isTag', 'isGroup'), contentEnabled: or('isCategory', 'isTag', 'isGroup'),
@computed('field.type') @computed('field.type')
filterOptions(fieldType) { contentOptions(fieldType) {
if (!this.canFilter) return {}; if (!this.contentEnabled) return {};
let options = { let options = {
hasOutput: true, hasOutput: true,

Datei anzeigen

@ -23,9 +23,14 @@ export default Ember.Component.extend({
if (!this.type) this.set('type', defaultInputType(this.options)); if (!this.type) this.set('type', defaultInputType(this.options));
}, },
@discourseComputed @discourseComputed('options.allowedInputs')
inputTypes() { allowedInputs(option) {
return ['conditional', 'assignment'].map((type) => { return option || 'conditional,assignment';
},
@discourseComputed('allowedInputs')
inputTypes(allowedInputs) {
return allowedInputs.split(',').map((type) => {
return { return {
id: type, id: type,
name: I18n.t(`admin.wizard.input.${type}.prefix`) name: I18n.t(`admin.wizard.input.${type}.prefix`)

Datei anzeigen

@ -1,10 +1,16 @@
import { getOwner } from 'discourse-common/lib/get-owner'; import { getOwner } from 'discourse-common/lib/get-owner';
import { on } from 'discourse-common/utils/decorators'; import { on } from 'discourse-common/utils/decorators';
import { newInput } from '../lib/custom-wizard'; import { newInput } from '../lib/custom-wizard';
import { default as discourseComputed } from 'discourse-common/utils/decorators';
export default Ember.Component.extend({ export default Ember.Component.extend({
classNames: 'field-mapper', classNames: 'field-mapper',
@discourseComputed('inputs.[]', 'options.singular')
canAdd(inputs, singular) {
return !singular || !inputs || inputs.length < 1;
},
actions: { actions: {
add() { add() {
if (!this.get('inputs')) this.set('inputs', Ember.A()); if (!this.get('inputs')) this.set('inputs', Ember.A());

Datei anzeigen

@ -10,16 +10,15 @@ function generateName(id) {
const profileFields = [ const profileFields = [
'name', 'name',
'user_avatar', 'username',
'email',
'date_of_birth', 'date_of_birth',
'title', 'title',
'locale', 'locale',
'location', 'location',
'website', 'website',
'bio_raw', 'bio_raw',
'profile_background', 'trust_level'
'card_background',
'theme_id'
]; ];
const connectors = [ const connectors = [
@ -69,7 +68,10 @@ const inputTypes = [
] ]
function defaultInputType(options = {}) { 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 = {}) { function defaultSelectionType(inputType, options = {}) {

Datei anzeigen

@ -12,8 +12,8 @@ const wizardProperties = [
'required', 'required',
'prompt_completion', 'prompt_completion',
'restart_on_revisit', 'restart_on_revisit',
'min_trust', 'theme_id',
'theme_id' 'permitted'
]; ];
const CustomWizard = EmberObject.extend({ const CustomWizard = EmberObject.extend({
@ -267,7 +267,7 @@ CustomWizard.reopenClass({
props['required'] = false; props['required'] = false;
props['prompt_completion'] = false; props['prompt_completion'] = false;
props['restart_on_revisit'] = false; props['restart_on_revisit'] = false;
props['min_trust'] = 0; props['permitted'] = null;
props['steps'] = Ember.A(); props['steps'] = Ember.A();
}; };

Datei anzeigen

@ -22,13 +22,7 @@
placeholderKey="admin.wizard.name_placeholder"}} placeholderKey="admin.wizard.name_placeholder"}}
</div> </div>
</div> </div>
</div>
<div class="wizard-header medium">
{{i18n 'admin.wizard.label'}}
</div>
<div class="wizard-settings">
<div class="setting"> <div class="setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n 'admin.wizard.background'}}</label> <label>{{i18n 'admin.wizard.background'}}</label>
@ -40,7 +34,13 @@
placeholderKey="admin.wizard.background_placeholder"}} placeholderKey="admin.wizard.background_placeholder"}}
</div> </div>
</div> </div>
</div>
<div class="wizard-header medium">
{{i18n 'admin.wizard.label'}}
</div>
<div class="wizard-settings">
<div class="setting"> <div class="setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n 'admin.wizard.save_submissions'}}</label> <label>{{i18n 'admin.wizard.save_submissions'}}</label>
@ -106,16 +106,6 @@
</div> </div>
</div> </div>
<div class="setting">
<div class="setting-label">
<label>{{i18n 'admin.wizard.min_trust'}}</label>
</div>
<div class="setting-value">
<span>{{i18n 'admin.wizard.min_trust_label'}}</span>
{{input type='number' value=model.min_trust class='input-small'}}
</div>
</div>
<div class="setting"> <div class="setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n 'admin.wizard.theme_id'}}</label> <label>{{i18n 'admin.wizard.theme_id'}}</label>
@ -144,17 +134,17 @@
<div class="setting full field-mapper-setting"> <div class="setting full field-mapper-setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n 'admin.wizard.group'}}</label> <label>{{i18n 'admin.wizard.permitted'}}</label>
</div> </div>
<div class="setting-value"> <div class="setting-value">
{{wizard-field-mapper {{wizard-field-mapper
inputs=model.group inputs=model.permitted
options=(hash options=(hash
hasOutput=true hasOutput=true
enableConnectors=true groupSelection='output'
userFieldSelection='key,value'
groupSelection=true
textDisabled='output' textDisabled='output'
allowedInputs='assignment'
singular=true
)}} )}}
</div> </div>
</div> </div>

Datei anzeigen

@ -170,17 +170,17 @@
</div> </div>
</div> </div>
{{#if canFilter}} {{#if contentEnabled}}
<div class="setting full field-mapper-setting"> <div class="setting full field-mapper-setting">
<div class="setting-label"> <div class="setting-label">
<label>{{i18n 'admin.wizard.field.filter'}}</label> <label>{{i18n 'admin.wizard.field.content'}}</label>
</div> </div>
<div class="setting-value"> <div class="setting-value">
{{wizard-field-mapper {{wizard-field-mapper
inputs=field.filters inputs=field.content
wizardFields=wizardFields wizardFields=wizardFields
options=filterOptions}} options=contentOptions}}
</div> </div>
</div> </div>
{{/if}} {{/if}}

Datei anzeigen

@ -10,6 +10,8 @@
remove=(action 'remove')}} remove=(action 'remove')}}
{{/each}} {{/each}}
{{#if canAdd}}
<div class="add-custom-input"> <div class="add-custom-input">
{{d-button action='add' label='admin.wizard.add' icon='plus'}} {{d-button action='add' label='admin.wizard.add' icon='plus'}}
</div> </div>
{{/if}}

Datei anzeigen

@ -30,6 +30,7 @@
//= require discourse/lib/show-modal //= require discourse/lib/show-modal
//= require discourse/lib/key-value-store //= require discourse/lib/key-value-store
//= require discourse/lib/settings //= require discourse/lib/settings
//= require discourse/lib/user-presence
//= require discourse/mixins/singleton //= require discourse/mixins/singleton

Datei anzeigen

@ -1,5 +0,0 @@
export default Ember.Component.extend({
didInsertElement() {
console.log(this.field)
}
})

Datei anzeigen

@ -3,10 +3,15 @@ import { computed } from "@ember/object";
import { makeArray } from "discourse-common/lib/helpers"; import { makeArray } from "discourse-common/lib/helpers";
export default ComboBox.extend({ export default ComboBox.extend({
content: computed("groups.[]", "whitelist.[]", function() { content: computed("groups.[]", "field.content.[]", function() {
const whitelist = makeArray(this.whitelist); const whitelist = makeArray(this.field.content);
return this.groups.filter(group => { return this.groups.filter(group => {
return !whitelist.length || whitelist.indexOf(group.id) > -1; return !whitelist.length || whitelist.indexOf(group.id) > -1;
}).map(g => {
return {
id: g.id,
name: g.name
}
}); });
}) })
}) })

Datei anzeigen

@ -14,7 +14,6 @@ export default Ember.Route.extend({
if (model) { if (model) {
const completed = model.get('completed'); const completed = model.get('completed');
const permitted = model.get('permitted'); const permitted = model.get('permitted');
const minTrust = model.get('min_trust');
const wizardId = model.get('id'); const wizardId = model.get('id');
const user = model.get('user'); const user = model.get('user');
const name = model.get('name'); const name = model.get('name');

Datei anzeigen

@ -1,6 +1,6 @@
{{wizard-category-selector {{wizard-category-selector
categories=categories categories=categories
whitelist=field.filter whitelist=field.content
maximum=field.limit maximum=field.limit
onChange=(action (mut categories))}} onChange=(action (mut categories))}}

Datei anzeigen

@ -1,6 +1,7 @@
{{wizard-group-selector {{wizard-group-selector
groups=wizard.groups groups=wizard.groups
whitelist=field.filter field=field
whitelist=field.content
value=field.value value=field.value
onChange=(action (mut field.value)) onChange=(action (mut field.value))
options=(hash options=(hash

Datei anzeigen

@ -5,7 +5,7 @@
{{wizard-no-access text=(i18n 'wizard.requires_login' name=name) wizardId=wizardId}} {{wizard-no-access text=(i18n 'wizard.requires_login' name=name) wizardId=wizardId}}
{{else}} {{else}}
{{#if notPermitted}} {{#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}} {{else}}
{{#if completed}} {{#if completed}}
{{wizard-no-access text=(i18n 'wizard.completed' name=name) wizardId=wizardId}} {{wizard-no-access text=(i18n 'wizard.completed' name=name) wizardId=wizardId}}

Datei anzeigen

@ -61,10 +61,6 @@ $setting-background: dark-light-diff($primary, $secondary, 96%, -65%);
} }
} }
.wizard-basic-details {
margin-bottom: 10px;
}
.content-list + .content { .content-list + .content {
overflow: hidden; overflow: hidden;
} }

Datei anzeigen

@ -36,8 +36,6 @@ en:
prompt_completion_label: "Prompt user to complete wizard." prompt_completion_label: "Prompt user to complete wizard."
restart_on_revisit: "Restart" restart_on_revisit: "Restart"
restart_on_revisit_label: "Restart the the wizard whenever the user revisits, regardless of prior progress." 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" theme_id: "Theme"
no_theme: "Select a Theme (optional)" no_theme: "Select a Theme (optional)"
save: "Save Changes" save: "Save Changes"
@ -61,6 +59,7 @@ en:
submission_key: 'submission key' submission_key: 'submission key'
param_key: 'param' param_key: 'param'
group: "Group" group: "Group"
permitted: "Permitted"
editor: editor:
show: "Show" show: "Show"
@ -125,7 +124,7 @@ en:
limit: "Limit" limit: "Limit"
property: "Property" property: "Property"
prefill: "Prefill" prefill: "Prefill"
filter: "Content" content: "Content"
action: action:
header: "Actions" header: "Actions"
@ -306,12 +305,12 @@ en:
wizard: wizard:
completed: "You have completed the {{name}} 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." none: "There is no wizard here."
return_to_site: "Return to {{siteName}}" return_to_site: "Return to {{siteName}}"
requires_login: "You need to be logged in to access the {{name}} wizard." requires_login: "You need to be logged in to access the {{name}} wizard."
reset: "Reset this 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: wizard_composer:
show_preview: "Preview Post" show_preview: "Preview Post"

Datei anzeigen

@ -33,8 +33,6 @@ fr:
required_label: "Les utilisatrices doivent compléter l'assistant." required_label: "Les utilisatrices doivent compléter l'assistant."
prompt_completion: "Invitation" prompt_completion: "Invitation"
prompt_completion_label: "Inviter l'utilisatrice à compléter l'assistant." 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" theme_id: "Thème"
no_theme: "Choisir un thème (optionnel)" no_theme: "Choisir un thème (optionnel)"
save: "Sauvegarder" save: "Sauvegarder"

Datei anzeigen

@ -14,11 +14,8 @@ class CustomWizard::AdminController < ::ApplicationController
params.require(:wizard) params.require(:wizard)
wizard = ::JSON.parse(params[:wizard]) wizard = ::JSON.parse(params[:wizard])
existing = PluginStore.get('custom_wizard', wizard['id']) || {} existing = PluginStore.get('custom_wizard', wizard['id']) || {}
new_time = false new_time = false
error = nil error = nil
if wizard["id"].blank? if wizard["id"].blank?

289
lib/custom_wizard/actions.rb Normale Datei
Datei anzeigen

@ -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

Datei anzeigen

@ -1,7 +1,4 @@
TagStruct = Struct.new(:id, :name)
class CustomWizard::Builder class CustomWizard::Builder
attr_accessor :wizard, :updater, :submissions attr_accessor :wizard, :updater, :submissions
def initialize(user=nil, wizard_id) def initialize(user=nil, wizard_id)
@ -10,10 +7,7 @@ class CustomWizard::Builder
@steps = data['steps'] @steps = data['steps']
@wizard = CustomWizard::Wizard.new(user, data) @wizard = CustomWizard::Wizard.new(user, data)
@submissions = Array.wrap(PluginStore.get("#{wizard_id}_submissions", user.id)) if user
if user
@submissions = Array.wrap(PluginStore.get("#{wizard_id}_submissions", user.id))
end
end end
def self.sorted_handlers def self.sorted_handlers
@ -42,47 +36,6 @@ class CustomWizard::Builder
@sorted_field_validators.sort_by! { |h| -h[:priority] } @sorted_field_validators.sort_by! { |h| -h[:priority] }
end 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 = {}) def build(build_opts = {}, params = {})
return @wizard if !SiteSetting.custom_wizard_enabled || return @wizard if !SiteSetting.custom_wizard_enabled ||
@ -135,13 +88,24 @@ class CustomWizard::Builder
step.permitted = false step.permitted = false
else else
required_data.each do |required| required_data.each do |required|
pairs = required['pairs'].map { |p| p['value'] = @submissions.last[p['value']] } pairs = required['pairs'].map do |p|
step.permitted = false unless validate_pairs(pairs) 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
end end
if !step.permitted 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 next
end end
end end
@ -183,7 +147,12 @@ class CustomWizard::Builder
if step_template['actions'] && step_template['actions'].length && data if step_template['actions'] && step_template['actions'].length && data
step_template['actions'].each do |action| 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
end end
@ -238,6 +207,10 @@ class CustomWizard::Builder
params[:value] = prefill_field(field_template, step_template) || params[:value] 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' if field_template['type'] === 'checkbox'
params[:value] = standardise_boolean(params[:value]) params[:value] = standardise_boolean(params[:value])
end end
@ -262,8 +235,12 @@ class CustomWizard::Builder
@wizard.needs_groups = true @wizard.needs_groups = true
end end
if (prefill = field_template['filters']).present? if (content = field_template['content']).present?
params[:filter] = get_output(field_template['filters']) params[:content] = CustomWizard::Mapper.new(
inputs: content,
user: @wizard.user,
data: @submissions.last
).output
end end
field = step.add_field(params) field = step.add_field(params)
@ -275,76 +252,11 @@ class CustomWizard::Builder
def prefill_field(field_template, step_template) def prefill_field(field_template, step_template)
if (prefill = field_template['prefill']).present? if (prefill = field_template['prefill']).present?
get_output(prefill) CustomWizard::Mapper.new(
end inputs: prefill,
end user: @wizard.user,
data: @submissions.last
def get_output(inputs, opts = {}) ).output
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)
end end
end end
@ -382,7 +294,10 @@ class CustomWizard::Builder
end end
if min_length && value.is_a?(String) && value.strip.length < min_length.to_i 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 end
## ensure all checkboxes are booleans ## ensure all checkboxes are booleans
@ -405,245 +320,6 @@ class CustomWizard::Builder
ActiveRecord::Type::Boolean.new.cast(value) ActiveRecord::Type::Boolean.new.cast(value)
end 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) def save_submissions(data, final_step)
if final_step if final_step
data['submitted_at'] = Time.now.iso8601 data['submitted_at'] = Time.now.iso8601
@ -661,29 +337,4 @@ class CustomWizard::Builder
PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, @submissions) PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, @submissions)
@wizard.reset @wizard.reset
end 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 end

114
lib/custom_wizard/mapper.rb Normale Datei
Datei anzeigen

@ -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

Datei anzeigen

@ -8,12 +8,12 @@ class CustomWizard::Template
:multiple_submissions, :multiple_submissions,
:prompt_completion, :prompt_completion,
:restart_on_revisit, :restart_on_revisit,
:min_trust,
:after_signup, :after_signup,
:after_time, :after_time,
:after_time_scheduled, :after_time_scheduled,
:required, :required,
:theme_id :theme_id,
:permitted
def initialize(data) def initialize(data)
data = data.is_a?(String) ? ::JSON.parse(data) : data data = data.is_a?(String) ? ::JSON.parse(data) : data
@ -28,12 +28,12 @@ class CustomWizard::Template
@multiple_submissions = data['multiple_submissions'] || false @multiple_submissions = data['multiple_submissions'] || false
@prompt_completion = data['prompt_completion'] || false @prompt_completion = data['prompt_completion'] || false
@restart_on_revisit = data['restart_on_revisit'] || false @restart_on_revisit = data['restart_on_revisit'] || false
@min_trust = data['min_trust'] || 0
@after_signup = data['after_signup'] @after_signup = data['after_signup']
@after_time = data['after_time'] @after_time = data['after_time']
@after_time_scheduled = data['after_time_scheduled'] @after_time_scheduled = data['after_time_scheduled']
@required = data['required'] || false @required = data['required'] || false
@theme_id = data['theme_id'] @theme_id = data['theme_id']
@permitted = data['permitted'] || nil
if data['theme'] if data['theme']
theme = Theme.find_by(name: data['theme']) theme = Theme.find_by(name: data['theme'])

Datei anzeigen

@ -14,13 +14,13 @@ class CustomWizard::Wizard
:background, :background,
:save_submissions, :save_submissions,
:multiple_submissions, :multiple_submissions,
:min_trust,
:after_time, :after_time,
:after_time_scheduled, :after_time_scheduled,
:after_signup, :after_signup,
:required, :required,
:prompt_completion, :prompt_completion,
:restart_on_revisit, :restart_on_revisit,
:permitted,
:needs_categories, :needs_categories,
:needs_groups :needs_groups
@ -127,7 +127,10 @@ class CustomWizard::Wizard
end end
def permitted? 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 end
def reset def reset
@ -147,21 +150,36 @@ class CustomWizard::Wizard
@groups ||= ::Site.new(Guardian.new(@user)).groups @groups ||= ::Site.new(Guardian.new(@user)).groups
end end
def self.after_signup def self.templates(filter = nil)
rows = PluginStoreRow.where(plugin_name: 'custom_wizard') rows = [*PluginStoreRow.where(plugin_name: 'custom_wizard')]
wizards = [*rows].select { |r| r.value['after_signup'] } rows = rows.select { |r| r.value[filter] } if filter
if wizards.any? rows
wizards.first.key end
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 else
false false
end end
end end
def self.prompt_completion(user) def self.prompt_completion(user)
rows = PluginStoreRow.where(plugin_name: 'custom_wizard') if (temps = templates('prompt_completion')).any?
wizards = [*rows].select { |r| r.value['prompt_completion'] } temps.reduce([]) do |result, w|
if wizards.any?
wizards.reduce([]) do |result, w|
data = ::JSON.parse(w.value) data = ::JSON.parse(w.value)
id = data['id'] id = data['id']
name = data['name'] name = data['name']
@ -175,10 +193,8 @@ class CustomWizard::Wizard
end end
def self.restart_on_revisit def self.restart_on_revisit
rows = PluginStoreRow.where(plugin_name: 'custom_wizard') if (temps = templates('restart_on_revisit')).any?
wizards = [*rows].select { |r| r.value['restart_on_revisit'] } temps.first.key
if wizards.any?
wizards.first.key
else else
false false
end end

Datei anzeigen

@ -7,7 +7,7 @@ module CustomWizardFieldExtension
:file_types, :file_types,
:limit, :limit,
:property, :property,
:filter :content
attr_accessor :dropdown_none attr_accessor :dropdown_none
@ -26,7 +26,7 @@ module CustomWizardFieldExtension
@file_types = attrs[:file_types] @file_types = attrs[:file_types]
@limit = attrs[:limit] @limit = attrs[:limit]
@property = attrs[:property] @property = attrs[:property]
@filter = attrs[:filter] @content = attrs[:content]
end end
def label def label

Datei anzeigen

@ -37,42 +37,44 @@ if respond_to?(:register_svg_icon)
end end
after_initialize do after_initialize do
[ %w[
'../lib/custom_wizard/engine.rb', ../lib/custom_wizard/engine.rb
'../config/routes.rb', ../config/routes.rb
'../controllers/custom_wizard/wizard.rb', ../controllers/custom_wizard/wizard.rb
'../controllers/custom_wizard/steps.rb', ../controllers/custom_wizard/steps.rb
'../controllers/custom_wizard/admin.rb', ../controllers/custom_wizard/admin.rb
'../controllers/custom_wizard/transfer.rb', ../controllers/custom_wizard/transfer.rb
'../controllers/custom_wizard/api.rb', ../controllers/custom_wizard/api.rb
'../controllers/application_controller.rb', ../controllers/application_controller.rb
'../controllers/extra_locales_controller.rb', ../controllers/extra_locales_controller.rb
'../controllers/invites_controller.rb', ../controllers/invites_controller.rb
'../jobs/clear_after_time_wizard.rb', ../jobs/clear_after_time_wizard.rb
'../jobs/refresh_api_access_token.rb', ../jobs/refresh_api_access_token.rb
'../jobs/set_after_time_wizard.rb', ../jobs/set_after_time_wizard.rb
'../lib/custom_wizard/builder.rb', ../lib/custom_wizard/actions.rb
'../lib/custom_wizard/field.rb', ../lib/custom_wizard/builder.rb
'../lib/custom_wizard/step_updater.rb', ../lib/custom_wizard/field.rb
'../lib/custom_wizard/template.rb', ../lib/custom_wizard/mapper.rb
'../lib/custom_wizard/wizard.rb', ../lib/custom_wizard/step_updater.rb
'../lib/custom_wizard/api/api.rb', ../lib/custom_wizard/template.rb
'../lib/custom_wizard/api/authorization.rb', ../lib/custom_wizard/wizard.rb
'../lib/custom_wizard/api/endpoint.rb', ../lib/custom_wizard/api/api.rb
'../lib/custom_wizard/api/log_entry.rb', ../lib/custom_wizard/api/authorization.rb
'../lib/wizard/choice.rb', ../lib/custom_wizard/api/endpoint.rb
'../lib/wizard/field.rb', ../lib/custom_wizard/api/log_entry.rb
'../lib/wizard/step.rb', ../lib/wizard/choice.rb
'../serializers/custom_wizard/api/authorization_serializer.rb', ../lib/wizard/field.rb
'../serializers/custom_wizard/api/basic_endpoint_serializer.rb', ../lib/wizard/step.rb
'../serializers/custom_wizard/api/endpoint_serializer.rb', ../serializers/custom_wizard/api/authorization_serializer.rb
'../serializers/custom_wizard/api/log_serializer.rb', ../serializers/custom_wizard/api/basic_endpoint_serializer.rb
'../serializers/custom_wizard/api_serializer.rb', ../serializers/custom_wizard/api/endpoint_serializer.rb
'../serializers/custom_wizard/basic_api_serializer.rb', ../serializers/custom_wizard/api/log_serializer.rb
'../serializers/custom_wizard/wizard_field_serializer.rb', ../serializers/custom_wizard/api_serializer.rb
'../serializers/custom_wizard/wizard_step_serializer.rb', ../serializers/custom_wizard/basic_api_serializer.rb
'../serializers/custom_wizard/wizard_serializer.rb', ../serializers/custom_wizard/wizard_field_serializer.rb
'../serializers/site_serializer.rb' ../serializers/custom_wizard/wizard_step_serializer.rb
../serializers/custom_wizard/wizard_serializer.rb
../serializers/site_serializer.rb
].each do |path| ].each do |path|
load File.expand_path(path, __FILE__) load File.expand_path(path, __FILE__)
end end
@ -85,13 +87,11 @@ after_initialize do
if user && if user &&
user.first_seen_at.blank? && user.first_seen_at.blank? &&
wizard_id = CustomWizard::Wizard.after_signup wizard = CustomWizard::Wizard.after_signup(user)
wizard = CustomWizard::Wizard.create(user, wizard_id) if !wizard.completed?
if !wizard.completed? && wizard.permitted?
custom_redirect = true custom_redirect = true
CustomWizard::Wizard.set_wizard_redirect(user, wizard_id) CustomWizard::Wizard.set_wizard_redirect(user, wizard.id)
end end
end end

Datei anzeigen

@ -7,7 +7,7 @@ class CustomWizardFieldSerializer < ::WizardFieldSerializer
:file_types, :file_types,
:limit, :limit,
:property, :property,
:filter :content
has_many :choices, serializer: WizardFieldChoiceSerializer, embed: :objects has_many :choices, serializer: WizardFieldChoiceSerializer, embed: :objects
@ -49,7 +49,7 @@ class CustomWizardFieldSerializer < ::WizardFieldSerializer
object.property object.property
end end
def filter def content
object.filter object.content
end end
end end

Datei anzeigen

@ -7,7 +7,6 @@ class CustomWizardSerializer < ::WizardSerializer
:background, :background,
:completed, :completed,
:required, :required,
:min_trust,
:permitted, :permitted,
:uncategorized_category_id :uncategorized_category_id