Best practice for creating a model with two belongs_to associations? - ruby-on-rails

This is a consistency problem that I'm running into often.
Let's consider a typical Forum:
User can create Posts
Posts belong to a Topic
Posts also belong to the User that created them
What's the best practice for choosing between these two options:
# Initialize #post on the User
def create
#post = current_user.posts.build(params[:post])
#post.topic_id = #topic.id
if #post.save
...
end
end
Or
# Initialize #post on the Topic
def create
#post = #topic.posts.build(params[:post])
#post.user_id = current_user.id
if #post.save
...
end
end
Or is there a better way, considering that, in the above examples, either #post's user_id or topic_id would have to be added to attr_accesssible (feels hacky)?

The cleanest approach I managed to find is using CanCan: when having a rule can :create, Post, :user_id => user.id and adding load_resource in your controller it will set the attributes.
But it is not always suitable. It would be nice to have some generic solution to initalize nested objects in one shot.
Update. I've come up with another option:
#post = #topic.posts.where(user_id: current_user.id).build(params[:post])
Generally speaking, all of these approaches break the Law of Demeter. It would be better to encapsulate in a method of the model, like this:
class Topic < ActiveRecord::Base
def new_post(params={}, author=nil)
posts.build(params).tap {|p| p.user = author}
end
end
Then in controller:
#post = #topic.new_post(params[:post], current_user)

You never need to monkey with IDs or attr_accessible. If a User has_many Posts and a Topic has_many Posts than you can do
# Initialize #post on the User
def create
#post = current_user.posts.build(params[:post])
#post.topic = #topic #assuming you've gotten the topic from somewhere
if #post.save
...
end
end
There really isn't a big difference in building from the user or from the topic, but going from the user seems more natural to me.

I prefer
#post = #topic.posts.build(params[:post])
#post.user = current_user
Although I dont see any problem with the other approach, building post via topic make more natural to me(as posts are mostly displayed in the context of its topic rather than the user itself).

Related

Rails STI not saving "type" field

I'm trying to set up a single table inheritance for Questions table. I've followed some advices adding a route this way :
resources :vfquestions, :controller => 'questions', :type =>
'Vfquestion'
And the model :
class Vfquestion < Question
end
It works, saving the question in the database, but the type field stays empty.
Here is my controller :
class QuestionsController < ApplicationController
before_action :authenticate_user!
def index
#user = current_user
#category = Category.find(params[:category_id])
#questions = #user.questions.where(:type => params[:type])
end
def new
#category = Category.find(params[:category_id])
#question = #category.questions.new
#question.type = params[:type]
end
def show
#user = current_user
#category = #user.categories.find(params[:category_id])
#question = #category.questions.find(params[:id])
end
def create
#user = current_user
#category = Category.find(params[:category_id])
#question = #category.questions.new(question_params)
#question.user_id = current_user.id
#question.save
end
private
def question_params
params.require(:question).permit(:title, :body)
end
end
Am I missing something to save this param ?
As far as I know, type is not saved for the base class. It also can't be overridden via params, as that would mean X.new would potentially yield an instance of a class other than X.
What you need to do is create the correct type on the way in:
#question =
case (params[:question][:type])
when 'Vfquestion'
Vfquestion.new(params[:question])
else
Question.new(params[:question])
end
#category.questions << #question
The relationship is also defined in terms of a singular base class, so all objects built in that scope will default to the base class.
I'm guessing in your Category model you have a line:
has_many :questions
The problem is that this relationship is pointing to the parent Question model; it has no idea you want to create or find any of its subtypes (Remember that Rails operates on convention over configuration; in this case, rails is locating your Question model because of the convention for naming has_many relationships).
One way to solve this is add the appropriate subtypes like so:
has_many :vfquestions
has_many :some_other_question_subtype
And then to create, for example, a new VFQuestion for a particular category, you would simply do:
#question = #category.vfquestions.new(question_params)
Side Note
Part of the problem in your situation is, in your create method, you have no way of distinguishing between a VFquestion, or some other question sub type when you go to create it. You'll have to figure out the best way to handle this for your particular domain, but possibly the simplest way to handle this is to pass a type parameter from the form. So, for example, if you have some kind of radio button that flops between the different question types, make sure it is named appropriately to it is sent when the form is submitted. Then simply check that piece of data in the params and either invoke .vfquestions, or some other question sub type.

What's the point of nested resources in Rails?

Say I have the prototypical blog app in Rails. I would have a Post model which has many Comments. My routes.rb might look like this:
resources :post do
resources :comment
end
This means that for instance the edit path for a comment looks like this: /post/21/comment/42/edit.
It appears to make sense when we have a has many/belongs to relationship between two models.
However, once you notice that the id of the post is not really needed to find the comment (or even the post), it starts to make less sense.
To see what I mean, consider these two equivalent implementations of the edit action in the controller:
# Nested resource version
def edit
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
# ...
end
vs.
# Un-nested resource version
def edit
#comment = Comment.find(params[:id])
#post = #comment.post
# ...
end
My question is: Is there a use case for this I haven't considered? Or are nested resources only good for making pretty URLs?

How do I get the intermediate object when using collection<<object function in has_many :through relationships

After having defined a has_many :through relationship,
#user = User.New(:name=>"Bob")
#project = Project.New( :name=>"Market Survey")
#user.projects << #project
Is there an easy way to fetch the new intermediate object it creates? such as in the above example, if the intermediate table is `memberships' then I could use:
#membership = #user.projects << #project
I have this feeling that there must be a better way of doing this than what we do all the time, i.e
#membership = Membership.where(:user_id=>x , :project_id=>y).first
There's no 'magic' way of doing this that I'm aware of. If you're looking for something that reads better than what you've done so far, the best I can come up with is to do something like this:
class User < ActiveRecord::Base
# ... other active record stuff here.
def membership_for(project)
memberships.where(:project_id => project.id).first
end
end
# Somewhere else...
#user = User.new(:name=>"Bob")
#project = Project.new(:name=>"Market Survey")
#user.projects << #project
#user.save!
membership = #user.membership_for(#project)
Not perfect, and requires additional code, but it does read better than your current code, and that counts for a lot in Ruby.
You could do something like:
#membership = #user.members.find_by_project_id(#project.id)
not sure if if it is easier/better than what your doing though.
I'm not sure if I understand your question, If one user has many projects, i think you can use this:
#user = User.create(:name=>"Bob")
# Create project and membership with user_id of #user same time, return project.
#project = #user.projects.create(:name=>"Market Survey")
If you want to find a membership, i think there is another way:
#membership = Membership.find(:first, conditions: { user_id: x, project_id: y })

Rails 3 Making a Form with two Models and Controllers

What I am trying to do is kinda complicated. Basically I have an order form and my client would like to be able to add and delete fields himself, such as different services you can purchase along with your item. So what I have done is I have made an orders controller and order model along with a field model and fields controller. How would I implement this now? My order model has a has_many :fields and my field model has a belongs_to :order, but aside from that I am stuck on how to implement this. So far in my orders controller i have a new and create method and heres what inside:
def new
#order = Order.new
#maybe i should put something like: #fields = Field.find(:all)
#title = "Order Form"
end
def create
#order = Order.new params[:order]
if #order.save
flash[:notice] = "Your order has been created"
redirect_to root_path
else
#title = "Order Form"
render 'new'
end
end
and in my fields controller I have a show new create edit update functions with nothing in them. What is the best practice to accomplish what I am trying to do?
Thanks in advance guys
You're looking for nested forms.
Check two screencasts:
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2
It will even answer your need:
my client would like to be able to add
and delete fields himself

Any way around putting hidden field in forms for resources with belongs_to association

I'm learning Rails by writing simple TODO tasks aplication.
Two models are:
class List < ActiveRecord::Base
has_many :tasks, :dependent => :destroy
# ...
end
class Task < ActiveRecord::Base
belongs_to :list
# ...
end
Tasks are routed as a nested resources under Lists. So when a new Task is created by user a POST message is sent to /lists/:list_id/tasks. So far in Tasks#new view's form there is
f.hidden_field :list_id, :value => params[:list_id]
but it's a terrible solution, because anyone can change value of that hidden field.
What is the convention here? Should I put something like
#task.list_id = params[:list_id]
in Tasks#create action and get rid of the hidden field, or maybe
#task = List.find(params[:list_id]).tasks.new(params[:task])
if #task.save
# ...
end
or there is even a better way I don't know about?
Edit:
Yeah, well there was similar question and its answer is pretty much covering my question. If you have different one please post it.
You're right - that would be horrible. No need for hidden fields. Something like the following.
In your TasksController:
def new
#list = List.find(params[:list_id])
#task = #list.tasks.build
end
def create
#list = List.find(params[:list_id])
#task = #list.tasks.new(params[:task])
# etc
end
In your Task#new view:
<% form_for [#list, #task] ... %>
...
<% end %>
If you are concerned about security (like one user creating to-dos in another user's lists - and I assume you are, because you didn't want to use a hidden field stating that anyone can change value of that hidden field), I don't see how #bjg solution is any better then yours, since you're getting #list from params anyways, and anybody can manipulate params on the browser (changing the URL to post to is as easy as changing the hidden field value).
One common way to solve this without having to implement a more complex permission solution is to just use current_user association's, like this:
def new
#list = current_user.lists.where(id: params[:list_id]).take
#task = #list.tasks.build
end
def create
#list = current_user.lists.where(id: params[:list_id]).take
#task = #list.tasks.new(params[:task])
# etc
end
This way, no matter what is the value of params[:list_id] (it could have been manipulated by the user), you can rest assured the #task will end up on that user's account, since #list will only find a record that belongs to current_user.
You can evolve this in a real-world app by returning an error message if #list is not found.

Resources