Class: Binda::Selection

Inherits:
ApplicationRecord show all
Includes:
FieldUniqueness, Fields
Defined in:
app/models/binda/selection.rb

Overview

‘Binda::Selection` class provides support for selection, radio and checkbox fields.

More specifically ‘Binda::Selection` is used for selection fields, whereas radio and

**checkbox** fields depend on `Binda::Radio` and `Binda::Checkbox` which are subclasses of `Binda::Selection`.

The architecture behind this class is pretty complex and deserves some attention.

Here the rules that defines the class behaviour:
  1. Every selection can have multiple choices (see ‘Binda::Choice`) or none.

  2. Every selection depends on a **field setting** (see ‘Binda::FieldSetting`

[documentation](http://www.rubydoc.info/gems/binda/Binda/FieldSetting)) which specify its behaviour.
  1. Every time you create a **field setting** a fallback ensure a selection exists for every

**component** or **board** to which this **field setting** belongs. This ensure calling `get_selection_choices`
method doesn't throw a error.
```ruby
# Create a field setting for a component
component = Binda::Component.first
field_setting = component.structure.field_groups.first.field_settings.create(
    name: 'my selection',
    field_type: 'selection'
)
# Reload component so the Active Record object is updated
component.reload
# We know for sure a selection exists
component.selections.any?
# => true
# We might not find any choice though
component.get_selection_choices(field_setting.slug)
# => []
```
  1. When a **field setting** is created there is no choice available yet. Only if a setting requires

at least a **choice**, a new **choice** is created. In that case the **choice** is also automatically set as 
the **default choice** for the **field setting** and applied to all **selections** that didn't have any **choice**. 
Again, this behaviour exists just for **field settings** that requires at least a **choice** and has just 
been created. Changing the **field setting** **default choice** persisted on the database with another one 
won't change the **selected choice** of any **selections** created before: they will keep the previous 
**default choice**. For more info look at `Binda::Choice` 
[documentation](http://www.rubydoc.info/gems/binda/Binda/Choice)
```ruby
# Create a field setting for a component (note: allow_null is set to false)
component = Binda::Component.first
field_setting = component.structure.field_groups.first.field_settings.create(
    name: 'my selection',
    field_type: 'selection', 
    allow_null: false # IMPORTANT: this means field setting requires at least a choice
)
# If field setting doesn't allow null, it means we always expect a choice to be selected
component.get_selection_choices(field_setting.slug)
# A default choice is returned
# => [{ label: 'Temporary choice', value: 'temporary-choice' }]
component.get_selection_choices(field_setting.slug)
# => [{ label: 'Temporary choice', value: 'temporary-choice' }]
# When we remove the initial choice we fallback to default
field_setting.choices.first.delete
# => error!
field_setting.choices.create(
    label: 'second', 
    value: 'second'
)
field_setting.choices.first.destroy
component.get_selection_choices(field_setting.slug)
# => [{ label: 'second', value: 'second' }]
```
  1. Every time a **field setting** is updated a fallback ensures all selections relying on it are updated as well.

```ruby
# Create a field setting for a component
component = Binda::Component.first
field_setting = component.structure.field_groups.first.field_settings.create(
    name: 'my selection',
    field_type: 'selection', 
    allow_null: false
)
field_setting.choices.create([
    {
        label: 'first', 
        value: 'first'
    },
    {
        label: 'second', 
        value: 'second'
    },
])
# field_setting automatically assigns **first choice** as default and replace the **temporary default choice**.
# Now reload component to update it with the instances just created
component.reload
component.selections.first.choices << field_setting.choices.first
component.selections.first.choices << field_setting.choices.last
component.get_selection_choices(field_setting.slug)
# => [{ label: 'first', value: 'first' }, { label: 'second', value: 'second' }]
# Remove the initial choice
field_setting.choices.first.delete
# Get selection choices again. Now you should get a different result
component.reload.get_selection_choices(field_setting.slug)
# => [{ label: 'second', value: 'second' }]
```
  1. (This is the trickiest)

Assuming that 
1) **field setting** requires at least a **choice**, 
2) there is only one **choice** available, 
3) some **selections** are associated to that **choice**. 
If the user replace that **choice** with another one, all **selections** fields will be associated to 
the new one.
```ruby
# Create a field setting for a component
component = Binda::Component.first
field_setting = component.structure.field_groups.first.field_settings.create(
    name: 'my selection',
    field_type: 'selection', 
    allow_null: false
)
# Trying to delete the default temporary choice, which is the only one, won't work
field_setting.choices.first.destroy
# => false
# You need to create another choice first
field_setting.choices.create(
    label: 'Second', 
    value: 'second'
)
field_setting.choices.first.destroy
component.reload.get_selection_choices(field_setting.slug)
# => [{ label: 'second', value: 'second' }]
# BE AWARE THAT:
field_setting.choices.delete_all
# => deletes everything skipping callbacks and will cause inconsistency throughout the database
```

Direct Known Subclasses

Checkbox, Radio

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FieldUniqueness

#field_uniqueness

Class Method Details

.add_default_choice_to_all_selections_with_no_choices(field_setting = nil) ⇒ Object

Fix selections which have no choice even though the field setting requires at least one

This method gets all ‘Binda::Selection` records that depend on a field setting which

requires that the selection has at least one choice and which haven't any choice selected. 
This method is used on the user interface and a rake task.

Raise an error if the related field setting doesn’t have any choice or the default choice isn’t selected



156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'app/models/binda/selection.rb', line 156

def self.add_default_choice_to_all_selections_with_no_choices(field_setting = nil)
	# Get all Binda::Selection records that:
	# - depend on a field setting which requires to have at least a choice
	# - have no choice selected

	selections = Selection.get_selections_which_must_have_a_choice_but_have_none(field_setting)
	selections.each do |selection|
		unless selection.field_setting.default_choice_id.nil?
			selection.assign_default_choice
		else
			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}\"."
		end
	end
end

.check_all_selections_depending_on(field_setting) ⇒ Object

Check all Binda::Selection records which are required to have a choice.

Parameters:

  • field_setting (ActiveRecord Object)


179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'app/models/binda/selection.rb', line 179

def self.check_all_selections_depending_on(field_setting)
	# Make sure Active Record object of field setting is updated
	field_setting.reload

	# Don't bother if field setting allow having no choice or if default_choice isn't set
	return if field_setting.allow_null? || !field_setting.default_choice.present?

	# Get all selection related to this field setting which have an issue with choices
	selections = Selection.get_selections_which_must_have_a_choice_but_have_none(field_setting)

	# Warn the user that there's a problem
	selections.each do |selection|
		selection.choices << field_setting.default_choice
		unless selection.save
			raise "It hasn't been possible to assign Binda::Choice with id=\"#{field_setting.default_choice_id}\" to Binda::Selection with id=\"#{self.id}\"."
		end
	end
end

.get_selections_which_must_have_a_choice_but_have_none(field_setting = nil) ⇒ array

Get selections which must have a choice, but have none

Get all Binda::Selection records that:

  • depend on a field setting which requires to have at least a choice

  • have no choice selected

Parameters:

  • field_setting (string) (defaults to: nil)

    Add the slug of a field setting to filter the query

Returns:

  • (array)

    An array of Binda::Selection objects



207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
# File 'app/models/binda/selection.rb', line 207

def self.get_selections_which_must_have_a_choice_but_have_none(field_setting = nil)
	if field_setting.nil?
		Selection.includes(:choices, :field_setting)
			.where(
				binda_choices_selections: { selection_id: nil },
				binda_field_settings: { allow_null: false }
			)
	else
		field_setting_id = FieldSetting.get_id(field_setting.slug)
		Selection.includes(:choices, :field_setting)
			.where(
				binda_choices_selections: { selection_id: nil }, 
				binda_selections: { field_setting_id: field_setting_id },
				binda_field_settings: { allow_null: false }
			)
	end
end

Instance Method Details

#assign_default_choiceObject

Assign default choice to a selection



172
173
174
# File 'app/models/binda/selection.rb', line 172

def assign_default_choice
	self.choices << self.field_setting.default_choice
end

#has_choicesObject

Get selection



226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'app/models/binda/selection.rb', line 226

def has_choices
	field_setting = self.field_setting
	case 
	when self.new_record?
		return true
	when !field_setting.allow_null? && field_setting.choices.any? && self.choices.empty?
		errors.add(:base, I18n.t("binda.selection.validation_message.choices", { arg1: self.id, arg2: field_setting.slug })
		)
		return false
	else
		return true
	end
end