I'm trying to make a simple rails app, where useres can leave comments on posts.
When I make a new post, I run into a undefined method 'user_name' for nil:NilClass error.
Specifically, the following:
As you can see in the picture, #post.comments seems to contain a single comment with nil variables.
my comment controller is as follows:
before_action :set_post
def create
#comment = #post.comments.build(comment_params)
#comment.user_id = current_user.id
if #comment.save
flash[:success] = "You commented the hell out of that post!"
redirect_to #post
else
flash[:alert] = "Check the comment form, something went horribly wrong."
redicect_to #post
end
end
#...
private
def comment_params
params.require(:comment).permit(:content)
end
def set_post
#post = Post.find(params[:post_id])
end
And my post controller:
before_action :set_post, only: [:show, :edit, :update, :destroy]
before_action :owned_post, only: [:edit, :update, :destroy]
#...
def create
#post = current_user.posts.build(post_params)
if #post.save
flash[:success] = "Your post has been created!"
redirect_to #post
else
flash.now[:alert] = "Your new post couldn't be created! Please check the form."
render :new
end
end
#...
private
def post_params
params.require(:post).permit(:image, :description)
end
def set_post
#post = Post.find(params[:id])
end
Thanks for the help. Sorry for and grammatical errors, I've been up for a while working on this, and am not a great speller when I'm awake.
try change
#comment.user_id = current_user.id
to
#comment.user = current_user
You need to either make sure comment does not get saved without an user/user_id, or you have to handle the case in view where user/user_id is nil
You are asking for the name of the user but you are not currently permitting a user_id in your comment_params:
This:
def comment_params
params.require(:comment).permit(:content)
end
Should be:
def comment_params
params.require(:comment).permit(:content, :user_id)
end
It is also possible that your code should be:
comment.user.name
instead of
comment.user.user_name
we would need to see your model to confirm. I would not use user_name as an attribute of user.
One of the comments for that post might not have a user. You can use try method here.
#comment.try(:user).try(:user_name)
But the ideal way to handle this would be to add a presence validation to the user model so that all comments created by any user in future will have a name.
Related
I'm currently trying to refactor some controller code and I came accross some code that I'm not sure how to implement in a correct way.
My application has users and companies, and both can have projects.
The current situation is that we have 2 urls:
example.com/projects/*action (for user projects)
example.com/company/:company_id/projects/*action (for company projects)
Those will route to the same controller which will handle the request differently based on if a company_id exists or not. This is not very clean in my opinion so I have been thinking about a better way to do this.
So far, I think the best way is to split them up in seperate controllers, like:
Users::ProjectsController
Companies::ProjectsControler
But since the only difference between a user project and a company project is pretty much that one has a 'user_id' and the other has a 'company_id', it feels like that will not be very DRY as I'll be writing a lot of duplicate code.
The current solution probably isn't as much of a problem, but I want to do this the correct way, so was hoping that someone over here would have a suggestion on how to handle this.
Thanks in advance!
EDIT:
This is how my ProjectsController#create currently looks
def create
if params[:company_id]
company = current_user.get_companies.find(params[:company_id])
#project = Project.new(project_params)
#project.company_id = company.id
else
#project = Project.new(project_params)
#project.user_id = current_user.id
end
#project = Project.new(project_params)
if #project.save
flash[:notice] = "Project '#{#project.name}' created."
if #project.company
redirect_to company_project_path(#project.company, #project)
else
redirect_to project_path(#project)
end
else
flash[:error] = #project.errors.full_messages
if params[:company_id]
redirect_to new_company_project_path(params[:company_id], #project)
else
redirect_to new_project_path(#project)
end
end
end
It's mainly the if/else logic I'd like to get rid off
So i should probably just add company_id and user_id to the permitted_params and let use a function to put either one of them in the params...
Because you said the only difference is associating company_id vs user_id, as #TomLord said, you might find something like below work for you:
Assuming that you are using shallow nested routes:
class ProjectsController < ApplicationController
COLLECTION_ACTIONS = [:index, :new, :create].freeze
MEMBER_ACTIONS = [:show, :edit, :update, :destroy].freeze
before_action :set_associated_record, only: COLLECTION_ACTIONS
before_action :set_project, only: MEMBER_ACTIONS
def index
#projects = #associated_record.projects
# ...
end
def new
#project = #associated_record.projects.new
# ...
end
def create
#project = #associated_record.projects.new(project_params)
# ...
end
private
def set_project
#project = Project.find(params[:id])
end
def set_company
if params[:company_id].present?
#company = Company.find(params[:company_id])
end
end
# you might want to remove this set_user method, because perhaps you are already setting #user from sesssion
def set_user
if params[:user_id].present?
#user = User.find(params[:user_id])
end
end
def set_associated_record
set_company
set_user
#associated_record = #company || #user
end
end
Okay I managed to handle it in a way that I'm happy with.
ProjectsController#create now looks like this:
def create
#project = owner.projects.new(project_params)
if #project.save
flash[:notice] = "Project '#{#project.name}' created."
redirect_to action: :show, id: #project.id
else
flash[:error] = #project.errors.full_messages
#project = Project.new(project_params)
render action: :new
end
end
def owner
if params[:company_id]
return policy_scope(Company).find(params[:company_id])
else
return current_user
end
end
I added the owner class to return the entity that the project belongs to.
Any suggestions for improvements are still welcome though!
In the comments controller, I am redirecting to the articles show page after both create and destroy.
So I decided to write an after_action which would do the redirect_to.
class CommentsController < ApplicationController
before_action :find_article
before_action :find_comment, only: [:destroy]
after_action :goto_articles_page, only: [:create, :destroy]
def create
#comment = #article.comments.create(comment_params)
end
def destroy
#comment.destroy
end
private
def comment_params
params.require(:comment).permit(:commenter, :body)
end
def find_article
#article = Article.find(params[:article_id])
end
def find_comment
#comment = #article.comments.find(params[:id])
end
def goto_articles_page
redirect_to article_path(#article) and return
end
end
But this gives me AbstractController::DoubleRenderError after both create and destroy.
Why am I getting this error?
By default, Rails will render views that correspond to the controller action. See Rails Guides.
So in your create and destroy actions, Rails is performing a render by default. Then your after_action (which happens after the action) is redirecting, so it's double rendering.
Instead of an after_action, you could call the goto_articles_page method in your controller actions.
For example:
def destroy
#comment.destroy
goto_articles_page
end
def goto_articles_page
redirect_to article_path(#article) #return not needed
end
I think using return when rendering any action but when redirect_to use then not need to use return then finally you can remove and return
Rails guide very nicely explained that you can follow this carefully
redirect_to explanation
Hope to help
I'm in the process of creating a website similar to Reddit. I would like to allow a moderator to be able to update a topic, but not be able to create or delete topic. I'm aware that I need to update TopicsController but I'm not sure how. My main problem is that I'm not sure how to make the code specific enough to ensure that a moderator can only update; not delete or create a topic, as an admin can.
My current code looks like this:
class PostsController < ApplicationController
before_action :require_sign_in, except: :show
before_action :authorize_user, except: [:show, :new, :create]
def show
#post = Post.find(params[:id])
end
def new
#topic = Topic.find(params[:topic_id])
#post = Post.new
end
def create
#post.body = params[:post][:body]
#topic = Topic.find(params[:topic_id])
#post = #topic.posts.build(post_params)
#post.user= current_user
if #post.save
flash[:notice] = "Post was saved"
redirect_to [#topic, #post]
else
flash[:error] = "There was an error saving the post. Please try again."
render :new
end
end
def edit
#post = Post.find(params[:id])
end
def update
#post = Post.find(params[:id])
#post.assign_attributes(post_params)
if #post.save
flash[:notice] = "Post was updated."
redirect_to [#post.topic, #post]
else
flash[:error] = "There was an error saving the post. Please try again."
render :edit
end
end
def destroy
#post = Post.find(params[:id])
if #post.destroy
flash[:notice] = "\"#{#post.title}\" was deleted successfully."
redirect_to #post.topic
else
flash[:error] = "There was an error deleting the post."
render :show
end
end
private
def post_params
params.require(:post).permit(:title, :body)
end
def authorize_user
post = Post.find(params[:id])
unless current_user == post.user || current_user.admin?
flash[:error] = "You must be an admin to do that."
redirect_to [post.topic, post]
end
end
end
I've already added a moderator role to the enum role.
I apologise if this seems really basic...but it has got me stumped!
Thanks in advance!
I could answer with some custom solution, but it's better to use a more structured and community-reviewed approach: authorization with cancan.
As tompave noticed you can use cancan gem for this.
Personally I prefer pundit.
In old days I used to define permissions directly in code everywhere: in controllers, in views and even models. But it's really bad practice. When your app grows, you are lost: you update a view, but you should make the same change in controller and sometimes in model too. It soon becomes absolutely unmanageable and you have no idea what your users can and cannot do.
Pundit, on the other hand, offers central place -- policy -- for defining what user can do. Views and controllers can then use those policies.
For example, if you need to define Post's policy you simply create app/policies/post_policy.rb file:
class PostPolicy
attr_reader :user
attr_reader :post
def initialize(user, post)
#user = user
#post = post
end
def author?
post.user == user
end
def update?
author? || user.admin? || user.moderator?
end
def create?
author? || user.admin?
end
def destroy?
author? || user.admin?
end
# etc.
end
Now whenever you need to check user's ability to perform action, you can simply invoke:
# in controller
def update
#post = Post.find(params[:id])
authorize #post
# do whatever required
end
# in view
<% if policy(post).update? %>
<%= link_to 'Edit Post', post_edit_path(post) %>
<% end %>
As you can see Pundit is very easy to comprehend and it uses the same "convention over configuration" approach as Rails. At the same time it's very flexible and allows you to test virtually anything.
You will definitely need Pundit or any similar gem to manage permission in your ambitious app.
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
I've been putting the finishing touches on my app all day with the help of some useful answers here and would like to know how this feature can be executed. I have an idea set up in my post_controller file where I want to show the top 10 most recent posts created based on the date that they were created. I also plan on doing this for my comments as well laster on. I am showing all users post in the views/post/index.html.erb file. I wrote this line of code in the posts_controller: posts = Post.order('created_at DESC').limit(10). I've searched here thoroughly but don't understand how some other users got this to work, any insight? Thanks in advance.
posts_controller
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update, :vote]
before_action :require_user, only: [:new, :create, :edit, :update, :vote]
before_action :require_creator, only:[:edit, :update]
def index
posts = Post.order('created_at DESC').limit(10)
#posts = Post.all.page(params[:page]).per_page(10)
end
def show
#comment = Comment.new
end
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
#post.creator = current_user
if #post.save
flash[:notice] = "You created a post!"
redirect_to posts_path
else
render :new
end
end
def edit
end
def update
#post = Post.find(params[:id])
if #post.update(post_params)
flash[:notice] = "You updated the post!"
redirect_to post_path(#post)
else
render :edit
end
end
def vote
Vote.create(voteable: #post, creator: current_user, vote: params[:vote])
respond_to do |format|
format.js { render :vote } # Renders views/posts/vote.js.erb
end
end
private
def post_params
params.require(:post).permit(:url, :title, :description)
end
def set_post
#post = Post.find(params[:id])
end
def require_creator
access_denied if #post.creator != current_user
end
end
The posts variable you've declared in your index action will not be available in your view. What you need to do is update the line where you're retrieving the posts using instance variable as follows:
def index
#posts = Post.page(params[:page]).order('created_at DESC').per_page(10)
end