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.
Related
My application has models Campaign & Post, I have:
class Campaign < ActiveRecord::Base
has_many :posts, inverse_of: :campaign
accepts_nested_attributes_for :posts, reject_if: :all_blank, allow_destroy: true
class Post < ActiveRecord::Base
belongs_to :campaign
My form:
= simple_form_for(#campaign) do |f|
= f.error_notification
= f.input :title
#posts
= f.simple_fields_for :posts do |post|
= render 'post_fields', f: post
.links
= link_to_add_association 'Add Post', f, :posts, wrap_object: Proc.new {|post| post.user_id = current_user.id; post }
I use Cocoon gem for nested_forms.
When I go to my campaigns#edit view, I can see all posts that were already added to a campaign (natural behavior of the gem), and I can add new posts to my campaign and/or edit existing posts .
I have also a button that has this param: add_to: 'existing_campaign' and what I am trying to achieve is, if my link has ?add_to=existing_campaign, I don't want to show/Pre-populate any of the posts that were already added to campaign, so user can only add new posts to the campaign
My link_to looks like:
= link_to 'Add Post', edit_campaign_path(campaign, add_to: 'existing_campaign'),
short explain: if edit link has param ?add_to=existing_campaign, I don't Pre-populate already added posts, if param doesn't exists, I Pre-populate posts
How can I achieve this?
Set up an attr_accessor in campaign to control whether or not existing posts should be seen...
class Campaign < ActiveRecord::Base
attr_accessor hide_posts
...
end
Set the value in your edit method
class CampaignsController < ApplicationController
def edit
#campaign.hide_posts = params[:add_to] == 'existing_campaign'
...
end
Ensure the temporary variable is in your strong parameters (so that redisplay after failed update remembers to hide posts)
def campaign_params
params.require(:campaign).permit( :hide_posts, ...
Now on your view you can do...
= f.hidden_field :hide_posts
= f.simple_fields_for :posts do |post|
= render('post_fields', f: post) unless #campaign.hide_posts && post.object.persisted?
Updating with more specifics ...
You as the design need to decide if you want to move the logic to a helper. If you aren't aware of skinny controller / fat model or another strategy for organizing your codebase read this ... Link. For now, I'm going to assume you will use a helper.
The theory & some nitpicky things ...
What's going to happen with skinny controller is you need that parameter accessible in the view logic (but that would be insecure and violate the rails way). Since you shouldn't just expose params to the view, you instead are passing the message containing the parameter's value. Object orientation teaches us to use pass messages. Rails says to use instances variables (#something) in the controller and the view has access to those.
We need to setup the conditional logic next. One of the Rails ways is to use helpers to remove extraneous logic from the view or make it readable. This qualifies as something that unless you have another reason I'm not aware of - this should be in a helper. I would be tempted here to just test for boolean and call another partial for the extra view you make.
Which means I have to assume your tests will change too (if not done in a standard way, you have to include to get access to that method/object).
Specific to your question
link_toon the page calling the nested form should be true/false...
= link_to 'Add Post', edit_campaign_path(campaign, show_posts: false)
Your controller will have #show_posts in whatever action of the campaign controller you are using (edit or new usually). You need to set #show_posts = params[:show_posts]
Write the helper ...
helpers/campaign_helper.rb
def showPosts?(show_posts)
testPart == true ? 'only_comment' : 'post_fields'
end
A new partial ... which is basically the same, but drops the simple_fields_for loop which populates the other posts.
Your _form partial will change from what you had above to ...
= f.simple_fields_for :posts do |post|
= render 'posts_fields', f: post
To ...
...
#posts
= render showPosts?(#show_posts), f: post
...
Update, I tested all the parts & got it working with your exact syntax - I ended up using ternary operator in the helper.
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])
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
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.
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