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 +26 -4
- data/changelog.md +10 -0
- data/lib/sapling.rb +2 -2
- data/lib/sapling/active_record.rb +27 -40
- data/lib/sapling/active_record_model.rb +9 -0
- data/lib/sapling/base.rb +61 -0
- data/lib/sapling/feature.rb +58 -0
- data/lib/sapling/generators/javascript_generator.rb +9 -22
- data/lib/sapling/memory.rb +3 -64
- data/lib/sapling/rails/action_controller.rb +3 -14
- data/lib/sapling/rails/controllers/sapling_controller.rb +5 -1
- data/lib/sapling/rails/view_helpers.rb +1 -7
- data/lib/sapling/util.rb +2 -1
- data/lib/sapling/version.rb +1 -1
- data/spec/{memory_feature_spec.rb → feature_spec.rb} +2 -2
- data/spec/javascript_generator_spec.rb +11 -4
- data/spec/rails_app/app/controllers/application_controller.rb +8 -3
- data/spec/rails_app/app/controllers/spaceman_spiffs_controller.rb +5 -72
- data/spec/rails_app/app/controllers/user_sessions_controller.rb +2 -0
- data/spec/rails_app/config/routes.rb +3 -3
- data/spec/rails_app/test/fixtures/sapling_settings.yml +4 -0
- data/spec/rails_app/test/integration/feature_test.rb +16 -0
- data/spec/sapling_examples.rb +28 -6
- metadata +10 -13
- data/lib/sapling/generators/css_generator.rb +0 -31
- data/lib/sapling/model.rb +0 -4
- data/spec/css_generator_spec.rb +0 -47
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
|
-
|
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="<%=
|
126
|
+
<div class="<%= sapling.css_class(:chat) %> enabled">
|
104
127
|
REJOICE - SPIFF CHAT ENABLED FOR YOU
|
105
128
|
</div>
|
106
129
|
|
107
|
-
<div class="<%=
|
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
|
data/changelog.md
ADDED
@@ -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)
|
data/lib/sapling.rb
CHANGED
@@ -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/
|
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
|
-
|
5
|
+
ActiveRecordModel.table_name
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
ret
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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
|
-
|
31
|
+
ActiveRecordModel.transaction do
|
49
32
|
deactivate_user(feature, user)
|
50
|
-
|
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
|
-
|
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
|
-
|
45
|
+
ActiveRecordModel.transaction do
|
61
46
|
deactivate_percentage(feature)
|
62
|
-
|
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
|
-
|
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
|
|
data/lib/sapling/base.rb
CHANGED
@@ -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 =
|
33
|
-
active_features =
|
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
|
data/lib/sapling/memory.rb
CHANGED
@@ -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
|
-
|
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
|
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
|
-
|
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 :
|
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 =
|
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
|
data/lib/sapling/util.rb
CHANGED
@@ -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
|
data/lib/sapling/version.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
2
|
|
3
|
-
describe "Sapling::
|
3
|
+
describe "Sapling::Feature" do
|
4
4
|
it "should support activating users" do
|
5
|
-
f=Sapling::
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
80
|
-
|
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
|
@@ -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
|
data/spec/sapling_examples.rb
CHANGED
@@ -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
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
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:
|
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/
|
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/
|
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.
|
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/
|
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
|
data/lib/sapling/model.rb
DELETED
data/spec/css_generator_spec.rb
DELETED
@@ -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
|