I have a comments_controller that uses inherited_resources and deals with this models:Comment (belongs_to Shop and belongs_to User), and Shop (belongs_to User).
Rails 4.1.1 and Inherited_resources v is 1.5.0.
Routes are:
resources :shop do
resources :comments, only: [:create, :destroy]
end
However, the below code doesn't work:
class CommentsController < InheritedResources::Base
before_filter :authenticate_user!
nested_belongs_to :user, :shop
actions :create, :destroy
def create
#comment = build_resource
#comment.shop = Shop.find(params[:hotel_id])
#comment.user = current_user
create!
end
def destroy
#hotel = Shop.find(params[:hotel_id])
#comment = Comment.find(params[:id])
#comment.user = current_user
destroy!
end
private
def permitted_params
params.permit(:comment => [:content])
end
Rspec that test creation/deletion of comments tell me Couldn't find User without an ID.
Thanks for any help.
UPD One of the failing tests:
let(:user) { FactoryGirl.create(:user) }
let(:shop) { FactoryGirl.create(:shop, user: user) }
describe "comment creation" do
before { visit shop_path(shop) }
describe "with invalid information" do
it "should not create a comment" do
expect { click_button "Post a comment" }.not_to change(Comment, :count)
end
end
From your routes, it looks like you want to deal with Comments belonging to a Shop. In this case, you don't need nested_belongs_to, instead change it to belongs_to :shop in your controller and that will take care of it. And add another line belongs_to :user separately.
So, your controller will look like this:
class CommentsController < InheritedResources::Base
before_filter :authenticate_user!
belongs_to :shop
belongs_to :user
actions :create, :destroy
.
.
.
end
Related
I'm trying to test the actions in controller spec but for some reason I get the no routes matches error. What should I do to make the route work?
ActionController::UrlGenerationError:
No route matches {:action=>"create", :comment=>{:body=>"Consectetur quo accusamus ea.",
:commentable=>"4"}, :controller=>"comments", :post_id=>"4"}
model
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true, touch: true
class Post < ActiveRecord::Base
has_many :comments, as: :commentable, dependent: :destroy
routes
resources :posts do
resources :comments, only: [:create, :update, :destroy], module: :posts
end
controller_spec
describe "POST create" do
let!(:user) { create(:user) }
let!(:profile) { create(:profile, user: #user) }
let!(:commentable) { create(:post, user: #user) }
context "with valid attributes" do
subject(:create_action) { xhr :post, :create, post_id: commentable, comment: attributes_for(:comment, commentable: commentable, user: #user) }
it "saves the new task in the db" do
expect{ create_action }.to change{ Comment.count }.by(1)
end
...
EDIT
The controller_spec from above can be found in spec/controllers/comments_controller_spec.rb
controllers/comments_controller.rb
class CommentsController < ApplicationController
before_action :authenticate_user!
def create
#comment = #commentable.comments.new(comment_params)
authorize #comment
#comment.user = current_user
if #comment.save
#comment.send_comment_creation_notification(#commentable)
respond_to :js
end
end
controllers/posts/comments_controller.rb
class Posts::CommentsController < CommentsController
before_action :set_commentable
private
def set_commentable
#commentable = Post.find(params[:post_id])
end
Using the module: :posts will route to Posts::CommentsController#create.
If that is not what you intended than remove the module option.
Otherwise you need to ensure that you have the correct class name for both your controller and spec.
class Posts::CommentsController
def create
end
end
RSpec.describe Posts::CommentsController do
# ...
end
Also note that if often does not make sense to nest the "individual actions" for a resource.
Instead you may want to declare the routes like so:
resources :comments, only: [:update, :destroy] # show, edit ...
resources :posts do
resources :comments, only: [:create], module: :posts # new, index
end
Which gives you:
class CommentsController < ApplicationController
before_action :set_posts
# DELETE /comments/:id
def destroy
# ...
end
# PUT|PATCH /comments/:id
def update
end
end
class Posts::CommentsController < ApplicationController
# POST /posts/:post_id/comments
def create
# ...
end
end
See Avoid Deeply Nested Routes in Rails for a deeper explaination of why.
Setting the up the controller to use inheritance in this case is a good idea - however you cannot test the create method through the parent CommentsController class in a controller spec since RSpec will always look at described_class when trying to resolve the route.
Instead you may want to use shared examples:
# /spec/support/shared_examples/comments.rb
RSpec.shared_examples "nested comments controller" do |parameter|
describe "POST create" do
let!(:user) { create(:user) }
context "with valid attributes" do
subject(:create_action) { xhr :post, :create, post_id: commentable, comment: attributes_for(:comment, commentable: commentable, user: #user) }
it "saves the new task in the db" do
expect{ create_action }.to change{ Comment.count }.by(1)
end
end
end
end
require 'rails_helper'
require 'shared_examples/comments'
RSpec.describe Posts::CommentsController
# ...
include_examples "nested comments controller" do
let(:commentable) { create(:post, ...) }
end
end
require 'rails_helper'
require 'shared_examples/comments'
RSpec.describe Products::CommentsController
# ...
include_examples "nested comments controller" do
let(:commentable) { create(:product, ...) }
end
end
The other alternative which I prefer is to use request specs instead:
require 'rails_helper'
RSpec.describe "Comments", type: :request do
RSpec.shared_example "has nested comments" do
let(:path) { polymorphic_path(commentable) + "/comments" }
let(:params) { attributes_for(:comment) }
describe "POST create" do
expect do
xhr :post, path, params
end.to change(commentable.comments, :count).by(1)
end
end
context "Posts" do
include_examples "has nested comments" do
let(:commentable) { create(:post) }
end
end
context "Products" do
include_examples "has nested comments" do
let(:commentable) { create(:product) }
end
end
end
Since you are really sending a HTTP request instead of faking it they cover more of the application stack. This does however come with a small price in terms of test speed. Both shared_context and shared_examples are two of the things which make RSpec really awesome.
So i'm relatively new to RoR, and am having some issues in trying to get my code back up and working. So previously I had users, and wikis that users could create. I've set up so that users can subscribe and get premium status to make wikis private. Now I'm in the process of making it so that Premium users can add standard users as collaborators to the wiki. I've decided to got about associating them through has_many :through relationships.
The issue I'm running into so that some of my buttons have started making errors that I don't understand. The one I'm stuck on right now is when showing the page that has a create new wiki button on it.
This is the error I am getting when I added the has_many through: relationship
No route matches {:action=>"new", :controller=>"wikis", :format=>nil, :user_id=>nil} missing required keys: [:user_id]
Here are the models:
collaborator.rb
class Collaborator < ActiveRecord::Base
belongs_to :wiki
belongs_to :user
end
user.rb
class User < ActiveRecord::Base
...
has_many :collaborators
has_many :wikis, :through => :collaborators
end
wiki.rb
class Wiki < ActiveRecord::Base
belongs_to :user
has_many :collaborators
has_many :users, :through => :collaborators
end
The important bits of the wiki_controller.rb
def new
#user = User.find(params[:user_id])
#wiki = Wiki.new
authorize #wiki
end
def create
#user = current_user
#wiki = #user.wikis.create(wiki_params)
authorize #wiki
if #wiki.save
flash[:notice] = "Wiki was saved"
redirect_to #wiki
else
flash[:error] = "There was an error saving the Wiki. Please try again"
render :new
end
end
And finally the show.html.erb file the button is located in.
<div class="center-align">
<%= link_to "New Wiki", new_user_wiki_path(#user, #wiki), class: 'btn grey darken-1' %>
</div>
If I'm missing any files or relevant info please let me know. This may be a simple stupid answer but I'm stuck for the life of me.
Thanks in advance.
Edit:
Here is the requested added info, first up the show info in the users_controllers.rb
def show
#wikis = policy_scope(Wiki)
end
the corresponding policy scope I'm using in the user_policy.rb
class UserPolicy < ApplicationPolicy
class Scope
attr_reader :user, :scope
def initialize(user, scope)
#user = user
#scope = scope
end
def resolve
wikis = []
all_wikis = scope.all
all_wikis.each do |wiki|
if wiki.user == user || wiki.users.include?(user)
wikis << wiki
end
end
end
wikis
end
end
and the route.rb file
Rails.application.routes.draw do
devise_for :users
resources :users, only: [:update, :show] do
resources :wikis, shallow: true
end
resources :wikis, only: [:index]
resources :charges, only: [:new, :create]
delete '/downgrade', to: 'charges#downgrade'
authenticated do
root to: "users#show", as: :authenticated
end
root to: 'welcome#index'
end
Hope it helps
I found out the problem. I set up the migrate file wrong when originally creating the collaboration model.
Thanks for all of your help.
Given the routes:
Example::Application.routes.draw do
concern :commentable do
resources :comments
end
resources :articles, concerns: :commentable
resources :forums do
resources :forum_topics, concerns: :commentable
end
end
And the model:
class Comment < ActiveRecord::Base
belongs_to :commentable, polymorphic: true
end
When I edit or add a comment, I need to go back to the "commentable" object. I have the following issues, though:
1) The redirect_to in the comments_controller.rb would be different depending on the parent object
2) The references on the views would differ as well
= simple_form_for comment do |form|
Is there a practical way to share views and controllers for this comment resource?
In Rails 4 you can pass options to concerns. So if you do this:
# routes.rb
concern :commentable do |options|
resources :comments, options
end
resources :articles do
concerns :commentable, commentable_type: 'Article'
end
Then when you rake routes, you will see you get a route like
POST /articles/:id/comments, {commentable_type: 'Article'}
That will override anything the request tries to set to keep it secure. Then in your CommentsController:
# comments_controller.rb
class CommentsController < ApplicationController
before_filter :set_commentable, only: [:index, :create]
def create
#comment = Comment.create!(commentable: #commentable)
respond_with #comment
end
private
def set_commentable
commentable_id = params["#{params[:commentable_type].underscore}_id"]
#commentable = params[:commentable_type].constantize.find(commentable_id)
end
end
One way to test such a controller with rspec is:
require 'rails_helper'
describe CommentsController do
let(:article) { create(:article) }
[:article].each do |commentable|
it "creates comments for #{commentable.to_s.pluralize} " do
obj = send(commentable)
options = {}
options["#{commentable.to_s}_id"] = obj.id
options["commentable_type".to_sym] = commentable.to_s.camelize
options[:comment] = attributes_for(:comment)
post :create, options
expect(obj.comments).to eq [Comment.all.last]
end
end
end
You can find the parent in a before filter like this:
comments_controller.rb
before_filter: find_parent
def find_parent
params.each do |name, value|
if name =~ /(.+)_id$/
#parent = $1.classify.constantize.find(value)
end
end
end
Now you can redirect or do whatever you please depending on the parent type.
For example in a view:
= simple_form_for [#parent, comment] do |form|
Or in a controller
comments_controller.rb
redirect_to #parent # redirect to the show page of the commentable.
I spent most of the day trying to root out a problem with a controller spec, and the current workaround seems unacceptable to me. Any take on why this works? ... and what I should do instead.
Given a simple hierarchy as follows, and the following ability.rb, the properties_controller_spec.rb does not allow the spec below to pass without the line saying:
ability = Ability.new(subject.current_user)
Can you tell me why this would be?
Thanks!
Models:
class Account < ActiveRecord::Base
has_many :properties, :dependent => :nullify
end
class Property < ActiveRecord::Base
belongs_to :account
end
class User < Refinery::Core::BaseModel #for RefineryCMS integration
belongs_to :account
end
Ability.rb:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new
if user.has_role? :user
can [:read, :create, :update, :destroy], Property, account_id: user.account_id
else
can [:show], Property
end
end
end
properties_contoller_spec.rb:
require 'spec_helper'
describe PropertiesController do
def valid_attributes
describe "Authenticated as Property user" do
describe "PUT update" do
describe "with invalid params" do
it "re-renders the 'edit' template" do
property = FactoryGirl.create(:property, account: property_user.account)
# Trigger the behavior that occurs when invalid params are submitted
Property.any_instance.stub(:save).and_return(false)
ability = Ability.new(subject.current_user) # seriously?
put :update, {:id => property.to_param, :property => { }}, {}
response.should render_template("edit")
end
end
end
end
end
Arg! Found it myself.
Here it is:
config.include Devise::TestHelpers, :type => :controller
Following is the code to sign in the property_user, as directed by the Devise docs. (The locals in question are created in a global_variables.rb that is included. These are used all over the place.)
def signed_in_as_a_property_user
property_user.add_role "User"
sign_in property_user
end
def sign_in_as_a_property_user
property_user.add_role 'User'
post_via_redirect user_session_path,
'user[email]' => property_user.email,
'user[password]' => property_user.password
end
I use devise and cancan gems and have simple model association: user has_many subscriptions, subscription belongs_to :user. Have following SubscriptionsController:
class SubscriptionsController < ApplicationController
load_and_authorize_resource :user
load_and_authorize_resource :subscription, through: :user
before_filter :authenticate_user!
def index
#subscriptions = #user.subscriptions.paginate(:page => params[:page]).order(:created_at)
end
#other actions
end
And Cancan Ability.rb:
class Ability
include CanCan::Ability
def initialize(user)
user ||=User.new
can [:read], [Edition, Kind]
if user.admin?
can :manage, :all
elsif user.id
can [:read, :create, :destroy, :pay], Subscription, user_id: user.id
can [:delete_from_cart, :add_to_cart, :cart], User, id: user.id
end
end
end
The problem is that i cannot use subscriptions actions as a user but can as a admin. And have no problems with UsersController. When i delete following lines from SubscriptionsController:
load_and_authorize_resource :user
load_and_authorize_resource :subscription, through: :user
before_filter :authenticate_user!
Have no problems at all. So the issue in these lines or in Ability.rb. Any suggestions?
UPDATE: It's interesting that if i add smth like can? :index, Subscription to html template it displays true. If add smth like can? :index, Subscription.first (subscription of another user) it shows false. Looks like Cancan works normally. But what's the problem?..
UPDATE: If change SubscriptionsControlle like:
class SubscriptionsController < ApplicationController
#load_and_authorize_resource :user
#load_and_authorize_resource :subscription, through: :user
before_filter :authenticate_user!
def show
#user = User.find params[:user_id] #line 1
#subscription = #user.subscriptions.find params[:id] #line 2
#container_items = #subscription.container_items.paginate(:page => params[:page])
authorize! :show, #subscription #line 4
end
#some actions
end
It works perfect and prevent unauthorized user access when need.
Are the lines #1, 2 and 4 not equivalent to commented?..
UPDATE: Have the following in routes.rb:
resources :users, except: [:show] do
member do
get 'cart'
delete 'delete_from_cart' => 'users#delete_from_cart'
post 'add_to_cart' => 'users#add_to_cart'
end
resources :subscriptions do
member do
post 'pay'
end
end
end
UPDATE: Next solution prevent unauthorized access to all of subscriptions actions except index:
class SubscriptionsController < ApplicationController
load_resource :user
load_resource :subscription, through: :user
authorize_resource through: :current_user
before_filter :authenticate_user!
#actions
end
So what's the best way to prevent access to index action?
Found only following solution:
before_filter :authorize_index, only: [:index]
def authorize_index
raise CanCan::AccessDenied unless params[:user_id] == current_user.id.to_s
end
It should be
load_and_authorize_resource :subscription
or just
load_and_authorize_resource
in your case, when you want nested resource, then
load_and_authorize_resource :through => :current_user
see https://github.com/ryanb/cancan/wiki/Nested-Resources