diff --git a/.github/workflows/discourse-plugin.yml b/.github/workflows/discourse-plugin.yml new file mode 100644 index 00000000..f5cf62e5 --- /dev/null +++ b/.github/workflows/discourse-plugin.yml @@ -0,0 +1,11 @@ +name: Discourse Plugin + +on: + push: + branches: + - main + pull_request: + +jobs: + ci: + uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1 diff --git a/.github/workflows/plugin-linting.yml b/.github/workflows/plugin-linting.yml deleted file mode 100644 index acb85230..00000000 --- a/.github/workflows/plugin-linting.yml +++ /dev/null @@ -1,54 +0,0 @@ -name: Linting - -on: - push: - branches: - - main - - stable - pull_request: - -concurrency: - group: plugin-linting-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }} - cancel-in-progress: true - -jobs: - build: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Set up Node.js - uses: actions/setup-node@v3 - with: - node-version: 16 - cache: yarn - - - name: Yarn install - run: yarn install - - - name: Set up ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 2.7 - bundler-cache: true - - - name: ESLint - if: ${{ always() }} - run: yarn eslint --ext .js,.js.es6 --no-error-on-unmatched-pattern {test,assets}/javascripts - - - name: Prettier - if: ${{ always() }} - shell: bash - run: | - yarn prettier -v - if [ 0 -lt $(find assets -type f \( -name "*.scss" -or -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then - yarn prettier --list-different "assets/**/*.{scss,js,es6}" - fi - if [ 0 -lt $(find test -type f \( -name "*.js" -or -name "*.es6" \) 2> /dev/null | wc -l) ]; then - yarn prettier --list-different "test/**/*.{js,es6}" - fi - - - name: Rubocop - if: ${{ always() }} - run: bundle exec rubocop . diff --git a/.github/workflows/plugin-tests.yml b/.github/workflows/plugin-tests.yml deleted file mode 100644 index f58f1b64..00000000 --- a/.github/workflows/plugin-tests.yml +++ /dev/null @@ -1,136 +0,0 @@ -name: Plugin Tests - -on: - push: - branches: - - main - - stable - pull_request: - -concurrency: - group: tests-${{ format('{0}-{1}', github.head_ref || github.run_number, github.job) }} - cancel-in-progress: true - -jobs: - build: - name: ${{ matrix.build_type }} - runs-on: ubuntu-latest - container: discourse/discourse_test:slim${{ startsWith(matrix.build_type, 'frontend') && '-browsers' || '' }} - timeout-minutes: 30 - - env: - DISCOURSE_HOSTNAME: www.example.com - RUBY_GLOBAL_METHOD_CACHE_SIZE: 131072 - RAILS_ENV: test - PGUSER: discourse - PGPASSWORD: discourse - - strategy: - fail-fast: false - - matrix: - build_type: ["backend", "frontend"] - - steps: - - uses: actions/checkout@v3 - with: - repository: discourse/discourse - fetch-depth: 1 - - - name: Install plugin - uses: actions/checkout@v3 - with: - path: plugins/${{ github.event.repository.name }} - fetch-depth: 1 - - - name: Setup Git - run: | - git config --global user.email "ci@ci.invalid" - git config --global user.name "Discourse CI" - - - name: Start redis - run: | - redis-server /etc/redis/redis.conf & - - - name: Start Postgres - run: | - chown -R postgres /var/run/postgresql - sudo -E -u postgres script/start_test_db.rb - sudo -u postgres psql -c "CREATE ROLE $PGUSER LOGIN SUPERUSER PASSWORD '$PGPASSWORD';" - - - name: Bundler cache - uses: actions/cache@v3 - with: - path: vendor/bundle - key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} - restore-keys: | - ${{ runner.os }}-gem- - - - name: Setup gems - run: | - gem install bundler --conservative -v $(awk '/BUNDLED WITH/ { getline; gsub(/ /,""); print $0 }' Gemfile.lock) - bundle config --local path vendor/bundle - bundle config --local deployment true - bundle config --local without development - bundle install --jobs 4 - bundle clean - - - name: Lint English locale - if: matrix.build_type == 'backend' - run: bundle exec ruby script/i18n_lint.rb "plugins/${{ github.event.repository.name }}/locales/{client,server}.en.yml" - - - name: Get yarn cache directory - id: yarn-cache-dir - run: echo "::set-output name=dir::$(yarn cache dir)" - - - name: Yarn cache - uses: actions/cache@v3 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Yarn install - run: yarn install - - - name: Fetch app state cache - uses: actions/cache@v3 - id: app-cache - with: - path: tmp/app-cache - key: >- - ${{ hashFiles('.github/workflows/tests.yml') }}- - ${{ hashFiles('db/**/*', 'plugins/**/db/**/*') }}- - - - name: Restore database from cache - if: steps.app-cache.outputs.cache-hit == 'true' - run: psql -f tmp/app-cache/cache.sql postgres - - - name: Restore uploads from cache - if: steps.app-cache.outputs.cache-hit == 'true' - run: rm -rf public/uploads && cp -r tmp/app-cache/uploads public/uploads - - - name: Create and migrate database - if: steps.app-cache.outputs.cache-hit != 'true' - run: | - bin/rake db:create - bin/rake db:migrate - - - name: Dump database for cache - if: steps.app-cache.outputs.cache-hit != 'true' - run: mkdir -p tmp/app-cache && pg_dumpall > tmp/app-cache/cache.sql - - - name: Dump uploads for cache - if: steps.app-cache.outputs.cache-hit != 'true' - run: rm -rf tmp/app-cache/uploads && cp -r public/uploads tmp/app-cache/uploads - - - name: Plugin RSpec - if: matrix.build_type == 'backend' - run: bin/rake plugin:spec[${{ github.event.repository.name }}] - - - name: Plugin QUnit - if: matrix.build_type == 'frontend' - run: QUNIT_EMBER_CLI=1 bundle exec rake plugin:qunit['${{ github.event.repository.name }}','1200000'] - timeout-minutes: 10 diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index 48cea364..66b401ac 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -1,4 +1,4 @@ -All code in this repository is Copyright 2018 by Angus McLeod. +All code in this repository is Copyright 2023 by Angus McLeod. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by diff --git a/app/controllers/custom_wizard/steps.rb b/app/controllers/custom_wizard/steps.rb index df3c2cb3..2a4305c7 100644 --- a/app/controllers/custom_wizard/steps.rb +++ b/app/controllers/custom_wizard/steps.rb @@ -1,6 +1,5 @@ # frozen_string_literal: true -class CustomWizard::StepsController < ::ApplicationController - before_action :ensure_logged_in +class CustomWizard::StepsController < ::CustomWizard::WizardClientController before_action :ensure_can_update def update @@ -22,7 +21,7 @@ class CustomWizard::StepsController < ::ApplicationController if updater.success? wizard_id = update_params[:wizard_id] - builder = CustomWizard::Builder.new(wizard_id, current_user) + builder = CustomWizard::Builder.new(wizard_id, current_user, guest_id) @wizard = builder.build(force: true) current_step = @wizard.find_step(update[:step_id]) @@ -85,7 +84,6 @@ class CustomWizard::StepsController < ::ApplicationController private def ensure_can_update - @builder = CustomWizard::Builder.new(update_params[:wizard_id], current_user) raise Discourse::InvalidParameters.new(:wizard_id) if @builder.template.nil? raise Discourse::InvalidAccess.new if !@builder.wizard || !@builder.wizard.can_access? diff --git a/app/controllers/custom_wizard/wizard.rb b/app/controllers/custom_wizard/wizard.rb index 7aafdd3b..dd4ea4ca 100644 --- a/app/controllers/custom_wizard/wizard.rb +++ b/app/controllers/custom_wizard/wizard.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true -class CustomWizard::WizardController < ::ApplicationController - before_action :ensure_plugin_enabled - before_action :ensure_logged_in, only: [:skip] - +class CustomWizard::WizardController < ::CustomWizard::WizardClientController def show if wizard.present? render json: CustomWizard::WizardSerializer.new(wizard, scope: guardian, root: false).as_json, status: 200 @@ -35,19 +32,8 @@ class CustomWizard::WizardController < ::ApplicationController def wizard @wizard ||= begin - builder = CustomWizard::Builder.new(params[:wizard_id].underscore, current_user) - return nil unless builder.present? - opts = {} - opts[:reset] = params[:reset] - builder.build(opts, params) - end - end - - private - - def ensure_plugin_enabled - unless SiteSetting.custom_wizard_enabled - redirect_to path("/") + return nil unless @builder.present? + @builder.build({ reset: params[:reset] }, params) end end end diff --git a/app/controllers/custom_wizard/wizard_client.rb b/app/controllers/custom_wizard/wizard_client.rb new file mode 100644 index 00000000..e898852a --- /dev/null +++ b/app/controllers/custom_wizard/wizard_client.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true +class CustomWizard::WizardClientController < ::ApplicationController + before_action :ensure_plugin_enabled + before_action :set_builder + + private + + def ensure_plugin_enabled + unless SiteSetting.custom_wizard_enabled + redirect_to path("/") + end + end + + def guest_id + return nil if current_user.present? + cookies[:custom_wizard_guest_id] ||= CustomWizard::Wizard.generate_guest_id + cookies[:custom_wizard_guest_id] + end + + def set_builder + @builder = CustomWizard::Builder.new(params[:wizard_id].underscore, current_user, guest_id) + end +end diff --git a/app/serializers/custom_wizard/submission_serializer.rb b/app/serializers/custom_wizard/submission_serializer.rb index 732d6743..48892c21 100644 --- a/app/serializers/custom_wizard/submission_serializer.rb +++ b/app/serializers/custom_wizard/submission_serializer.rb @@ -2,12 +2,15 @@ class CustomWizard::SubmissionSerializer < ApplicationSerializer attributes :id, :fields, - :submitted_at - - has_one :user, serializer: ::BasicUserSerializer, embed: :objects + :submitted_at, + :user def include_user? - object.user.present? + object.wizard.user.present? + end + + def user + ::BasicUserSerializer.new(object.wizard.user).as_json end def fields diff --git a/assets/javascripts/discourse/components/custom-wizard-composer-editor.js.es6 b/assets/javascripts/discourse/components/custom-wizard-composer-editor.js.es6 index 5e2ef424..5335da81 100644 --- a/assets/javascripts/discourse/components/custom-wizard-composer-editor.js.es6 +++ b/assets/javascripts/discourse/components/custom-wizard-composer-editor.js.es6 @@ -12,6 +12,7 @@ import { alias } from "@ember/object/computed"; import Site from "discourse/models/site"; import { uploadIcon } from "discourse/lib/uploads"; import { dasherize } from "@ember/string"; +import showModal from "discourse/lib/show-modal"; const IMAGE_MARKDOWN_REGEX = /!\[(.*?)\|(\d{1,4}x\d{1,4})(,\s*\d{1,3}%)?(.*?)\]\((upload:\/\/.*?)\)(?!(.*`))/g; @@ -19,7 +20,6 @@ export default ComposerEditor.extend({ classNameBindings: ["fieldClass"], allowUpload: true, showLink: false, - showHyperlinkBox: false, topic: null, showToolbar: true, focusTarget: "reply", @@ -29,6 +29,7 @@ export default ComposerEditor.extend({ draftStatus: "null", replyPlaceholder: alias("field.translatedPlaceholder"), wizardEventFieldId: null, + composerEventPrefix: "wizard-editor", @on("didInsertElement") _composerEditorInit() { @@ -77,24 +78,13 @@ export default ComposerEditor.extend({ $input.on("scroll", this._throttledSyncEditorAndPreviewScroll); this._bindUploadTarget(); - const wizardEventNames = ["insert-text", "replace-text"]; - const eventPrefix = this.eventPrefix; - this.appEvents.reopen({ - trigger(name, ...args) { - let eventParts = name.split(":"); - let currentEventPrefix = eventParts[0]; - let currentEventName = eventParts[1]; + const field = this.field; + this.editorInputClass = `.${dasherize(field.type)}-${dasherize( + field.id + )} .d-editor-input`; - if ( - currentEventPrefix !== "wizard-editor" && - wizardEventNames.some((wen) => wen === currentEventName) - ) { - let wizardEventName = name.replace(eventPrefix, "wizard-editor"); - return this._super(wizardEventName, ...args); - } else { - return this._super(name, ...args); - } - }, + this._uppyInstance.on("file-added", () => { + this.session.set("wizardEventFieldId", field.id); }); }, @@ -116,12 +106,6 @@ export default ComposerEditor.extend({ return uploadIcon(false, this.siteSettings); }, - click(e) { - if ($(e.target).hasClass("wizard-composer-hyperlink")) { - this.set("showHyperlinkBox", false); - } - }, - @bind _handleImageDeleteButtonClick(event) { if (!event.target.classList.contains("delete-image-button")) { @@ -165,7 +149,7 @@ export default ComposerEditor.extend({ shortcut: "K", trimLeading: true, unshift: true, - sendAction: () => component.set("showHyperlinkBox", true), + sendAction: (event) => component.send("showLinkModal", event), }); if (this.siteSettings.mentionables_enabled) { @@ -206,17 +190,18 @@ export default ComposerEditor.extend({ this._super(...arguments); }, - addLink(linkName, linkUrl) { - let link = `[${linkName}](${linkUrl})`; - this.appEvents.trigger("wizard-editor:insert-text", { - fieldId: this.field.id, - text: link, - }); - this.set("showHyperlinkBox", false); - }, + showLinkModal(toolbarEvent) { + let linkText = ""; + this._lastSel = toolbarEvent.selected; - hideBox() { - this.set("showHyperlinkBox", false); + if (this._lastSel) { + linkText = this._lastSel.value; + } + + showModal("insert-hyperlink").setProperties({ + linkText, + toolbarEvent, + }); }, showUploadModal() { diff --git a/assets/javascripts/discourse/components/custom-wizard-composer-hyperlink.js.es6 b/assets/javascripts/discourse/components/custom-wizard-composer-hyperlink.js.es6 deleted file mode 100644 index a56b7aff..00000000 --- a/assets/javascripts/discourse/components/custom-wizard-composer-hyperlink.js.es6 +++ /dev/null @@ -1,15 +0,0 @@ -import Component from "@ember/component"; - -export default Component.extend({ - classNames: ["wizard-composer-hyperlink"], - - actions: { - addLink() { - this.addLink(this.linkName, this.linkUrl); - }, - - hideBox() { - this.hideBox(); - }, - }, -}); diff --git a/assets/javascripts/discourse/components/custom-wizard-tag-chooser.js.es6 b/assets/javascripts/discourse/components/custom-wizard-tag-chooser.js.es6 index 32a1fd6a..8d439aa4 100644 --- a/assets/javascripts/discourse/components/custom-wizard-tag-chooser.js.es6 +++ b/assets/javascripts/discourse/components/custom-wizard-tag-chooser.js.es6 @@ -4,7 +4,10 @@ export default TagChooser.extend({ searchTags(url, data, callback) { if (this.tagGroups) { let tagGroupsString = this.tagGroups.join(","); - data.tag_groups = tagGroupsString; + data.filterForInput = { + name: "custom-wizard-tag-chooser", + groups: tagGroupsString, + }; } return this._super(url, data, callback); diff --git a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 index a257ed12..e19e4917 100644 --- a/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper-selector.js.es6 @@ -15,6 +15,7 @@ import { import Component from "@ember/component"; import { bind, later } from "@ember/runloop"; import I18n from "I18n"; +import Subscription from "../mixins/subscription"; const customFieldActionMap = { topic: ["create_topic", "send_message"], @@ -26,7 +27,7 @@ const customFieldActionMap = { const values = ["present", "true", "false"]; -export default Component.extend({ +export default Component.extend(Subscription, { classNameBindings: [":mapper-selector", "activeType"], showText: computed("activeType", function () { @@ -116,6 +117,9 @@ export default Component.extend({ groupEnabled: computed("options.groupSelection", "inputType", function () { return this.optionEnabled("groupSelection"); }), + guestGroup: computed("options.guestGroup", "inputType", function () { + return this.optionEnabled("guestGroup"); + }), userEnabled: computed("options.userSelection", "inputType", function () { return this.optionEnabled("userSelection"); }), @@ -126,7 +130,29 @@ export default Component.extend({ return this.connector === "is"; }), - groups: alias("site.groups"), + @discourseComputed("site.groups", "guestGroup", "subscriptionType") + groups(groups, guestGroup, subscriptionType) { + let result = groups; + if (!guestGroup) { + return result; + } + + if (["standard", "business"].includes(subscriptionType)) { + let guestIndex; + result.forEach((r, index) => { + if (r.id === 0) { + r.name = I18n.t("admin.wizard.selector.label.users"); + guestIndex = index; + } + }); + result.splice(guestIndex, 0, { + id: -1, + name: I18n.t("admin.wizard.selector.label.guests"), + }); + } + + return result; + }, categories: alias("site.categories"), showComboBox: or( "showWizardField", diff --git a/assets/javascripts/discourse/components/wizard-mapper.js.es6 b/assets/javascripts/discourse/components/wizard-mapper.js.es6 index 95aabb1c..ec58e3f2 100644 --- a/assets/javascripts/discourse/components/wizard-mapper.js.es6 +++ b/assets/javascripts/discourse/components/wizard-mapper.js.es6 @@ -32,6 +32,7 @@ export default Component.extend({ pairConnector: options.pairConnector || null, outputConnector: options.outputConnector || null, context: options.context || null, + guestGroup: options.guestGroup || false, }; let inputTypes = ["key", "value", "output"]; diff --git a/assets/javascripts/discourse/components/wizard-subscription-selector.js.es6 b/assets/javascripts/discourse/components/wizard-subscription-selector.js.es6 index 53f7d19c..351b5782 100644 --- a/assets/javascripts/discourse/components/wizard-subscription-selector.js.es6 +++ b/assets/javascripts/discourse/components/wizard-subscription-selector.js.es6 @@ -1,6 +1,6 @@ import SingleSelectComponent from "select-kit/components/single-select"; import Subscription from "../mixins/subscription"; -import wizardSchema from "discourse/plugins/discourse-custom-wizard/discourse/lib/wizard-schema"; +import { filterValues } from "discourse/plugins/discourse-custom-wizard/discourse/lib/wizard-schema"; import discourseComputed from "discourse-common/utils/decorators"; import I18n from "I18n"; @@ -40,9 +40,9 @@ export default SingleSelectComponent.extend(Subscription, { return allowedTypes; }, - @discourseComputed("feature", "attribute") + @discourseComputed("feature", "attribute", "wizard.allowGuests") content(feature, attribute) { - return wizardSchema[feature][attribute] + return filterValues(this.wizard, feature, attribute) .map((value) => { let allowedSubscriptionTypes = this.allowedSubscriptionTypes( feature, 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 c9a80e0e..75ea0ff7 100644 --- a/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 +++ b/assets/javascripts/discourse/controllers/admin-wizards-wizard-show.js.es6 @@ -10,6 +10,7 @@ import { later, scheduleOnce } from "@ember/runloop"; 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"; export default Controller.extend({ hasName: notEmpty("wizard.name"), @@ -59,6 +60,19 @@ export default Controller.extend({ } return wizardFieldList(steps); }, + + @discourseComputed("fieldTypes", "wizard.allowGuests") + filteredFieldTypes(fieldTypes) { + const fieldTypeIds = fieldTypes.map((f) => f.id); + const allowedTypeIds = filterValues( + this.wizard, + "field", + "type", + fieldTypeIds + ); + return fieldTypes.filter((f) => allowedTypeIds.includes(f.id)); + }, + getErrorMessage(result) { if (result.backend_validation_error) { return result.backend_validation_error; diff --git a/assets/javascripts/discourse/lib/wizard-schema.js.es6 b/assets/javascripts/discourse/lib/wizard-schema.js.es6 index 8c3323ff..24bda1d3 100644 --- a/assets/javascripts/discourse/lib/wizard-schema.js.es6 +++ b/assets/javascripts/discourse/lib/wizard-schema.js.es6 @@ -72,6 +72,7 @@ const field = { required: null, type: null, condition: null, + tag_groups: null, }, types: {}, mapped: ["prefill", "content", "condition", "index"], @@ -210,11 +211,41 @@ const action = { objectArrays: {}, }; +const filters = { + allow_guests: { + field: { + type: [ + "text", + "textarea", + "text_only", + "date", + "time", + "date_time", + "number", + "checkbox", + "url", + "dropdown", + "tag", + "category", + "group", + "user_selector", + ], + }, + action: { + type: ["route_to", "send_message"], + }, + }, +}; + const custom_field = { klass: ["topic", "post", "group", "category"], type: ["string", "boolean", "integer", "json"], }; +export function buildFieldTypes(types) { + wizardSchema.field.types = types; +} + field.type = Object.keys(field.types); action.type = Object.keys(action.types); @@ -224,16 +255,29 @@ const wizardSchema = { field, custom_field, action, + filters, }; -export function buildFieldTypes(types) { - wizardSchema.field.types = types; -} - export function buildFieldValidations(validations) { wizardSchema.field.validations = validations; } +export function filterValues(currentWizard, feature, attribute, values = null) { + values = values || wizardSchema[feature][attribute]; + + if (currentWizard && currentWizard.allowGuests) { + const filteredFeature = wizardSchema.filters.allow_guests[feature]; + if (filteredFeature) { + const filtered = filteredFeature[attribute]; + if (filtered) { + values = values.filter((v) => filtered.includes(v)); + } + } + } + + return values; +} + const siteSettings = getOwner(this).lookup("service:site-settings"); if (siteSettings.wizard_apis_enabled) { wizardSchema.action.types.send_to_api = { diff --git a/assets/javascripts/discourse/mixins/undo-changes.js.es6 b/assets/javascripts/discourse/mixins/undo-changes.js.es6 index b2ab322d..e98cfb0e 100644 --- a/assets/javascripts/discourse/mixins/undo-changes.js.es6 +++ b/assets/javascripts/discourse/mixins/undo-changes.js.es6 @@ -4,6 +4,8 @@ import { get, set } from "@ember/object"; import Mixin from "@ember/object/mixin"; import { deepEqual } from "discourse-common/lib/object"; +const observedCache = []; + export default Mixin.create({ didInsertElement() { this._super(...arguments); @@ -32,7 +34,13 @@ export default Mixin.create({ }; listProperties(componentType, opts).forEach((property) => { - obj.removeObserver(property, this, this.toggleUndo); + if (observedCache.includes(property)) { + obj.removeObserver(property, this, this.toggleUndo); + let index = observedCache.indexOf(property); + if (index !== -1) { + observedCache.splice(index, 1); + } + } }); }, @@ -45,6 +53,9 @@ export default Mixin.create({ }; listProperties(componentType, opts).forEach((property) => { + if (observedCache.indexOf(property) === -1) { + observedCache.push(property); + } obj.addObserver(property, this, this.toggleUndo); }); }, diff --git a/assets/javascripts/discourse/models/custom-wizard-admin.js.es6 b/assets/javascripts/discourse/models/custom-wizard-admin.js.es6 index 65c7aa7f..afca4833 100644 --- a/assets/javascripts/discourse/models/custom-wizard-admin.js.es6 +++ b/assets/javascripts/discourse/models/custom-wizard-admin.js.es6 @@ -5,8 +5,20 @@ import wizardSchema from "../lib/wizard-schema"; import { Promise } from "rsvp"; import { ajax } from "discourse/lib/ajax"; import { popupAjaxError } from "discourse/lib/ajax-error"; +import discourseComputed from "discourse-common/utils/decorators"; + +const GUEST_GROUP_ID = -1; const CustomWizardAdmin = EmberObject.extend({ + @discourseComputed("permitted.@each.output") + allowGuests(permitted) { + return ( + permitted && + permitted.filter((p) => p.output && p.output.includes(GUEST_GROUP_ID)) + .length + ); + }, + save(opts) { return new Promise((resolve, reject) => { let wizard = this.buildJson(this, "wizard"); diff --git a/assets/javascripts/discourse/routes/custom-wizard-index.js.es6 b/assets/javascripts/discourse/routes/custom-wizard-index.js.es6 index 1d5a71c7..5ffe83c6 100644 --- a/assets/javascripts/discourse/routes/custom-wizard-index.js.es6 +++ b/assets/javascripts/discourse/routes/custom-wizard-index.js.es6 @@ -4,13 +4,7 @@ import Route from "@ember/routing/route"; export default Route.extend({ beforeModel() { const wizard = getCachedWizard(); - if ( - wizard && - wizard.user && - wizard.permitted && - !wizard.completed && - wizard.start - ) { + if (wizard && wizard.permitted && !wizard.completed && wizard.start) { this.replaceWith("customWizardStep", wizard.start); } }, @@ -26,7 +20,7 @@ export default Route.extend({ const wizardId = model.get("id"); const user = model.get("user"); const name = model.get("name"); - const requiresLogin = !user; + const requiresLogin = !user && !permitted; const notPermitted = !permitted; const props = { diff --git a/assets/javascripts/discourse/routes/custom-wizard-step.js.es6 b/assets/javascripts/discourse/routes/custom-wizard-step.js.es6 index 969df1eb..dd7b8be8 100644 --- a/assets/javascripts/discourse/routes/custom-wizard-step.js.es6 +++ b/assets/javascripts/discourse/routes/custom-wizard-step.js.es6 @@ -7,7 +7,7 @@ export default Route.extend({ const wizard = getCachedWizard(); this.set("wizard", wizard); - if (!wizard || !wizard.user || !wizard.permitted || wizard.completed) { + if (!wizard || !wizard.permitted || wizard.completed) { this.replaceWith("customWizard"); } }, diff --git a/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs b/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs index 11a2b415..a81ec6db 100644 --- a/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs +++ b/assets/javascripts/discourse/templates/admin-wizards-wizard-show.hbs @@ -140,6 +140,7 @@ context="wizard" inputTypes="assignment,validation" groupSelection="output" + guestGroup=true userFieldSelection="key" textSelection="value" inputConnector="and" @@ -160,7 +161,7 @@ wizard=wizard currentField=currentField wizardFields=wizardFields - fieldTypes=fieldTypes + fieldTypes=filteredFieldTypes subscribed=subscribed}} {{/if}} @@ -178,7 +179,7 @@ apis=apis removeAction="removeAction" wizardFields=wizardFields - fieldTypes=fieldTypes}} + fieldTypes=filteredFieldTypes}} {{/each}}
diff --git a/assets/javascripts/discourse/templates/components/custom-wizard-composer-hyperlink.hbs b/assets/javascripts/discourse/templates/components/custom-wizard-composer-hyperlink.hbs deleted file mode 100644 index f430fb59..00000000 --- a/assets/javascripts/discourse/templates/components/custom-wizard-composer-hyperlink.hbs +++ /dev/null @@ -1,21 +0,0 @@ - diff --git a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs index 8245f1b8..106c61ee 100644 --- a/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs +++ b/assets/javascripts/discourse/templates/components/wizard-custom-action.hbs @@ -17,6 +17,7 @@ feature="action" attribute="type" onChange=(action "changeType") + wizard=wizard options=(hash none="admin.wizard.select_type" ) diff --git a/assets/stylesheets/common/wizard/step.scss b/assets/stylesheets/common/wizard/step.scss index 0b774224..2820cfcb 100644 --- a/assets/stylesheets/common/wizard/step.scss +++ b/assets/stylesheets/common/wizard/step.scss @@ -17,7 +17,7 @@ body.custom-wizard .wizard-column { } } - img.emoji { + .emoji { width: 20px; height: 20px; vertical-align: middle; @@ -29,7 +29,7 @@ body.custom-wizard .wizard-column { p { img { - @extend img.emoji; + @extend .emoji; } } diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 1ec2b3ce..8a856636 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -217,6 +217,8 @@ en: list: "list" custom_field: "custom field" value: "value" + users: "users" + guests: "users and guests" placeholder: text: "Enter text" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 08cf5336..e8ceb44b 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -53,7 +53,8 @@ en: after_signup_after_time: "You can't use 'after time' and 'after signup' on the same wizard." after_time: "After time setting is invalid." liquid_syntax_error: "Liquid syntax error in %{attribute}: %{message}" - subscription: "%{type} %{property} is subscription only" + 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" site_settings: custom_wizard_enabled: "Enable custom wizards." diff --git a/lib/custom_wizard/action.rb b/lib/custom_wizard/action.rb index edd3b23c..34f81455 100644 --- a/lib/custom_wizard/action.rb +++ b/lib/custom_wizard/action.rb @@ -6,6 +6,14 @@ class CustomWizard::Action :guardian, :result + REQUIRES_USER = %w[ + create_topic + update_profile + open_composer + watch_categories + add_to_group + ] + def initialize(opts) @wizard = opts[:wizard] @action = opts[:action] @@ -17,6 +25,12 @@ class CustomWizard::Action end def perform + if REQUIRES_USER.include?(action['id']) && !@user + log_error("action requires user", "id: #{action['id']};") + @result.success = false + return @result + end + ActiveRecord::Base.transaction do self.send(action['type'].to_sym) end @@ -76,7 +90,6 @@ class CustomWizard::Action end def send_message - if action['required'].present? required = CustomWizard::Mapper.new( inputs: action['required'], @@ -123,13 +136,14 @@ class CustomWizard::Action params[:archetype] = Archetype.private_message - creator = PostCreator.new(user, params) + poster = user || Discourse.system_user + creator = PostCreator.new(poster, params) post = creator.create if creator.errors.present? messages = creator.errors.full_messages.join(" ") log_error("failed to create message", messages) - elsif action['skip_redirect'].blank? + elsif user && action['skip_redirect'].blank? @submission.redirect_on_complete = post.topic.url end @@ -809,10 +823,12 @@ class CustomWizard::Action end def save_log + username = user ? user.username : @wizard.actor_id + CustomWizard::Log.create( @wizard.id, action['type'], - user.username, + username, @log.join('; ') ) end diff --git a/lib/custom_wizard/builder.rb b/lib/custom_wizard/builder.rb index 3b12ad27..0d0b689d 100644 --- a/lib/custom_wizard/builder.rb +++ b/lib/custom_wizard/builder.rb @@ -2,10 +2,10 @@ class CustomWizard::Builder attr_accessor :wizard, :updater, :template - def initialize(wizard_id, user = nil) + def initialize(wizard_id, user = nil, guest_id = nil) @template = CustomWizard::Template.create(wizard_id) return nil if @template.nil? - @wizard = CustomWizard::Wizard.new(template.data, user) + @wizard = CustomWizard::Wizard.new(template.data, user, guest_id) end def self.sorted_handlers @@ -182,7 +182,7 @@ class CustomWizard::Builder if field_template['description'].present? params[:description] = mapper.interpolate( field_template['description'], - user: true, + user: @wizard.user, value: true, wizard: true, template: true @@ -192,7 +192,7 @@ class CustomWizard::Builder if field_template['preview_template'].present? preview_template = mapper.interpolate( field_template['preview_template'], - user: true, + user: @wizard.user, value: true, wizard: true, template: true @@ -204,7 +204,7 @@ class CustomWizard::Builder if field_template['placeholder'].present? params[:placeholder] = mapper.interpolate( field_template['placeholder'], - user: true, + user: @wizard.user, value: true, wizard: true, template: true @@ -248,7 +248,7 @@ class CustomWizard::Builder if step_template['description'] step.description = mapper.interpolate( step_template['description'], - user: true, + user: @wizard.user, value: true, wizard: true, template: true diff --git a/lib/custom_wizard/extensions/discourse_tagging.rb b/lib/custom_wizard/extensions/discourse_tagging.rb index 3c81bbb3..701158bf 100644 --- a/lib/custom_wizard/extensions/discourse_tagging.rb +++ b/lib/custom_wizard/extensions/discourse_tagging.rb @@ -1,10 +1,9 @@ # frozen_string_literal: true -require 'request_store' module CustomWizardDiscourseTagging def filter_allowed_tags(guardian, opts = {}) - if tag_groups = ::RequestStore.store[:tag_groups] - tag_group_array = tag_groups.split(",") + if opts[:for_input].respond_to?(:dig) && (groups = opts.dig(:for_input, :groups)).present? + tag_group_array = groups.split(",") filtered_tags = TagGroup.includes(:tags).where(name: tag_group_array).map do |tag_group| tag_group.tags.pluck(:name) end.flatten diff --git a/lib/custom_wizard/extensions/tags_controller.rb b/lib/custom_wizard/extensions/tags_controller.rb deleted file mode 100644 index 0ddacb5f..00000000 --- a/lib/custom_wizard/extensions/tags_controller.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true -require 'request_store' - -module CustomWizardTagsController - def search - ::RequestStore.store[:tag_groups] = params[:tag_groups] if params[:tag_groups].present? - super - end -end diff --git a/lib/custom_wizard/field.rb b/lib/custom_wizard/field.rb index 6215fc8c..ec85ff3a 100644 --- a/lib/custom_wizard/field.rb +++ b/lib/custom_wizard/field.rb @@ -29,6 +29,11 @@ class CustomWizard::Field attr_accessor :index, :step + REQUIRES_USER = %w[ + composer + upload + ] + def initialize(attrs) @raw = attrs || {} @id = attrs[:id] diff --git a/lib/custom_wizard/mapper.rb b/lib/custom_wizard/mapper.rb index b677a710..9d26c82e 100644 --- a/lib/custom_wizard/mapper.rb +++ b/lib/custom_wizard/mapper.rb @@ -203,6 +203,8 @@ class CustomWizard::Mapper end def map_user_field(value) + return nil unless user + if value.include?(User::USER_FIELD_PREFIX) user.custom_fields[value] elsif PROFILE_FIELDS.include?(value) @@ -229,7 +231,7 @@ class CustomWizard::Mapper def interpolate(string, opts = { user: true, wizard: true, value: true, template: false }) return string if string.blank? || string.frozen? - if opts[:user] + if opts[:user] && @user.present? string.gsub!(/u\{(.*?)\}/) { |match| map_user_field($1) || '' } end @@ -253,7 +255,7 @@ class CustomWizard::Mapper end end - if opts[:template] && CustomWizard::Subscription.subscribed? + if opts[:template] #&& CustomWizard::Subscription.subscribed? template = Liquid::Template.parse(string) string = template.render(data) end @@ -282,4 +284,8 @@ class CustomWizard::Mapper user.avatar_template_url.gsub("{size}", parts.last) end end + + def self.mapped_value?(value) + value.is_a?(Array) && value.all? { |v| v.is_a?(Hash) && v.key?("type") } + end end diff --git a/lib/custom_wizard/step_updater.rb b/lib/custom_wizard/step_updater.rb index ab86f3fa..511001c2 100644 --- a/lib/custom_wizard/step_updater.rb +++ b/lib/custom_wizard/step_updater.rb @@ -5,8 +5,7 @@ class CustomWizard::StepUpdater attr_accessor :refresh_required, :result attr_reader :step, :submission - def initialize(current_user, wizard, step, submission) - @current_user = current_user + def initialize(wizard, step, submission) @wizard = wizard @step = step @refresh_required = false @@ -22,9 +21,9 @@ class CustomWizard::StepUpdater @step.updater.call(self) - UserHistory.create( - action: UserHistory.actions[:custom_wizard_step], - acting_user_id: @current_user.id, + CustomWizard::UserHistory.create( + action: CustomWizard::UserHistory.actions[:step], + actor_id: @wizard.actor_id, context: @wizard.id, subject: @step.id ) diff --git a/lib/custom_wizard/submission.rb b/lib/custom_wizard/submission.rb index a52172e3..fc10cf31 100644 --- a/lib/custom_wizard/submission.rb +++ b/lib/custom_wizard/submission.rb @@ -7,8 +7,6 @@ class CustomWizard::Submission META ||= %w(updated_at submitted_at route_to redirect_on_complete redirect_to) attr_reader :id, - :user, - :user_id, :wizard attr_accessor :fields, @@ -18,15 +16,8 @@ class CustomWizard::Submission class_eval { attr_accessor attr } end - def initialize(wizard, data = {}, user_id = nil) + def initialize(wizard, data = {}) @wizard = wizard - @user_id = user_id - - if user_id - @user = User.find_by(id: user_id) - else - @user = wizard.user - end data = (data || {}).with_indifferent_access @id = data['id'] || SecureRandom.hex(12) @@ -44,13 +35,13 @@ class CustomWizard::Submission return nil unless wizard.save_submissions validate - submission_list = self.class.list(wizard, user_id: user.id) + submission_list = self.class.list(wizard) submissions = submission_list.submissions.select { |submission| submission.id != self.id } self.updated_at = Time.now.iso8601 submissions.push(self) submission_data = submissions.map { |submission| data_to_save(submission) } - PluginStore.set("#{wizard.id}_#{KEY}", user.id, submission_data) + PluginStore.set("#{wizard.id}_#{KEY}", wizard.actor_id, submission_data) end def validate @@ -93,25 +84,21 @@ class CustomWizard::Submission data end - def self.get(wizard, user_id) - data = PluginStore.get("#{wizard.id}_#{KEY}", user_id).last - new(wizard, data, user_id) + def self.get(wizard) + data = PluginStore.get("#{wizard.id}_#{KEY}", wizard.actor_id).last + new(wizard, data) end def remove if present? - user_id = @user.id - wizard_id = @wizard.id - submission_id = @id - data = PluginStore.get("#{wizard_id}_#{KEY}", user_id) - data.delete_if { |sub| sub["id"] == submission_id } - PluginStore.set("#{wizard_id}_#{KEY}", user_id, data) + data = PluginStore.get("#{@wizard.id}_#{KEY}", wizard.actor_id) + data.delete_if { |sub| sub["id"] == @id } + PluginStore.set("#{@wizard.id}_#{KEY}", wizard.actor_id, data) end end def self.cleanup_incomplete_submissions(wizard) - user_id = wizard.user.id - all_submissions = list(wizard, user_id: user_id) + all_submissions = list(wizard) sorted_submissions = all_submissions.submissions.sort_by do |submission| zero_epoch_time = DateTime.strptime("0", '%s') [ @@ -129,12 +116,12 @@ class CustomWizard::Submission end valid_data = valid_submissions.map { |submission| submission.data_to_save(submission) } - PluginStore.set("#{wizard.id}_#{KEY}", user_id, valid_data) + PluginStore.set("#{wizard.id}_#{KEY}", wizard.actor_id, valid_data) end - def self.list(wizard, user_id: nil, order_by: nil, page: nil) + def self.list(wizard, order_by: nil, page: nil) params = { plugin_name: "#{wizard.id}_#{KEY}" } - params[:key] = user_id if user_id.present? + params[:key] = wizard.actor_id if wizard.actor_id query = PluginStoreRow.where(params) result = OpenStruct.new(submissions: [], total: nil) @@ -142,7 +129,7 @@ class CustomWizard::Submission query.each do |record| if (submission_data = ::JSON.parse(record.value)).any? submission_data.each do |data| - result.submissions.push(new(wizard, data, record.key)) + result.submissions.push(new(wizard, data)) end end end diff --git a/lib/custom_wizard/subscription.rb b/lib/custom_wizard/subscription.rb index c3c9803d..dfb75324 100644 --- a/lib/custom_wizard/subscription.rb +++ b/lib/custom_wizard/subscription.rb @@ -17,7 +17,7 @@ class CustomWizard::Subscription none: [], standard: ['*'], business: ['*'], - community: ['*'] + community: ['*', "!#{CustomWizard::Wizard::GUEST_GROUP_ID}"] }, restart_on_revisit: { none: [], @@ -114,8 +114,15 @@ class CustomWizard::Subscription ## Subscription type does not support the attribute. return false if values.blank? + ## Value is an exception for the subscription type + if (exceptions = get_exceptions(values)).any? + value = mapped_output(value) if CustomWizard::Mapper.mapped_value?(value) + value = [*value].map(&:to_s) + return false if (exceptions & value).length > 0 + end + ## Subscription type supports all values of the attribute. - return true if values.first === "*" + return true if values.include?("*") ## Subscription type supports some values of the attributes. values.include?(value) @@ -192,4 +199,21 @@ class CustomWizard::Subscription def self.includes?(feature, attribute, value) new.includes?(feature, attribute, value) end + + protected + + def get_exceptions(values) + values.reduce([]) do |result, value| + result << value.split("!").last if value.start_with?("!") + result + end + end + + def mapped_output(value) + value.reduce([]) do |result, v| + ## We can only validate mapped assignment values at the moment + result << v["output"] if v.is_a?(Hash) && v["type"] === "assignment" + result + end.flatten + end end diff --git a/lib/custom_wizard/template.rb b/lib/custom_wizard/template.rb index 4163a1f7..12a86bf6 100644 --- a/lib/custom_wizard/template.rb +++ b/lib/custom_wizard/template.rb @@ -23,7 +23,6 @@ class CustomWizard::Template normalize_data validate_data prepare_data - return false if errors.any? ActiveRecord::Base.transaction do diff --git a/lib/custom_wizard/user_history.rb b/lib/custom_wizard/user_history.rb new file mode 100644 index 00000000..1d5ee3e1 --- /dev/null +++ b/lib/custom_wizard/user_history.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true +UserHistory.actions[:custom_wizard_step] = 1000 + +class CustomWizard::UserHistory + def self.where(actor_id: nil, action: nil, context: nil, subject: nil) + ::UserHistory.where(where_opts(actor_id, action, context, subject)) + end + + def self.create(actor_id: nil, action: nil, context: nil, subject: nil) + ::UserHistory.create(create_opts(actor_id, action, context, subject)) + end + + def self.create!(actor_id: nil, action: nil, context: nil, subject: nil) + ::UserHistory.create!(create_opts(actor_id, action, context, subject)) + end + + def self.actions + @actions ||= + Enum.new( + step: UserHistory.actions[:custom_wizard_step] + ) + end + + def self.where_opts(actor_id, action, context, subject) + opts = { + context: context + } + opts[:action] = action if action + opts[:subject] = subject if subject + add_actor(opts, actor_id) + end + + def self.create_opts(actor_id, action, context, subject) + opts = { + action: action, + context: context + } + opts[:subject] = subject if subject + add_actor(opts, actor_id) + end + + def self.add_actor(opts, actor_id) + acting_user_id = actor_id + + if actor_id.is_a?(String) && actor_id.include?(CustomWizard::Wizard::GUEST_ID_PREFIX) + opts[:acting_user_id] = Discourse.system_user.id + opts[:details] = actor_id + else + opts[:acting_user_id] = actor_id + end + + opts + end +end diff --git a/lib/custom_wizard/validators/template.rb b/lib/custom_wizard/validators/template.rb index 079f9884..60652322 100644 --- a/lib/custom_wizard/validators/template.rb +++ b/lib/custom_wizard/validators/template.rb @@ -30,6 +30,7 @@ class CustomWizard::TemplateValidator validate_subscription(field, :field) check_required(field, :field) validate_liquid_template(field, :field) + validate_guests(field, :field) end end end @@ -39,6 +40,7 @@ class CustomWizard::TemplateValidator validate_subscription(action, :action) check_required(action, :action) validate_liquid_template(action, :action) + validate_guests(action, :action) end end @@ -80,6 +82,21 @@ class CustomWizard::TemplateValidator end end + def validate_guests(object, type) + guests_permitted = @data[:permitted] && @data[:permitted].any? do |m| + m["output"].include?(CustomWizard::Wizard::GUEST_GROUP_ID) + end + return unless guests_permitted + + if type === :action && CustomWizard::Action::REQUIRES_USER.include?(object[:type]) + errors.add :base, I18n.t("wizard.validation.not_permitted_for_guests", object_id: object[:id]) + end + + if type === :field && CustomWizard::Field::REQUIRES_USER.include?(object[:type]) + errors.add :base, I18n.t("wizard.validation.not_permitted_for_guests", object_id: object[:id]) + end + end + def validate_after_signup return unless ActiveRecord::Type::Boolean.new.cast(@data[:after_signup]) diff --git a/lib/custom_wizard/wizard.rb b/lib/custom_wizard/wizard.rb index 223aeaa5..4ed4037d 100644 --- a/lib/custom_wizard/wizard.rb +++ b/lib/custom_wizard/wizard.rb @@ -4,8 +4,6 @@ require_dependency 'wizard/field' require_dependency 'wizard/step_updater' require_dependency 'wizard/builder' -UserHistory.actions[:custom_wizard_step] = 1000 - class CustomWizard::Wizard include ActiveModel::SerializerSupport @@ -31,13 +29,22 @@ class CustomWizard::Wizard :actions, :action_ids, :user, + :guest_id, :submissions, :template attr_reader :all_step_ids - def initialize(attrs = {}, user = nil) - @user = user + GUEST_ID_PREFIX ||= "guest" + GUEST_GROUP_ID = -1 + + def initialize(attrs = {}, user = nil, guest_id = nil) + if user + @user = user + elsif guest_id + @guest_id = guest_id + end + attrs = attrs.with_indifferent_access @id = attrs['id'] @@ -81,6 +88,10 @@ class CustomWizard::Wizard @template = attrs end + def actor_id + user ? user.id : guest_id + end + def cast_bool(val) val.nil? ? false : ActiveRecord::Type::Boolean.new.cast(val) end @@ -141,17 +152,16 @@ class CustomWizard::Wizard end def last_completed_step_id - if user && unfinished? && last_completed_step = ::UserHistory.where( - acting_user_id: user.id, - action: ::UserHistory.actions[:custom_wizard_step], - context: id, - subject: all_step_ids - ).order("created_at").last + return nil unless actor_id && unfinished? - last_completed_step.subject - else - nil - end + last_completed_step = CustomWizard::UserHistory.where( + actor_id: actor_id, + action: CustomWizard::UserHistory.actions[:step], + context: id, + subject: all_step_ids + ).order("created_at").last + + last_completed_step&.subject end def find_step(step_id) @@ -161,15 +171,15 @@ class CustomWizard::Wizard def create_updater(step_id, submission) step = @steps.find { |s| s.id == step_id } wizard = self - CustomWizard::StepUpdater.new(user, wizard, step, submission) + CustomWizard::StepUpdater.new(wizard, step, submission) end def unfinished? - return nil if !user + return nil unless actor_id - most_recent = ::UserHistory.where( - acting_user_id: user.id, - action: ::UserHistory.actions[:custom_wizard_step], + most_recent = CustomWizard::UserHistory.where( + actor_id: actor_id, + action: CustomWizard::UserHistory.actions[:step], context: id, ).distinct.order('updated_at DESC').first @@ -183,11 +193,11 @@ class CustomWizard::Wizard end def completed? - return nil if !user + return nil unless actor_id - history = ::UserHistory.where( - acting_user_id: user.id, - action: ::UserHistory.actions[:custom_wizard_step], + history = CustomWizard::UserHistory.where( + actor_id: actor_id, + action: CustomWizard::UserHistory.actions[:step], context: id ) @@ -200,8 +210,9 @@ class CustomWizard::Wizard end def permitted? - return false unless user - return true if user.admin? || permitted.blank? + return nil unless actor_id + return true if user && (user.admin? || permitted.blank?) + return false if !user && permitted.blank? mapper = CustomWizard::Mapper.new( inputs: permitted, @@ -215,27 +226,32 @@ class CustomWizard::Wizard return true if mapper.blank? mapper.all? do |m| - if m[:type] === 'assignment' - [*m[:result]].include?(Group::AUTO_GROUPS[:everyone]) || - GroupUser.exists?(group_id: m[:result], user_id: user.id) - elsif m[:type] === 'validation' - m[:result] + if !user + m[:type] === 'assignment' && [*m[:result]].include?(GUEST_GROUP_ID) else - true + if m[:type] === 'assignment' + [*m[:result]].include?(GUEST_GROUP_ID) || + [*m[:result]].include?(Group::AUTO_GROUPS[:everyone]) || + GroupUser.exists?(group_id: m[:result], user_id: user.id) + elsif m[:type] === 'validation' + m[:result] + else + true + end end end end def can_access? - return false unless user - return true if user.admin - permitted? && (multiple_submissions || !completed?) + permitted? && (user&.admin? || (multiple_submissions || !completed?)) end def reset - ::UserHistory.create( - action: ::UserHistory.actions[:custom_wizard_step], - acting_user_id: user.id, + return nil unless actor_id + + CustomWizard::UserHistory.create( + action: CustomWizard::UserHistory.actions[:step], + actor_id: actor_id, context: id, subject: "reset" ) @@ -263,8 +279,7 @@ class CustomWizard::Wizard end def submissions - return nil unless user.present? - @submissions ||= CustomWizard::Submission.list(self, user_id: user.id).submissions + @submissions ||= CustomWizard::Submission.list(self).submissions end def current_submission @@ -300,15 +315,17 @@ class CustomWizard::Wizard end def remove_user_redirect + return unless user.present? + if id == user.redirect_to_wizard user.custom_fields.delete('redirect_to_wizard') user.save_custom_fields(true) end end - def self.create(wizard_id, user = nil) + def self.create(wizard_id, user = nil, guest_id = nil) if template = CustomWizard::Template.find(wizard_id) - new(template.to_h, user) + new(template.to_h, user, guest_id) else false end @@ -319,7 +336,7 @@ class CustomWizard::Wizard CustomWizard::Template.list(**template_opts).reduce([]) do |result, template| wizard = new(template, user) - result.push(wizard) if wizard.can_access? && ( + result.push(wizard) if wizard.permitted? && ( !not_completed || !wizard.completed? ) result @@ -380,4 +397,8 @@ class CustomWizard::Wizard false end end + + def self.generate_guest_id + "#{self::GUEST_ID_PREFIX}_#{SecureRandom.hex(12)}" + end end diff --git a/plugin.rb b/plugin.rb index 4c0ce987..41d9270d 100644 --- a/plugin.rb +++ b/plugin.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true # name: discourse-custom-wizard # about: Forms for Discourse. Better onboarding, structured posting, data enrichment, automated actions and much more. +<<<<<<< HEAD # version: 2.1.5 +======= +# version: 2.2.9 +>>>>>>> main # authors: Angus McLeod, Faizaan Gagan, Robert Barrow, Keegan George, Kaitlin Maddever # url: https://github.com/paviliondev/discourse-custom-wizard # contact_emails: development@pavilion.tech @@ -41,6 +45,7 @@ after_initialize do ../app/controllers/custom_wizard/admin/logs.rb ../app/controllers/custom_wizard/admin/manager.rb ../app/controllers/custom_wizard/admin/custom_fields.rb + ../app/controllers/custom_wizard/wizard_client.rb ../app/controllers/custom_wizard/wizard.rb ../app/controllers/custom_wizard/steps.rb ../app/controllers/custom_wizard/realtime_validations.rb @@ -65,6 +70,7 @@ after_initialize do ../lib/custom_wizard/subscription.rb ../lib/custom_wizard/template.rb ../lib/custom_wizard/wizard.rb + ../lib/custom_wizard/user_history.rb ../lib/custom_wizard/api/api.rb ../lib/custom_wizard/api/authorization.rb ../lib/custom_wizard/api/endpoint.rb @@ -88,7 +94,6 @@ after_initialize do ../lib/custom_wizard/extensions/extra_locales_controller.rb ../lib/custom_wizard/extensions/invites_controller.rb ../lib/custom_wizard/extensions/users_controller.rb - ../lib/custom_wizard/extensions/tags_controller.rb ../lib/custom_wizard/extensions/guardian.rb ../lib/custom_wizard/extensions/custom_field/preloader.rb ../lib/custom_wizard/extensions/custom_field/serializer.rb @@ -230,7 +235,6 @@ after_initialize do end reloadable_patch do |plugin| - ::TagsController.prepend CustomWizardTagsController ::DiscourseTagging.singleton_class.prepend CustomWizardDiscourseTagging end diff --git a/spec/components/custom_wizard/action_spec.rb b/spec/components/custom_wizard/action_spec.rb index 07c1084f..79c64520 100644 --- a/spec/components/custom_wizard/action_spec.rb +++ b/spec/components/custom_wizard/action_spec.rb @@ -18,6 +18,7 @@ describe CustomWizard::Action do let(:api_test_endpoint) { get_wizard_fixture("endpoints/test_endpoint") } let(:api_test_endpoint_body) { get_wizard_fixture("endpoints/test_endpoint_body") } let(:api_test_no_authorization) { get_wizard_fixture("api/no_authorization") } + let(:guests_permitted) { get_wizard_fixture("wizard/guests_permitted") } def update_template(template) CustomWizard::Template.save(template, skip_jobs: true) @@ -78,8 +79,8 @@ describe CustomWizard::Action do updater.update expect(updater.success?).to eq(true) - expect(UserHistory.where( - acting_user_id: user.id, + expect(CustomWizard::UserHistory.where( + actor_id: user.id, context: "super_mega_fun_wizard", subject: "step_3" ).exists?).to eq(true) @@ -301,6 +302,28 @@ describe CustomWizard::Action do expect(topic.first.allowed_groups.map(&:name)).to include('cool_group', 'cool_group_1') expect(post.exists?).to eq(true) end + + it "send_message works with guests are permitted" do + wizard_template["permitted"] = guests_permitted["permitted"] + wizard_template.delete("actions") + wizard_template['actions'] = [send_message] + update_template(wizard_template) + + User.create(username: 'angus1', email: "angus1@email.com") + + 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 + + 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.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 end context "business subscription actions" do diff --git a/spec/components/custom_wizard/builder_spec.rb b/spec/components/custom_wizard/builder_spec.rb index ebcc355b..1e55b203 100644 --- a/spec/components/custom_wizard/builder_spec.rb +++ b/spec/components/custom_wizard/builder_spec.rb @@ -80,14 +80,11 @@ describe CustomWizard::Builder do it 'returns no steps if user has completed it' do @template[:steps].each do |step| - UserHistory.create!( - { - action: UserHistory.actions[:custom_wizard_step], - acting_user_id: user.id, - context: @template[:id] - }.merge( - subject: step[:id] - ) + CustomWizard::UserHistory.create!( + action: CustomWizard::UserHistory.actions[:step], + actor_id: user.id, + context: @template[:id], + subject: step[:id] ) end diff --git a/spec/components/custom_wizard/mapper_spec.rb b/spec/components/custom_wizard/mapper_spec.rb index 56778a07..f460cc01 100644 --- a/spec/components/custom_wizard/mapper_spec.rb +++ b/spec/components/custom_wizard/mapper_spec.rb @@ -373,7 +373,7 @@ describe CustomWizard::Mapper do expect(result).to eq(template_params["step_1_field_1"]) end - it "requires a subscription" do + it "does not require a subscription" do template = '{{ "w{step_1_field_1}" | size }}' mapper = create_template_mapper(template_params, user1) result = mapper.interpolate( @@ -383,7 +383,7 @@ describe CustomWizard::Mapper do wizard: true, value: true ) - expect(result).to eq("{{ \"#{template_params["step_1_field_1"]}\" | size }}") + expect(result).to eq("5") end context "with a subscription" do diff --git a/spec/components/custom_wizard/submission_spec.rb b/spec/components/custom_wizard/submission_spec.rb index ff9df88a..d0e0c986 100644 --- a/spec/components/custom_wizard/submission_spec.rb +++ b/spec/components/custom_wizard/submission_spec.rb @@ -4,6 +4,7 @@ describe CustomWizard::Submission do fab!(:user) { Fabricate(:user) } fab!(:user2) { Fabricate(:user) } let(:template_json) { get_wizard_fixture("wizard") } + let(:guest_id) { CustomWizard::Wizard.generate_guest_id } before do CustomWizard::Template.save(template_json, skip_jobs: true) @@ -13,10 +14,20 @@ describe CustomWizard::Submission do it "saves a user's submission" do expect( - described_class.get(@wizard, user.id).fields["step_1_field_1"] + described_class.get(@wizard).fields["step_1_field_1"] ).to eq("I am user submission") end + it "saves a guest's submission" do + CustomWizard::Template.save(template_json, skip_jobs: true) + @wizard = CustomWizard::Wizard.create(template_json["id"], nil, guest_id) + described_class.new(@wizard, step_1_field_1: "I am guest submission").save + + expect( + described_class.get(@wizard).fields["step_1_field_1"] + ).to eq("I am guest submission") + end + describe "#list" do before do freeze_time Time.now @@ -37,14 +48,17 @@ describe CustomWizard::Submission do end it "list submissions by wizard" do + @wizard.user = nil expect(described_class.list(@wizard).total).to eq(@count + 2) end it "list submissions by wizard and user" do - expect(described_class.list(@wizard, user_id: user.id).total).to eq(@count + 1) + @wizard.user = user + expect(described_class.list(@wizard).total).to eq(@count + 1) end it "paginates submission lists" do + @wizard.user = nil expect(described_class.list(@wizard, page: 1).submissions.size).to eq((@count + 2) - CustomWizard::Submission::PAGE_LIMIT) end @@ -59,7 +73,7 @@ describe CustomWizard::Submission do described_class.new(@wizard, step_1_field_1: "I am the second submission").save builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder.build - submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions + submissions = described_class.list(@wizard).submissions expect(submissions.length).to eq(1) expect(submissions.first.fields["step_1_field_1"]).to eq("I am the second submission") @@ -75,7 +89,7 @@ describe CustomWizard::Submission do PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, sub_data) builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder.build - submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions + submissions = described_class.list(@wizard).submissions expect(submissions.length).to eq(1) expect(submissions.first.fields["step_1_field_1"]).to eq("I am the second submission") @@ -92,7 +106,7 @@ describe CustomWizard::Submission do builder = CustomWizard::Builder.new(@wizard.id, @wizard.user) builder.build - submissions = described_class.list(@wizard, user_id: @wizard.user.id).submissions + submissions = described_class.list(@wizard).submissions expect(submissions.length).to eq(1) expect(submissions.first.fields["step_1_field_1"]).to eq("I am the third submission") diff --git a/spec/components/custom_wizard/subscription_spec.rb b/spec/components/custom_wizard/subscription_spec.rb index 5f06397b..2ac191e1 100644 --- a/spec/components/custom_wizard/subscription_spec.rb +++ b/spec/components/custom_wizard/subscription_spec.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true describe CustomWizard::Subscription do + let(:guests_permitted) { get_wizard_fixture("wizard/guests_permitted") } + def undefine_client_classes Object.send(:remove_const, :SubscriptionClient) if Object.constants.include?(:SubscriptionClient) Object.send(:remove_const, :SubscriptionClientSubscription) if Object.constants.include?(:SubscriptionClientSubscription) @@ -40,7 +42,7 @@ describe CustomWizard::Subscription do expect(described_class.includes?(:wizard, :after_signup, true)).to eq(true) end - it "ubscriber features are not included" do + it "subscriber features are not included" do expect(described_class.includes?(:wizard, :permitted, {})).to eq(false) end end @@ -69,6 +71,16 @@ describe CustomWizard::Subscription do end end + context "with a subscription" do + it "handles mapped values" do + SubscriptionClientSubscription.stubs(:product_id).returns(CustomWizard::Subscription::STANDARD_PRODUCT_ID) + expect(described_class.includes?(:wizard, :permitted, guests_permitted["permitted"])).to eq(true) + + SubscriptionClientSubscription.stubs(:product_id).returns(CustomWizard::Subscription::COMMUNITY_PRODUCT_ID) + expect(described_class.includes?(:wizard, :permitted, guests_permitted["permitted"])).to eq(false) + end + end + context "with standard subscription" do before do SubscriptionClientSubscription.stubs(:product_id).returns(CustomWizard::Subscription::STANDARD_PRODUCT_ID) diff --git a/spec/components/custom_wizard/template_validator_spec.rb b/spec/components/custom_wizard/template_validator_spec.rb index b149706f..b9b257e3 100644 --- a/spec/components/custom_wizard/template_validator_spec.rb +++ b/spec/components/custom_wizard/template_validator_spec.rb @@ -7,6 +7,8 @@ describe CustomWizard::TemplateValidator do let(:user_condition) { get_wizard_fixture("condition/user_condition") } let(:permitted_json) { get_wizard_fixture("wizard/permitted") } let(:composer_preview) { get_wizard_fixture("field/composer_preview") } + let(:guests_permitted) { get_wizard_fixture("wizard/guests_permitted") } + let(:upload_field) { get_wizard_fixture("field/upload") } let(:valid_liquid_template) { <<-LIQUID.strip @@ -146,6 +148,20 @@ describe CustomWizard::TemplateValidator do ).to eq(true) end + it "validates user-only features" do + template[:permitted] = guests_permitted['permitted'] + template[:steps][0][:fields] << upload_field + 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") + ) + end + it "validates step attributes" do template[:steps][0][:condition] = user_condition['condition'] expect( diff --git a/spec/components/custom_wizard/wizard_spec.rb b/spec/components/custom_wizard/wizard_spec.rb index 8268849c..591eee8c 100644 --- a/spec/components/custom_wizard/wizard_spec.rb +++ b/spec/components/custom_wizard/wizard_spec.rb @@ -6,11 +6,14 @@ describe CustomWizard::Wizard do fab!(:admin_user) { Fabricate(:user, admin: true) } let(:template_json) { get_wizard_fixture("wizard") } let(:permitted_json) { get_wizard_fixture("wizard/permitted") } + let(:guests_permitted_json) { get_wizard_fixture("wizard/guests_permitted") } before do Group.refresh_automatic_group!(:trust_level_3) @permitted_template = template_json.dup @permitted_template["permitted"] = permitted_json["permitted"] + @guests_permitted_template = template_json.dup + @guests_permitted_template["permitted"] = guests_permitted_json["permitted"] @wizard = CustomWizard::Wizard.new(template_json, user) end @@ -21,10 +24,10 @@ describe CustomWizard::Wizard do @wizard.update! end - def progress_step(step_id, acting_user: user, wizard: @wizard) - UserHistory.create( - action: UserHistory.actions[:custom_wizard_step], - acting_user_id: acting_user.id, + def progress_step(step_id, actor_id: user.id, wizard: @wizard) + CustomWizard::UserHistory.create( + action: CustomWizard::UserHistory.actions[:step], + actor_id: actor_id, context: wizard.id, subject: step_id ) @@ -158,9 +161,9 @@ describe CustomWizard::Wizard do it "lets a permitted user access a complete wizard with multiple submissions" do append_steps - progress_step("step_1", acting_user: trusted_user) - progress_step("step_2", acting_user: trusted_user) - progress_step("step_3", acting_user: trusted_user) + progress_step("step_1", actor_id: trusted_user.id) + progress_step("step_2", actor_id: trusted_user.id) + progress_step("step_3", actor_id: trusted_user.id) @permitted_template["multiple_submissions"] = true @@ -172,9 +175,9 @@ describe CustomWizard::Wizard do it "does not let an unpermitted user access a complete wizard without multiple submissions" do append_steps - progress_step("step_1", acting_user: trusted_user) - progress_step("step_2", acting_user: trusted_user) - progress_step("step_3", acting_user: trusted_user) + progress_step("step_1", actor_id: trusted_user.id) + progress_step("step_2", actor_id: trusted_user.id) + progress_step("step_3", actor_id: trusted_user.id) @permitted_template['multiple_submissions'] = false @@ -200,6 +203,30 @@ describe CustomWizard::Wizard do end end + context "with subscription and guest wizard" do + before do + enable_subscription("standard") + end + + it "permits admins" do + expect( + CustomWizard::Wizard.new(@guests_permitted_template, admin_user).permitted? + ).to eq(true) + end + + it "permits regular users" do + expect( + CustomWizard::Wizard.new(@guests_permitted_template, user).permitted? + ).to eq(true) + end + + it "permits guests" do + expect( + CustomWizard::Wizard.new(@guests_permitted_template, nil, "guest123").permitted? + ).to eq(true) + end + end + context "submissions" do before do CustomWizard::Submission.new(@wizard, step_1_field_1: "I am a user submission").save diff --git a/spec/extensions/discourse_tagging_spec.rb b/spec/extensions/discourse_tagging_spec.rb new file mode 100644 index 00000000..14adaf5b --- /dev/null +++ b/spec/extensions/discourse_tagging_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +describe ::DiscourseTagging, type: :request do + fab!(:user) { Fabricate(:user) } + fab!(:tag_1) { Fabricate(:tag, name: "Angus") } + fab!(:tag_2) { Fabricate(:tag, name: "Faizaan") } + fab!(:tag_3) { Fabricate(:tag, name: "Robert") } + fab!(:tag_4) { Fabricate(:tag, name: "Eli") } + fab!(:tag_5) { Fabricate(:tag, name: "Jeff") } + + fab!(:tag_group_1) { Fabricate(:tag_group, tags: [tag_1, tag_2]) } + fab!(:tag_group_2) { Fabricate(:tag_group, tags: [tag_3, tag_4]) } + + describe "#filter_allowed_tags" do + let(:guardian) { Guardian.new(user) } + + context "for_input is a boolean" do + it "works normally" do + filter_params = { + q: '', + for_input: true + } + tags = DiscourseTagging.filter_allowed_tags(guardian, filter_params) + names = tags.map(&:name) + all_tag_names = Tag.all.pluck(:name) + expect(names).to contain_exactly(*all_tag_names) + end + end + + context "for_input is an object including a tag group" do + it "returns tags only in the tag group" do + filter_params = { + q: "", + for_input: { + name: "custom-wizard-tag-chooser", + groups: tag_group_1.name + } + } + tags = DiscourseTagging.filter_allowed_tags(guardian, filter_params) + names = tags.map(&:name) + expected_tag_names = TagGroup + .includes(:tags) + .where(id: tag_group_1.id) + .map { |tag_group| tag_group.tags.pluck(:name) }.flatten + + expect(names).to contain_exactly(*expected_tag_names) + end + end + + context "for_input is an object including an empty tag group string" do + it "returns all tags" do + filter_params = { + q: "", + for_input: { + name: "custom-wizard-tag-chooser", + groups: "" + } + } + tags = DiscourseTagging.filter_allowed_tags(guardian, filter_params) + names = tags.map(&:name) + + all_tag_names = Tag.all.pluck(:name) + expect(names).to contain_exactly(*all_tag_names) + end + end + end +end diff --git a/spec/extensions/tags_controller_spec.rb b/spec/extensions/tags_controller_spec.rb deleted file mode 100644 index b3c1ccc8..00000000 --- a/spec/extensions/tags_controller_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -describe ::TagsController, type: :request do - fab!(:tag_1) { Fabricate(:tag, name: "Angus") } - fab!(:tag_2) { Fabricate(:tag, name: "Faizaan") } - fab!(:tag_3) { Fabricate(:tag, name: "Robert") } - fab!(:tag_4) { Fabricate(:tag, name: "Eli") } - fab!(:tag_5) { Fabricate(:tag, name: "Jeff") } - - fab!(:tag_group_1) { Fabricate(:tag_group, tags: [tag_1, tag_2]) } - fab!(:tag_group_2) { Fabricate(:tag_group, tags: [tag_3, tag_4]) } - - before do - ::RequestStore.store[:tag_groups] = nil - end - - describe "#search" do - context "tag group param present" do - it "returns tags only in the tag group" do - get "/tags/filter/search.json", params: { q: '', tag_groups: [tag_group_1.name, tag_group_2.name] } - expect(response.status).to eq(200) - results = response.parsed_body['results'] - names = results.map { |result| result['name'] } - - expected_tag_names = TagGroup - .includes(:tags) - .where(id: [tag_group_1.id, tag_group_2.id]) - .map { |tag_group| tag_group.tags.pluck(:name) }.flatten - - expect(names).to contain_exactly(*expected_tag_names) - end - end - - context "tag group param not present" do - it "returns all tags" do - get "/tags/filter/search.json", params: { q: '' } - expect(response.status).to eq(200) - results = response.parsed_body['results'] - names = results.map { |result| result['name'] } - - all_tag_names = Tag.all.pluck(:name) - expect(names).to contain_exactly(*all_tag_names) - end - end - end -end diff --git a/spec/fixtures/actions/route_to.json b/spec/fixtures/actions/route_to.json new file mode 100644 index 00000000..442b1556 --- /dev/null +++ b/spec/fixtures/actions/route_to.json @@ -0,0 +1,12 @@ +{ + "id": "route_to", + "type": "route_to", + "url": [ + { + "type": "assignment", + "output": "https://google.com", + "output_type": "text", + "output_connector": "set" + } + ] +} \ No newline at end of file diff --git a/spec/fixtures/field/upload.json b/spec/fixtures/field/upload.json new file mode 100644 index 00000000..ebf6c21d --- /dev/null +++ b/spec/fixtures/field/upload.json @@ -0,0 +1,6 @@ +{ + "id": "step_2_field_7", + "label": "Upload", + "type": "upload", + "file_types": ".jpg,.jpeg,.png" +} \ No newline at end of file diff --git a/spec/fixtures/sprockets/require_tree_discourse_empty.js b/spec/fixtures/sprockets/require_tree_discourse_empty.js index 953c5ec4..4f034d13 100644 --- a/spec/fixtures/sprockets/require_tree_discourse_empty.js +++ b/spec/fixtures/sprockets/require_tree_discourse_empty.js @@ -1 +1 @@ -//= require_tree_discourse \ No newline at end of file +//= require_tree_discourse diff --git a/spec/fixtures/sprockets/require_tree_discourse_non_existant.js b/spec/fixtures/sprockets/require_tree_discourse_non_existant.js index d9b2be76..fc4752e5 100644 --- a/spec/fixtures/sprockets/require_tree_discourse_non_existant.js +++ b/spec/fixtures/sprockets/require_tree_discourse_non_existant.js @@ -1 +1 @@ -//= require_tree_discourse dummy_path \ No newline at end of file +//= require_tree_discourse dummy_path diff --git a/spec/fixtures/sprockets/require_tree_discourse_test.js b/spec/fixtures/sprockets/require_tree_discourse_test.js index a86aa0d7..56451213 100644 --- a/spec/fixtures/sprockets/require_tree_discourse_test.js +++ b/spec/fixtures/sprockets/require_tree_discourse_test.js @@ -1 +1 @@ -//= require_tree_discourse sptest \ No newline at end of file +//= require_tree_discourse sptest diff --git a/spec/fixtures/wizard.json b/spec/fixtures/wizard.json index de5e636e..5868001e 100644 --- a/spec/fixtures/wizard.json +++ b/spec/fixtures/wizard.json @@ -74,12 +74,6 @@ "id": "step_2_field_5", "label": "Checkbox", "type": "checkbox" - }, - { - "id": "step_2_field_7", - "label": "Upload", - "type": "upload", - "file_types": ".jpg,.jpeg,.png" } ], "description": "Because I couldn't think of another name for this step :)" diff --git a/spec/fixtures/wizard/guests_permitted.json b/spec/fixtures/wizard/guests_permitted.json new file mode 100644 index 00000000..3a332f31 --- /dev/null +++ b/spec/fixtures/wizard/guests_permitted.json @@ -0,0 +1,12 @@ +{ + "permitted": [ + { + "type": "assignment", + "output_type": "group", + "output_connector": "set", + "output": [ + -1 + ] + } + ] +} \ No newline at end of file diff --git a/spec/requests/custom_wizard/admin/manager_controller_spec.rb b/spec/requests/custom_wizard/admin/manager_controller_spec.rb index 30c1aa3a..608b61fb 100644 --- a/spec/requests/custom_wizard/admin/manager_controller_spec.rb +++ b/spec/requests/custom_wizard/admin/manager_controller_spec.rb @@ -13,7 +13,7 @@ describe CustomWizard::AdminManagerController do template_3["id"] = 'super_mega_fun_wizard_3' @template_array = [template, template_2, template_3] - FileUtils.mkdir_p(file_from_fixtures_tmp_folder) unless Dir.exists?(file_from_fixtures_tmp_folder) + FileUtils.mkdir_p(file_from_fixtures_tmp_folder) unless Dir.exist?(file_from_fixtures_tmp_folder) @tmp_file_path = File.join(file_from_fixtures_tmp_folder, SecureRandom.hex << 'wizards.json') File.write(@tmp_file_path, @template_array.to_json) end diff --git a/spec/requests/custom_wizard/steps_controller_spec.rb b/spec/requests/custom_wizard/steps_controller_spec.rb index e05ba917..4d8b96eb 100644 --- a/spec/requests/custom_wizard/steps_controller_spec.rb +++ b/spec/requests/custom_wizard/steps_controller_spec.rb @@ -7,192 +7,209 @@ describe CustomWizard::StepsController do let(:wizard_field_condition_template) { get_wizard_fixture("condition/wizard_field_condition") } let(:user_condition_template) { get_wizard_fixture("condition/user_condition") } let(:permitted_json) { get_wizard_fixture("wizard/permitted") } + let(:route_to_template) { get_wizard_fixture("actions/route_to") } + let(:guests_permitted) { get_wizard_fixture("wizard/guests_permitted") } before do CustomWizard::Template.save(wizard_template, skip_jobs: true) - sign_in(user) end - it 'performs a step update' do - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Text input" + def guest_template + temp = wizard_template.dup + temp["permitted"] = guests_permitted["permitted"] + temp.delete("actions") + temp["actions"] = [route_to_template] + temp + end + + context "with guest" do + it "does not perform a step update" do + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Text input" + } } - } - expect(response.status).to eq(200) - expect(response.parsed_body['wizard']['start']).to eq("step_2") - - wizard_id = response.parsed_body['wizard']['id'] - wizard = CustomWizard::Wizard.create(wizard_id, user) - expect(wizard.current_submission.fields['step_1_field_1']).to eq("Text input") - end - - context "raises an error" do - it "when the wizard doesnt exist" do - put '/w/not-super-mega-fun-wizard/steps/step_1.json' - expect(response.status).to eq(400) - end - - it "when the user cant access the wizard" do - enable_subscription("standard") - new_template = wizard_template.dup - new_template["permitted"] = permitted_json["permitted"] - CustomWizard::Template.save(new_template, skip_jobs: true) - - put '/w/super-mega-fun-wizard/steps/step_1.json' expect(response.status).to eq(403) end - it "when the step doesnt exist" do - put '/w/super-mega-fun-wizard/steps/step_10.json' - expect(response.status).to eq(400) + context "with guests permitted" do + before do + enable_subscription("standard") + result = CustomWizard::Template.save(guest_template, skip_jobs: true) + end + + it "performs a step update" do + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Text input" + } + } + expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_2") + + wizard_id = response.parsed_body['wizard']['id'] + wizard = CustomWizard::Wizard.create(wizard_id, nil, cookies[:custom_wizard_guest_id]) + expect(wizard.current_submission.fields['step_1_field_1']).to eq("Text input") + end + + context "raises an error" do + it "when the wizard doesnt exist" do + put '/w/not-super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(400) + end + + it "when the user cant access the wizard" do + enable_subscription("standard") + new_template = guest_template.dup + new_template["permitted"] = permitted_json["permitted"] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(403) + end + + it "when the step doesnt exist" do + put '/w/super-mega-fun-wizard/steps/step_10.json' + expect(response.status).to eq(400) + end + end + + it "works if the step has no fields" do + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_2") + end + + it "returns an updated wizard when condition passes" do + new_template = guest_template.dup + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will pass" + } + } + expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_2") + end + + it "runs completion actions if guest has completed wizard" do + new_template = guest_template.dup + + ## route_to action + new_template['actions'].last['run_after'] = 'wizard_completion' + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json' + put '/w/super-mega-fun-wizard/steps/step_2.json' + put '/w/super-mega-fun-wizard/steps/step_3.json' + expect(response.status).to eq(200) + expect(response.parsed_body['redirect_on_complete']).to eq("https://google.com") + end end end - it "works if the step has no fields" do - put '/w/super-mega-fun-wizard/steps/step_1.json' - expect(response.status).to eq(200) - expect(response.parsed_body['wizard']['start']).to eq("step_2") - end - - it "returns an updated wizard when condition passes" do - new_template = wizard_template.dup - new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] - CustomWizard::Template.save(new_template, skip_jobs: true) - - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Condition will pass" - } - } - expect(response.status).to eq(200) - expect(response.parsed_body['wizard']['start']).to eq("step_2") - end - - it "runs completion actions if user has completed wizard" do - new_template = wizard_template.dup - - ## route_to action - new_template['actions'].last['run_after'] = 'wizard_completion' - CustomWizard::Template.save(new_template, skip_jobs: true) - - put '/w/super-mega-fun-wizard/steps/step_1.json' - put '/w/super-mega-fun-wizard/steps/step_2.json' - put '/w/super-mega-fun-wizard/steps/step_3.json' - expect(response.status).to eq(200) - expect(response.parsed_body['redirect_on_complete']).to eq("https://google.com") - end - - it "saves results of completion actions if user has completed wizard" do - new_template = wizard_template.dup - new_template['actions'].first['run_after'] = 'wizard_completion' - CustomWizard::Template.save(new_template, skip_jobs: true) - - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Topic title", - step_1_field_2: "Topic post" - } - } - put '/w/super-mega-fun-wizard/steps/step_2.json' - put '/w/super-mega-fun-wizard/steps/step_3.json' - - wizard_id = response.parsed_body['wizard']['id'] - wizard = CustomWizard::Wizard.create(wizard_id, user) - - topic_id = wizard.submissions.first.fields[new_template['actions'].first['id']] - topic = Topic.find(topic_id) - expect(topic.present?).to eq(true) - end - - it "returns a final step without conditions" do - put '/w/super-mega-fun-wizard/steps/step_1.json' - expect(response.status).to eq(200) - expect(response.parsed_body['final']).to eq(false) - - put '/w/super-mega-fun-wizard/steps/step_2.json' - expect(response.status).to eq(200) - expect(response.parsed_body['final']).to eq(false) - - put '/w/super-mega-fun-wizard/steps/step_3.json' - expect(response.status).to eq(200) - expect(response.parsed_body['final']).to eq(true) - end - - context "subscription" do + context "with user" do before do - enable_subscription("standard") + sign_in(user) end - it "raises an error when user cant see the step due to conditions" do - sign_in(user2) - - new_wizard_template = wizard_template.dup - new_wizard_template['steps'][0]['condition'] = user_condition_template['condition'] - CustomWizard::Template.save(new_wizard_template, skip_jobs: true) - - put '/w/super-mega-fun-wizard/steps/step_1.json' - expect(response.status).to eq(403) - end - - it "returns an updated wizard when condition doesnt pass" do - new_template = wizard_template.dup - new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] - CustomWizard::Template.save(new_template, skip_jobs: true) - + it 'performs a step update' do put '/w/super-mega-fun-wizard/steps/step_1.json', params: { fields: { - step_1_field_1: "Condition wont pass" + step_1_field_1: "Text input" } } expect(response.status).to eq(200) - expect(response.parsed_body['wizard']['start']).to eq("step_3") + expect(response.parsed_body['wizard']['start']).to eq("step_2") + + wizard_id = response.parsed_body['wizard']['id'] + wizard = CustomWizard::Wizard.create(wizard_id, user) + expect(wizard.current_submission.fields['step_1_field_1']).to eq("Text input") end - it "returns the correct final step when the conditional final step and last step are the same" do - new_template = wizard_template.dup - new_template['steps'][0]['condition'] = user_condition_template['condition'] - new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] - CustomWizard::Template.save(new_template, skip_jobs: true) + context "raises an error" do + it "when the wizard doesnt exist" do + put '/w/not-super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(400) + end + + it "when the user cant access the wizard" do + enable_subscription("standard") + new_template = wizard_template.dup + new_template["permitted"] = permitted_json["permitted"] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(403) + end + + it "when the step doesnt exist" do + put '/w/super-mega-fun-wizard/steps/step_10.json' + expect(response.status).to eq(400) + end end - it "raises an error when user cant see the step due to conditions" do - sign_in(user2) - - new_wizard_template = wizard_template.dup - new_wizard_template['steps'][0]['condition'] = user_condition_template['condition'] - CustomWizard::Template.save(new_wizard_template, skip_jobs: true) - + it "works if the step has no fields" do put '/w/super-mega-fun-wizard/steps/step_1.json' - expect(response.status).to eq(403) + expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_2") end - it "returns an updated wizard when condition doesnt pass" do + it "returns an updated wizard when condition passes" do new_template = wizard_template.dup new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] CustomWizard::Template.save(new_template, skip_jobs: true) - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Condition wont pass" - } - } - expect(response.status).to eq(200) - expect(response.parsed_body['wizard']['start']).to eq("step_3") - end - - it "returns the correct final step when the conditional final step and last step are the same" do - new_template = wizard_template.dup - new_template['steps'][0]['condition'] = user_condition_template['condition'] - new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] - CustomWizard::Template.save(new_template, skip_jobs: true) - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { fields: { step_1_field_1: "Condition will pass" } } expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_2") + end + + it "runs completion actions if user has completed wizard" do + new_template = wizard_template.dup + + ## route_to action + new_template['actions'].last['run_after'] = 'wizard_completion' + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json' + put '/w/super-mega-fun-wizard/steps/step_2.json' + put '/w/super-mega-fun-wizard/steps/step_3.json' + expect(response.status).to eq(200) + expect(response.parsed_body['redirect_on_complete']).to eq("https://google.com") + end + + it "saves results of completion actions if user has completed wizard" do + new_template = wizard_template.dup + new_template['actions'].first['run_after'] = 'wizard_completion' + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Topic title", + step_1_field_2: "Topic post" + } + } + put '/w/super-mega-fun-wizard/steps/step_2.json' + put '/w/super-mega-fun-wizard/steps/step_3.json' + + wizard_id = response.parsed_body['wizard']['id'] + wizard = CustomWizard::Wizard.create(wizard_id, user) + + topic_id = wizard.submissions.first.fields[new_template['actions'].first['id']] + topic = Topic.find(topic_id) + expect(topic.present?).to eq(true) + end + + it "returns a final step without conditions" do + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(200) expect(response.parsed_body['final']).to eq(false) put '/w/super-mega-fun-wizard/steps/step_2.json' @@ -204,66 +221,152 @@ describe CustomWizard::StepsController do expect(response.parsed_body['final']).to eq(true) end - it "returns the correct final step when the conditional final step and last step are different" do - new_template = wizard_template.dup - new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] - CustomWizard::Template.save(new_template, skip_jobs: true) + context "subscription" do + before do + enable_subscription("standard") + end - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Condition will not pass" + it "raises an error when user cant see the step due to conditions" do + sign_in(user2) + + new_wizard_template = wizard_template.dup + new_wizard_template['steps'][0]['condition'] = user_condition_template['condition'] + CustomWizard::Template.save(new_wizard_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(403) + end + + it "returns an updated wizard when condition doesnt pass" do + new_template = wizard_template.dup + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition wont pass" + } } - } - expect(response.status).to eq(200) - expect(response.parsed_body['final']).to eq(false) + expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_3") + end - put '/w/super-mega-fun-wizard/steps/step_2.json' - expect(response.status).to eq(200) - expect(response.parsed_body['final']).to eq(true) - end + it "returns the correct final step when the conditional final step and last step are the same" do + new_template = wizard_template.dup + new_template['steps'][0]['condition'] = user_condition_template['condition'] + new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + end - it "returns the correct final step when the conditional final step is determined in the same action" do - new_template = wizard_template.dup - new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] - new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] - CustomWizard::Template.save(new_template, skip_jobs: true) + it "raises an error when user cant see the step due to conditions" do + sign_in(user2) - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Condition will not pass" + new_wizard_template = wizard_template.dup + new_wizard_template['steps'][0]['condition'] = user_condition_template['condition'] + CustomWizard::Template.save(new_wizard_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json' + expect(response.status).to eq(403) + end + + it "returns an updated wizard when condition doesnt pass" do + new_template = wizard_template.dup + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition wont pass" + } } - } - expect(response.status).to eq(200) - expect(response.parsed_body['final']).to eq(true) - end + expect(response.status).to eq(200) + expect(response.parsed_body['wizard']['start']).to eq("step_3") + end - it "excludes the non-included conditional fields from the submissions" do - new_template = wizard_template.dup - new_template['steps'][1]['fields'][0]['condition'] = wizard_field_condition_template['condition'] - CustomWizard::Template.save(new_template, skip_jobs: true) + it "returns the correct final step when the conditional final step and last step are the same" do + new_template = wizard_template.dup + new_template['steps'][0]['condition'] = user_condition_template['condition'] + new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Condition will pass" + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will pass" + } } - } + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(false) - put '/w/super-mega-fun-wizard/steps/step_2.json', params: { - fields: { - step_2_field_1: "1995-04-23" + put '/w/super-mega-fun-wizard/steps/step_2.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(false) + + put '/w/super-mega-fun-wizard/steps/step_3.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(true) + end + + it "returns the correct final step when the conditional final step and last step are different" do + new_template = wizard_template.dup + new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will not pass" + } } - } + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(false) - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Condition will not pass" + put '/w/super-mega-fun-wizard/steps/step_2.json' + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(true) + end + + it "returns the correct final step when the conditional final step is determined in the same action" do + new_template = wizard_template.dup + new_template['steps'][1]['condition'] = wizard_field_condition_template['condition'] + new_template['steps'][2]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will not pass" + } } - } + expect(response.status).to eq(200) + expect(response.parsed_body['final']).to eq(true) + end - wizard_id = response.parsed_body['wizard']['id'] - wizard = CustomWizard::Wizard.create(wizard_id, user) - submission = wizard.current_submission - expect(submission.fields.keys).not_to include("step_2_field_1") + it "excludes the non-included conditional fields from the submissions" do + new_template = wizard_template.dup + new_template['steps'][1]['fields'][0]['condition'] = wizard_field_condition_template['condition'] + CustomWizard::Template.save(new_template, skip_jobs: true) + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will pass" + } + } + + put '/w/super-mega-fun-wizard/steps/step_2.json', params: { + fields: { + step_2_field_1: "1995-04-23" + } + } + + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Condition will not pass" + } + } + + wizard_id = response.parsed_body['wizard']['id'] + wizard = CustomWizard::Wizard.create(wizard_id, user) + submission = wizard.current_submission + expect(submission.fields.keys).not_to include("step_2_field_1") + end end end end diff --git a/spec/requests/custom_wizard/wizard_controller_spec.rb b/spec/requests/custom_wizard/wizard_controller_spec.rb index aa1f479b..93ec196b 100644 --- a/spec/requests/custom_wizard/wizard_controller_spec.rb +++ b/spec/requests/custom_wizard/wizard_controller_spec.rb @@ -8,7 +8,6 @@ describe CustomWizard::WizardController do before do CustomWizard::Template.save(wizard_template, skip_jobs: true) @template = CustomWizard::Template.find("super_mega_fun_wizard") - sign_in(user) end context 'plugin disabled' do @@ -32,65 +31,70 @@ describe CustomWizard::WizardController do expect(response.parsed_body["error"]).to eq("We couldn't find a wizard at that address.") end - context 'when user skips the wizard' do - - it 'skips a wizard if user is allowed to skip' do - put '/w/super-mega-fun-wizard/skip.json' - expect(response.status).to eq(200) + context "with user" do + before do + sign_in(user) end - it 'lets user skip if user cant access wizard' do - enable_subscription("standard") - @template["permitted"] = permitted_json["permitted"] - CustomWizard::Template.save(@template, skip_jobs: true) - put '/w/super-mega-fun-wizard/skip.json' - expect(response.status).to eq(200) - end + context 'when user skips' do + it 'skips a wizard if user is allowed to skip' do + put '/w/super-mega-fun-wizard/skip.json' + expect(response.status).to eq(200) + end - it 'returns a no skip message if user is not allowed to skip' do - enable_subscription("standard") - @template['required'] = 'true' - CustomWizard::Template.save(@template) - put '/w/super-mega-fun-wizard/skip.json' - expect(response.parsed_body['error']).to eq("Wizard can't be skipped") - end + it 'lets user skip if user cant access wizard' do + enable_subscription("standard") + @template["permitted"] = permitted_json["permitted"] + CustomWizard::Template.save(@template, skip_jobs: true) + put '/w/super-mega-fun-wizard/skip.json' + expect(response.status).to eq(200) + end - it 'skip response contains a redirect_to if in users submissions' do - @wizard = CustomWizard::Wizard.create(@template["id"], user) - CustomWizard::Submission.new(@wizard, redirect_to: "/t/2").save - put '/w/super-mega-fun-wizard/skip.json' - expect(response.parsed_body['redirect_to']).to eq('/t/2') - end + it 'returns a no skip message if user is not allowed to skip' do + enable_subscription("standard") + @template['required'] = 'true' + CustomWizard::Template.save(@template) + put '/w/super-mega-fun-wizard/skip.json' + expect(response.parsed_body['error']).to eq("Wizard can't be skipped") + end - it 'deletes the users redirect_to_wizard if present' do - user.custom_fields['redirect_to_wizard'] = @template["id"] - user.save_custom_fields(true) - @wizard = CustomWizard::Wizard.create(@template["id"], user) - put '/w/super-mega-fun-wizard/skip.json' - expect(response.status).to eq(200) - expect(user.reload.redirect_to_wizard).to eq(nil) - end + it 'skip response contains a redirect_to if in users submissions' do + @wizard = CustomWizard::Wizard.create(@template["id"], user) + CustomWizard::Submission.new(@wizard, redirect_to: "/t/2").save + put '/w/super-mega-fun-wizard/skip.json' + expect(response.parsed_body['redirect_to']).to eq('/t/2') + end - it "deletes the submission if user has filled up some data" do - @wizard = CustomWizard::Wizard.create(@template["id"], user) - CustomWizard::Submission.new(@wizard, step_1_field_1: "Hello World").save - current_submission = @wizard.current_submission - put '/w/super-mega-fun-wizard/skip.json' - submissions = CustomWizard::Submission.list(@wizard).submissions + it 'deletes the users redirect_to_wizard if present' do + user.custom_fields['redirect_to_wizard'] = @template["id"] + user.save_custom_fields(true) + @wizard = CustomWizard::Wizard.create(@template["id"], user) + put '/w/super-mega-fun-wizard/skip.json' + expect(response.status).to eq(200) + expect(user.reload.redirect_to_wizard).to eq(nil) + end - expect(submissions.any? { |submission| submission.id == current_submission.id }).to eq(false) - end + it "deletes the submission if user has filled up some data" do + @wizard = CustomWizard::Wizard.create(@template["id"], user) + CustomWizard::Submission.new(@wizard, step_1_field_1: "Hello World").save + current_submission = @wizard.current_submission + put '/w/super-mega-fun-wizard/skip.json' + submissions = CustomWizard::Submission.list(@wizard).submissions - it "starts from the first step if user visits after skipping the wizard" do - put '/w/super-mega-fun-wizard/steps/step_1.json', params: { - fields: { - step_1_field_1: "Text input" + expect(submissions.any? { |submission| submission.id == current_submission.id }).to eq(false) + end + + it "starts from the first step if user visits after skipping the wizard" do + put '/w/super-mega-fun-wizard/steps/step_1.json', params: { + fields: { + step_1_field_1: "Text input" + } } - } - put '/w/super-mega-fun-wizard/skip.json' - get '/w/super-mega-fun-wizard.json' + put '/w/super-mega-fun-wizard/skip.json' + get '/w/super-mega-fun-wizard.json' - expect(response.parsed_body["start"]).to eq('step_1') + expect(response.parsed_body["start"]).to eq('step_1') + end end end end diff --git a/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb b/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb index 1ac2579e..0568f898 100644 --- a/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb +++ b/spec/serializers/custom_wizard/wizard_field_serializer_spec.rb @@ -29,6 +29,5 @@ describe CustomWizard::FieldSerializer do scope: Guardian.new(user) ).as_json expect(json_array[0][:format]).to eq("YYYY-MM-DD") - expect(json_array[5][:file_types]).to eq(".jpg,.jpeg,.png") end end diff --git a/test/javascripts/acceptance/field-test.js b/test/javascripts/acceptance/field-test.js index 5eecce4f..f1d97130 100644 --- a/test/javascripts/acceptance/field-test.js +++ b/test/javascripts/acceptance/field-test.js @@ -54,6 +54,70 @@ acceptance("Field | Fields", function (needs) { "Input in composer" ); }); + test("Composer - Hyperlink", async function (assert) { + await visit("/w/wizard"); + assert.ok( + visible(".wizard-field.composer-field .wizard-field-composer textarea") + ); + assert.ok( + exists(".wizard-field.composer-field .d-editor-button-bar button") + ); + assert.ok(visible(".wizard-btn.toggle-preview")); + await fillIn( + ".wizard-field.composer-field .wizard-field-composer textarea", + "This is a link to " + ); + assert.ok( + !exists(".insert-link.modal-body"), + "no hyperlink modal by default" + ); + await click( + ".wizard-field.composer-field .wizard-field-composer .d-editor button.link" + ); + assert.ok(exists(".insert-link.modal-body"), "hyperlink modal visible"); + + await fillIn(".modal-body .link-url", "google.com"); + await fillIn(".modal-body .link-text", "Google"); + await click(".modal-footer button.btn-primary"); + + assert.strictEqual( + query(".wizard-field.composer-field .wizard-field-composer textarea") + .value, + "This is a link to [Google](https://google.com)", + "adds link with url and text, prepends 'https://'" + ); + + assert.ok( + !exists( + ".wizard-field.composer-field .wizard-field-composer .insert-link.modal-body" + ), + "modal dismissed after submitting link" + ); + + await fillIn( + ".wizard-field.composer-field .wizard-field-composer textarea", + "Reset textarea contents." + ); + + await click( + ".wizard-field.composer-field .wizard-field-composer .d-editor button.link" + ); + await fillIn(".modal-body .link-url", "google.com"); + await fillIn(".modal-body .link-text", "Google"); + await click(".modal-footer button.btn-danger"); + + assert.strictEqual( + query(".wizard-field.composer-field .wizard-field-composer textarea") + .value, + "Reset textarea contents.", + "does not insert anything after cancelling" + ); + + assert.ok( + !exists(".insert-link.modal-body"), + "modal dismissed after cancelling" + ); + }); test("Text Only", async function (assert) { await visit("/w/wizard"); diff --git a/test/javascripts/acceptance/wizard-test.js b/test/javascripts/acceptance/wizard-test.js index 063c80fb..e2e6ce04 100644 --- a/test/javascripts/acceptance/wizard-test.js +++ b/test/javascripts/acceptance/wizard-test.js @@ -9,6 +9,7 @@ import { import { wizard, wizardCompleted, + wizardGuest, wizardNoUser, wizardNotPermitted, } from "../helpers/wizard"; @@ -106,3 +107,59 @@ acceptance("Wizard | Wizard", function (needs) { assert.strictEqual($("body.custom-wizard").length, 0); }); }); + +acceptance("Wizard | Guest access", function (needs) { + needs.pretender((server, helper) => { + server.get("/w/wizard.json", () => helper.response(wizardGuest)); + }); + + test("Does not require login", async function (assert) { + await visit("/w/wizard"); + assert.ok(!exists(".wizard-no-access.requires-login")); + }); + + test("Starts", async function (assert) { + await visit("/w/wizard"); + assert.ok(query(".wizard-column"), true); + }); + + test("Applies the wizard body class", async function (assert) { + await visit("/w/wizard"); + assert.ok($("body.custom-wizard").length); + }); + + test("Applies the body background color", async function (assert) { + await visit("/w/wizard"); + assert.ok($("body")[0].style.background); + }); + + test("Renders the wizard form", async function (assert) { + await visit("/w/wizard"); + assert.ok(exists(".wizard-column-contents .wizard-step"), true); + assert.ok(exists(".wizard-footer img"), true); + }); + + test("Renders the first step", async function (assert) { + await visit("/w/wizard"); + assert.strictEqual( + query(".wizard-step-title p").textContent.trim(), + "Text" + ); + assert.strictEqual( + query(".wizard-step-description p").textContent.trim(), + "Text inputs!" + ); + assert.strictEqual( + query(".wizard-step-description p").textContent.trim(), + "Text inputs!" + ); + assert.strictEqual(count(".wizard-step-form .wizard-field"), 6); + assert.ok(exists(".wizard-step-footer .wizard-progress"), true); + assert.ok(exists(".wizard-step-footer .wizard-buttons"), true); + }); + + test("Removes the wizard body class when navigating away", async function (assert) { + await visit("/"); + assert.strictEqual($("body.custom-wizard").length, 0); + }); +}); diff --git a/test/javascripts/fixtures/wizard.js.es6 b/test/javascripts/fixtures/wizard.js.es6 index 73fe45c1..a3b83063 100644 --- a/test/javascripts/fixtures/wizard.js.es6 +++ b/test/javascripts/fixtures/wizard.js.es6 @@ -6,7 +6,7 @@ export default { submission_last_updated_at: "2022-03-15T21:11:01+01:00", theme_id: 2, required: false, - permitted: true, + permitted: false, uncategorized_category_id: 1, categories: [], subscribed: false, diff --git a/test/javascripts/helpers/wizard.js b/test/javascripts/helpers/wizard.js index 700cedc7..e02e2e99 100644 --- a/test/javascripts/helpers/wizard.js +++ b/test/javascripts/helpers/wizard.js @@ -6,8 +6,11 @@ import updateJson from "../fixtures/update"; import { cloneJSON } from "discourse-common/lib/object"; const wizardNoUser = cloneJSON(wizardJson); +const wizardGuest = cloneJSON(wizardJson); +wizardGuest.permitted = true; const wizard = cloneJSON(wizardJson); wizard.user = cloneJSON(userJson); +wizard.permitted = true; const wizardNotPermitted = cloneJSON(wizard); wizardNotPermitted.permitted = false; @@ -40,6 +43,7 @@ export { wizardNoUser, wizardNotPermitted, wizardCompleted, + wizardGuest, stepNotPermitted, allFieldsWizard, wizard,