I'm trying to make a concern that checks if a user is subscribed to an appropriate plan for my SaaS app.
Here's basically what I'm trying to do:
module SubscriptionControlled extend ActiveSupport::Concern
class_methods do
def requires_subscription_to(perm)
##perms = [perm]
end
end
included do
validate :check_subscription
end
def check_subscription
##perms.each do |perm|
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
This provides this api for a model:
class SomeModel < ApplicationModel
include SubscriptionControlled
requires_subscription_to :pro
end
The problem I'm having is that ##perms seems to be scoped to the CONCERN, rather than the MODEL. So this value is the same for all models. So whichever model is loaded last sets this value for all models.
eg: if loaded in this order:
Model1 -> sets ##perms to [:pro]
Model2 -> sets ##perms to [:business]
Both model 1 and model 2 will only require a subscription to :business
Is there a way of storing class-level variables in a concern that take effect on a per-model basis to accomplish this API?
I don't have a Ruby interpreter at hand right now but I'm fairly certain that using a single # in the class method should do the trick. Another thing that comes to mind is something along the lines of
included do
define_singleton_method :requires_subscription_to do |new_perm|
##perms ||= []
##perms << Array(new_perm)
end
end
Since that will create a new method every time the concern is included, it should work. I just seem to remember that methods defined like that are slightly slower - but since it will probably only be called during initialization, it shouldn't pose a problem in any case.
So I found the right way to do this using a ClassMethods module
module SubscriptionControlled extend ActiveSupport::Concern
module ClassMethods
#perms = []
def requires_subscription_to(perm)
#perms = [perm]
end
def perms
#perms
end
end
included do
validate :check_subscription
end
def check_subscription
self.class.perms.each do |perm|
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
this keeps the permissions scoped to the class, not the concern.
I think you're overcomplicating this. You don't need the check_subscription method at all and that method is why you're trying to make ##perms (or #perm) work.
validate is just a class method like any other and you can give validate block. You can use that block to capture the perm and do away with all the extra machinery:
module SubscriptionControlled extend ActiveSupport::Concern
module ClassMethods
def requires_subscription_to(perm)
validate do
self.errors.add(:base, "Subscription upgrade required for access to this feature") unless self.user[perm]
end
end
end
end
Related
I am just getting my hands on Concerns in Rails and try to implement a simple logging for ActiveRecord classes. In there I want to define the field that should go into the log and have the log written automatically after save.
What I have is this:
#logable.rb (the concern)
module Logable
extend ActiveSupport::Concern
#field = nil
module ClassMethods
def set_log_field(field)
#feild = field
end
end
def print_log
p "LOGGING: #{self[#index.to_s]}"
end
end
#houses.rb (the model using the concern)
class House < ActiveRecord::Base
include Logable
after_save :print_log
set_log_field :id
end
Unfortunately the call to set_log_field does not have an effect - or rather the given value does not make it to print_log.
What am I doing wrong?
Thanks for your help!
You probably mean this (btw, why not Loggable?):
# logable.rb
module Logable
extend ActiveSupport::Concern
# Here we define class-level methods.
# Note, that #field, defined here cannot be referenced as #field from
# instance (it's class level!).
# Note also, in Ruby there is no need to declare #field in the body of a class/module.
class_methods do
def set_log_field(field)
#field = field
end
def log_field
#field
end
end
# Here we define instance methods.
# In order to access class level method (log_field), we use self.class.
included do
def print_log
p "LOGGING: #{self.class.log_field}"
end
end
end
Update You also asked about what's the difference between methods in included block and those within method body.
To make a short resume there is seemingly no difference. In very good approximation you can consider them the same. The only minor difference is in dependency management. Great illustration of it is given in the end of ActiveSupport::Concern documentation. It worth reading, take a look!
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 have an ActiveSupport::Concern module which looks roughly like the following:
module MyModel
module Acceptance
extend ActiveSupport::Concern
included do
enum status: [:declined, :accepted]
end
def declined!
self.status = :declined
# some extra logic
self.save!
end
def accepted!
self.status = :accepted
# some extra logic
self.save!
end
end
end
This is only ever going to be included into ActiveRecord classes, hence the use of enum. Basically, I'm overriding the declined! and accepted! methods that are created by ActiveRecord::Enum.enum with some extra, custom logic of my own.
The problem is, this doesn't work, because when I call #model.declined! it justs call the original implementation of declined! and ignores my custom method.
Looks like my custom methods are being included into the calling class before the included block is being run - meaning my custom methods are being overridden by the ones defined by enum, instead of the other way around.
There some easy workarounds in this particular situation (e.g. I could move the call enum back into the including class and make sure it's above the line include MyModel::Acceptance, but I'm wondering if there's a way I can solve this problem while keeping it all in the same module.
Is there any way I can call a class method within included that defines an instance method, then override that instance method from within the same Concern module?
I think you're looking for define_method.
module MyModel
module Acceptance
extend ActiveSupport::Concern
included do
enum status: [:declined, :accepted]
define_method :declined! do
self.status = :declined
# some extra logic
self.save!
end
define_method :accepted! do
self.status = :accepted
# some extra logic
self.save!
end
end
end
end
I would like to setup a before_create for all of my modules
what i have been trying is:
module ActiveRecord
module UserMonitor
require 'securerandom'
before_create :attach_uuid
def attach_uuid
self.uuid = SecureRandom.uuid.gsub("-","")
end
end
end
This does not seem to be working.
if i go into each module and add it in there it works, but i want to do it on a global scale.
Any thoughts or ideas on how i can achieve this in this manner? i know i could do it in triggers and such but i don't want to go that route and i would like to avoid hitting every module/class in case i need to change something.
Currently using Ruby 1.9.3 Can not currently upgrade my app until i make future code changes.
Thanks!
An other solution - I use, is to put the logic for UUID in an own module, that you include. I already have some (class-) methods I add to my AR, like set_default_if, so it was a good place for me.
module MyRecordExt
def self.included base
base.extend ClassMethods # in my case some other stuff
base.before_create :attach_uuid # now add the UUID
end
def attach_uuid
begin
self.uuid = SecureRandom.uuid
rescue
# do the "why dont we have a UUID filed?" here
end
end
# some other things not needed for add_uuid
module ClassMethods
include MySpecialBase # just an eg.
def default_for_if(...)
...
end
end
end
and then
class Articel < ActiveRecord::Base
include MyRecordExt
...
end
In general I avoid doing something for ALL models modifying AR base - I made the first bad experience with adding the UUID to all, and crashed with devise GEMs models ...
If you define attach_uuid in the ActiveRecord module, can't you just call the before_create :attach_uuid at the top of each controller? This is DRY.
Is there a UserMonitor controller that you could add it to?
class UserMonitor < ActiveRecord::Base
before_create :attach_uuid
end
I have a very big function in my model and I want to store it somewhere else in order to keep my model dry. I read that storing methods in ApplicationHelper and then calling them from a model is a bad idea. What is a good idea then?
I want to have a separate file with my big methods and call them from a model.
You can create a "plain old ruby object (PORO)" to do your work for you. let's say you had a method that calculates the amount overdue for a user.
So, you can create app/services/calculates_overages.rb
class CalculatesOverages
def initialize(user)
#user = user
end
def calculate
# your method goes here
end
end
Then, you can:
class User < ActiveRecord::Base
def overage_amount
CaluclatesOverage.new(self).calculate
end
end
Or, in a controller you could:
def show
#amount = CaluclatesOverage.new(current_user).calculate
end
The app/services directory could also be app/models, or the lib directory. There's no set convention for this (yet).
Use a Concern. https://gist.github.com/1014971
It's simple. In app/models/concerns create a file your_functionality.rb as follows:
module YourFunctionality
extend ActiveSupport::Concern
def your_fat_method
# insert...
end
end
And in your model simply:
include YourFunctionality