Rails create in model - except attributes - ruby-on-rails

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(...)

Related

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

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

what var type to dynamically access Model's attribute from another controller? (Rails 4.2)

Goal: dynamically update another Model's properties (Tracker) from Controller (cards_controller.rb), when cards_controller is running the def update action.
Error I receive : NameError in CardsController#update, and it calls out the 2nd last line in the
def update_tracker(card_attribute) :
updated_array = #tracker.instance_variable_get("#{string_tracker_column}")[Time.zone.now, #card.(eval(card_attribute.to_s))]
Perceived problem: I have everything working except that I don't know the appropriate way to 'call' the attribute of Tracker correctly, when using dynamic attributes.
The attribute of the Tracker is an array (using PG as db works fine), I want to
figure out what property has been changed (works)
read the corresponding property array from Tracker's model, and make a local var from it. (works I think, )
push() a new array to the local var. This new array contains the datetime (of now) and, a string (with the value of the updated string of the Card) (works)
updated the Tracker with the correct attribute.
With the following code from the cards_controller.rb
it's the if #card.deck.tracked in the update method that makes the process start
cards_controller.rb
...
def update
#card = Card.find(params[:id])
if #card.deck.tracked
detect_changes
end
if #card.update_attributes(card_params)
if #card.deck.tracked
prop_changed?
end
flash[:success] = "Card info updated."
respond_to do |format|
format.html { render 'show' }
end
else
render 'edit'
end
end
...
private
def detect_changes
#changed = []
#changed << :front if #card.front != params[:card][:front]
#changed << :hint if #card.hint != params[:card][:hint]
#changed << :back if #card.back != params[:card][:back]
end
def prop_changed?
#changed.each do |check|
#changed.include? check
puts "Following property has been changed : #{check}"
update_tracker(check)
end
end
def update_tracker(card_attribute)
tracker_attribute = case card_attribute
when :front; :front_changed
when :back; :back_changed
when :hint; :hint_changed
end
string_tracker_column = tracker_attribute.to_s
#tracker ||= Tracker.find_by(card_id: #card.id)
updated_array = #tracker.instance_variable_get("#{string_tracker_column}")[Time.zone.now, #card.(eval(card_attribute.to_s))]
#tracker.update_attribute(tracker_attribute, updated_array)
end
Edit: For clarity here's the app/models/tracker.rb:
class Tracker < ActiveRecord::Base
belongs_to :card
end
Your use of instance_variable_get has been corrected, however this approach is destined to fail because ActiveRecord column values aren't stored as individual instance variables.
You can use
#tracker[string_column_changed]
#card[card_attribute]
To retrieve attribute values by name. If you want to get an association, use public_send. The latter is also useful if there is some accessor wrapping the column value (eg carrierwave)
From your error it seem your issue is this:
#tracker.instance_variable_get("#{string_tracker_column}")
evaluates to this after string interpolation:
#tracker.instance_variable_get("front_changed")
which is incorrect use of instance_variable_get. It needs an # prepended:
#tracker.instance_variable_get("#front_changed")
Seems like using instance_variable_get is unnecessary, though, if you set attr_reader :front_changed on the Tracker model.

How to create/update record if some variables are not set through strong params?

Some of the variables are required for a record but not included in the params. How to then create/update such a record? Would the code below be correct? I belief the new method works, but have doubts about the update method.
New record:
connect = Connect.new(update_params(connection))
connect.first_id = comp1.id
connect.second_id = comp2.id
connect.save
Update existing record:
#If record exists, do this:
connect.first_id = comp1.id
connect.second_id = comp2.id
if connect.save
unless connect.update_attributes(update_connection_params(connection))
return render json: #organization, message: connect.errors.full_messages, status: :bad_request
end
end
Just write validations for these fields, on validation error(s) the record will not be saved and save will return false.
Common pattern is to render the form again, with errors, asking the user to correct
Your code for new record is correct, just check return value of save like for existing:
connect.assign_attributes update_connection_params(connection)
connect.first_id = comp1.id
connect.second_id = comp2.id
if connect.save
redirect_to somewhere_path
else
return render json: #organization, message: connect.errors.full_messages, status: :bad_request
end

Not adding data to associated table

With the code below and entry is created in the venuetypes table with the correct *venue_id* and time stamps however, the type column remains as null
def new
#new1 = "gfdsgfd"
#venue = Venue.new
#venue.save
#venuetype = #venue.venuetypes.create(:type => "test")
#venuetype.save
respond_to do |format|
format.html # new.html.erb
format.json { render json: #venue }
end
end
Unless you've specified otherwise, rails expects a type column to be used for single table inheritance which is probably causing problems.
Also, venuetypes.create will only save the venue type if it is created successfully, as will the .save call afterwards. You have almost certainly got an error on the venue type which is causing it not to be saved. Try using .save! which will throw an error or by lookins at #venuetype.errors which will contain any error messages that have caused it not to be saved.

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