sapling 0.2.2 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -44,10 +44,16 @@ Server-side Usage
44
44
 
45
45
  To check if a feature is enabled for a user in rails controllers or views: use
46
46
 
47
- feature_active?(:space_chat [, :user => the current user])
47
+ sapling.active?(:space_chat [, :user => the current user])
48
+
48
49
 
49
50
  *Note*: sapling will automatically populate the user argument by calling `current_user`
50
51
 
52
+ To get the css classes the javascript adds to the HTML element for a feature:
53
+
54
+ sapling.css_class(:space_chat)
55
+ sapling.css_toggle_class(:space_chat)
56
+
51
57
  To enable a feature for a specific user, in the rails console:
52
58
 
53
59
  Sapling::ActiveRecord.new.activate_user(:space_chat, space_admin)
@@ -67,6 +73,23 @@ To disable a feature activated for anyone but individually-activated users, in t
67
73
  *Note*: Individually-activated users are always activated, regardless of the percentage setting. A deactivated user
68
74
  may still have access to a feature if they fall within an active percentage.
69
75
 
76
+ Custom Feature-Active Tests
77
+ ---------------------------
78
+
79
+ You can optionally inject your own code for testing if a feature is active. In rails, just added methods of the following form to your ApplicationController:
80
+
81
+ # Example: override feature-active? for feature :new_homepage
82
+ # options:
83
+ # :feature => object of type Sapling::Feature
84
+ # (current settings for the feature from the database)
85
+ # :user => user to test for feature-active
86
+ # return true if the feature is active.
87
+ def new_homepage_active?(options={})
88
+ # your test here
89
+ end
90
+
91
+ *Note* If you are using Sapling manually, when you create a Sapling instance, you can pass in an any object which responds to "current_user" and implements zero or more of your feature-active overrides.
92
+
70
93
  Client-side Usage
71
94
  -----------------
72
95
 
@@ -100,11 +123,11 @@ CSS:
100
123
 
101
124
  ERB:
102
125
 
103
- <div class="<%= feature_class(:chat) %> enabled">
126
+ <div class="<%= sapling.css_class(:chat) %> enabled">
104
127
  REJOICE - SPIFF CHAT ENABLED FOR YOU
105
128
  </div>
106
129
 
107
- <div class="<%= feature_class(:chat) %> disabled">
130
+ <div class="<%= sapling.css_class(:chat) %> disabled">
108
131
  DESPAIR - SPIFF CHAT NOT ENABLED FOR YOU
109
132
  </div>
110
133
 
@@ -116,5 +139,4 @@ TODO
116
139
 
117
140
  * Rails 3 compatibility
118
141
  * Remove mootools dependency
119
- * CSS generator
120
142
  * Database migration generator
@@ -0,0 +1,10 @@
1
+ API changes in 0.3.0
2
+ ====================
3
+
4
+ When using Sapling in rails, there are some api changes:
5
+
6
+ old: feature_active?(feature_name)
7
+ new: sapling.active?(feature_name,options={})
8
+
9
+ old: feature_class(feature_name)
10
+ new: sapling.css_class(feature_name)
@@ -1,11 +1,11 @@
1
1
  require "sapling/version"
2
2
  require "sapling/util"
3
3
  require "sapling/base"
4
+ require "sapling/feature"
4
5
  require "sapling/memory"
5
- require "sapling/model"
6
+ require "sapling/active_record_model"
6
7
  require "sapling/active_record"
7
8
  require "sapling/generators/javascript_generator"
8
- require "sapling/generators/css_generator"
9
9
 
10
10
  module Sapling
11
11
  # Your code goes here...
@@ -2,71 +2,58 @@ module Sapling
2
2
  class ActiveRecord < Base
3
3
 
4
4
  def table_name
5
- Model.table_name
5
+ ActiveRecordModel.table_name
6
6
  end
7
7
 
8
- private
9
- def query_conditions(feature,normalized_options={})
10
- ret=[
11
- "#{'feature = ? AND ' if feature} ((user_id IS NOT NULL AND user_id = ?) OR (percentage IS NOT NULL AND ? < percentage)) "
12
- ]
13
- ret<<feature.to_s if feature
14
- ret+=[
15
- normalized_options[:user_id],
16
- Util::modded_context_id(normalized_options)
17
- ]
18
- ret
19
- end
20
- public
21
-
22
- module ClientAPI
23
- # see Sapling::API::Client
24
- def active?(feature, options={})
25
- options = Util.normalized_options options
26
- v = Model.count(:conditions => query_conditions(feature,options))
27
- v > 0
28
- end
29
-
30
- def features
31
- features = Model.find :all, :select => "feature", :group => "feature", :order => "feature"
32
- features.map {|record| record.feature.to_sym}
8
+ # returns hash of all features
9
+ # keys are feature names (symbols)
10
+ # values are:
11
+ # :percentage => nil or 0-100
12
+ # :user_ids => [] or array of user_ids (integers)
13
+ def features
14
+ @features ||= ActiveRecordModel.find(:all).inject({}) do |ret,feature|
15
+ name = feature.feature.to_sym
16
+ ret[name]||=Feature.new(name)
17
+ ret[name].percentage ||= feature.percentage
18
+ ret[name].users[feature.user_id]=true if feature.user_id
19
+ ret
33
20
  end
21
+ end
34
22
 
35
- # returns a list of features enabled for a user
36
- # see Sapling::API::Client
37
- def active_features(options={})
38
- options = Util.normalized_options options
39
- features = Model.find :all, :conditions => query_conditions(nil,options)
40
- features.map {|record| record.feature.to_sym}.uniq
41
- end
23
+ def reload
24
+ @features=nil
25
+ true
42
26
  end
43
- include ClientAPI
44
27
 
45
28
  module AdminAPI
46
29
 
47
30
  def activate_user(feature, user)
48
- Model.transaction do
31
+ ActiveRecordModel.transaction do
49
32
  deactivate_user(feature, user)
50
- Model.create(:feature => feature.to_s, :user_id => user.id)
33
+ ActiveRecordModel.create(:feature => feature.to_s, :user_id => user.id)
51
34
  end
35
+ reload
52
36
  end
53
37
 
54
38
  def deactivate_user(feature, user)
55
- Model.delete_all ["feature = ? AND percentage IS NULL and user_id = ?",feature.to_s,user.id]
39
+ ActiveRecordModel.delete_all ["feature = ? AND percentage IS NULL and user_id = ?",feature.to_s,user.id]
40
+ reload
56
41
  end
57
42
 
58
43
  def activate_percentage(feature, percentage)
59
44
  raise "invalid percentage #{percentage.inspect}" unless percentage.kind_of?(Integer) && percentage>=0 && percentage<=100
60
- Model.transaction do
45
+ ActiveRecordModel.transaction do
61
46
  deactivate_percentage(feature)
62
- Model.create(:feature => feature.to_s, :percentage => percentage)
47
+ ActiveRecordModel.create(:feature => feature.to_s, :percentage => percentage)
63
48
  end
49
+ reload
64
50
  end
65
51
 
66
52
  def deactivate_percentage(feature)
67
- Model.delete_all ["feature = ? AND percentage IS NOT NULL AND user_id IS NULL",
53
+ ActiveRecordModel.delete_all ["feature = ? AND percentage IS NOT NULL AND user_id IS NULL",
68
54
  feature.to_s
69
55
  ]
56
+ reload
70
57
  end
71
58
  end
72
59
 
@@ -0,0 +1,9 @@
1
+ require "active_record"
2
+ class Sapling::ActiveRecordModel < ::ActiveRecord::Base
3
+ set_table_name "sapling_settings"
4
+
5
+ #structure:
6
+ # feature varchar(255),
7
+ # percentage integer, -- 0 to 100 or NULL
8
+ # user_id integer -- user's ID or NULL
9
+ end
@@ -1,4 +1,65 @@
1
1
  module Sapling
2
2
  class Base
3
+ attr_accessor :controller
4
+
5
+ def initialize(controller=nil)
6
+ @controller = controller
7
+ end
8
+
9
+ def features; @features||={}; end
10
+
11
+ private
12
+ def active_internal(feature_name,options)
13
+ feature_method = "#{feature_name}_active?".to_sym
14
+ if controller && controller.respond_to?(feature_method)
15
+ # use override
16
+ controller.send(feature_method,options.merge(:feature => features[feature_name]))
17
+ else
18
+ (f=features[feature_name]) && f.active?(options)
19
+ end
20
+ end
21
+
22
+ # override feature test example for feature :new_homepage
23
+ # options:
24
+ # feature is of type Feature
25
+ # :user
26
+ # returns true if the feature is enabled
27
+ # def new_homepage_active?(options={})
28
+ # # your test here
29
+ # end
30
+ public
31
+
32
+ # see Sapling::API::Client
33
+ def active?(feature, options={})
34
+ options = Util.normalized_options options, controller
35
+ active_internal(feature,options)
36
+ end
37
+
38
+ # returns a list of features enabled for a user
39
+ # see Sapling::API::Client
40
+ def active_features(options={})
41
+ options = Util.normalized_options options, controller
42
+ ret={}
43
+ features.each {|feature_name,feature| ret[feature_name]=feature if active_internal(feature_name,options)}
44
+ ret
45
+ end
46
+
47
+ def js_generator
48
+ @js_generator ||= JavascriptGenerator.new
49
+ end
50
+
51
+ def css_class_prefix
52
+ "sapling_feature"
53
+ end
54
+
55
+ # Use these classes on the container elements of your features
56
+ def css_class(feature)
57
+ "#{css_class_prefix}_#{feature.to_s}"
58
+ end
59
+
60
+ # Put these on the html element to turn on/off features
61
+ def css_toggle_class(feature, on)
62
+ "#{css_class(feature)}_#{on ? 'on' : 'off'}"
63
+ end
3
64
  end
4
65
  end
@@ -0,0 +1,58 @@
1
+ module Sapling
2
+ class Feature
3
+ attr_accessor :name,:users,:percentage
4
+
5
+ protected
6
+ def normalize_users
7
+ return unless users.kind_of?(Array)
8
+ users = @users
9
+ @users = {}
10
+ users.each {|user_id| @users[user_id]=true}
11
+ end
12
+ public
13
+
14
+ # options
15
+ # :users => array or hash
16
+ # users can be an array of user-ids or hash of user_ids mapped to true
17
+ # :percentage =>
18
+ def initialize(name=nil,options={})
19
+ @name = name
20
+ @users = options[:users] || {}
21
+ normalize_users
22
+ self.percentage = options[:percentage]
23
+ end
24
+
25
+ # the Feature reports its name with "to_s"
26
+ def to_s; name.to_s; end
27
+
28
+ # see Sapling::API::Client
29
+ def active?(options={})
30
+ options = Util.normalized_options(options)
31
+ individually_active?(options[:user]) || percentage_active?(options)
32
+ end
33
+
34
+ def percentage_active?(options={})
35
+ percentage && (Util.modded_context_id(options)) < percentage
36
+ end
37
+
38
+ def individually_active?(user)
39
+ user && users[user.id]
40
+ end
41
+
42
+ def activate_user(user)
43
+ users[user.id]=true
44
+ end
45
+
46
+ def deactivate_user(user)
47
+ users.delete(user.id)
48
+ end
49
+
50
+ def activate_percentage(percentage)
51
+ @percentage=percentage
52
+ end
53
+
54
+ def deactivate_percentage
55
+ @percentage=0
56
+ end
57
+ end
58
+ end
@@ -7,39 +7,26 @@ class Sapling::JavascriptGenerator
7
7
  @sapling=sapling
8
8
  end
9
9
 
10
- def prefix
11
- "sapling_feature"
12
- end
13
-
14
- # Use these classes on the container elements of your features
15
- def css_container_class(feature)
16
- "#{prefix}_#{feature.to_s}"
17
- end
18
-
19
- # Put these on the html element to turn on/off features
20
- def css_toggle_class(feature, on)
21
- "#{css_container_class(feature)}_#{on ? 'on' : 'off'}"
22
- end
23
-
24
-
25
10
  # see Sapling::API::Client for options
26
-
27
11
  def mootools_for_feature(feature, on)
28
- "html.removeClass('" + css_toggle_class(feature,!on) + "');html.addClass('" + css_toggle_class(feature,on) + "');"
12
+ "html.removeClass('" + sapling.css_toggle_class(feature,!on) + "');html.addClass('" + sapling.css_toggle_class(feature,on) + "');"
29
13
  end
30
-
14
+
15
+ # options
16
+ # options is passed directly into sapling.active_features
17
+ # One useful option is to override the user:
18
+ # :user => user
31
19
  def generate(options={})
32
- features = Set.new @sapling.features
33
- active_features = Set.new @sapling.active_features options
20
+ features = sapling.features.keys
21
+ active_features = sapling.active_features(options).keys
34
22
  inactive_features = features - active_features
35
23
 
36
-
37
24
  <<-END
38
25
  (function() {
39
26
  if (window.MooTools) {
40
27
  html = $$('html')[0];
41
28
  #{inactive_features.map{|f| mootools_for_feature(f,false) }.join}
42
- #{active_features.map{|f| mootools_for_feature(f,true) }.join}
29
+ #{active_features.map{|f| mootools_for_feature(f,true) }.join}
43
30
  }
44
31
  })();
45
32
  END
@@ -1,69 +1,9 @@
1
1
  module Sapling
2
2
  class Memory < Base
3
3
 
4
- class Feature
5
- attr_accessor :users,:percentage
6
-
7
- def initialize
8
- @users={}
9
- self.percentage = 0
10
- end
11
-
12
- # see Sapling::API::Client
13
- def active?(options={})
14
- options = Util.normalized_options(options)
15
- individually_active?(options[:user]) || percentage_active?(options)
16
- end
17
-
18
- def percentage_active?(options={})
19
- (Util.modded_context_id(options)) < percentage
20
- end
21
-
22
- def individually_active?(user)
23
- user && users[user.id]
24
- end
25
-
26
- def activate_user(user)
27
- users[user.id]=true
28
- end
29
-
30
- def deactivate_user(user)
31
- users.delete(user.id)
32
- end
33
-
34
- def activate_percentage(percentage)
35
- @percentage=percentage
36
- end
37
-
38
- def deactivate_percentage
39
- @percentage=0
40
- end
41
- end
42
-
43
- def initialize
44
- @features={}
45
- end
46
-
47
- module ClientAPI
48
- # see Sapling::API::Client
49
- def active?(feature, options={})
50
- options = Util.normalized_options(options)
51
- (f = @features[feature]) && f.active?(options)
52
- end
53
-
54
- def features
55
- @features.keys.sort_by {|k| k.to_s}
56
- end
57
-
58
- # see Sapling::API::Client
59
- def active_features(options={})
60
- features.select {|feature| active?(feature,options)}
61
- end
62
- end
63
-
64
4
  module AdminAPI
65
5
  def activate_feature(feature)
66
- @features[feature]||=Feature.new
6
+ features[feature]||=Feature.new
67
7
  end
68
8
 
69
9
  def activate_user(feature, user)
@@ -71,7 +11,7 @@ module Sapling
71
11
  end
72
12
 
73
13
  def deactivate_user(feature, user)
74
- (f=@features[feature]) && f.deactivate_user(user)
14
+ (f=features[feature]) && f.deactivate_user(user)
75
15
  end
76
16
 
77
17
  def activate_percentage(feature, percentage)
@@ -82,8 +22,7 @@ module Sapling
82
22
  activate_feature(feature).deactivate_percentage
83
23
  end
84
24
  end
85
-
86
- include ClientAPI
87
25
  include AdminAPI
26
+
88
27
  end
89
28
  end
@@ -1,23 +1,12 @@
1
1
  require 'action_controller'
2
2
 
3
3
  module Sapling::ActionControllerExt
4
- def feature_active?(feature, options={})
5
- options[:user] ||= current_user
6
- sapling.active?(feature, options)
7
- end
8
-
9
-
10
4
  def sapling
11
- @@sapling ||= Sapling::ActiveRecord.new
12
- end
13
-
14
- def sapling_js_generator
15
- @@sapling_js_generator ||= Sapling::JavascriptGenerator.new(sapling)
5
+ @sapling ||= Sapling::ActiveRecord.new(self)
16
6
  end
17
-
18
7
  end
19
8
 
20
9
  class ActionController::Base
21
10
  include Sapling::ActionControllerExt
22
- helper_method :feature_active?, :sapling, :sapling_js_generator
23
- end
11
+ helper_method :sapling
12
+ end
@@ -1,7 +1,11 @@
1
1
  class SaplingController < ApplicationController
2
2
  unloadable
3
+
3
4
  def script
4
- js = sapling_js_generator.generate(:user => current_user)
5
+ js = sapling.js_generator.generate(
6
+ :features => sapling.features.keys,
7
+ :active_features => sapling.active_features
8
+ )
5
9
  render :text => js, :content_type => 'text/javascript'
6
10
  end
7
11
  end
@@ -1,10 +1,4 @@
1
1
  module Sapling
2
2
  module ViewHelpers
3
-
4
- # include ActionView::Helpers::CaptureHelper
5
- def feature_class(feature)
6
- sapling_js_generator.css_container_class(feature)
7
- end
8
-
9
3
  end
10
- end
4
+ end
@@ -10,7 +10,8 @@ module Sapling
10
10
  ((cid=context_id(options)) && (cid%100)) || CONTEXT_ID_ONLY_ENABLED_IF_100_PERCENT_ENABLED
11
11
  end
12
12
 
13
- def normalized_options(options)
13
+ def normalized_options(options,controller=nil)
14
+ options[:user] ||= controller.current_user if controller
14
15
  options[:user_id] ||= options[:user].id if options[:user]
15
16
  options[:context_id] ||= options[:user_id]
16
17
  options
@@ -1,3 +1,3 @@
1
1
  module Sapling
2
- VERSION = "0.2.2"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -1,8 +1,8 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
2
 
3
- describe "Sapling::Memory::Feature" do
3
+ describe "Sapling::Feature" do
4
4
  it "should support activating users" do
5
- f=Sapling::Memory::Feature.new
5
+ f=Sapling::Feature.new
6
6
  u=UserMock.new
7
7
 
8
8
  f.active?(:user=>u).should be_false
@@ -17,8 +17,15 @@ describe "Sapling::JavascriptGenerator" do
17
17
  @sapling.activate_user(:chat, stub(:id => 1))
18
18
  end
19
19
 
20
- it "outputs js" do
21
- Sapling::JavascriptGenerator.new(@sapling).generate(:user => stub(:id => 1)).should include 'html.addClass(\'sapling_feature_chat_on\');'
20
+ it "outputs js with user set via controller's current_user method" do
21
+ @sapling.controller = stub(:current_user => stub(:id => 1))
22
+ output = Sapling::JavascriptGenerator.new(@sapling).generate
23
+ output.should include 'html.addClass(\'sapling_feature_chat_on\');'
24
+ end
25
+
26
+ it "outputs js with user set by passing in the user as an option" do
27
+ output = Sapling::JavascriptGenerator.new(@sapling).generate(:user => stub(:id => 1))
28
+ output.should include 'html.addClass(\'sapling_feature_chat_on\');'
22
29
  end
23
30
  end
24
31
 
@@ -29,7 +36,7 @@ describe "Sapling::JavascriptGenerator" do
29
36
  @sapling.activate_user(:pwn, stub(:id => 102))
30
37
  @sapling.activate_user(:juggle, stub(:id => 115))
31
38
  end
32
-
39
+
33
40
  it "test user bicycle & pwn user" do
34
41
  output = Sapling::JavascriptGenerator.new(@sapling).generate(:user => stub(:id => 102))
35
42
  expected_features = {
@@ -43,7 +50,7 @@ describe "Sapling::JavascriptGenerator" do
43
50
  output.should include "html.removeClass('sapling_feature_#{key}_#{!enabled ? 'on' : 'off'}');"
44
51
  end
45
52
  end
46
-
53
+
47
54
  it "test chat & juggle user" do
48
55
  output = Sapling::JavascriptGenerator.new(@sapling).generate(:user => stub(:id => 115))
49
56
  expected_features = {
@@ -7,14 +7,19 @@ class ApplicationController < ActionController::Base
7
7
 
8
8
  # Scrub sensitive parameters from your log
9
9
  # filter_parameter_logging :password
10
-
10
+
11
11
  def current_user
12
12
  u = User.new
13
- u.id = session[:current_user_id]
13
+ u.id = session[:current_user_id]
14
14
  u
15
15
  end
16
-
16
+
17
17
  def current_user_id
18
18
  session[:current_user_id]
19
19
  end
20
+
21
+ def my_custom_test_feature_active?(options)
22
+ # if the user is in the sapling list, the feature is DISABLED for them
23
+ !options[:feature].users[current_user.id]
24
+ end
20
25
  end
@@ -2,89 +2,22 @@ class SpacemanSpiffsController < ApplicationController
2
2
  # GET /spaceman_spiffs
3
3
  # GET /spaceman_spiffs.xml
4
4
  def index
5
- if feature_active?(:listing)
5
+ if sapling.active?(:listing)
6
6
  @spaceman_spiffs = SpacemanSpiff.all
7
7
 
8
8
  respond_to do |format|
9
9
  format.html # index.html.erb
10
10
  format.xml { render :xml => @spaceman_spiffs }
11
11
  end
12
- else
12
+ else
13
13
  render :nothing => true, :status => :forbidden
14
14
  end
15
15
  end
16
-
17
- def multiple_features
18
- end
19
-
20
- # GET /spaceman_spiffs/1
21
- # GET /spaceman_spiffs/1.xml
22
- def show
23
- @spaceman_spiff = SpacemanSpiff.find(params[:id])
24
-
25
- respond_to do |format|
26
- format.html # show.html.erb
27
- format.xml { render :xml => @spaceman_spiff }
28
- end
29
- end
30
-
31
- # GET /spaceman_spiffs/new
32
- # GET /spaceman_spiffs/new.xml
33
- def new
34
- @spaceman_spiff = SpacemanSpiff.new
35
-
36
- respond_to do |format|
37
- format.html # new.html.erb
38
- format.xml { render :xml => @spaceman_spiff }
39
- end
40
- end
41
-
42
- # GET /spaceman_spiffs/1/edit
43
- def edit
44
- @spaceman_spiff = SpacemanSpiff.find(params[:id])
45
- end
46
16
 
47
- # POST /spaceman_spiffs
48
- # POST /spaceman_spiffs.xml
49
- def create
50
- @spaceman_spiff = SpacemanSpiff.new(params[:spaceman_spiff])
51
-
52
- respond_to do |format|
53
- if @spaceman_spiff.save
54
- format.html { redirect_to(@spaceman_spiff, :notice => 'SpacemanSpiff was successfully created.') }
55
- format.xml { render :xml => @spaceman_spiff, :status => :created, :location => @spaceman_spiff }
56
- else
57
- format.html { render :action => "new" }
58
- format.xml { render :xml => @spaceman_spiff.errors, :status => :unprocessable_entity }
59
- end
60
- end
61
- end
62
-
63
- # PUT /spaceman_spiffs/1
64
- # PUT /spaceman_spiffs/1.xml
65
- def update
66
- @spaceman_spiff = SpacemanSpiff.find(params[:id])
67
-
68
- respond_to do |format|
69
- if @spaceman_spiff.update_attributes(params[:spaceman_spiff])
70
- format.html { redirect_to(@spaceman_spiff, :notice => 'SpacemanSpiff was successfully updated.') }
71
- format.xml { head :ok }
72
- else
73
- format.html { render :action => "edit" }
74
- format.xml { render :xml => @spaceman_spiff.errors, :status => :unprocessable_entity }
75
- end
76
- end
17
+ def multiple_features
77
18
  end
78
19
 
79
- # DELETE /spaceman_spiffs/1
80
- # DELETE /spaceman_spiffs/1.xml
81
- def destroy
82
- @spaceman_spiff = SpacemanSpiff.find(params[:id])
83
- @spaceman_spiff.destroy
84
-
85
- respond_to do |format|
86
- format.html { redirect_to(spaceman_spiffs_url) }
87
- format.xml { head :ok }
88
- end
20
+ def custom_test_feature
21
+ render :text => (sapling.active?(:my_custom_test_feature) ? "1" : "0")
89
22
  end
90
23
  end
@@ -3,9 +3,11 @@ class UserSessionsController < ApplicationController
3
3
  def show
4
4
  render :text => current_user_id.to_s
5
5
  end
6
+
6
7
  def create
7
8
  session[:current_user_id] = params[:user_id]
8
9
  end
10
+
9
11
  def set_manually
10
12
  session[:current_user_id] = params[:user_id]
11
13
  render :text => current_user_id.to_s
@@ -1,7 +1,7 @@
1
1
  ActionController::Routing::Routes.draw do |map|
2
- map.resources :spaceman_spiffs, :collection => {:multiple_features => :get }
2
+ map.resources :spaceman_spiffs, :collection => {:multiple_features => :get, :custom_test_feature => :get }
3
3
  map.resource :user_sessions, :collection => {:set_manually => :get }
4
-
4
+
5
5
  map.sapling_script 'sapling/script.js', :controller => 'sapling', :action => 'script'
6
6
 
7
7
  # The priority is based upon order of creation: first created -> highest priority.
@@ -22,7 +22,7 @@ ActionController::Routing::Routes.draw do |map|
22
22
 
23
23
  # Sample resource route with sub-resources:
24
24
  # map.resources :products, :has_many => [ :comments, :sales ], :has_one => :seller
25
-
25
+
26
26
  # Sample resource route with more complex sub-resources
27
27
  # map.resources :products do |products|
28
28
  # products.resources :comments
@@ -4,6 +4,10 @@ calvin_allowed:
4
4
  feature: listing
5
5
  user_id: 1
6
6
 
7
+ calvin_not_allowed:
8
+ feature: my_custom_test_feature
9
+ user_id: 1
10
+
7
11
  hobbes_allowed:
8
12
  feature: chat
9
13
  user_id: 2
@@ -10,6 +10,22 @@ class FeatureTest < ActionController::IntegrationTest
10
10
  assert_equal '1', response.body
11
11
  end
12
12
 
13
+ test "users cant use 'my_custom_test_feature'" do
14
+ # user1 is listed, user2 is not
15
+ assert Sapling::ActiveRecord.new.features[:my_custom_test_feature].users[1]
16
+ assert_nil Sapling::ActiveRecord.new.features[:my_custom_test_feature].users[2]
17
+
18
+ # but user1 is denied
19
+ post user_sessions_path, :user_id => 1
20
+ get custom_test_feature_spaceman_spiffs_path
21
+ assert_equal '0', response.body
22
+
23
+ # and user2 is permitted
24
+ post user_sessions_path, :user_id => 2
25
+ get custom_test_feature_spaceman_spiffs_path
26
+ assert_equal '1', response.body
27
+ end
28
+
13
29
  test "listing is enabled for user one" do
14
30
  post user_sessions_path, :user_id => 1
15
31
  get spaceman_spiffs_path
@@ -40,11 +40,11 @@ shared_examples_for Sapling do
40
40
  (1..1000).select { |id| @sapling.active?(:chat, :user=>UserMock.new(id)) }.length.should == 200
41
41
  end
42
42
 
43
-
43
+
44
44
  it "should not be active even without a context or user" do
45
45
  @sapling.should_not be_active(:chat)
46
46
  end
47
-
47
+
48
48
  end
49
49
 
50
50
  describe "activating a 100 percent" do
@@ -55,7 +55,7 @@ shared_examples_for Sapling do
55
55
  it "activates the feature for that percentage of users" do
56
56
  (1..1000).select { |id| @sapling.active?(:chat, :user=>UserMock.new(id)) }.length.should == 1000
57
57
  end
58
-
58
+
59
59
 
60
60
  it "should be active even without a context or user" do
61
61
  @sapling.should be_active(:chat)
@@ -123,7 +123,7 @@ shared_examples_for Sapling do
123
123
  end
124
124
 
125
125
  it "returns only two features" do
126
- @sapling.features.should == [:chat, :pwn]
126
+ @sapling.features.keys.should == [:chat, :pwn]
127
127
  end
128
128
  end
129
129
 
@@ -136,8 +136,30 @@ shared_examples_for Sapling do
136
136
  end
137
137
 
138
138
  it "should return only the active features for the specific user" do
139
- @sapling.active_features(:user => stub(:id => 102)).should == [:bicycle, :pwn]
140
- @sapling.active_features(:user => stub(:id => 115)).should == [:chat, :juggle]
139
+ @sapling.active_features(:user => stub(:id => 102)).keys.should == [:bicycle, :pwn]
140
+ @sapling.active_features(:user => stub(:id => 115)).keys.should == [:chat, :juggle]
141
+ end
142
+ end
143
+
144
+ describe "test custom override" do
145
+ before do
146
+ @sapling.activate_user(:chat, stub(:id => 115,:name=>"fred"))
147
+ @sapling.activate_user(:chat, stub(:id => 116,:name=>"harry"))
148
+ @sapling.activate_user(:chat, stub(:id => 117,:name=>"franny"))
149
+ end
150
+
151
+ class Override
152
+ def chat_active?(options)
153
+ !!options[:user].name[/^f/]
154
+ end
155
+ end
156
+
157
+ it "should return only the active features for the specific user" do
158
+ @sapling.controller = Override.new
159
+
160
+ @sapling.active?(:chat,:user => stub(:id=>115,:name => "fred")).should == true
161
+ @sapling.active?(:chat,:user => stub(:id=>120,:name => "h")).should == false
162
+ @sapling.active?(:chat,:user => stub(:id=>130,:name => "frank")).should == true
141
163
  end
142
164
  end
143
165
 
metadata CHANGED
@@ -5,9 +5,9 @@ version: !ruby/object:Gem::Version
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
9
- - 2
10
- version: 0.2.2
8
+ - 3
9
+ - 0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Shane Brinkman-Davis
@@ -16,8 +16,7 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2011-12-20 00:00:00 -08:00
20
- default_executable:
19
+ date: 2012-02-03 00:00:00 Z
21
20
  dependencies:
22
21
  - !ruby/object:Gem::Dependency
23
22
  name: activerecord
@@ -111,15 +110,16 @@ files:
111
110
  - Gemfile
112
111
  - README.md
113
112
  - Rakefile
113
+ - changelog.md
114
114
  - db/create.sql
115
115
  - lib/sapling.rb
116
116
  - lib/sapling/active_record.rb
117
+ - lib/sapling/active_record_model.rb
117
118
  - lib/sapling/api.rb
118
119
  - lib/sapling/base.rb
119
- - lib/sapling/generators/css_generator.rb
120
+ - lib/sapling/feature.rb
120
121
  - lib/sapling/generators/javascript_generator.rb
121
122
  - lib/sapling/memory.rb
122
- - lib/sapling/model.rb
123
123
  - lib/sapling/rails.rb
124
124
  - lib/sapling/rails/action_controller.rb
125
125
  - lib/sapling/rails/controllers/sapling_controller.rb
@@ -130,9 +130,8 @@ files:
130
130
  - rails/init.rb
131
131
  - sapling.gemspec
132
132
  - spec/active_record_spec.rb
133
- - spec/css_generator_spec.rb
133
+ - spec/feature_spec.rb
134
134
  - spec/javascript_generator_spec.rb
135
- - spec/memory_feature_spec.rb
136
135
  - spec/memory_spec.rb
137
136
  - spec/rails_app/README
138
137
  - spec/rails_app/Rakefile
@@ -202,7 +201,6 @@ files:
202
201
  - spec/sapling_examples.rb
203
202
  - spec/spec.opts
204
203
  - spec/spec_helper.rb
205
- has_rdoc: true
206
204
  homepage: https://github.com/imikimi/sapling
207
205
  licenses: []
208
206
 
@@ -232,15 +230,14 @@ required_rubygems_version: !ruby/object:Gem::Requirement
232
230
  requirements: []
233
231
 
234
232
  rubyforge_project: sapling
235
- rubygems_version: 1.5.3
233
+ rubygems_version: 1.8.10
236
234
  signing_key:
237
235
  specification_version: 3
238
236
  summary: Incrementally roll out your features. Uses ActiveRecord to store configuration and supports client-side roll-out of cached pages.
239
237
  test_files:
240
238
  - spec/active_record_spec.rb
241
- - spec/css_generator_spec.rb
239
+ - spec/feature_spec.rb
242
240
  - spec/javascript_generator_spec.rb
243
- - spec/memory_feature_spec.rb
244
241
  - spec/memory_spec.rb
245
242
  - spec/rails_app/README
246
243
  - spec/rails_app/Rakefile
@@ -1,31 +0,0 @@
1
- require "set"
2
-
3
- module Sapling
4
- class CssGenerator
5
- attr_accessor :sapling
6
-
7
- def initialize(sapling)
8
- @sapling=sapling
9
- end
10
-
11
- def prefix
12
- "sapling_css"
13
- end
14
-
15
- def css_class(feature, on)
16
- "#{prefix}_#{feature.to_s}_#{on ? 'on' : 'off'}"
17
- end
18
-
19
- # see Sapling::API::Client for options
20
- def to_s(options={})
21
- features = Set.new @sapling.features
22
- active_features = Set.new @sapling.active_features options
23
- inactive_features = features - active_features
24
-
25
- [
26
- active_features.collect {|f|".#{css_class(f, false)} { display:none !important; }"},
27
- inactive_features.collect {|f|".#{css_class(f, true)} { display:none !important; }"},
28
- ].flatten.join("\n")
29
- end
30
- end
31
- end
@@ -1,4 +0,0 @@
1
- require "active_record"
2
- class Sapling::Model < ::ActiveRecord::Base
3
- set_table_name "sapling_settings"
4
- end
@@ -1,47 +0,0 @@
1
- require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
-
3
- describe "Sapling::CssGenerator" do
4
-
5
- before do
6
- ActiveRecord::Base.establish_connection(
7
- :adapter => 'sqlite3',
8
- :database => ':memory:'
9
- )
10
- sql = File.read(File.expand_path(File.dirname(__FILE__) + '/../db/create.sql'))
11
- ActiveRecord::Base.connection.execute sql
12
- @sapling = Sapling::ActiveRecord.new
13
- end
14
-
15
- describe "creating basic css" do
16
- before do
17
- @sapling.activate_user(:chat, stub(:id => 1))
18
- end
19
-
20
- it "outputs css" do
21
- Sapling::CssGenerator.new(@sapling).to_s(:user => stub(:id => 1)).should == ".sapling_css_chat_off { display:none !important; }"
22
- end
23
- end
24
-
25
- describe "creating more complex css" do
26
- before do
27
- @sapling.activate_percentage(:bicycle, 10)
28
- @sapling.activate_user(:chat, stub(:id => 115))
29
- @sapling.activate_user(:pwn, stub(:id => 102))
30
- @sapling.activate_user(:juggle, stub(:id => 115))
31
- end
32
-
33
- it "test user bicycle & pwn user" do
34
- output = Sapling::CssGenerator.new(@sapling).to_s(:user => stub(:id => 102))
35
- %w{ bicycle_off chat_on juggle_on pwn_off }.each do |key|
36
- output.should =~ /#{key}[^}]+?display:none/
37
- end
38
- end
39
-
40
- it "test chat & juggle user" do
41
- output = Sapling::CssGenerator.new(@sapling).to_s(:user => stub(:id => 115))
42
- %w{ bicycle_on chat_off juggle_off pwn_on }.each do |key|
43
- output.should =~ /#{key}[^}]+?display:none/
44
- end
45
- end
46
- end
47
- end