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.
Related
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.
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
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.
Hi and thanks for taking the time to answer my question!
I have the following code in my api/ProjectController:
class Api::ProjectsController < ApplicationController
respond_to :json
def index
respond_with Project.all
end
def show
respond_with Project.find(params[:id])
end
def create
#project = Project.new(project_params)
if #project.save
redirect_to #project
else
render 'new'
end
end
end
I keep getting the following error:
undefined local variable or method `project_params' for #<Api::ProjectsController:0x00003d6d80b448>
When I change the second line in the create method to:
#project = Project.new(params[:project])
I get the following error:
ActiveModel::ForbiddenAttributesError
I am using ember and am sending JSON representation of the Project object from the front end. In both instances this is the json that's being propagated to the controller:
{"project"=>{"name"=>"asdfsdfsdf"}}
Can someone please spot where the mistake is.. I feel like I'm spending more time on this than I really should.. :/
Thank you so much and happy new year!!
You must define the project_params method that you're trying to use:
def project_params
params.require(:project).permit(:name)
end
You'll find more information and example on Rails 4 Strong parameters.
Looks like you don't have the project_params method defined in your Api::ProjectsController class. You need to add a method to your Api::ProjectsController called project_params like:
def project_params
params.require(:project).permit(:attribute)
end
Just replace the :attribute symbol with the actual attributes for the corresponding fields in your form. So if you have a 'name' and 'start_date' field for your project, then your require will look like params.require(:project).permit(:name, :start_date).
I'm working on a blog like application,
my user module has_many posts and the posts module belongs_to user
I want to access both users/:id/posts and posts/
routes.rb is something like this:
resources :users do
resources :posts
end
resources:posts
how can i know within the posts controller if its accessed directly (/posts) or through the nested route (/users/:id/posts) ?
for example, what should be the index method of the posts controller for doing the correct INDEX action for /users/:id/posts and for /posts
is there a better way for doing this ?
One solution could be to use a before filter on your controller, like:
before_filter :load_user
def load_user
#user = User.find(params[:user_id]) if params[:user_id]
#posts = #user ? #user.posts : Post.all
end
Then you have to rewrite your controller a bit to function properly.
No refactoring needed on index action, #posts already loaded correctly, but you can do further filtering as you like
def index
#posts = #posts.where('updated_at < ?' Time.now)
end
Then update every member action: new, create, show, edit, update, destroy and use posts as a base like:
def new
#post = #posts.build
end
def create
#post = #posts.build(params[:task])
end
def show
#post = #posts.find(params[:id])
end
def edit
#post = #posts.find(params[:id])
end
def update
#post = #posts.find(params[:id])
end
def destroy
#post = #posts.find(params[:id])
end
Of course you can add other before filters to remove duplicate code.
Check the params.
If just post you'll just have :id
If user/post you'll have user and ID for post.
So check if params[:user]...
n.b. If not user, try params[:user_id]
As for the index method for posts I think it will actually be the SAME in both cases. What will change things is its usage, association and scoping within user.