I am creating a webiste where people can debate with each other. It has 4 main models - post, for_the_motion, against_the_motion, and user( added in the respective order). I ran a migration and made a association between for model and against model.
For each view in "for" model I want to show which user added that particular motion. But I am getting an error
undefined method `image_url' for nil:NilClass
Stuck from long time on this. This is how the models look
user.rb
class User < ApplicationRecord
has_many :posts
has_many :fors
has_many :againsts
class << self
def from_omniauth(auth_hash)
user = find_or_create_by(uid: auth_hash['uid'], provider: auth_hash['provider'])
user.name = auth_hash['info']['name']
user.image_url = auth_hash['info']['image']
user.url = auth_hash['info']['urls'][user.provider.capitalize]
user.save!
user
end
end
end
for.rb
class For < ApplicationRecord
belongs_to :post, optional: true
belongs_to :user,optional: true
end
post.rb
class Post < ApplicationRecord
has_many :fors, dependent: :destroy
has_many :againsts, dependent: :destroy
belongs_to :user, optional: true
end
against.rb
class Against < ApplicationRecord
belongs_to :post, optional: true
belongs_to :user, optional:true
end
CONTROLLERS
posts_controller.rb
class PostsController < ApplicationController
def index
#posts = Post.all
end
def land
end
def show
#post = Post.find(params[:id])
end
def new
#post = Post.new
end
def create
#post = Post.new(post_params)
#post.user = current_user
if #post.save
redirect_to #post
else
render 'new'
end
end
private
def post_params
params.require(:post).permit(:title)
end
end
fors_controller.rb
class ForsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#for = #post.fors.create(fors_params)
#for.user = current_user
redirect_to post_path(#post)
end
private
def fors_params
params.require(:for).permit(:content)
end
end
sessions_controller.rb
class SessionsController < ApplicationController
def create
begin
#user = User.from_omniauth(request.env['omniauth.auth'])
session[:user_id] = #user.id
# flash[:success] = "Welcome, #{#user.name}!"
rescue
# flash[:warning] = "There was an error while trying to authenticate you..."
end
redirect_to root_path
def destroy
if current_user
session.delete(:user_id)
# flash[:success] = 'See you!'
end
redirect_to root_path
end
end
end
This is where I am getting the error
<h1><%=#post.title%></h1>
<div class="fort">
<h3>For the motion</h3>
<%#post.fors.each do |f|%>
<p><%=f.content%></p>
<p><%=f.user.image_url%></p>/*This is where errors arise*/
<%end%>
<%= render "fors/form"%>
</div>
<div class="against">
<h3>Against the motion</h3>
<%#post.againsts.each do |f|%>
<p><%=f.content%></p>
<p><%= #post.user.name%></p>
<%end%>
<%= render "againsts/form"%>
</div>
Here is the github link for any other required information
https://github.com/sarfrazbaig/DebatingSociety2
Seems like you missed saving the .user on fors_controller.rb:
class ForsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#for = #post.fors.create(fors_params)
# .create above already will save a new For record in DB
# therefore your #for.user assignation will be only assigned in memory, but not yet in DB
#for.user = current_user
# you'll need to save it again afterwards:
#for.save
redirect_to post_path(#post)
end
# ...
end
Suggestion:
use .new instead of .create to not-yet-save into the DB, and only call save when everything that you need to assign is already assigned.
class ForsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#for = #post.fors.new(fors_params)
#for.user = current_user
#for.save
redirect_to post_path(#post)
end
# ...
end
Take note that you would still encounter that error even if you already updated your code with the above; this is because currently your For records in the DB all are missing the .user value. You'll have to manually assign and save the .user accordingly for each For record, and probably best that you'd write a...
class For < ApplicationRecord
validates :user, presence: true
end
... validation so that this error will be prevented in the future.
One of the #post.fors is lacking a user, which is permitted by the belongs_to :user, optional: true in your For model.
You can restrict your query to showing only fors that have an associated user:
#post.fors.joins(:users) or you can use the safe navigation operator to return nil when attempting to read the image_url for a non-existent user - f.user&.image_url
I am following this tutorial
I am trying to authorize user only If user is admin he should be able to see all post and comments otherwise the normal user can see its own post only .I have read github page but was quite confusing
[post_controller.rb]
class PostsController < ApplicationController
before_action :authenticate_user!, except: [:index, :show]
def index
#posts = Post.all.order('created_at DESC')
end
def new
#post = Post.new
end
def show
#post = Post.find(params[:id])
end
def create
#post = Post.new(post_params)
#post.user = current_user
if #post.save
redirect_to #post
else
render 'new'
end
end
def edit
#post = Post.find(params[:id])
end
def update
#post = Post.find(params[:id])
if #post.update(params[:post].permit(:title, :body))
redirect_to #post
else
render 'edit'
end
end
def destroy
#post = Post.find(params[:id])
#post.destroy
redirect_to posts_path
end
private
def post_params
params.require(:post).permit(:title, :body)
end
end
[comments_controller]
class CommentsController < ApplicationController
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.create(params[:comment].permit(:name, :body))
#comment.user = current_user
redirect_to post_path(#post)
end
def destroy
#post = Post.find(params[:post_id])
#comment = #post.comments.find(params[:id])
#comment.destroy
redirect_to post_path(#post)
end
end
[ability.rb]
class Ability
include CanCan::Ability
def initialize(user)
unless user
else
case user.roles
when 'admin'
can :manage, Post
can :manage, Comment
when 'user' # or whatever role you assigned to a normal logged in user
can :manage, Post, user_id: user.id
can :manage, Comment, user_id: user.id
end
end
[comment.rb]
class Comment < ActiveRecord::Base
belongs_to :post
end
[post.rb]
class Post < ActiveRecord::Base
has_many :comments, dependent: :destroy
validates :title, presence: true, length: {minimum: 5}
validates :body, presence: true
end
[user.rb]
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
end
[migration]
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table(:users) do |t|
## Database authenticatable
t.string :email, null: false, default: ""
t.string :encrypted_password, null: false, default: ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
t.integer :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
t.timestamps
end
add_index :users, :email, unique: true
add_index :users, :reset_password_token, unique: true
end
end
[migration]
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :name
t.text :body
t.references :post, index: true
t.timestamps
end
end
end
[migration]
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.string :title
t.text :body
t.timestamps
end
end
end
It seems you do not yet have a user relationship to post and comment in which you need in order to identify if the user owns/created the comment/post
Run:
rails generate migration AddUserToPost user:belongs_to
rails generate migration AddUserToComment user:belongs_to
bundle exec rake db:migrate
Then add the association relationships:
post.rb
class Post < ActiveRecord::Base
belongs_to :user
# ..
end
comment.rb
class Comment < ActiveRecord::Base
belongs_to :user
# ..
end
user.rb
class User < ActiveRecord::Base
has_many :posts
has_many :comments
# ..
end
Now you can identify who owns the post/comment, and what posts/comments a user owned/created with something like the following pseudo-code:
# rails console
post = Post.find(1)
post_owner = post.user
comment = Comment.find(1)
comment_owner = comment.user
user = User.find(1)
user_comments = user.comments
user_posts = user.posts
Now, the next step is to auto-associate the logged-in user to newly created posts/comments. This is done through the controllers:
posts_controller.rb
class PostsController < ApplicationController
authorize_resource
# ..
def create
#post = Post.new(post_params)
#post.user = current_user # I assume you have a variable current_user, or if you are using Devise current_user is already accessible
if #post.save
redirect_to #post
else
render :new
end
end
end
comments_controller.rb
class CommentsController < Application
authorize_resource
# ..
def create
#post = Post.find(params[:post_id])
#comment = #post.comments.build(params[:comment].permit(:name, :body))
#puts "hhhhhhhhhh#{#comment}"
#comment.user = current_user # I assume you have a variable current_user, or if you are using Devise current_user is already accessible
#comment.save
redirect_to post_path(#post)
end
end
Now, at this point. Whenever a post/comment gets created, the logged-in user is automatically associated to it (as the owner).
Finally, we could just update the Ability class to only authorize users to :edit, :update, :show, and :destroy actions, if the user_id: current_user (logged-in user).
ability.rb
class Ability
include CanCan::Ability
def initialize(user)
# if not logged in (Guest)
unless user
# cant do anything unless you add more `can` here
# else if logged in
else
case user.role
when 'admin'
can :manage, Post
can :manage, Comment
when 'normal' # or whatever role you assigned to a normal logged in user
can :manage, Post, user_id: user.id
can :manage, Comment, user_id: user.id
# If you don't have a role name for a normal user, then use the else condition like Rich Peck's answer. Uncomment the following instead, and then comment the `when 'normal' block of code just above
# else
# can :manage, Post, user_id: user.id
# can :manage, Comment, user_id: user.id
end
end
end
end
Just a final helpful information to the Ability above:
can :manage, Post, user_id: user.id
This is just a shorthand equal to:
can [:show, :edit, :update, :destroy], Post, user_id: user.id
can [:index, :new, :create], Post
You will notice that user_id: user.id is not taken into consideration for :index, :new, and :create because these are :collection methods, and not :member methods. More info here
If you want readability and customizability, you may opt to use the longer one above instead of the shorthand :manage.
#app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
case user.role
when "admin"
can :manage, :all
else
can :read, Post #-> cannot read comments
end
end
end
The above is how the ability class should look. You can replace the switch/case with if/else.
--
You're missing the evaluation of your objects, specifically with the can? & authorize methods:
#app/controllers/comments_controller.rb
class CommentsController < ApplicationController
def create
#post = Post.find params[:post_id]
#comment = #post.comments.new comment_params
#comment.save if authorize! :create, #comment
redirect_to #post
end
def destroy
#post = Post.find params[:post_id]
#comment = #post.comments.find params[:id]
#comment.destroy if authorize! :destroy, #comment
redirect_to #post
end
private
def comment_params
params.require(:comment).permit(:name, :body)
end
end
#app/controllers/posts_controller.rb
class PostsController < ApplicationController
def show
#post = Post.find params[:id]
end
end
#app/views/posts/show.html.erb
<%= #post.title %>
<%= render #post.comments if can? :read, #post.comments %>
1) Change this line in PostsController, delete this condition: except [index, show]. Or user could see pages without authorization.
before_action :authenticate_user!
2) Change index action and other with this style. Use - current_user.
def index
if current_user.has_role? :admin
#posts = Post.all.order('created_at DESC')
else
#posts = current_user.posts.order('created_at DESC')
end
end
You can write you abilities in this way
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
case user.role
when "admin"
can :manage, :all
else
can :read, Post, :user_id => user.id
end
end
end
And just load resources of post using ability resource so that it only load post of current user if other than admin
class CommentsController < Application
load_and_authorize_resource
def index
#posts = #posts
end
end
My problem is that how to assign multiple belongs_to associations with nested attributes?
I want to build a issue system. When I create a issue, I also want to create the first comment as the issue body.
So, I have following Models:
class Issue < ActiveRecord::Base
has_many :comments, as: :commentable
validates :title, presence: true
accepts_nested_attributes_for :comments
end
class Comment < ActiveRecord::Base
belongs_to :user
belongs_to :commentable, polymorphic: true
validates :content, :user, presence: true
end
and I have the IssuesController as follow:
class IssuesController < ApplicationController
before_action :authenticate_user! #devise authentication
def new
#issue = Issue.new
#issue.comments.build
end
def create
#issue = Issue.new(issue_params)
#issue.save
end
private
def issue_params
params.require(:issue).permit(:title, comments_attributes: [:content])
end
end
and the following is my form (using slim template with simple_form and nested_form gems):
= simple_nested_form_for #issue do |f|
= f.input :title
= f.fields_for :comments do |cf|
= cf.input :content
= f.button :submit
In this case, I don't know how to assign current_user to the comment created by nested attributes.
Any suggestions or other approaches? Thanks!
As I wrote in the comments, there's two ways of doing this.
The first way is to add a hidden field in your subform to set the current user:
= simple_nested_form_for(#issue) do |f|
= f.input :title
= f.fields_for(:comments) do |cf|
= cf.input(:content)
= cf.hidden(:user, current_user)
= f.submit
If you do not trust this approach in fear of your users fiddling with the fields in the browser, you can also do this in your controller.
class IssuesController < ApplicationController
before_action :authenticate_user! #devise authentication
def new
#issue = Issue.new
#issue.comments.build
end
def create
#issue = Issue.new(issue_params)
#issue.comments.first.user = current_user
#issue.save
end
private
def issue_params
params.require(:issue).permit(:title, comments_attributes: [:content])
end
end
This way you take the first comment that is created through the form and just manually assign the user to it. Then you know for the sure that the first created comment belongs to your current user.
You could also add user_id as current_user.id when you use params
class IssuesController < ApplicationController
before_action :authenticate_user! #devise authentication
def new
#issue = Issue.new
#issue.comments.build
end
def create
#issue = Issue.new(issue_params)
#issue.save
end
private
def issue_params
params[:issue][:comment][:user_id] = current_user.id
params.require(:issue).permit(:title, comments_attributes: [:content, :user_id])
end
end
I'm (very) new to ror and have read many tutorials for this issue but none seem to work. I'm trying to let one user create one booth to sell things.
This is my db migration:
class CreateBooths < ActiveRecord::Migration
def change
create_table :booths do |t|
t.string :name
t.references :user, index: true
t.timestamps null: false
end
add_index :booths, [:user_id]
end
end
Here is the booth controller:
class BoothsController < ApplicationController
before_action :logged_in_user
def new
#booth = Booth.new
end
def create
#booth = current_user.booths.build(booth_params)
if #booth.save
flash[:success] = "Congrats on opening your booth!"
redirect_to root_url
else
render 'new'
end
end
private
def booth_params
params.require(:booth).permit(:name)
end
end
And this is the booth model:
class Booth < ActiveRecord::Base
belongs_to :user
validates :user_id, presence: true
end
I also added this to the user model:
has_one :booth, dependent: :destroy
When I include validates :user_id, presence: true it won't save to the db. When I exclude it, it saves but does not include a user id in the database. If you are still reading thank you and I hope you can help!
You need to change create method of your BoothsController to this:
def create
#booth = current_user.build_booth(booth_params)
if #booth.save
flash[:success] = "Congrats on opening your booth!"
redirect_to root_url
else
render 'new'
end
end
Here, you have one-to-one association between user and booth, and that's why you have to instantiate booth for current_user using build_<singular_association_name>, which is build_booth and pass params to it: build_booth(booth_params).
booths.build(booth_params) works for one-to-many association, for example: user has many booths, not vice a versa.
If I have a nested resource like so:
resources :users
resources :posts
end
and a user has_many posts, it is possible to have Rails start numbering based on the parent association in the URL? For example, currently, nesting resources just grabs the ID:
#user.posts.find(params[:id])
This correctly namespaces the posts, only allowing posts from #user... however, is there a way such that the post_id is independent? I.E. I want each user's posts to start at 1, where:
/users/1/posts/1
/users/2/posts/1
Actually refer to two different posts?
It can be quite a bit of work, but basically you can do it with these steps:
Create a migration to add a new attribute to store the specific user-post count. (I used user_post_id)
Override Post's to_param method to use the new value you just created. (It has to be a string.)
to_param is the method that the url and path helpers use.
Create a before_save filter that will actually increment the user_post_id value for each new post.
Change all your controller methods to find on user_post_id
#user = User.find(params[:user_id])
#post = #user.posts.where(:user_post_id => (params[:id])).first
Change all your Views that might not work now
You can see the source here: Custom Nested Resource URL example
Code
migration:
class AddUserPostIdToPosts < ActiveRecord::Migration
def change
add_column :posts, :user_post_id, :integer
end
end
post.rb:
class Post < ActiveRecord::Base
before_save :set_next_user_post_id
belongs_to :user
validates :user_post_id, :uniqueness => {:scope => :user_id}
def to_param
self.user_post_id.to_s
end
private
def set_next_user_post_id
self.user_post_id ||= get_new_user_post_id
end
def get_new_user_post_id
user = self.user
max = user.posts.maximum('user_post_id') || 0
max + 1
end
end
A couple controller methods
posts_controller.rb:
class PostsController < ApplicationController
respond_to :html, :xml
before_filter :find_user
def index
#posts = #user.posts.all
respond_with #posts
end
def show
#post = #user.posts.where(:user_post_id => (params[:id])).first
respond_with [#user, #post]
end
...
end