Use grouped_collection_select to group a model by enum - ruby-on-rails

Say I have two classes
class Recording
belongs_to :conversation
end
class Conversation < ActiveRecord::Base
has_many :recordings
enum status: [ :active, :archived ]
end
And I want to select the conversation stored on the 'Recording' model. How can I use grouped_collection_select to group all of the records in the Conversation table by the two enums, active and archived in this case?
All of the examples I can find about grouped_collection_select refer to calling method on members to provide a collection; versus grouping an existing collection.

The grouped_collection_select method is not the best tool for your needs because it really deals with associated records of a main record, whereas you, if I understand correctly, simply want to add all Conversation records to a select tag, but grouped by its attribute, not association.
You can easily construct the grouped options manually, though. I'd put this code into a helper not to clutter the view template too much:
# app/helpers/conversations_helper.rb
module ConversationsHelper
def grouped_conversations_options_for_select(selected_conversation_id = nil)
options = {}
Conversation.statuses.keys.each do |status|
options[status] = Conversation.with_status(status).pluck(:name, :id)
end
grouped_options_for_select(options, selected_conversation_id)
end
end
# app/view/show.html.erb
<%= select_tag :conversation_id, grouped_conversations_options_for_select(params[:conversation_id]) %>
The helper first constructs a hash with the following structure:
{
active: [[ conversation.id, conversation.name ], [...]],
archived: [[ conversation.id, conversation.name ], [...]],
}
This hash can be then passed to the grouped_options_for_select which converts it to the <OPTIONS> tags including proper <OPTGROUP> tags. The helper also supports setting the currently selected value in the select options. Its output is then passed to the select tag in the view template.
For the helper to work, you also need to add the following scope to the Conversation model:
# app/models/conversation.rb
scope :with_status, ->(status) { where(status: statuses[status]) }

Related

Rails accepts_nested_attributes_for with belongs_to. Why I can't set id?

I use Rails 5.1.6 and have troubles with accepts_nested_attributes_for.
I have two models
class Material < ApplicationRecord
belongs_to :rubric, optional: true
accepts_nested_attributes_for :rubric
end
class Rubric < ApplicationRecord
has_many :materials, dependent: :nullify
end
I try to set rubric id to new item by rubric_attributes.
describe 'create material' do
it 'should set rubric: :id' do
# prepare
item = FactoryBot.build(:material)
rubric = FactoryBot.create(:rubric)
# action
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
# check
expect(item.valid?).to eq(true)
expect(item.save).to eq(true)
expect(item.rubric_id).to eq(rubric.id)
end
end
But I have an error:
Failure/Error:
item.assign_attributes(
rubric_attributes: {
id: rubric.id
}
)
ActiveRecord::RecordNotFound:
Couldn't find Rubric with ID=1 for Material with ID=1
And I have the same error with updating a material.
Is it a predictable behavior of accepts_nested_attributes_for, and I can't use rubric_attributes for setting existed rubric id?
Docs say:
For each hash that does not have an id key a new record will be instantiated, unless the hash also contains a _destroy key that evaluates to true.
It suggest that if you pass id in nested attributes, it's treated as an existing record that should be updated.
You most likely don't need accepts_nested_attributes_for in the first place.
If you want the user to be able to select records with a select you don't actually need to do anything besides create a select and whitelist the material_id attribute:
<%= form_for(#material) do |f| %>
<div class="field">
<%= f.label :rubic_id %>
<%= f.collection_select :rubic_id, Rubic.all :id, :name %>
</div>
<%= f.submit %>
<% end %>
The select will create an array in the params.
class MaterialsController
# POST /materials
def create
#material = Material.new(material_params)
if #material.save
redirect_to #material
else
render :new
end
end
private
def material_params
params.require(:material)
.permit(:foo, :bar, material_ids: [])
end
end
accepts_nested_attributes_for is really intended for the case where you need to create / edit nested resources in the same request. The only reason you would use it here is:
The user should be able to create the material in the same form.
You have a join table with additional attributes (like quantity for example) which you want the user to be able to set.
You can still do 1. together with the select above, but you can't use accepts_nested_attributes_for to set a simple belongs_to association. Nor would you want to either as its like using a rocket to beat in a nail.
Just leaving this in case somebody else may have a problem as I did, populating nested children records in a Rails backend via an API, but using hash_ids via friendly_id.
Came about this when trying to PATCH Rails records via an API. First setup was to mirror the Rails way of sending the record values in nested form fashion. Meaning, I've purposefully built the params hash I was sending from the frontend over to a Rails backend like in a typical nested form transmission:
{ "children": {
"0": {
"name": "foo",
"category_id": "1",
"hash_id": "HDJPQT"
}
}
accepts_nested_attributes_for needs id to PATCH records. Otherwise it is going to create a new record altogether. Which i did not want in my scenario. I was sending over hash_id and therefore creating new records unintentionally.
Solution
For the time being I am not replicating a nested form value hash anymore to send to the Rails backend anymore. Instead I am simply updating children records separately in a chained fetch query from the Javascript frontend.
Note:
If you want to keep sending a mirrored nested form array of hashes, there could be a way to change the primary key of the database table to hash_id or UUID, depending on your needs. Have not yet tested this solution.

Rails 4 - find nested attributes, select certain child

I have and object with a nested model. I am currently getting all the nested objects like so:
#no = Parent.find(params[:parent_id]).children
Now, one of these children has an attribute that identifies them as the favorite. How can I get the favorite child from among the children?
In addition, how can I edit the attributes using fields_for for just that single object in the view/update?
I don't know the name of your attribute that identifies the record as the favorite, but let's say it is a boolean named is_favorite. Considering this abose, the following should work:
children = Parent.find(params[:parent_id]).children
#favorited_children = children.where(is_favorite: true) # return 0..N records! not only 0..1 !
To edit its attributes, you can do as following (you will have to translate it in ERB or HAML, depending on what your app uses):
form_for #favorited_children do |form_builder|
form_builder.text_field :name
form_builder.check_box :is_favorite
end
Hope this helps!
You could also look at using an ActiveRecord Association Extension
This basically works by creating instance methods you can chain onto the child association, like so:
#app/models/parent.rb
Class Parent < ActiveRecord::Base
has_many :children do
def favorites
where(is_favorite: true) #-> to use MrYoshi's example
end
end
end
This will allow you to use the following:
#parent = Parent.find params[:id]
#favorites = #parent.children.favorites

Best Practice for Helper/Partial that render a Model driven select

Let's assume I have a model Product, and I want a drop-down select box that contains all products. This drop-down is used in several views, so it is going to be created by a helper method. Where is the 'best practice' location to get the select options from Product? Do I set #products = Product.all in every controller action that needs to show the drop-down, or do I make the helper method self contained by having it call Product.all? Does the answer change if I am dealing with a partial, or if I am filtering the products (i.e. Product.in_category(#category))? MVC says use the controller, but DRY says use the helper.
Look at the collection_select form helper that's built in. You can pass in different collections (Product.all, Product.) as and where needed in different views.
collection_select
From the link:
collection_select(object, method, collection, value_method,
text_method, options = {}, html_options = {})
Returns and tags for the collection of existing
return values of method for object‘s class. The value returned from
calling method on the instance object will be selected. If calling
method returns nil, no selection is made without including :prompt or
:include_blank in the options hash.
The :value_method and :text_method parameters are methods to be called
on each member of collection. The return values are used as the value
attribute and contents of each tag, respectively. They can
also be any object that responds to call, such as a proc, that will be
called for each member of the collection to retrieve the value/text.
Example object structure for use with this method:
class Post < ActiveRecord::Base belongs_to :author end
class Author < ActiveRecord::Base has_many :posts def
name_with_initial
"#{first_name.first}. #{last_name}" end end
Sample usage (selecting the associated Author for an instance of Post,
#post):
collection_select(:post, :author_id, Author.all, :id,
:name_with_initial, prompt: true)
If #post.author_id is already 1, this would return:
Please
select D. Heinemeier
Hansson D. Thomas M. Clark
In my opinion, Controller should decide what data the user sees. How the user sees it can be decided by the view or by the helper.
So i would advise you to put
#products = Product.all
or
Product.in_category(#category)
in your controller
Any kind of filter you apply should be done in the controller as well
With rails being a model-view-controller (MVC) framework, you're going to want that logic to be on the model. Having some method that returns the options for your select would probably be best (though, take that with a grain of salt because these things change a lot with application). Something I might try would be along the lines of:
class Product < ActiveRecord::Base
def self.get_select_options(category=nil)
if category.nil?
Product.all
else
Product.in_category(category)
end
end
end
... which you could then call with Product.get_select_options or Product.get_select_options(#category)

ActiveRecord includes. Specify included columns

I have model Profile. Profile has_one User. User model has field email. When I call
Profile.some_scope.includes(:user)
it calls
SELECT users.* FROM users WHERE users.id IN (some ids)
But my User model has many fields that I am not using in rendering. Is it possible to load only emails from users? So, SQL should look like
SELECT users.email FROM users WHERE users.id IN (some ids)
Rails doesn't have the facility to pass the options for include query. But we can pass these params with the association declaration under the model.
For your scenario, you need to create a new association with users model under the profile model, like below
belongs_to :user_only_fetch_email, :select => "users.id, users.email", :class_name => "User"
I just created one more association but it points to User model only. So your query will be,
Profile.includes(:user_only_fetch_email)
or
Profile.includes(:user_only_fetch_email).find(some_profile_ids)
If you want to select specific attributes, you should use joins rather than includes.
From this asciicast:
the include option doesn’t really work with the select option as we don’t have control over how the first part of the SELECT statement is generated. If you need control over the fields in the SELECT then you should use joins over include.
Using joins:
Profile.some_scope.joins(:users).select("users.email")
You need extra belongs to in the model.
For simple association:
belongs_to :user_restricted, -> { select(:id, :email) }, class_name: 'User'
For Polymorphic association (for example, :commentable):
belongs_to :commentable_restricted, -> { select(:id, :title) }, polymorphic: true, foreign_type: :commentable_type, foreign_key: :commentable_id
You can choose whatever belongs_to name you want. For the examples given above, you can use them like Article.featured.includes(:user_restricted), Comment.recent.includes(:commentable_restricted) etc.
Rails does not support to select specific columns when includes. You know ,it's just lazy load.
It use the ActiveRecord::Associations::Preloader module to load the associated data before data actually using. By the method:
def preload(records, associations, preload_scope = nil)
records = Array.wrap(records).compact
if records.empty?
[]
else
records.uniq!
Array.wrap(associations).flat_map { |association|
preloaders_on association, records, preload_scope
}
end
end
preload_scope the third params of preload, is a way to select specify columns. But can't lazy load anymore.
At Rails 5.1.6
relation = Profile.where(id: [1,2,3])
user_columns = {:select=>[:updated_at, :id, :name]}
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(relation, :user, user_columns)
It will select the specify columns you passed in. But, it just for single association. You need create a patch for ActiveRecord::Associations::Preloader to support loading multiple complex associations at once.
Here is a example for patch
The way to use it, example
I wanted that functionality myself,so please use it.
Include this method in your class
#ACCEPTS args in string format "ASSOCIATION_NAME:COLUMN_NAME-COLUMN_NAME"
def self.includes_with_select(*m)
association_arr = []
m.each do |part|
parts = part.split(':')
association = parts[0].to_sym
select_columns = parts[1].split('-')
association_macro = (self.reflect_on_association(association).macro)
association_arr << association.to_sym
class_name = self.reflect_on_association(association).class_name
self.send(association_macro, association, -> {select *select_columns}, class_name: "#{class_name.to_sym}")
end
self.includes(*association_arr)
end
And you will be able to call like: Contract.includes_with_select('user:id-name-status', 'confirmation:confirmed-id'), and it will select those specified columns.
Using Mohanaj's example, you can do this:
belongs_to :user_only_fetch_email, -> { select [:id, :email] }, :class_name => "User"

Rails sorting child objects and displaying

Relating to my last question here: Rails: Finding all associated objects to a parent object
Is it possible to sort multiple separate child objects in Rails by creation date, and then list them? Using the previous example I have a resume with two different has_many child objects, I would like to fetch them and then sort them based on creation date and then use that to display them.
I assume that you have two (or more) seperate models for children objects, so your Parent model looks like this:
class Parent < ActiveRecord::Base
has_many :dogs
has_many :cats
end
To sort them and get them generally as children you can write method (similar to #gertas answer):
def children
#children ||= (self.dogs.all + self.cats.all).sort(&:created_at)
end
and put it in Parent model. Then you can use it in controller:
#parent = Parent.find(params[:id])
#children = #parent.children
Now we'll try to display them in a view. I assume that you have created two partials for each model _cat.html.erb and _dog.html.erb. In view:
<h1>Children list:</h1>
<% #parent.children.each do |child| %>
<%= render child %>
<% end %>
It should automaticaly find which partial should be used, but it can be used only if you follow Rails way. If you want to name partials in different way, or store it in different directory, then you would have to write your own methods that will choose correct partial based on type od object.
You can add an accessor method on your parent model:
class Parent < ActiveRecord::Base
def sorted_children
children.scoped( :order => 'created_at DESC' )
# or, in rails3:
# children.order('created_at DESC')
end
end
If the natural order for your child model is the date field and you would like to do that everywhere, then just set a default scope on it:
class Child < ActiveRecord::Base
default_scope :order => 'created_at DESC'
end
As child objects are in different types and they are fetched separately (separate has_many) you have to do sorting in Ruby:
sorted_childs=(#resume.child1_sections.all + #resume.child2_sections.all).sort(&:created_at)
Otherwise you would need to introduce table inheritance with common columns in parent. Then it would be possible to have another has_many for all children with :order.

Resources