I'm using rails_best_practices for linting my code.
Comments belong to post, which belongs to the user.
My comments_controller.rb file looks like this
class CommentsController < ApplicationController
before_action :find_post
def create
#comment = #post.comments.create comment_params
#comment.user_id = current_user.id
redirect_to #post if #comment.save
end
private
def find_post
#post = Post.find(params[:post_id])
end
def comment_params
params.require(:comment).permit(:post_id, :body)
end
end
And I'm getting this error use model association (for #comment).
After refactoring my create method looks like this
def create
#comment = #post.comments.create(
comment_params.merge(user_id: current_user.id)
)
redirect_to #post if #comment.save
end
My question is: What is the best and correct way to do this?
Normally I'd suggest baking in any required parameters inside your controller-specific _params function. That is, do this:
def comment_params
params.require(:comment).permit(:post_id, :body).merge(
user: current_user
)
end
Then by the time it gets to your controller action you're pretty much good to go.
What I tend to do is have a build method that constructs the right object for both new and create:
def build_comment
#comment = #post.comments.build(comment_params)
end
Now this will correctly populate if you relax the require constraint on params, but it's up to you how to make that flexible. I find this consistently populates and prepares the same object for both multiple edit rounds, and the first round where you need to set some defaults.
Related
So, I created app/services folder and then some classes inside with .call methods(I am trying to understand the logic of the services and query objects)
app/services/add_comment_to_post.rb
class AddCommentToPost
def initialize(post:, comment:)
#post = post
#comment = comment
end
def call
#post.comments.create(#comment)
#post
end
end
app/services/remove_comment_from_class.rb
class RemoveCommentFromPost
def initialize(post:, comment:)
#post = post
#comment = comment
end
def call
#post.comments.#comment.id.destroy
#post
end
end
and in the comments_controller.rb
def create
#this one works:
##post.comments.create! comment_params
AddCommentToPost.new(#post, #comment).call
redirect_to #post
def destroy
RemoveCommentFromPost.new(#post,#comment).call
redirect_to #post
Can anybody tell me what should I change to make it works, or where to look for similar examples? The posts and comments are scaffolded, and I use nested routes.
Rails.application.routes.draw do
resources :posts do
resources :comments
end
root "posts#index"
end
In general, it's helpful if you include the error you're getting from what you've tried. In this case, I've scanned the code and noticed a couple of errors that you should correct.
Your service objects define their initialize method to take keyword arguments like so:
def initialize(post:, comment:)
#post = post
#comment = comment
end
But you are initializing them by passing positional arguments like so:
AddCommentToPost.new(#post, #comment).call
You should initialize them with the expected keyword arguments like so:
AddCommentToPost.new(post: #post, comment: #comment).call
Additionally, as pasted above, your destroy method:
def destroy
RemoveCommentFromPost.new(#post,#comment).call
redirect_to #post
is missing an end.
Finally, you'll still want to check the return value of those service object calls to determine if the call succeeded or failed and handle appropriately. You are currently redirecting regardless.
So I got a very basic blog app running with three links to a blog post. However, when I click on a post of my selection and edit the post and click on "update blog", I will get an error saying NameError in BlogsController#update and undefined local variable or method 'blog_params' for blogscontroller. I cannot figure out what the issue is so I would like some help to guide me through
This is what my blogs controller file looks like
class BlogsController < ApplicationController
def index
#blogs = Blog.all
end
def show
#blog = Blog.find(params[:id])
end
def new
#blog = Blog.new
end
def create
#blog = Blog.new
#blog.title = params[:blog][:title]
#blog.body = params[:blog][:body]
#blog.save
redirect_to blog_path(#blog)
end
def destroy
#blog = Blog.find(params[:id])
#blog.destroy
redirect_to blog_path(#blog)
end
def edit
#blog = Blog.find(params[:id])
end
def update
#blog = Blog.find(params[:id])
#blog.update(blog_params)
redirect_to blog_path(#blog)
end
end
def update
#blog = Blog.find(params[:id])
**#blog.update(blog_params)**
redirect_to blog_path(#blog)
end
here you are calling blog_params but you haven't defined it anywhere in your code.
See example here:
See strong params
You need to do this:
#app/controllers/blogs_controller.rb
class BlogsController < ApplicationController
def update
#blog = Blog.find params[:id]
#blog = #blog.update blog_params
end
private
def blog_params
params.require(:blog).permit(:title, :body) #-> in "permit" put the attributes of your "blog" model
end
end
The error is a standard programming issue - undeclared function.
Because you're starting - and to give you more context - the reason this is a problem is because you're calling blog_params when you run the .update method:
#blog.update blog_params
This, as mentioned by Pardeep Dhingra, is the strong params pattern introduced into Rails 4. Strong params prevents mass assignment by explicitly permitting particular attributes of your model.
Whilst your code is okay, you lack the strong params method (in your case blog_params), which Rails needs to complete the request. Adding the method will fix the issue.
The rails before action seems useful for setting a variable shared by a number of actions in a controller.
But isn't the default implementation of the set_post that we see commonly on tutorials etc open to an attack by a malicious user?
If we take a controller like this:
PostsController < Application Controller
before_action :set_post , only: [:show,:create,:update]
def show
...
end
def create
...
end
def update
...
end
private
def set_post
#post = Post.find(params[:id])
end
end
When a user is presented the opportunity to update a post for example the form would be generated for them, and on post, params[:id] would contain the ID of the appropiate post - probably owned by the current_user.
However, it would not be difficult for a malicious user to alter the posted :id variable to allow them to actually end up setting the #post variable in the controller, to represent a different post, rather than the original being updated.
I could see this being safer:
private
def set_post
#post = Post.find(params[:id])
if(#post.user_id != current_user.id)
redirect_to homepage, alert: "you can edit your own posts"
end
end
However - that would stop other users viewing other people's posts! So how and where should this kind of check be performed to ensure that only the owner of a particular post can update / edit it. Is that something for the update controller action to handle itself with a check like this :
def update
if #post.user_id != current_user.id
redirect_to homepage, alert: "you can edit your own posts"
end
...
end
You are right, and I actually see that security issue being made very often by newbie Rails programmers. They just generate scaffolds and don't change things to their needs.
I'm using something like the following in my controllers:
before_action :set_post
before_action :check_post_ownership, except: :show
private
def set_post
#post = Post.find(params[:id])
end
def check_post_ownership
redirect_to homepage, alert: "..." unless #post.user_id == current_user.id
end
I really want to start learning Rails best practices, especially following the "fat model, skinny controller" logic.
Say I have the following comment controller
class CommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.create(comment_params)
#comment.user_id = current_user.id if current_user
#comment.save!
if #comment.save
redirect_to post_path(#post)
else
render 'new'
end
end
def edit
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
end
def update
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
if #comment.update(params[:comment].permit(:comment))
redirect_to post_path(#post)
else
render 'Edit'
end
end
def destroy
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
#comment.destroy
redirect_to post_path(#post)
end
private
def comment_params
params.require(:comment).permit(:comment)
end
What's a good place to start refactoring the code?
Immediately I think I an make the #post and #comment in both edit and update into a separate method, follow by calling a before_action on the method. But that is still putting all the code in the controller.
Are there any code that I can move to the model? If so, how should I structure them?
This code doesn't have much room for improvement, it's a basic crud, here's an example of a before_action like you suggested
before_action :load_post_and_comment, only: %i(edit update destroy)
def load_post_and_comment
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
end
And here a couple of other notes
def create
# ...
#comment.save!
if #comment.save
# ...
else
# ..
end
end
In this codition the you should remove the extra #comment.save! you only need to save once.
def update
# ...
if #comment.update(params[:comment].permit(:comment))
# ...
else
# ...
end
end
You already have the comment_params method, use it, because if you at any point add a new attribute to the comment, you'll update that method but you'll probably forget this part and you'll get werid errors till you notice that you need to permit here too.
If you want to really go all out with the skinny controller model, there is this gem: https://github.com/NullVoxPopuli/skinny_controllers
Where, you'd configure your CommentsController like so:
class CommentsController < ApplicationController
include SkinnyControllers::Diet
def create
if model.errors.present?
render 'new'
else
redirect_to post_path(model)
end
end
def update
redirect_to post_path(model)
end
# ... etc
private
def comment_params
params.require(:comment).permit(:comment)
end
end
how to make this code clean in rails?
profiles_controller.rb :
class ProfilesController < ApplicationController
before_action :find_profile, only: [:edit, :update]
def index
#profiles = Profile.all
end
def new
#profile = Profile.new
end
def create
profile, message = Profile.create_object(params["profile"], current_user)
flash[:notice] = message
redirect_to profile_url
end
def edit
end
def update
profile, message = #profile.update_object(params["profile"])
flash[:notice] = message
redirect_to profile_url
end
private
def find_profile
#profile = Profile.friendly.find(params["id"])
end
end
i look flash[:notice] and redirct_to profile_url is duplicate in my code, how to make the code to clean and dry?
How about moving the repetitive code to a separate method and call that method inside the actions.
def flash_redirect # you can come up with a better name
flash[:notice] = message
redirect_to profile_url
end
then in update action:
def update
profile, message = #profile.update_object(params["profile"])
flash_redirect
end
do the same thing for create action
UPDATE:
in case you are wondering about usingafter_action, you can't use it to redirect as the call-back is appended after the action runs out its course. see this answer
Take a look at Inherited Resources. It's based on the fact that many CRUD controllers in Rails have the exact same general structure. It does most of the work for you and is fully customisable in case things are done a little different in your controllers.
Using this gem, your code would look like this:
class ProfilesController < InheritedResources::Base
def create
redirect_to_profile(*Profile.create_object(params[:profile], current_user))
end
def update
redirect_to_profile(*#profile.update_object(params[:profile]))
end
private
def redirect_to_profile(profile, message)
redirect_to(profile_url, notice: message)
end
def resource
#profile ||= Profile.friendly.find(params[:id])
end
end
The create and update methods return multiple values, so I used the splat operator to DRY this up.
create_object and update_object don't follow the Rails default, so we need to implement those actions for Inherited Resources instead. Currently they don't seem to be handling validation errors. If you can, refactor them to use ActiveRecord's save and update, it would make everything even easier and DRYer.