1
0
Fork 0

Merge pull request #69 from paviliondev/realtime-validation

FEATURE: Implemented realtime validation framework
Dieser Commit ist enthalten in:
Angus McLeod 2021-02-17 15:50:39 +11:00 committet von GitHub
Commit 7f021791f5
Es konnte kein GPG-Schlüssel zu dieser Signatur gefunden werden
GPG-Schlüssel-ID: 4AEE18F83AFDEB23
39 geänderte Dateien mit 701 neuen und 13 gelöschten Zeilen

Datei anzeigen

@ -4,6 +4,7 @@ import { computed } from "@ember/object";
import { selectKitContent } from '../lib/wizard'; import { selectKitContent } from '../lib/wizard';
import UndoChanges from '../mixins/undo-changes'; import UndoChanges from '../mixins/undo-changes';
import Component from "@ember/component"; import Component from "@ember/component";
import wizardSchema from '../lib/wizard-schema';
export default Component.extend(UndoChanges, { export default Component.extend(UndoChanges, {
componentType: 'field', componentType: 'field',
@ -26,6 +27,19 @@ export default Component.extend(UndoChanges, {
showAdvanced: alias('field.type'), showAdvanced: alias('field.type'),
messageUrl: 'https://thepavilion.io/t/2809', messageUrl: 'https://thepavilion.io/t/2809',
@discourseComputed('field.type')
validations(type) {
const applicableToField = [];
for(let validation in wizardSchema.field.validations) {
if ((wizardSchema.field.validations[validation]["types"]).includes(type)) {
applicableToField.push(validation)
}
}
return applicableToField;
},
@discourseComputed('field.type') @discourseComputed('field.type')
isDateTime(type) { isDateTime(type) {
return ['date_time', 'date', 'time'].indexOf(type) > -1; return ['date_time', 'date', 'time'].indexOf(type) > -1;

Datei anzeigen

@ -0,0 +1,44 @@
import Component from "@ember/component";
import EmberObject from "@ember/object";
import { cloneJSON } from "discourse-common/lib/object";
import Category from "discourse/models/category";
export default Component.extend({
classNames: ["realtime-validations"],
init() {
this._super(...arguments);
if (!this.validations) return;
if (!this.field.validations) {
const validations = {};
this.validations.forEach((validation) => {
validations[validation] = {};
});
this.set("field.validations", EmberObject.create(validations));
}
const validationBuffer = cloneJSON(this.get("field.validations"));
let bufferCategories;
if (
validationBuffer.similar_topics &&
(bufferCategories = validationBuffer.similar_topics.categories)
) {
const categories = Category.findByIds(bufferCategories);
validationBuffer.similar_topics.categories = categories;
}
this.set("validationBuffer", validationBuffer);
},
actions: {
updateValidationCategories(type, validation, categories) {
this.set(`validationBuffer.${type}.categories`, categories);
this.set(
`field.validations.${type}.categories`,
categories.map((category) => category.id)
);
},
},
});

Datei anzeigen

@ -244,6 +244,10 @@ export function buildFieldTypes(types) {
wizardSchema.field.types = types; wizardSchema.field.types = types;
} }
export function buildFieldValidations(validations) {
wizardSchema.field.validations = validations;
}
if (Discourse.SiteSettings.wizard_apis_enabled) { if (Discourse.SiteSettings.wizard_apis_enabled) {
wizardSchema.action.types.send_to_api = { wizardSchema.action.types.send_to_api = {
api: null, api: null,

Datei anzeigen

@ -1,5 +1,5 @@
import DiscourseRoute from "discourse/routes/discourse"; import DiscourseRoute from "discourse/routes/discourse";
import { buildFieldTypes } from '../lib/wizard-schema'; import { buildFieldTypes, buildFieldValidations } from '../lib/wizard-schema';
import EmberObject, { set } from "@ember/object"; import EmberObject, { set } from "@ember/object";
import { A } from "@ember/array"; import { A } from "@ember/array";
import { all } from "rsvp"; import { all } from "rsvp";
@ -12,7 +12,8 @@ export default DiscourseRoute.extend({
afterModel(model) { afterModel(model) {
buildFieldTypes(model.field_types); buildFieldTypes(model.field_types);
buildFieldValidations(model.realtime_validations);
return all([ return all([
this._getThemes(model), this._getThemes(model),
this._getApis(model), this._getApis(model),

Datei anzeigen

@ -219,6 +219,9 @@
</div> </div>
</div> </div>
{{#if validations}}
{{wizard-realtime-validations field=field validations=validations}}
{{/if}}
</div> </div>
{{/if}} {{/if}}
{{/if}} {{/if}}

Datei anzeigen

@ -0,0 +1,48 @@
<h3>{{i18n 'admin.wizard.field.validations.header'}}</h3>
<ul>
{{#each-in field.validations as |type props|}}
<li>
<span class="setting-title">
<h4>{{i18n (concat 'admin.wizard.field.validations.' type)}}</h4>
{{input type="checkbox" checked=props.status}}
{{i18n 'admin.wizard.field.validations.enabled'}}
</span>
<div class="validation-container">
<div class="validation-section">
<div class="setting-label">
<label>{{i18n 'admin.wizard.field.validations.categories'}}</label>
</div>
<div class="setting-value">
{{category-selector
categories=(get this (concat 'validationBuffer.' type '.categories'))
onChange=(action 'updateValidationCategories' type props)
class="wizard"}}
</div>
</div>
<div class="validation-section">
<div class="setting-label">
<label>{{i18n 'admin.wizard.field.validations.date_after'}}</label>
</div>
<div class="setting-value">
{{date-picker-past
value=(readonly props.date_after)
containerId="date-container"
onSelect=(action (mut props.date_after))}}
</div>
</div>
<div class="validation-section">
<div class="setting-label">
<label>{{i18n 'admin.wizard.field.validations.position'}}</label>
</div>
<div class="setting-value">
{{radio-button name=(concat type field.id) value="above" selection=props.position}}
{{i18n 'admin.wizard.field.validations.above'}}
{{radio-button name=(concat type field.id) value="below" selection=props.position}}
{{i18n 'admin.wizard.field.validations.below'}}
</div>
</div>
</div>
</li>
{{/each-in}}
</ul>

Datei anzeigen

@ -0,0 +1,9 @@
import Component from "@ember/component";
import { observes } from "discourse-common/utils/decorators";
export default Component.extend({
actions: {
perform() {
this.appEvents.trigger("custom-wizard:validate");
},
},
});

Datei anzeigen

@ -0,0 +1,84 @@
import WizardFieldValidator from "../../wizard/components/validator";
import { deepMerge } from "discourse-common/lib/object";
import { observes } from "discourse-common/utils/decorators";
import { cancel, later } from "@ember/runloop";
import { A } from "@ember/array";
import EmberObject, { computed } from "@ember/object";
import { notEmpty, and, equal, empty } from "@ember/object/computed";
export default WizardFieldValidator.extend({
classNames: ['similar-topics-validator'],
similarTopics: null,
hasInput: notEmpty('field.value'),
hasSimilarTopics: notEmpty('similarTopics'),
hasNotSearched: equal('similarTopics', null),
noSimilarTopics: computed('similarTopics', function() {
return this.similarTopics !== null && this.similarTopics.length == 0;
}),
showDefault: computed('hasNotSearched', 'hasInput', 'typing', function() {
return this.hasInput && (this.hasNotSearched || this.typing);
}),
showSimilarTopics: computed('typing', 'hasSimilarTopics', function() {
return this.hasSimilarTopics && !this.typing;
}),
showNoSimilarTopics: computed('typing', 'noSimilarTopics', function() {
return this.noSimilarTopics && !this.typing;
}),
validate() {},
@observes("field.value")
customValidate() {
const field = this.field;
if (!field.value) return;
const value = field.value;
this.set("typing", true);
if (value && value.length < 5) {
this.set('similarTopics', null);
return;
}
const lastKeyUp = new Date();
this._lastKeyUp = lastKeyUp;
// One second from now, check to see if the last key was hit when
// we recorded it. If it was, the user paused typing.
cancel(this._lastKeyTimeout);
this._lastKeyTimeout = later(() => {
if (lastKeyUp !== this._lastKeyUp) {
return;
}
this.set("typing", false);
this.updateSimilarTopics();
}, 1000);
},
updateSimilarTopics() {
this.set('updating', true);
this.backendValidate({
title: this.get("field.value"),
categories: this.get("validation.categories"),
date_after: this.get("validation.date_after"),
}).then((result) => {
const similarTopics = A(
deepMerge(result["topics"], result["similar_topics"])
);
similarTopics.forEach(function (topic, index) {
similarTopics[index] = EmberObject.create(topic);
});
this.set("similarTopics", similarTopics);
}).finally(() => this.set('updating', false));
},
actions: {
closeMessage() {
this.set("showMessage", false);
},
},
});

Datei anzeigen

@ -0,0 +1,44 @@
import Component from "@ember/component";
import { equal } from "@ember/object/computed";
import { ajax } from "discourse/lib/ajax";
import { getToken } from "wizard/lib/ajax";
export default Component.extend({
classNames: ['validator'],
classNameBindings: ["isValid", "isInvalid"],
validMessageKey: null,
invalidMessageKey: null,
isValid: null,
isInvalid: equal("isValid", false),
layoutName: "components/validator", // useful for sharing the template with extending components
init() {
this._super(...arguments);
if (this.get("validation.backend")) {
// set a function that can be called as often as it need to
// from the derived component
this.backendValidate = (params) => {
return ajax("/realtime-validations", {
data: {
type: this.get("type"),
authenticity_token: getToken(),
...params,
},
});
};
}
},
didInsertElement() {
this.appEvents.on("custom-wizard:validate", this, this.checkIsValid);
},
willDestroyElement() {
this.appEvents.off("custom-wizard:validate", this, this.checkIsValid);
},
checkIsValid() {
this.set("isValid", this.validate());
},
});

Datei anzeigen

@ -0,0 +1,36 @@
import Component from "@ember/component";
import { bind } from "@ember/runloop";
import { observes } from "discourse-common/utils/decorators";
export default Component.extend({
classNames: ['wizard-similar-topics'],
showTopics: true,
didInsertElement() {
$(document).on("click", bind(this, this.documentClick));
},
willDestroyElement() {
$(document).off("click", bind(this, this.documentClick));
},
documentClick(e) {
if (this._state == "destroying") return;
let $target = $(e.target);
if (!$target.hasClass('show-topics')) {
this.set('showTopics', false);
}
},
@observes('topics')
toggleShowWhenTopicsChange() {
this.set('showTopics', true);
},
actions: {
toggleShowTopics() {
this.set('showTopics', true);
}
}
})

Datei anzeigen

@ -0,0 +1,22 @@
import { registerUnbound } from "discourse-common/lib/helpers";
import { longDate, number, relativeAge } from "discourse/lib/formatter";
export default registerUnbound("date-node", function (dt) {
if (typeof dt === "string") {
dt = new Date(dt);
}
if (dt) {
const attributes = {
title: longDate(dt),
"data-time": dt.getTime(),
"data-format": "tiny",
};
const finalString = `<span class="relative-date" title="${
attributes["title"]
}" data-time="${attributes["data-time"]}" data-format="${
attributes["data-format"]
}">${relativeAge(dt)}</span>`;
return new Handlebars.SafeString(finalString);
}
});

Datei anzeigen

@ -0,0 +1,13 @@
{{#if field.validations}}
{{#each-in field.validations.above as |type validation|}}
{{component validation.component field=field type=type validation=validation}}
{{/each-in}}
{{yield (hash perform=(action 'perform'))}}
{{#each-in field.validations.below as |type validation|}}
{{component validation.component field=field type=type validation=validation}}
{{/each-in}}
{{else}}
{{yield}}
{{/if}}

Datei anzeigen

@ -0,0 +1,12 @@
{{#if loading}}
<label>{{i18n 'realtime_validations.similar_topics.loading'}}</label>
{{else if showSimilarTopics}}
<label>{{i18n 'realtime_validations.similar_topics.results'}}</label>
{{wizard-similar-topics topics=similarTopics}}
{{else if showNoSimilarTopics}}
<label>{{i18n 'realtime_validations.similar_topics.no_results'}}</label>
{{else if showDefault}}
<label>{{i18n 'realtime_validations.similar_topics.default'}}</label>
{{else}}
<label></label>
{{/if}}

Datei anzeigen

@ -0,0 +1,5 @@
{{#if isValid}}
{{i18n validMessageKey}}
{{else}}
{{i18n invalidMessageKey}}
{{/if}}

Datei anzeigen

@ -10,11 +10,13 @@
<div class='field-description'>{{cookedDescription}}</div> <div class='field-description'>{{cookedDescription}}</div>
{{/if}} {{/if}}
{{#if inputComponentName}} {{#field-validators field=field as |validators|}}
<div class='input-area'> {{#if inputComponentName}}
{{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard}} <div class='input-area'>
</div> {{component inputComponentName field=field step=step fieldClass=fieldClass wizard=wizard }}
{{/if}} </div>
{{/if}}
{{/field-validators}}
{{#if field.char_counter}} {{#if field.char_counter}}
{{#if textType}} {{#if textType}}

Datei anzeigen

@ -0,0 +1,4 @@
<a href="{{topic.url}}" target="_blank">
<span class="title">{{html-safe topic.fancy_title}}</span>
<div class="blurb">{{date-node topic.created_at}} - {{html-safe topic.blurb}}</div>
</a>

Datei anzeigen

@ -0,0 +1,11 @@
{{#if showTopics}}
<ul>
{{#each topics as |topic|}}
<li>{{wizard-similar-topic topic=topic}}</li>
{{/each}}
</ul>
{{else}}
<a class="show-topics" {{action "toggleShowTopics"}}>
{{i18n 'realtime_validations.similar_topics.show'}}
</a>
{{/if}}

Datei anzeigen

@ -642,4 +642,32 @@
} }
} }
} }
} }
.realtime-validations > ul {
list-style: none;
margin: 0;
> li {
background-color: var(--primary-low);
padding: 1em;
margin: 0 0 1em 0;
input {
margin-bottom: 0;
}
}
}
.validation-container {
display: flex;
padding: 1em 0;
.validation-section {
width: 250px;
}
}
.wizard.category-selector {
width: 200px !important;
}

Datei anzeigen

@ -155,4 +155,8 @@
.select-kit.combo-box.group-dropdown { .select-kit.combo-box.group-dropdown {
min-width: 220px; min-width: 220px;
} }
.text-field input {
margin-bottom: 0;
}
} }

Datei anzeigen

@ -0,0 +1,40 @@
.similar-topics-validator {
position: relative;
display: flex;
label {
min-height: 20px;
}
}
.wizard-similar-topics {
margin-left: 3px;
ul {
background-color: var(--tertiary-low);
padding: 5px;
margin: 0;
list-style: none;
position: absolute;
left: 0;
top: 25px;
box-shadow: shadow("dropdown");
width: 100%;
box-sizing: border-box;
z-index: 100;
.title {
color: var(--primary);
}
.blurb {
margin-left: 0.5em;
color: var(--primary-high);
font-size: $font-down-1;
}
}
.show-topics {
cursor: pointer;
}
}

Datei anzeigen

@ -10,6 +10,7 @@
@import "custom/wizard"; @import "custom/wizard";
@import "custom/step"; @import "custom/step";
@import "custom/field"; @import "custom/field";
@import "custom/validators";
@import "custom/mobile"; @import "custom/mobile";
@import "custom/autocomplete"; @import "custom/autocomplete";
@import "custom/composer"; @import "custom/composer";

Datei anzeigen

@ -176,6 +176,15 @@ en:
date_time_format: date_time_format:
label: "Format" label: "Format"
instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>" instructions: "<a href='https://momentjs.com/docs/#/displaying/format/' target='_blank'>Moment.js format</a>"
validations:
header: "Realtime Validations"
enabled: "Enabled"
similar_topics: "Similar Topics"
position: "Position"
above: "Above"
below: "Below"
categories: "Categories"
date_after: "Date After"
type: type:
text: "Text" text: "Text"
@ -511,3 +520,11 @@ en:
yourself_confirm: yourself_confirm:
title: "Did you forget to add recipients?" title: "Did you forget to add recipients?"
body: "Right now this message is only being sent to yourself!" body: "Right now this message is only being sent to yourself!"
realtime_validations:
similar_topics:
default: "When you stop typing we'll look for similar topics."
results: "Your topic is similar to..."
no_results: "No similar topics."
loading: "Looking for similar topics..."
show: "show"

Datei anzeigen

@ -9,6 +9,7 @@ end
Discourse::Application.routes.append do Discourse::Application.routes.append do
mount ::CustomWizard::Engine, at: 'w' mount ::CustomWizard::Engine, at: 'w'
post 'wizard/authorization/callback' => "custom_wizard/authorization#callback" post 'wizard/authorization/callback' => "custom_wizard/authorization#callback"
get 'realtime-validations' => 'custom_wizard/realtime_validations#validate'
scope module: 'custom_wizard', constraints: AdminConstraint.new do scope module: 'custom_wizard', constraints: AdminConstraint.new do
get 'admin/wizards' => 'admin#index' get 'admin/wizards' => 'admin#index'

Datei anzeigen

@ -8,6 +8,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController
each_serializer: CustomWizard::BasicWizardSerializer each_serializer: CustomWizard::BasicWizardSerializer
), ),
field_types: CustomWizard::Field.types, field_types: CustomWizard::Field.types,
realtime_validations: CustomWizard::RealtimeValidation.types,
custom_fields: custom_field_list custom_fields: custom_field_list
) )
end end
@ -63,7 +64,7 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController
output: [], output: [],
] ]
end end
def save_wizard_params def save_wizard_params
params.require(:wizard).permit( params.require(:wizard).permit(
:id, :id,
@ -104,7 +105,8 @@ class CustomWizard::AdminWizardController < CustomWizard::AdminController
:limit, :limit,
:property, :property,
prefill: mapped_params, prefill: mapped_params,
content: mapped_params content: mapped_params,
validations: {},
] ]
], ],
actions: [ actions: [

Datei anzeigen

@ -0,0 +1,18 @@
# frozen_string_literal: true
class CustomWizard::RealtimeValidationsController < ::ApplicationController
def validate
klass_str = "CustomWizard::RealtimeValidation::#{validation_params[:type].camelize}"
result = klass_str.constantize.new(current_user).perform(validation_params)
render_serialized(result.items, "#{klass_str}Serializer".constantize, result.serializer_opts)
end
private
def validation_params
params.require(:type)
settings = ::CustomWizard::RealtimeValidation.types[params[:type].to_sym]
params.require(settings[:required_params]) if settings[:required_params].present?
params
end
end

Datei anzeigen

@ -1,5 +1,5 @@
{ {
"result": { "result": {
"covered_percent": 89.45 "line": 89.57
} }
} }

Datei anzeigen

@ -4,6 +4,7 @@ module CustomWizardFieldExtension
:description, :description,
:image, :image,
:key, :key,
:validations,
:min_length, :min_length,
:max_length, :max_length,
:char_counter, :char_counter,
@ -20,6 +21,7 @@ module CustomWizardFieldExtension
@description = attrs[:description] @description = attrs[:description]
@image = attrs[:image] @image = attrs[:image]
@key = attrs[:key] @key = attrs[:key]
@validations = attrs[:validations]
@min_length = attrs[:min_length] @min_length = attrs[:min_length]
@max_length = attrs[:max_length] @max_length = attrs[:max_length]
@char_counter = attrs[:char_counter] @char_counter = attrs[:char_counter]

Datei anzeigen

@ -158,6 +158,7 @@ class CustomWizard::Builder
params[:description] = field_template['description'] if field_template['description'] params[:description] = field_template['description'] if field_template['description']
params[:image] = field_template['image'] if field_template['image'] params[:image] = field_template['image'] if field_template['image']
params[:key] = field_template['key'] if field_template['key'] params[:key] = field_template['key'] if field_template['key']
params[:validations] = field_template['validations'] if field_template['validations']
params[:min_length] = field_template['min_length'] if field_template['min_length'] params[:min_length] = field_template['min_length'] if field_template['min_length']
params[:max_length] = field_template['max_length'] if field_template['max_length'] params[:max_length] = field_template['max_length'] if field_template['max_length']
params[:char_counter] = field_template['char_counter'] if field_template['char_counter'] params[:char_counter] = field_template['char_counter'] if field_template['char_counter']

Datei anzeigen

@ -5,7 +5,8 @@ class CustomWizard::Field
min_length: nil, min_length: nil,
max_length: nil, max_length: nil,
prefill: nil, prefill: nil,
char_counter: nil char_counter: nil,
validations: nil
}, },
textarea: { textarea: {
min_length: nil, min_length: nil,

Datei anzeigen

@ -0,0 +1,14 @@
# frozen_string_literal: true
class CustomWizard::RealtimeValidation
cattr_accessor :types
@@types ||= {
similar_topics: {
types: [:text],
component: "similar-topics-validator",
backend: true,
required_params: []
}
}
end

Datei anzeigen

@ -0,0 +1,11 @@
class CustomWizard::RealtimeValidation::Result
attr_accessor :type,
:items,
:serializer_opts
def initialize(type)
@type = type
@items = []
@serializer_opts = {}
end
end

Datei anzeigen

@ -0,0 +1,42 @@
class CustomWizard::RealtimeValidation::SimilarTopics
attr_accessor :user
def initialize(user)
@user = user
end
class SimilarTopic
def initialize(topic)
@topic = topic
end
attr_reader :topic
def blurb
Search::GroupedSearchResults.blurb_for(cooked: @topic.try(:blurb))
end
end
def perform(params)
title = params[:title]
raw = params[:raw]
categories = params[:categories]
date_after = params[:date_after]
result = CustomWizard::RealtimeValidation::Result.new(:similar_topic)
if title.length < SiteSetting.min_title_similar_length || !Topic.count_exceeds_minimum?
return result
end
topics = Topic.similar_to(title, raw, user).to_a
topics.select! { |t| categories.include?(t.category.id.to_s) } if categories.present?
topics.select! { |t| t.created_at > DateTime.parse(date_after) } if date_after.present?
topics.map! { |t| SimilarTopic.new(t) }
result.items = topics
result.serializer_opts = { root: :similar_topics }
result
end
end

Datei anzeigen

@ -47,6 +47,7 @@ after_initialize do
../controllers/custom_wizard/admin/custom_fields.rb ../controllers/custom_wizard/admin/custom_fields.rb
../controllers/custom_wizard/wizard.rb ../controllers/custom_wizard/wizard.rb
../controllers/custom_wizard/steps.rb ../controllers/custom_wizard/steps.rb
../controllers/custom_wizard/realtime_validations.rb
../jobs/clear_after_time_wizard.rb ../jobs/clear_after_time_wizard.rb
../jobs/refresh_api_access_token.rb ../jobs/refresh_api_access_token.rb
../jobs/set_after_time_wizard.rb ../jobs/set_after_time_wizard.rb
@ -58,6 +59,9 @@ after_initialize do
../lib/custom_wizard/cache.rb ../lib/custom_wizard/cache.rb
../lib/custom_wizard/custom_field.rb ../lib/custom_wizard/custom_field.rb
../lib/custom_wizard/field.rb ../lib/custom_wizard/field.rb
../lib/custom_wizard/realtime_validation.rb
../lib/custom_wizard/realtime_validations/result.rb
../lib/custom_wizard/realtime_validations/similar_topics.rb
../lib/custom_wizard/mapper.rb ../lib/custom_wizard/mapper.rb
../lib/custom_wizard/log.rb ../lib/custom_wizard/log.rb
../lib/custom_wizard/step_updater.rb ../lib/custom_wizard/step_updater.rb
@ -79,6 +83,7 @@ after_initialize do
../serializers/custom_wizard/wizard_step_serializer.rb ../serializers/custom_wizard/wizard_step_serializer.rb
../serializers/custom_wizard/wizard_serializer.rb ../serializers/custom_wizard/wizard_serializer.rb
../serializers/custom_wizard/log_serializer.rb ../serializers/custom_wizard/log_serializer.rb
../serializers/custom_wizard/realtime_validation/similar_topics_serializer.rb
../extensions/extra_locales_controller.rb ../extensions/extra_locales_controller.rb
../extensions/invites_controller.rb ../extensions/invites_controller.rb
../extensions/users_controller.rb ../extensions/users_controller.rb

Datei anzeigen

@ -0,0 +1,2 @@
class ::CustomWizard::RealtimeValidation::SimilarTopicsSerializer < ::SimilarTopicSerializer
end

Datei anzeigen

@ -8,6 +8,7 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer
:limit, :limit,
:property, :property,
:content, :content,
:validations,
:max_length, :max_length,
:char_counter, :char_counter,
:number :number
@ -58,6 +59,17 @@ class CustomWizard::FieldSerializer < ::WizardFieldSerializer
object.choices.present? object.choices.present?
end end
def validations
validations = {}
object.validations&.each do |type, props|
next unless props["status"]
validations[props["position"]] ||= {}
validations[props["position"]][type] = props.merge CustomWizard::RealtimeValidation.types[type.to_sym]
end
validations
end
def max_length def max_length
object.max_length object.max_length
end end

Datei anzeigen

@ -0,0 +1,19 @@
# frozen_string_literal: true
require_relative '../../plugin_helper'
describe CustomWizard::RealtimeValidation do
validation_names = CustomWizard::RealtimeValidation.types.keys
validation_names.each do |name|
klass_str = "CustomWizard::RealtimeValidation::#{name.to_s.camelize}"
it "ensure class for validation: #{name} exists" do
expect(klass_str.safe_constantize).not_to be_nil
end
it "#{klass_str} has a perform() method" do
expect(klass_str.safe_constantize.instance_methods).to include(:perform)
end
end
end

Datei anzeigen

@ -0,0 +1,41 @@
# frozen_string_literal: true
require_relative '../../../plugin_helper'
describe ::CustomWizard::RealtimeValidation::SimilarTopics do
let(:post) { create_post(title: "matching similar topic") }
let(:topic) { post.topic }
let(:user) { post.user }
let(:category) { Fabricate(:category) }
let(:cat_post) { create_post(title: "matching similar topic slightly different", category: category) }
let(:cat_topic) { cat_post.topic }
let(:user) { cat_post.user }
before do
SiteSetting.min_title_similar_length = 5
Topic.stubs(:count_exceeds_minimum?).returns(true)
SearchIndexer.enable
end
it "fetches similar topics" do
validation = ::CustomWizard::RealtimeValidation::SimilarTopics.new(user)
result = validation.perform({ title: topic.title.chars.take(10).join })
expect(result.items.length).to eq(2)
end
it "filters topics based on category" do
validation = ::CustomWizard::RealtimeValidation::SimilarTopics.new(user)
result = validation.perform({ title: "matching similar", categories: [category.id.to_s] })
expect(result.items.length).to eq(1)
end
it "filters topics based on created date" do
topic.update!(created_at: 1.day.ago)
cat_topic.update!(created_at: 2.days.ago)
validation = ::CustomWizard::RealtimeValidation::SimilarTopics.new(user)
result = validation.perform({ title: "matching similar", date_after: 1.day.ago.to_date.to_s })
expect(result.items.length).to eq(1)
end
end

Datei anzeigen

@ -0,0 +1,71 @@
# frozen_string_literal: true
require_relative '../../plugin_helper'
describe CustomWizard::RealtimeValidationsController do
fab!(:validation_type) { "test_stub" }
fab!(:validation_type_stub) {
{
types: [:text],
component: "similar-topics-validator",
backend: true,
required_params: []
}
}
before(:all) do
sign_in(Fabricate(:user))
CustomWizard::RealtimeValidation.types = { test_stub: validation_type_stub }
class CustomWizard::RealtimeValidation::TestStub
attr_accessor :user
def initialize(user)
@user = user
end
def perform(params)
result = CustomWizard::RealtimeValidation::Result.new(:test_stub)
result.items = ["hello", "world"]
result
end
end
class ::CustomWizard::RealtimeValidation::TestStubSerializer < ApplicationSerializer
attributes :item
def item
object
end
end
end
it "gives the correct response for a given type" do
get '/realtime-validations.json', params: { type: validation_type }
expect(response.status).to eq(200)
expected_response = [
{ "item" => "hello" },
{ "item" => "world" }
]
expect(JSON.parse(response.body)).to eq(expected_response)
end
it "gives 400 error when no type is passed" do
get '/realtime-validations.json'
expect(response.status).to eq(400)
end
it "gives 400 error when a required additional param is missing" do
CustomWizard::RealtimeValidation.types[:test_stub][:required_params] = [:test1]
get '/realtime-validations.json', params: { type: validation_type }
expect(response.status).to eq(400)
# the addition is only relevant to this test, so getting rid of it
CustomWizard::RealtimeValidation.types[:test_stub][:required_params] = []
end
it "gives 500 response code when a non existant type is passed" do
get '/realtime-validations.json', params: { type: "random_type" }
expect(response.status).to eq(500)
end
end

Datei anzeigen

@ -33,6 +33,6 @@ describe CustomWizard::FieldSerializer do
).as_json ).as_json
expect(json_array[0][:format]).to eq("YYYY-MM-DD") expect(json_array[0][:format]).to eq("YYYY-MM-DD")
expect(json_array[5][:file_types]).to eq(".jpg,.png") expect(json_array[5][:file_types]).to eq(".jpg,.png")
expect(json_array[4][:number]).to eq("5") expect(json_array[4][:number]).to eq(5)
end end
end end