rails: nested form attributes how to avoid creating new entries in the database? - ruby-on-rails

I have a model Books and a model Authors.
The form for adding books, contains a nested for allowing to add authors. That works. However, I have an autocomplete function on the authors fields, so when the form is posted to the controller, the author (almost) for sure exists in the database.
I should somehow do a find_or_initialize_by on the nested attributed.
I'm maybe looking at the wrong place, but I can't find this in the rails guides. I tried this (found on SO):
def create
#book = Book.new(params_book)
small_name = params[:book][:authors_attributes]["0"]["name"].downcase
aut_id = Author.where("\"authors\".\"name\" = :name",{name: small_name}).pluck(:id).join
#book.authors = Author.find_or_initialize_by(id: aut_id)
if #book.save
redirect_to see_book_url(Book.last)
else
render 'new'
end
end
This creates an error:
undefined method `each' for #<Author:0x007fac59c7e1a8>
referring to the line #book.authors = Author.find_or_initialize_by(id: aut_id)
EDIT
After the comments on this question, I updated the code to this:
def create
book_params = params_book
small_name = params[:book][:authors_attributes]["0"]["name"].downcase
id = Author.where("\"authors\".\"name\" = :name",{name: small_name}).pluck(:id).join
book_params["authors_attributes"]["0"]["id"] = id
#book = Book.new(book_params)
if #book.save
redirect_to see_book_url(Biblio.last)
else
....
The book params look like this:
<ActionController::Parameters {"title"=>"Testus Testa",
"authors_attributes"=><ActionController::Parameters {
"0"=><ActionController::Parameters {"name"=>"Vabien", "id"=>"22"}
permitted: true>} permitted: true>} permitted: true>
That looks fine to me, BUT, I get this error:
ActiveRecord::RecordNotFound in Administration::BooksController#create
Couldn't find Author with ID=22 for Book with ID=

Ok so the easiest way to get what you want is to change autocomplete in your form from an array of names like: ['author 1 name', 'author 2 name'] change it to an array of objects containing the name and id of the author like: [{label: 'author 1 name', value: 0}, {label: 'author 2 name', value: 1}] so then as long as that form field is now for "id" instead of "name" then in your controller all you have to do is:
def create
#book = Book.new(params_book)
if #book.save
redirect_to see_book_url(Book.last)
else
render 'new'
end
end
Because only attributes without an ID will be created as new objects. Just make sure you set accepts_nested_attributes_for :authors in your Book model.
The error you are getting is because #book.authors is a many relationship so it expects a collection when you set it not an individual author. To add an individual author to the collection you do #book.authors << Author.find_or_initialize_by(id: aut_id) instead of #book.authors = Author.find_or_initialize_by(id: aut_id) although its redundant to fetch the id using the name just to initialize with an id. The id will be created automatically. Use Author.find_or_initialize_by(name: small_name) instead.
In your current code you have multiple authors being created not only due to the lack of "id" being used but because #book = Book.new(params_book) passes the nested attributes to the object initializer and then after you are accessing the nested attribute params and adding authors again. Also if you have multiple authors with the same name then Author.where("\"authors\".\"name\" = :name",{name: small_name}).pluck(:id).join would actually make an ID out of the combined ID of all authors with that name.
If you want to do it manually then remove :authors_attributes from your permit in "params_book" method so it won't be passed to Book.new then do the following:
def create
#book = Book.new(params_book)
params[:book][:author_attributes].each{|k,v| #book.authors << Author.find_or_initialize_by(name: v['name'])}
if #book.save
redirect_to see_book_url(Book.last)
else
render 'new'
end
end
Let me know if you have trouble!
After response from poster
remove :authors_attributes from your permit in "params_book" method and try this:
def create
#book = Book.new(params_book)
#book.authors_attributes = params[:book][:author_attributes].inject({}){|hash,(k,v)| hash[k] = Author.find_or_initialize_by(name: v['name']).attributes.merge(v) and hash}
if #book.save
redirect_to see_book_url(Book.last)
else
render 'new'
end
end

Solved, thanks a lot to Jose Castellanos and this post:
Adding existing has_many records to new record with accepts_nested_attributes_for
The code now is:
# the strong params isn't a Hash, so this is necessary
# to manipulate data in params :
book_params = params_book
# All registrations in the DB are small case
small_name = params[:book][:authors_attributes]["0"]["name"].downcase
# the form sends the author's name, but I need to test against the id:
id = Author.where("\"authors\".\"name\" = :name",{name: small_name}).pluck(:id).join
book_params["authors_attributes"]["0"]["name"] = params[:book][:authors_attributes]["0"]["name"].downcase
# this author_ids is the line that I was missing! necessary to
# test whether the author already exists and avoids adding a
# new identical author to the DB.
book_params["author_ids"] = id
book_params["authors_attributes"]["0"]["id"] = id
# the rest is pretty standard:
#book = Book.new(book_params)
if #book.save
redirect_to see_book_url(Book.last)
else

Related

Call 'structure' to save in postgres using rails

I have a 'publication' data structure, and my table name in postgres is 'publications'. I get a tweet from streaming and parse it according in class TweetFetcher. How do I call publication controller to save it to the database? PublicationController has the standard scrum abilities (new, create, show, edit...), where create is:
def create
#publication = Publications.new(params[:publication])
if #publication.save
redirect_to :action => 'list'
else
#subjects = Subject.find(:all)
render :action => 'new'
end
end
and parte of my twitter code is
class TweetFetcher
def saveTweet(parsedTweet)
pT = JSON.parse(parsedTweet)
#save here. like this?
#PublicationController.create(parsedTweet)
end
end
You shouldn't be using your controller in this situation, instead you can just build your record like this:
class TweetFetcher
def saveTweet(parsedTweet)
pT = JSON.parse(parsedTweet)
publication = Publications.new
publication.sample_field = pT.sample_field
... # set additional attributes
publication.save
end
end
Additionally, here is some advice/information that is not directly related to your question, but are things you should know:
The class name of your models should be singular, so instead of Publications, it should be Publication.
Method and variable names should be in snake case rather than camel case (i.e. save_tweet instead of saveTweet or parsed_tweet instead of parsedTweet).
The new hash syntax is prefered, unless of course you are using a Ruby version below 1.9. This looks like { key: value } instead of { key => value }.
Indentation for Ruby code is typically 2 spaces.
That being said, I would change your code like this:
PublicationsController#create
def create
#publication = Publication.new(params[:publication])
if #publication.save
redirect_to action: 'list'
else
#subjects = Subject.find(:all)
render action: 'new'
end
end
TweeFetcher
class TweetFetcher
def save_tweet(tweet)
parsed_tweet = JSON.parse(tweet)
publication = Publication.new
publication.sample_field = parsed_tweet.sample_field
... # set additional attributes
publication.save
end
end

Updating a HABTM model associations only deletes the associations

Creating new HABTM associations work just fine for me, but each time I try to update them, all the associations get deleted.
This is how my update action looks like.
def update
params[:dish][:cuisine_ids] ||= []
params[:dish][:diet_ids] ||= []
#user = current_user
#dish = #user.dishes_selling.find_by(:id => params[:id])
#dish.cuisines = Cuisine.where(:id => params[:dish][:cuisine_ids]) ### (1) ###
#dish.diets = Diet.where(:id => params[:dish][:diet_ids]) ### (1) ###
if #dish
respond_to do |format|
if #dish.update(dish_params) ### (2) ###
format.html { redirect_to dish_path(#dish), notice: 'Dish was edited.' }
else
format.html { render action: 'edit' }
end
end
else
flash[:error] = "You can only edit your dish!"
render :index
end
end
My dish params looks like so
def dish_params
params.require(:dish).permit(:title, :desc, cuisine_ids: [:id], diet_ids: [:id])
end
If on edit I click on two checkboxes of cuisine, the logs while on step (2) reads this (Note only Delete and no update or Insert for cuisine_dishes table)
(0.1ms) begin transaction
SQL (0.4ms) DELETE FROM "cuisines_dishes" WHERE "cuisines_dishes"."dish_id" = ? AND "cuisines_dishes"."cuisine_id" IN (7, 9) [["dish_id", 61]]
SQL (0.2ms) UPDATE "dishes" SET "desc" = ?, "updated_at" = ? WHERE "dishes"."id" = 61 [["desc", "yema1245"], ["updated_at", "2014-06-12 06:57:36.622811"]]
(5.5ms) commit transaction
Redirected to http://localhost:3000/dishes/61
Completed 302 Found in 48ms (ActiveRecord: 13.7ms)
Question : Why does updating HABTM associations try to delete the association, instead of trying to create new ones if they do not previously exist, and delete if they previously existed but do not any more.
You should try using the << operator instead of = ;
#dish.cuisines << Cuisine.where(:id => params[:dish][:cuisine_ids]).last
This should add and not replace the required ids in both the tables.
I think the answer is born from a question I have:
Why does updating HABTM associations
What do you mean by updating HABTM associations?
HABTM
HABTM associations are a collection of records which are available to add (<<), or remove (.delete). You can't "update" a record with this - you either have to add or delete (mainly because they don't have primary keys.
We do that in this way:
#app/controllers/your_controller.rb
def update
#obj = Model.find params[:id]
#obj2 = Model2.find params[:model_2]
#obj.asssociative_data << #obj2
end
The problem you have is you're passing new data to your habtm association. The only way you can "update" this data is to overwrite all the current collection with the new ids you've posted (which is what I think you're doing)
Fix
The way to fix this will be to make sure you're passing the correct params in your cuisine_ids: [:id], diet_ids: [:id] strong_params method.
I think your problem will be that you're passing :id only. I think Rails expects you to pass :cuisine_ids and :diet_ids, with those arrays populated with numerical data only, like with our code:
def profile_params
params.require(:profile).permit(attributes(Profile), :image, :category_ids)
end
To do this, you'll have to do this with your form:
<%= f.select(:category_ids, Category.all, prompt: "Category") %>

Rails create in model - except attributes

I'm creating an :expense from a :recurringexpense
The :recurring expense has attributes that are not in :expense.
I thought this would work:
def copy_to_expense
#recurringexpense = Recurringexpense.find(params[:id]) # find original recurring expense
#expense = Expense.create(#recurringexpense.attributes).except(:frequency, :last_date)
redirect_to #recurringexpenses, notice: 'Expense was successfully created.'
end
But, I'm getting this:
unknown attribute: frequency
You just need to call except directly on the attributes hash:
#expense = Expense.create(#recurringexpense.attributes.except(:frequency, :last_date))
As you note in your comment, you also need to make sure your keys in the except argument are the right type. You could also do this with
.attributes.symbolize_keys.except(...)

Rails 3 ActiveRecord nested associations select query

I'm trying to make an API for a quiz admin I have made.
I have a Quiz model which has_many Questions and has_many Results
The Question model also has_many Answers
I want the url /data/quiz/7 to return all the Questions+Answers and Results to the Quiz with id=7
Here is the method for this I have at the moment.
def quiz
#quiz = Quiz.find(params[:id])
#questions = #quiz.questions.select('id, content') # returns only selected fields
#results = #quiz.results.select('id, content, points_limit') # returns only selected fields
#questions.each do |question|
question['answers'] = question.answers.select('id, content, points') #returns whole object
end
#return = Hash.new
#return['questions'] = #questions
#return['results'] = #results
respond_to do |format|
format.json { render json: #return }
format.xml { render xml: #return }
end
end
Everything works EXCEPT the answers are returning the FULL answer object including created-at, id, updated-at etc etc and all I want are the fields selected in the query like I have.
Why is the .select working for #questions and #results but not the associated #answers?
No matter what I try it seems to ignore the select statement for the answers loop and always return the full object.
--
In console... I know doing the same thing
question.answers.select('id, content, points')
returns exactly what I'm after. So it's something to do with the way I'm putting it into the Array/Hash I'm guessing but still can't work it out.
I think if you try something like this it will work:
#answers = Answer.select([:id, :content, :points])
.where(question_id: #questions.pluck(:id))
This is the same as:
SELECT id, content, points
FROM answers
WHERE question_id in (<question_ids array>)

Passing information from an existing record, to a new record, of a different table

If I have Table A with columns title and description, and I want to create a record in Table B, which also has a title and description column, is there a way to pass that information to new_b_path such that the /views/b/_form.html.erb populates with the data from the A record?
I am using the below code to clone a workout but this is acting within Table A so to speak. I want to clone across tables. Any ideas?
workouts_controller.rb
...
def new
#workout_count = Workout.count
if params[:cloned_workout]
workout_to_clone = Workout.find params[:cloned_workout]
#workout = workout_to_clone.clone
else
#workout = current_user.workouts.new
end
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #workout }
end
end
...
/views/workouts/show.html.erb
<%= link_to "Clone Workout", new_workout_url + "?cloned_workout=#{#workout.id}", :class => "float-right" %>
Hmm. Judging by your explanation it sounds like a better strategy might be to simply create a table called Recommended with two belongs_to relationships that just has a workout_id and user_id column. Then you can associate the recommendation to a user and the workout. You could also have a gym_id or trainer_id column for the gym or trainer that made the recommendation. Make sense?

Resources