Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP

Loading…

Recipient-less features #24

Closed
wants to merge 6 commits into from

2 participants

@plukevdh
Collaborator

Would be nice to have simple on/off switches for features that don't require a recipient, especially for jobs/models that dont have or need access to what user is responsible for the action.

@plukevdh plukevdh was assigned
@plukevdh
Collaborator

Could be seen as relating to issue #8

@plukevdh plukevdh Add global features
Does not require a user. Is either on (deployment_percentage == 100)
or off (deployment_percentage != 100).
5a1cd32
@plukevdh
Collaborator

Still retrievable using Arturo::Feature.to_feature(:symbol)

Will return correct class (Feature or GlobalFeature)

Would appreciate feedback.

@plukevdh
Collaborator

hrm, the migration will be a bit tricky. need upgrade procedures if people want to use global features as it now requires a class_name field.

@plukevdh
Collaborator

Would also be nice to have a UI refresh that reflects global features.

@jamesarosen
Owner

I'd like to make it easier to quickly turn on/off features, but I feel like this is a pretty complex solution. What if we just enhance the UI to make it easy to push the % to 0 or 100 really easily?

@plukevdh
Collaborator
@plukevdh plukevdh closed this
@jamesarosen jamesarosen was assigned
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jun 28, 2012
  1. @plukevdh

    Add global features

    plukevdh authored
    Does not require a user. Is either on (deployment_percentage == 100)
    or off (deployment_percentage != 100).
  2. @plukevdh

    add quick test/check

    plukevdh authored
  3. @plukevdh
  4. @plukevdh

    helper for feature_enabled?

    plukevdh authored
  5. @plukevdh

    correct type check

    plukevdh authored
  6. @plukevdh

    nil case should be false

    plukevdh authored
This page is out of date. Refresh to see the latest.
View
2  .rvmrc
@@ -1 +1 @@
-rvm 1.9.2
+rvm 1.9.3@arturo --create
View
29 app/models/arturo/feature.rb
@@ -6,9 +6,10 @@ module Arturo
class Feature < ::ActiveRecord::Base
include Arturo::SpecialHandling
+ self.inheritance_column = 'class_name'
Arturo::Feature::SYMBOL_REGEX = /^[a-zA-z][a-zA-Z0-9_]*$/
- DEFAULT_ATTRIBUTES = { :deployment_percentage => 0 }.with_indifferent_access
+ DEFAULT_ATTRIBUTES = { :deployment_percentage => 0, :class_name => "Arturo::Feature" }.with_indifferent_access
attr_readonly :symbol
@@ -33,19 +34,6 @@ def initialize(attributes = {}, options = {}, &block)
super(DEFAULT_ATTRIBUTES.merge(attributes || {}), options, &block)
end
- # @param [Object] feature_recipient a User, Account,
- # or other model with an #id method
- # @return [true,false] whether or not this feature is enabled
- # for feature_recipient
- # @see Arturo::SpecialHandling#whitelisted?
- # @see Arturo::SpecialHandling#blacklisted?
- def enabled_for?(feature_recipient)
- return false if feature_recipient.nil?
- return false if blacklisted?(feature_recipient)
- return true if whitelisted?(feature_recipient)
- passes_threshold?(feature_recipient)
- end
-
def name
return I18n.translate("arturo.feature.nameless") if symbol.blank?
I18n.translate("arturo.feature.#{symbol}", :default => symbol.to_s.titleize)
@@ -59,6 +47,19 @@ def to_param
persisted? ? "#{id}-#{symbol.to_s.parameterize}" : nil
end
+ # @param [Object] feature_recipient a User, Account,
+ # or other model with an #id method
+ # @return [true,false] whether or not this feature is enabled
+ # for feature_recipient
+ # @see Arturo::SpecialHandling#whitelisted?
+ # @see Arturo::SpecialHandling#blacklisted?
+ def enabled_for?(feature_recipient)
+ return false if feature_recipient.nil?
+ return false if blacklisted?(feature_recipient)
+ return true if whitelisted?(feature_recipient)
+ passes_threshold?(feature_recipient)
+ end
+
def inspect
"<Arturo::Feature #{name}, deployed to #{deployment_percentage}%>"
end
View
33 app/models/arturo/feature/global_feature.rb
@@ -0,0 +1,33 @@
+module Arturo
+ class GlobalFeature < Feature
+ def base_class
+ GlobalFeature
+ end
+
+ # @return [true,false] whether or not this feature is enabled
+ def enabled?
+ deployment_percentage == 100
+ end
+
+ # @param [Object] feature_recipient a User, Account,
+ # or other model with an #id method
+ # @return [true,false] whether or not this feature is enabled
+ # really just an alias for #enabled?
+ def enabled_for?(feature_recipient)
+ enabled?
+ end
+
+ def enable!
+ update_attribute :deployment_percentage, 100
+ end
+
+ def disable!
+ update_attribute :deployment_percentage, 0
+ end
+
+ def inspect
+ "<Arturo::GlobalFeature #{name}, state: #{enabled? ? "enabled" : "disabled"}>"
+ end
+
+ end
+end
View
33 app/models/arturo/feature/user_feature.rb
@@ -0,0 +1,33 @@
+module Arturo
+ class UserFeature < Feature
+ def base_class
+ UserFeature
+ end
+
+ # @param [Object] feature_recipient a User, Account,
+ # or other model with an #id method
+ # @return [true,false] whether or not this feature is enabled
+ # for feature_recipient
+ # @see Arturo::SpecialHandling#whitelisted?
+ # @see Arturo::SpecialHandling#blacklisted?
+ def enabled_for?(feature_recipient)
+ return false if feature_recipient.nil?
+ return false if blacklisted?(feature_recipient)
+ return true if whitelisted?(feature_recipient)
+ passes_threshold?(feature_recipient)
+ end
+
+ def inspect
+ "<Arturo::UserFeature #{name}, deployed to #{deployment_percentage}%>"
+ end
+
+ protected
+
+ def passes_threshold?(feature_recipient)
+ threshold = self.deployment_percentage || 0
+ return false if threshold == 0
+ return true if threshold == 100
+ (((feature_recipient.id + (self.id || 1) + 17) * 13) % 100) < threshold
+ end
+ end
+end
View
7 lib/arturo.rb
@@ -19,6 +19,13 @@ def feature_enabled_for?(feature_name, recipient)
f && f.enabled_for?(recipient)
end
+ def feature_enabled?(feature_name)
+ f = self::Feature.to_feature(feature_name)
+ method_missing :feature_enabled?, feature_name if f && f.class != Arturo::GlobalFeature
+
+ f && f.enabled?
+ end
+
ENABLED_FOR_METHOD_NAME = /^(\w+)_enabled_for\?$/
def respond_to?(symbol)
View
4 lib/arturo/feature_factories.rb
@@ -1,4 +1,8 @@
Factory.define :feature, :class => Arturo::Feature do |f|
f.sequence(:symbol) { |n| "feature_#{n}".to_sym }
f.deployment_percentage { |_| rand(101) }
+end
+
+Factory.define :global_feature, :class => Arturo::GlobalFeature do |f|
+ f.sequence(:symbol) { |n| "feature_#{n}".to_sym }
end
View
1  lib/generators/arturo/templates/migration.rb
@@ -5,6 +5,7 @@ def self.up
create_table :features do |t|
t.string :symbol, :null => false
t.integer :deployment_percentage, :null => false
+ t.string :class_name, :null => false, :default => "Arturo::Feature"
#Any additional fields here
t.timestamps
View
1  test/dummy_app/db/migrate/20101017195547_create_features.rb
@@ -5,6 +5,7 @@ def self.up
create_table :features do |t|
t.string :symbol, :null => false
t.integer :deployment_percentage, :null => false
+ t.string :class_name, :null => false, :default => "Arturo::Feature"
#Any additional fields here
t.timestamps
View
1  test/dummy_app/db/schema.rb
@@ -16,6 +16,7 @@
create_table "features", :force => true do |t|
t.string "symbol", :null => false
t.integer "deployment_percentage", :null => false
+ t.string "class_name", :null => false, :default => "Arturo::Feature"
t.datetime "created_at", :null => false
t.datetime "updated_at", :null => false
end
View
8 test/dummy_app/test/unit/feature_test.rb
@@ -20,6 +20,14 @@ def test_to_feature
assert_nil ::Arturo::Feature.to_feature(:does_not_exist)
end
+ def test_does_not_respond_to_feature_enabled
+ assert_raises(NoMethodError) { ::Arturo.feature_enabled?(feature.symbol) }
+ end
+
+ def test_correct_class_name_used
+ assert_equal feature.class_name, "Arturo::Feature"
+ end
+
def test_feature_enabled_for_existent_feature
feature.update_attribute(:deployment_percentage, 100)
recipient = stub('User', :to_s => 'Paula', :id => 12)
View
61 test/dummy_app/test/unit/global_feature_test.rb
@@ -0,0 +1,61 @@
+require File.expand_path('../../test_helper', __FILE__)
+
+class ArturoGlobalFeatureTest < ActiveSupport::TestCase
+ def setup
+ reset_translations!
+ end
+
+ def feature
+ @feature ||= Factory(:global_feature)
+ end
+
+ def test_correct_class_name_used
+ assert_equal feature.class_name, "Arturo::GlobalFeature"
+ end
+
+ def test_responds_to_feature_enabled_helper
+ feature.enable!
+ assert ::Arturo.feature_enabled?(feature.symbol)
+ end
+
+ def test_feature_enabled_false_for_nil_feature
+ assert !::Arturo.feature_enabled?(:not_found)
+ end
+
+ def test_to_feature_finds_global_feature
+ assert_equal feature, ::Arturo::Feature.to_feature(feature)
+ assert_equal feature, ::Arturo::Feature.to_feature(feature.symbol)
+ assert_equal Arturo::GlobalFeature, feature.class
+ end
+
+ def test_enable_feature
+ feature.enable!
+ assert_equal 100, feature.deployment_percentage
+ assert feature.enabled?, "Feature should be enabled"
+ end
+
+ def test_disable_feature
+ feature.deployment_percentage == 100
+
+ feature.disable!
+ assert_equal 0, feature.deployment_percentage
+ assert !feature.enabled?, "Feature should be disabled."
+ end
+
+ def test_global_feature_overrides_enabled_for
+ feature.update_attribute(:deployment_percentage, 100)
+ recipient = stub('User', :to_s => 'Paula', :id => 12)
+ assert ::Arturo.feature_enabled_for?(feature.symbol, recipient), "#{feature} should be enabled for #{recipient}"
+
+ feature.update_attribute(:deployment_percentage, 99)
+ assert !::Arturo.feature_enabled_for?(feature.symbol, recipient), "#{feature} should not be enabled for #{recipient}"
+ end
+
+ def test_global_feature_enabled_only_for_full_engagement
+ feature.update_attribute(:deployment_percentage, 22)
+ assert !feature.enabled?
+
+ feature.update_attribute(:deployment_percentage, 100)
+ assert feature.enabled?
+ end
+end
Something went wrong with that request. Please try again.