Rails4 polymorphic with self referencing - ruby-on-rails-4

I am quite new to Rails so this question might be a bit weird... I am building a system where I want Users to be able to leave Feedback regarding the other Users, but also regarding the Item they bought from them. Think of eBay feedback where they ask: Were you pleased with the item? and Would you recommend working with Joe?
Feedback model is obviously polymorphic since it can be of type User or Item
What I have is:
class User
has_many :feedbacks, as: :feedbackable
end
class Item
has_many :feedbacks, as: :feedbackable
end
class Feedback
belongs_to :feedbackable, polymorphic: true
end
The problem I have with this is that Users are creators of the Feedback, but also the receivers (feedbackable) of the same feedback...
How do I achieve something like this:
class User
has_many :feedbacks # feedback.user
has_many :feedbacks, as: :feedbackable #feedback.feedbackable.type_id
end
In the sense that I can get all feedbacks that user A has left to other users and items and also all feedbacks that other users has left about the user A

I am not sure about the politics of answering my own question, but I'll leave this answer here for anyone dealing with the same doubt...
For those of you who started reading about STI or many-to-many :through etc. it seems it's much easier to achieve this.
I ended up doing the following in the User model:
has_many :feedbacks
has_many :feedbacks_received, as: :feedbackable, :class_name => "Feedback"
Now I can do:
user.feedbacks and get all feedbacks that this user created (on Item or User)
and
user.feedbacks_received and get all feedbacks other users left about this user

Related

Assign nested attributes records to current user when using Cocoon gem

In my application I have models Post & Slides & I have:
class Post < ActiveRecord::Base
has_many :slides, inverse_of: :post
accepts_nested_attributes_for :slides, reject_if: :all_blank, allow_destroy: true
Everything works fine, only thing I need (because of how my application will work), is when a slide is created, I need to assign it to current_user or user that is creating the record.
I already have user_id in my slides table and:
class User < ActiveRecord::Base
has_many :posts
has_many :slide
end
class Slide < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
My PostsController looks like this:
def new
#post = current_user.posts.build
// This is for adding a slide without user needing to click on link_to_add_association when they enter new page/action
#post.slides.build
end
def create
#post = current_user.posts.build(post_params)
respond_to do |format|
if #post.save
format.html { redirect_to #post, notice: 'Was successfully created.' }
else
format.html { render :new }
end
end
end
Any help is appreciated!
There are two ways to accomplish this:
First option: when saving the slide, fill in the user-id, but this will get pretty messy quickly. You either do it in the model in a before_save, but how do you know the current-user-id? Or do it in the controller and change the user-id if not set before saving/after saving.
There is, however, an easier option :) Using the :wrap_object option of the link_to_add_association (see doc) you can prefill the user_id in the form! So something like:
= link_to_add_association ('add slide', #form_obj, :slides,
wrap_object: Proc.new {|slide| slide.user_id = current_user.id; slide })
To be completely correct, you would also have to change your new method as follows
#post.slides.build(user_id: current_user.id)
Then of course, we have to add the user_id to the form, as a hidden field, so it is sent back to the controller, and do not forget to fix your strong parameters clause to allow setting the user_id as well :)
When I'm looking at this I see three ways to go about it, but since you're on cocoon already, I would drop the connection between user & slides - as it kind of violates good database practices (until you hit a point where you page is so popular you have to optimize of course, but that would be done differently).
You are using cocoon, but you're not utilizing the nesting of the relationship fully yet ...
The best practice would be to have cocoon's nesting create both & instead of trying to assign to current_user you call something like:
#slides = current_user.posts.find_first(param[:id]).slides
The #slides saves all the results, the .Post.find(param[:id]) finds a specific post for current_user.
Note: this is not the most optimized way & I haven't tested this, but it shows you the format of one way you can think about the relationships. You will need to hit rails console and run some tests like ...
(rails console)> #user = User.first
Next we test that there are posts available, as it's frustrating to test blanks & not get the results ...
(rails console)> #posts = #user.posts
Then we use the find method & I'm going to use Post.first just to get a working id, you can easily put "1" or any number you know is valid ...
(rails console)> #post = #posts.find(Post.first)
Finally, we go with either all slides to make sure its a valid dataset
(rails console)> #post.slides
If you want a specific slide later & have a has_many relationship just tag that find method on the .slides after.
Also one last thing - when you state earlier in there you need the current_user to be related, you can use an entry in your model.rb to create a method or a scope to get the data & allow you to link it to the current_user more easily & even drop some directed SQL query with the .where method to pull that information up if performance is an issue.
I spotted a second optimization in there ... if everything really is working - don't worry about this!
And don't forget about the strong_parameters nesting to do this fully ... Strong Param white listing
Basic format ... `.permit(:id, :something, slide_attributes: [:id, :name, :whatever, :_destroy])

How do I get a set of objects belonging to another set of objects in Rails 4 ActiveRecord?

If I have two models, say, an user Model and a Company model like those defined below. How do I get all of the companies of a set of users?
class User < ActiveRecord::Base
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :users
end
users = User.where(:state => "Florida")
From this, I need a variable that holds all of the companies belonging to those users
companies = users.?
Do I need to loop through each of the users and add them to an array? I'm guessing there is a better way to do this. Any help is appreciated.
You can do this
Company.includes(:users).where(users: { state: 'Florida' })
BUT Taking into account http://guides.rubyonrails.org/ recommendations
It will be more correct to do
Company.joins(:users).where(users: { state: 'Florida'})
This does INNER JOIN instead of LEFT OUTER JOIN

Eager Loading Multiple Associations and serialization

In my project I have 3 models Assignment, Question and MultipleChoice with the following associations
assignment.rb
has_many :questions, dependent: :destroy
question.rb
belongs_to :assignment, class_name: 'Assignment', foreign_key: :assignment_id
has_many :multiple_choices, dependent: :destroy
multiple_choice.rb
belongs_to :question
Now I want to make a query like below
#assignment = Assignment.find(params[:id])
#questions = #assignment.questions.includes(:multiple_choices)
This is not working as expected.
So, I want all questions that belongs to the assignment for the params[:id] and the associated multiple choices that belongs to a question. My above query do not give any error but it only show questions not multiple choices associated with question. How can I do this? I am learning api development for rails. So I want to send this value as json and probably I will need serialization. How can I do this? I am working on rails 4.
Edit
well the output for
#questions = #assignment.questions.includes(:multiple_choices) and
#questions = #assignment.questions.eager_load(:multiple_choices) and
#questions = #assignment.questions are all same.
I dont understand why the output do not include any value from multiple choices table
Output of the command
#assignment.questions.eager_load(:multiple_choices).to_sql id
=> "SELECT \"questions\".\"id\" AS t0_r0, \"questions\".\"content\" AS t0_r1, \"questions\".\"q_type\" AS t0_r2, \"quest
ions\".\"created_at\" AS t0_r3, \"questions\".\"updated_at\" AS t0_r4, \"questions\".\"assignment_id\" AS t0_r5, \"multi
ple_choices\".\"id\" AS t1_r0, \"multiple_choices\".\"content\" AS t1_r1, \"multiple_choices\".\"created_at\" AS t1_r2,
\"multiple_choices\".\"updated_at\" AS t1_r3, \"multiple_choices\".\"question_id\" AS t1_r4 FROM \"questions\" LEFT OUTE
R JOIN \"multiple_choices\" ON \"multiple_choices\".\"question_id\" = \"questions\".\"id\" WHERE \"questions\".\"assignm
ent_id\" = $1"
The behaviour for includes function changed in Rails 4. You can find more details here:
http://blog.arkency.com/2013/12/rails4-preloading/
I also suppose, that if you will use eager_load instead of includes, you will get the result you need.
#assignment is an object from which you are getting the questions.
But with #questions = #assignment.questions.includes(:multiple_choices) how can you get the multiple_choices without calling this on an object?
Try this->
#assignment = Assignment.includes(:questions).find(params[:id])
#assignment.questions.includes(:multiple_choices).collect {|question| question.multiple_choices }
This also includes eager loading.
Hope you'll get what you expect.

param is missing or the value is empty

I have two models: Boards and Topics. I want to be able to add Topics to Boards. My nested resources are:
resources :boards do
resources :topics
end
My 'boards#show' action:
def show
#board = Board.find(params[:id])
#new_topics = Topic.all
end
which lists all posts and has a link_to:
<ul>
<%#new_topics.each do |i|%>
<li><%=i.title%> <%=link_to "Add", board_topic_path(#board,i), :method=> :put%></li>
<%end%>
</ul>
I'm also using strong_params for my Boards and Topics controller as follows:
boards_controller:
def update
#board = Board.find(params[:board_id])
#topic = Topic.find(params[:id])
if #board.update(board_params)
flash[:notice] = "Added!"
#board.topics << #topic
redirect_to boards_path
else
flash[:alert] = "Problem!"
redirect_to boards_path
end
end
...
private
def board_params
params.require(:board).permit(:name,:description)
end
topics_controller:
...
private
def topic_params
params.require(:topic).permit(:title,:body,:user_id)
end
the error message I'm getting: param is missing or the value is empty: topic.
I believe that your design is wrong.
Starting from the beginning, I would say that you have a business model Board that references one or more Topics and a Topic that is referenced by one or more Boards. So, logically you have something like this:
So, these are two independent resources, that they have a many-to-many relationship.
My model with Rails would have been:
# routes
resources :boards
resources :topics
In other words, topics are not nested resource of boards. If it were, this would mean that the topics of a board would die when the board would die. Which is not your case here, as far as I understand.
Now, since the relationship is many-to-many, then you will need a 3rd table to hold your associations (table boards and table topics are not enough). Read this on Rails Guides.
Briefly:
class Board
has_and_belongs_to_many :topics
end
class Topic
has_and_belongs_to_many :boards
end
Now, if you want to add topics to boards on your UI, then you need to have a form to edit the board. This form, besides the others, needs to have a multiple select box with the topics that would be added to the board. Then on your boards_controller#update method the param[:board] would have an attribute topic_ids[] which will automatically be used to associate the particular/selected topics to the board that you are editing. Rails does that automatically.
Note I am not inclined to be using has_and_belongs_to_many Rails association. It has a lot of limitations. You can always design your own table that will hold the many-to-many association and other extra attributes that your business model will require. For example, for each topic that is attached to a board, you might want to hold the subject, or the author. I do not know. In that case a more custom model might be needed:
class Board
has_many :board_topics
has_many :topics, through: :board_topics
end
class Topic
has_many :board_topics
has_many :boards, through: :board_topics
end
class BoardTopic
belongs_to :topic, inverse_of: :board_topics
belongs_to :board, inverse_of: :board_topics
.... add other attributes that give real business value to this association ....
end
In a RESTful situation as yours, with that link you should be hitting the update action of TopicsController with two params: board_id and id.
Try this instead:
# boards_controller.rb
def update
#board = Board.find(params[:id])
#topic = Topic.find(params[:topic_id])
if #board.update(board_params)
flash[:notice] = "Added!"
#board.topics << #topic
redirect_to boards_path
else
flash[:alert] = "Problem!"
redirect_to boards_path
end
end
# In the view
<%=link_to "Add", board_path(#board, topic_id: i.id), :method=> :put%>
Still, this is still off from any convention, as you are not updating a whole topic. You probably want to use an extra action to add a topic to a board, using the PATCH verb.

Rails 4: strong_params,nested_attributes_for and belongs_to association trouble

I really can't get my head around Rails 4 strong parameters, belongs_to association and form with fields_for.
Imagine I have model for quoting some price:
class Quote < ActiveRecord::Base
belongs_to :fee
accepts_nested_attributes_for :fee
Now, I have seeded some fees into the db, and have put some radiobuttons on my form_for #quote using fields_for. The values of the radiobuttons are simply ids of the records.
Here is the troubling part, the controller:
def create
#quote = Quote.new(quote_params)
...
end
def quote_params
params.require(:quote).permit(:amount_from, fee_attributes: [:id])
end
From my understanding, automagically Rails should fetch fee record with some id, but there is some mystic error instead.
params hash is: "quote"=>{"amount_from"=>"1200", "fee_attributes"=>{"id"=>"1"}}
Log tail:
Completed 404 Not Found in 264ms
ActiveRecord::RecordNotFound (Couldn't find Fee with ID=1 for Quote with ID=)
app/controllers/quotes_controller.rb:14:in `create'
I really don't understand what is going on here, have read Rails association guide, googled for hour for all info, but to no avail.
What I want to achieve here is to understand the correct "Rails way" to fetch some associations for new Quote object using some params I've put in the form.
Guess I got nested_attributes_for wrong, somehow thought it would call Fee.find automagically.
I've opted for ditching fields_for helpers from the form and rendering fields manually like
radio_button_tag 'fee[id]', fee.id
Then in controller I have 2 params methods now:
def quote_params
params.require(:quote).permit(:amount_from)
end
def fee_params
params.require(:fee).permit(:id)
end
And my action looks like
def create
#quote = Quote.new(quote_params)
#quote.fee = Fee.find(fee_params[:id])
...
Any additions on best practices when one has to handle lots of different objects with not so straight init logic are welcome.