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