0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-11-09 20:02:54 +01:00
Dieser Commit ist enthalten in:
jumagura 2023-03-15 21:43:03 -04:00
Commit 9b8a3589bd
64 geänderte Dateien mit 1123 neuen und 746 gelöschten Zeilen

11
.github/workflows/discourse-plugin.yml gevendort Normale Datei
Datei anzeigen

@ -0,0 +1,11 @@
name: Discourse Plugin
on:
push:
branches:
- main
pull_request:
jobs:
ci:
uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -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() {

Datei anzeigen

@ -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();
},
},
});

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -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"];

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -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) => {
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);
});
},

Datei anzeigen

@ -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");

Datei anzeigen

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

Datei anzeigen

@ -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");
}
},

Datei anzeigen

@ -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}}
<div class="admin-wizard-buttons">

Datei anzeigen

@ -1,21 +0,0 @@
<div class="wizard-composer-hyperlink-contents">
<h3>{{i18n "composer.link_dialog_title"}}</h3>
{{input
class="composer-link-name"
placeholder=(i18n "composer.link_optional_text")
type="text"
value=linkName}}
{{input
class="composer-link-url"
placeholder=(i18n "composer.link_url_placeholder")
type="text"
value=linkUrl}}
{{d-button
label="wizard_composer.modal_ok"
class="add-link btn-primary"
click=(action "addLink")}}
{{d-button
label="wizard_composer.modal_cancel"
class="hide-hyperlink-box btn-danger"
click=(action "hideBox")}}
</div>

Datei anzeigen

@ -17,6 +17,7 @@
feature="action"
attribute="type"
onChange=(action "changeType")
wizard=wizard
options=(hash
none="admin.wizard.select_type"
)

Datei anzeigen

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

Datei anzeigen

@ -217,6 +217,8 @@ en:
list: "list"
custom_field: "custom field"
value: "value"
users: "users"
guests: "users and guests"
placeholder:
text: "Enter text"

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -29,6 +29,11 @@ class CustomWizard::Field
attr_accessor :index,
:step
REQUIRES_USER = %w[
composer
upload
]
def initialize(attrs)
@raw = attrs || {}
@id = attrs[:id]

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -23,7 +23,6 @@ class CustomWizard::Template
normalize_data
validate_data
prepare_data
return false if errors.any?
ActiveRecord::Base.transaction do

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -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)
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],
return nil unless actor_id && unfinished?
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
else
nil
end
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,7 +226,11 @@ class CustomWizard::Wizard
return true if mapper.blank?
mapper.all? do |m|
if !user
m[:type] === 'assignment' && [*m[:result]].include?(GUEST_GROUP_ID)
else
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'
@ -225,17 +240,18 @@ class CustomWizard::Wizard
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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

@ -80,15 +80,12 @@ 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(
CustomWizard::UserHistory.create!(
action: CustomWizard::UserHistory.actions[:step],
actor_id: user.id,
context: @template[:id],
subject: step[:id]
)
)
end
expect(

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

Datei anzeigen

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

12
spec/fixtures/actions/route_to.json gevendort Normale Datei
Datei anzeigen

@ -0,0 +1,12 @@
{
"id": "route_to",
"type": "route_to",
"url": [
{
"type": "assignment",
"output": "https://google.com",
"output_type": "text",
"output_connector": "set"
}
]
}

6
spec/fixtures/field/upload.json gevendort Normale Datei
Datei anzeigen

@ -0,0 +1,6 @@
{
"id": "step_2_field_7",
"label": "Upload",
"type": "upload",
"file_types": ".jpg,.jpeg,.png"
}

Datei anzeigen

@ -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 :)"

12
spec/fixtures/wizard/guests_permitted.json gevendort Normale Datei
Datei anzeigen

@ -0,0 +1,12 @@
{
"permitted": [
{
"type": "assignment",
"output_type": "group",
"output_connector": "set",
"output": [
-1
]
}
]
}

Datei anzeigen

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

Datei anzeigen

@ -7,9 +7,111 @@ 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)
end
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(403)
end
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
context "with user" do
before do
sign_in(user)
end
@ -267,3 +369,4 @@ describe CustomWizard::StepsController do
end
end
end
end

Datei anzeigen

@ -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,8 +31,12 @@ 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
context "with user" do
before do
sign_in(user)
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)
@ -94,3 +97,4 @@ describe CustomWizard::WizardController do
end
end
end
end

Datei anzeigen

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

Datei anzeigen

@ -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");

Datei anzeigen

@ -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);
});
});

Datei anzeigen

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

Datei anzeigen

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