binda 0.1.3 → 0.1.4
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 +4 -4
- data/README.md +83 -25
- data/app/assets/javascripts/binda/application.js +3 -3
- data/app/assets/javascripts/binda/components/bootstrap.js +3 -4
- data/app/assets/javascripts/binda/components/field_group_editor.js +10 -10
- data/app/assets/javascripts/binda/components/field_setting_choices.js +61 -49
- data/app/assets/javascripts/binda/components/fileupload.js +135 -118
- data/app/assets/javascripts/binda/components/form_item.js +65 -65
- data/app/assets/javascripts/binda/components/form_item_editor.js +19 -19
- data/app/assets/javascripts/binda/components/form_item_image.js +11 -13
- data/app/assets/javascripts/binda/components/form_item_repeater.js +77 -71
- data/app/assets/javascripts/binda/components/login-shader.js +171 -164
- data/app/assets/javascripts/binda/components/login_form.js +65 -73
- data/app/assets/javascripts/binda/components/radio-toggle.js +8 -12
- data/app/assets/javascripts/binda/components/select2.js +19 -14
- data/app/assets/javascripts/binda/components/sortable.js +76 -71
- data/app/assets/javascripts/binda/dist/binda.bundle.js +735 -727
- data/app/assets/javascripts/binda/index.js +49 -35
- data/app/assets/stylesheets/binda/components/assets_manager.scss +13 -22
- data/app/assets/stylesheets/binda/components/b-alert.scss +18 -14
- data/app/assets/stylesheets/binda/components/b-btn.scss +24 -43
- data/app/assets/stylesheets/binda/components/field_setting_choices.scss +16 -31
- data/app/assets/stylesheets/binda/components/fileupload.scss +25 -42
- data/app/assets/stylesheets/binda/components/form_item.scss +51 -93
- data/app/assets/stylesheets/binda/components/form_item_choices.scss +7 -10
- data/app/assets/stylesheets/binda/components/login.scss +2 -2
- data/app/assets/stylesheets/binda/components/main_header.scss +5 -10
- data/app/assets/stylesheets/binda/components/main_sidebar.scss +42 -46
- data/app/assets/stylesheets/binda/components/main_sortable_table.scss +12 -21
- data/app/assets/stylesheets/binda/components/main_table.scss +18 -35
- data/app/assets/stylesheets/binda/components/popup_warning.scss +14 -27
- data/app/assets/stylesheets/binda/components/select2.scss +46 -48
- data/app/assets/stylesheets/binda/components/sortable.scss +25 -45
- data/app/assets/stylesheets/binda/components/standard-form.scss +43 -73
- data/app/assets/stylesheets/binda/controllers/users_sessions_new.scss +52 -89
- data/app/assets/stylesheets/binda/index.scss +0 -1
- data/app/assets/stylesheets/binda/settings/buttons.scss +9 -10
- data/app/assets/stylesheets/binda/settings/common.scss +17 -22
- data/app/assets/stylesheets/binda/settings/fonts.scss +112 -67
- data/app/assets/stylesheets/binda/settings/tiny_mce_overrides.scss +20 -36
- data/app/assets/stylesheets/binda/settings/variables.scss +38 -43
- data/app/controllers/binda/choices_controller.rb +14 -11
- data/app/controllers/binda/components_controller.rb +6 -4
- data/app/controllers/binda/structures_controller.rb +7 -3
- data/app/helpers/binda/components_helper.rb +69 -3
- data/app/helpers/binda/field_groups_helper.rb +16 -6
- data/app/helpers/binda/structures_helper.rb +1 -4
- data/app/models/binda/application_record.rb +4 -1
- data/app/models/binda/asset.rb +3 -1
- data/app/models/binda/b.rb +1 -0
- data/app/models/binda/category.rb +1 -0
- data/app/models/binda/checkbox.rb +2 -0
- data/app/models/binda/choice.rb +74 -41
- data/app/models/binda/component.rb +1 -1
- data/app/models/binda/date.rb +4 -0
- data/app/models/binda/deprecation.rb +7 -0
- data/app/models/binda/field_group.rb +16 -3
- data/app/models/binda/field_setting.rb +168 -41
- data/app/models/binda/image.rb +1 -0
- data/app/models/binda/radio.rb +2 -0
- data/app/models/binda/relation.rb +3 -0
- data/app/models/binda/repeater.rb +3 -0
- data/app/models/binda/selection.rb +237 -0
- data/app/models/binda/string.rb +4 -0
- data/app/models/binda/structure.rb +25 -14
- data/app/models/binda/text.rb +9 -0
- data/app/models/binda/video.rb +1 -0
- data/app/models/concerns/binda/default_helpers.rb +40 -31
- data/app/models/concerns/binda/deprecations.rb +6 -0
- data/app/models/concerns/binda/fieldable_association_helpers.rb +366 -0
- data/app/models/concerns/binda/fieldable_associations.rb +32 -369
- data/app/views/binda/boards/edit.html.erb +15 -2
- data/app/views/binda/categories/_form.html.erb +24 -51
- data/app/views/binda/categories/edit.html.erb +23 -3
- data/app/views/binda/categories/index.html.erb +49 -25
- data/app/views/binda/categories/new.html.erb +21 -2
- data/app/views/binda/components/edit.html.erb +27 -4
- data/app/views/binda/components/index.html.erb +47 -50
- data/app/views/binda/components/new.html.erb +12 -2
- data/app/views/binda/components/sort_index.html.erb +28 -13
- data/app/views/binda/field_groups/_form_body.html.erb +43 -82
- data/app/views/binda/field_groups/_form_item.html.erb +3 -120
- data/app/views/binda/field_groups/_form_section.html.erb +11 -16
- data/app/views/binda/field_groups/_form_section_repeater.html.erb +7 -15
- data/app/views/binda/field_groups/edit.html.erb +14 -2
- data/app/views/binda/field_groups/form_item/_form_item_choice_editor.html.erb +11 -0
- data/app/views/binda/field_groups/form_item/_form_item_editor.html.erb +14 -0
- data/app/views/binda/field_groups/form_item/_form_item_header.html.erb +25 -0
- data/app/views/binda/field_groups/form_item/_form_item_new_editor.html.erb +8 -0
- data/app/views/binda/field_groups/form_item/_form_item_persisted_editor.html.erb +27 -0
- data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_allow_null_choice.html.erb +11 -0
- data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_choice_header.html.erb +11 -0
- data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_default_choice.html.erb +11 -0
- data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_new_choice.html.erb +16 -0
- data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_persisted_choices.html.erb +16 -0
- data/app/views/binda/field_groups/new.html.erb +14 -2
- data/app/views/binda/field_settings/_form_body.html.erb +1 -3
- data/app/views/binda/field_settings/edit.html.erb +1 -1
- data/app/views/binda/field_settings/new.html.erb +1 -1
- data/app/views/binda/fieldable/_form_body.html.erb +24 -72
- data/app/views/binda/fieldable/_form_item_date.html.erb +1 -4
- data/app/views/binda/fieldable/_form_item_image.html.erb +3 -7
- data/app/views/binda/fieldable/_form_item_new_repeater.html.erb +0 -13
- data/app/views/binda/fieldable/_form_item_selections.html.erb +20 -112
- data/app/views/binda/fieldable/form_item_selections/_form_item_checkbox.html.erb +34 -0
- data/app/views/binda/fieldable/form_item_selections/_form_item_radio.html.erb +28 -0
- data/app/views/binda/fieldable/form_item_selections/_form_item_selection.html.erb +30 -0
- data/app/views/binda/manage/users/_form_body.html.erb +1 -31
- data/app/views/binda/manage/users/edit.html.erb +12 -2
- data/app/views/binda/manage/users/index.html.erb +36 -19
- data/app/views/binda/manage/users/new.html.erb +14 -3
- data/app/views/binda/structures/_form_body.html.erb +2 -25
- data/app/views/binda/structures/_form_section.html.erb +43 -65
- data/app/views/binda/structures/_form_sidebar.html.erb +19 -12
- data/app/views/binda/structures/edit.html.erb +20 -3
- data/app/views/binda/structures/index.html.erb +46 -26
- data/app/views/binda/structures/new.html.erb +13 -2
- data/app/views/binda/structures/sort_index.html.erb +37 -17
- data/app/views/binda/users/sessions/new.html.erb +25 -20
- data/app/views/layouts/binda/_form_errors.html.erb +10 -0
- data/app/views/layouts/binda/_sidebar.html.erb +6 -6
- data/app/views/layouts/binda/application.html.erb +1 -1
- data/config/initializers/carrierwave.rb +3 -2
- data/config/locales/en.yml +56 -12
- data/config/tinymce.yml +2 -2
- data/db/migrate/1_create_binda_tables.rb +1 -1
- data/lib/binda/version.rb +1 -1
- data/lib/generators/binda/setup/setup_generator.rb +2 -2
- data/lib/tasks/add_default_choice_to_all_selections_with_no_choices_task.rake +6 -0
- metadata +58 -8
- data/app/assets/stylesheets/binda/components/form_item_image.scss +0 -0
- data/app/views/binda/field_groups/_form_item_choice.erb +0 -104
data/app/models/binda/date.rb
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
module Binda
|
|
2
|
+
# This class provides support for dates.
|
|
2
3
|
class Date < ApplicationRecord
|
|
3
4
|
|
|
4
5
|
# Associations
|
|
5
6
|
belongs_to :fieldable, polymorphic: true
|
|
6
7
|
belongs_to :field_setting
|
|
7
8
|
|
|
9
|
+
validates :fieldable_id, presence: true
|
|
10
|
+
validates :fieldable_type, presence: true
|
|
11
|
+
|
|
8
12
|
end
|
|
9
13
|
end
|
|
@@ -9,8 +9,11 @@ module Binda
|
|
|
9
9
|
has_many :field_settings, dependent: :destroy
|
|
10
10
|
|
|
11
11
|
# Validations
|
|
12
|
-
validates :name, presence:
|
|
13
|
-
|
|
12
|
+
validates :name, presence: {
|
|
13
|
+
message: I18n.t("binda.field_group.validation_message.name")
|
|
14
|
+
}
|
|
15
|
+
validate :slug_uniqueness
|
|
16
|
+
validates_associated :field_settings
|
|
14
17
|
accepts_nested_attributes_for :field_settings, allow_destroy: true, reject_if: :is_rejected
|
|
15
18
|
|
|
16
19
|
# Slug
|
|
@@ -43,11 +46,21 @@ module Binda
|
|
|
43
46
|
attributes['name'].blank? && attributes['field_type'].blank?
|
|
44
47
|
end
|
|
45
48
|
|
|
49
|
+
def slug_uniqueness
|
|
50
|
+
record_with_same_slug = self.class.where(slug: slug)
|
|
51
|
+
if record_with_same_slug.any? && !record_with_same_slug.ids.include?(id)
|
|
52
|
+
errors.add(:slug, I18n.t("binda.field_group.validation_message.slug", { arg1: slug }))
|
|
53
|
+
return false
|
|
54
|
+
else
|
|
55
|
+
return true
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
46
59
|
private
|
|
47
60
|
|
|
48
61
|
def update_position
|
|
49
62
|
if self.position.nil?
|
|
50
|
-
self.update_attribute(
|
|
63
|
+
self.update_attribute('position', self.structure.field_groups.length)
|
|
51
64
|
end
|
|
52
65
|
end
|
|
53
66
|
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
module Binda
|
|
2
2
|
class FieldSetting < ApplicationRecord
|
|
3
|
+
cattr_accessor :field_settings_array
|
|
4
|
+
cattr_accessor :get_field_classes
|
|
5
|
+
|
|
6
|
+
# An array of all classes which represent fields associated to Binda::FieldSetting
|
|
7
|
+
# This definition must stay on the top of the file
|
|
8
|
+
def self.get_field_classes
|
|
9
|
+
%w( String Text Date Image Video Repeater Radio Selection Checkbox Relation )
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# ASSOCIATIONS
|
|
13
|
+
# ------------
|
|
3
14
|
|
|
4
15
|
belongs_to :field_group
|
|
5
16
|
has_ancestry orphan_strategy: :destroy
|
|
6
17
|
|
|
7
|
-
#
|
|
8
|
-
# has_and_belongs_to_many :structures
|
|
9
|
-
|
|
10
|
-
# Fields Associations
|
|
18
|
+
# FIELDS ASSOCIATIONS
|
|
11
19
|
#
|
|
12
20
|
# If you add a new field remember to update:
|
|
13
21
|
# - get_field_classes (see here below)
|
|
@@ -37,8 +45,15 @@ module Binda
|
|
|
37
45
|
has_many :selections, dependent: :destroy
|
|
38
46
|
has_many :checkboxes, dependent: :destroy
|
|
39
47
|
has_many :relations, dependent: :destroy
|
|
48
|
+
has_many :assets, dependent: :destroy
|
|
49
|
+
has_many :images, dependent: :destroy
|
|
50
|
+
has_many :videos, dependent: :destroy
|
|
51
|
+
|
|
52
|
+
# We don't want to run callbacks for choices!
|
|
53
|
+
# If you run a callback the last choice will throw a error
|
|
54
|
+
# @see `Binda::Choice` `before_destroy :check_last_choice`
|
|
55
|
+
has_many :choices, dependent: :delete_all
|
|
40
56
|
|
|
41
|
-
has_many :choices, dependent: :destroy
|
|
42
57
|
has_one :default_choice, -> (field_setting) { where(id: field_setting.default_choice_id) }, class_name: 'Binda::Choice'
|
|
43
58
|
|
|
44
59
|
has_and_belongs_to_many :accepted_structures, class_name: 'Binda::Structure'
|
|
@@ -52,32 +67,71 @@ module Binda
|
|
|
52
67
|
attributes['label'].blank? || attributes['content'].blank?
|
|
53
68
|
end
|
|
54
69
|
|
|
55
|
-
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# CALLBACKS
|
|
73
|
+
# ---------
|
|
74
|
+
|
|
75
|
+
before_save do
|
|
76
|
+
check_allow_null_for_radio
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
after_save do
|
|
80
|
+
add_choice_if_allow_null_is_false
|
|
81
|
+
end
|
|
56
82
|
|
|
57
83
|
after_create do
|
|
58
|
-
self.class.reset_field_settings_array
|
|
59
|
-
|
|
84
|
+
self.class.reset_field_settings_array
|
|
85
|
+
convert_allow_null__nil_to_false
|
|
60
86
|
create_field_instances
|
|
61
87
|
end
|
|
62
88
|
|
|
89
|
+
after_update do
|
|
90
|
+
if %w(radio selection checkbox).include?(self.field_type) && self.choices.empty?
|
|
91
|
+
check_allow_null_option
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
63
95
|
after_destroy do
|
|
64
96
|
self.class.reset_field_settings_array
|
|
65
97
|
end
|
|
66
98
|
|
|
67
|
-
def self.get_field_classes
|
|
68
|
-
%w( String Text Date Image Video Repeater Radio Selection Checkbox Relation )
|
|
69
|
-
end
|
|
70
99
|
|
|
71
|
-
# Validations
|
|
72
|
-
validates :name, presence: true
|
|
73
|
-
validates :field_type, inclusion: { in: [ *FieldSetting.get_field_classes.map{ |fc| fc.to_s.underscore } ], allow_nil: false, message: "Select field type among these: #{ FieldSetting.get_field_classes.join(", ") }" }
|
|
74
|
-
validates :field_group_id, presence: true
|
|
75
100
|
|
|
76
|
-
|
|
101
|
+
# VALIDATIONS
|
|
102
|
+
# -----------
|
|
103
|
+
# @see http://guides.rubyonrails.org/active_record_validations.html#message
|
|
104
|
+
|
|
105
|
+
validates :name, presence: {
|
|
106
|
+
message: I18n.t("binda.field_setting.validation_message.name")
|
|
107
|
+
}
|
|
108
|
+
validates :field_type, inclusion: {
|
|
109
|
+
in: [ *FieldSetting.get_field_classes.map{ |fc| fc.to_s.underscore } ],
|
|
110
|
+
allow_nil: false,
|
|
111
|
+
message: -> (field_setting) {
|
|
112
|
+
I18n.t(
|
|
113
|
+
"binda.field_setting.validation_message.field_type",
|
|
114
|
+
{ arg1: field_setting.name, arg2: "#{FieldSetting.get_field_classes.join(", ")}" }
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
validates :field_group_id, presence: {
|
|
119
|
+
message: -> (field_setting, data) {
|
|
120
|
+
I18n.t(
|
|
121
|
+
"binda.field_setting.validation_messag e.field_group_id",
|
|
122
|
+
{ arg1: field_setting.name, arg2: data[:value] }
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
validate :slug_uniqueness
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# FRIENDLY ID
|
|
130
|
+
# -----------
|
|
131
|
+
|
|
77
132
|
extend FriendlyId
|
|
78
133
|
friendly_id :default_slug, use: [:slugged, :finders]
|
|
79
134
|
|
|
80
|
-
|
|
81
135
|
# Friendly id preference on slug generation
|
|
82
136
|
#
|
|
83
137
|
# Method inherited from friendly id
|
|
@@ -98,28 +152,48 @@ module Binda
|
|
|
98
152
|
slug << '-'
|
|
99
153
|
slug << self.parent.name
|
|
100
154
|
end
|
|
101
|
-
|
|
102
|
-
possible_names = [
|
|
155
|
+
return [
|
|
103
156
|
"#{ slug }--#{ self.name }",
|
|
104
157
|
"#{ slug }--#{ self.name }-1",
|
|
105
158
|
"#{ slug }--#{ self.name }-2",
|
|
106
159
|
"#{ slug }--#{ self.name }-3"
|
|
107
160
|
]
|
|
161
|
+
end
|
|
108
162
|
|
|
109
|
-
|
|
163
|
+
# Check slug uniqueness
|
|
164
|
+
def slug_uniqueness
|
|
165
|
+
record_with_same_slug = self.class.where(slug: slug)
|
|
166
|
+
if record_with_same_slug.any? && !record_with_same_slug.ids.include?(id)
|
|
167
|
+
errors.add(:slug, I18n.t("binda.field_setting.validation_message.slug", { arg1: slug }))
|
|
168
|
+
return false
|
|
169
|
+
else
|
|
170
|
+
return true
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# It makes sure radio buttons have allow_null set to false.
|
|
175
|
+
def check_allow_null_for_radio
|
|
176
|
+
if field_type == 'radio' && allow_null?
|
|
177
|
+
self.allow_null = false
|
|
178
|
+
warn "WARNING: it's not possible that a field setting with type `radio` has allow_null=true."
|
|
179
|
+
end
|
|
110
180
|
end
|
|
111
181
|
|
|
182
|
+
|
|
183
|
+
# MISCELLANEOUS
|
|
184
|
+
# -------------
|
|
185
|
+
|
|
112
186
|
# Retrieve the ID if a slug is provided and update the field_settings_array
|
|
113
187
|
# in order to avoid calling the database (or the cached response) every time.
|
|
114
188
|
# This should speed up requests and make Rails logs are cleaner.
|
|
115
189
|
#
|
|
116
190
|
# @return [integer] The ID of the field setting
|
|
117
|
-
def self.get_id(
|
|
191
|
+
def self.get_id(field_slug)
|
|
118
192
|
# Get field setting id from slug, without multiple calls to database
|
|
119
193
|
# (the query runs once and caches the result, then any further call uses the cached result)
|
|
120
194
|
@@field_settings_array = self.pluck(:slug, :id) if @@field_settings_array.nil?
|
|
121
195
|
selected_field_setting = @@field_settings_array.select{ |fs| fs[0] == field_slug }[0]
|
|
122
|
-
raise ArgumentError, "There isn't any field setting with the current slug.", caller if selected_field_setting.nil?
|
|
196
|
+
raise ArgumentError, "There isn't any field setting with the current slug \"#{field_slug}\".", caller if selected_field_setting.nil?
|
|
123
197
|
id = selected_field_setting[1]
|
|
124
198
|
return id
|
|
125
199
|
end
|
|
@@ -136,11 +210,33 @@ module Binda
|
|
|
136
210
|
end
|
|
137
211
|
|
|
138
212
|
# Make sure that allow_null is set to false instead of nil.
|
|
139
|
-
# This isn't done with a database constraint in order to gain flexibility
|
|
140
|
-
|
|
213
|
+
# (This isn't done with a database constraint in order to gain flexibility)
|
|
214
|
+
#
|
|
215
|
+
# REVIEW: not sure what flexibility is needed. Maybe should be done differently
|
|
216
|
+
def convert_allow_null__nil_to_false
|
|
141
217
|
self.allow_null = false if self.allow_null.nil?
|
|
142
218
|
end
|
|
143
219
|
|
|
220
|
+
# Get structure on which the current field setting is attached. It should be one, but in order to
|
|
221
|
+
# be able to add other methods to the query this methods returns a `ActiveRecord::Relation` object, not
|
|
222
|
+
# a `ActiveRecord`
|
|
223
|
+
#
|
|
224
|
+
# @return [ActiveRecord::Relation] An array of `Binda::Structure` instances
|
|
225
|
+
def structures
|
|
226
|
+
Structure.left_outer_joins(
|
|
227
|
+
field_groups: [:field_settings]
|
|
228
|
+
).where(
|
|
229
|
+
binda_field_settings: { id: self.id }
|
|
230
|
+
)
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Get the structure of the field group to which the field setting belongs.
|
|
234
|
+
#
|
|
235
|
+
# @return [ActiveRecord] The `Binda::Structure` instance
|
|
236
|
+
def structure
|
|
237
|
+
self.structures.first
|
|
238
|
+
end
|
|
239
|
+
|
|
144
240
|
# Generates a default field instances for each existing component or board
|
|
145
241
|
# which is associated to that field setting. This avoid having issues
|
|
146
242
|
# with Binda::FieldSetting.get_id method which would throw an ambiguous error
|
|
@@ -151,31 +247,62 @@ module Binda
|
|
|
151
247
|
# a field instance is always present no matter if the component has been created
|
|
152
248
|
# before the field setting or the other way around.
|
|
153
249
|
def create_field_instances
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
structure.components.each do |component|
|
|
160
|
-
create_field_instances_for_component( component, field_class, field_setting_id )
|
|
161
|
-
end
|
|
162
|
-
when structure.board.present?
|
|
163
|
-
create_field_instances_for_board( structure.board, field_class, field_setting_id )
|
|
250
|
+
# Get the structure
|
|
251
|
+
structure = self.structures.includes(:board, components: [:repeaters]).first
|
|
252
|
+
field_class = "Binda::#{self.field_type.classify}"
|
|
253
|
+
structure.components.each do |component|
|
|
254
|
+
create_field_instances_for_instance(component, field_class, self.id)
|
|
164
255
|
end
|
|
256
|
+
create_field_instances_for_instance(structure.board, field_class, self.id) if structure.board.present?
|
|
165
257
|
end
|
|
166
258
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
259
|
+
def create_field_instance_for(instance)
|
|
260
|
+
if self.is_root?
|
|
261
|
+
create_field_instances_for_instance(instance, field_class, self.id)
|
|
262
|
+
else
|
|
263
|
+
instance.repeaters.select{|r| r.field_setting_id == self.parent_id}.each do |repeater|
|
|
264
|
+
create_field_instances_for_instance(repeater, field_class, self.id)
|
|
265
|
+
end
|
|
171
266
|
end
|
|
172
267
|
end
|
|
173
268
|
|
|
174
269
|
# Helper for create_field_instances method
|
|
175
|
-
def
|
|
176
|
-
|
|
177
|
-
|
|
270
|
+
def create_field_instances_for_instance(instance, field_class, field_setting_id)
|
|
271
|
+
field_class.constantize.find_or_create_by!(
|
|
272
|
+
field_setting_id: field_setting_id,
|
|
273
|
+
fieldable_id: instance.id,
|
|
274
|
+
fieldable_type: instance.class.name
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Check `allow_null` option
|
|
279
|
+
#
|
|
280
|
+
# Creating a selection with `allow_null` set to `false` will automatically generate a critical error.
|
|
281
|
+
# This is due to the fact that 1) there is no choice to select, but 2) the selection field must have at least one.
|
|
282
|
+
# The error can be easily removed by assigning a choice to the current field setting.
|
|
283
|
+
#
|
|
284
|
+
# This method is preferred to a validation because it allows to remove all choices before adding new ones.
|
|
285
|
+
#
|
|
286
|
+
def check_allow_null_option
|
|
287
|
+
return if self.allow_null?
|
|
288
|
+
Selection.check_all_selections_depending_on(self)
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Validation method that check if the current `Binda::Selection` instance has at least a choice before
|
|
292
|
+
# updating allow null to false
|
|
293
|
+
def add_choice_if_allow_null_is_false
|
|
294
|
+
if %(selection radio checkbox).include?(self.field_type) &&
|
|
295
|
+
!self.allow_null?
|
|
296
|
+
if self.choices.empty?
|
|
297
|
+
# Add a choice if there is none, it will be automatically assign as default choice
|
|
298
|
+
self.choices.create!(label: I18n.t("binda.choice.default_label"), value: I18n.t("binda.choice.default_value"))
|
|
299
|
+
elsif self.default_choice_id.nil?
|
|
300
|
+
# Assign a choice as default one if there is any
|
|
301
|
+
# REVIEW there is some deprecation going on, but I'm not sure i directly involves the `update` method
|
|
302
|
+
self.update!(default_choice_id: self.choices.first.id)
|
|
303
|
+
end
|
|
178
304
|
end
|
|
179
305
|
end
|
|
306
|
+
|
|
180
307
|
end
|
|
181
308
|
end
|
data/app/models/binda/image.rb
CHANGED
data/app/models/binda/radio.rb
CHANGED
|
@@ -21,6 +21,9 @@ module Binda
|
|
|
21
21
|
belongs_to :fieldable, polymorphic: true
|
|
22
22
|
belongs_to :field_setting
|
|
23
23
|
|
|
24
|
+
validates :fieldable_id, presence: true
|
|
25
|
+
validates :fieldable_type, presence: true
|
|
26
|
+
|
|
24
27
|
# Relations are the connection between a Owner to its Dependents
|
|
25
28
|
# The Active Relation connects a Relation to a Dependent (which is can be a Component or a Board)
|
|
26
29
|
# The Passive Relation connects a Relation to a Owner (which is can be a Component or a Board)
|
|
@@ -7,6 +7,9 @@ module Binda
|
|
|
7
7
|
belongs_to :fieldable, polymorphic: true
|
|
8
8
|
belongs_to :field_setting
|
|
9
9
|
|
|
10
|
+
validates :fieldable_id, presence: true
|
|
11
|
+
validates :fieldable_type, presence: true
|
|
12
|
+
|
|
10
13
|
# The following direct association is used to securely delete associated fields
|
|
11
14
|
# Infact via `fieldable` the associated fields might not be deleted
|
|
12
15
|
# as the fieldable_id is related to the `component` rather than the `field_setting`
|
|
@@ -1,8 +1,245 @@
|
|
|
1
1
|
module Binda
|
|
2
|
+
# `Binda::Selection` class provides support for **selection**, **radio** and **checkbox** fields.
|
|
3
|
+
#
|
|
4
|
+
# More specifically `Binda::Selection` is used for **selection** fields, whereas **radio** and
|
|
5
|
+
# **checkbox** fields depend on `Binda::Radio` and `Binda::Checkbox` which are subclasses of `Binda::Selection`.
|
|
6
|
+
#
|
|
7
|
+
# The architecture behind this class is pretty complex and deserves some attention.
|
|
8
|
+
# Here the rules that defines the class behaviour:
|
|
9
|
+
#
|
|
10
|
+
# 1. Every selection can have multiple **choices** (see `Binda::Choice`) or none.
|
|
11
|
+
#
|
|
12
|
+
# 2. Every **selection** depends on a **field setting** (see `Binda::FieldSetting`
|
|
13
|
+
# [documentation](http://www.rubydoc.info/gems/binda/Binda/FieldSetting)) which specify its behaviour.
|
|
14
|
+
#
|
|
15
|
+
# 3. Every time you create a **field setting** a fallback ensure a **selection** exists for every
|
|
16
|
+
# **component** or **board** to which this **field setting** belongs. This ensure calling `get_selection_choices`
|
|
17
|
+
# method doesn't throw a error.
|
|
18
|
+
# ```ruby
|
|
19
|
+
# # Create a field setting for a component
|
|
20
|
+
# component = Binda::Component.first
|
|
21
|
+
# field_setting = component.structure.field_groups.first.field_settings.create(
|
|
22
|
+
# name: 'my selection',
|
|
23
|
+
# field_type: 'selection'
|
|
24
|
+
# )
|
|
25
|
+
# # Reload component so the Active Record object is updated
|
|
26
|
+
# component.reload
|
|
27
|
+
# # We know for sure a selection exists
|
|
28
|
+
# component.selections.any?
|
|
29
|
+
# # => true
|
|
30
|
+
# # We might not find any choice though
|
|
31
|
+
# component.get_selection_choices(field_setting.slug)
|
|
32
|
+
# # => []
|
|
33
|
+
# ```
|
|
34
|
+
#
|
|
35
|
+
# 4. When a **field setting** is created there is no **choice** available yet. Only if a **setting** requires
|
|
36
|
+
# at least a **choice**, a new **choice** is created. In that case the **choice** is also automatically set as
|
|
37
|
+
# the **default choice** for the **field setting** and applied to all **selections** that didn't have any **choice**.
|
|
38
|
+
# Again, this behaviour exists just for **field settings** that requires at least a **choice** and has just
|
|
39
|
+
# been created. Changing the **field setting** **default choice** persisted on the database with another one
|
|
40
|
+
# won't change the **selected choice** of any **selections** created before: they will keep the previous
|
|
41
|
+
# **default choice**. For more info look at `Binda::Choice`
|
|
42
|
+
# [documentation](http://www.rubydoc.info/gems/binda/Binda/Choice)
|
|
43
|
+
# ```ruby
|
|
44
|
+
# # Create a field setting for a component (note: allow_null is set to false)
|
|
45
|
+
# component = Binda::Component.first
|
|
46
|
+
# field_setting = component.structure.field_groups.first.field_settings.create(
|
|
47
|
+
# name: 'my selection',
|
|
48
|
+
# field_type: 'selection',
|
|
49
|
+
# allow_null: false # IMPORTANT: this means field setting requires at least a choice
|
|
50
|
+
# )
|
|
51
|
+
# # If field setting doesn't allow null, it means we always expect a choice to be selected
|
|
52
|
+
# component.get_selection_choices(field_setting.slug)
|
|
53
|
+
# # A default choice is returned
|
|
54
|
+
# # => [{ label: 'Temporary choice', value: 'temporary-choice' }]
|
|
55
|
+
# component.get_selection_choices(field_setting.slug)
|
|
56
|
+
# # => [{ label: 'Temporary choice', value: 'temporary-choice' }]
|
|
57
|
+
# # When we remove the initial choice we fallback to default
|
|
58
|
+
# field_setting.choices.first.delete
|
|
59
|
+
# # => error!
|
|
60
|
+
# field_setting.choices.create(
|
|
61
|
+
# label: 'second',
|
|
62
|
+
# value: 'second'
|
|
63
|
+
# )
|
|
64
|
+
# field_setting.choices.first.destroy
|
|
65
|
+
# component.get_selection_choices(field_setting.slug)
|
|
66
|
+
# # => [{ label: 'second', value: 'second' }]
|
|
67
|
+
# ```
|
|
68
|
+
#
|
|
69
|
+
# 5. Every time a **field setting** is updated a fallback ensures all **selections** relying on it are updated as well.
|
|
70
|
+
# ```ruby
|
|
71
|
+
# # Create a field setting for a component
|
|
72
|
+
# component = Binda::Component.first
|
|
73
|
+
# field_setting = component.structure.field_groups.first.field_settings.create(
|
|
74
|
+
# name: 'my selection',
|
|
75
|
+
# field_type: 'selection',
|
|
76
|
+
# allow_null: false
|
|
77
|
+
# )
|
|
78
|
+
# field_setting.choices.create([
|
|
79
|
+
# {
|
|
80
|
+
# label: 'first',
|
|
81
|
+
# value: 'first'
|
|
82
|
+
# },
|
|
83
|
+
# {
|
|
84
|
+
# label: 'second',
|
|
85
|
+
# value: 'second'
|
|
86
|
+
# },
|
|
87
|
+
# ])
|
|
88
|
+
# # field_setting automatically assigns **first choice** as default and replace the **temporary default choice**.
|
|
89
|
+
# # Now reload component to update it with the instances just created
|
|
90
|
+
# component.reload
|
|
91
|
+
# component.selections.first.choices << field_setting.choices.first
|
|
92
|
+
# component.selections.first.choices << field_setting.choices.last
|
|
93
|
+
# component.get_selection_choices(field_setting.slug)
|
|
94
|
+
# # => [{ label: 'first', value: 'first' }, { label: 'second', value: 'second' }]
|
|
95
|
+
# # Remove the initial choice
|
|
96
|
+
# field_setting.choices.first.delete
|
|
97
|
+
# # Get selection choices again. Now you should get a different result
|
|
98
|
+
# component.reload.get_selection_choices(field_setting.slug)
|
|
99
|
+
# # => [{ label: 'second', value: 'second' }]
|
|
100
|
+
# ```
|
|
101
|
+
#
|
|
102
|
+
# 6. (This is the trickiest)
|
|
103
|
+
# Assuming that
|
|
104
|
+
# 1) **field setting** requires at least a **choice**,
|
|
105
|
+
# 2) there is only one **choice** available,
|
|
106
|
+
# 3) some **selections** are associated to that **choice**.
|
|
107
|
+
# If the user replace that **choice** with another one, all **selections** fields will be associated to
|
|
108
|
+
# the new one.
|
|
109
|
+
# ```ruby
|
|
110
|
+
# # Create a field setting for a component
|
|
111
|
+
# component = Binda::Component.first
|
|
112
|
+
# field_setting = component.structure.field_groups.first.field_settings.create(
|
|
113
|
+
# name: 'my selection',
|
|
114
|
+
# field_type: 'selection',
|
|
115
|
+
# allow_null: false
|
|
116
|
+
# )
|
|
117
|
+
# # Trying to delete the default temporary choice, which is the only one, won't work
|
|
118
|
+
# field_setting.choices.first.destroy
|
|
119
|
+
# # => false
|
|
120
|
+
# # You need to create another choice first
|
|
121
|
+
# field_setting.choices.create(
|
|
122
|
+
# label: 'Second',
|
|
123
|
+
# value: 'second'
|
|
124
|
+
# )
|
|
125
|
+
# field_setting.choices.first.destroy
|
|
126
|
+
# component.reload.get_selection_choices(field_setting.slug)
|
|
127
|
+
# # => [{ label: 'second', value: 'second' }]
|
|
128
|
+
# # BE AWARE THAT:
|
|
129
|
+
# field_setting.choices.delete_all
|
|
130
|
+
# # => deletes everything skipping callbacks and will cause inconsistency throughout the database
|
|
131
|
+
# ```
|
|
132
|
+
#
|
|
2
133
|
class Selection < ApplicationRecord
|
|
3
134
|
|
|
4
135
|
has_and_belongs_to_many :choices
|
|
5
136
|
belongs_to :field_setting
|
|
6
137
|
|
|
138
|
+
validate :has_choices
|
|
139
|
+
validates :fieldable_id, presence: true
|
|
140
|
+
|
|
141
|
+
after_save do
|
|
142
|
+
if !self.field_setting.allow_null &&
|
|
143
|
+
!self.field_setting.default_choice_id.nil? &&
|
|
144
|
+
!self.choices.any?
|
|
145
|
+
assign_default_choice
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Fix selections which have no choice even though the field setting requires at least one
|
|
150
|
+
#
|
|
151
|
+
# This method gets all `Binda::Selection` records that depend on a field setting which
|
|
152
|
+
# requires that the selection has at least one choice and which haven't any choice selected.
|
|
153
|
+
# This method is used on the user interface and a rake task.
|
|
154
|
+
#
|
|
155
|
+
# Raise an error if the related field setting doesn't have any choice or the default choice isn't selected
|
|
156
|
+
def self.add_default_choice_to_all_selections_with_no_choices(field_setting = nil)
|
|
157
|
+
# Get all Binda::Selection records that:
|
|
158
|
+
# - depend on a field setting which requires to have at least a choice
|
|
159
|
+
# - have no choice selected
|
|
160
|
+
|
|
161
|
+
selections = Selection.get_selections_which_must_have_a_choice_but_have_none(field_setting)
|
|
162
|
+
selections.each do |selection|
|
|
163
|
+
unless selection.field_setting.default_choice_id.nil?
|
|
164
|
+
selection.assign_default_choice
|
|
165
|
+
else
|
|
166
|
+
raise "Binda::Selection with id=\"#{selection.id}\" cannot be fixed because its field setting doesn't have any available choice. Please add at least a choice to field setting \"#{selection.field_setting.slug}\"."
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Assign default choice to a selection
|
|
172
|
+
def assign_default_choice
|
|
173
|
+
self.choices << self.field_setting.default_choice
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Check all Binda::Selection records which are required to have a choice.
|
|
177
|
+
#
|
|
178
|
+
# The purpose of this method is to check, not to update. You don't won't to decide which choice
|
|
179
|
+
# must be selected if the selection is required to have one. You just want the user to know that
|
|
180
|
+
# it's not possible to leave it without any.
|
|
181
|
+
#
|
|
182
|
+
# @param field_setting [ActiveRecord Object]
|
|
183
|
+
def self.check_all_selections_depending_on(field_setting)
|
|
184
|
+
# Make sure Active Record object of field setting is updated
|
|
185
|
+
field_setting.reload
|
|
186
|
+
|
|
187
|
+
# Don't bother if field setting allow having no choice or if default_choice isn't set
|
|
188
|
+
return if field_setting.allow_null? || field_setting.default_choice_id.nil?
|
|
189
|
+
|
|
190
|
+
# Get all selection related to this field setting which have an issue with choices
|
|
191
|
+
selections = Selection.get_selections_which_must_have_a_choice_but_have_none(field_setting)
|
|
192
|
+
|
|
193
|
+
# Warn the user that there's a problem
|
|
194
|
+
selections.each do |selection|
|
|
195
|
+
selection.choices << field_setting.default_choice
|
|
196
|
+
unless selection.save
|
|
197
|
+
raise "It hasn't been possible to assign Binda::Choice with id=\"#{field_setting.default_choice_id}\" to Binda::Selection with id=\"#{self.id}\"."
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Get selections which must have a choice, but have none
|
|
203
|
+
#
|
|
204
|
+
# Get all Binda::Selection records that:
|
|
205
|
+
# - depend on a field setting which requires to have at least a choice
|
|
206
|
+
# - have no choice selected
|
|
207
|
+
#
|
|
208
|
+
# @param field_setting_slug [string] Add the slug of a field setting to filter the query
|
|
209
|
+
#
|
|
210
|
+
# @return [array] An array of Binda::Selection objects
|
|
211
|
+
def self.get_selections_which_must_have_a_choice_but_have_none(field_setting = nil)
|
|
212
|
+
if field_setting.nil?
|
|
213
|
+
Selection.includes(:choices, :field_setting)
|
|
214
|
+
.where(
|
|
215
|
+
binda_choices_selections: { selection_id: nil },
|
|
216
|
+
binda_field_settings: { allow_null: false }
|
|
217
|
+
)
|
|
218
|
+
else
|
|
219
|
+
field_setting_id = FieldSetting.get_id(field_setting.slug)
|
|
220
|
+
Selection.includes(:choices, :field_setting)
|
|
221
|
+
.where(
|
|
222
|
+
binda_choices_selections: { selection_id: nil },
|
|
223
|
+
binda_selections: { field_setting_id: field_setting_id },
|
|
224
|
+
binda_field_settings: { allow_null: false }
|
|
225
|
+
)
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Get selection
|
|
230
|
+
def has_choices
|
|
231
|
+
field_setting = self.field_setting
|
|
232
|
+
case
|
|
233
|
+
when self.new_record?
|
|
234
|
+
return true
|
|
235
|
+
when !field_setting.allow_null? && field_setting.choices.any? && self.choices.empty?
|
|
236
|
+
errors.add(:base, I18n.t("binda.selection.validation_message.choices", { arg1: self.id, arg2: field_setting.slug })
|
|
237
|
+
)
|
|
238
|
+
return false
|
|
239
|
+
else
|
|
240
|
+
return true
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
7
244
|
end
|
|
8
245
|
end
|