Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

Feature idea: Generalize guarded links to REST actions from views #57

Closed
kristianmandrup opened this Issue Apr 20, 2010 · 16 comments

Comments

Projects
None yet
3 participants

I thought having all the link logic in the view was kinda ugly, so I refactored a bit and a clear pattern emerged!

# experiences/show.html.erb
        <%= edit_link(experience) %> 
        <%= destroy_link(experience) %>
# experiences_helper.rb 
  def create_link
    link_to('New', [:create, experience]) if can?(:create, Experience)  
  end
  
  def edit_link
    link_to('Edit', [:edit, experience]) if can?(:edit, Experience)  
  end

  def delete_link
    link_to('Delete', [:destroy, experience]) if can?(:delete, Experience)  
  end

  def read_link
    link_to('Show', experience) if can?(:read, Experience)  
  end

Not sure if the symbols are all correct in the example above, but still the pattern is there!
Reminds me a lot of the Rails framework Hobo where they end up with the same kind of pattern and have it generalized. How would I make all my model helpers have these methods generated and available for my views in a nice and easy fashion. Would make for a nice cancan add-on I think ;)

Looking forward to your next screencast - hoping it is about how to extend devise with cancan :)

Owner

ryanb commented Apr 20, 2010

I don't think this should go in CanCan, but I think it is a great pattern to follow. considering it's only a few methods it can easily be moved into a helper module. I'll add a wiki page for this. BTW, you can make this more generic like this.

def create_link(object)
  link_to("New", [:new, object]) if can?(:create, object)
end

def edit_link(object)
  link_to("Edit", [:edit, object]) if can?(:edit, object)
end

def delete_link(object)
  link_to("Delete", object, :method => :delete, :confirm => "Are you sure?") if can?(:destroy, object)
end

def show_link(object)
  link_to("Show", object) if can?(:read, object)
end

You'll likely want to customize to fit your needs.

Thanks! I'm adding it to a general authentication-assistance gem, so far targeting devise and cancan. I also added an optional label argument.
Was a great 2. screencast about devise, would be could to see one about integrating it with cancan. I'll try that myself today. Maybe post a wiki page about it. Just the same!

Owner

ryanb commented Apr 20, 2010

There shouldn't be any necessary steps to integrate with Devise. CanCan only requires a current_user method which Devise already provides. If you do have trouble integrating it, please post an issue.

I am trying to generalize the login and registration menu links in a similar fashion.

<% can? :destroy, Session %>
  
  • <%= link_to('Logout', destroy_admin_session_path) %>
  • <% end %>

    Would be nice with a convenience 'shortcut' for can? and can_not? like this:

    <% can? :sign_out %>
      
  • <%= sign_out_link('Log out') %>
  • <% end %>

    And similarly for :sign_in, :sign_up etc. What do you think? Does it fit inside cancan or should I monkeypatch or extend these methods in my own extension gem?

    Owner

    ryanb commented Apr 20, 2010

    Do you want to restrict who can access the sign-in and sign-out process? Usually those pages should be accessible from everyone and should not go through CanCan. You probably won't need to use CanCan on the controller actions that Devise manages. It should work great for managing everything else in the app though.

    If you do need to manage permissions on the Devise actions then feel free to provide an example and I'll see if there's a good solution.

    Sorry, I was on autopilot there, creating helepr methods with my mind wandering off... you are totally right!

    Hi Ryan,

    I am pretty close to having my AuthAssistant working, however I am having severe problems getting link_to to work form within a helper method!!! Please help!

    Trying to add a simple helper method to my views in order to ensure links are only displayed if the user has the proper rights to access that functionality.

      def show_link(object, label = nil) 
        label ||= auth_labels[:show] 
        if can?(:read, object)
          link_to(label, object) 
        end
      end
    

    But using link_to from within this context, somehow I don't have access to the controller variable used internally, fx in url_for.

      def url_for(options = {})
        ...
        when :back
          escape = false
          controller.request.env["HTTP_REFERER"] || 'javascript:history.back()'
        else
        ...
      end
    

    If I output self in the show_link method, I can see that my current context of self is ProjectsController, so I guess it makes sense that I don't have a method controller inside an instance of ActionController?
    I access my helper method from here:

    index.html.erb

    <%= show_link project %>

    And the setup

    if defined? ActionController
    ActionController::Base.class_eval do
    AuthAssistant::ViewHelpers::AuthLink
    end
    end

    module AuthAssistant::ViewHelpers::AuthLink

      def self.included(base)  
        base.helper_method :create_link, :delete_link, :edit_link, :show_link
        base.helper_method :new_link, :destroy_link, :update_link, :read_link                        
      end
    

    I thought maybe the issue was that I someone wrongly ended up inside the context of an ActionController instead of inside a Helper? I am lost...

    It worked only when I included them in ApplicationController, but this seems like an ugly hack :(

    module ApplicationHelper
    include AuthAssistant::ViewHelpers
    end

    Also my create_link

      def create_link(object, label = nil)
        label ||= auth_labels[:new]
        link_to(label, [:new, object]) if can?(:create, object)
      end
    

    Creates a link in the form http://localhost:3000/projects/new.1
    Strange!

    Owner

    ryanb commented Aug 3, 2010

    I created a wiki page for the link helpers. You can find it here.

    http://wiki.github.com/ryanb/cancan/link-helpers

    voxik commented Aug 13, 2010

    The create_link as is mentioned in wiki cannot work IMO. It should be modified as follows:

    def create_link(object, content = "New")
      link_to(content, [:new, object.name.underscore.to_sym]) if can?(:create, object)
    end
    

    and then used as follows:

    <%= create_link Project, 'New Project' %>
    
    Owner

    ryanb commented Aug 13, 2010

    Thanks for pointing this out. Should it be object.class.name.underscore.to_sym? I'll need to experiment.

    Owner

    ryanb commented Aug 13, 2010

    Oh wait, I see you are passing the class in the method. Maybe a check to see if it's a class and then change behavior. Sometimes a new instance is necessary to perform a full check.

    voxik commented Aug 13, 2010

    I'm not CanCan expert yet, but what is the scenario for the case you are describing: "Sometimes a new instance is necessary to perform a full check."? I'm just wondering it is not too complex scenario for this helper anyway ...

    Great to hear that the link helpers are being standardized in a fashion that works for all.
    I have been developing a whole bunch of role related gems to make it easy to apply a role strategy to a project.

    http://github.com/kristianmandrup/roles_generic

    There are also role strategies and generators for a set of common Rails orms, such as DM, AR, MM and Mongoid. They will all soon be added to my auth-assistant gem:

    http://github.com/kristianmandrup/auth-assistant

    This gem which will undergo a big refactoring effort to taking advantage of all these new gems, making it much simpler and light weight. It will also have the link helpers integrated in a Rails engine fashion, similar to what you find in Devise.

    It also includes a nice wrapper structure around cancan, to make it easy to centralize the permission logic and have one Permission class per role.

    I might need to update it to reflect the latest cancan feature additions ;)

      class Admin < Base
        def initialize(ability)
          super
        end
    
        def permit?(user)    
          super
          return if !user.role? :admin    
          can :manage, :all    
        end  
      end
    

    Finally a roles and permissions system will be really easy and flexible to setup with Rails :)

    Owner

    ryanb commented Aug 13, 2010

    @voxik, the new/create action can be dependent upon certain attributes such as being nested under a record which one owns.

    can :create, Task, :project => { :user_id => user.id }
    
    <%= create_link @project.tasks.build %>
    

    This issue was closed.

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment