has_many through blowing away the association's metadata on mass association - ruby-on-rails

Hey,
Not a Rails noob but this has stumped me.
With has many through associations in Rails. When I mass assign wines to a winebar through a winelist association (or through) table with something like this.
class WineBarController
def update
#winebar = WineBar.find(params[:id])
#winebar.wines = Wine.find(params[:wine_bar][:wine_ids].split(",")) // Mass assign wines.
render (#winebar.update_attributes(params[:wine_bar]) ? :update_success : :update_failure)
end
end
This will delete every winelist row associated with that winebar. Then it finds all of the wines in wine_ids, which we presume is a comma separated string of wine ids. Then it inserts back into the winelist a new association. This would be expensive, but fine if the destroyed association rows didn't have metadata such as the individual wine bar's price per glass and bottle.
Is there a way to have it not blow everything away, just do an enumerable comparison of the arrays and insert delete whatever changes. I feel like that's something rails does and I'm just missing something obvious.
Thanks.

Your problem looks like it's with your first statement in the update method - you're creating a new wine bar record, instead of loading an existing record and updating it. That's why when you examine the record, there's nothing showing of the relationship. Rails is smart enough not to drop/create every record on the list, so don't worry about that.
If you're using the standard rails setup for your forms:
<% form_for #wine_bar do |f| %>
Then you can call your update like this:
class WineBarController
def update
#winebar = WineBar.find(params[:id])
render (#winebar.update_attributes(params[:wine_bar]) ? :update_success : :update_failure)
end
end
You don't need to explicitly update your record with params[:wine_bar][:wine_ids], because when you updated it with params[:wine_bar], the wine_ids were included as part of that. I hope this helps!
UPDATE: You mentioned that this doesn't work because of how the forms are setup, but you can fix it easily. In your form, you'll want to rename the input field from wine_bar[wine_ids] to wine_bar[wine_ids_string]. Then you just need to create the accessors in your model, like so:
class WineBar < ActiveRecord::Base
def wine_ids_string
wines.map(&:id).join(',')
end
def wine_ids_string= id_string
self.wine_ids = id_string.split(/,/)
end
end
The first method above is the "getter" - it takes the list of associated wine ids and converts them to a string that the form can use. The next method is the "setter", and it accepts a comma-delimited string of ids, and breaks it up into the array that wine_ids= accepts.
You might also be interested in my article Dynamic Form Elements in Rails, which outlines how rails form inputs aren't limited to the attributes in the database record. Any pair of accessor methods can be used.

Related

Rails: Creating instance and associations after checking for existing record (and using the existing record)

I have a very specific problem, and I cannot seem to find a solution for it. Forgive me if this is too specific, but I don't know where else to ask.
I'm trying to create an instance of a model that has has_many through associations, but the has_many associations are part of a "master" record type table, where only 1 record of something exists (joined together with a join table).
I am trying to implement the following:
Create a Recipe using accepts_nested_attributes_for with Ingredients. The ingredient is to be used only if it is not found in the Ingredients table. If it is found, however, I want to use the already existing record.
If we need a specific example, here goes:
Say we are creating a recipe "Chili" that consists of beans, onions, ground beef. Beans and onions are brand new ingredients (not found in ingredients table) but ground beef already exists in the table. Instead of creating another ground beef record, I want to use the already existing ground beef record found in the ingredients table.
How do I go about doing this?
I thought perhaps I could use a before save: :check_if_exists call on the Recipe model, with check_if_exists checking through all the form's ingredients if they exist or not. Relevant code:
Recipe Model
before_save :check_if_exists
def check_if_exists
self.ingredients.each do |ingredient|
if Ingredient.exists?(ingredient.name)
return ingredient
else
#ingredient = Ingredient.new(name: ingredient.name)
return #ingredient
end
end
end
I know the code above is complete nonsense, but hopefully my idea comes through. I would like to use an existing record if it already exists, otherwise create a new one.
I cannot wrap my head around how to accomplish this, though. Any help would be much appreciated!
The documentation (https://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html) says pretty clearly, that you could add ids to your hashes of associated records and instead of creating new ingredients old ones will be used.
So I think in your sanitizing method which filters params for saving you could add some verifying if ingredient exists. Something like that:
params.require(:recipe).permit(:name, ingredients_attributes: [:name]).tap do |p|
p[:ingredients_attributes].each do |i|
ingredient = Ingredient.find_by(name: i[:name])
i[:id] = ingredient.id if ingredient
end
end

Saving multiple instances of models in a rails form using CRUD

I'm trying to create a form that has multiple instances of different models at once.
I have my main model visualizations. A Visualization (:title, :cover_image) has_many Rows. A Row has_many Panes (:text_field, :image)
Basically when a user tries to create a Visualization, they can choose the cover image and title easily enough. But I get a bit confused when I come to the next two levels.
The user is prompted to create a new Row in the form and they can choose either 1, 2, or 3 Panes per Row. Each pane can take in text and an image, but Row doesn't necessarily have any attributes itself.
How can I generate multiple Rows with multiple Panes in this form? The end result will need to possess a bunch of rows consisting of many panes. Can I even do this in rails?
Thanks for any help!
You can do anything in rails! The best approach in my opinion is to create what is known as a Form Model since this form will have a lot going on and you don't want to bog down several models with validations and such for one view of your app. To do this you're basically going to create a class that will take all of this information in, run whatever validations you need, and then create whatever records you need in whatever models you have. To do this lets create a new file in your model folder called so_much.rb (You can make any filename you want just make sure you name the class the same as the file so Rails finds it automagically!)
Then in your so_much.rb file do:
class SoMuch
include ActiveModel::Model #This gives us rails validations & model helpers
attr_accessor :visual_title
attr_accessor :visual_cover #These are virtual attributes so you can make as many as needed to handle all of your form fields. Obviously these aren't tied to a database table so we'll run our validations and then save them to their proper models as needed below!
#Add whatever other form fields youll have
validate :some_validator_i_made
def initialize(params={})
self.visual_title = params[:visual_title]
self.visual_cover = params[:visual_cover]
#Assign whatever fields you added here
end
def some_validator_i_made
if self.visual_title.blank?
errors.add(:visual_title, "This can't be blank!")
end
end
end
Now you can go into your controller that is processing this form and do something like:
def new
#so_much = SoMuch.new
end
def create
user_input = SoMuch.new(form_params)
if user_input.valid? #This runs our validations before we try to save
#Save the params to their appropriate models
else
#errors = user_input.errors
end
end
private
def form_params
params.require(#so_much).permit(all your virtual attributes we just made here)
end
Then in your view you would set your form_for up with #so_much like:
<%= form_for #so_much do %>
whatever virtual attributes etc
<% end %>
Form Models are a bit advanced in Rails but are a life saver when it comes to larger apps where you have many different types of forms for one model and you don't want all of the clutter.

Set Position Value from Index for nested model attributes

How can I set a position field in the attributes for a nested model so that my has_many relationship remembers it's sort order?
I want to set the position from the index so that it reflects the order the relations have been dragged into.
I have a form with Nested fields, using the cocoon gem with JQuery Sortable allowing each field-set to be drag-sortable.
I want to update the order of all the fields on saving the form.
Try to use "act_as_list" gem.
class TodoList < ActiveRecord::Base
has_many :todo_items, -> { order(position: :asc) }
end
class TodoItem < ActiveRecord::Base
belongs_to :todo_list
acts_as_list scope: :todo_list
end
todo_list = TodoList.find(...)
todo_list.todo_items.first.move_to_bottom
todo_list.todo_items.last.move_higher
Refer: https://github.com/swanandp/acts_as_list
If you have the standard CRUD/restful routes and controller actions set up, then all you are wanting to do is to call "update_attributes(:position => 3)" on an instance of the nested class (education in this case).
The usual route to update an education which is nested under "resume" would be
UPDATE /resumes/123/educations/456
so, you'll be making an ajax call to this url. The "UPDATE" method isn't really an update method, it's sort of spoofed by passing through a parameter "_method" = "UPDATE", so your ajax call will need to include this parameter too.
So, basically, on the "finished dragging" event, you're going to be making an ajax call to this url
"/resumes/<%= #resume.id %>/educations/<%= education.id %>"
and passing data like
{"_method": "UPDATE", "education[position]": <new position>}
This should update the position of the education object.
The other remaining issue is that, with acts_as_list, when we change the position of one item, we want the others to adjust their position automatically, too. I'm not sure if acts_as_list does this automatically. Try it.
Ok, non ajaxy version.
Let's assume that your form is a form_for #resume, which means that it will call the resumes#create or resumes#update action.
If all the education rows in your list have a hidden field like this
<%= hidden_field_tag "resume[education_ids][]", education.id %>
then when the form is submitted, they will go through into an array in params[:resume][:education_ids] in the order in which they appear in the page when the form was submitted, which is what you want.
The association gives you the setter method Resume#education_ids, allowing you to set the associations, in order, this way.
Ie, your update action will (if it's a normal update action) be saying something like
#resume = Resume.find_by_id(params[:id])
if #resume.update_attributes(params[:resume])
...
in this case, this will be saying
#resume.update_attributes(:education_ids => [5,6,2,1])
which is like saying "set my educations to be those with ids 5,6,2,1, in that order.
CAVEAT: in my version of rails (this might be fixed in subsequent version), if you use this _ids method, and it already has associations, but in a different order, it WILL NOT reorder them. Give it a go and see what happens.
This can't be the best way to do it, but I have got this working in my controller.
p = 1
experiences = []
params[:user][:resume_attributes][:experiences_attributes].each do |e|
e = e.last.merge(:position=>p)
experiences << e
p = p + 1
end
params[:user][:resume_attributes][:experiences_attributes] = experiences
which at least illustrates what i want to achieve.

Rails - Good way to associate units of measurement with my database columns?

I've got one model with about 50 columns of measurement data, each with a different unit of measurement (ie. grams, ounces, etc.). What is a good way to associate units of measurement with columns in my database? The primary use for this is simply for display purposes. (Ruby on Rails)
EDIT: To clarify, my model is an object, and the attributes are different measurements of that object. So, an example would be if I had the model Car and the attribute columns :power, :torque, :weight, :wheelbase, etc. I would want car.power.unit to return hp and car.weight.unit to return lbs., etc. This way, I would be able to do something like this:
<%= car.power + car.power.unit %>
and it would return
400hp
Updated Answer
Since you're storing many columns of data, but each column is only one type, and your concern is strictly presentational, I would just use a decorator to accomplish what you need. See this railscast for an example of a great way to do this using Draper.
Basically, a decorator wraps your model with presentation specific methods, so instead of:
#CarsController.rb
def show
#car = Car.find(params[:id])
end
You would use
#CarsController.rb
def show
#car = CarDecorator.find(params[:id])
end
You would define a decorator like so:
class CarDecorator < ApplicationDecorator
decorates :car
def horsepower
model.power.to_s + "hp" #call to_s just in case
end
end
Then in your view any time you called #car.horsepower you would get 123hp instead of 123. In this way you can build a big long reusable list of presentation methods. You can share methods between objects using inheritance, and you can allow methods from the original model to be called as well. See the railscast and the docs etc. You can use Draper or you could roll your own presenter class if you don't want to use a library.
Previous Answer (Abridged):
I can see two nice, easy ways to do this:
1) Just add a text column for units to your data model. IE: to get "400hp" use [data.value,data.units].join
2) You could get a little richer association by having a Units model, perhaps with help from something like ActiveEnum.
You could add a unit model with a for attribute, where you save the attribute in the messurement, you want to apply the unit to. Example:
def Unit < ActiveRecord::Base
scope :for, lambda{|messurement| find_by_for( messurement.to_s ) }
end
This allows you stuff like:
<%= #car.torque + Unit.for(:torque).symbol %>
I do not know if this is of so much advantage, but its a way to solve your problem...

fields_for to stop pluralizing

I have a fields_for tag, where I specify the prefix (lets say for some good reasons), and this is supposed to represent a one-to-one relationship.
I am trying to represent a relationship
widget has_many thingamagigs
thingamagig has_one whatchamacallit
The field_for code is:
fields_for "widgt[thingamagigs_attributes][][whatchamacallit_attributes]", thingamagig.whatchamacallit do |x|
which generates names (wrongly):
widget[thingamagigs_attributes][][whatchamacallit_attributes][][value]
The better solution would be
t.fields_for :whatchamacallit do |x|
where t = fields_for the thingamagig... However if I do that, the following names are generated
widgt[thingamagigs_attributes][whatchamacallit_attributes][]
which is completely wrong as all other fields for a thingamagig is...
widgt[thingamagigs_attributes][][name]
So in all cases I am screwed. The original field_for using a string cannot be used with accepts_nested_attributes_for :whatchamacallit since whatchamacallit is a singular relationship and an object is expected not an array. The second fields_for will simply not work because rails cannot parse the params object correctly. Is there a way to tell the first forms_for to not add the [] after [whatchamacallit_attributes] in all field names?
My current solution is to augment the model with
def whatchamacallit_attributes=(whatchamacallit_attributes)
assign_nested_attributes_for_one_to_one_association(:whatchamacallit, whatchamacallit_attributes[0])
end
Which will work even with the broken form fields. However this feels extremely hacky, does anyone have a solution?
def whatchamacallit_attributes=(whatchamacallit_attributes)
assign_nested_attributes_for_one_to_one_association(:whatchamacallit, whatchamacallit_attributes[0])
end
Looks like I have to stick with this solution since nothing else was offered.

Resources