0
0
Fork 1
Spiegel von https://github.com/paviliondev/discourse-custom-wizard.git synchronisiert 2024-11-22 09:20:29 +01:00
Dieser Commit ist enthalten in:
Angus McLeod 2017-11-01 12:21:14 +08:00
Ursprung a09376e645
Commit be81aa7f4d
25 geänderte Dateien mit 657 neuen und 94 gelöschten Zeilen

Datei anzeigen

@ -1,4 +1,5 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
import showModal from 'discourse/lib/show-modal';
export default Ember.Controller.extend({
@computed('model.id', 'model.name')
@ -6,6 +7,12 @@ export default Ember.Controller.extend({
return window.location.origin + '/w/' + Ember.String.dasherize(wizardId);
},
@computed('model.after_time_scheduled')
nextSessionScheduledLabel(scheduled) {
return scheduled ? moment(scheduled).format('MMMM Do, HH:mm') :
I18n.t('admin.wizard.after_time_time_label');
},
actions: {
save() {
this.setProperties({
@ -21,7 +28,6 @@ export default Ember.Controller.extend({
this.send("refreshWizard");
}
}).catch((result) => {
console.log(result)
this.set('saving', false);
this.set('error', I18n.t(`admin.wizard.error.${result.error}`));
Ember.run.later(() => this.set('error', null), 10000);
@ -29,9 +35,21 @@ export default Ember.Controller.extend({
},
remove() {
this.get('model').remove().then(() => {
const wizard = this.get('model');
wizard.remove().then(() => {
this.send("refreshAllWizards");
});
}
},
setNextSessionScheduled() {
let controller = showModal('next-session-scheduled', {
model: {
dateTime: this.get('model.after_time_scheduled'),
update: (dateTime) => this.set('model.after_time_scheduled', dateTime)
}
});
controller.setup();
},
}
});

Datei anzeigen

@ -0,0 +1,56 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
export default Ember.Controller.extend({
title: 'admin.wizard.after_time_modal.title',
setup() {
const dateTime = this.get('model.dateTime');
const ROUNDING = 30 * 60 * 1000;
const nextInterval = moment(Math.ceil((+moment()) / ROUNDING) * ROUNDING);
const mDateTime = dateTime ? moment(dateTime) : nextInterval;
const mDateTimeLocal = mDateTime.local();
const date = mDateTimeLocal.format('YYYY-MM-DD');
const time = mDateTimeLocal.format('HH:mm');
this.setProperties({ date, time });
Ember.run.scheduleOnce('afterRender', this, () => {
const $timePicker = $("#time-picker");
$timePicker.timepicker({ timeFormat: 'H:i' });
$timePicker.timepicker('setTime', time);
$timePicker.change(() => this.set('time', $timePicker.val()));
});
},
@computed('date', 'time')
dateTime: function(date, time) {
return moment(date + 'T' + time).format();
},
@computed('dateTime')
submitDisabled(dateTime) {
return moment().isAfter(dateTime);
},
resetProperties() {
this.setProperties({
date: null,
time: null
});
},
actions: {
clear() {
this.resetProperties();
this.get('model.update')(null);
},
submit() {
const dateTime = this.get('dateTime');
const formatted = moment(dateTime).utc().toISOString();
this.get('model.update')(formatted);
this.resetProperties();
this.send("closeModal");
}
}
});

Datei anzeigen

@ -1,6 +1,5 @@
import { registerUnbound } from 'discourse-common/lib/helpers';
registerUnbound('dasherize', function(string) {
console.log(string)
return Ember.String.dasherize(string);
});

Datei anzeigen

@ -0,0 +1,15 @@
export default {
name: "custom-wizard-redirect",
after: "message-bus",
initialize: function (container) {
const messageBus = container.lookup('message-bus:main');
if (!messageBus) { return; }
messageBus.subscribe("/redirect_to_wizard", function (wizardId) {
const wizardUrl = window.location.origin + '/w/' + wizardId;
window.location.href = wizardUrl;
});
}
};

Datei anzeigen

@ -1,5 +1,16 @@
import { ajax } from 'discourse/lib/ajax';
const wizardProperties = [
'name',
'background',
'save_submissions',
'multiple_submissions',
'after_signup',
'after_time',
'after_time_scheduled',
'required'
];
const CustomWizard = Discourse.Model.extend({
save() {
return new Ember.RSVP.Promise((resolve, reject) => {
@ -8,14 +19,22 @@ const CustomWizard = Discourse.Model.extend({
let wizard = { id: id.underscore() };
wizardProperties.forEach((p) => {
const value = this.get(p);
if (value) wizard[p] = value;
});
if (wizard['after_time'] && wizard['after_time_scheduled']) {
return reject({ error: 'after_time_need_time' });
};
const steps = this.get('steps');
if (steps.length > 0) {
const stepsResult = this.buildSteps(steps);
console.log(stepsResult)
if (stepsResult.error) {
reject({ error: stepsResult.error })
reject({ error: stepsResult.error });
} else {
wizard['steps'] = stepsResult;
wizard['steps'] = stepsResult.steps;
}
}
@ -23,25 +42,12 @@ const CustomWizard = Discourse.Model.extend({
return reject({ error: 'steps_required' });
}
const name = this.get('name');
if (name) wizard['name'] = name;
const background = this.get('background');
if (background) wizard['background'] = background;
const save_submissions = this.get('save_submissions');
if (save_submissions) wizard['save_submissions'] = save_submissions;
const multiple_submissions = this.get('multiple_submissions');
if (multiple_submissions) wizard['multiple_submissions'] = multiple_submissions;
ajax("/admin/wizards/custom/save", {
type: 'PUT',
data: {
wizard: JSON.stringify(wizard)
}
}).then((result) => {
console.log(result)
if (result.error) {
reject(result);
} else {
@ -86,10 +92,10 @@ const CustomWizard = Discourse.Model.extend({
if (f.type === 'dropdown') {
const choices = f.choices;
//if ((!choices || choices.length < 1) && !f.choices_key && !f.choices_categories) {
//error = 'field.need_choices';
//return;
//}
if ((!choices || choices.length < 1) && !f.choices_key && !f.choices_categories) {
error = 'field.need_choices';
return;
}
}
delete f.isNew;
@ -166,10 +172,10 @@ CustomWizard.reopenClass({
if (w) {
props['id'] = w.id;
props['existingId'] = true;
props['name'] = w.name;
props['background'] = w.background;
props['save_submissions'] = w.save_submissions;
props['multiple_submissions'] = w.multiple_submissions;
wizardProperties.forEach((p) => {
props[p] = w[p];
});
if (w.steps && w.steps.length) {
w.steps.forEach((s) => {
@ -226,6 +232,9 @@ CustomWizard.reopenClass({
props['background'] = '';
props['save_submissions'] = true;
props['multiple_submissions'] = false;
props['after_signup'] = false;
props['after_time'] = false;
props['required'] = false;
props['steps'] = Ember.A();
};

Datei anzeigen

@ -9,7 +9,7 @@ export default Discourse.Route.extend({
},
setupController(controller, model) {
let fields = ['user_id', 'completed'];
let fields = ['user'];
model.wizard.steps.forEach((s) => {
if (s.fields) {

Datei anzeigen

@ -41,7 +41,7 @@
</div>
</div>
<div class="setting full">
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.multiple_submissions'}}</h3>
</div>
@ -51,6 +51,37 @@
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.required'}}</h3>
</div>
<div class="setting-value">
{{input type='checkbox' checked=model.required}}
<span for="save">{{i18n 'admin.wizard.required_label'}}</span>
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.after_signup'}}</h3>
</div>
<div class="setting-value">
{{input type='checkbox' checked=model.after_signup}}
<span for="save">{{i18n 'admin.wizard.after_signup_label'}}</span>
</div>
</div>
<div class="setting">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.after_time'}}</h3>
</div>
<div class="setting-value">
{{input type='checkbox' checked=model.after_time}}
<span for="save">{{i18n 'admin.wizard.after_time_label'}}</span>
{{d-button action='setNextSessionScheduled' translatedLabel=nextSessionScheduledLabel icon='calendar-o'}}
</div>
</div>
<div class="setting full">
<div class="setting-label">
<h3>{{i18n 'admin.wizard.url'}}</h3>

Datei anzeigen

@ -0,0 +1,24 @@
{{#d-modal-body class="next-session-time-modal" title=title}}
<div class="date-time-card">
<div class="modal-date-time-set">
<div class="modal-date-area">
<label class="input-group-label">
{{i18n "admin.wizard.after_time_modal.date"}}
</label>
{{date-picker value=date containerId="date-container"}}
</div>
<div class="modal-time-area">
<label class="input-group-label">
{{i18n "admin.wizard.after_time_modal.time"}}
</label>
<input type="text" id="time-picker"/>
</div>
</div>
<div id="date-container"/>
</div>
{{/d-modal-body}}
<div class="modal-footer">
{{d-button action="submit" class="btn-primary" label="admin.wizard.after_time_modal.done" disabled=submitDisabled}}
<a class="clear" {{action 'clear'}}>{{i18n 'admin.wizard.after_time_modal.clear'}}</a>
</div>

Datei anzeigen

@ -19,14 +19,6 @@ export default StepController.extend({
showMessage(message) {
this.set('stepMessage', message);
},
finished(result) {
let url = "/";
if (result.topic_id) {
url += `t/${result.topic_id}`;
}
window.location.href = getUrl(url);
}
}
});

Datei anzeigen

@ -32,6 +32,12 @@ export default {
});
WizardStep.reopen({
showQuitButton: function() {
const index = this.get('step.index');
const required = this.get('wizard.required');
return index === 0 && !required;
}.property('step.index', 'wizard.required'),
bannerImage: function() {
const src = this.get('step.banner');
if (!src) return;
@ -53,7 +59,7 @@ export default {
this.get('step').save()
.then(response => {
if (this.get('finalStep')) {
this.sendAction('finished', response);
this.get('wizard').finished(response);
} else {
this.sendAction('goNext', response);
}
@ -64,8 +70,12 @@ export default {
actions: {
quit() {
this.set('finalStep', true);
this.send('nextStep');
if ($(event.target).hasClass('quit')) {
this.get('wizard').skip();
} else {
this.set('finalStep', true);
this.send('nextStep');
};
},
showMessage(message) {

Datei anzeigen

@ -1,11 +1,28 @@
import { default as computed } from 'ember-addons/ember-computed-decorators';
import getUrl from 'discourse-common/lib/get-url';
import WizardField from 'wizard/models/wizard-field';
import { ajax } from 'wizard/lib/ajax';
import Step from 'wizard/models/step';
const CustomWizard = Ember.Object.extend({
@computed('steps.length')
totalSteps: length => length
totalSteps: length => length,
skip() {
if (this.get('required')) return;
const id = this.get('id');
ajax({ url: `/w/${id}/skip`, type: 'PUT' }).then((result) => {
this.finished(result);
});
},
finished(result) {
let url = "/";
if (result.redirect_to) {
url = result.redirect_to;
}
window.location.href = getUrl(url);
}
});
export function findCustomWizard(wizardId) {

7
assets/lib/jquery.timepicker.min.js gevendort Normale Datei

Dateidiff unterdrückt, weil mindestens eine Zeile zu lang ist

Datei anzeigen

@ -0,0 +1,72 @@
.ui-timepicker-wrapper {
overflow-y: auto;
max-height: 150px;
width: 6.5em;
background: #fff;
border: 1px solid #ddd;
-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);
-moz-box-shadow:0 5px 10px rgba(0,0,0,0.2);
box-shadow:0 5px 10px rgba(0,0,0,0.2);
outline: none;
z-index: 10001;
margin: 0;
}
.ui-timepicker-wrapper.ui-timepicker-with-duration {
width: 13em;
}
.ui-timepicker-wrapper.ui-timepicker-with-duration.ui-timepicker-step-30,
.ui-timepicker-wrapper.ui-timepicker-with-duration.ui-timepicker-step-60 {
width: 11em;
}
.ui-timepicker-list {
margin: 0;
padding: 0;
list-style: none;
}
.ui-timepicker-duration {
margin-left: 5px; color: #888;
}
.ui-timepicker-list:hover .ui-timepicker-duration {
color: #888;
}
.ui-timepicker-list li {
padding: 3px 0 3px 5px;
cursor: pointer;
white-space: nowrap;
color: #000;
list-style: none;
margin: 0;
}
.ui-timepicker-list:hover .ui-timepicker-selected {
background: #fff; color: #000;
}
li.ui-timepicker-selected,
.ui-timepicker-list li:hover,
.ui-timepicker-list .ui-timepicker-selected:hover {
background: #1980EC; color: #fff;
}
li.ui-timepicker-selected .ui-timepicker-duration,
.ui-timepicker-list li:hover .ui-timepicker-duration {
color: #ccc;
}
.ui-timepicker-list li.ui-timepicker-disabled,
.ui-timepicker-list li.ui-timepicker-disabled:hover,
.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {
color: #888;
cursor: default;
}
.ui-timepicker-list li.ui-timepicker-disabled:hover,
.ui-timepicker-list li.ui-timepicker-selected.ui-timepicker-disabled {
background: #f2f2f2;
}

Datei anzeigen

@ -55,6 +55,11 @@
span {
font-size: 0.929em;
}
button {
margin-top: 5px;
display: block;
}
}
&.full {
@ -162,3 +167,54 @@
width: 100%;
min-height: 150px;
}
.next-session-time-modal {
text-align: center;
.date-time-card {
width: 270px;
padding: 10px 20px;
text-align: left;
}
.modal-date-time-set{
padding-top: 3px;
padding-bottom: 4px;
display: flex;
flex-direction: row;
.modal-date-area{
order: 1;
}
.modal-time-area{
order: 2;
margin-left: 10px;
.modal-time{
width: 127px;
}
}
}
.ui-timepicker-input {
width: 119px;
text-align: center;
}
.date-picker{
width: 121px;
}
.pika-single {
position: relative !important;
.pika-lendar {
border: 1px solid #eee;
padding: 14px;
margin: 0;
float: none;
width: auto;
}
}
}

Datei anzeigen

@ -11,9 +11,22 @@ en:
background: "Background"
background_placeholder: "Background css property"
save_submissions: "Save"
save_submissions_label: "Save wizard submissions"
save_submissions_label: "Save wizard submissions."
multiple_submissions: "Multiple"
multiple_submissions_label: "Allow multiple submissions by the same user"
multiple_submissions_label: "Allow multiple submissions by the same user."
after_signup: "After Signup"
after_signup_label: "Users are directed to wizard after signup."
after_time: "After Time"
after_time_label: "Users are directed to wizard after the start time until wizard is completed or skipped."
after_time_time_label: "Start Time"
after_time_modal:
title: "Wizard Start Time"
date: "Date"
time: "Time"
done: "Set Time"
clear: "Clear"
required: "Required"
required_label: "Users cannot skip the wizard."
save: "Save Changes"
remove: "Delete Wizard"
header: "Wizard"
@ -32,6 +45,8 @@ en:
name_required: "Wizards must have a name."
steps_required: "Wizards must have at least one step."
id_required: "All wizards, steps, fields and actions need an id."
after_time_need_time: "After time is enabled but no time is set."
after_time_invalid: "After time is invalid."
field:
need_choices: "All dropdowns need choices."
choices_label_empty: "Custom choice labels cannot be empty."

Datei anzeigen

@ -4,3 +4,4 @@ en:
field:
too_short: "%{label} must be at least %{min} characters"
none: "We couldn't find a wizard at that address."
no_skip: "Wizard can't be skipped"

Datei anzeigen

@ -17,18 +17,30 @@ class CustomWizard::AdminController < ::ApplicationController
error = nil
if !wizard["id"] || wizard["id"].empty?
if wizard["id"].blank?
error = 'id_required'
elsif !wizard["name"] || wizard["name"].empty?
elsif wizard["name"].blank?
error = 'name_required'
elsif !wizard["steps"] || wizard["steps"].empty?
elsif wizard["steps"].blank?
error = 'steps_required'
elsif wizard["after_time"]
if !wizard["after_time_scheduled"]
error = 'after_time_need_time'
else
after_time_scheduled = Time.parse(wizard["after_time_scheduled"]).utc
begin
if after_time_scheduled < Time.now.utc
error = 'after_time_invalid'
end
rescue ArgumentError
error = 'after_time_invalid'
end
end
end
return render json: { error: error } if error
wizard["steps"].each do |s|
puts "HERE IS THE ID: #{s["id"]}"
if s["id"].blank?
error = 'id_required'
break
@ -63,6 +75,17 @@ class CustomWizard::AdminController < ::ApplicationController
return render json: { error: error } if error
existing = PluginStore.get('custom_wizard', params[:id])
if wizard['after_time'] && after_time_scheduled != Time.parse(existing['after_time_scheduled']).utc
Jobs.cancel_scheduled_job(:set_after_time_wizard)
Jobs.enqueue_at(after_time_scheduled, :set_after_time_wizard, wizard_id: wizard['id'])
end
if existing['after_time'] && !wizard['after_time']
Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard['id'])
end
PluginStore.set('custom_wizard', wizard["id"], wizard)
render json: success_json
@ -71,6 +94,12 @@ class CustomWizard::AdminController < ::ApplicationController
def remove
params.require(:id)
wizard = PluginStore.get('custom_wizard', params[:id])
if wizard['after_time']
Jobs.enqueue(:clear_after_time_wizard, wizard_id: wizard['id'])
end
PluginStore.remove('custom_wizard', params[:id])
render json: success_json

Datei anzeigen

@ -11,9 +11,9 @@ class CustomWizard::WizardController < ::ApplicationController
def index
respond_to do |format|
format.json do
template = CustomWizard::Builder.new(current_user, params[:wizard_id].underscore)
if template.wizard.present?
wizard = template.build
builder = CustomWizard::Builder.new(current_user, params[:wizard_id].underscore)
if builder.wizard.present?
wizard = builder.build
render_serialized(wizard, WizardSerializer)
else
render json: { error: I18n.t('wizard.none') }
@ -22,4 +22,34 @@ class CustomWizard::WizardController < ::ApplicationController
format.html {}
end
end
## clean up if user skips wizard
def skip
wizard_id = params[:wizard_id]
wizard = PluginStore.get('custom_wizard', wizard_id.underscore)
if wizard['required']
return render json: { error: I18n.t('wizard.no_skip') }
end
user = current_user
result = success_json
submission = Array.wrap(PluginStore.get("#{wizard_id}_submissions", user.id)).last
if submission && submission['redirect_to']
result.merge!(redirect_to: submission['redirect_to'])
end
if submission && !wizard['save_submissions']
PluginStore.remove("#{wizard['id']}_submissions", user.id)
end
if user.custom_fields['redirect_to_wizard'] === wizard_id
user.custom_fields.delete('redirect_to_wizard')
user.save_custom_fields(true)
end
render json: result
end
end

Datei anzeigen

@ -0,0 +1,14 @@
module Jobs
class ClearNextSessionWizard < Jobs::Base
sidekiq_options queue: 'critical'
def execute(args)
User.human_users.each do |u|
if u.custom_fields['redirect_to_wizard'] === args[:wizard_id]
u.custom_fields.delete('redirect_to_wizard')
u.save_custom_fields(true)
end
end
end
end
end

Datei anzeigen

@ -0,0 +1,15 @@
module Jobs
class SetNextSessionWizard < Jobs::Base
def execute(args)
if PluginStoreRow.exists?(plugin_name: 'custom_wizard', key: args[:wizard_id])
user_ids = []
User.human_users.each do |u|
u.custom_fields['redirect_to_wizard'] = args[:wizard_id]
u.save_custom_fields(true)
user_ids.push(u.id)
end
MessageBus.publish "/redirect_to_wizard", args[:wizard_id], user_ids: user_ids
end
end
end
end

Datei anzeigen

@ -7,13 +7,16 @@ class CustomWizard::Builder
return if data.blank?
@template = CustomWizard::Template.new(data)
@steps = data['steps']
@wizard = CustomWizard::Wizard.new(user,
id: wizard_id,
save_submissions: data['save_submissions'],
multiple_submissions: data['multiple_submissions'],
background: data["background"],
name: data["name"]
name: data["name"],
after_time: data["after_time"],
after_signup: data["after_signup"],
required: data["required"]
)
@submissions = Array.wrap(PluginStore.get("#{wizard_id}_submissions", user.id))
end
@ -32,9 +35,9 @@ class CustomWizard::Builder
end
def build
unless (@wizard.completed? && !@template.respond_to?(:multiple_submissions)) ||
!@template.steps
@template.steps.each do |s|
unless (@wizard.completed? && !@wizard.respond_to?(:multiple_submissions)) ||
!@steps
@steps.each do |s|
@wizard.append_step(s['id']) do |step|
step.title = s['title'] if s['title']
step.description = s['description'] if s['description']
@ -53,7 +56,7 @@ class CustomWizard::Builder
params[:description] = f['description'] if f['description']
params[:key] = f['key'] if f['key']
if @submissions.last && @submissions.last['completed'] === false
if @submissions.last
submission = @submissions.last
params[:value] = submission[f['id']] if submission[f['id']]
end
@ -122,7 +125,14 @@ class CustomWizard::Builder
next if updater.errors.any?
data = @wizard.save_submissions ? submission : step_input
if @wizard.save_submissions
data = submission
else
data = step_input
# Allow redirect to be passed to wizard that doesn't save submissions.
data['redirect_to'] = submission['redirect_to'] if submission['redirect_to']
end
if s['actions'] && s['actions'].length
s['actions'].each do |a|
@ -177,7 +187,8 @@ class CustomWizard::Builder
end
post.topic.save_custom_fields(true)
end
updater.result = { topic_id: post.topic.id }
data['redirect_to'] = post.topic.url
end
end
end
@ -198,7 +209,7 @@ class CustomWizard::Builder
if creator.errors.present?
updater.errors.add(:send_message, creator.errors.full_messages.join(" "))
else
updater.result = { topic_id: post.topic_id }
data['redirect_to'] = post.topic.url
end
end
end
@ -214,20 +225,32 @@ class CustomWizard::Builder
end
end
if updater.errors.empty?
updater.result = { redirect_to: data['redirect_to'] }
end
if @wizard.save_submissions && updater.errors.empty?
@submissions.pop(1) if submission && submission['completed'] === false
submission['user_id'] = @wizard.user.id
submission['completed'] = updater.step.next.nil?
if step_input
step_input.each do |key, value|
submission[key] = value
data[key] = value
end
end
@submissions.push(submission)
PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, @submissions)
if data.present?
@submissions.pop(1) if @wizard.unfinished?
@submissions.push(data)
PluginStore.set("#{@wizard.id}_submissions", @wizard.user.id, @submissions)
end
end
# Ensure there is no submission left over after the user has completed a wizard with save_submissions off
if !@wizard.save_submissions && updater.step.next.nil?
PluginStore.remove("#{@wizard.id}_submissions", @wizard.user.id)
end
if @wizard.after_time && updater.step.next.nil?
@wizard.user.custom_fields.delete('redirect_to_wizard');
@wizard.user.save_custom_fields(true)
end
end
end

Datei anzeigen

@ -1,14 +1,27 @@
class CustomWizard::Template
attr_reader :id, :name, :steps, :background, :save_submissions, :multiple_submissions, :custom
attr_reader :id,
:name,
:steps,
:background,
:save_submissions,
:multiple_submissions,
:after_signup,
:after_time,
:after_time_scheduled,
:required
def initialize(data)
data = data.is_a?(String) ? ::JSON.parse(data) : data
@id = data['id']
@name = data['name']
@background = data['background']
@save_submissions = data['save_submissions']
@multiple_submissions = data['multiple_submissions']
@steps = data['steps']
@background = data['background']
@save_submissions = data['save_submissions'] || false
@multiple_submissions = data['multiple_submissions'] || false
@after_signup = data['after_signup']
@after_time = data['after_time']
@after_time_scheduled = data['after_time_scheduled']
@required = data['required']
end
end

Datei anzeigen

@ -6,17 +6,24 @@ require_dependency 'wizard/builder'
class CustomWizard::Wizard
attr_reader :steps, :user
attr_accessor :id, :name, :background, :save_submissions, :multiple_submissions
attr_accessor :id,
:name,
:background,
:save_submissions,
:multiple_submissions,
:after_time,
:after_signup,
:required
def initialize(user, attrs = {})
@steps = []
@user = user
@first_step = nil
@id = attrs[:id] if attrs[:id]
@name = attrs[:name] if attrs[:name]
@save_submissions = attrs[:save_submissions] if attrs[:save_submissions]
@multiple_submissions = attrs[:multiple_submissions] if attrs[:multiple_submissions]
@background = attrs[:background] if attrs[:background]
attrs.each do |key, value|
setter = "#{key}="
send(setter, value) if respond_to?(setter.to_sym, false)
end
end
def create_step(step_name)
@ -58,26 +65,59 @@ class CustomWizard::Wizard
@first_step
end
def completed_steps?(steps)
steps = [steps].flatten.uniq
completed = ::UserHistory.where(
acting_user_id: @user.id,
action: ::UserHistory.actions[:custom_wizard_step],
context: @id,
subject: steps
).distinct.order(:subject).pluck(:subject)
steps.sort == completed
end
def create_updater(step_id, fields)
step = @steps.find { |s| s.id == step_id.dasherize }
step = @steps.find { |s| s.id == step_id }
wizard = self
CustomWizard::StepUpdater.new(@user, wizard, step, fields)
end
def unfinished?
most_recent = ::UserHistory.where(
acting_user_id: @user.id,
action: ::UserHistory.actions[:custom_wizard_step],
context: @id,
).distinct.order(:updated_at).first
if most_recent
last_finished_step = most_recent.subject
last_step = CustomWizard::Wizard.step_ids(@id).last
last_finished_step != last_step
else
true
end
end
def completed?
completed_steps?(@steps.map(&:id))
steps = CustomWizard::Wizard.step_ids(@id)
history = ::UserHistory.where(
acting_user_id: @user.id,
action: ::UserHistory.actions[:custom_wizard_step],
context: @id
)
if @completed_after
history.where("updated_at > ?", @completed_after)
end
completed = history.distinct.order(:subject).pluck(:subject)
(steps - completed).empty?
end
def self.after_signup
rows = PluginStoreRow.where(plugin_name: 'custom_wizard')
wizards = [*rows].select { |r| r.value['after_signup'] }
if wizards.any?
wizards.first.key
else
false
end
end
def self.step_ids(wizard_id)
data = PluginStore.get('custom_wizard', wizard_id)
steps = data['steps'] || []
steps.map { |s| s['id'] }.flatten.uniq
end
end

Datei anzeigen

@ -1,6 +1,28 @@
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 = nil
if user && wizard_id = CustomWizard::Wizard.after_signup
custom_redirect = wizard_id.dasherize
if CustomWizard::Wizard.new(user, id: wizard_id).completed?
custom_redirect = nil
end
end
$redis.set('custom_wizard_redirect', custom_redirect)
!!custom_redirect
end
end
::Wizard::Field.class_eval do
attr_reader :label, :description, :key, :min_length
@ -24,7 +46,7 @@ class ::Wizard::Step
end
::WizardSerializer.class_eval do
attributes :id, :background, :completed
attributes :id, :background, :completed, :required
def id
object.id
@ -57,6 +79,10 @@ end
def include_steps?
!include_completed?
end
def required
object.required
end
end
::WizardStepSerializer.class_eval do

Datei anzeigen

@ -4,6 +4,8 @@
# authors: Angus McLeod
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')
@ -22,6 +24,7 @@ after_initialize do
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'
@ -45,6 +48,8 @@ after_initialize do
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__)
@ -54,4 +59,50 @@ after_initialize do
load File.expand_path('../controllers/wizard.rb', __FILE__)
load File.expand_path('../controllers/steps.rb', __FILE__)
load File.expand_path('../controllers/admin.rb', __FILE__)
::UsersController.class_eval do
def wizard_path
if custom_wizard_redirect = $redis.get('custom_wizard_redirect')
"#{Discourse.base_url}/w/#{custom_wizard_redirect}"
else
"#{Discourse.base_url}/wizard"
end
end
end
module InvitesControllerCustomWizard
def path(url)
if Wizard.user_requires_completion?(@user)
wizard_path = $redis.get('custom_wizard_redirect')
unless url === '/'
PluginStore.set("#{wizard_path.underscore}_submissions", @user.id, [{ redirect_to: url }])
end
url = "/w/#{wizard_path}"
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
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']
if @wizard_id && request.original_url !~ /w/ && request.original_url !~ /admin/
redirect_to "/w/#{@wizard_id}"
end
end
end
add_to_serializer(:current_user, :redirect_to_wizard) { object.custom_fields['redirect_to_wizard'] }
end