create_custom_attributes 0.5.0

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