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
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