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

Merge pull request #27 from paviliondev/add_tests

Add tests
Dieser Commit ist enthalten in:
Angus McLeod 2019-12-11 15:33:56 +11:00 committet von GitHub
Commit c182435805
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
47 geänderte Dateien mit 1066 neuen und 510 gelöschten Zeilen

1
.gitignore gevendort Normale Datei
Datei anzeigen

@ -0,0 +1 @@
coverage

Datei anzeigen

@ -1,27 +1,12 @@
# Uncomment tests runner when tests are added.
# We want to use the KVM-based system, so require sudo
sudo: required
#names:
#- docker
services:
- docker
before_install:
- plugin_name=${PWD##*/} && echo $plugin_name
- chmod -R 777 .
- git clone --depth=1 https://github.com/discourse/discourse-plugin-ci
#script:
#- >
#docker run
#-e "COMMIT_HASH=origin/tests-passed"
#-e "SKIP_LINT=1"
#-e "RUBY_ONLY=1"
#-e SINGLE_PLUGIN=$plugin_name
#-v $(pwd):/var/www/discourse/plugins/$plugin_name
#discourse/discourse_test:release
install: true # Prevent travis doing bundle install
after_success:
- pip install virtualenv
- virtualenv ~/env
- source ~/env/bin/activate
- pip install transifex-client
- sudo echo $'[https://www.transifex.com]\nhostname = https://www.transifex.com\nusername = '"$TRANSIFEX_USER"$'\npassword = '"$TRANSIFEX_PASSWORD"$'\ntoken = '"$TRANSIFEX_API_TOKEN"$'\n' > ~/.transifexrc
- tx push -s
script:
- discourse-plugin-ci/script.sh

Datei anzeigen

@ -3,7 +3,11 @@ import DiscourseURL from 'discourse/lib/url';
export default {
name: 'custom-wizard-edits',
initialize() {
initialize(container) {
const siteSettings = container.lookup('site-settings:main');
if (!siteSettings.custom_wizard_enabled) return;
withPluginApi('0.8.12', api => {
api.modifyClass('component:global-notice', {
buildBuffer(buffer) {

Datei anzeigen

@ -6,8 +6,9 @@ export default {
initialize: function (container) {
const messageBus = container.lookup('message-bus:main');
const siteSettings = container.lookup('site-settings:main');
if (!messageBus) { return; }
if (!siteSettings.custom_wizard_enabled || !messageBus) return;
messageBus.subscribe("/redirect_to_wizard", function (wizardId) {
const wizardUrl = window.location.origin + '/w/' + wizardId;

Datei anzeigen

@ -8,6 +8,7 @@ en:
custom_title: "Wizard"
field:
too_short: "%{label} must be at least %{min} characters"
required: "%{label} is required."
none: "We couldn't find a wizard at that address."
no_skip: "Wizard can't be skipped"
export:
@ -21,5 +22,6 @@ en:
no_valid_wizards: "File doesn't contain any valid wizards"
site_settings:
custom_wizard_enabled: "Enable custom wizards."
wizard_redirect_exclude_paths: "Routes excluded from wizard redirects."
wizard_recognised_image_upload_formats: "File types which will result in upload displaying an image preview"

36
config/routes.rb Normale Datei
Datei anzeigen

@ -0,0 +1,36 @@
CustomWizard::Engine.routes.draw do
get ':wizard_id' => 'wizard#index'
put ':wizard_id/skip' => 'wizard#skip'
get ':wizard_id/steps' => 'wizard#index'
get ':wizard_id/steps/:step_id' => 'wizard#index'
put ':wizard_id/steps/:step_id' => 'steps#update'
end
Discourse::Application.routes.append do
mount ::CustomWizard::Engine, at: 'w'
post 'wizard/authorization/callback' => "custom_wizard/authorization#callback"
scope module: 'custom_wizard', constraints: AdminConstraint.new do
get 'admin/wizards' => 'admin#index'
get 'admin/wizards/field-types' => 'admin#field_types'
get 'admin/wizards/custom' => 'admin#index'
get 'admin/wizards/custom/new' => 'admin#index'
get 'admin/wizards/custom/all' => 'admin#custom_wizards'
get 'admin/wizards/custom/:wizard_id' => 'admin#find_wizard'
put 'admin/wizards/custom/save' => 'admin#save'
delete 'admin/wizards/custom/remove' => 'admin#remove'
get 'admin/wizards/submissions' => 'admin#index'
get 'admin/wizards/submissions/:wizard_id' => 'admin#submissions'
get 'admin/wizards/apis' => 'api#list'
get 'admin/wizards/apis/new' => 'api#index'
get 'admin/wizards/apis/:name' => 'api#find'
put 'admin/wizards/apis/:name' => 'api#save'
delete 'admin/wizards/apis/:name' => 'api#remove'
delete 'admin/wizards/apis/logs/:name' => 'api#clearlogs'
get 'admin/wizards/apis/:name/redirect' => 'api#redirect'
get 'admin/wizards/apis/:name/authorize' => 'api#authorize'
get 'admin/wizards/transfer' => 'transfer#index'
get 'admin/wizards/transfer/export' => 'transfer#export'
post 'admin/wizards/transfer/import' => 'transfer#import'
end
end

Datei anzeigen

@ -1,4 +1,5 @@
plugins:
custom_wizard_enabled: true
wizard_redirect_exclude_paths:
client: true
type: list

Datei anzeigen

@ -0,0 +1,27 @@
module ApplicationControllerCWExtension
extend ActiveSupport::Concern
included do
before_action :redirect_to_wizard_if_required, if: :current_user
end
def redirect_to_wizard_if_required
wizard_id = current_user.custom_fields['redirect_to_wizard']
@excluded_routes ||= SiteSetting.wizard_redirect_exclude_paths.split('|') + ['/w/']
url = request.referer || request.original_url
if request.format === 'text/html' && !@excluded_routes.any? {|str| /#{str}/ =~ url} && wizard_id
if request.referer !~ /\/w\// && request.referer !~ /\/invites\//
CustomWizard::Wizard.set_submission_redirect(current_user, wizard_id, request.referer)
end
if CustomWizard::Wizard.exists?(wizard_id)
redirect_to "/w/#{wizard_id.dasherize}"
end
end
end
end
class ApplicationController
prepend ApplicationControllerCWExtension if SiteSetting.custom_wizard_enabled
end

Datei anzeigen

@ -0,0 +1,20 @@
module CustomWizardExtraLocalesController
def show
if request.referer && URI(request.referer).path.include?('/w/')
bundle = params[:bundle]
if params[:v]&.size == 32
hash = ExtraLocalesController.bundle_js_hash(bundle)
immutable_for(1.year) if hash == params[:v]
end
render plain: ExtraLocalesController.bundle_js(bundle), content_type: "application/javascript"
else
super
end
end
end
class ExtraLocalesController
prepend CustomWizardExtraLocalesController if SiteSetting.custom_wizard_enabled
end

Datei anzeigen

@ -0,0 +1,22 @@
module InvitesControllerCustomWizard
def path(url)
if Wizard.user_requires_completion?(@user)
wizard_id = @user.custom_fields['custom_wizard_redirect']
if wizard_id && url != '/'
CustomWizard::Wizard.set_submission_redirect(@user, wizard_id, url)
url = "/w/#{wizard_id.dasherize}"
end
end
super(url)
end
private def post_process_invite(user)
super(user)
@user = user
end
end
class InvitesController
prepend InvitesControllerCustomWizard if SiteSetting.custom_wizard_enabled
end

Datei anzeigen

@ -77,7 +77,14 @@ class CustomWizard::Builder
end
def build(build_opts = {}, params = {})
unless (@wizard.completed? && !@wizard.multiple_submissions && !@wizard.user.admin) || !@steps || !@wizard.permitted?
return @wizard if !SiteSetting.custom_wizard_enabled ||
(!@wizard.multiple_submissions &&
@wizard.completed? &&
!@wizard.user.admin) ||
!@steps ||
!@wizard.permitted?
reset_submissions if build_opts[:reset]
@steps.each do |step_template|
@ -91,9 +98,10 @@ class CustomWizard::Builder
if permitted_params = step_template['permitted_params']
permitted_data = {}
permitted_params.each do |param|
key = param['key'].to_sym
permitted_data[key] = params[key] if params[key]
permitted_params.each do |p|
params_key = p['key'].to_sym
submission_key = p['value'].to_sym
permitted_data[submission_key] = params[params_key] if params[params_key]
end
if permitted_data.present?
@ -146,7 +154,7 @@ class CustomWizard::Builder
next if updater.errors.any?
data = updater.fields.to_h
data = updater.fields
## if the wizard has data from the previous steps make that accessible to the actions.
if @submissions && @submissions.last && !@submissions.last.key?("submitted_at")
@ -187,7 +195,6 @@ class CustomWizard::Builder
end
end
end
end
@wizard
end
@ -217,7 +224,7 @@ class CustomWizard::Builder
if profile_actions.any?
profile_actions.each do |action|
if update = action['profile_updates'].select { |u| u['key'] === field_template['id'] }.first
params[:value] = prefill_profile_field(update)
params[:value] = prefill_profile_field(update) || params[:value]
end
end
end
@ -313,13 +320,17 @@ class CustomWizard::Builder
def validate_field(field, updater, step_template)
value = updater.fields[field['id']]
min_length = false
label = field['label'] || I18n.t("#{field['key']}.label")
if field['required'] && !value
updater.errors.add(field['id'].to_s, I18n.t('wizard.field.required', label: label))
end
if is_text_type(field)
min_length = field['min_length']
end
if min_length && value.is_a?(String) && value.strip.length < min_length.to_i
label = field['label'] || I18n.t("#{field['key']}.label")
updater.errors.add(field['id'].to_s, I18n.t('wizard.field.too_short', label: label, min: min_length.to_i))
end

6
lib/custom_wizard/engine.rb Normale Datei
Datei anzeigen

@ -0,0 +1,6 @@
module ::CustomWizard
class Engine < ::Rails::Engine
engine_name 'custom_wizard'
isolate_namespace CustomWizard
end
end

Datei anzeigen

@ -8,11 +8,13 @@ class CustomWizard::StepUpdater
@wizard = wizard
@step = step
@refresh_required = false
@fields = fields
@fields = fields.to_h.with_indifferent_access
@result = {}
end
def update
return false if !SiteSetting.custom_wizard_enabled
@step.updater.call(self) if @step.present? && @step.updater.present?
if success?

Datei anzeigen

@ -3,6 +3,8 @@ require_dependency 'wizard/field'
require_dependency 'wizard/step_updater'
require_dependency 'wizard/builder'
UserHistory.actions[:custom_wizard_step] = 1000
class CustomWizard::Wizard
attr_reader :steps, :user
@ -190,8 +192,8 @@ class CustomWizard::Wizard
end
end
def self.add_wizard(json)
wizard = ::JSON.parse(json)
def self.add_wizard(obj)
wizard = obj.is_a?(String) ? ::JSON.parse(json) : obj
PluginStore.set('custom_wizard', wizard["id"], wizard)
end

17
lib/wizard/choice.rb Normale Datei
Datei anzeigen

@ -0,0 +1,17 @@
module CustomWizardChoiceExtension
def initialize(id, opts)
@id = id
@opts = opts
@data = opts[:data]
@extra_label = opts[:extra_label]
@icon = opts[:icon]
end
def label
@label ||= PrettyText.cook(@opts[:label])
end
end
class Wizard::Choice
prepend CustomWizardChoiceExtension if SiteSetting.custom_wizard_enabled
end

37
lib/wizard/field.rb Normale Datei
Datei anzeigen

@ -0,0 +1,37 @@
module CustomWizardFieldExtension
attr_reader :label,
:description,
:image,
:key,
:min_length,
:file_types,
:limit,
:property
attr_accessor :dropdown_none
def initialize(attrs)
@attrs = attrs || {}
@id = attrs[:id]
@type = attrs[:type]
@required = !!attrs[:required]
@description = attrs[:description]
@image = attrs[:image]
@key = attrs[:key]
@min_length = attrs[:min_length]
@value = attrs[:value]
@choices = []
@dropdown_none = attrs[:dropdown_none]
@file_types = attrs[:file_types]
@limit = attrs[:limit]
@property = attrs[:property]
end
def label
@label ||= PrettyText.cook(@attrs[:label])
end
end
class Wizard::Field
prepend CustomWizardFieldExtension if SiteSetting.custom_wizard_enabled
end

7
lib/wizard/step.rb Normale Datei
Datei anzeigen

@ -0,0 +1,7 @@
module CustomWizardStepExtension
attr_accessor :title, :description, :key, :permitted, :permitted_message
end
class Wizard::Step
prepend CustomWizardStepExtension if SiteSetting.custom_wizard_enabled
end

Datei anzeigen

@ -1,227 +0,0 @@
require_dependency 'wizard'
require_dependency 'wizard/field'
require_dependency 'wizard/step'
::Wizard.class_eval do
def self.user_requires_completion?(user)
wizard_result = self.new(user).requires_completion?
return wizard_result if wizard_result
custom_redirect = false
if user && user.first_seen_at.blank? && wizard_id = CustomWizard::Wizard.after_signup
wizard = CustomWizard::Wizard.create(user, wizard_id)
if !wizard.completed? && wizard.permitted?
custom_redirect = true
CustomWizard::Wizard.set_wizard_redirect(user, wizard_id)
end
end
!!custom_redirect
end
end
::Wizard::Field.class_eval do
attr_reader :label, :description, :image, :key, :min_length, :file_types, :limit, :property
attr_accessor :dropdown_none
def initialize(attrs)
@attrs = attrs || {}
@id = attrs[:id]
@type = attrs[:type]
@required = !!attrs[:required]
@description = attrs[:description]
@image = attrs[:image]
@key = attrs[:key]
@min_length = attrs[:min_length]
@value = attrs[:value]
@choices = []
@dropdown_none = attrs[:dropdown_none]
@file_types = attrs[:file_types]
@limit = attrs[:limit]
@property = attrs[:property]
end
def label
@label ||= PrettyText.cook(@attrs[:label])
end
end
::Wizard::Choice.class_eval do
def initialize(id, opts)
@id = id
@opts = opts
@data = opts[:data]
@extra_label = opts[:extra_label]
@icon = opts[:icon]
end
def label
@label ||= PrettyText.cook(@opts[:label])
end
end
class ::Wizard::Step
attr_accessor :title, :description, :key, :permitted, :permitted_message
end
::WizardSerializer.class_eval do
attributes :id,
:name,
:background,
:completed,
:required,
:min_trust,
:permitted,
:user,
:categories,
:uncategorized_category_id
def id
object.id
end
def include_id?
object.respond_to?(:id)
end
def name
object.name
end
def include_name?
object.respond_to?(:name)
end
def background
object.background
end
def include_background?
object.respond_to?(:background)
end
def completed
object.completed?
end
def include_completed?
object.completed? &&
(!object.respond_to?(:multiple_submissions) || !object.multiple_submissions) &&
!scope.is_admin?
end
def min_trust
object.min_trust
end
def include_min_trust?
object.respond_to?(:min_trust)
end
def permitted
object.permitted?
end
def include_permitted?
object.respond_to?(:permitted?)
end
def include_start?
object.start && include_steps?
end
def include_steps?
!include_completed?
end
def required
object.required
end
def include_required?
object.respond_to?(:required)
end
def user
object.user
end
def categories
begin
site = ::Site.new(scope)
::ActiveModel::ArraySerializer.new(site.categories, each_serializer: BasicCategorySerializer)
rescue => e
puts "HERE IS THE ERROR: #{e.inspect}"
end
end
def uncategorized_category_id
SiteSetting.uncategorized_category_id
end
end
::WizardStepSerializer.class_eval do
attributes :permitted, :permitted_message
def title
return PrettyText.cook(object.title) if object.title
PrettyText.cook(I18n.t("#{object.key || i18n_key}.title", default: ''))
end
def description
return object.description if object.description
PrettyText.cook(I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url))
end
def permitted
object.permitted
end
def permitted_message
object.permitted_message
end
end
::WizardFieldSerializer.class_eval do
attributes :dropdown_none, :image, :file_types, :limit, :property
def label
return object.label if object.label.present?
I18n.t("#{object.key || i18n_key}.label", default: '')
end
def description
return object.description if object.description.present?
I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url)
end
def image
object.image
end
def include_image?
object.image.present?
end
def placeholder
I18n.t("#{object.key || i18n_key}.placeholder", default: '')
end
def dropdown_none
object.dropdown_none
end
def file_types
object.file_types
end
def limit
object.limit
end
def property
object.property
end
end

221
plugin.rb
Datei anzeigen

@ -8,10 +8,12 @@ register_asset 'stylesheets/wizard_custom_admin.scss'
register_asset 'lib/jquery.timepicker.min.js'
register_asset 'lib/jquery.timepicker.scss'
config = Rails.application.config
config.assets.paths << Rails.root.join('plugins', 'discourse-custom-wizard', 'assets', 'javascripts')
config.assets.paths << Rails.root.join('plugins', 'discourse-custom-wizard', 'assets', 'stylesheets', 'wizard')
enabled_site_setting :custom_wizard_enabled
config = Rails.application.config
plugin_asset_path = "#{Rails.root}/plugins/discourse-custom-wizard/assets"
config.assets.paths << "#{plugin_asset_path}/javascripts"
config.assets.paths << "#{plugin_asset_path}/stylesheets/wizard"
if Rails.env.production?
config.assets.precompile += %w{
@ -35,153 +37,77 @@ if respond_to?(:register_svg_icon)
end
after_initialize do
UserHistory.actions[:custom_wizard_step] = 1000
[
'../lib/custom_wizard/engine.rb',
'../config/routes.rb',
'../controllers/custom_wizard/wizard.rb',
'../controllers/custom_wizard/steps.rb',
'../controllers/custom_wizard/admin.rb',
'../controllers/custom_wizard/transfer.rb',
'../controllers/custom_wizard/api.rb',
'../controllers/application_controller.rb',
'../controllers/extra_locales_controller.rb',
'../controllers/invites_controller.rb',
'../jobs/clear_after_time_wizard.rb',
'../jobs/refresh_api_access_token.rb',
'../jobs/set_after_time_wizard.rb',
'../lib/custom_wizard/builder.rb',
'../lib/custom_wizard/field.rb',
'../lib/custom_wizard/step_updater.rb',
'../lib/custom_wizard/template.rb',
'../lib/custom_wizard/wizard.rb',
'../lib/custom_wizard/api/api.rb',
'../lib/custom_wizard/api/authorization.rb',
'../lib/custom_wizard/api/endpoint.rb',
'../lib/custom_wizard/api/log_entry.rb',
'../lib/wizard/choice.rb',
'../lib/wizard/field.rb',
'../lib/wizard/step.rb',
'../serializers/custom_wizard/api_serializer.rb',
'../serializers/custom_wizard/basic_api_serializer.rb',
'../serializers/custom_wizard/api/authorization_serializer.rb',
'../serializers/custom_wizard/api/basic_endpoint_serializer.rb',
'../serializers/custom_wizard/api/endpoint_serializer.rb',
'../serializers/custom_wizard/api/log_serializer.rb',
'../serializers/site_serializer.rb',
'../serializers/wizard_serializer.rb',
'../serializers/wizard_step_serializer.rb',
'../serializers/wizard_field_serializer.rb'
].each do |path|
load File.expand_path(path, __FILE__)
end
module ::CustomWizard
class Engine < ::Rails::Engine
engine_name 'custom_wizard'
isolate_namespace CustomWizard
add_class_method(:wizard, :user_requires_completion?) do |user|
wizard_result = self.new(user).requires_completion?
return wizard_result if wizard_result
custom_redirect = false
if user &&
user.first_seen_at.blank? &&
wizard_id = CustomWizard::Wizard.after_signup
wizard = CustomWizard::Wizard.create(user, wizard_id)
if !wizard.completed? && wizard.permitted?
custom_redirect = true
CustomWizard::Wizard.set_wizard_redirect(user, wizard_id)
end
end
CustomWizard::Engine.routes.draw do
get ':wizard_id' => 'wizard#index'
put ':wizard_id/skip' => 'wizard#skip'
get ':wizard_id/steps' => 'wizard#index'
get ':wizard_id/steps/:step_id' => 'wizard#index'
put ':wizard_id/steps/:step_id' => 'steps#update'
!!custom_redirect
end
Discourse::Application.routes.append do
mount ::CustomWizard::Engine, at: 'w'
post 'wizard/authorization/callback' => "custom_wizard/authorization#callback"
scope module: 'custom_wizard', constraints: AdminConstraint.new do
get 'admin/wizards' => 'admin#index'
get 'admin/wizards/field-types' => 'admin#field_types'
get 'admin/wizards/custom' => 'admin#index'
get 'admin/wizards/custom/new' => 'admin#index'
get 'admin/wizards/custom/all' => 'admin#custom_wizards'
get 'admin/wizards/custom/:wizard_id' => 'admin#find_wizard'
put 'admin/wizards/custom/save' => 'admin#save'
delete 'admin/wizards/custom/remove' => 'admin#remove'
get 'admin/wizards/submissions' => 'admin#index'
get 'admin/wizards/submissions/:wizard_id' => 'admin#submissions'
get 'admin/wizards/apis' => 'api#list'
get 'admin/wizards/apis/new' => 'api#index'
get 'admin/wizards/apis/:name' => 'api#find'
put 'admin/wizards/apis/:name' => 'api#save'
delete 'admin/wizards/apis/:name' => 'api#remove'
delete 'admin/wizards/apis/logs/:name' => 'api#clearlogs'
get 'admin/wizards/apis/:name/redirect' => 'api#redirect'
get 'admin/wizards/apis/:name/authorize' => 'api#authorize'
get 'admin/wizards/transfer' => 'transfer#index'
get 'admin/wizards/transfer/export' => 'transfer#export'
post 'admin/wizards/transfer/import' => 'transfer#import'
end
end
load File.expand_path('../jobs/clear_after_time_wizard.rb', __FILE__)
load File.expand_path('../jobs/set_after_time_wizard.rb', __FILE__)
load File.expand_path('../lib/builder.rb', __FILE__)
load File.expand_path('../lib/field.rb', __FILE__)
load File.expand_path('../lib/step_updater.rb', __FILE__)
load File.expand_path('../lib/template.rb', __FILE__)
load File.expand_path('../lib/wizard.rb', __FILE__)
load File.expand_path('../lib/wizard_edits.rb', __FILE__)
load File.expand_path('../controllers/wizard.rb', __FILE__)
load File.expand_path('../controllers/steps.rb', __FILE__)
load File.expand_path('../controllers/admin.rb', __FILE__)
#transfer code
load File.expand_path('../controllers/transfer.rb', __FILE__)
load File.expand_path('../jobs/refresh_api_access_token.rb', __FILE__)
load File.expand_path('../lib/api/api.rb', __FILE__)
load File.expand_path('../lib/api/authorization.rb', __FILE__)
load File.expand_path('../lib/api/endpoint.rb', __FILE__)
load File.expand_path('../lib/api/log_entry.rb', __FILE__)
load File.expand_path('../controllers/api.rb', __FILE__)
load File.expand_path('../serializers/api/api_serializer.rb', __FILE__)
load File.expand_path('../serializers/api/authorization_serializer.rb', __FILE__)
load File.expand_path('../serializers/api/basic_api_serializer.rb', __FILE__)
load File.expand_path('../serializers/api/endpoint_serializer.rb', __FILE__)
load File.expand_path('../serializers/api/basic_endpoint_serializer.rb', __FILE__)
load File.expand_path('../serializers/api/log_serializer.rb', __FILE__)
::UsersController.class_eval do
def wizard_path
add_to_class(:users_controller, :wizard_path) do
if custom_wizard_redirect = current_user.custom_fields['redirect_to_wizard']
"#{Discourse.base_url}/w/#{custom_wizard_redirect.dasherize}"
else
"#{Discourse.base_url}/wizard"
end
end
end
module InvitesControllerCustomWizard
def path(url)
if Wizard.user_requires_completion?(@user)
wizard_id = @user.custom_fields['custom_wizard_redirect']
if wizard_id && url != '/'
CustomWizard::Wizard.set_submission_redirect(@user, wizard_id, url)
url = "/w/#{wizard_id.dasherize}"
end
end
super(url)
end
private def post_process_invite(user)
super(user)
@user = user
end
end
require_dependency 'invites_controller'
class ::InvitesController
prepend InvitesControllerCustomWizard
end
require_dependency 'application_controller'
class ::ApplicationController
before_action :redirect_to_wizard_if_required, if: :current_user
def redirect_to_wizard_if_required
wizard_id = current_user.custom_fields['redirect_to_wizard']
@excluded_routes ||= SiteSetting.wizard_redirect_exclude_paths.split('|') + ['/w/']
url = request.referer || request.original_url
if request.format === 'text/html' && !@excluded_routes.any? {|str| /#{str}/ =~ url} && wizard_id
if request.referer !~ /\/w\// && request.referer !~ /\/invites\//
CustomWizard::Wizard.set_submission_redirect(current_user, wizard_id, request.referer)
end
if CustomWizard::Wizard.exists?(wizard_id)
redirect_to "/w/#{wizard_id.dasherize}"
end
end
end
end
add_to_serializer(:current_user, :redirect_to_wizard) {object.custom_fields['redirect_to_wizard']}
## TODO limit this to the first admin
SiteSerializer.class_eval do
attributes :complete_custom_wizard
def include_wizard_required?
scope.is_admin? && Wizard.new(scope.user).requires_completion?
end
def complete_custom_wizard
if scope.user && requires_completion = CustomWizard::Wizard.prompt_completion(scope.user)
requires_completion.map {|w| {name: w[:name], url: "/w/#{w[:id]}"}}
end
end
def include_complete_custom_wizard?
complete_custom_wizard.present?
end
add_to_serializer(:current_user, :redirect_to_wizard) do
object.custom_fields['redirect_to_wizard']
end
DiscourseEvent.on(:user_approved) do |user|
@ -190,26 +116,5 @@ after_initialize do
end
end
module CustomWizardExtraLocalesController
def show
if request.referer && URI(request.referer).path.include?('/w/')
bundle = params[:bundle]
if params[:v]&.size == 32
hash = ExtraLocalesController.bundle_js_hash(bundle)
immutable_for(1.year) if hash == params[:v]
end
render plain: ExtraLocalesController.bundle_js(bundle), content_type: "application/javascript"
else
super
end
end
end
class ::ExtraLocalesController
prepend CustomWizardExtraLocalesController
end
DiscourseEvent.trigger(:custom_wizard_ready)
end

Datei anzeigen

@ -0,0 +1,27 @@
## TODO limit this to the first admin
module SiteSerializerCWX
extend ActiveSupport::Concern
included do
attributes :complete_custom_wizard
end
def include_wizard_required?
scope.is_admin? && Wizard.new(scope.user).requires_completion?
end
def complete_custom_wizard
if scope.user && requires_completion = CustomWizard::Wizard.prompt_completion(scope.user)
requires_completion.map {|w| {name: w[:name], url: "/w/#{w[:id]}"}}
end
end
def include_complete_custom_wizard?
complete_custom_wizard.present?
end
end
class SiteSerializer
prepend SiteSerializerCWX if SiteSetting.custom_wizard_enabled
end

Datei anzeigen

@ -0,0 +1,49 @@
module CustomWizardWizardFieldSerializerExtension
extend ActiveSupport::Concern
included do
attributes :dropdown_none, :image, :file_types, :limit, :property
end
def label
return object.label if object.label.present?
I18n.t("#{object.key || i18n_key}.label", default: '')
end
def description
return object.description if object.description.present?
I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url)
end
def image
object.image
end
def include_image?
object.image.present?
end
def placeholder
I18n.t("#{object.key || i18n_key}.placeholder", default: '')
end
def dropdown_none
object.dropdown_none
end
def file_types
object.file_types
end
def limit
object.limit
end
def property
object.property
end
end
class WizardFieldSerializer
prepend CustomWizardWizardFieldSerializerExtension if SiteSetting.custom_wizard_enabled
end

Datei anzeigen

@ -0,0 +1,103 @@
module CustomWizardWizardSerializerExtension
extend ActiveSupport::Concern
included do
attributes :id,
:name,
:background,
:completed,
:required,
:min_trust,
:permitted,
:user,
:categories,
:uncategorized_category_id
end
def id
object.id
end
def include_id?
object.respond_to?(:id)
end
def name
object.name
end
def include_name?
object.respond_to?(:name)
end
def background
object.background
end
def include_background?
object.respond_to?(:background)
end
def completed
object.completed?
end
def include_completed?
object.completed? &&
(!object.respond_to?(:multiple_submissions) || !object.multiple_submissions) &&
!scope.is_admin?
end
def min_trust
object.min_trust
end
def include_min_trust?
object.respond_to?(:min_trust)
end
def permitted
object.permitted?
end
def include_permitted?
object.respond_to?(:permitted?)
end
def include_start?
object.start && include_steps?
end
def include_steps?
!include_completed?
end
def required
object.required
end
def include_required?
object.respond_to?(:required)
end
def user
object.user
end
def categories
begin
site = ::Site.new(scope)
::ActiveModel::ArraySerializer.new(site.categories, each_serializer: BasicCategorySerializer)
rescue => e
puts "HERE IS THE ERROR: #{e.inspect}"
end
end
def uncategorized_category_id
SiteSetting.uncategorized_category_id
end
end
class WizardSerializer
prepend CustomWizardWizardSerializerExtension if SiteSetting.custom_wizard_enabled
end

Datei anzeigen

@ -0,0 +1,29 @@
module CustomWizardWizardStepSerializerExtension
extend ActiveSupport::Concern
included do
attributes :permitted, :permitted_message
end
def title
return PrettyText.cook(object.title) if object.title
PrettyText.cook(I18n.t("#{object.key || i18n_key}.title", default: ''))
end
def description
return object.description if object.description
PrettyText.cook(I18n.t("#{object.key || i18n_key}.description", default: '', base_url: Discourse.base_url))
end
def permitted
object.permitted
end
def permitted_message
object.permitted_message
end
end
class WizardStepSerializer
prepend CustomWizardWizardStepSerializerExtension if SiteSetting.custom_wizard_enabled
end

Datei anzeigen

@ -0,0 +1,21 @@
# frozen_string_literal: true
require 'rails_helper'
describe CustomWizard::Api do
context 'authorization' do
it 'authorizes with an oauth2 api' do
end
it 'refreshes the api access token' do
end
end
context 'endpoint' do
it 'requests an api endpoint' do
end
end
end

Datei anzeigen

@ -0,0 +1,370 @@
# frozen_string_literal: true
require 'rails_helper'
describe CustomWizard::Builder do
fab!(:user) { Fabricate(:user, username: 'angus') }
fab!(:trusted_user) { Fabricate(:user, trust_level: 3) }
fab!(:group) { Fabricate(:group) }
let!(:template) do
JSON.parse(File.open(
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/wizard.json"
).read)
end
let(:permitted_params) {[{"key":"param_key","value":"submission_param_key"}]}
let(:required_data) {[{"key":"nickname","connector":"equals","value":"name"}]}
let(:required_data_message) {"Nickname is required to match your name"}
let(:checkbox_field) {{"id":"checkbox","type":"checkbox","label":"Checkbox"}}
let(:composer_field) {{"id": "composer","label":"Composer","type":"composer"}}
let(:dropdown_categories_field) {{"id": "dropdown_categories","type": "dropdown","label": "Dropdown Categories","choices_type": "preset","choices_preset": "categories"}}
let(:tag_field) {{"id": "tag","type": "tag","label": "Tag","limit": "2"}}
let(:category_field) {{"id": "category","type": "category","limit": "1","label": "Category"}}
let(:image_field) {{"id": "image","type": "image","label": "Image"}}
let(:text_field) {{"id": "text","type": "text","label": "Text"}}
let(:textarea_field) {{"id": "textarea","type": "textarea","label": "Textarea"}}
let(:text_only_field) {{"id": "text_only","type": "text-only","label": "Text only"}}
let(:upload_field) {{"id": "upload","type": "upload","file_types": ".jpg,.png,.pdf","label": "Upload"}}
let(:user_selector_field) {{"id": "user_selector","type": "user-selector","label": "User selector"}}
let(:dropdown_groups_field) {{"id": "dropdown_groups","type": "dropdown","choices_type": "preset","choices_preset": "groups","label": "Dropdown Groups"}}
let(:dropdown_tags_field) {{"id": "dropdown_tags","type": "dropdown","choices_type": "preset","choices_preset": "tags","label": "Dropdown Tags"}}
let(:dropdown_custom_field) {{"id": "dropdown_custom","type": "dropdown","choices_type": "custom","choices": [{"key": "option_1","value": "Option 1"},{"key": "option_2","value": "Option 2"}]}}
let(:dropdown_translation_field) {{"id": "dropdown_translation","type": "dropdown","choices_type": "translation","choices_key": "key1.key2"}}
let(:dropdown_categories_filtered_field) {{"id": "dropdown_categories_filtered_field","type": "dropdown","choices_type": "preset","choices_preset": "categories","choices_filters": [{"key": "slug","value": "staff"}]}}
let(:create_topic_action) {{"id":"create_topic","type":"create_topic","title":"text","post":"textarea"}}
let(:send_message_action) {{"id":"send_message","type":"send_message","title":"text","post":"textarea","username":"angus"}}
let(:route_to_action) {{"id":"route_to","type":"route_to","url":"https://google.com"}}
let(:open_composer_action) {{"id":"open_composer","type":"open_composer","title":"text","post":"textarea"}}
let(:add_to_group_action) {{"id":"add_to_group","type":"add_to_group","group_id":"dropdown_groups"}}
def build_wizard(t = template, u = user, build_opts = {}, params = {})
CustomWizard::Wizard.add_wizard(t)
CustomWizard::Builder.new(u, 'welcome').build(build_opts, params)
end
def add_submission_data(data = {})
PluginStore.set("welcome_submissions", user.id, {
name: 'Angus',
website: 'https://thepavilion.io'
}.merge(data))
end
def get_submission_data
PluginStore.get("welcome_submissions", user.id)
end
def run_update(t = template, step_id = nil, data = {})
wizard = build_wizard(t)
updater = wizard.create_updater(step_id || t['steps'][0]['id'], data)
updater.update
updater
end
def send_message(extra_field = nil, extra_action_opts = {})
fields = [text_field, textarea_field]
if extra_field
fields.push(extra_field)
end
template['steps'][0]['fields'] = fields
template['steps'][0]["actions"] = [send_message_action.merge(extra_action_opts)]
run_update(template, nil,
text: "Message Title",
textarea: "message body"
)
end
context 'disabled' do
before do
SiteSetting.custom_wizard_enabled = false
end
it "returns no steps" do
wizard = build_wizard
expect(wizard.steps.length).to eq(0)
expect(wizard.name).to eq('Welcome')
end
it "doesn't save submissions" do
run_update(template, nil, name: 'Angus')
expect(get_submission_data.blank?).to eq(true)
end
end
context 'enabled' do
before do
SiteSetting.custom_wizard_enabled = true
end
it "returns steps" do
expect(build_wizard.steps.length).to eq(2)
end
it 'returns no steps if the multiple submissions are disabled and user has completed it' do
history_params = {
action: UserHistory.actions[:custom_wizard_step],
acting_user_id: user.id,
context: template['id']
}
UserHistory.create!(history_params.merge(subject: template['steps'][0]['id']))
UserHistory.create!(history_params.merge(subject: template['steps'][1]['id']))
template["multiple_submissions"] = false
expect(build_wizard(template).steps.length).to eq(0)
end
it 'returns no steps if has min trust and user does not meet it' do
template["min_trust"] = 3
expect(build_wizard(template).steps.length).to eq(0)
end
it 'returns steps if it has min trust and user meets it' do
template["min_trust"] = 3
expect(build_wizard(template, trusted_user).steps.length).to eq(2)
end
it 'returns a wizard with prefilled data if user has partially completed it' do
add_submission_data
wizard = build_wizard
expect(wizard.steps[0].fields.first.value).to eq('Angus')
expect(wizard.steps[1].fields.first.value).to eq('https://thepavilion.io')
end
it 'returns a wizard with no prefilled data if options include reset' do
add_submission_data
wizard = build_wizard(template, user, reset: true)
expect(wizard.steps[0].fields.first.value).to eq(nil)
expect(wizard.steps[1].fields.first.value).to eq(nil)
end
context 'building steps' do
it 'returns step metadata' do
expect(build_wizard.steps[0].title).to eq('Welcome to Pavilion')
expect(build_wizard.steps[1].title).to eq('Tell us about you')
end
it 'saves permitted params' do
template['steps'][0]['permitted_params'] = permitted_params
wizard = build_wizard(template, user, {}, param_key: 'param_value')
submissions = get_submission_data
expect(submissions.first['submission_param_key']).to eq('param_value')
end
it 'is not permitted if required data is not present' do
template['steps'][0]['required_data'] = required_data
expect(build_wizard(template, user).steps[0].permitted).to eq(false)
end
it "is not permitted if required data is not present" do
template['steps'][0]['required_data'] = required_data
add_submission_data(nickname: "John")
expect(build_wizard(template, user).steps[0].permitted).to eq(false)
end
it 'it shows required data message if required data has message' do
template['steps'][0]['required_data'] = required_data
template['steps'][0]['required_data_message'] = required_data_message
add_submission_data(nickname: "John")
wizard = build_wizard(template, user)
expect(wizard.steps[0].permitted).to eq(false)
expect(wizard.steps[0].permitted_message).to eq(required_data_message)
end
it 'is permitted if required data is present' do
template['steps'][0]['required_data'] = required_data
PluginStore.set('welcome_submissions', user.id, nickname: "Angus", name: "Angus")
expect(build_wizard(template, user).steps[0].permitted).to eq(true)
end
it 'returns field metadata' do
expect(build_wizard(template, user).steps[0].fields[0].label).to eq("<p>Name</p>")
expect(build_wizard(template, user).steps[0].fields[0].type).to eq("text")
end
it 'returns fields' do
template['steps'][0]['fields'][1] = checkbox_field
expect(build_wizard(template, user).steps[0].fields.length).to eq(2)
end
end
context 'on update' do
it 'saves submissions' do
run_update(template, nil, name: 'Angus')
expect(get_submission_data.first['name']).to eq('Angus')
end
context 'validation' do
it 'applies min length' do
template['steps'][0]['fields'][0]['min_length'] = 10
updater = run_update(template, nil, name: 'short')
expect(updater.errors.messages[:name].first).to eq(
I18n.t('wizard.field.too_short', label: 'Name', min: 10)
)
end
it 'standardises boolean entries' do
template['steps'][0]['fields'][0] = checkbox_field
run_update(template, nil, checkbox: 'false')
expect(get_submission_data.first['checkbox']).to eq(false)
end
it 'requires required fields' do
template['steps'][0]['fields'][0]['required'] = true
expect(run_update(template).errors.messages[:name].first).to eq(
I18n.t('wizard.field.required', label: 'Name')
)
end
end
context 'actions' do
it 'runs actions attached to a step' do
run_update(template, template['steps'][1]['id'], name: "Gus")
expect(user.name).to eq('Gus')
end
it 'interpolates user data correctly' do
user.name = "Angus"
user.save!
expect(
CustomWizard::Builder.fill_placeholders(
"My name is u{name}",
user,
{}
)
).to eq('My name is Angus')
end
it 'creates a topic' do
template['steps'][0]['fields'] = [text_field, textarea_field]
template['steps'][0]["actions"] = [create_topic_action]
updater = run_update(template, nil,
text: "Topic Title",
textarea: "topic body"
)
topic = Topic.where(title: "Topic Title")
expect(topic.exists?).to eq(true)
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "topic body"
).exists?).to eq(true)
end
it 'creates a topic with a custom title' do
user.name = "Angus"
user.save!
template['steps'][0]['fields'] = [text_field, textarea_field]
create_topic_action['custom_title_enabled'] = true
create_topic_action['custom_title'] = "u{name}' Topic Title"
template['steps'][0]["actions"] = [create_topic_action]
run_update(template, nil, textarea: "topic body")
topic = Topic.where(title: "Angus' Topic Title")
expect(topic.exists?).to eq(true)
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "topic body"
).exists?).to eq(true)
end
it 'creates a topic with a custom post' do
user.name = "Angus"
user.save!
template['steps'][0]['fields'] = [text_field, textarea_field]
create_topic_action['post_builder'] = true
create_topic_action['post_template'] = "u{name}' w{textarea}"
template['steps'][0]["actions"] = [create_topic_action]
run_update(template, nil,
text: "Topic Title",
textarea: "topic body"
)
topic = Topic.where(title: "Topic Title")
expect(topic.exists?).to eq(true)
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "Angus' topic body"
).exists?).to eq(true)
end
it 'sends a message' do
send_message
topic = Topic.where(
archetype: Archetype.private_message,
title: "Message Title"
)
expect(topic.exists?).to eq(true)
expect(
topic.first.topic_allowed_users.first.user.username
).to eq('angus')
expect(Post.where(
topic_id: topic.pluck(:id),
raw: "message body"
).exists?).to eq(true)
end
it 'doesnt sent a message if the required data is not present' do
send_message(user_selector_field, required: "user_selector")
topic = Topic.where(
archetype: Archetype.private_message,
title: "Message Title"
)
expect(topic.exists?).to eq(false)
end
it 'updates a profile' do
run_update(template, template['steps'][1]['id'], name: "Sally")
expect(user.name).to eq('Sally')
end
it 'opens a composer' do
template['steps'][0]['fields'] = [text_field, textarea_field]
template['steps'][0]["actions"] = [open_composer_action]
updater = run_update(template, nil,
text: "Topic Title",
textarea: "topic body"
)
expect(updater.result.blank?).to eq(true)
updater = run_update(template, template['steps'][1]['id'])
expect(updater.result[:redirect_on_complete]).to eq(
"/new-topic?title=Topic%20Title&body=topic%20body"
)
end
it 'adds a user to a group' do
template['steps'][0]['fields'] = [dropdown_groups_field]
template['steps'][0]["actions"] = [add_to_group_action]
updater = run_update(template, nil, dropdown_groups: group.id)
expect(group.users.first.username).to eq('angus')
end
it 're-routes a user' do
template['steps'][0]["actions"] = [route_to_action]
updater = run_update(template, nil, {})
expect(updater.result[:redirect_on_next]).to eq(
"https://google.com"
)
end
end
end
end
end

49
spec/fixtures/wizard.json gevendort Normale Datei
Datei anzeigen

@ -0,0 +1,49 @@
{
"id": "welcome",
"name": "Welcome",
"background": "#006da3",
"save_submissions": true,
"multiple_submissions": true,
"after_signup": true,
"theme_id": 4,
"steps": [
{
"id": "welcome",
"title": "Welcome to Pavilion",
"raw_description": "Hey there, thanks for signing up.\n\nWe're Pavilion, an international freelancer cooperative that specialises in online communities.\n\nThis site is our own community, where we work with our clients, users of our open source work and our broader community.\n\n",
"description": "<p>Hey there, thanks for signing up.</p>\n<p>Were Pavilion, an international freelancer cooperative that specialises in online communities.</p>\n<p>This site is our own community, where we work with our clients, users of our open source work and our broader community.</p>",
"fields": [
{
"id": "name",
"type": "text",
"label": "Name"
}
]
},
{
"id": "about_you",
"title": "Tell us about you",
"raw_description": "We'd like to know a little more about you. Add a your name and your website below. This will update your user profile.",
"description": "<p>Wed like to know a little more about you. Add a your name and your website below. This will update your user profile.</p>",
"fields": [
{
"id": "website",
"label": "Website",
"type": "text"
}
],
"actions": [
{
"id": "update_profile",
"type": "update_profile",
"profile_updates": [
{
"key": "name",
"value": "name"
}
]
}
]
}
]
}

8
spec/plugin_helper.rb Normale Datei
Datei anzeigen

@ -0,0 +1,8 @@
require 'simplecov'
SimpleCov.configure do
add_filter do |src|
src.filename !~ /discourse-custom-wizard/ ||
src.filename =~ /spec/
end
end

Datei anzeigen

@ -0,0 +1,5 @@
require 'rails_helper'
describe CustomWizard::AdminController do
end

Datei anzeigen

@ -0,0 +1,5 @@
require 'rails_helper'
describe ApplicationController do
end

Datei anzeigen

@ -0,0 +1,31 @@
require 'rails_helper'
describe CustomWizard::WizardController do
it 'returns a wizard if enabled' do
end
it 'returns a disabled message if disabled' do
end
it 'returns a missing message if no wizard exists' do
end
it 'returns a custom wizard theme' do
end
it 'updates the page title' do
end
it 'skips a wizard if user is allowed to skip' do
end
it 'returns a no skip message if user is not allowed to skip' do
end
end