create_custom_attributes 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|