create_custom_attributes 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +12 -0
- data/Rakefile +35 -0
- data/lib/custom_attributes.rb +64 -0
- data/lib/custom_attributes/acts_as/acts_as_custom_field.rb +167 -0
- data/lib/custom_attributes/acts_as/acts_as_custom_value.rb +53 -0
- data/lib/custom_attributes/acts_as/acts_as_customizable.rb +216 -0
- data/lib/custom_attributes/api/custom_attributes_controller_helper.rb +32 -0
- data/lib/custom_attributes/concerns/searchable.rb +64 -0
- data/lib/custom_attributes/custom_attributes_api_helper.rb +6 -0
- data/lib/custom_attributes/custom_field_value.rb +54 -0
- data/lib/custom_attributes/field_type.rb +153 -0
- data/lib/custom_attributes/field_types/bool_field_type.rb +40 -0
- data/lib/custom_attributes/field_types/date_field_type.rb +28 -0
- data/lib/custom_attributes/field_types/float_field_type.rb +19 -0
- data/lib/custom_attributes/field_types/int_field_type.rb +19 -0
- data/lib/custom_attributes/field_types/list.rb +57 -0
- data/lib/custom_attributes/field_types/list_field_type.rb +36 -0
- data/lib/custom_attributes/field_types/numeric.rb +5 -0
- data/lib/custom_attributes/field_types/string_field_type.rb +5 -0
- data/lib/custom_attributes/field_types/text_field_type.rb +10 -0
- data/lib/custom_attributes/field_types/unbounded.rb +16 -0
- data/lib/custom_attributes/fluent_search_query.rb +168 -0
- data/lib/custom_attributes/search_query.rb +229 -0
- data/lib/custom_attributes/search_query_field.rb +48 -0
- data/lib/custom_attributes/version.rb +3 -0
- data/lib/tasks/custom_attributes_tasks.rake +4 -0
- data/lib/tasks/elasticsearch_tasks.rake +1 -0
- metadata +144 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'custom_attributes/custom_attributes_api_helper'
|
2
|
+
|
3
|
+
module CustomAttributes
|
4
|
+
module ControllerHelper
|
5
|
+
module Customizable
|
6
|
+
def self.all_available_fields(customizable_type)
|
7
|
+
customizable = customizable_type
|
8
|
+
if customizable_type.is_a? String
|
9
|
+
customizable = get_customizable_class_or_fail(customizable_type).new
|
10
|
+
end
|
11
|
+
|
12
|
+
customizable.available_custom_fields
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.all_values(customizable)
|
16
|
+
return customizable.custom_field_values
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.value_by_field_id(customizable, field_id)
|
20
|
+
return customizable.populated_custom_field_value(field_id)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.update_field_value(customizable, field_id, value)
|
24
|
+
to_change = { field_id => value }
|
25
|
+
customizable.custom_field_values = to_change
|
26
|
+
customizable.save!
|
27
|
+
|
28
|
+
customizable.custom_value_for(field_id)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'elasticsearch/model'
|
2
|
+
require 'custom_attributes/fluent_search_query'
|
3
|
+
|
4
|
+
module CustomAttributes
|
5
|
+
module Searchable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
included do
|
9
|
+
include Elasticsearch::Model
|
10
|
+
|
11
|
+
index_name "#{name.downcase.pluralize}-#{Rails.env}"
|
12
|
+
|
13
|
+
# For ActiveRecord-based models, use the after_commit callback to
|
14
|
+
# protect your data against inconsistencies caused by transaction rollbacks
|
15
|
+
if any? { |obj| obj.is_a?(ActiveRecord::Base) }
|
16
|
+
after_commit on: [:create] do
|
17
|
+
index_document_or_fail
|
18
|
+
end
|
19
|
+
|
20
|
+
after_commit on: [:update] do
|
21
|
+
index_document_or_fail
|
22
|
+
end
|
23
|
+
|
24
|
+
after_commit on: [:destroy] do
|
25
|
+
delete_document_or_fail
|
26
|
+
end
|
27
|
+
else
|
28
|
+
# everything else can use the standard implementation
|
29
|
+
include Elasticsearch::Model::Callbacks
|
30
|
+
end
|
31
|
+
|
32
|
+
# helper function to initialize index
|
33
|
+
def self.set_up_search
|
34
|
+
__elasticsearch__.create_index!
|
35
|
+
__elasticsearch__.refresh_index!
|
36
|
+
import
|
37
|
+
end
|
38
|
+
|
39
|
+
# entrypoint for the fluent query builder
|
40
|
+
def self.searchable
|
41
|
+
CustomAttributes::FluentSearchQuery.new new
|
42
|
+
end
|
43
|
+
|
44
|
+
# default connection error handling, override in model!
|
45
|
+
def self.handle_search_connection_error(exception)
|
46
|
+
raise exception
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def index_document_or_fail
|
52
|
+
__elasticsearch__.index_document
|
53
|
+
rescue Faraday::ConnectionFailed => e
|
54
|
+
handle_search_connection_error(e)
|
55
|
+
end
|
56
|
+
|
57
|
+
def delete_document_or_fail
|
58
|
+
__elasticsearch__.delete_document
|
59
|
+
rescue Faraday::ConnectionFailed => e
|
60
|
+
handle_search_connection_error(e)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module CustomAttributes
|
2
|
+
# Decorator for CustomValues
|
3
|
+
class CustomFieldValue
|
4
|
+
attr_accessor :custom_field, :customizable, :value, :value_was
|
5
|
+
|
6
|
+
def initialize(attributes = {})
|
7
|
+
attributes.each do |name, v|
|
8
|
+
send "#{name}=", v
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def custom_field_id
|
13
|
+
custom_field.id
|
14
|
+
end
|
15
|
+
|
16
|
+
def custom_field_slug
|
17
|
+
custom_field.slug
|
18
|
+
end
|
19
|
+
|
20
|
+
def true?
|
21
|
+
value == '1'
|
22
|
+
end
|
23
|
+
|
24
|
+
def visible?
|
25
|
+
custom_field.visible?
|
26
|
+
end
|
27
|
+
|
28
|
+
def required?
|
29
|
+
custom_field.is_required?
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_s
|
33
|
+
value.to_s
|
34
|
+
end
|
35
|
+
|
36
|
+
def value=(v)
|
37
|
+
@value = custom_field.set_custom_field_value(self, v)
|
38
|
+
end
|
39
|
+
|
40
|
+
def value_present?
|
41
|
+
if value.is_a?(Array)
|
42
|
+
value.any?(&:present?)
|
43
|
+
else
|
44
|
+
value.present?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def validate_value
|
49
|
+
custom_field.validate_custom_value(self).each do |message|
|
50
|
+
customizable.errors.add(:base, custom_field.name + ' ' + message)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
module CustomAttributes
|
2
|
+
class FieldType
|
3
|
+
class_attribute :type_name
|
4
|
+
class_attribute :multiple_supported
|
5
|
+
|
6
|
+
# Defines if this type
|
7
|
+
self.multiple_supported = false
|
8
|
+
|
9
|
+
# Pretty name of the field
|
10
|
+
self.type_name = nil
|
11
|
+
|
12
|
+
def name
|
13
|
+
self.class.type_name
|
14
|
+
end
|
15
|
+
|
16
|
+
def label
|
17
|
+
"label_#{name}"
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.available_types
|
21
|
+
descendants
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.find(name)
|
25
|
+
"CustomAttributes::#{name}FieldType".constantize.instance
|
26
|
+
rescue NameError
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
# Validate a CustomValue according to the type roles. This method can be
|
31
|
+
# overridden by field types if the way bulk handling should happen changes
|
32
|
+
# (e.g. List type)
|
33
|
+
def validate_custom_value(custom_value)
|
34
|
+
# 1. Wrap in Array so we can pass Array of values and single values, reject empty values
|
35
|
+
values = Array.wrap(custom_value.value).reject { |value| value.to_s == '' }
|
36
|
+
# 2. Validate each value
|
37
|
+
errors = values.map do |value|
|
38
|
+
validate_single_value(custom_value.custom_field, value, custom_value.customizable)
|
39
|
+
end
|
40
|
+
errors.flatten.uniq
|
41
|
+
end
|
42
|
+
|
43
|
+
# Override this in subclass to validate custom values
|
44
|
+
def validate_single_value(_custom_field, _value, _customizable = nil)
|
45
|
+
[]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Overide this in subclass to validate custom fields
|
49
|
+
def validate_custom_field(_custom_field)
|
50
|
+
[]
|
51
|
+
end
|
52
|
+
|
53
|
+
def after_save_custom_value(custom_field, custom_value); end
|
54
|
+
|
55
|
+
def before_custom_field_save(custom_field); end
|
56
|
+
|
57
|
+
# Prepares and sets CustomFieldValues value.
|
58
|
+
def set_custom_field_value(_custom_field, _custom_field_value, value)
|
59
|
+
if value.is_a?(Array)
|
60
|
+
value = value.map(&:to_s).reject { |v| v == '' }.uniq
|
61
|
+
value << '' if value.empty?
|
62
|
+
else
|
63
|
+
value = value.to_s
|
64
|
+
end
|
65
|
+
|
66
|
+
value
|
67
|
+
end
|
68
|
+
|
69
|
+
# Cast the value of an existing CustomValue
|
70
|
+
def cast_custom_value(custom_value)
|
71
|
+
cast_value(custom_value.custom_field, custom_value.value, custom_value.customizable)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Cast the value according to field type rules
|
75
|
+
def cast_value(custom_field, value, customizable = nil)
|
76
|
+
if value.blank?
|
77
|
+
nil
|
78
|
+
elsif value.is_a?(Array)
|
79
|
+
casted = value.map do |v|
|
80
|
+
cast_single_value(custom_field, v, customizable)
|
81
|
+
end
|
82
|
+
casted.compact.sort
|
83
|
+
else
|
84
|
+
cast_single_value(custom_field, value, customizable)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def cast_single_value(_custom_field, value, _customizable = nil)
|
89
|
+
value.to_s
|
90
|
+
end
|
91
|
+
|
92
|
+
# Checks if a keyword is in the possible values and returns the possible value if found
|
93
|
+
# If there are no possible values, returns the keyword
|
94
|
+
def value_from_keyword(custom_field, keyword, object)
|
95
|
+
possible_values_options = possible_values_options(custom_field, object)
|
96
|
+
if possible_values_options.present?
|
97
|
+
parse_keyword(custom_field, keyword) do |k|
|
98
|
+
if v = possible_values_options.detect { |text, _id| k.casecmp(text) == 0 }
|
99
|
+
if v.is_a?(Array)
|
100
|
+
v.last
|
101
|
+
else
|
102
|
+
v
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
else
|
107
|
+
keyword
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Parses keyword, allows comma delimited values
|
112
|
+
def parse_keyword(custom_field, keyword)
|
113
|
+
separator = Regexp.escape ','
|
114
|
+
keyword = keyword.to_s
|
115
|
+
|
116
|
+
if custom_field.multiple?
|
117
|
+
values = []
|
118
|
+
until keyword.empty?
|
119
|
+
k = keyword.dup
|
120
|
+
loop do
|
121
|
+
if value = yield(k.strip)
|
122
|
+
values << value
|
123
|
+
break
|
124
|
+
elsif k.slice!(/#{separator}([^#{separator}]*)\Z/).nil?
|
125
|
+
break
|
126
|
+
end
|
127
|
+
end
|
128
|
+
keyword.slice!(/\A#{Regexp.escape k}#{separator}?/)
|
129
|
+
end
|
130
|
+
values
|
131
|
+
else
|
132
|
+
yield keyword.strip
|
133
|
+
end
|
134
|
+
end
|
135
|
+
protected :parse_keyword
|
136
|
+
|
137
|
+
# Use to get the possible values of a CustomValue
|
138
|
+
def possible_custom_value_options(custom_value)
|
139
|
+
possible_values_options(custom_value.custom_field, custom_value.customizable)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Override this in subclass to specify the possible values for the field with this type
|
143
|
+
def possible_values_options(_custom_field, _object = nil)
|
144
|
+
[]
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns the HTML Tag to edit this field format.
|
148
|
+
def edit_tag(view, tag_id, tag_name, custom_value, options = {})
|
149
|
+
view.text_field_tag(tag_name, custom_value.value, options.merge(id: tag_id))
|
150
|
+
end
|
151
|
+
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module CustomAttributes
|
2
|
+
class BoolFieldType < List
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
self.multiple_supported = false
|
6
|
+
|
7
|
+
def label
|
8
|
+
'label_boolean'
|
9
|
+
end
|
10
|
+
|
11
|
+
def cast_single_value(_custom_field, value, _customizable = nil)
|
12
|
+
value == '1'
|
13
|
+
end
|
14
|
+
|
15
|
+
# Boolean supports either True or False as value
|
16
|
+
def possible_values_options(_custom_field, _object = nil)
|
17
|
+
[[::I18n.t(:general_text_Yes), '1'], [::I18n.t(:general_text_No), '0']]
|
18
|
+
end
|
19
|
+
|
20
|
+
# Boolean supports either checkbox, radiobutton or select field as edit tag
|
21
|
+
def edit_tag(view, tag_id, tag_name, custom_value, options = {})
|
22
|
+
case custom_value.custom_field.edit_tag_style
|
23
|
+
when 'check_box'
|
24
|
+
single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
|
25
|
+
when 'radio'
|
26
|
+
check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
|
27
|
+
else
|
28
|
+
select_edit_tag(view, tag_id, tag_name, custom_value, options)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Renders the edit tag as a simple check box
|
33
|
+
def single_check_box_edit_tag(view, tag_id, tag_name, custom_value, options = {})
|
34
|
+
s = ''.html_safe
|
35
|
+
s << view.hidden_field_tag(tag_name, '0', id: nil)
|
36
|
+
s << view.check_box_tag(tag_name, '1', custom_value.value.to_s == '1', id: tag_id)
|
37
|
+
view.content_tag('span', s, options)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module CustomAttributes
|
2
|
+
class DateFieldType < Unbounded
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
def cast_single_value(_custom_field, value, _customized = nil)
|
6
|
+
value.to_date
|
7
|
+
rescue
|
8
|
+
nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_single_value(_custom_field, value, _customizable = nil)
|
12
|
+
if value =~ /^\d{4}-\d{2}-\d{2}$/ && (begin
|
13
|
+
value.to_date
|
14
|
+
rescue
|
15
|
+
false
|
16
|
+
end)
|
17
|
+
[]
|
18
|
+
else
|
19
|
+
[::I18n.t('activerecord.errors.messages.not_a_date')]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def edit_tag(view, tag_id, tag_name, custom_value, options = {})
|
24
|
+
view.date_field_tag(tag_name, custom_value.value, options.merge(id: tag_id, size: 10)) +
|
25
|
+
view.calendar_for(tag_id)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CustomAttributes
|
2
|
+
class FloatFieldType < Numeric
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
def cast_single_value(_custom_field, value, _customizable = nil)
|
6
|
+
value.to_f
|
7
|
+
end
|
8
|
+
|
9
|
+
def validate_single_value(custom_field, value, customizable = nil)
|
10
|
+
errs = super
|
11
|
+
errs << ::I18n.t('activerecord.errors.messages.invalid') unless begin
|
12
|
+
Kernel.Float(value)
|
13
|
+
rescue
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
errs
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module CustomAttributes
|
2
|
+
class IntFieldType < Numeric
|
3
|
+
include Singleton
|
4
|
+
|
5
|
+
def label
|
6
|
+
"label_integer"
|
7
|
+
end
|
8
|
+
|
9
|
+
def cast_single_value(custom_field, value, customized=nil)
|
10
|
+
value.to_i
|
11
|
+
end
|
12
|
+
|
13
|
+
def validate_single_value(custom_field, value, customizable = nil)
|
14
|
+
errs = super
|
15
|
+
errs << ::I18n.t('activerecord.errors.messages.not_a_number') unless value.to_s =~ /^[+-]?\d+$/
|
16
|
+
errs
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module CustomAttributes
|
2
|
+
class List < FieldType
|
3
|
+
self.multiple_supported = true
|
4
|
+
|
5
|
+
def edit_tag(view, tag_id, tag_name, custom_value, options = {})
|
6
|
+
if custom_value.custom_field.edit_tag_style == 'check_box'
|
7
|
+
check_box_edit_tag(view, tag_id, tag_name, custom_value, options)
|
8
|
+
else
|
9
|
+
select_edit_tag(view, tag_id, tag_name, custom_value, options)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
protected
|
14
|
+
|
15
|
+
# Renders the edit tag as a select tag
|
16
|
+
def select_edit_tag(view, tag_id, tag_name, custom_value, options = {})
|
17
|
+
blank_option = ''.html_safe
|
18
|
+
unless custom_value.custom_field.multiple?
|
19
|
+
if custom_value.custom_field.is_required?
|
20
|
+
unless custom_value.custom_field.default_value.present?
|
21
|
+
blank_option = view.content_tag('option', "--- #{::I18n.t('actionview_instancetag_blank_option')} ---", value: '')
|
22
|
+
end
|
23
|
+
else
|
24
|
+
blank_option = view.content_tag('option', ' '.html_safe, value: '')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
options_tags = blank_option + view.options_for_select(possible_custom_value_options(custom_value), custom_value.value)
|
28
|
+
s = view.select_tag(tag_name, options_tags, options.merge(id: tag_id, multiple: custom_value.custom_field.multiple?))
|
29
|
+
if custom_value.custom_field.multiple?
|
30
|
+
s << view.hidden_field_tag(tag_name, '')
|
31
|
+
end
|
32
|
+
s
|
33
|
+
end
|
34
|
+
|
35
|
+
# Renders the edit tag as check box or radio tags
|
36
|
+
def check_box_edit_tag(view, _tag_id, tag_name, custom_value, options = {})
|
37
|
+
opts = []
|
38
|
+
unless custom_value.custom_field.multiple? || custom_value.custom_field.is_required?
|
39
|
+
opts << ["(#{::I18n.t('label_none')})", '']
|
40
|
+
end
|
41
|
+
opts += possible_custom_value_options(custom_value)
|
42
|
+
s = ''.html_safe
|
43
|
+
tag_method = custom_value.custom_field.multiple? ? :check_box_tag : :radio_button_tag
|
44
|
+
opts.each do |label, value|
|
45
|
+
value ||= label
|
46
|
+
checked = (custom_value.value.is_a?(Array) && custom_value.value.include?(value)) || custom_value.value.to_s == value
|
47
|
+
tag = view.send(tag_method, tag_name, value, checked, id: nil)
|
48
|
+
s << view.content_tag('label', tag + ' ' + label)
|
49
|
+
end
|
50
|
+
if custom_value.custom_field.multiple?
|
51
|
+
s << view.hidden_field_tag(tag_name, '', id: nil)
|
52
|
+
end
|
53
|
+
css = "#{options[:class]} check_box_group"
|
54
|
+
view.content_tag('span', s, options.merge(class: css))
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|