sapling 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +108 -0
- data/lib/sapling/generators/javascript_generator.rb +47 -0
- data/lib/sapling/rails/action_controller.rb +3 -3
- data/lib/sapling/rails/controllers/sapling_controller.rb +4 -5
- data/lib/sapling/rails/view_helpers.rb +4 -16
- data/lib/sapling/version.rb +1 -1
- data/lib/sapling.rb +1 -0
- data/spec/javascript_generator_spec.rb +61 -0
- data/spec/rails_app/app/views/layouts/spaceman_spiffs.html.erb +3 -1
- data/spec/rails_app/app/views/spaceman_spiffs/multiple_features.html.erb +6 -6
- data/spec/rails_app/config/routes.rb +1 -1
- data/spec/rails_app/public/stylesheets/sapling_features.css +15 -0
- metadata +10 -9
- data/spec/rails_app/test/unit/spaceman_spiff_test.rb +0 -8
- data/spec/rails_app/test/unit/user_test.rb +0 -8
data/README.md
CHANGED
@@ -10,3 +10,111 @@ Heritage
|
|
10
10
|
|
11
11
|
This is a port of the [rollout gem](https://github.com/jamesgolick/rollout) for
|
12
12
|
use with ActiveRecord instead of Redis. We dropped the groups functionality, but otherwise we mirrored the API.
|
13
|
+
|
14
|
+
Setup
|
15
|
+
-----
|
16
|
+
|
17
|
+
Add a route to your routes file. Feel free to change the 'sapling/stylesheet.css' to whatever you want.
|
18
|
+
|
19
|
+
map.sapling_script 'sapling/script.js', :controller => 'sapling', :action => 'script'
|
20
|
+
|
21
|
+
After including your javascript library, add this:
|
22
|
+
|
23
|
+
<%= javascript_include_tag sapling_script_path, :id => 'sapling_script' %>
|
24
|
+
|
25
|
+
Run the `db/create.sql` file to create the sapling_settings table on your DB
|
26
|
+
|
27
|
+
Ensure you have a current_user method to your application controller, which returns a user object, or nil if no active user.
|
28
|
+
|
29
|
+
class ApplicationController < ActionController::Base
|
30
|
+
...
|
31
|
+
def current_user
|
32
|
+
# Return your current user
|
33
|
+
# This is already done for you in Devise, RestfulAuthentication, and other authentication lib
|
34
|
+
end
|
35
|
+
...
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
For a given feature `space_chat`,
|
41
|
+
|
42
|
+
Server-side Usage
|
43
|
+
-----------------
|
44
|
+
|
45
|
+
To check if a feature is enabled for a user in rails controllers or views: use
|
46
|
+
|
47
|
+
feature_active?(:space_chat [, :user => the current user])
|
48
|
+
|
49
|
+
*Note*: sapling will automatically populate the user argument by calling `current_user`
|
50
|
+
|
51
|
+
To enable a feature for a specific user, in the rails console:
|
52
|
+
|
53
|
+
Sapling::ActiveRecord.new.activate_user(:space_chat, space_admin)
|
54
|
+
|
55
|
+
To disable a feature for a specific user, in the rails console:
|
56
|
+
|
57
|
+
Sapling::ActiveRecord.new.deactivate_user(:space_chat, space_admin)
|
58
|
+
|
59
|
+
To enable a feature for 50% of the users, in the rails console:
|
60
|
+
|
61
|
+
Sapling::ActiveRecord.new.activate_percentage(:space_chat, 50)`
|
62
|
+
|
63
|
+
To disable a feature activated for anyone but individually-activated users, in the rails console:
|
64
|
+
|
65
|
+
Sapling::ActiveRecord.new.deactivate_percentage(:space_chat)
|
66
|
+
|
67
|
+
*Note*: Individually-activated users are always activated, regardless of the percentage setting. A deactivated user
|
68
|
+
may still have access to a feature if they fall within an active percentage.
|
69
|
+
|
70
|
+
Client-side Usage
|
71
|
+
-----------------
|
72
|
+
|
73
|
+
Sapling include a javascript helper which add and remove classes on the HTML element for a given user, based upon the
|
74
|
+
features available to that user.
|
75
|
+
|
76
|
+
Given the feature `space_chat`, if it is enabled for the given user, the html root will get an class `sapling_feature_space_chat_on`. If disabled for the given user, the class will be `sapling_feature_space_chat_off`.
|
77
|
+
|
78
|
+
To retrieve the class name for a given feature, you can use the `sapling_js_generator.css_container_class(feature)` and `sapling_js_generator.css_toggle_class(feature, on? (boolean) )` helpers in your ERB files.
|
79
|
+
|
80
|
+
Using the client-side tools, you can define CSS classes which will be visible/invisible when a feature is enabled/disabled. For example:
|
81
|
+
|
82
|
+
CSS:
|
83
|
+
|
84
|
+
/* Chat Defaults to being hidden for everyone */
|
85
|
+
.sapling_feature_chat.enabled {
|
86
|
+
display: none;
|
87
|
+
}
|
88
|
+
.sapling_feature_chat.disabled {
|
89
|
+
display: block;
|
90
|
+
}
|
91
|
+
|
92
|
+
/* When chat is enabled, we hide the disabled container, and show the enabled container */
|
93
|
+
html.sapling_feature_chat_on .sapling_feature_chat.enabled {
|
94
|
+
display: block;
|
95
|
+
}
|
96
|
+
html.sapling_feature_chat_on .sapling_feature_chat.disabled {
|
97
|
+
display: none;
|
98
|
+
}
|
99
|
+
|
100
|
+
|
101
|
+
ERB:
|
102
|
+
|
103
|
+
<div class="<%= feature_class(:chat) %> enabled">
|
104
|
+
REJOICE - SPIFF CHAT ENABLED FOR YOU
|
105
|
+
</div>
|
106
|
+
|
107
|
+
<div class="<%= feature_class(:chat) %> disabled">
|
108
|
+
DESPAIR - SPIFF CHAT NOT ENABLED FOR YOU
|
109
|
+
</div>
|
110
|
+
|
111
|
+
|
112
|
+
*Note* Currently, the javascript generated depends on MooTools being loaded first.
|
113
|
+
|
114
|
+
TODO
|
115
|
+
----
|
116
|
+
|
117
|
+
* Rails 3 compatibility
|
118
|
+
* Remove mootools dependency
|
119
|
+
* CSS generator
|
120
|
+
* Database migration generator
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require "set"
|
2
|
+
|
3
|
+
class Sapling::JavascriptGenerator
|
4
|
+
attr_accessor :sapling
|
5
|
+
|
6
|
+
def initialize(sapling)
|
7
|
+
@sapling=sapling
|
8
|
+
end
|
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
|
+
# see Sapling::API::Client for options
|
26
|
+
|
27
|
+
def mootools_for_feature(feature, on)
|
28
|
+
"html.removeClass('" + css_toggle_class(feature,!on) + "');html.addClass('" + css_toggle_class(feature,on) + "');"
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate(options={})
|
32
|
+
features = Set.new @sapling.features
|
33
|
+
active_features = Set.new @sapling.active_features options
|
34
|
+
inactive_features = features - active_features
|
35
|
+
|
36
|
+
|
37
|
+
<<-END
|
38
|
+
(function() {
|
39
|
+
if (window.MooTools) {
|
40
|
+
html = $$('html')[0];
|
41
|
+
#{inactive_features.map{|f| mootools_for_feature(f,false) }.join}
|
42
|
+
#{active_features.map{|f| mootools_for_feature(f,true) }.join}
|
43
|
+
}
|
44
|
+
})();
|
45
|
+
END
|
46
|
+
end
|
47
|
+
end
|
@@ -11,13 +11,13 @@ module Sapling::ActionControllerExt
|
|
11
11
|
@@sapling ||= Sapling::ActiveRecord.new
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
@@
|
14
|
+
def sapling_js_generator
|
15
|
+
@@sapling_js_generator ||= Sapling::JavascriptGenerator.new(sapling)
|
16
16
|
end
|
17
17
|
|
18
18
|
end
|
19
19
|
|
20
20
|
class ActionController::Base
|
21
21
|
include Sapling::ActionControllerExt
|
22
|
-
helper_method :feature_active?, :sapling, :
|
22
|
+
helper_method :feature_active?, :sapling, :sapling_js_generator
|
23
23
|
end
|
@@ -1,7 +1,6 @@
|
|
1
|
-
class SaplingController < ApplicationController
|
2
|
-
def
|
3
|
-
|
4
|
-
|
5
|
-
render :text => styles, :content_type => 'text/css'
|
1
|
+
class SaplingController < ApplicationController
|
2
|
+
def script
|
3
|
+
js = sapling_js_generator.generate(:user => current_user, :context_id => (request.session_options[:id] if request.session))
|
4
|
+
render :text => js, :content_type => 'text/javascript'
|
6
5
|
end
|
7
6
|
end
|
@@ -2,21 +2,9 @@ module Sapling
|
|
2
2
|
module ViewHelpers
|
3
3
|
|
4
4
|
# include ActionView::Helpers::CaptureHelper
|
5
|
-
def
|
6
|
-
|
5
|
+
def feature_class(feature)
|
6
|
+
sapling_js_generator.css_container_class(feature)
|
7
7
|
end
|
8
|
-
|
9
|
-
def feature_off(feature, &block)
|
10
|
-
feature_block(sapling_css_generator.css_class(feature, nil, false), &block)
|
11
|
-
end
|
12
|
-
|
13
|
-
private
|
14
|
-
def feature_block(css_class, params, &block)
|
15
|
-
concat "<span class=\"#{css_class}\" #{params}>"
|
16
|
-
block.call
|
17
|
-
concat "</span>"
|
18
|
-
end
|
19
|
-
|
8
|
+
|
20
9
|
end
|
21
|
-
end
|
22
|
-
|
10
|
+
end
|
data/lib/sapling/version.rb
CHANGED
data/lib/sapling.rb
CHANGED
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe "Sapling::JavascriptGenerator" 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 js" do
|
16
|
+
before do
|
17
|
+
@sapling.activate_user(:chat, stub(:id => 1))
|
18
|
+
end
|
19
|
+
|
20
|
+
it "outputs js" do
|
21
|
+
Sapling::JavascriptGenerator.new(@sapling).generate(:user => stub(:id => 1)).should include 'html.addClass(\'sapling_feature_chat_on\');'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
describe "creating more complex js" 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::JavascriptGenerator.new(@sapling).generate(:user => stub(:id => 102))
|
35
|
+
expected_features = {
|
36
|
+
'bicycle' => true,
|
37
|
+
'chat' => false,
|
38
|
+
'juggle' => false,
|
39
|
+
'pwn' => true
|
40
|
+
}
|
41
|
+
expected_features.each_pair do |key, enabled|
|
42
|
+
output.should include "html.addClass('sapling_feature_#{key}_#{enabled ? 'on' : 'off'}');"
|
43
|
+
output.should include "html.removeClass('sapling_feature_#{key}_#{!enabled ? 'on' : 'off'}');"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
it "test chat & juggle user" do
|
48
|
+
output = Sapling::JavascriptGenerator.new(@sapling).generate(:user => stub(:id => 115))
|
49
|
+
expected_features = {
|
50
|
+
'bicycle' => false,
|
51
|
+
'chat' => true,
|
52
|
+
'juggle' => true,
|
53
|
+
'pwn' => false
|
54
|
+
}
|
55
|
+
expected_features.each_pair do |key, enabled|
|
56
|
+
output.should include "html.addClass('sapling_feature_#{key}_#{enabled ? 'on' : 'off'}');"
|
57
|
+
output.should include "html.removeClass('sapling_feature_#{key}_#{!enabled ? 'on' : 'off'}');"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -6,7 +6,9 @@
|
|
6
6
|
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
|
7
7
|
<title>SpacemanSpiffs: <%= controller.action_name %></title>
|
8
8
|
<%= stylesheet_link_tag 'scaffold' %>
|
9
|
-
<%= stylesheet_link_tag
|
9
|
+
<%= stylesheet_link_tag 'sapling_features' %>
|
10
|
+
<%= javascript_include_tag 'https://ajax.googleapis.com/ajax/libs/mootools/1.4.1/mootools.js' %>
|
11
|
+
<%= javascript_include_tag sapling_script_path %>
|
10
12
|
</head>
|
11
13
|
<body>
|
12
14
|
|
@@ -1,7 +1,7 @@
|
|
1
|
-
|
2
|
-
SPIFF CHAT ENABLED
|
3
|
-
|
1
|
+
<div class="<%= feature_class(:chat) %> enabled">
|
2
|
+
REJOICE - SPIFF CHAT ENABLED FOR YOU
|
3
|
+
</div>
|
4
4
|
|
5
|
-
|
6
|
-
SPIFF CHAT
|
7
|
-
|
5
|
+
<div class="<%= feature_class(:chat) %> disabled">
|
6
|
+
DESPAIR - SPIFF CHAT NOT ENABLED FOR YOU
|
7
|
+
</div>
|
@@ -2,7 +2,7 @@ ActionController::Routing::Routes.draw do |map|
|
|
2
2
|
map.resources :spaceman_spiffs, :collection => {:multiple_features => :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.
|
8
8
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
/* Chat Defaults */
|
2
|
+
.sapling_feature_chat.enabled {
|
3
|
+
display: none;
|
4
|
+
}
|
5
|
+
.sapling_feature_chat.disabled {
|
6
|
+
display: block;
|
7
|
+
}
|
8
|
+
|
9
|
+
/* Chat Enabled */
|
10
|
+
html.sapling_feature_chat_on .sapling_feature_chat.enabled {
|
11
|
+
display: block;
|
12
|
+
}
|
13
|
+
html.sapling_feature_chat_on .sapling_feature_chat.disabled {
|
14
|
+
display: none;
|
15
|
+
}
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sapling
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 2
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Shane Brinkman-Davis
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2011-12-
|
19
|
+
date: 2011-12-20 00:00:00 -08:00
|
20
20
|
default_executable:
|
21
21
|
dependencies:
|
22
22
|
- !ruby/object:Gem::Dependency
|
@@ -117,6 +117,7 @@ files:
|
|
117
117
|
- lib/sapling/api.rb
|
118
118
|
- lib/sapling/base.rb
|
119
119
|
- lib/sapling/generators/css_generator.rb
|
120
|
+
- lib/sapling/generators/javascript_generator.rb
|
120
121
|
- lib/sapling/memory.rb
|
121
122
|
- lib/sapling/model.rb
|
122
123
|
- lib/sapling/rails.rb
|
@@ -130,6 +131,7 @@ files:
|
|
130
131
|
- sapling.gemspec
|
131
132
|
- spec/active_record_spec.rb
|
132
133
|
- spec/css_generator_spec.rb
|
134
|
+
- spec/javascript_generator_spec.rb
|
133
135
|
- spec/memory_feature_spec.rb
|
134
136
|
- spec/memory_spec.rb
|
135
137
|
- spec/rails_app/README
|
@@ -178,6 +180,7 @@ files:
|
|
178
180
|
- spec/rails_app/public/javascripts/effects.js
|
179
181
|
- spec/rails_app/public/javascripts/prototype.js
|
180
182
|
- spec/rails_app/public/robots.txt
|
183
|
+
- spec/rails_app/public/stylesheets/sapling_features.css
|
181
184
|
- spec/rails_app/public/stylesheets/scaffold.css
|
182
185
|
- spec/rails_app/script/about
|
183
186
|
- spec/rails_app/script/console
|
@@ -196,8 +199,6 @@ files:
|
|
196
199
|
- spec/rails_app/test/performance/browsing_test.rb
|
197
200
|
- spec/rails_app/test/test_helper.rb
|
198
201
|
- spec/rails_app/test/unit/helpers/spaceman_spiffs_helper_test.rb
|
199
|
-
- spec/rails_app/test/unit/spaceman_spiff_test.rb
|
200
|
-
- spec/rails_app/test/unit/user_test.rb
|
201
202
|
- spec/sapling_examples.rb
|
202
203
|
- spec/spec.opts
|
203
204
|
- spec/spec_helper.rb
|
@@ -238,6 +239,7 @@ summary: Incrementally roll out your features. Uses ActiveRecord to store config
|
|
238
239
|
test_files:
|
239
240
|
- spec/active_record_spec.rb
|
240
241
|
- spec/css_generator_spec.rb
|
242
|
+
- spec/javascript_generator_spec.rb
|
241
243
|
- spec/memory_feature_spec.rb
|
242
244
|
- spec/memory_spec.rb
|
243
245
|
- spec/rails_app/README
|
@@ -286,6 +288,7 @@ test_files:
|
|
286
288
|
- spec/rails_app/public/javascripts/effects.js
|
287
289
|
- spec/rails_app/public/javascripts/prototype.js
|
288
290
|
- spec/rails_app/public/robots.txt
|
291
|
+
- spec/rails_app/public/stylesheets/sapling_features.css
|
289
292
|
- spec/rails_app/public/stylesheets/scaffold.css
|
290
293
|
- spec/rails_app/script/about
|
291
294
|
- spec/rails_app/script/console
|
@@ -304,8 +307,6 @@ test_files:
|
|
304
307
|
- spec/rails_app/test/performance/browsing_test.rb
|
305
308
|
- spec/rails_app/test/test_helper.rb
|
306
309
|
- spec/rails_app/test/unit/helpers/spaceman_spiffs_helper_test.rb
|
307
|
-
- spec/rails_app/test/unit/spaceman_spiff_test.rb
|
308
|
-
- spec/rails_app/test/unit/user_test.rb
|
309
310
|
- spec/sapling_examples.rb
|
310
311
|
- spec/spec.opts
|
311
312
|
- spec/spec_helper.rb
|