Angus McLeod 7 lat temu
rodzic
commit
be81aa7f4d

+ 21 - 3
assets/javascripts/discourse/controllers/admin-wizard.js.es6

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

+ 56 - 0
assets/javascripts/discourse/controllers/next-session-scheduled.js.es6

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

+ 0 - 1
assets/javascripts/discourse/helpers/custom-wizard.js.es6

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

+ 15 - 0
assets/javascripts/discourse/initializers/custom-wizard-redirect.js.es6

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

+ 33 - 24
assets/javascripts/discourse/models/custom-wizard.js.es6

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

+ 1 - 1
assets/javascripts/discourse/routes/admin-wizard-submissions.js.es6

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

+ 32 - 1
assets/javascripts/discourse/templates/admin-wizard.hbs

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

+ 24 - 0
assets/javascripts/discourse/templates/modal/next-session-scheduled.hbs

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

+ 0 - 8
assets/javascripts/wizard/controllers/custom-step.js.es6

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

+ 13 - 3
assets/javascripts/wizard/initializers/custom.js.es6

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

+ 18 - 1
assets/javascripts/wizard/models/custom.js.es6

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

Plik diff jest za duży
+ 7 - 0
assets/lib/jquery.timepicker.min.js


+ 72 - 0
assets/lib/jquery.timepicker.scss

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

+ 56 - 0
assets/stylesheets/wizard_custom_admin.scss

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

+ 17 - 2
config/locales/client.en.yml

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

+ 1 - 0
config/locales/server.en.yml

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

+ 33 - 4
controllers/admin.rb

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

+ 33 - 3
controllers/wizard.rb

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

+ 14 - 0
jobs/clear_next_session_wizard.rb

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

+ 15 - 0
jobs/set_next_session_wizard.rb

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

+ 40 - 17
lib/builder.rb

@@ -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 @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 updater.errors.empty?
+              updater.result = { redirect_to: data['redirect_to'] }
+            end
 
+            if @wizard.save_submissions && updater.errors.empty?
               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

+ 17 - 4
lib/template.rb

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

+ 58 - 18
lib/wizard.rb

@@ -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
+  def create_updater(step_id, fields)
+    step = @steps.find { |s| s.id == step_id }
+    wizard = self
+    CustomWizard::StepUpdater.new(@user, wizard, step, fields)
+  end
 
-    completed = ::UserHistory.where(
+  def unfinished?
+    most_recent = ::UserHistory.where(
       acting_user_id: @user.id,
       action: ::UserHistory.actions[:custom_wizard_step],
       context: @id,
-      subject: steps
-    ).distinct.order(:subject).pluck(:subject)
+    ).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?
+    steps = CustomWizard::Wizard.step_ids(@id)
 
-    steps.sort == completed
+    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 create_updater(step_id, fields)
-    step = @steps.find { |s| s.id == step_id.dasherize }
-    wizard = self
-    CustomWizard::StepUpdater.new(@user, wizard, step, fields)
+  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 completed?
-    completed_steps?(@steps.map(&:id))
+  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

+ 27 - 1
lib/wizard_edits.rb

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

+ 51 - 0
plugin.rb

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