From cff8f9f427d41705f41deffeabfdf6317e7db780 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Tue, 9 Jul 2024 11:23:57 +0200 Subject: [PATCH] Add poster and guest_email to topic and message creation && allow guests to create_topic --- app/controllers/custom_wizard/admin/wizard.rb | 2 + .../components/wizard-custom-action.js.es6 | 1 + .../components/wizard-mapper-selector.js.es6 | 14 ++ .../discourse/components/wizard-mapper.js.es6 | 2 + .../components/wizard-user-chooser.js.es6 | 55 ++++++ .../wizard-user-chooser-row.js | 5 + .../discourse/lib/wizard-schema.js.es6 | 5 + .../components/wizard-custom-action.hbs | 43 ++++ .../components/wizard-mapper-selector.hbs | 4 +- .../wizard-user-chooser-row.hbs | 20 ++ config/locales/client.en.yml | 6 + lib/custom_wizard/action.rb | 47 ++++- lib/custom_wizard/field.rb | 1 - spec/components/custom_wizard/action_spec.rb | 183 +++++++++++++----- .../custom_wizard/template_validator_spec.rb | 3 - 15 files changed, 336 insertions(+), 55 deletions(-) create mode 100644 assets/javascripts/discourse/components/wizard-user-chooser.js.es6 create mode 100644 assets/javascripts/discourse/components/wizard-user-chooser/wizard-user-chooser-row.js create mode 100644 assets/javascripts/discourse/templates/components/wizard-user-chooser/wizard-user-chooser-row.hbs diff --git a/app/controllers/custom_wizard/admin/wizard.rb b/app/controllers/custom_wizard/admin/wizard.rb index 955deb3f..993d0e6c 100644 --- a/app/controllers/custom_wizard/admin/wizard.rb +++ b/app/controllers/custom_wizard/admin/wizard.rb @@ -145,6 +145,8 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController custom_fields: mapped_params, visible: mapped_params, required: mapped_params, + poster: mapped_params, + guest_email: mapped_params, recipient: mapped_params, categories: mapped_params, mute_remainder: mapped_params, diff --git a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 index b9329617..1f4c914a 100644 --- a/assets/javascripts/discourse/components/wizard-custom-action.js.es6 +++ b/assets/javascripts/discourse/components/wizard-custom-action.js.es6 @@ -5,6 +5,7 @@ import { computed } from "@ember/object"; import UndoChanges from "../mixins/undo-changes"; import Component from "@ember/component"; import I18n from "I18n"; +import { WIZARD_USER } from "./wizard-user-chooser"; export default Component.extend(UndoChanges, { componentType: "action", diff --git a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 index eb9e735a..5d67b8f7 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 @@ -121,6 +121,9 @@ export default Component.extend({ guestGroup: computed("options.guestGroup", "inputType", function () { return this.optionEnabled("guestGroup"); }), + includeMessageableGroups: computed("options.includeMessageableGroups", "inputType", function () { + return this.optionEnabled("includeMessageableGroups"); + }), userEnabled: computed("options.userSelection", "inputType", function () { return this.optionEnabled("userSelection"); }), @@ -352,6 +355,17 @@ export default Component.extend({ return result; }, + @discourseComputed("includeMessageableGroups", "options.userLimit") + userOptions(includeMessageableGroups, userLimit) { + const opts = { + includeMessageableGroups + } + if (userLimit) { + opts.maximum = userLimit + } + return opts; + }, + optionEnabled(type) { const options = this.options; if (!options) { diff --git a/assets/javascripts/discourse/components/wizard-mapper.js.es6 b/assets/javascripts/discourse/components/wizard-mapper.js.es6 index ec58e3f2..16e2e570 100644 --- a/assets/javascripts/discourse/components/wizard-mapper.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper.js.es6 @@ -33,6 +33,8 @@ export default Component.extend({ outputConnector: options.outputConnector || null, context: options.context || null, guestGroup: options.guestGroup || false, + includeMessageableGroups: options.includeMessageableGroups || false, + userLimit: options.userLimit || null }; let inputTypes = ["key", "value", "output"]; diff --git a/assets/javascripts/discourse/components/wizard-user-chooser.js.es6 b/assets/javascripts/discourse/components/wizard-user-chooser.js.es6 new file mode 100644 index 00000000..d52c733d --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-user-chooser.js.es6 @@ -0,0 +1,55 @@ +import UserChooserComponent from "select-kit/components/user-chooser"; + +export const WIZARD_USER = "wizard-user"; + +export default UserChooserComponent.extend({ + pluginApiIdentifiers: ["wizard-user-chooser"], + classNames: ["user-chooser", "wizard-user-chooser"], + classNameBindings: ["selectKit.options.fullWidthWrap:full-width-wrap"], + valueProperty: "id", + nameProperty: "name", + + modifyComponentForRow() { + return "wizard-user-chooser/wizard-user-chooser-row"; + }, + + modifyNoSelection() { + return this.defaultItem(WIZARD_USER, I18n.t("admin.wizard.action.poster.wizard_user")); + }, + + selectKitOptions: { + fullWidthWrap: false, + autoWrap: false, + }, + + search() { + const superPromise = this._super(...arguments); + if (!superPromise) { + return; + } + return superPromise.then((results) => { + console.log(results) + if (!results || results.length === 0) { + return; + } + return results.map((item) => { + const reconstructed = {}; + if (item.username) { + reconstructed.id = item.username; + if (item.username.includes("@")) { + reconstructed.isEmail = true; + } else { + reconstructed.isUser = true; + reconstructed.name = item.name; + reconstructed.showUserStatus = this.showUserStatus; + } + } else if (item.name) { + reconstructed.id = item.name; + reconstructed.name = item.full_name; + reconstructed.isGroup = true; + } + return { ...item, ...reconstructed }; + }); + }); + }, +}); diff --git a/assets/javascripts/discourse/components/wizard-user-chooser/wizard-user-chooser-row.js b/assets/javascripts/discourse/components/wizard-user-chooser/wizard-user-chooser-row.js new file mode 100644 index 00000000..125bccca --- /dev/null +++ b/assets/javascripts/discourse/components/wizard-user-chooser/wizard-user-chooser-row.js @@ -0,0 +1,5 @@ +import SelectKitRowComponent from "select-kit/components/select-kit/select-kit-row"; + +export default SelectKitRowComponent.extend({ + classNames: ["user-row", "wizard-user-chooser-row"], +}); diff --git a/assets/javascripts/discourse/lib/wizard-schema.js.es6 b/assets/javascripts/discourse/lib/wizard-schema.js.es6 index 710a3594..0c76fa53 100644 --- a/assets/javascripts/discourse/lib/wizard-schema.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-schema.js.es6 @@ -100,6 +100,8 @@ const action = { custom_fields: null, skip_redirect: null, suppress_notifications: null, + poster: 'wizard-user', + guest_email: null, add_event: null, add_location: null, }, @@ -111,6 +113,8 @@ const action = { skip_redirect: null, custom_fields: null, required: null, + poster: 'wizard-user', + guest_email: null, recipient: null, suppress_notifications: null, }, @@ -184,6 +188,7 @@ const action = { "custom_fields", "required", "recipient", + "poster", "profile_updates", "group", "url", diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index 80152674..1116d79f 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -98,6 +98,48 @@ {{/if}} + +
+
+ +
+ +
+ {{wizard-mapper + inputs=this.action.poster + property="poster" + onUpdate=(action "mappedFieldUpdated") + options=(hash + textSelection='key,value' + wizardFieldSelection=true + userSelection="output" + outputDefaultSelection="user" + userLimit="1" + context="action" + ) + }} +
+
+ +
+
+ +
+ +
+ {{wizard-mapper + inputs=this.action.guest_email + property="guest_email" + onUpdate=(action "mappedFieldUpdated") + options=(hash + textSelection="key,value" + wizardFieldSelection=true + outputPlaceholder="admin.wizard.action.guest_email.placeholder" + context="action" + ) + }} +
+
{{/if}} {{#if publicTopicFields}} @@ -217,6 +259,7 @@ userSelection="output" outputDefaultSelection="user" context="action" + includeMessageableGroups="true" ) }} diff --git a/assets/javascripts/discourse/templates/components/wizard-mapper-selector.hbs b/assets/javascripts/discourse/templates/components/wizard-mapper-selector.hbs index 80669aa4..3971cd26 100644 --- a/assets/javascripts/discourse/templates/components/wizard-mapper-selector.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-mapper-selector.hbs @@ -66,12 +66,12 @@ {{/if}} {{#if showUser}} - {{email-group-user-chooser + {{wizard-user-chooser placeholderKey=placeholderKey value=value autocomplete="discourse" onChange=(action "changeUserValue") - options=(hash includeMessageableGroups="true") + options=userOptions }} {{/if}} \ No newline at end of file diff --git a/assets/javascripts/discourse/templates/components/wizard-user-chooser/wizard-user-chooser-row.hbs b/assets/javascripts/discourse/templates/components/wizard-user-chooser/wizard-user-chooser-row.hbs new file mode 100644 index 00000000..8ff700b0 --- /dev/null +++ b/assets/javascripts/discourse/templates/components/wizard-user-chooser/wizard-user-chooser-row.hbs @@ -0,0 +1,20 @@ +{{#if this.item.isUser}} + {{avatar this.item imageSize="tiny"}} +
+ {{format-username this.item.id}} + {{this.item.name}} +
+ {{#if (and this.item.showUserStatus this.item.status)}} + + {{/if}} + {{decorate-username-selector this.item.id}} +{{else if this.item.isGroup}} + {{d-icon "users"}} +
+ {{this.item.id}} + {{this.item.full_name}} +
+{{else}} + {{d-icon "envelope"}} + {{this.item.id}} +{{/if}} \ No newline at end of file diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 71d5eeac..3e7feba1 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -344,6 +344,12 @@ en: include: "Include Fields" title: "Title" post: "Post" + poster: + label: "Poster" + wizard_user: "Wizard user" + guest_email: + label: "Guest email" + placeholder: "Field for guest email" topic_attr: "Topic Attribute" interpolate_fields: "Insert wizard fields using the field_id in w{}. Insert user fields using field key in u{}." diff --git a/lib/custom_wizard/action.rb b/lib/custom_wizard/action.rb index 7972f92c..d539d785 100644 --- a/lib/custom_wizard/action.rb +++ b/lib/custom_wizard/action.rb @@ -7,13 +7,14 @@ class CustomWizard::Action :result REQUIRES_USER = %w[ - create_topic update_profile open_composer watch_categories add_to_group ] + WIZARD_USER = 'wizard-user' + def initialize(opts) @wizard = opts[:wizard] @action = opts[:action] @@ -69,7 +70,7 @@ class CustomWizard::Action end if params[:title].present? && params[:raw].present? - creator = PostCreator.new(user, params) + creator = PostCreator.new(topic_poster, params) post = creator.create if creator.errors.present? @@ -138,8 +139,7 @@ class CustomWizard::Action params[:archetype] = Archetype.private_message - poster = user || Discourse.system_user - creator = PostCreator.new(poster, params) + creator = PostCreator.new(topic_poster, params) post = creator.create if creator.errors.present? @@ -653,6 +653,45 @@ class CustomWizard::Action params end + def topic_poster + @topic_poster ||= begin + poster_id = CustomWizard::Mapper.new( + inputs: action['poster'], + data: mapper_data, + user: user, + ).perform + poster_id = [*poster_id].first if poster_id.present? + + if poster_id.blank? || poster_id === WIZARD_USER + poster = user || guest_user + else + poster = User.find_by_username(poster_id) + end + + poster || Discourse.system_user + end + end + + def guest_user + @guest_user ||= begin + return nil unless action['guest_email'] + + email = CustomWizard::Mapper.new( + inputs: action['guest_email'], + data: mapper_data, + ).perform + + if email&.match(/@/) + User.create!( + email: email, + username: UserNameSuggester.suggest(email), + name: User.suggest_name(email), + staged: true, + ) + end + end + end + def new_group_params params = {} diff --git a/lib/custom_wizard/field.rb b/lib/custom_wizard/field.rb index e8e7bbf9..c0ebaae3 100644 --- a/lib/custom_wizard/field.rb +++ b/lib/custom_wizard/field.rb @@ -31,7 +31,6 @@ class CustomWizard::Field :step REQUIRES_USER = %w[ - composer upload ] diff --git a/spec/components/custom_wizard/action_spec.rb b/spec/components/custom_wizard/action_spec.rb index 4ce6d842..13f1b13f 100644 --- a/spec/components/custom_wizard/action_spec.rb +++ b/spec/components/custom_wizard/action_spec.rb @@ -46,7 +46,7 @@ describe CustomWizard::Action do update_template(wizard_template) end - context 'creating a topic' do + describe '#create_topic' do it "works" do wizard = CustomWizard::Builder.new(@template[:id], user).build wizard.create_updater( @@ -160,6 +160,43 @@ describe CustomWizard::Action do expect(action.result.success?).to eq(true) expect(TopicCustomField.exists?(name: custom_field_name)).to eq(true) end + + it "allows poster to be set" do + wizard_template[:actions][0]["poster"] = [ + { + "type": "assignment", + "output_type": "user", + "output_connector": "set", + "output": [ + "angus1" + ] + } + ] + update_template(wizard_template) + + wizard = CustomWizard::Builder.new(@template[:id], user).build + wizard.create_updater( + wizard.steps.first.id, + step_1_field_1: "Topic Title", + step_1_field_2: "topic body" + ).update + wizard.create_updater(wizard.steps.second.id, {}).update + wizard.create_updater(wizard.steps.last.id, + step_3_field_3: category.id + ).update + + topic = Topic.where( + title: "Topic Title", + category_id: category.id + ) + expect(topic.exists?).to eq(true) + post = Post.find_by( + topic_id: topic.pluck(:id), + raw: "topic body" + ) + expect(post.present?).to eq(true) + expect(post.user.username).to eq('angus1') + end end it 'updates a profile' do @@ -234,6 +271,7 @@ describe CustomWizard::Action do context "standard subscription actions" do before do enable_subscription("standard") + Jobs.run_immediately! end it 'watches tags' do @@ -333,61 +371,117 @@ describe CustomWizard::Action do expect(user2.reload.notifications.count).to eq(1) end - it "send_message works when guests are permitted" do - wizard_template["permitted"] = guests_permitted["permitted"] - wizard_template.delete("actions") - wizard_template['actions'] = [send_message] - update_template(wizard_template) + context "with a guest" do + describe "#create_topic" do + it "creates a staged guest poster if guest_email is set" do + Jobs.run_immediately! - User.create(username: 'angus1', email: "angus1@email.com") + wizard_template["permitted"] = guests_permitted["permitted"] + wizard_template[:steps][0][:fields] << { + "id": "step_1_field_5", + "label": "Guest Email", + "type": "text", + "min_length": "3", + }.as_json + create_topic["run_after"] = "step_3" + create_topic["guest_email"] = [ + { + "type": "assignment", + "output": "step_1_field_5", + "output_type": "wizard_field", + "output_connector": "set" + } + ] + create_topic["category"] = [ + { + "type": "assignment", + "output": "step_3_field_3", + "output_type": "wizard_field", + "output_connector": "set" + } + ] + wizard_template.delete("actions") + wizard_template[:actions] = [create_topic] - wizard = CustomWizard::Builder.new(wizard_template["id"], nil, CustomWizard::Wizard.generate_guest_id).build - wizard.create_updater(wizard.steps[0].id, {}).update - updater = wizard.create_updater(wizard.steps[1].id, {}) - updater.update + update_template(wizard_template) - topic = Topic.where(archetype: Archetype.private_message, title: "Message title") - post = Post.where(topic_id: topic.pluck(:id)) + wizard = CustomWizard::Builder.new( + @template[:id], + nil, + CustomWizard::Wizard.generate_guest_id + ).build + wizard.create_updater( + wizard.steps.first.id, + step_1_field_5: "guest@email.com" + ).update + wizard.create_updater(wizard.steps.second.id, {}).update + wizard.create_updater(wizard.steps.last.id, + step_3_field_3: category.id + ).update - expect(topic.exists?).to eq(true) - expect(topic.first.topic_allowed_users.first.user.username).to eq('angus1') - expect(topic.first.topic_allowed_users.second.user.username).to eq(Discourse.system_user.username) - expect(post.exists?).to eq(true) - end + topic = Topic.where(category_id: category.id).first + expect(topic.present?).to eq(true) + expect(topic.posts.first.user.staged).to eq(true) + expect(topic.posts.first.user.primary_email.email).to eq('guest@email.com') + end + end - it "send_message works when guests are permitted and the target is an email address" do - Jobs.run_immediately! + describe "#send_message" do + it "works" do + wizard_template["permitted"] = guests_permitted["permitted"] + wizard_template.delete("actions") + wizard_template['actions'] = [send_message] + update_template(wizard_template) - wizard_template["permitted"] = guests_permitted["permitted"] - wizard_template.delete("actions") + wizard = CustomWizard::Builder.new(wizard_template["id"], nil, CustomWizard::Wizard.generate_guest_id).build + wizard.create_updater(wizard.steps[0].id, {}).update + updater = wizard.create_updater(wizard.steps[1].id, {}) + updater.update - send_message["recipient"] = [ - { - "type": "assignment", - "output": "step_1_field_1", - "output_type": "wizard_field", - "output_connector": "set" - } - ] + topic = Topic.where(archetype: Archetype.private_message, title: "Message title") + post = Post.where(topic_id: topic.pluck(:id)) - wizard_template['actions'] = [send_message] - update_template(wizard_template) + expect(topic.exists?).to eq(true) + expect(topic.first.topic_allowed_users.first.user.username).to eq(user1.username) + expect(topic.first.topic_allowed_users.second.user.username).to eq(Discourse.system_user.username) + expect(post.exists?).to eq(true) + end - NotificationEmailer.expects(:process_notification).once + it "works when the target is an email address" do + Jobs.run_immediately! - wizard = CustomWizard::Builder.new(wizard_template["id"], nil, CustomWizard::Wizard.generate_guest_id).build - wizard.create_updater(wizard.steps[0].id, step_1_field_1: "guest@email.com").update - updater = wizard.create_updater(wizard.steps[1].id, {}) - updater.update + wizard_template["permitted"] = guests_permitted["permitted"] + wizard_template.delete("actions") - topic = Topic.where(archetype: Archetype.private_message, title: "Message title") - post = Post.where(topic_id: topic.pluck(:id)) + send_message["recipient"] = [ + { + "type": "assignment", + "output": "step_1_field_1", + "output_type": "wizard_field", + "output_connector": "set" + } + ] - expect(topic.exists?).to eq(true) - expect(topic.first.topic_allowed_users.first.user.staged).to eq(true) - expect(topic.first.topic_allowed_users.first.user.primary_email.email).to eq('guest@email.com') - expect(topic.first.topic_allowed_users.second.user.username).to eq(Discourse.system_user.username) - expect(post.exists?).to eq(true) + wizard_template['actions'] = [send_message] + update_template(wizard_template) + + NotificationEmailer.expects(:process_notification).once + + wizard = CustomWizard::Builder.new(wizard_template["id"], nil, CustomWizard::Wizard.generate_guest_id).build + wizard.create_updater(wizard.steps[0].id, step_1_field_1: "guest@email.com").update + updater = wizard.create_updater(wizard.steps[1].id, {}) + updater.update + + topic = Topic.where(archetype: Archetype.private_message, title: "Message title") + post = Post.where(topic_id: topic.pluck(:id)) + + expect(topic.exists?).to eq(true) + expect(topic.first.topic_allowed_users.first.user.staged).to eq(true) + expect(topic.first.topic_allowed_users.first.user.primary_email.email).to eq('guest@email.com') + expect(topic.first.topic_allowed_users.second.user.username).to eq(Discourse.system_user.username) + expect(post.exists?).to eq(true) + end + end end end @@ -528,7 +622,6 @@ describe CustomWizard::Action do wizard.create_updater(wizard.steps.second.id, {}).update wizard.create_updater(wizard.steps.last.id, step_3_field_3: category.id) .update - User.create(username: 'angus1', email: 'angus1@email.com') wizard.create_updater(wizard.steps[0].id, {}).update wizard.create_updater(wizard.steps[1].id, {}).update topic = Topic.where(title: 'Topic Title', category_id: category.id) diff --git a/spec/components/custom_wizard/template_validator_spec.rb b/spec/components/custom_wizard/template_validator_spec.rb index fe61be91..38288409 100644 --- a/spec/components/custom_wizard/template_validator_spec.rb +++ b/spec/components/custom_wizard/template_validator_spec.rb @@ -155,9 +155,6 @@ describe CustomWizard::TemplateValidator do validator = CustomWizard::TemplateValidator.new(template) expect(validator.perform).to eq(false) errors = validator.errors.to_a - expect(errors).to include( - I18n.t("wizard.validation.not_permitted_for_guests", object_id: "action_1") - ) expect(errors).to include( I18n.t("wizard.validation.not_permitted_for_guests", object_id: "step_2_field_7") )