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.
Files changed (30) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +12 -0
  4. data/Rakefile +35 -0
  5. data/lib/custom_attributes.rb +64 -0
  6. data/lib/custom_attributes/acts_as/acts_as_custom_field.rb +167 -0
  7. data/lib/custom_attributes/acts_as/acts_as_custom_value.rb +53 -0
  8. data/lib/custom_attributes/acts_as/acts_as_customizable.rb +216 -0
  9. data/lib/custom_attributes/api/custom_attributes_controller_helper.rb +32 -0
  10. data/lib/custom_attributes/concerns/searchable.rb +64 -0
  11. data/lib/custom_attributes/custom_attributes_api_helper.rb +6 -0
  12. data/lib/custom_attributes/custom_field_value.rb +54 -0
  13. data/lib/custom_attributes/field_type.rb +153 -0
  14. data/lib/custom_attributes/field_types/bool_field_type.rb +40 -0
  15. data/lib/custom_attributes/field_types/date_field_type.rb +28 -0
  16. data/lib/custom_attributes/field_types/float_field_type.rb +19 -0
  17. data/lib/custom_attributes/field_types/int_field_type.rb +19 -0
  18. data/lib/custom_attributes/field_types/list.rb +57 -0
  19. data/lib/custom_attributes/field_types/list_field_type.rb +36 -0
  20. data/lib/custom_attributes/field_types/numeric.rb +5 -0
  21. data/lib/custom_attributes/field_types/string_field_type.rb +5 -0
  22. data/lib/custom_attributes/field_types/text_field_type.rb +10 -0
  23. data/lib/custom_attributes/field_types/unbounded.rb +16 -0
  24. data/lib/custom_attributes/fluent_search_query.rb +168 -0
  25. data/lib/custom_attributes/search_query.rb +229 -0
  26. data/lib/custom_attributes/search_query_field.rb +48 -0
  27. data/lib/custom_attributes/version.rb +3 -0
  28. data/lib/tasks/custom_attributes_tasks.rake +4 -0
  29. data/lib/tasks/elasticsearch_tasks.rake +1 -0
  30. 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,6 @@
1
+ def get_customizable_class_or_fail(type)
2
+ model = type.camelize.constantize
3
+
4
+ return nil unless model.method_defined? :custom_field_values
5
+ model
6
+ 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', '&nbsp;'.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