Passing information from an existing record, to a new record, of a different table - ruby-on-rails

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?

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

NoMethodError working with two models in Rails 4

I have a rating system for real estate agents. I have an agent model and an agent_review model. The rating is stored in the agent_review table, but I need to display the average rating in a view under the agent model and am running into some issue. All code is posted below, please and thank you in advance.
agent model:
has_many :agent_reviews
agent_review model:
belongs_to :agent
agent view:
<h3>Agent Rating: <%= #agent.agent_reviews.rating %> (<%= #agent.agent_reviews.count %>)</h3>
agent controller show method:
def show
#agent_reviews = AgentReview.all
#agent = Agent.find_by_slug(params[:id]) || Agent.find(params[:id])
if #agent.private_profile? && !current_agent&.super_admin?
redirect_to root_path, notice: "That user has a private profile"
else
#favorite_listings = #agent.liked_listings.available.includes(:neighborhood)
#agent_listings = #agent.sales_agent_listings.available.visible
#mate_posts = #agent.liked_mates.order(:when)
respond_to do |format|
format.html
format.json { render json: #agent }
end
end
end
error:
Adding to Jhon Feltz answer, you can to do this in a short mode. Like this:
def average_rating
agent_reviews = self.agent_reviews
agent_reviews.any? ? (agent_reviews.map(&:rating).sum / agent_reviews.count) : nil
end
#agent.agent_reviews is an Active Record relationship - there is no 'rating' for that, since it's more than one agent_review object (the fact that it's plural should tell you that).
So if an agent has 6 reviews, with ratings that vary from 1 to 5, you want to show the average of those. You need to add the following to the agent.rb model file:
def average_rating
if self.agent_reviews.any?
sum = 0
self.agent_reviews.each do |agent_review|
sum += agent_review.rating
end
return sum/self.agent_reviews.count
else
return nil # agent has no reviews, don't divide by zero!
end
end
(that's more verbose than it needs to be, you could condense it with some SQL magic)
And reference that new method in your view:
<h3>Agent Rating: <%= #agent.average_rating %> (<%= #agent.agent_reviews.count %>)</h3>

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

Intercepting creation of new object

I'm adding a categorization functionality to my app and struggling with it. Objects have many categories through categorizations. I'm trying to intercept the creation of a new categorization, check if theres a similar one, if so, increment it's count, if not, create a new object. Here's what I have so far.
validate :check_unique
protected
def check_unique
categorization = Categorization.where(:category_id => self.category_id, :categorizable_id => self.categorizable_id, :categorizable_type => self.categorizable_type)
if categorization.first
categorization.first.increment(:count)
end
end
This kind of logic should not exist in the controller. This is really business domain and should be in the model. Here's how you should go about it:
categorization = Categorization.find_or_create_by_category_id_and_categorizable_id_and_categorizable_type(self.category_id, self.categorizable_id, self.categorizable_type)
categorization.increment!(:count)
find_or_create will try to find the category in the DB, and if it doesn't exist, it'll create it. Now just make sure that count defaults to zero, and this code will do what you want. (when initially created the count would be 1, then later it'll increment)
PS: I'm not sure if find_or_create has changed in rails 3. But this is the main idea
I decided to move it out of the model object and put it into the controller method creating the categorization. It now works (Yay!) and here's the code if anyone is interested.
def add_tag
object = params[:controller].classify.constantize
#item = object.find(params[:id])
#categories = Category.find(params[:category_ids])
#categories.each do |c|
categorization = #item.categorizations.find(:first, :conditions => "category_id = #{c.id}")
if categorization
categorization.increment!(:count)
else
#item.categorizations.create(:category_id => c.id, :user_id => current_user.id)
end
end
if #item.save
current_user.update_attribute(:points, current_user.points + 15) unless #item.categorizations.exists?(:user_id => current_user.id)
flash[:notice] = "Categories added"
redirect_to #item
else
flash[:notice] = "Error"
redirect_to 'categorize'
end
end

Resources