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

Add custom field spec and improve custom field structure

Dieser Commit ist enthalten in:
Angus McLeod 2020-11-08 14:24:20 +11:00
Ursprung 1f1f2c5726
Commit 3da4d546b2
18 geänderte Dateien mit 639 neuen und 96 gelöschten Zeilen

Datei anzeigen

@ -2,16 +2,16 @@ import Controller from "@ember/controller";
import EmberObject from '@ember/object';
import { ajax } from 'discourse/lib/ajax';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import CustomWizardCustomField from "../models/custom-wizard-custom-field";
export default Controller.extend({
fieldKeys: ['klass', 'type', 'serializers', 'name'],
documentationUrl: "https://thepavilion.io/t/3572",
actions: {
addField() {
this.get('customFields').pushObject(
EmberObject.create({
new: true
})
CustomWizardCustomField.create()
);
},
@ -21,22 +21,17 @@ export default Controller.extend({
saveFields() {
this.set('saving', true);
ajax(`/admin/wizards/custom-fields`, {
type: 'PUT',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({
custom_fields: this.customFields
})
}).then(result => {
if (result.success) {
this.set('saveIcon', 'check');
} else {
this.set('saveIcon', 'times');
}
setTimeout(() => this.set('saveIcon', ''), 5000);
}).finally(() => this.set('saving', false))
.catch(popupAjaxError);
CustomWizardCustomField.saveFields(this.customFields)
.then(result => {
if (result.success) {
this.set('saveIcon', 'check');
} else {
this.set('saveIcon', 'times');
}
setTimeout(() => this.set('saveIcon', ''), 5000);
}).finally(() => {
this.set('saving', false);
});
}
}
});

Datei anzeigen

@ -0,0 +1,29 @@
import { ajax } from 'discourse/lib/ajax';
import { popupAjaxError } from 'discourse/lib/ajax-error';
import EmberObject from "@ember/object";
import { isEmpty } from "@ember/utils";
const CustomWizardCustomField = EmberObject.extend({
isNew: isEmpty('id')
});
const basePath = '/admin/wizards/custom-fields';
CustomWizardCustomField.reopenClass({
listFields() {
return ajax(basePath).catch(popupAjaxError);
},
saveFields(customFields) {
return ajax(basePath, {
type: 'PUT',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({
custom_fields: customFields
})
}).catch(popupAjaxError);
}
});
export default CustomWizardCustomField;

Datei anzeigen

@ -1,13 +1,14 @@
import DiscourseRoute from "discourse/routes/discourse";
import { ajax } from 'discourse/lib/ajax';
import CustomWizardCustomField from "../models/custom-wizard-custom-field";
import { A } from "@ember/array";
export default DiscourseRoute.extend({
model() {
return ajax('/admin/wizards/custom-fields');
return CustomWizardCustomField.listFields();
},
setupController(controller, model) {
controller.set('customFields', A(model || []));
const customFields = A(model || []);
controller.set('customFields', customFields);
}
});

Datei anzeigen

@ -19,6 +19,11 @@
</div>
</div>
{{wizard-message
key='create'
url=documentationUrl
component='custom_fields'}}
<div class="admin-wizard-container">
{{#if customFields}}
<table>

Datei anzeigen

@ -537,6 +537,10 @@
}
.admin-wizards-custom-fields {
h3 {
margin-bottom: 0;
}
.select-kit {
width: 200px;
}

Datei anzeigen

@ -72,6 +72,9 @@ en:
type: "Select an action type"
edit: "You're editing an action"
documentation: "Check out the action documentation"
custom_fields:
create: "Create or edit a custom field record"
documentation: Check out the custom field documentation
editor:
show: "Show"

Datei anzeigen

@ -6,6 +6,18 @@ en:
wizard:
custom_title: "Wizard"
custom_field:
error:
required_attribute: "'%{attr}' is a required attribute"
unsupported_class: "'%{class}' is not a supported class"
unsupported_serializers: "'%{serializers}' are not supported serializers for '%{class}'"
unsupported_type: "%{type} is not a supported custom field type"
name_invalid: "'%{name}' is not a valid custom field name"
name_too_short: "'%{name}' is too short for a custom field name (min length is #{min_length})"
name_already_taken: "'%{value}' is already taken as a custom field name"
save_default: "Failed to save custom field '%{name}'"
field:
too_short: "%{label} must be at least %{min} characters"
required: "%{label} is required."

Datei anzeigen

@ -4,32 +4,34 @@ class CustomWizard::AdminCustomFieldsController < CustomWizard::AdminController
end
def update
custom_fields = custom_field_params[:custom_fields].map do |data|
CustomWizard::CustomField.new(data.to_h)
fields_to_save = []
custom_field_params[:custom_fields].each do |field_param|
field_id = nil
field_data = {}
if saved_field = CustomWizard::CustomField.find(field_param[:name])
CustomWizard::CustomField::ATTRS.each do |attr|
field_data[attr] = field_param[attr] || saved_field.send(attr)
end
field_id = saved_field.id
end
fields_to_save.push(CustomWizard::CustomField.new(field_id, field_data))
end
custom_fields.each do |custom_field|
custom_field.validate
unless custom_field.valid?
raise Discourse::InvalidParameters,
custom_field.errors.full_messages.join("\n\n")
PluginStoreRow.transaction do
fields_to_save.each do |field|
unless field.save
raise ActiveRecord::Rollback.new,
field.errors.any? ?
field.errors.full_messages.join("\n\n") :
I18n.t("wizard.custom_field.error.save_default", name: field.name)
end
end
end
all_fields_saved = true
custom_fields.each do |field|
unless field.save
all_fields_saved = false
end
end
if all_fields_saved
render json: success_json
else
render json: error_json
end
render json: success_json
end
private
@ -37,8 +39,8 @@ class CustomWizard::AdminCustomFieldsController < CustomWizard::AdminController
def custom_field_params
params.permit(
custom_fields: [
:klass,
:name,
:klass,
:type,
serializers: []
]

Datei anzeigen

@ -1,19 +1,40 @@
# frozen_string_literal: true
class ::CustomWizard::CustomField
include HasErrors
include ActiveModel::Serialization
CLASSES ||= ["topic", "group", "category", "post"]
SERIALIZERS ||= ["topic_view", "topic_list_item", "post", "basic_category"]
TYPES ||= ["string", "boolean", "integer", "json"]
ATTRS ||= ["name", "klass", "type", "serializers"]
KEY ||= "custom_wizard_custom_fields"
attr_reader :id
def initialize(data)
ATTRS ||= ["name", "klass", "type", "serializers"]
REQUIRED ||= ["name", "klass"]
NAMESPACE ||= "custom_wizard_custom_fields"
NAME_MIN_LENGTH ||= 3
CLASSES ||= {
topic: ["topic_view", "topic_list_item"],
group: ["basic_group"],
category: ["basic_category"],
post: ["post"]
}
TYPES ||= ["string", "boolean", "integer", "json"]
def self.serializers
CLASSES.values.flatten.uniq
end
def initialize(id, data)
@id = id
data = data.with_indifferent_access
ATTRS.each do |attr|
self.class.class_eval { attr_accessor attr }
send("#{attr}=", data[attr]) if data[attr].present?
value = data[attr]
if value.present?
send("#{attr}=", value)
end
end
end
@ -22,19 +43,18 @@ class ::CustomWizard::CustomField
if valid?
data = {}
name = nil
key = name
ATTRS.each do |attr|
value = send(attr)
if attr == 'name'
name = value.parameterize(separator: '_')
else
data[attr] = value
end
(ATTRS - ['name']).each do |attr|
data[attr] = send(attr)
end
PluginStore.set(KEY, name, data)
if self.class.save_to_store(id, key, data)
self.class.reset
true
else
false
end
else
false
end
@ -43,50 +63,111 @@ class ::CustomWizard::CustomField
def validate
ATTRS.each do |attr|
value = send(attr)
i18n_key = "wizard.custom_field.error"
if value.blank?
add_error("Attribute required: #{attr}")
if REQUIRED.include?(attr) && value.blank?
I18n.t("#{i18n_key}.required_attribute", attr: attr)
next
end
if attr == 'klass' && CLASSES.exclude?(value)
add_error("Unsupported class: #{value}")
if (attr == 'klass' && CLASSES.keys.exclude?(value.to_sym)) ||
(attr == 'serializers' && CLASSES[klass.to_sym].blank?)
add_error(I18n.t("#{i18n_key}.unsupported_class", class: value))
next
end
if attr == 'serializers' && value.present? && (SERIALIZERS & value).empty?
add_error("Unsupported serializer: #{value}")
if attr == 'serializers' && (unsupported = value - CLASSES[klass.to_sym]).length > 0
add_error(I18n.t("#{i18n_key}.unsupported_serializers",
class: klass,
serializers: unsupported.join(", ")
))
end
if attr == 'type' && TYPES.exclude?(value)
add_error("Unsupported type: #{value}")
add_error(I18n.t("#{i18n_key}.unsupported_type", type: value))
end
if attr == 'name' && value.length < 3
add_error("Field name is too short")
if attr == 'name'
unless value.is_a?(String)
add_error(I18n.t("#{i18n_key}.name_invalid", name: value))
end
if value.length < NAME_MIN_LENGTH
add_error(I18n.t("#{i18n_key}.name_too_short", name: value, min_length: NAME_MIN_LENGTH))
end
if new? && self.class.exists?(name)
add_error(I18n.t("#{i18n_key}.name_already_taken", name: value))
end
begin
@name = value.parameterize(separator: '_')
rescue
add_error(I18n.t("#{i18n_key}.name_invalid", name: value))
end
end
end
end
def new?
id.blank?
end
def valid?
errors.blank?
end
def self.reset
@list = nil
end
def self.list
PluginStoreRow.where(plugin_name: KEY)
.map do |record|
data = JSON.parse(record.value)
data[:name] = record.key
self.new(data)
end
@list ||= PluginStoreRow.where(plugin_name: NAMESPACE)
.map { |record| create_from_store(record) }
end
def self.list_by(attr, value)
self.list.select do |cf|
if attr == 'serializers'
if attr == :serializers
cf.send(attr).include?(value)
else
cf.send(attr) == value
end
end
end
def self.exists?(name)
PluginStoreRow.where(plugin_name: NAMESPACE, key: name).exists?
end
def self.find(name)
records = PluginStoreRow.where(plugin_name: NAMESPACE, key: name)
if records.exists?
create_from_store(records.first)
else
false
end
end
def self.create_from_store(record)
data = JSON.parse(record.value)
data[:name] = record.key
new(record.id, data)
end
def self.save_to_store(id = nil, key, data)
if id
record = PluginStoreRow.find_by(id: id, plugin_name: NAMESPACE, key: key)
return false if !record
record.value = data.to_json
record.save
else
record = PluginStoreRow.new(plugin_name: NAMESPACE, key: key)
record.type_name = "JSON"
record.value = data.to_json
record.save
end
end
end

Datei anzeigen

@ -167,29 +167,44 @@ after_initialize do
import_files(DiscoursePluginRegistry.stylesheets["wizard_custom"])
end
CustomWizard::CustomField::CLASSES.each do |klass|
add_model_callback(klass.to_sym, :after_initialize) do
CustomWizard::CustomField.list_by('klass', klass).each do |field|
klass.classify
CustomWizard::CustomField::CLASSES.keys.each do |klass|
add_model_callback(klass, :after_initialize) do
CustomWizard::CustomField.list_by(:klass, klass.to_s).each do |field|
klass.to_s
.classify
.constantize
.register_custom_field_type(field.name, field.type.to_sym)
end
end
end
CustomWizard::CustomField::SERIALIZERS.each do |serializer_klass|
"#{serializer_klass}_serializer".classify.constantize.class_eval do
CustomWizard::CustomField.list_by('serializers', serializer_klass).each do |field|
attributes(field.name.to_sym)
class_eval %{def #{field.name}
if "#{serializer_klass}" == "topic_view"
object.topic.custom_fields["#{field.name}"]
module CustomWizardCustomFieldSerialization
def attributes(*args)
hash = super
@cw_klass = self.class.name.underscore.gsub("_serializer", "")
if cw_fields.any?
cw_fields.each do |field|
if @cw_klass == "topic_view"
hash[field.name.to_sym] = object.topic.custom_fields["#{field.name}"]
else
object.custom_fields["#{field.name}"]
hash[field.name.to_sym] = object.custom_fields["#{field.name}"]
end
end}
end
end
hash
end
private
def cw_fields
@cw_fields ||= CustomWizard::CustomField.list_by(:serializers, @cw_klass)
end
end
CustomWizard::CustomField.serializers.each do |serializer_klass|
"#{serializer_klass}_serializer".classify.constantize.prepend CustomWizardCustomFieldSerialization
end
DiscourseEvent.trigger(:custom_wizard_ready)

Datei anzeigen

@ -1,3 +1,3 @@
class CustomWizard::CustomFieldSerializer < ApplicationSerializer
attributes :klass, :name, :type, :serializers
attributes :id, :klass, :name, :type, :serializers
end

Datei anzeigen

@ -0,0 +1,178 @@
# frozen_string_literal: true
require 'rails_helper'
describe CustomWizard::CustomField do
let(:custom_field_json) {
JSON.parse(File.open(
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/custom_field/custom_fields.json"
).read)
}
it "saves custom field records" do
custom_field_json['custom_fields'].each do |field_json|
custom_field = CustomWizard::CustomField.new(nil, field_json)
expect(custom_field.save).to eq(true)
expect(
PluginStoreRow.where("
plugin_name = '#{CustomWizard::CustomField::NAMESPACE}' AND
key = '#{custom_field.name}' AND
value::jsonb = '#{field_json.except('name').to_json}'::jsonb
", ).exists?
).to eq(true)
end
end
it "updates existing custom field records" do
custom_field_json['custom_fields'].each do |field_json|
CustomWizard::CustomField.new(nil, field_json).save
end
updated_field_json = custom_field_json['custom_fields'][0]
updated_field_json['serializers'] = ["topic_view"]
existing_field = CustomWizard::CustomField.find(updated_field_json["name"])
updated_field = CustomWizard::CustomField.new(existing_field.id, updated_field_json)
expect(updated_field.save).to eq(true)
expect(
PluginStoreRow.where("
plugin_name = '#{CustomWizard::CustomField::NAMESPACE}' AND
key = '#{updated_field.name}' AND
value::jsonb = '#{updated_field_json.except('name').to_json}'::jsonb
", ).exists?
).to eq(true)
end
context "validation" do
it "does not save with an unsupported class" do
invalid_field_json = custom_field_json['custom_fields'].first
invalid_field_json['klass'] = 'user'
custom_field = CustomWizard::CustomField.new(nil, invalid_field_json)
expect(custom_field.save).to eq(false)
expect(custom_field.valid?).to eq(false)
expect(custom_field.errors.full_messages.first).to eq(
I18n.t("wizard.custom_field.error.unsupported_class", class: "user")
)
expect(
PluginStoreRow.where(
plugin_name: CustomWizard::CustomField::NAMESPACE,
key: custom_field.name
).exists?
).to eq(false)
end
it "does not save with an unsupported serializer" do
invalid_field_json = custom_field_json['custom_fields'].first
invalid_field_json['klass'] = 'category'
invalid_field_json['serializers'] = ['category', 'site_category']
custom_field = CustomWizard::CustomField.new(nil, invalid_field_json)
expect(custom_field.save).to eq(false)
expect(custom_field.valid?).to eq(false)
expect(custom_field.errors.full_messages.first).to eq(
I18n.t("wizard.custom_field.error.unsupported_serializers",
class: "category",
serializers: "category, site_category"
)
)
expect(
PluginStoreRow.where(
plugin_name: CustomWizard::CustomField::NAMESPACE,
key: custom_field.name
).exists?
).to eq(false)
end
it "does not save with an unsupported type" do
invalid_field_json = custom_field_json['custom_fields'].first
invalid_field_json['type'] = 'bigint'
custom_field = CustomWizard::CustomField.new(nil, invalid_field_json)
expect(custom_field.save).to eq(false)
expect(custom_field.valid?).to eq(false)
expect(custom_field.errors.full_messages.first).to eq(
I18n.t("wizard.custom_field.error.unsupported_type", type: "bigint")
)
expect(
PluginStoreRow.where(
plugin_name: CustomWizard::CustomField::NAMESPACE,
key: custom_field.name
).exists?
).to eq(false)
end
it "does not save with a short field name" do
invalid_field_json = custom_field_json['custom_fields'].first
invalid_field_json['name'] = 'cf'
custom_field = CustomWizard::CustomField.new(nil, invalid_field_json)
expect(custom_field.save).to eq(false)
expect(custom_field.valid?).to eq(false)
expect(custom_field.errors.full_messages.first).to eq(
I18n.t("wizard.custom_field.error.name_too_short", name: "cf")
)
expect(
PluginStoreRow.where(
plugin_name: CustomWizard::CustomField::NAMESPACE,
key: custom_field.name
).exists?
).to eq(false)
end
it "does not save with an existing name if new" do
custom_field_json['custom_fields'].each do |field_json|
CustomWizard::CustomField.new(nil, field_json).save
end
first_field_json = custom_field_json['custom_fields'][0]
custom_field = CustomWizard::CustomField.new(nil, first_field_json)
expect(custom_field.save).to eq(false)
expect(custom_field.valid?).to eq(false)
expect(custom_field.errors.full_messages.first).to eq(
I18n.t("wizard.custom_field.error.name_already_taken", name: "topic_field_1")
)
end
it "does not save with an invalid name" do
invalid_field_json = custom_field_json['custom_fields'].first
invalid_field_json['name'] = ["invalid_name"]
custom_field = CustomWizard::CustomField.new(nil, invalid_field_json)
expect(custom_field.save).to eq(false)
expect(custom_field.valid?).to eq(false)
expect(custom_field.errors.full_messages.first).to eq(
I18n.t("wizard.custom_field.error.name_invalid", name: ["invalid_name"])
)
expect(
PluginStoreRow.where(
plugin_name: CustomWizard::CustomField::NAMESPACE,
key: custom_field.name
).exists?
).to eq(false)
end
end
context "lists" do
before do
custom_field_json['custom_fields'].each do |field_json|
CustomWizard::CustomField.new(nil, field_json).save
end
end
it "lists saved custom field records" do
expect(CustomWizard::CustomField.list.length).to eq(4)
end
it "lists saved custom field records by attribute value" do
expect(CustomWizard::CustomField.list_by(:klass, 'topic').length).to eq(1)
end
end
end

Datei anzeigen

@ -0,0 +1,117 @@
# frozen_string_literal: true
require "rails_helper"
describe "custom field extensions" do
fab!(:topic) { Fabricate(:topic) }
fab!(:post) { Fabricate(:post) }
fab!(:category) { Fabricate(:category) }
fab!(:group) { Fabricate(:group) }
fab!(:user) { Fabricate(:user) }
let(:custom_field_json) {
JSON.parse(File.open(
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/custom_field/custom_fields.json"
).read)
}
before do
custom_field_json['custom_fields'].each do |field_json|
custom_field = CustomWizard::CustomField.new(nil, field_json)
custom_field.save
end
end
context "topic" do
it "registers topic custom fields" do
topic
expect(Topic.get_custom_field_type("topic_field_1")).to eq(:boolean)
end
it "adds topic custom fields to the topic_view serializer" do
topic.custom_fields["topic_field_1"] = true
topic.save_custom_fields(true)
serializer = TopicViewSerializer.new(
TopicView.new(topic.id, user),
scope: Guardian.new(user),
root: false
).as_json
expect(serializer[:topic_field_1]).to eq(true)
end
it "adds topic custom fields to the topic_list_item serializer" do
topic.custom_fields["topic_field_1"] = true
topic.save_custom_fields(true)
serializer = TopicListItemSerializer.new(
topic,
scope: Guardian.new(user),
root: false
).as_json
expect(serializer[:topic_field_1]).to eq(true)
end
end
context "post" do
it "registers post custom fields" do
post
expect(Post.get_custom_field_type("post_field_1")).to eq(:integer)
end
it "adds post custom fields to the post serializer" do
post.custom_fields["post_field_1"] = 7
post.save_custom_fields(true)
serializer = PostSerializer.new(
post,
scope: Guardian.new(user),
root: false
).as_json
expect(serializer[:post_field_1]).to eq(7)
end
end
context "category" do
it "registers category custom fields" do
category
expect(Category.get_custom_field_type("category_field_1")).to eq(:json)
end
it "adds category custom fields to the basic category serializer" do
category.custom_fields["category_field_1"] = { a: 1, b: 2 }.to_json
category.save_custom_fields(true)
serializer = BasicCategorySerializer.new(
category,
scope: Guardian.new(user),
root: false
).as_json
expect(serializer[:category_field_1]).to eq({ a: 1, b: 2 }.to_json)
end
end
context "group" do
it "registers group custom fields" do
group
expect(Group.get_custom_field_type("group_field_1")).to eq(:string)
end
it "adds group custom fields to the basic group serializer" do
group.custom_fields["group_field_1"] = "Hello"
group.save_custom_fields(true)
serializer = BasicGroupSerializer.new(
group,
scope: Guardian.new(user),
root: false
).as_json
expect(serializer[:group_field_1]).to eq("Hello")
end
end
end

Datei anzeigen

@ -14,6 +14,9 @@ describe ExtraLocalesControllerCustomWizard, type: :request do
end
it "returns locales when requested by wizard" do
@controller.request = ActionController::TestRequest.create(@controller.class)
@controller.request.env['HTTP_REFERER'] = "/w/super-mega-fun-wizard"
expect(
ExtraLocalesController.url("wizard")
).to eq(

Datei anzeigen

@ -0,0 +1,37 @@
{
"custom_fields": [
{
"klass": "topic",
"name": "topic_field_1",
"type": "boolean",
"serializers": [
"topic_list_item",
"topic_view"
]
},
{
"klass": "post",
"name": "post_field_1",
"type": "integer",
"serializers": [
"post"
]
},
{
"klass": "category",
"name": "category_field_1",
"type": "json",
"serializers": [
"basic_category"
]
},
{
"klass": "group",
"name": "group_field_1",
"type": "string",
"serializers": [
"basic_group"
]
}
]
}

Datei anzeigen

@ -0,0 +1,32 @@
require 'rails_helper'
describe CustomWizard::AdminCustomFieldsController do
fab!(:admin_user) { Fabricate(:user, admin: true) }
let(:custom_field_json) {
JSON.parse(File.open(
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/custom_field/custom_fields.json"
).read)
}
before do
custom_field_json['custom_fields'].each do |field_json|
CustomWizard::CustomField.new(nil, field_json).save
end
sign_in(admin_user)
end
it "returns the list of custom fields" do
get "/admin/wizards/custom-fields.json"
expect(response.parsed_body.length).to eq(4)
end
it "updates the list of custom fields" do
custom_field_json['custom_fields'][0]['type'] = 'string'
put "/admin/wizards/custom-fields.json", params: custom_field_json
expect(response.status).to eq(200)
expect(
CustomWizard::CustomField.find('topic_field_1').type
).to eq('string')
end
end

Datei anzeigen

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'rails_helper'
describe CustomWizard::CustomFieldSerializer do
fab!(:user) { Fabricate(:user) }
let(:custom_field_json) {
JSON.parse(File.open(
"#{Rails.root}/plugins/discourse-custom-wizard/spec/fixtures/custom_field/custom_fields.json"
).read)
}
it 'should return custom field attributes' do
custom_field_json['custom_fields'].each do |field_json|
CustomWizard::CustomField.new(nil, field_json).save
end
json = CustomWizard::CustomFieldSerializer.new(
CustomWizard::CustomField.find("topic_field_1"),
scope: Guardian.new(user),
root: false
).as_json
expect(json[:name]).to eq("topic_field_1")
expect(json[:klass]).to eq("topic")
expect(json[:type]).to eq("boolean")
expect(json[:serializers]).to match_array(["topic_list_item","topic_view"])
end
end