Example: a Survey has many Questions. A request is made to create a survey, and the request contains the survey title and metadata, plus all the questions.
This all happens on one page -- when the user clicks submit, the survey and its questions are created.
So far I have all the logic in the SurveysController but I'm not sure if this is MVC, especially because I have methods like add_question and remove_question.
Is there a preferred way of doing this?
If you're creating the questions through accepts_nested_attributes_for, then it would be okay.
--
When you mention add_question / remove question - this would be best handled in a separate questions controller (with nested resource routing):
#config/routes.rb
resources :surveys do
resources :questions, only: [:create, :destroy]
end
This allows you to use the following:
#app/controllers/surverys_controller.rb
class SurveysController < ApplicationController
def show
#survey = Survey.find params[:id]
#new_question = #survey.questions.new
end
end
#app/views/surveys/show.html.erb
<%= #survey.title %>
<% #survey.questions.each do |question| %>
<%= link_to "Remove", surveys_question_path(#survey, question), method: :delete %>
<% end %>
<%= form_for #question do |f| %>
<%= f.text_field :text %>
<%= f.submit %>
<% end %>
This would keep your controllers conventional.
Nested resources:
resources :surveys do
resources :questions # check http://localhost:3000/rails/info/routes for generated routes
end
Now you can create QuestionsController with normal CRUD actions and survey_id in params. I.e:
class QuestionsController
def create
#question = Question.new(survey_id: params[:survey_id], ... )
end
end
And to create survey with all questions at once use accepts_nested_attributes_for on Survey model http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Related
I have a two-part question about form_for and nested resources. Let's say I'm writing a blog engine and I want to relate a comment to an article. I've defined a nested resource as follows:
map.resources :articles do |articles|
articles.resources :comments
end
The comment form is in the show.html.erb view for articles, underneath the article itself, for instance like this:
<%= render :partial => "articles/article" %>
<% form_for([ :article, #comment]) do |f| %>
<%= f.text_area :text %>
<%= submit_tag "Submit" %>
<% end %>
This gives an error, "Called id for nil, which would mistakenly etc." I've also tried
<% form_for #article, #comment do |f| %>
Which renders correctly but relates f.text_area to the article's 'text' field instead of the comment's, and presents the html for the article.text attribute in that text area. So I seem to have this wrong as well. What I want is a form whose 'submit' will call the create action on CommentsController, with an article_id in the params, for instance a post request to /articles/1/comments.
The second part to my question is, what's the best way to create the comment instance to begin with? I'm creating a #comment in the show action of the ArticlesController, so a comment object will be in scope for the form_for helper. Then in the create action of the CommentsController, I create new #comment using the params passed in from the form_for.
Thanks!
Travis R is correct. (I wish I could upvote ya.) I just got this working myself. With these routes:
resources :articles do
resources :comments
end
You get paths like:
/articles/42
/articles/42/comments/99
routed to controllers at
app/controllers/articles_controller.rb
app/controllers/comments_controller.rb
just as it says at http://guides.rubyonrails.org/routing.html#nested-resources, with no special namespaces.
But partials and forms become tricky. Note the square brackets:
<%= form_for [#article, #comment] do |f| %>
Most important, if you want a URI, you may need something like this:
article_comment_path(#article, #comment)
Alternatively:
[#article, #comment]
as described at http://edgeguides.rubyonrails.org/routing.html#creating-paths-and-urls-from-objects
For example, inside a collections partial with comment_item supplied for iteration,
<%= link_to "delete", article_comment_path(#article, comment_item),
:method => :delete, :confirm => "Really?" %>
What jamuraa says may work in the context of Article, but it did not work for me in various other ways.
There is a lot of discussion related to nested resources, e.g. http://weblog.jamisbuck.org/2007/2/5/nesting-resources
Interestingly, I just learned that most people's unit-tests are not actually testing all paths. When people follow jamisbuck's suggestion, they end up with two ways to get at nested resources. Their unit-tests will generally get/post to the simplest:
# POST /comments
post :create, :comment => {:article_id=>42, ...}
In order to test the route that they may prefer, they need to do it this way:
# POST /articles/42/comments
post :create, :article_id => 42, :comment => {...}
I learned this because my unit-tests started failing when I switched from this:
resources :comments
resources :articles do
resources :comments
end
to this:
resources :comments, :only => [:destroy, :show, :edit, :update]
resources :articles do
resources :comments, :only => [:create, :index, :new]
end
I guess it's ok to have duplicate routes, and to miss a few unit-tests. (Why test? Because even if the user never sees the duplicates, your forms may refer to them, either implicitly or via named routes.) Still, to minimize needless duplication, I recommend this:
resources :comments
resources :articles do
resources :comments, :only => [:create, :index, :new]
end
Sorry for the long answer. Not many people are aware of the subtleties, I think.
Be sure to have both objects created in controller: #post and #comment for the post, eg:
#post = Post.find params[:post_id]
#comment = Comment.new(:post=>#post)
Then in view:
<%= form_for([#post, #comment]) do |f| %>
Be sure to explicitly define the array in the form_for, not just comma separated like you have above.
You don't need to do special things in the form. You just build the comment correctly in the show action:
class ArticlesController < ActionController::Base
....
def show
#article = Article.find(params[:id])
#new_comment = #article.comments.build
end
....
end
and then make a form for it in the article view:
<% form_for #new_comment do |f| %>
<%= f.text_area :text %>
<%= f.submit "Post Comment" %>
<% end %>
by default, this comment will go to the create action of CommentsController, which you will then probably want to put redirect :back into so you're routed back to the Article page.
How can I best implement this feature: As an admin, I can assign a Resident Manager to a Hall.
I have a User model with a namespace routing for the admin -I intend on having another namespace routing that would hold the functions of the RM-
I have a Hall model.
Since its a many-many relationship between the above to models, I have a Management join model which contains only user_id and hall_id columns.
I know implementing the above feature, entails creating a new record in the management table but I don't know how to do it. I didn't think using a form (management#new) would solve this because the admin should not know the user_ids/hall_ids...
BELOW IS WHAT I HAVE TRIED TO DO BUT I CAN'T GET IT RIGHT
When the admin gets to the user index page, s/he should see a link for Hall Assignment for each user. This link leads to the management show page for that particular user, which would show the list of halls assigned to that user and also the show all the other remaining halls that isn't assigned to the user. So, either clicking an ADD button or on the hall's name should add it to that user's assigned halls list which is on the same page.
Management#show page
<h2><%= #user.email %>'s assigned halls</h2>
<% #user.managements.each do |management| %>
<%= management.hall.name %>
<% end %>
<p> HALL LISTS </p>
<ul>
<% #halls.each do |hall| %>
<li><%= hall.name %> <%= button_to "Add" %> </li>
<% end %>
</ul>
Here's is my Management controller
class Admin::ManagementsController < ApplicationController
def index
#managements = Management.all
end
def show
#user = User.find(params[:id])
#halls = Hall.all
end
def create
#management = Management.create(managements_params)
redirect_to admin_management_path
end
private
def managements_params
params.
require(:management).
permit(user_id: params[:user_id], hall_id: params[:hall_id])
end
end
And here's a piece of what my routes file looks like:
namespace :admin do
resources :users, only: [:index, :update]
resources :halls, only: [:index, :new, :create]
resources :managements, only: [:index, :new, :create, :show] do
resources :halls, only: [:index]
end
end
Your "add" button is just a mini form (with mostly hidden fields). You can instead just make it an actual form (with the submit-button having the text "Add") and the id-values filled in from the item on the page... it just points to the same routes that you'd normally point the form that you'd find in the new template.
if you want more detail, then show us the code that you have written (rather than a verbal description of it).
Edit:
Ok, so you'd put a button on the page like this
<ul>
<% #halls.each do |hall| %>
<li><%= hall.name %> <%= button_to "Add", managements_path(management: {user_id: #user.id, hall_id: hall.id}, method: :put ) %> </li>
<% end %>
</ul>
Notice the managements_path - you might need to check that that routing is correct (check it against what is in rake routes). Note that you're passing in the user id and the hall id, and that you must set the method to "put" on the button.
First things first -
How can I implement a 'create' action without 'new' action
It's relatively simple to do - you will need a create action somewhere, but a new action is just a way to build the respective ActiveRecord object for your controller.
If you do this in another action, you just have to make sure you point the form to the correct create action (and that create action to redirect back to
--
New / Create
Here's how you could handle the new / create actions in different controllers, as an example for you:
#app/controllers/users_controller.rb
Class UsersController < ApplicationController
def index
#hall = Hall.new
end
end
#app/controllers/halls_controller.rb
Class HallsController < ApplicationController
def create
#hall = Hall.new hall_params
redirect_to users_path if #hall.save
end
private
def hall_params
params.require(:hall).(:hall, :attributes, :user_id)
end
end
This will allow you to show the following:
#app/views/users/index.html.erb
<% #users.each do |user| %>
<%= link_to user.name, user %>
<%= form_for #hall, url: hall_path do |f| %>
<%= f.hidden_field :user_id, value: user.id %>
<%= f.text_field :x %>
<%= f.submit %>
<% end %>
<% end %>
--
Fix
ADD button or on the hall's name should add it to that user's assigned halls list
For this, I don't think you'd need a create action in the "traditional" sense - it will be more about adding new halls to a user's current halls. This is much different than creating a new hall itself:
#config/routes.rb
namespace :admin do
resources :users do
post "hall/:id", to: :add_all #=> domain.com/admin/users/:user_id/hall/:id
end
end
#app/controllers/admin/users_controller.rb
Class UsersController < ApplicationController
def add_hall
#user = User.find params[:user_id]
#hall = Hall.find params[:id]
#user.halls << #hall
end
end
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :user_halls
has_many :halls, through: :user_halls
end
#app/models/hall.rb
Class Hall < ActiveRecord::Base
has_many :user_halls
has_many :users, through: :user_halls
end
#app/models/user_hall.rb
Class UserHall < ActiveRecord::Base
belongs_to :user
belongs_to :hall
end
This uses the ActiveRecord collection methods to make this work, to which you'll be able to provide the following:
#app/views/users/index.html.erb
<% #users.each do |user| %>
<%= link_to user.name, user %>
<%= button_to "Add Hall Test", user_add_hall_path(user, 1) %>
<% end %>
I am creating a website blog and don't know what I'm going to do for adding a comment in a blog. Is there a gem or trick for that? Can you please help me and give me some tips?
There is no "trick" - you just have to code it up like the rest of your application.
Associations
Since your comments will be directly associated to your posts, it makes to start by looking at the association between the two models:
#app/models/comment.rb
Class Comment < ActiveRecord::Base
belongs_to :post
end
#app/models/post.rb
Class Post < ActiveRecord::Base
has_many :comments
end
The importance of this association is that if you want to create comments, although they'll be their own individual objects, they'll have to be "tied" to a post. The importance of this lies in the object-orientated nature of Ruby / Rails:
--
OOP
Most people don't realize that since Ruby is an object orientated language, Rails is also an object-orientated framework.
This means that you need to structure you methods, actions, etc around objects. Most beginners think you just have to create a "logical" app flow - whereas the reality is you need to build your app around the objects you hope to serve
--
Resources
Further to this, you then need to consider how you'll interact with the resources / objects in your application. I would recommend using nested resources to do this:
#config/routes.rb
resources :posts do
resources :comments #-> domain.com/posts/5/comments/new
end
The reason this is important is because of how you can then create a comment with it:
#app/controllers/comments_controller.rb
Class CommentsController < ApplicationController
def new
#post = Post.find params[:post_id]
#comment = Comment.new
end
def create
#post = Post.find params[:post_id]
#comment = Comment.new(comment_params)
end
private
def comment_params
params.require(:comment).permit(:comment, :params, :post_id)
end
end
This allows you to use the following:
#app/views/comments/new.html.erb
<%= form_for [#post, #comment] do |f| %>
<%= f.text_field :comment_attributes %>
<%= f.submit %>
<% end %>
This will help you create comment objects as a child of the post objects you want - giving you the ability to create comments for each post.
Bonus
A bonus here is that if you wanted to then nest the comments on your site, you'll want to use the Ancestry gem, as follows:
The way to do this is relatively simple. If you have a comments creation system set up, as outlined above, you'll want to add the ancestry gem to your Comment model:
#app/models/comments.rb
Class Comment < ActiveRecord::Base
has_ancestry
end
You'll need to migrate an ancestry column to your comments datatable, and then be able to populate the ancestry attribute:
You can then use the following partial to show the comments in a tree fashion:
#app/views/comments/_tree.html.erb
<ol class="categories">
<% collection.arrange.each do |category, sub_item| %>
<li>
<div class="category">
<%= link_to category.title, edit_admin_category_path(category) %>
</div>
<!-- Children -->
<% if category.has_children? %>
<%= render partial: "category", locals: { collection: category.children } %>
<% end %>
</li>
<% end %>
</ol>
You can then call it as follows:
<%= render partial: "comments/tree", locals: { collecton: #comments } %>
You could implement external comments engine, such as Disqus, or keeping it in house by making a Comment resource, which would have a relation to your post, like
class Post < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :post
end
This way you have a right way of treating your comments.
It needs some other work, controllers, views etc... But the spirit is there.
Suppose I have two models model1 and model2, and that model2 belongs_to model1 (conversely, model1 has many model2). Suppose now I want to create a model2, from the model1/1 page view, (the page showing the model1 with id 1). Here's what I did :
<%= form_for(#model2, remote: true) do |f| %>
<%= f.text_field :title %>
<%= f.submit "POST" %>
<% end %>
(#model2 was instantiated in the model1 controller show method). Is this a best practice ? Should I use nested attributes ?
What CDub said is right. However you can achieve the nested CRUD resources this way:
user = model1
post = model2
class user < ActiveRecord::Base
has_many :posts
end
class post < ActiveRecord::Base
belongs_to :user
end
In your routes you can do this:
routes.rb
resources :users do
resources :posts
end
and in your posts controller you can do this:
class UsersController < ApplicationController
def new
#post = current_user.posts.new
end
def create
#post = current_user.posts.new(params[:post])
if #post.save
redirect_to user_posts_path(current_user, #post)
else
render :new
end
end
end
You can trigger this route by doing:
<%= link_to 'new post', new_user_post_path(current_user) %>
and edit:
<%= link_to 'edit post', edit_user_post_path(current_user, #post) %>
checkout: nested resources rails api
I don't know about the best practices, but I think it makes the most sense to try and only CRUD models within their resource scope. That said, I prefer to use accepts_nested_attributes_for and creating it through a form submission to #model1, but again, it's simply preference - either will work.
I have three models associated between: User, Post, Comment. Comment is nested resource with Post.
routes.rb
resources :posts do
resources :comments
end
User model:
has_many :comments
Post model:
has_many :comments
Comment model:
belonsg_to :user
belonsg_to :post
The goal is when User makes new Comment it creates association with that user. So you can see it like User knows all comments he has made.
comments_controller.rb
def create
#post = Post.find(params[post_id]
#comment = #post.comments.build[:comment]
current_user.comments >> #comment
....
end
new.html.erb
<% form_for [#post, #post.comment.build] do |f| %>
.....
<% end %>
This gives me an error no method comments. What should I make to avoid this?
Most likely you are missing "S" letter in new.html.erb. Should be comments:
<% form_for [#post, #post.comments.build] do |f| %>
.....
<% end %>
If there is some more logic behind you didn't post let us know. Your create action looks fine. Try to look in console student_id attribute, if its populated with ID than you are fine.cheers.
Use
#post.comments.build
Instead of
#post.comment.build (x)
this should work, if possible move this line of code from view to controller
for more info
http://guides.rubyonrails.org/association_basics.html#detailed-association-reference
In new.html.erb file, you are using "s" for build method.
It should be,
<% form_for [#post, #post.comments.build] do |f| %>
.....
<% end %>