sapling 0.1.1 → 0.2.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 +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
|