Is it possible to DRY-up the following code:
def is_user?
is_role? ROLES[:user]
end
def is_mod?
is_role? ROLES[:mod]
end
def is_admin?
is_role? ROLES[:admin]
end
private
def is_role?(role)
self.roles & role == role
end
Into a single function, yet still have the ability to call the function names as currently (is_user?, is_mod?, etc)
UPDATE:
Using Aetherus' answer below I created the following for managing user roles (where a user can have multiple roles):
class User < ActiveRecord::Base
# Use bitwise values for more roles (double the previous values)
ROLES = { user: 1, dummy: 2, mod: 4, admin: 8 }
# Add the desired role
def add_role(role)
self.roles |= ROLES[role]
end
# eg: add_role :admin
# Removed the desired role
def remove_role(role)
self.roles &= ~ROLES[role]
end
# methods for each role (mod? admin? etc)
ROLES.keys.each do |role|
define_method("#{role}?") do
self.roles & ROLES[role] == ROLES[role]
end
end
end
You can define multiple methods with one single ruby code block.
%w(user mod admin).each do |role|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{role}?
role == '#{role}' && roles.include?('#{role}')
end
RUBY
end
Or a more clear way:
%w(user mod admin).each do |role|
define_method("#{role}?") do
self.role == role && roles.include?(role)
end
end
By the way, in ruby, the is_ prefix is not needed, since the trailing ? tells the programmers that method returns a true or a false.
You can go with method_missing.
The simplest possible solution would be something like:
class Roles
def method_missing(method_name, *args, &block)
if /^is_(?<role_name>\w+)\?$/ =~ method_name
is_role?(role_name.to_sym)
else
super
end
end
private
def is_role?(role_name)
# just for demo purposes
p "Checking #{role_name}"
end
end
roles = Roles.new
roles.is_user?
roles.is_mod?
roles.is_admin?
In method_missing I'm trying to catch any method that is not implemented (please note, I removed the proper methods is_user?, is_mod? and is_admin?), later, I'm checking if the name of method is of proper format with Regex (/^is_(?<role_name>\w+)\?$/), and if it is, I'm reusing captured role_name.
Slightly more restrictive method_missing.
Problem with this approach is, it will accept any method call, like let's say is_super_user?. In some cases this might be desirable, sometimes not. If you would like to restrict it only to the 3 type of users you've mentioned, you can change the Regex to:
/^is_(user|mod|admin)\?$/
One last thing. When implementing method_missing, you should also take care about respond_to_missing?, which is quite crucial when you would like to assert if the object responds to those magic methods:
class Roles
# ...
def respond_to_missing?(method_name, include_private = false)
/^is_(user|mod|admin)\?$/ =~ method_name
end
end
With this in place, you are able to do:
roles = Roles.new
roles.respond_to? :is_admin? # => true
roles.respond_to? :is_super_user? # => false
Read more here.
Hope that helps!
#Jacob, if you use rails 4, you can use AR#enum feature (http://api.rubyonrails.org/classes/ActiveRecord/Enum.html), no need to implement this by hands.
Related
I am facing a design decision I cannot solve. In the application a user will have the ability to create a campaign from a set of different campaign types available to them.
Originally, I implemented this by creating a Campaign and CampaignType model where a campaign has a campaign_type_id attribute to know which type of campaign it was.
I seeded the database with the possible CampaignType models. This allows me to fetch all CampaignType's and display them as options to users when creating a Campaign.
I was looking to refactor because in this solution I am stuck using switch or if/else blocks to check what type a campaign is before performing logic (no subclasses).
The alternative is to get rid of CampaignType table and use a simple type attribute on the Campaign model. This allows me to create Subclasses of Campaign and get rid of the switch and if/else blocks.
The problem with this approach is I still need to be able to list all available campaign types to my users. This means I need to iterate Campaign.subclasses to get the classes. This works except it also means I need to add a bunch of attributes to each subclass as methods for displaying in UI.
Original
CampaignType.create! :fa_icon => "fa-line-chart", :avatar=> "spend.png", :name => "Spend Based", :short_description => "Spend X Get Y"
In STI
class SpendBasedCampaign < Campaign
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
Neither way feels right to me. What is the best approach to this problem?
A not very performant solution using phantom methods. This technique only works with Ruby >= 2.0, because since 2.0, unbound methods from modules can be bound to any object, while in earlier versions, any unbound method can only be bound to the objects kind_of? the class defining that method.
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
enum :campaign_type => [:spend_based, ...]
def method_missing(name, *args, &block)
campaign_type_module.instance_method(name).bind(self).call
rescue NameError
super
end
def respond_to_missing?(name, include_private=false)
super || campaign_type_module.instance_methods(include_private).include?(name)
end
private
def campaign_type_module
Campaigns.const_get(campaign_type.camelize)
end
end
# app/models/campaigns/spend_based.rb
module Campaigns
module SpendBased
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
end
Update
Use class macros to improve performance, and keep your models as clean as possible by hiding nasty things to concerns and builder.
This is your model class:
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
include CampaignAttributes
enum :campaign_type => [:spend_based, ...]
campaign_attr :name, :fa_icon, :avatar, ...
end
And this is your campaign type definition:
# app/models/campaigns/spend_based.rb
Campaigns.build 'SpendBased' do
name 'Spend Based'
fa_icon 'fa-line-chart'
avatar 'spend.png'
end
A concern providing campaign_attr to your model class:
# app/models/concerns/campaign_attributes.rb
module CampaignAttributes
extend ActiveSupport::Concern
module ClassMethods
private
def campaign_attr(*names)
names.each do |name|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name}
Campaigns.const_get(campaign_type.camelize).instance_method(:#{name}).bind(self).call
end
EOS
end
end
end
end
And finally, the module builder:
# app/models/campaigns/builder.rb
module Campaigns
class Builder < BasicObject
def initialize
#mod = ::Module.new
end
def method_missing(name, *args)
value = args.shift
#mod.send(:define_method, name) { value }
end
def build(&block)
instance_eval &block
#mod
end
end
def self.build(module_name, &block)
const_set module_name, Builder.new.build(&block)
end
end
I've looked through the documentation and did some searching but i don't see an option for an omnipotent user (super user) level, or how one can be created.
Has anyone seen or created a why of doing this? im thinking it may be possible to tie into core authentication system but im not sure where to do the tie-in.
many thanks..
The only way to do this is to have your authorization checks return true for a user or role that has been designated a "super user." So, it would look like this:
def update?
*normal authorization logic* or is_superuser?
end
def edit?
*normal authorization logic* or is_superuser?
end
#etc...
private
def is_superuser?
# configure how to determine who the super users are and return true/false
end
You can define the is_superuser? private method in the ApplicationPolicy assuming that you inherit your class level policies from the Application Policy; otherwise, you will need to define it in each policy.
I've found a way a little DRYer, by using the inheritance of ApplicationPolicy. I alias access methods and I tie a superuser test before invoking any of them. If user is a super user, I simply return true. I do that at initialization before I need the instance method to be defined to be aliased.
ALIAS_PREFIX = '__original_'
def initialize(user, record)
#user = user
#record = record
[:index?,:show?,:create?,:new?, :update?, :edit?, :destroy?].each do |access_method|
alias_name = ALIAS_PREFIX+access_method.to_s
aliasing_original_method(access_method,alias_name)
self.class.send(:define_method, access_method) do |*args|
superuser? ? (return true) : send(alias_name, *args)
end
end
end
private
def superuser?
#whatever you want to define a super user
end
def aliasing_original_method(old_name, new_name)
self.class.send(:alias_method, new_name, old_name)
self.class.send(:private, new_name)
end
And in [AnyFile]Policy I do:
def initialize(user, record)
super(user, record)
end
This will ensure the true return for each method in sub-policy.
[UPDATE]
The first solution is a little messy, and my knowledge in ruby (and deadlines) doesn't permit me to push it farther. Anyway, I've found another way. As I was always switch-casing the roles of users, I've implemented in ApplicationPolicy a for_roles method.
def for_roles(*args,&block)
return true if superuser?
if args.include?(:all) || (#user.role_symbols & args).any?
block.call
else false
end
end
Then, in any policy, you can do for example
for_roles(:client_admin,:technician) do
#any rule computation, DB request you want
end
#or
for_roles(:all) do
#any rule computation, DB request you want
end
I have a Rails 4 app using Devise (the most recent) and am trying to create a random token for each user (like the ID, but longer, etc.) Using this answer I was able to come up with the follow code:
# app/models/model_name.rb
class ModelName < ActiveRecord::Base
include Tokenable
end
# app/models/concerns/tokenable.rb
module Tokenable
extend ActiveSupport::Concern
included do
before_create :generate_token
end
protected
def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless self.class.exists?(token: random_token)
end
end
end
This code works fantastically for tokens that are unique for any given model. I.e. All Users will have unique tokens, and all Admins will have unique tokens. But an Admin may have the same token as a User – this behavior is unwanted.
Is there an elegant way, short of abstracting the token into its own model and using "has_one" relationships, to ensure that the token does not exist in all the models it is a part of?
(I guess I could hard code unless (User.exists? ... or Admin.exists? ... ) into the unless clause, though this seems bulky.)
Any thoughts or suggestions are appreciated! Thanks!
Rails 5 comes with a new feaeture has_secure_tokenis really easy to use:
# Schema: User(token:string, auth_token:string)
class User < ActiveRecord::Base
has_secure_token :auth_token
end
user = User.new
user.save
user.auth_token # => "pX27zsMN2ViQKta1bGfLmVJE"
user.regenerate_auth_token # => true
Since Rails 5 isn't already released, you can use the Backport has_secure_token gem
I would create a method that lists each of the Classes that are including my concern and then test against the token for each. Something like this:
# app/models/concerns/tokenable.rb
module Tokenable
extend ActiveSupport::Concern
included do
before_create :generate_token
end
protected
def included_classes
ActiveRecord::Base.descendants.select do |c|
c.included_modules.include(Concerns::Tokenable)}.map(&:name)
end
end
def generate_token
self.token = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless included_classes.map {|c| c.constantize.exists?(token: random_token) }.include?(true)
end
end
end
So include_classes is going to return an array of names as strings of each of the classes that include the Tokenable concern. And then in the loop within generate_token is going to check against each of these classes generating an array of true or false which then we just check if any are true with include?(true).
Here is were I found how to get included classes (first answer).
EDIT
In Rails 5 the included_classes looks like this (note the ApplicationRecord and not needing the Concerns::Tokenable):
def included_classes
ApplicationRecord.descendants.select do |c|
c.included_modules.include?(Tokenable)
end
end
This is the weirdest thing ever happened to me with ruby/rails.
I have a model, Store, which has_many Balances. And I have a method that gives me the default balance based on the store's currency.
Store model.
class Store < ActiveRecord::Base
has_many :balances, as: :balanceable, dependent: :destroy
def default_balance
#puts self.inspect <- weird part.
balances.where(currency: self.currency)[0]
end
...
end
Balance model.
class Balance < ActiveRecord::Base
belongs_to :balanceable, :polymorphic => true
...
end
Ok, so then in the Balance controller I have the show action, that will give me a specific balance or the default one.
Balance controller.
class Api::Stores::BalancesController < Api::Stores::BaseController
before_filter :load_store
# Returns a specific alert
# +URL+:: GET /api/stores/:store_id/balances/:id
def show
#puts #store.inspect <- weird part.
#balance = (params[:id] == "default") ? #store.default_balance : Balance.find(params[:id])
respond_with #balance, :api_template => :default
end
...
private
# Provides a shortcut to access the current store
def load_store
#store = Store.find(params[:store_id])
authorize! :manage, #store
end
end
Now here is where the weird part comes...
If I make a call to the show action; for example:
GET /api/stores/148/balances/default
It returns null (because the currency was set as null, and there is no Balance with null currency), and the SQL query generated is:
SELECT `balances`.* FROM `balances` WHERE `balances`.`balanceable_id` = 148 AND `balances`.`balanceable_type` = 'Store' AND `balances`.`currency` IS NULL
So I DON'T know why... it is setting the currency as NULL. BUT if in any where in that process I put
puts #store.inspect
or inside the default_balance method:
puts self.inspect
it magically works!!!.
So I don't know why is that happening?... It seems like the store object is not getting loaded until I "inspect" it or something like that.
Thanks
Sam and Adrien are on the right path.
ActiveRecord overrides method_missing to add a whole bunch of dynamic methods including the accessors for the column-backed attributes like Store#currency. While I'm glossing over a lot, suffice it to say that when the logic is invoked then the dynamic class/instance methods are added to the Store class/instances so that subsequent calls no longer require the method_missing hook.
When YOU overrode method_missing without calling super, you effectively disabled this functionality. Fortunately, this functionality can be invoked by other means, one of which you tripped upon when you called store#inspect.
By adding the call to super, you simply assured that ActiveRecord's dynamic methods are always added to the class when they're needed.
OK finally after a lot of debugging, I found the reason...
In the Store model I have a method_missing method and I had it like this:
def method_missing method_name, *args
if method_name =~ /^(\w+)_togo$/
send($1, *args).where(togo: true)
elsif method_name =~ /^(\w+)_tostay$/
send($1, *args).where(tostay: true)
end
end
So when I was calling self.currency it went first to the method_missing and then returned null. What I was missing here was the super call.
def method_missing method_name, *args
if method_name =~ /^(\w+)_togo$/
send($1, *args).where(togo: true)
elsif method_name =~ /^(\w+)_tostay$/
send($1, *args).where(tostay: true)
else
super
end
end
But I continue wondering why after I had called puts #store.inspect or puts self.inspect it worked well?. I mean, why in that case that super call wasn't needed?
I'm defining my own AR class in Rails that will include dynamically created instance methods for user fields 0-9. The user fields are not stored in the db directly, they'll be serialized together since they'll be used infrequently. Is the following the best way to do this? Alternatives?
Where should the start up code for adding the methods be called from?
class Info < ActiveRecord::Base
end
# called from an init file to add the instance methods
parts = []
(0..9).each do |i|
parts.push "def user_field_#{i}" # def user_field_0
parts.push "get_user_fields && #user_fields[#{i}]"
parts.push "end"
end
Info.class_eval parts.join
One nice way, especially if you might have more than 0..9 user fields, would be to use method_missing:
class Info
USER_FIELD_METHOD = /^user_field_(\n+)$/
def method_missing(method, *arg)
return super unless method =~ USER_FIELD_METHOD
i = Regexp.last_match[1].to_i
get_user_fields && #user_fields[i]
end
# Useful in 1.9.2, or with backports gem:
def respond_to_missing?(method, private)
super || method =~ USER_FIELD_METHOD
end
end
If you prefer to define methods:
10.times do |i|
Info.class_eval do
define_method :"user_field_#{i}" do
get_user_fields && #user_fields[i]
end
end
end
Using method_missing is very difficult to maintain and unnecessary. The other alternative using define_method is better but leads to poorly performing code. The following 1 liner is all you need:
class Info
end
Info.class_eval 10.times.inject("") {|s,i| s += <<END}
def user_field_#{i}
puts "in user_field_#{i}"
end
END
puts Info.new.user_field_4