sapling 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|