Set Position Value from Index for nested model attributes - ruby-on-rails

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.

Related

Create a model that only exists in the view and doesn't get saved

Whenever I create a new object, I want to create a nested object that is available in the view, but is not saved on submit.
So far, my controller for my main model looks like this:
def new
#main = Main.new
#tmp = Sub.new
#tmp.hide = 1
#main.subs << #tmp
end
Where hide is an integer field that designates whether it's the fake model or not.
My problem the hide value, doesn't appear to exist after I click submit (it exists in the views).
For example, if I submit normally, the #tmp is saved, along with any other fields. But if I try and display hide in show, #tmp is displayed, but it's hide value isn't.
hide defaults to nil. So it would appear after submit, the change in the controller is not present.
My understanding is I can use:
accepts_nested_attributes_for :subs, :allow_destroy => true, :reject_if => proc { |a| a[:hide] == 1 }
In the main model, but this doesn't seem to work.
How can I create the model, either so it's never in the database, or deleted when the parent is saved to the database?
In depth:
I have an Experiments model that has_many exptypes.
In my form I used the nested-form gem, and as such (by virtue of trying to mix it with tabs), #experiment.exptypes cannot be empty.
The hide attribute is used on line 93 and 118 to determine whether to hide the tab or not.
This way, whenever an experiment is created, they'll be a fake exptype already in the array, and I don't want this saved on submission.
Note: This may be an XY problem, so this proposed solution may not have an affect on the overall issue. If that is the case, we may need to see the Sub model and the form you referred to.
In this kind of scenario, you will likely want to use the build (documentation) association method to create a dependent Sub object. So your controller code would change like so:
def new
#main = Main.new
#main.subs.build(hide: 1)
end
This will create a new Sub object, but it will not be saved to the database. The << method does save to the database, but this is typically not desired behaviour from the new resource route.
In your view form, you can still access the Sub objects as you would any dependent object, using fields_for. Example:
<% f.fields_for :subs do |subs_form| %>
<%= subs_form.text_field :hide %>
<% end %>
which will give you a text field already populated with the hide value you set in the controller.
Let me know whether that makes sense.

Use accepts_nested_attributes_for for new object without showing the user all the child records

I am trying to use accepts_nested_attributes_for in a slightly irregular way.
An estimate has many moneys:
class Estimate
has_many :moneys , :as => :moneyable, :dependent => :destroy
accepts_nested_attributes_for :moneys
...
end
But in an estimate's edit form, I don't want the user to see nested fieldsets of all the previous moneys' values. I only want her to see one set of empty money fields. From Rails' point of view, the estimate will have a nice history of all the previous moneys but from the user's point of view she will only see some empty fields which she can fill in if she wants to change the estimate.
= f.semantic_fields_for #estimate.moneys.build do |money|
= money.input :quantity
...
Normally you'd do f.semantic_fields_for :moneys, but that will display all the previous child moneys records. I could also use javascript to generate fields on the fly, but I want to avoid that because it's another button for the user to have to click on. Hence I'm building a new object in the form (which I'd probably eventually move to the controller, for decency's sake).
Trouble is, if I build a new object as in the form above, I won't get the accepts_nested_attributes_for goodness because Rails won't pass the money params as :money_attributes. It'll pass them as :money:
Parameters: {..."estimate"=>{"book_id"=>"1418", ..., "money"=>{"quantity"=>"321",...}}, "commit"=>"Save", "id"=>"67"}
I can faff around in the controller, adding the parent's id (and type, in this case, because it's a polymorphic relationship), but that is ugly. Am I missing a way that I can use ANAF whilst building a new object and not showing the user all the child records in the view?
Okey, I understand the problem. There is few possible solutions.
One is you manually manipulate the form with raw html attribute
You can check if the form builder object's object is new a record or not?.
Then condition may looks like:
= f.semantic_fields_for #estimate.moneys.build do |money|
- if money.object.new_record?
= add your form attribute here
So this will only let you to show the form to create a new nested record for your form.
I hope this answer will at least give an idea.

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)

has_many through blowing away the association's metadata on mass association

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.

How can I force valadition to occur when appending an object to another object in Rails?

In one of my model objects I have an array of objects.
In the view I created a simple form to add additional objects to the array via a selection box.
In the controller I use the append method to add user selected objects to the array:
def add_adjacents
#site = Site.find(params[:id])
if request.post?
#site.adjacents << Site.find(params[:adjacents])
redirect_to :back
end
end
I added a validation to the model to validate_the uniqueness_of :neighbors but using the append method appears to be bypassing the validation.
Is there a way to force the validation? Or a more appropriate way to add an element to the array so that the validation occurs? Been googling all over for this and going over the books, but can't find anything on this.
Have you tried checking the validity afterwards by calling the ".valid?" method, as shown below?
def add_adjacents
#site = Site.find(params[:id])
#site.neighbors << Site.find(params[:neighbors])
unless #site.valid?
#it's not valid, do something to fix it!
end
end
A couple of comments:
Then only way to guarantee uniqueness is to add a unique constraint on your database. validates_uniqueness_of has it's gotchas when there are many users in the system:
Process 1 checks uniqueness, returns true.
Process 2 checks uniqueness, returns true.
Process 1 saves.
Process 2 saves.
You're in trouble.
Why do you have to test for request.post?? This should be handled by your routes, so in my view it's logic that is fattening your controller unnecessarily. I'd imagine something like the following in config/routes.rb: map.resources :sites, :member => { :add_adjacents => :post }
Need to know more about your associations to figure out how validates_uniqueness_of should play in with this setup...
I think you're looking for this:
#site.adjacents.build params[:adjacents]
the build method will accept an array of attribute hashes. These will be validated along with the parent model at save time.
Since you're validating_uniqueness_of, you might get some weirdness when you are saving multiple conflicting records at the same time, depending on the rails implementation for the save and validation phases of the association.
A hacky workaround would be to unique your params when they come in the door, like so:
#site.adjacents.build params[:adjacents].inject([]) do |okay_group, candidate|
if okay_group.all? { |item| item[:neighbor_id] != candidate[:neighbor_id] }
okay_group << candidate
end
okay_group
end
For extra credit you can factor this operation back into the model.

Resources