From 009bb71a2424c5c00be68719552325444b5123c1 Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 17 Oct 2024 16:15:25 +0200 Subject: [PATCH 1/2] Add time groups feature --- app/controllers/custom_wizard/admin/user.rb | 17 ++++++ app/controllers/custom_wizard/admin/wizard.rb | 1 + app/jobs/regular/set_after_time_wizard.rb | 22 ++++++-- .../admin-user-wizard-details.hbs | 28 ++++++++++ .../admin-wizards-wizard-show.js.es6 | 20 +++++++ .../initializers/custom-wizard-edits.js.es6 | 20 +++++++ .../discourse/lib/wizard-schema.js.es6 | 2 + .../routes/admin-wizards-wizard-show.js.es6 | 1 + .../templates/admin-wizards-wizard-show.hbs | 16 ++++++ assets/stylesheets/common/admin.scss | 7 ++- config/locales/client.en.yml | 9 +++ config/locales/server.en.yml | 1 + config/routes.rb | 2 + lib/custom_wizard/subscription.rb | 6 ++ lib/custom_wizard/template.rb | 4 +- lib/custom_wizard/validators/template.rb | 9 +++ lib/custom_wizard/wizard.rb | 21 +++++++ plugin.rb | 4 +- .../components/custom_wizard/template_spec.rb | 3 + spec/jobs/set_after_time_wizard_spec.rb | 23 ++++++++ .../application_controller_spec.rb | 56 +++++++++++++++++-- 21 files changed, 258 insertions(+), 14 deletions(-) create mode 100644 app/controllers/custom_wizard/admin/user.rb create mode 100644 assets/javascripts/discourse/connectors/admin-user-details/admin-user-wizard-details.hbs diff --git a/app/controllers/custom_wizard/admin/user.rb b/app/controllers/custom_wizard/admin/user.rb new file mode 100644 index 00000000..77452c6b --- /dev/null +++ b/app/controllers/custom_wizard/admin/user.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true +class CustomWizard::UserController < ::Admin::AdminController + before_action :ensure_admin + requires_plugin "discourse-custom-wizard" + + def clear_redirect + user = User.find_by(id: params[:id]) + + if user + user.custom_fields["redirect_to_wizard"] = nil + user.save_custom_fields(true) + render json: success_json + else + render json: failed_json + end + end +end diff --git a/app/controllers/custom_wizard/admin/wizard.rb b/app/controllers/custom_wizard/admin/wizard.rb index fdb1ca70..9c297b36 100644 --- a/app/controllers/custom_wizard/admin/wizard.rb +++ b/app/controllers/custom_wizard/admin/wizard.rb @@ -78,6 +78,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController :resume_on_revisit, :theme_id, permitted: mapped_params, + after_time_groups: [], steps: [ :id, :index, diff --git a/app/jobs/regular/set_after_time_wizard.rb b/app/jobs/regular/set_after_time_wizard.rb index 2938524b..15077e0e 100644 --- a/app/jobs/regular/set_after_time_wizard.rb +++ b/app/jobs/regular/set_after_time_wizard.rb @@ -3,20 +3,32 @@ module Jobs class SetAfterTimeWizard < ::Jobs::Base def execute(args) if SiteSetting.custom_wizard_enabled - wizard = CustomWizard::Wizard.create(args[:wizard_id]) + @wizard = CustomWizard::Wizard.create(args[:wizard_id]) - if wizard && wizard.after_time + if @wizard && @wizard.after_time user_ids = [] - User.human_users.each do |user| - user_ids.push(user.id) if CustomWizard::Wizard.set_user_redirect(wizard.id, user) + target_users.each do |user| + user_ids.push(user.id) if CustomWizard::Wizard.set_after_time_redirect(@wizard.id, user) end CustomWizard::Template.clear_cache_keys - MessageBus.publish "/redirect_to_wizard", wizard.id, user_ids: user_ids + MessageBus.publish "/redirect_to_wizard", @wizard.id, user_ids: user_ids end end end + + def target_users + users = [] + + if @wizard.after_time_groups.exists? + @wizard.after_time_groups.each { |group| users += group.users } + else + users = User.human_users + end + + users + end end end diff --git a/assets/javascripts/discourse/connectors/admin-user-details/admin-user-wizard-details.hbs b/assets/javascripts/discourse/connectors/admin-user-details/admin-user-wizard-details.hbs new file mode 100644 index 00000000..ddf15546 --- /dev/null +++ b/assets/javascripts/discourse/connectors/admin-user-details/admin-user-wizard-details.hbs @@ -0,0 +1,28 @@ +
+

{{i18n "admin.wizard.user.label"}}

+ +
+
{{i18n "admin.wizard.user.redirect.label"}}
+
+ {{#if model.redirect_to_wizard}} + + {{model.redirect_to_wizard}} + + {{else}} + — + {{/if}} +
+
+ {{#if model.redirect_to_wizard}} + + {{/if}} +
+
+
\ No newline at end of file diff --git a/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 b/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 index 7ae48709..e84154b6 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 @@ -12,9 +12,11 @@ import Controller from "@ember/controller"; import copyText from "discourse/lib/copy-text"; import I18n from "I18n"; import { filterValues } from "discourse/plugins/discourse-custom-wizard/discourse/lib/wizard-schema"; +import { action } from "@ember/object"; export default Controller.extend({ modal: service(), + site: service(), hasName: notEmpty("wizard.name"), @observes("currentStep") @@ -91,6 +93,24 @@ export default Controller.extend({ return I18n.t(`admin.wizard.error.${errorType}`, errorParams); }, + setAfterTimeGroupIds() { + const groups = this.site.groups.filter((g) => + this.wizard.after_time_groups.includes(g.name) + ); + this.setProperties({ + afterTimeGroupIds: groups.map((g) => g.id), + }); + }, + + @action + setAfterTimeGroups(groupIds) { + const groups = this.site.groups.filter((g) => groupIds.includes(g.id)); + this.setProperties({ + afterTimeGroupIds: groups.map((g) => g.id), + "wizard.after_time_groups": groups.map((g) => g.name), + }); + }, + actions: { save() { this.setProperties({ diff --git a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 index 5debc380..52c400d4 100644 --- a/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 +++ b/assets/javascripts/discourse/initializers/custom-wizard-edits.js.es6 @@ -2,6 +2,8 @@ import DiscourseURL from "discourse/lib/url"; import { withPluginApi } from "discourse/lib/plugin-api"; import getUrl from "discourse-common/lib/get-url"; import { observes } from "discourse-common/utils/decorators"; +import { popupAjaxError } from "discourse/lib/ajax-error"; +import { ajax } from "discourse/lib/ajax"; export default { name: "custom-wizard-edits", @@ -105,6 +107,24 @@ export default { route: "adminWizardsWizard", icon: "hat-wizard", }); + + if (api.getCurrentUser()?.admin) { + api.modifyClass("model:admin-user", { + pluginId: "custom-wizard", + + clearWizardRedirect(user) { + return ajax(`/admin/users/${user.id}/wizards/clear_redirect`, { + type: "PUT", + }) + .then(() => { + user.setProperties({ + redirect_to_wizard: null, + }); + }) + .catch(popupAjaxError); + }, + }); + } }); }, }; diff --git a/assets/javascripts/discourse/lib/wizard-schema.js.es6 b/assets/javascripts/discourse/lib/wizard-schema.js.es6 index 3c286e18..688bc245 100644 --- a/assets/javascripts/discourse/lib/wizard-schema.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-schema.js.es6 @@ -17,11 +17,13 @@ const wizard = { resume_on_revisit: null, theme_id: null, permitted: null, + after_time_groups: null, }, mapped: ["permitted"], required: ["id"], dependent: { after_time: "after_time_scheduled", + after_time: "after_time_groups", }, objectArrays: { step: { diff --git a/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 b/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 index 2ed2627f..9934b003 100644 --- a/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 +++ b/assets/javascripts/discourse/routes/admin-wizards-wizard-show.js.es6 @@ -47,5 +47,6 @@ export default DiscourseRoute.extend({ }; controller.setProperties(props); + controller.setAfterTimeGroupIds(); }, }); diff --git a/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs b/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs index 053deb73..af3f0634 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs @@ -162,6 +162,22 @@ }} + +
+
+ +
+
+ +
+ {{i18n "admin.wizard.after_time_groups.description"}} +
+
+
{{/wizard-subscription-container}} diff --git a/assets/stylesheets/common/admin.scss b/assets/stylesheets/common/admin.scss index 5e831c41..1c50edde 100644 --- a/assets/stylesheets/common/admin.scss +++ b/assets/stylesheets/common/admin.scss @@ -373,11 +373,16 @@ $error: #ef1700; } .input .select-kit, - > .select-kit { + > .select-kit:not(.group-chooser) { max-width: 250px !important; min-width: 250px !important; } + .group-chooser { + max-width: 400px !important; + min-width: 400px !important; + } + &.force-final { padding: 1em; background-color: var(--primary-very-low); diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 15a78ed3..864e01de 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -97,6 +97,9 @@ en: time: "Time" done: "Set Time" clear: "Clear" + after_time_groups: + label: Time Groups + description: Groups directed to wizard after start time. required: "Required" required_label: "Users cannot skip the wizard." prompt_completion: "Prompt" @@ -587,6 +590,12 @@ en: community: label: Support title: There is a Custom Wizard Community subscription active on this forum. + user: + label: Wizards + redirect: + label: Redirect + remove_label: Remove + remove_title: Remove wizard redirect wizard_js: group: diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 9fa23d53..7e31b00f 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -52,6 +52,7 @@ en: after_signup: "You can only have one 'after signup' wizard at a time. %{wizard_id} has 'after signup' enabled." after_signup_after_time: "You can't use 'after time' and 'after signup' on the same wizard." after_time: "After time setting is invalid." + after_time_group: "After time group does not exist: %{group_name}" liquid_syntax_error: "Liquid syntax error in %{attribute}: %{message}" subscription: "%{type} %{property} usage is not supported on your subscription" not_permitted_for_guests: "%{object_id} is not permitted when guests can access the wizard" diff --git a/config/routes.rb b/config/routes.rb index db42f94f..50cc4f4e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,4 +46,6 @@ Discourse::Application.routes.append do post "admin/wizards/manager/import" => "admin_manager#import" delete "admin/wizards/manager/destroy" => "admin_manager#destroy" end + + put "/admin/users/:id/wizards/clear_redirect" => "custom_wizard/user#clear_redirect" end diff --git a/lib/custom_wizard/subscription.rb b/lib/custom_wizard/subscription.rb index e7c1c4e7..14b95843 100644 --- a/lib/custom_wizard/subscription.rb +++ b/lib/custom_wizard/subscription.rb @@ -25,6 +25,12 @@ class CustomWizard::Subscription business: ["*"], community: ["*"], }, + after_time_groups: { + none: [], + standard: [], + business: ["*"], + community: [], + }, }, step: { condition: { diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb index 217851d2..cc77d078 100644 --- a/lib/custom_wizard/template.rb +++ b/lib/custom_wizard/template.rb @@ -176,10 +176,10 @@ class CustomWizard::Template end if enqueue_wizard_at - Jobs.cancel_scheduled_job(:set_after_time_wizard, wizard_id: wizard_id) + self.class.clear_user_wizard_redirect(wizard_id, after_time: true) Jobs.enqueue_at(enqueue_wizard_at, :set_after_time_wizard, wizard_id: wizard_id) elsif old_data && old_data[:after_time] - clear_user_wizard_redirect(wizard_id, after_time: true) + self.class.clear_user_wizard_redirect(wizard_id, after_time: true) end end end diff --git a/lib/custom_wizard/validators/template.rb b/lib/custom_wizard/validators/template.rb index c3202b37..f3bc9dd7 100644 --- a/lib/custom_wizard/validators/template.rb +++ b/lib/custom_wizard/validators/template.rb @@ -128,6 +128,15 @@ class CustomWizard::TemplateValidator if invalid_time || active_time.blank? || active_time < Time.now.utc errors.add :base, I18n.t("wizard.validation.after_time") end + + group_names = @data[:after_time_groups] + if group_names.present? + group_names.each do |group_name| + unless Group.exists?(name: group_name) + errors.add :base, I18n.t("wizard.validation.after_time_group", group_name: group_name) + end + end + end end def validate_liquid_template(object, type) diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index 20f65ae6..3c491b21 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -15,6 +15,7 @@ class CustomWizard::Wizard :multiple_submissions, :after_time, :after_time_scheduled, + :after_time_group_names, :after_signup, :required, :prompt_completion, @@ -58,6 +59,7 @@ class CustomWizard::Wizard @after_signup = cast_bool(attrs["after_signup"]) @after_time = cast_bool(attrs["after_time"]) @after_time_scheduled = attrs["after_time_scheduled"] + @after_time_group_names = attrs["after_time_groups"] @required = cast_bool(attrs["required"]) @permitted = attrs["permitted"] || nil @theme_id = attrs["theme_id"] @@ -251,6 +253,10 @@ class CustomWizard::Wizard permitted?(always_allow_admin: always_allow_admin) && can_submit? end + def should_redirect? + can_access?(always_allow_admin: false) && after_time_target? + end + def reset return nil unless actor_id @@ -329,6 +335,16 @@ class CustomWizard::Wizard end end + def after_time_groups + @after_time_groups ||= Group.where(name: after_time_group_names) + end + + def after_time_target? + return true if after_time_group_names.blank? || !after_time_groups.exists? + return true if after_time_groups.joins(:users).where(users: { username: user.username }).exists? + false + end + def self.create(wizard_id, user = nil, guest_id = nil) if template = CustomWizard::Template.find(wizard_id) new(template.to_h, user, guest_id) @@ -372,6 +388,11 @@ class CustomWizard::Wizard end end + def self.set_after_time_redirect(wizard_id, user) + wizard = self.create(wizard_id, user) + set_user_redirect(wizard_id, user) if wizard.after_time_target? + end + def self.set_user_redirect(wizard_id, user) wizard = self.create(wizard_id, user) diff --git a/plugin.rb b/plugin.rb index 434f193f..4a01a34a 100644 --- a/plugin.rb +++ b/plugin.rb @@ -46,6 +46,7 @@ after_initialize do require_relative "app/controllers/custom_wizard/admin/logs.rb" require_relative "app/controllers/custom_wizard/admin/manager.rb" require_relative "app/controllers/custom_wizard/admin/custom_fields.rb" + require_relative "app/controllers/custom_wizard/admin/user.rb" require_relative "app/controllers/custom_wizard/wizard_client.rb" require_relative "app/controllers/custom_wizard/wizard.rb" require_relative "app/controllers/custom_wizard/steps.rb" @@ -146,6 +147,7 @@ after_initialize do end add_to_serializer(:current_user, :redirect_to_wizard) { object.redirect_to_wizard } + add_to_serializer(:admin_user_list, :redirect_to_wizard) { object.redirect_to_wizard } on(:user_approved) do |user| if wizard = CustomWizard::Wizard.after_signup(user) @@ -168,7 +170,7 @@ after_initialize do end wizard = CustomWizard::Wizard.create(wizard_id, current_user) - redirect_to "/w/#{wizard_id.dasherize}" if wizard.can_access?(always_allow_admin: false) + redirect_to "/w/#{wizard_id.dasherize}" if wizard.should_redirect? end end end diff --git a/spec/components/custom_wizard/template_spec.rb b/spec/components/custom_wizard/template_spec.rb index 7f71d0ed..a3fc7283 100644 --- a/spec/components/custom_wizard/template_spec.rb +++ b/spec/components/custom_wizard/template_spec.rb @@ -154,6 +154,9 @@ describe CustomWizard::Template do expect_not_enqueued_with(job: :set_after_time_wizard) do CustomWizard::Template.save(@after_time_template) end + expect( + UserCustomField.exists?(name: "redirect_to_wizard", value: @after_time_template[:id]), + ).to eq(false) end end end diff --git a/spec/jobs/set_after_time_wizard_spec.rb b/spec/jobs/set_after_time_wizard_spec.rb index b85dd70d..01410766 100644 --- a/spec/jobs/set_after_time_wizard_spec.rb +++ b/spec/jobs/set_after_time_wizard_spec.rb @@ -47,6 +47,29 @@ describe Jobs::SetAfterTimeWizard do end end + context "when after_time_groups is set" do + fab!(:group1) { Fabricate(:group) } + fab!(:group_user) { Fabricate(:group_user, group: group1, user: user2) } + + before do + enable_subscription("business") + @after_time_template["after_time_groups"] = [group1.name] + CustomWizard::Template.save(@after_time_template.as_json) + end + + it "only redirects users in the group" do + messages = + MessageBus.track_publish("/redirect_to_wizard") do + described_class.new.execute(wizard_id: "super_mega_fun_wizard") + end + expect(messages.first.data).to eq("super_mega_fun_wizard") + expect(messages.first.user_ids).to match_array([user2.id]) + expect( + UserCustomField.where(name: "redirect_to_wizard", value: "super_mega_fun_wizard").length, + ).to eq(1) + end + end + context "when user has completed the wizard" do before do @after_time_template[:steps].each do |step| diff --git a/spec/requests/custom_wizard/application_controller_spec.rb b/spec/requests/custom_wizard/application_controller_spec.rb index 5e9ea3f3..3705605b 100644 --- a/spec/requests/custom_wizard/application_controller_spec.rb +++ b/spec/requests/custom_wizard/application_controller_spec.rb @@ -78,8 +78,16 @@ describe ApplicationController do end context "when time has passed" do - it "redirects if time has passed" do + def run_job! travel_to Time.now + 4.hours + MessageBus.expects(:publish).at_least_once + Jobs::SetAfterTimeWizard.new.execute( + Jobs::SetAfterTimeWizard.jobs.first["args"].first.symbolize_keys, + ) + end + + it "redirects if time has passed" do + run_job! get "/" expect(response).to redirect_to("/w/super-mega-fun-wizard") end @@ -93,7 +101,7 @@ describe ApplicationController do context "when user is in permitted group" do it "redirects user" do - travel_to Time.now + 4.hours + run_job! get "/" expect(response).to redirect_to("/w/super-mega-fun-wizard") end @@ -103,7 +111,7 @@ describe ApplicationController do before { Group.find(13).remove(user) } it "does not redirect user" do - travel_to Time.now + 4.hours + run_job! user.trust_level = TrustLevel[2] user.save! get "/" @@ -111,7 +119,7 @@ describe ApplicationController do end it "does not redirect if user is an admin" do - travel_to Time.now + 4.hours + run_job! user.trust_level = TrustLevel[2] user.admin = true user.save! @@ -134,11 +142,49 @@ describe ApplicationController do end it "does not redirect" do - travel_to Time.now + 4.hours + run_job! get "/" expect(response).not_to redirect_to("/w/super-mega-fun-wizard") end end + + context "when after_time_groups is set" do + fab!(:group) + + before do + enable_subscription("business") + @template["after_time_groups"] = [group.name] + CustomWizard::Template.save(@template.as_json) + end + + context "when user is in group" do + before { group.add(user) } + + it "redirects user" do + run_job! + get "/" + expect(response).to redirect_to("/w/super-mega-fun-wizard") + end + end + + context "when user is not in group" do + before { group.remove(user) } + + it "does not redirect user" do + run_job! + get "/" + expect(response).to_not redirect_to("/w/super-mega-fun-wizard") + end + + it "does not redirect if user is an admin" do + run_job! + user.admin = true + user.save! + get "/" + expect(response).to_not redirect_to("/w/super-mega-fun-wizard") + end + end + end end end end From 4fdb430b944750fbe2e49e8d6ac0458f2700622f Mon Sep 17 00:00:00 2001 From: Angus McLeod Date: Thu, 17 Oct 2024 16:15:48 +0200 Subject: [PATCH 2/2] Bump version --- plugin.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin.rb b/plugin.rb index 4a01a34a..fd22179a 100644 --- a/plugin.rb +++ b/plugin.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true # name: discourse-custom-wizard # about: Forms for Discourse. Better onboarding, structured posting, data enrichment, automated actions and much more. -# version: 2.8.13 +# version: 2.9.0 # authors: Angus McLeod, Faizaan Gagan, Robert Barrow, Keegan George, Kaitlin Maddever, Juan Marcos Gutierrez Ramos # url: https://github.com/paviliondev/discourse-custom-wizard # contact_emails: development@pavilion.tech