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.
Files changed (132) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +83 -25
  3. data/app/assets/javascripts/binda/application.js +3 -3
  4. data/app/assets/javascripts/binda/components/bootstrap.js +3 -4
  5. data/app/assets/javascripts/binda/components/field_group_editor.js +10 -10
  6. data/app/assets/javascripts/binda/components/field_setting_choices.js +61 -49
  7. data/app/assets/javascripts/binda/components/fileupload.js +135 -118
  8. data/app/assets/javascripts/binda/components/form_item.js +65 -65
  9. data/app/assets/javascripts/binda/components/form_item_editor.js +19 -19
  10. data/app/assets/javascripts/binda/components/form_item_image.js +11 -13
  11. data/app/assets/javascripts/binda/components/form_item_repeater.js +77 -71
  12. data/app/assets/javascripts/binda/components/login-shader.js +171 -164
  13. data/app/assets/javascripts/binda/components/login_form.js +65 -73
  14. data/app/assets/javascripts/binda/components/radio-toggle.js +8 -12
  15. data/app/assets/javascripts/binda/components/select2.js +19 -14
  16. data/app/assets/javascripts/binda/components/sortable.js +76 -71
  17. data/app/assets/javascripts/binda/dist/binda.bundle.js +735 -727
  18. data/app/assets/javascripts/binda/index.js +49 -35
  19. data/app/assets/stylesheets/binda/components/assets_manager.scss +13 -22
  20. data/app/assets/stylesheets/binda/components/b-alert.scss +18 -14
  21. data/app/assets/stylesheets/binda/components/b-btn.scss +24 -43
  22. data/app/assets/stylesheets/binda/components/field_setting_choices.scss +16 -31
  23. data/app/assets/stylesheets/binda/components/fileupload.scss +25 -42
  24. data/app/assets/stylesheets/binda/components/form_item.scss +51 -93
  25. data/app/assets/stylesheets/binda/components/form_item_choices.scss +7 -10
  26. data/app/assets/stylesheets/binda/components/login.scss +2 -2
  27. data/app/assets/stylesheets/binda/components/main_header.scss +5 -10
  28. data/app/assets/stylesheets/binda/components/main_sidebar.scss +42 -46
  29. data/app/assets/stylesheets/binda/components/main_sortable_table.scss +12 -21
  30. data/app/assets/stylesheets/binda/components/main_table.scss +18 -35
  31. data/app/assets/stylesheets/binda/components/popup_warning.scss +14 -27
  32. data/app/assets/stylesheets/binda/components/select2.scss +46 -48
  33. data/app/assets/stylesheets/binda/components/sortable.scss +25 -45
  34. data/app/assets/stylesheets/binda/components/standard-form.scss +43 -73
  35. data/app/assets/stylesheets/binda/controllers/users_sessions_new.scss +52 -89
  36. data/app/assets/stylesheets/binda/index.scss +0 -1
  37. data/app/assets/stylesheets/binda/settings/buttons.scss +9 -10
  38. data/app/assets/stylesheets/binda/settings/common.scss +17 -22
  39. data/app/assets/stylesheets/binda/settings/fonts.scss +112 -67
  40. data/app/assets/stylesheets/binda/settings/tiny_mce_overrides.scss +20 -36
  41. data/app/assets/stylesheets/binda/settings/variables.scss +38 -43
  42. data/app/controllers/binda/choices_controller.rb +14 -11
  43. data/app/controllers/binda/components_controller.rb +6 -4
  44. data/app/controllers/binda/structures_controller.rb +7 -3
  45. data/app/helpers/binda/components_helper.rb +69 -3
  46. data/app/helpers/binda/field_groups_helper.rb +16 -6
  47. data/app/helpers/binda/structures_helper.rb +1 -4
  48. data/app/models/binda/application_record.rb +4 -1
  49. data/app/models/binda/asset.rb +3 -1
  50. data/app/models/binda/b.rb +1 -0
  51. data/app/models/binda/category.rb +1 -0
  52. data/app/models/binda/checkbox.rb +2 -0
  53. data/app/models/binda/choice.rb +74 -41
  54. data/app/models/binda/component.rb +1 -1
  55. data/app/models/binda/date.rb +4 -0
  56. data/app/models/binda/deprecation.rb +7 -0
  57. data/app/models/binda/field_group.rb +16 -3
  58. data/app/models/binda/field_setting.rb +168 -41
  59. data/app/models/binda/image.rb +1 -0
  60. data/app/models/binda/radio.rb +2 -0
  61. data/app/models/binda/relation.rb +3 -0
  62. data/app/models/binda/repeater.rb +3 -0
  63. data/app/models/binda/selection.rb +237 -0
  64. data/app/models/binda/string.rb +4 -0
  65. data/app/models/binda/structure.rb +25 -14
  66. data/app/models/binda/text.rb +9 -0
  67. data/app/models/binda/video.rb +1 -0
  68. data/app/models/concerns/binda/default_helpers.rb +40 -31
  69. data/app/models/concerns/binda/deprecations.rb +6 -0
  70. data/app/models/concerns/binda/fieldable_association_helpers.rb +366 -0
  71. data/app/models/concerns/binda/fieldable_associations.rb +32 -369
  72. data/app/views/binda/boards/edit.html.erb +15 -2
  73. data/app/views/binda/categories/_form.html.erb +24 -51
  74. data/app/views/binda/categories/edit.html.erb +23 -3
  75. data/app/views/binda/categories/index.html.erb +49 -25
  76. data/app/views/binda/categories/new.html.erb +21 -2
  77. data/app/views/binda/components/edit.html.erb +27 -4
  78. data/app/views/binda/components/index.html.erb +47 -50
  79. data/app/views/binda/components/new.html.erb +12 -2
  80. data/app/views/binda/components/sort_index.html.erb +28 -13
  81. data/app/views/binda/field_groups/_form_body.html.erb +43 -82
  82. data/app/views/binda/field_groups/_form_item.html.erb +3 -120
  83. data/app/views/binda/field_groups/_form_section.html.erb +11 -16
  84. data/app/views/binda/field_groups/_form_section_repeater.html.erb +7 -15
  85. data/app/views/binda/field_groups/edit.html.erb +14 -2
  86. data/app/views/binda/field_groups/form_item/_form_item_choice_editor.html.erb +11 -0
  87. data/app/views/binda/field_groups/form_item/_form_item_editor.html.erb +14 -0
  88. data/app/views/binda/field_groups/form_item/_form_item_header.html.erb +25 -0
  89. data/app/views/binda/field_groups/form_item/_form_item_new_editor.html.erb +8 -0
  90. data/app/views/binda/field_groups/form_item/_form_item_persisted_editor.html.erb +27 -0
  91. data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_allow_null_choice.html.erb +11 -0
  92. data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_choice_header.html.erb +11 -0
  93. data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_default_choice.html.erb +11 -0
  94. data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_new_choice.html.erb +16 -0
  95. data/app/views/binda/field_groups/form_item/form_item_choice/_form_item_persisted_choices.html.erb +16 -0
  96. data/app/views/binda/field_groups/new.html.erb +14 -2
  97. data/app/views/binda/field_settings/_form_body.html.erb +1 -3
  98. data/app/views/binda/field_settings/edit.html.erb +1 -1
  99. data/app/views/binda/field_settings/new.html.erb +1 -1
  100. data/app/views/binda/fieldable/_form_body.html.erb +24 -72
  101. data/app/views/binda/fieldable/_form_item_date.html.erb +1 -4
  102. data/app/views/binda/fieldable/_form_item_image.html.erb +3 -7
  103. data/app/views/binda/fieldable/_form_item_new_repeater.html.erb +0 -13
  104. data/app/views/binda/fieldable/_form_item_selections.html.erb +20 -112
  105. data/app/views/binda/fieldable/form_item_selections/_form_item_checkbox.html.erb +34 -0
  106. data/app/views/binda/fieldable/form_item_selections/_form_item_radio.html.erb +28 -0
  107. data/app/views/binda/fieldable/form_item_selections/_form_item_selection.html.erb +30 -0
  108. data/app/views/binda/manage/users/_form_body.html.erb +1 -31
  109. data/app/views/binda/manage/users/edit.html.erb +12 -2
  110. data/app/views/binda/manage/users/index.html.erb +36 -19
  111. data/app/views/binda/manage/users/new.html.erb +14 -3
  112. data/app/views/binda/structures/_form_body.html.erb +2 -25
  113. data/app/views/binda/structures/_form_section.html.erb +43 -65
  114. data/app/views/binda/structures/_form_sidebar.html.erb +19 -12
  115. data/app/views/binda/structures/edit.html.erb +20 -3
  116. data/app/views/binda/structures/index.html.erb +46 -26
  117. data/app/views/binda/structures/new.html.erb +13 -2
  118. data/app/views/binda/structures/sort_index.html.erb +37 -17
  119. data/app/views/binda/users/sessions/new.html.erb +25 -20
  120. data/app/views/layouts/binda/_form_errors.html.erb +10 -0
  121. data/app/views/layouts/binda/_sidebar.html.erb +6 -6
  122. data/app/views/layouts/binda/application.html.erb +1 -1
  123. data/config/initializers/carrierwave.rb +3 -2
  124. data/config/locales/en.yml +56 -12
  125. data/config/tinymce.yml +2 -2
  126. data/db/migrate/1_create_binda_tables.rb +1 -1
  127. data/lib/binda/version.rb +1 -1
  128. data/lib/generators/binda/setup/setup_generator.rb +2 -2
  129. data/lib/tasks/add_default_choice_to_all_selections_with_no_choices_task.rake +6 -0
  130. metadata +58 -8
  131. data/app/assets/stylesheets/binda/components/form_item_image.scss +0 -0
  132. data/app/views/binda/field_groups/_form_item_choice.erb +0 -104
@@ -26,7 +26,7 @@ module Binda
26
26
 
27
27
  aasm :column => 'publish_state' do
28
28
  state :draft, initial: true
29
- state :published
29
+ state :published
30
30
 
31
31
  event :publish do
32
32
  transitions from: :draft, to: :published
@@ -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
@@ -0,0 +1,7 @@
1
+ module Binda
2
+ class Deprecation < ActiveSupport::Deprecation
3
+ def initialize(deprecation_horizon = 'next major release', gem_name = 'Binda')
4
+ super
5
+ end
6
+ end
7
+ end
@@ -9,8 +9,11 @@ module Binda
9
9
  has_many :field_settings, dependent: :destroy
10
10
 
11
11
  # Validations
12
- validates :name, presence: true
13
- validates :slug, uniqueness: true
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( 'position', self.structure.field_groups.length )
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
- # Is this reallly needed? Or can we just use accepted_structures?
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
- cattr_accessor :field_settings_array
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
- set_allow_null
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
- # Slug
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
- return possible_names
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( field_slug )
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
- def set_allow_null
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
- structure = self.field_group.structure
155
- field_class = self.field_type.pluralize
156
- field_setting_id = self.id
157
- case
158
- when structure.components.any?
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
- # Helper for create_field_instances method
168
- def create_field_instances_for_component( component, field_class, field_setting_id )
169
- unless component.send(field_class).where(field_setting_id: field_setting_id).any?
170
- component.send(field_class).create!(field_setting_id: field_setting_id)
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 create_field_instances_for_board ( board, field_class, field_setting_id )
176
- unless board.send(field_class).where(field_setting_id: field_setting_id).any?
177
- board.send(field_class).create!(field_setting_id: field_setting_id)
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
@@ -1,4 +1,5 @@
1
1
  module Binda
2
+ # This class provides support for uploading images.
2
3
  class Image < Asset
3
4
 
4
5
  mount_uploader :image, ImageUploader
@@ -1,4 +1,6 @@
1
1
  module Binda
2
+ # This class provides support for radio buttons and depends on `Binda::Selection`.
3
+ # You can find more info at the _selection_ [documentation](http://www.rubydoc.info/gems/binda/Binda/Selection)
2
4
  class Radio < Selection
3
5
 
4
6
  end
@@ -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