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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7018b4e9074371f1263c36a537695fd2d9b92165
4
+ data.tar.gz: ba70ed21c53fa7b2363959714627e2a0f7f90651
5
+ SHA512:
6
+ metadata.gz: 151eca8d70347c7318b44c92cc90aa3a808db5b82351630020704424798a859c99c2814356e45291d8d5738a81d5c88d39a2171d4b368eee8e28aab1123ac5de
7
+ data.tar.gz: 46ce57830f19bca3f35a33d68a8da4a9b32bcdc02760311cbd4284d3479e117343c07cafd2fe8a1cc7200f3d91ff38f938b31adae8b4d25d0dedae5fa5ba038e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Daniel Grützmacher
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,12 @@
1
+
2
+ # CustomAttributes
3
+ CustomAttributes allows the management of custom attributes (or metadata, meta fields) for ActiveRecord models.
4
+
5
+ ## Usage
6
+
7
+
8
+ ## Installation
9
+ This gem is not suited for standalone use, please use it by adding it to the Gemfile of a Rails application.
10
+
11
+ ## License
12
+ CustomAttributes is open source and released under the terms of the GNU General Public License v2 (GPL).
data/Rakefile ADDED
@@ -0,0 +1,35 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'CustomAttributes'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ Bundler::GemHelper.install_tasks
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'lib'
28
+ t.libs << 'test'
29
+ t.pattern = 'test/**/*_test.rb'
30
+ t.verbose = false
31
+ t.warning = false
32
+ end
33
+
34
+
35
+ task default: :test
@@ -0,0 +1,64 @@
1
+ require 'yaml'
2
+
3
+ require 'custom_attributes/acts_as/acts_as_customizable'
4
+ require 'custom_attributes/acts_as/acts_as_custom_field'
5
+ require 'custom_attributes/acts_as/acts_as_custom_value'
6
+
7
+ require 'custom_attributes/field_type'
8
+ require 'custom_attributes/field_types/unbounded'
9
+ require 'custom_attributes/field_types/list'
10
+ require 'custom_attributes/field_types/numeric'
11
+
12
+ require 'custom_attributes/field_types/bool_field_type'
13
+ require 'custom_attributes/field_types/date_field_type'
14
+ require 'custom_attributes/field_types/float_field_type'
15
+ require 'custom_attributes/field_types/text_field_type'
16
+ require 'custom_attributes/field_types/string_field_type'
17
+ require 'custom_attributes/field_types/list_field_type'
18
+ require 'custom_attributes/field_types/int_field_type'
19
+
20
+ require 'custom_attributes/custom_field_value'
21
+
22
+ require 'custom_attributes/api/custom_attributes_controller_helper'
23
+
24
+ require 'custom_attributes/concerns/searchable'
25
+
26
+ module CustomAttributes
27
+ # Gem configuration credits to: https://stackoverflow.com/questions/6233124/where-to-place-access-config-file-in-gem#10112179
28
+ @config = {
29
+ search_user: 'elastic',
30
+ search_pass: 'changeme',
31
+ search_host: 'localhost:9200'
32
+ }
33
+
34
+ @valid_config_keys = @config.keys
35
+
36
+ # Configure through hash
37
+ def self.configure(opts = {})
38
+ opts.each { |k, v| @config[k.to_sym] = v if @valid_config_keys.include? k.to_sym }
39
+
40
+ after_configuration
41
+ end
42
+
43
+ # Configure through yaml file
44
+ def self.configure_with(path_to_yaml_file)
45
+ begin
46
+ config = YAML.safe_load(IO.read(path_to_yaml_file))
47
+ rescue Errno::ENOENT
48
+ log(:warning, "YAML configuration file couldn't be found. Using defaults."); return
49
+ rescue Psych::SyntaxError
50
+ log(:warning, 'YAML configuration file contains invalid syntax. Using defaults.'); return
51
+ end
52
+
53
+ configure(config)
54
+ end
55
+
56
+ def self.config
57
+ @config
58
+ end
59
+
60
+ def self.after_configuration
61
+ # Set up ElasticSearch
62
+ Elasticsearch::Model.client = Elasticsearch::Client.new host: "http://#{@config[:search_user]}:#{@config[:search_pass]}@#{@config[:search_host]}"
63
+ end
64
+ end
@@ -0,0 +1,167 @@
1
+ module CustomAttributes
2
+ module ActsAsCustomField
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_custom_field(_options = {})
10
+ include CustomAttributes::ActsAsCustomField::InstanceMethods
11
+
12
+ scope :sorted, -> { order(:position) }
13
+
14
+ serialize :possible_values
15
+
16
+ attr_accessor :edit_tag_style
17
+
18
+ validates_numericality_of :min_length, :only_integer => true, :greater_than_or_equal_to => 0
19
+ validates_numericality_of :max_length, :only_integer => true, :greater_than_or_equal_to => 0
20
+ validates_presence_of :name, :field_type
21
+ validates_uniqueness_of :name, scope: :model_type
22
+ validates_length_of :name, maximum: 30
23
+ validates_inclusion_of :field_type, in: proc { CustomAttributes::FieldType.available_types.map { |ft| ft.name.gsub(/CustomAttributes::|FieldType/, '') } }
24
+ validate :validate_custom_field
25
+
26
+ before_create do |field|
27
+ field.set_slug
28
+ end
29
+
30
+ before_save do |field|
31
+ field.type.before_custom_field_save(field)
32
+ end
33
+ end
34
+ end
35
+
36
+ module InstanceMethods
37
+ def type
38
+ @type ||= CustomAttributes::FieldType.find(field_type)
39
+ end
40
+
41
+ # Called upon Customizable Model validation
42
+ # Actual validation handled by FieldType
43
+ def validate_custom_value(custom_value)
44
+ value = custom_value.value
45
+ errs = type.validate_custom_value(custom_value)
46
+
47
+ unless errs.any?
48
+ if value.is_a?(Array)
49
+ errs << ::I18n.t('activerecord.errors.messages.invalid') unless multiple?
50
+ if is_required? && value.detect(&:present?).nil?
51
+ errs << ::I18n.t('activerecord.errors.messages.blank')
52
+ end
53
+ else
54
+ if is_required? && value.blank?
55
+ errs << ::I18n.t('activerecord.errors.messages.blank')
56
+ end
57
+ end
58
+ end
59
+
60
+ errs
61
+ end
62
+
63
+ # Returns possible *options* that are determined by the *FieldType*
64
+ # Don't mistake for possible_values, which is a CustomField specific dynamic setting
65
+ def possible_custom_value_options(custom_value)
66
+ type.possible_custom_value_options(custom_value)
67
+ end
68
+
69
+ # Validate the CustomField according to type rules and check if the selected default value
70
+ # is indeed a valid value for this field
71
+ def validate_custom_field
72
+ if type.nil?
73
+ errors.add :default, ::I18n.t('activerecord.errors.messages.invalid_type')
74
+ return
75
+ end
76
+
77
+ type.validate_custom_field(self).each do |attribute, message|
78
+ errors.add attribute, message
79
+ end
80
+
81
+ if default.present?
82
+ validate_field_value(default).each do |message|
83
+ errors.add :default, message
84
+ end
85
+ end
86
+ end
87
+
88
+ # Helper function used in validate_custom_field
89
+ def validate_field_value(value)
90
+ validate_custom_value(CustomAttributes::CustomFieldValue.new(custom_field: self, value: value))
91
+ end
92
+
93
+ # Helper function to check if a value is a valid value for this field
94
+ def valid_field_value?(value)
95
+ validate_field_value(value).empty?
96
+ end
97
+
98
+ # Used to set the value of CustomFieldValue. No database persistance happening.
99
+ # A convenient way to override how values are being parsed via FieldType
100
+ def set_custom_field_value(custom_field_value, value)
101
+ type.set_custom_field_value(self, custom_field_value, value)
102
+ end
103
+
104
+ # Called after CustomValue has been saved
105
+ # Overrideable through FieldType
106
+ def after_save_custom_value(custom_value)
107
+ type.after_save_custom_value(self, custom_value)
108
+ end
109
+
110
+ # Serializer for possible values attribute
111
+ def possible_values
112
+ values = read_attribute(:possible_values)
113
+ if values.is_a?(Array)
114
+ values.each do |value|
115
+ value.to_s.force_encoding('UTF-8')
116
+ end
117
+ values
118
+ else
119
+ []
120
+ end
121
+ end
122
+
123
+ # Returns the value in type specific form (Integer, Float, ...)
124
+ def cast_value(value)
125
+ type.cast_value(self, value)
126
+ end
127
+
128
+ # Finds a value in a field that has predefined possible values.
129
+ # Returns array of values if the field supports multiple values
130
+ # Comma delimited keywords possible
131
+ def value_from_keyword(keyword, customized)
132
+ type.value_from_keyword(self, keyword, customized)
133
+ end
134
+
135
+ def field_type=(arg)
136
+ # cannot change type of a saved custom field
137
+ if new_record?
138
+ @type = nil
139
+ super
140
+ end
141
+ end
142
+
143
+ def possible_values=(arg)
144
+ if arg.is_a?(Array)
145
+ values = arg.compact.map { |a| a.to_s.strip }.reject(&:blank?)
146
+ write_attribute(:possible_values, values)
147
+ else
148
+ self.possible_values = arg.to_s.split(/[\n\r]+/)
149
+ end
150
+ end
151
+
152
+ protected
153
+
154
+ def set_slug
155
+ self.slug = create_slug
156
+ end
157
+
158
+ def create_slug(iterator = 0)
159
+ new_slug = name.strip.gsub(/([^A-Za-z0-9])+/) { '_' }.downcase
160
+
161
+ new_slug = "#{new_slug}_#{iterator}" unless iterator == 0
162
+ new_slug = create_slug(iterator += 1) unless CustomField.where(slug: new_slug).count == 0
163
+ new_slug
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,53 @@
1
+ module CustomAttributes
2
+ module ActsAsCustomValue
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_custom_value(_options = {})
10
+ include CustomAttributes::ActsAsCustomValue::InstanceMethods
11
+
12
+ belongs_to :custom_field
13
+ belongs_to :customizable, polymorphic: true
14
+
15
+ after_save :custom_field_after_save_custom_value
16
+ end
17
+ end
18
+
19
+ module InstanceMethods
20
+ def initialize(attributes = nil, *args)
21
+ super
22
+ if new_record? && custom_field && !attributes.key?(:value)
23
+ self.value ||= custom_field.default
24
+ end
25
+ end
26
+
27
+ def true?
28
+ value == '1'
29
+ end
30
+
31
+ def visible?
32
+ custom_field.visible?
33
+ end
34
+
35
+ def required?
36
+ custom_field.is_required?
37
+ end
38
+
39
+ def to_s
40
+ value.to_s
41
+ end
42
+
43
+ private
44
+
45
+ # Calls CustomAttributes::FieldType.after_save_custom_value
46
+ # Thus extendable by FieldType
47
+ # Default is: do nothing
48
+ def custom_field_after_save_custom_value
49
+ custom_field.after_save_custom_value(self)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,216 @@
1
+ module CustomAttributes
2
+ module ActsAsCustomizable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ end
7
+
8
+ module ClassMethods
9
+ def acts_as_customizable(options = {})
10
+ cattr_accessor :customizable_options
11
+
12
+ self.customizable_options = options
13
+ has_many :custom_values, -> { includes(:custom_field).order("#{CustomField.table_name}.position") },
14
+ as: :customizable,
15
+ inverse_of: :customizable,
16
+ dependent: :delete_all,
17
+ validate: false
18
+
19
+ include CustomAttributes::ActsAsCustomizable::InstanceMethods
20
+
21
+ validate :validate_custom_field_values
22
+ after_save :save_custom_field_values
23
+ end
24
+
25
+ # Helper function to index custom values
26
+ # Use this in mapping context and pass `self` as parameter
27
+ def index_custom_values(context)
28
+ return unless context.class.name == 'Elasticsearch::Model::Indexing::Mappings'
29
+
30
+ context.indexes :visible_in_search, type: 'boolean'
31
+ context.indexes :custom_values, type: 'nested' do
32
+ context.indexes :value, type: 'text', fields: { raw: { type: 'keyword' } }
33
+ context.indexes :custom_field_id, type: 'integer'
34
+ end
35
+ end
36
+
37
+ def available_custom_fields
38
+ CustomField.where("model_type = '#{self.name}CustomField'").sorted.to_a
39
+ end
40
+
41
+ def default_fields
42
+ []
43
+ end
44
+ end
45
+
46
+ module InstanceMethods
47
+ # Set JSON representation in elasticsearch.
48
+ # Default is to decorate the models JSON presentation with custom values
49
+ def use_custom_value_json(hash = {})
50
+ to_json = {
51
+ methods: :visible_in_search,
52
+ include: {
53
+ custom_values: {
54
+ only: %i[custom_field_id value]
55
+ }
56
+ }
57
+ }.merge(hash)
58
+ as_json(
59
+ to_json
60
+ )
61
+ end
62
+
63
+ # Helper function to access available custom fields from an instance
64
+ def available_custom_fields
65
+ self.class.available_custom_fields
66
+ end
67
+
68
+ # Override this to have control over entity visibility in search.
69
+ # Entities that return false here will be filtered out by default.
70
+ def visible_in_search
71
+ true
72
+ end
73
+
74
+ # Sets the values of the object's custom fields
75
+ # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
76
+ def assign_custom_values=(values)
77
+ values_to_hash = values.each_with_object({}) do |v, hash|
78
+ v = v.stringify_keys
79
+ hash[v['id']] = v['value'] if v['id'] && v.key?('value')
80
+ end
81
+
82
+ self.custom_field_values = values_to_hash
83
+ end
84
+
85
+ # Sets the values of the object's custom fields
86
+ # values is a hash like {'1' => 'foo', 2 => 'bar'}
87
+ def custom_field_values=(values)
88
+ values = values.stringify_keys
89
+
90
+ custom_field_values.each do |custom_field_value|
91
+ key = custom_field_value.custom_field_id.to_s
92
+ slug = custom_field_value.custom_field_slug
93
+
94
+ if values.key?(key)
95
+ custom_field_value.value = values[key]
96
+ elsif values.key?(slug)
97
+ custom_field_value.value = values[slug]
98
+ end
99
+ end
100
+
101
+ @custom_field_values_changed = true
102
+ end
103
+
104
+ # Accessor for custom fields, returns array of CustomFieldValues
105
+ def custom_field_values
106
+ @custom_field_values ||= available_custom_fields.collect do |field|
107
+ populate_custom_field_value(field)
108
+ end
109
+ end
110
+
111
+ def populated_custom_field_value(c)
112
+ field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
113
+ field = available_custom_fields.select { |field| field.id == field_id }.first
114
+
115
+ return populate_custom_field_value(field) unless field.nil?
116
+ CustomAttributes::CustomFieldValue.new
117
+ end
118
+
119
+ def populate_custom_field_value(field)
120
+ x = CustomAttributes::CustomFieldValue.new
121
+ x.custom_field = field
122
+ x.customizable = self
123
+ if field.multiple?
124
+ values = custom_values.select { |v| v.custom_field == field }
125
+ if values.empty?
126
+ values << custom_values.build(customizable: self, custom_field: field)
127
+ end
128
+ x.instance_variable_set('@value', values.map(&:value))
129
+ else
130
+ cv = custom_values.detect { |v| v.custom_field == field }
131
+ cv ||= custom_values.build(customizable: self, custom_field: field)
132
+ x.instance_variable_set('@value', cv.value)
133
+ end
134
+ x.value_was = x.value.dup if x.value
135
+ x
136
+ end
137
+
138
+ def visible_custom_field_values
139
+ custom_field_values.select(&:visible?)
140
+ end
141
+
142
+ def custom_field_values_changed?
143
+ @custom_field_values_changed == true
144
+ end
145
+
146
+ # Returns a CustomValue object for the passed CustomField object or ID
147
+ def custom_value_for(c)
148
+ field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
149
+ custom_values.detect { |v| v.custom_field_id == field_id }
150
+ end
151
+
152
+ # Returns the value for the passed CustomField object or ID
153
+ def custom_field_value(c)
154
+ field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
155
+ custom_field_values.detect { |v| v.custom_field_id == field_id }.try(:value)
156
+ end
157
+
158
+ # Extends model validation
159
+ # 1. Calls .validate_value on each CustomFieldValue
160
+ # 2. .validate_value calls .validate_custom_value on the CustomField,
161
+ # 3. which calls the validate_custom_value method on the assigned FieldType.
162
+ #
163
+ # The FieldType is therefor responsible for CustomValue validation.
164
+ def validate_custom_field_values
165
+ if new_record? || custom_field_values_changed?
166
+ custom_field_values.each(&:validate_value)
167
+ end
168
+ end
169
+
170
+ # Called *after* save of the extended model, so validation should already be over.
171
+ # This method is responsible for persisting the values that have been written to
172
+ # the CustomFieldValues and handle and save CustomValues correctly.
173
+ def save_custom_field_values
174
+ target_custom_values = []
175
+
176
+ custom_field_values.each do |custom_field_value|
177
+ if custom_field_value.value.is_a?(Array)
178
+ custom_field_value.value.each do |v|
179
+ target = custom_values.detect { |cv| cv.custom_field == custom_field_value.custom_field && cv.value == v }
180
+ target ||= custom_values.build(customizable: self, custom_field: custom_field_value.custom_field, value: v)
181
+ target_custom_values << target
182
+ end
183
+ else
184
+ target = custom_values.detect { |cv| cv.custom_field == custom_field_value.custom_field }
185
+ target ||= custom_values.build(customizable: self, custom_field: custom_field_value.custom_field)
186
+ target.value = custom_field_value.value
187
+ target_custom_values << target
188
+ end
189
+ end
190
+ self.custom_values = target_custom_values
191
+ custom_values.each(&:save)
192
+ @custom_field_values_changed = false
193
+ true
194
+ end
195
+
196
+ def reassign_custom_field_values
197
+ if @custom_field_values
198
+ values = @custom_field_values.each_with_object({}) { |v, h| h[v.custom_field_id] = v.value; }
199
+ @custom_field_values = nil
200
+ self.custom_field_values = values
201
+ end
202
+ end
203
+
204
+ def reset_custom_values!
205
+ @custom_field_values = nil
206
+ @custom_field_values_changed = true
207
+ end
208
+
209
+ def reload(*args)
210
+ @custom_field_values = nil
211
+ @custom_field_values_changed = false
212
+ super
213
+ end
214
+ end
215
+ end
216
+ end