Using cancan the proper way - ruby-on-rails

I have a certain requirement where the views have different content based upon the type of user. Lets say I have the index action for the users controller. Then I can use cancan to authorize the action like this
authorize! :index, #users
Further for filtering the content I have another authorization like
if can :view_all,User
Further another authorization like
if can :view_some,User will require another one.
This will result in lots of conditions. Instead of this, I could have used just simple conditions like
If the user is with view_all access show him all
else if the user is with view_some access show him some
else access denied
Cancan requires one extra query, isn't it? I might be using cancan the wrong way. So need some suggestions.
Here is the rough snippet of my ability.rb file
can :index, User do |user1|
role.accesses.include?(Access.where(:name => "access1").first) || role.accesses.include?(Access.where(:name => "access2").first)
end
can :view_all, User do |user1|
role.accesses.include?(Access.where(:name => "access1").first)
end
can :view_some, User do |user1|
role.accesses.include?(Access.where(:name => "access2").first)
end

Cancan requires one extra query?
When combining abilities, cancan will use a single query.
If you look at the specs, eg. spec/cancan/model_adapters/active_record_adapter_spec.rb, you'll find specs like this:
it "should fetch any articles which are published or secret", focus: true do
#ability.can :read, Article, :published => true
#ability.can :read, Article, :secret => true
article1 = Article.create!(:published => true, :secret => false)
article2 = Article.create!(:published => true, :secret => true)
article3 = Article.create!(:published => false, :secret => true)
article4 = Article.create!(:published => false, :secret => false)
Article.accessible_by(#ability).should == [article1, article2, article3]
end
And if you turn on SQL logging, you'll see that the query combines the conditions:
Article Load (0.2ms)
SELECT "with_model_articles_95131".*
FROM "with_model_articles_95131"
WHERE (("with_model_articles_95131"."secret" = 't')
OR ("with_model_articles_95131"."published" = 't'))

Related

Rails / Redis / Soulmate - How to limit search results by user role?

I have set up my app to use Soulmate and soulmate.js for autocomplete. It searches two models - Users and Books. But, I have multiple roles for users - some are authors and some are readers. I only want authors to show up in the results.
Is there a way to limit what is added to Redis as new users sign up, based on their role?
in user.rb (using the "if role = "author") did not work
def load_into_soulmate
loader = Soulmate::Loader.new("authors")
loader.add("term" => fullname, "id" => self.id, "data" => {
"link" => Rails.application.routes.url_helpers.user_path(self)
}) if self.role = "author"
end
def remove_from_soulmate
loader = Soulmate::Loader.new("authors")
loader.remove("id" => self.id)
end
in pages.js
$('#search').soulmate({
url: '/search/search',
types: ['books','authors'],
renderCallback : render,
selectCallback : select,
minQueryLength : 2,
maxResults : 5
})
Alternatively, could I add the role to what Redis stores and then tell the js to only serve users where role = "author"?
Thanks!!
I solved it with a conditional in the after_save in user.rb:
after_save :load_into_soulmate, :if => :is_author
and
def is_author
if self.role == "author"
true
else
false
end
end

using scope in Rails custom validations

I want to apply scope limiter in my custom validation
I have this Product Model
which has make,model,serial_number, vin as a attributes
Now I have a custom validation to check against vin if vin is not present to check for combination of make+model+serial_number uniqueness in database something like this
validate :combination_vin,:if => "vin.nil?"
def combination_vin
if Product.exists?(:make => make,:model => model,:serial_number => serial_number)
errors.add(:base,"The Combination of 'make+model+serial_number' already present")
end
end
I want to introduce a scope in this validator against user_id
Now I know I could easily write this to achieve same using
def combination_vin
if Product.exists?(:make => make,:model => model,:serial_number => serial_number,:user_id => user_id)
errors.add(:base,"The Combination of 'make+model+serial_number' already present")
end
end
But out of curiosity I was thinking is there a scope validator (something like {:scope => :user_id}) on custom validation
so that I dont have to pass that extra user_id in the exists? hash
Thanks
Try :
validate :combination_vin , :uniqueness => { :scope => :user_id } , :if => "vin.nil?"

acts_as_taggable_on with permissions required to create new tags

How can I prevent users from adding new tags which don't already exist in the tags db?
I want them to be able to add any tags that already exist to another model which they can fully edit, but not be able to create new tags if they don't yet exist?
I'm using declarative_auth so some users with permissions should be create to add whatever tags they want.
user.rb
acts_as_tagger
post.rb
acts_as_taggable_on :features
https://github.com/mbleigh/acts-as-taggable-on
UPDATE:
This seems to do it except I can't get the error message variable to work:
validates :feature_list, :inclusion => {
:in => SomeModel.tag_counts_on(:features).map(&:name),
:message => "does not include {s}" }
I havn't used acts_as_taggable, but can you pass normal rails validations?
# LIKE is used for cross-database case-insensitivity
validates_inclusion_of :name => lambda { find(:all, :conditions => ["name LIKE ?", name]) }
Could probably be more robust and rails validation like but this works:
validate :valid_feature_tag
def valid_feature_tag
invalid_tags = false
feature_list.each do |tag|
list = SomeModel.tag_counts_on(:features).map(&:name)
unless list.include?(tag)
invalid_tags = true
end
end
unless invalid_tags == false
errors.add(:feature_list, 'cannot contain new tags, please suggest new tags to us')
return false
else
return true
end
end
Here's an efficient and clean way to enforce allowed tags:
validate :must_have_valid_tags
def must_have_valid_tags
valid_tags = ActsAsTaggableOn::Tag.select('LOWER(name) name').where(name: tag_list).map(&:name)
invalid_tags = tag_list - valid_tags
if invalid_tags.any?
errors.add(:tag_list, "contains unknown tags: [#{invalid_tags.join(', ')}]")
end
end

Trying to master Ruby. How can I optimize this method?

I'm learning new tricks all the time and I'm always on the lookout for better ideas.
I have this rather ugly method. How would you clean it up?
def self.likesit(user_id, params)
game_id = params[:game_id]
videolink_id = params[:videolink_id]
like_type = params[:like_type]
return false if like_type.nil?
if like_type == "videolink"
liked = Like.where(:user_id => user_id, :likeable_id => videolink_id, :likeable_type => "Videolink").first unless videolink_id.nil?
elsif like_type == "game"
liked = Like.where(:user_id => user_id, :likeable_id => game_id, :likeable_type => "Game").first unless game_id.nil?
end
if liked.present?
liked.amount = 1
liked.save
return true
else # not voted on before...create Like record
if like_type == "videolink"
Like.create(:user_id => user_id, :likeable_id => videolink_id, :likeable_type => "Videolink", :amount => 1)
elsif like_type == "game"
Like.create(:user_id => user_id, :likeable_id => game_id, :likeable_type => "Game", :amount => 1)
end
return true
end
return false
end
I would do something like:
class User < ActiveRecord::Base
has_many :likes, :dependent => :destroy
def likes_the(obj)
like = likes.find_or_initialize_by_likeable_type_and_likeable_id(obj.class.name, obj.id)
like.amount += 1
like.save
end
end
User.first.likes_the(VideoLink.first)
First, I think its wrong to deal with the "params" hash on the model level. To me its a red flag when you pass the entire params hash to a model. Thats in the scope of your controllers, your models should have no knowledge of the structure of your params hash, imo.
Second, I think its always cleaner to use objects when possible instead of class methods. What you are doing deals with an object, no reason to perform this on the class level. And finding the objects should be trivial in your controllers. After all this is the purpose of the controllers. To glue everything together.
Finally, eliminate all of the "return false" and "return true" madness. The save method takes care of that. The last "return false" in your method will never be called, because the if else clause above prevents it. In my opinion you should rarely be calling "return" in ruby, since ruby always returns the last evaluated line. In only use return if its at the very top of the method to handle an exception.
Hope this helps.
I'm not sure what the rest of your code looks like but you might consider this as a replacement:
def self.likesit(user_id, params)
return false unless params[:like_type]
query = {:user_id => user_id,
:likeable_id => eval("params[:#{params[:like_type]}_id]"),
:likeable_type => params[:like_type].capitalize}
if (liked = Like.where(query).first).present?
liked.amount = 1
liked.save
else # not voted on before...create Like record
Like.create(query.merge({:amount => 1}))
end
end
I assume liked.save and Like.create return true if they are succesful, otherwise nil is returned. And what about the unless game_id.nil? ? Do you really need that? If it's nil, it's nil and saved as nil. But you might as well check in your data model for nil's. (validations or something)

rendering specific fields with Rails

If I have an object say
#user
and I want to render only certain fields in it say first_name and last_name(I'm using AMF)
render :amf => #user
For instance I have a property for #user which is 'dob' (date of birth) I would like to use it inside the controller business logic but I don't want to send it to the client (in this case Flex) I can defenitaly do something like this before rendering:
#user.dob = nil
But I thought there must be a better way of doing this.
how do I do that?
I know I can use :select when doing the 'find' but I need to use the other field at the server side but don't want to send them with AMF to the client side and I don't want to do a second 'find'
Thanks,
Tam
This article gives the details for the approach.
You have configure the config/rubyamf_config.rb file as follows:
require 'app/configuration'
module RubyAMF
module Configuration
ClassMappings.ignore_fields = ['created_at','updated_at']
ClassMappings.translate_case = true
ClassMappings.assume_types = false
ParameterMappings.scaffolding = false
ClassMappings.register(
:actionscript => 'User',
:ruby => 'User',
:type => 'active_record',
:associations => ["employees"],
:ignore_fields => ["dob"]
:attributes => ["id", "name", "location", "created_at", "updated_at"]
)
ClassMappings.force_active_record_ids = true
ClassMappings.use_ruby_date_time = false
ClassMappings.use_array_collection = true
ClassMappings.check_for_associations = true
ParameterMappings.always_add_to_params = true
end
end

Resources