arturo 0.2.3.1
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/HISTORY.md +16 -0
- data/README.md +277 -0
- data/app/controllers/arturo/features_controller.rb +125 -0
- data/app/helpers/arturo/features_helper.rb +38 -0
- data/app/models/arturo/feature.rb +85 -0
- data/app/views/arturo/features/_feature.html.erb +5 -0
- data/app/views/arturo/features/_form.html.erb +16 -0
- data/app/views/arturo/features/edit.html.erb +2 -0
- data/app/views/arturo/features/forbidden.html.erb +2 -0
- data/app/views/arturo/features/index.html.erb +29 -0
- data/app/views/arturo/features/new.html.erb +2 -0
- data/app/views/arturo/features/show.html.erb +2 -0
- data/config/locales/en.yml +40 -0
- data/lib/arturo.rb +10 -0
- data/lib/arturo/controller_filters.rb +33 -0
- data/lib/arturo/engine.rb +15 -0
- data/lib/arturo/feature_availability.rb +37 -0
- data/lib/arturo/feature_factories.rb +4 -0
- data/lib/arturo/feature_management.rb +23 -0
- data/lib/arturo/range_form_support.rb +17 -0
- data/lib/arturo/special_handling.rb +62 -0
- data/lib/generators/arturo/arturo_generator.rb +40 -0
- data/lib/generators/arturo/templates/arturo.css +67 -0
- data/lib/generators/arturo/templates/arturo.js +23 -0
- data/lib/generators/arturo/templates/arturo_customizations.css +1 -0
- data/lib/generators/arturo/templates/initializer.rb +29 -0
- data/lib/generators/arturo/templates/migration.rb +17 -0
- data/lib/generators/arturo/templates/semicolon.png +0 -0
- metadata +184 -0
data/HISTORY.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
## v 1.1.0 - cleanup
|
2
|
+
* changed `require_feature!` to `require_feature`
|
3
|
+
* replaced `Arturo.permit_management` and `Arturo.feature_recipient`
|
4
|
+
blocks with instance methods
|
5
|
+
`Arturo::FeatureManagement.may_manage_features?` and
|
6
|
+
`Arturo::FeatureAvailability.feature_recipient`
|
7
|
+
|
8
|
+
## v 1.0.0 - Initial Release
|
9
|
+
* `require_feature!` controller filter
|
10
|
+
* `if_feature_enabled` controller and view method
|
11
|
+
* `feature_enabled?` controller and view method
|
12
|
+
* CRUD for features
|
13
|
+
* `Arturo.permit_management` to configure management permission
|
14
|
+
* `Arturo.feature_recipient` to configure on what basis features are deployed
|
15
|
+
* whitelists and blacklists
|
16
|
+
|
data/README.md
ADDED
@@ -0,0 +1,277 @@
|
|
1
|
+
## Rails 2.3
|
2
|
+
|
3
|
+
This is the Rails 2.3 branch of Arturo. It is available as a gem with the
|
4
|
+
version number `0.2.3.x`. See [Installation](#installation), below. For
|
5
|
+
Rails 3.0 support, see the
|
6
|
+
[master branch](http://github.com/jamesarosen/arturo).
|
7
|
+
|
8
|
+
## What
|
9
|
+
|
10
|
+
Arturo provides feature sliders for Rails. It lets you turn features on and off
|
11
|
+
just like
|
12
|
+
[feature flippers](http://code.flickr.com/blog/2009/12/02/flipping-out/),
|
13
|
+
but offers more fine-grained control. It supports deploying features only for
|
14
|
+
a given percent* of your users and whitelisting and blacklisting users based
|
15
|
+
on any criteria you can express in Ruby.
|
16
|
+
|
17
|
+
* The selection isn't random. It's not even pseudo-random. It's completely
|
18
|
+
deterministic. This assures that if a user has a feature on Monday, the
|
19
|
+
user will still have it on Tuesday (unless, of course, you *decrease*
|
20
|
+
the feature's deployment percentage or change its white- or blacklist
|
21
|
+
settings).
|
22
|
+
|
23
|
+
### A quick example
|
24
|
+
|
25
|
+
Trish, a developer is working on a new feature: a live feed of recent postings
|
26
|
+
in the user's city that shows up in the user's sidebar. First, she uses Arturo's
|
27
|
+
view helpers to control who sees the sidebar widget:
|
28
|
+
|
29
|
+
<%# in app/views/layout/_sidebar.html.erb: %>
|
30
|
+
<% if_feature_enabled(:live_postings) do %>
|
31
|
+
<div class='widget'>
|
32
|
+
<h3>Recent Postings</h3>
|
33
|
+
<ol id='live_postings'>
|
34
|
+
</ol>
|
35
|
+
</div>
|
36
|
+
<% end %>
|
37
|
+
|
38
|
+
Then Trish writes some Javascript that will poll the server for recent
|
39
|
+
postings and put them in the sidebar widget:
|
40
|
+
|
41
|
+
// in public/javascript/live_postings.js:
|
42
|
+
$(function() {
|
43
|
+
var livePostingsList = $('#live_postings');
|
44
|
+
if (livePostingsList.length > 0) {
|
45
|
+
var updatePostingsList = function() {
|
46
|
+
livePostingsList.load('/listings/recent');
|
47
|
+
setTimeout(updatePostingsList, 30);
|
48
|
+
}
|
49
|
+
updatePostingsList();
|
50
|
+
}
|
51
|
+
});
|
52
|
+
|
53
|
+
Trish uses Arturo's Controller filters to control who has access to
|
54
|
+
the feature:
|
55
|
+
|
56
|
+
# in app/controllers/postings_controller:
|
57
|
+
class PostingsController < ApplicationController
|
58
|
+
require_feature :live_postings, :only => :recent
|
59
|
+
# ...
|
60
|
+
end
|
61
|
+
|
62
|
+
Trish then deploys this code to production. Nobody will see the feature yet,
|
63
|
+
since it's not on for anyone. (In fact, the feature doesn't yet exist
|
64
|
+
in the database, which is the same as being deployed to 0% of users.) A week
|
65
|
+
later, when the company is ready to start deploying the feature to a few
|
66
|
+
people, the product manager, Vijay, signs in to their site and navigates
|
67
|
+
to `/features`, adds a new feature called "live_postings" and sets its
|
68
|
+
deployment percentage to 3%. After a few days, the operations team decides
|
69
|
+
that the increase in traffic is not going to overwhelm their servers, and
|
70
|
+
Vijay can bump the deployment percentage up to 50%. A few more days go by
|
71
|
+
and they clean up the last few bugs they found with the "live_postings"
|
72
|
+
feature and deploy it to all users.
|
73
|
+
|
74
|
+
## <span id='installation'>Installation</span>
|
75
|
+
|
76
|
+
Arturo's support for Rails 2.3 is available as a gem with version
|
77
|
+
numbers matching `0.2.3.x`. The best strategy is to use the
|
78
|
+
"pessimistic" gem version `"~> 0.2.3"`, which will get you the
|
79
|
+
latest version of Rails 2.3 support.
|
80
|
+
|
81
|
+
### In Rails 2.3, with Bundler
|
82
|
+
|
83
|
+
In your `Gemfile`:
|
84
|
+
|
85
|
+
gem 'arturo', :git => 'git://github.com/jamesarosen/arturo.git',
|
86
|
+
:version => '~> 0.2.3'
|
87
|
+
|
88
|
+
Unfortunately, Rails 2.3 won't automatically load Arturo as a Plugin
|
89
|
+
(and thus not pick up its "engine-ness") without **also** declaring
|
90
|
+
it as a Rails gem. Thus, in `config/environment.rb`:
|
91
|
+
|
92
|
+
config.gem 'arturo', :version => '~> 0.2.3'
|
93
|
+
|
94
|
+
### In Rails 2.3, without Bundler
|
95
|
+
|
96
|
+
Put the `rails_2_3` branch of `git://github.com/jamesarosen/arturo.git` into
|
97
|
+
your `vendor/plugins/` directory. You can use Git submodules or a simple
|
98
|
+
checkout.
|
99
|
+
|
100
|
+
## Configuration
|
101
|
+
|
102
|
+
### In Rails
|
103
|
+
|
104
|
+
#### Run the generators:
|
105
|
+
|
106
|
+
$ script/generate arturo:migration
|
107
|
+
$ script/generate arturo:initializer
|
108
|
+
$ script/generate arturo:route
|
109
|
+
$ script/generate arturo:assets
|
110
|
+
|
111
|
+
#### Edit the generated migration as necessary
|
112
|
+
|
113
|
+
#### Run the migration:
|
114
|
+
|
115
|
+
$ rake db:migrate
|
116
|
+
|
117
|
+
#### Edit the configuration
|
118
|
+
|
119
|
+
##### Initializer
|
120
|
+
|
121
|
+
Open up the newly-generated `config/initializers/arturo_initializer.rb`.
|
122
|
+
There are configuration options for the following:
|
123
|
+
|
124
|
+
* the method that determines whether a user has permission to manage features
|
125
|
+
(see [admin permissions](#adminpermissions))
|
126
|
+
* the method that returns the object that has features
|
127
|
+
(e.g. User, Person, or Account; see
|
128
|
+
[feature recipients](#featurerecipients))
|
129
|
+
* whitelists and blacklists for features
|
130
|
+
(see [white- and blacklisting](#wblisting))
|
131
|
+
|
132
|
+
##### CSS
|
133
|
+
|
134
|
+
Open up the newly-generated `public/stylehseets/arturo_customizations.css`.
|
135
|
+
You can add any overrides you like to the feature configuration page styles
|
136
|
+
here. **Do not** edit `public/stylehseets/arturo.css` as that file may be
|
137
|
+
overwritten in future updates to Arturo.
|
138
|
+
|
139
|
+
### In other frameworks
|
140
|
+
|
141
|
+
Arturo is a Rails engine. I want to promote reuse on other frameworks by
|
142
|
+
extracting key pieces into mixins, though this isn't done yet. Open an
|
143
|
+
[issue](http://github.com/jamesarosen/arturo/issues) and I'll be happy to
|
144
|
+
work with you on support for your favorite framework.
|
145
|
+
|
146
|
+
## Deep-Dive
|
147
|
+
|
148
|
+
### <span id='adminpermissions'>Admin Permissions</span>
|
149
|
+
|
150
|
+
`Arturo::FeatureManagement#may_manage_features?` is a method that is run in
|
151
|
+
the context of a Controller or View instance. It should return `true` if
|
152
|
+
and only if the current user may manage permissions. The default implementation
|
153
|
+
is as follows:
|
154
|
+
|
155
|
+
current_user.present? && current_user.admin?
|
156
|
+
|
157
|
+
You can change the implementation in
|
158
|
+
`config/initializers/arturo_initializer.rb`. A reasonable implementation
|
159
|
+
might be
|
160
|
+
|
161
|
+
Arturo.permit_management do
|
162
|
+
signed_in? && current_user.can?(:manage_features)
|
163
|
+
end
|
164
|
+
|
165
|
+
### <span id='featurerecipients'>Feature Recipients</span>
|
166
|
+
|
167
|
+
Clients of Arturo may want to deploy new features on a per-user, per-project,
|
168
|
+
per-account, or other basis. For example, it is likely Twitter deployed
|
169
|
+
"#newtwitter" on a per-user basis. Conversely, Facebook -- at least in its
|
170
|
+
early days -- may have deployed features on a per-university basis. It wouldn't
|
171
|
+
make much sense to deploy a feature to one user of a Basecamp project but not
|
172
|
+
to others, so 37Signals would probably want a per-project or per-account basis.
|
173
|
+
|
174
|
+
`Arturo::FeatureAvailability#feature_recipient` is intended to support these
|
175
|
+
many use cases. It is a method that returns the current "thing" (a user, account,
|
176
|
+
project, university, ...) that is a member of the category that is the basis for
|
177
|
+
deploying new features. It should return an `Object` that responds to `#id`.
|
178
|
+
|
179
|
+
The default implementation simply returns `current_user`. Like
|
180
|
+
`Arturo::FeatureManagement#may_manage_features?`, this method can be configured
|
181
|
+
in `config/initializers/arturo_initializer.rb`. If you want to deploy features
|
182
|
+
on a per-account basis, a reasonable implementation might be
|
183
|
+
|
184
|
+
Arturo.feature_recipient do
|
185
|
+
current_account
|
186
|
+
end
|
187
|
+
|
188
|
+
or
|
189
|
+
|
190
|
+
Arturo.feature_recipient do
|
191
|
+
current_user.account
|
192
|
+
end
|
193
|
+
|
194
|
+
If the block returns `nil`, the feature will be disabled.
|
195
|
+
|
196
|
+
### <span id='wblisting'>Whitelists & Blacklists</span>
|
197
|
+
|
198
|
+
Whitelists and blacklists allow you to control exactly which users or accounts
|
199
|
+
will have a feature. For example, if all premium users should have the
|
200
|
+
`:awesome` feature, place the following in
|
201
|
+
`config/initializers/arturo_initializer.rb`:
|
202
|
+
|
203
|
+
Arturo::Feature.whitelist(:awesome) do |user|
|
204
|
+
user.account.premium?
|
205
|
+
end
|
206
|
+
|
207
|
+
If, on the other hand, no users on the free plan should have the
|
208
|
+
`:awesome` feature, place the following in
|
209
|
+
`config/initializers/arturo_initializer.rb`:
|
210
|
+
|
211
|
+
Arturo::Feature.blacklist(:awesome) do |user|
|
212
|
+
user.account.free?
|
213
|
+
end
|
214
|
+
|
215
|
+
### Feature Conditionals
|
216
|
+
|
217
|
+
All that configuration is just a waste of time if Arturo didn't modify the
|
218
|
+
behavior of your application based on feature availability. There are a few
|
219
|
+
ways to do so.
|
220
|
+
|
221
|
+
#### Controller Filters
|
222
|
+
|
223
|
+
If an action should only be available to those with a feature enabled,
|
224
|
+
use a before filter. The following will raise a 403 Forbidden error for
|
225
|
+
every action within `BookHoldsController` that is invoked by a user who
|
226
|
+
does not have the `:hold_book` feature.
|
227
|
+
|
228
|
+
class BookHoldsController < ApplicationController
|
229
|
+
require_feature :hold_book
|
230
|
+
end
|
231
|
+
|
232
|
+
`require_feature` accepts as a second argument a `Hash` that it passes on
|
233
|
+
to `before_filter`, so you can use `:only` and `:except` to specify exactly
|
234
|
+
which actions are filtered.
|
235
|
+
|
236
|
+
If you want to customize the page that is rendered on 403 Forbidden
|
237
|
+
responses, put the view in
|
238
|
+
`RAILS_ROOT/app/views/arturo/features/forbidden.html.erb`. Rails will
|
239
|
+
check there before falling back on Arturo's forbidden page.
|
240
|
+
|
241
|
+
#### Conditional Evaluation
|
242
|
+
|
243
|
+
Both controllers and views have access to the `if_feature_enabled` and
|
244
|
+
`feature_enabled?` methods. The former is used like so:
|
245
|
+
|
246
|
+
<% if_feature_enabled?(:reserve_table) %>
|
247
|
+
<%= link_to 'Reserve a table', new_restaurant_reservation_path(:restaurant_id => @restaurant) %>
|
248
|
+
<% end %>
|
249
|
+
|
250
|
+
The latter can be used like so:
|
251
|
+
|
252
|
+
def widgets_for_sidebar
|
253
|
+
widgets = []
|
254
|
+
widgets << twitter_widget if feature_enabled?(:twitter_integration)
|
255
|
+
...
|
256
|
+
widgets
|
257
|
+
end
|
258
|
+
|
259
|
+
#### Caching
|
260
|
+
|
261
|
+
**Note**: Arturo does not yet have caching support. Be very careful when
|
262
|
+
caching actions or pages that involve feature detection as you will get
|
263
|
+
strange behavior when a user who has access to a feature requests a page
|
264
|
+
just after one who does not (and vice versa). The following is the
|
265
|
+
**intended** support for caching.
|
266
|
+
|
267
|
+
Both the `require_feature` before filter and the `if_feature_enabled` block
|
268
|
+
evaluation automatically append a string based on the feature's
|
269
|
+
`last_modified` timestamp to cache keys that Rails generates. Thus, you don't
|
270
|
+
have to worry about expiring caches when you increase a feature's deployment
|
271
|
+
percentage. See `Arturo::CacheSupport` for more information.
|
272
|
+
|
273
|
+
## The Name
|
274
|
+
|
275
|
+
Arturo gets its name from
|
276
|
+
[Professor Maximillian Arturo](http://en.wikipedia.org/wiki/Maximillian_Arturo)
|
277
|
+
on [Sliders](http://en.wikipedia.org/wiki/Sliders).
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'action_controller'
|
2
|
+
|
3
|
+
module Arturo
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'application_controller'
|
7
|
+
rescue LoadError
|
8
|
+
# do nothing
|
9
|
+
end
|
10
|
+
|
11
|
+
base = Object.const_defined?(:ApplicationController) ? ApplicationController : ActionController::Base
|
12
|
+
|
13
|
+
# Handles all Feature actions. Clients of the Arturo engine
|
14
|
+
# should redefine Arturo::FeaturesController#permitted? to
|
15
|
+
# return true only for users who are permitted to manage features.
|
16
|
+
class FeaturesController < base
|
17
|
+
include Arturo::FeatureManagement
|
18
|
+
|
19
|
+
unloadable
|
20
|
+
before_filter :require_permission
|
21
|
+
before_filter :load_feature, :only => [ :show, :edit, :update, :destroy ]
|
22
|
+
|
23
|
+
def index
|
24
|
+
@features = Arturo::Feature.all
|
25
|
+
respond_to do |format|
|
26
|
+
format.html { }
|
27
|
+
format.json { render :json => @features }
|
28
|
+
format.xml { render :xml => @features }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def update_all
|
33
|
+
updated_count = 0
|
34
|
+
errors = []
|
35
|
+
features_params = params[:features] || {}
|
36
|
+
features_params.each do |id, attributes|
|
37
|
+
feature = Arturo::Feature.find_by_id(id)
|
38
|
+
if feature.blank?
|
39
|
+
errors << t('arturo.features.flash.no_such_feature', :id => id)
|
40
|
+
elsif feature.update_attributes(attributes)
|
41
|
+
updated_count += 1
|
42
|
+
else
|
43
|
+
errors << t('arturo.features.flash.error_updating', :id => id)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
if errors.any?
|
47
|
+
flash[:error] = errors
|
48
|
+
else
|
49
|
+
flash[:success] = t('arturo.features.flash.updated_many', :count => updated_count)
|
50
|
+
end
|
51
|
+
redirect_to features_path
|
52
|
+
end
|
53
|
+
|
54
|
+
def show
|
55
|
+
respond_to do |format|
|
56
|
+
format.html { }
|
57
|
+
format.json { render :json => @feature }
|
58
|
+
format.xml { render :xml => @feature }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def new
|
63
|
+
@feature = Arturo::Feature.new(params[:feature])
|
64
|
+
respond_to do |format|
|
65
|
+
format.html { }
|
66
|
+
format.json { render :json => @feature }
|
67
|
+
format.xml { render :xml => @feature }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def create
|
72
|
+
@feature = Arturo::Feature.new(params[:feature])
|
73
|
+
if @feature.save
|
74
|
+
flash[:notice] = t('arturo.features.flash.created', :name => @feature.to_s)
|
75
|
+
redirect_to features_path
|
76
|
+
else
|
77
|
+
flash[:alert] = t('arturo.features.flash.error_creating', :name => @feature.to_s)
|
78
|
+
render :action => 'new'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def edit
|
83
|
+
respond_to do |format|
|
84
|
+
format.html { }
|
85
|
+
format.json { render :json => @feature }
|
86
|
+
format.xml { render :xml => @feature }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def update
|
91
|
+
if @feature.update_attributes(params[:feature])
|
92
|
+
flash[:notice] = t('arturo.features.flash.updated', :name => @feature.to_s)
|
93
|
+
redirect_to feature_path(@feature)
|
94
|
+
else
|
95
|
+
flash[:alert] = t('arturo.features.flash.error_updating', :name => @feature.to_s)
|
96
|
+
render :action => 'edit'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def destroy
|
101
|
+
if @feature.destroy
|
102
|
+
flash[:notice] = t('arturo.features.flash.removed', :name => @feature.to_s)
|
103
|
+
else
|
104
|
+
flash[:alert] = t('arturo.features.flash.error_removing', :name => @feature.to_s)
|
105
|
+
end
|
106
|
+
redirect_to features_path
|
107
|
+
end
|
108
|
+
|
109
|
+
protected
|
110
|
+
|
111
|
+
def require_permission
|
112
|
+
unless may_manage_features?
|
113
|
+
render :action => 'forbidden', :status => 403
|
114
|
+
return false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def load_feature
|
119
|
+
@feature ||= Arturo::Feature.find(params[:id])
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'action_view/helpers/tag_helper'
|
2
|
+
require 'action_view/helpers/form_tag_helper'
|
3
|
+
|
4
|
+
module Arturo
|
5
|
+
module FeaturesHelper
|
6
|
+
include ActionView::Helpers::TagHelper
|
7
|
+
|
8
|
+
def deployment_percentage_range_and_output_tags(name, value, options = {})
|
9
|
+
id = sanitize_to_id(name)
|
10
|
+
options = {
|
11
|
+
'type' => 'range',
|
12
|
+
'name' => name,
|
13
|
+
'id' => id,
|
14
|
+
'value' => value,
|
15
|
+
'min' => '0',
|
16
|
+
'max' => '100',
|
17
|
+
'step' => '1',
|
18
|
+
'class' => 'deployment_percentage'
|
19
|
+
}.update(options.stringify_keys)
|
20
|
+
tag(:input, options) + deployment_percentage_output_tag(id, value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def deployment_percentage_output_tag(id, value)
|
24
|
+
content_tag(:output, value, { 'for' => id, 'class' => 'deployment_percentage no_js' })
|
25
|
+
end
|
26
|
+
|
27
|
+
def error_messages_for(feature, attribute)
|
28
|
+
errors = feature.errors.on(attribute)
|
29
|
+
if errors && errors.any?
|
30
|
+
content_tag(:ul, :class => 'errors') do
|
31
|
+
errors.map { |msg| content_tag(:li, msg, :class => 'error') }.join(''.html_safe)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
''
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
# a stub
|
4
|
+
# possible TODO: remove and and refactor into an acts_as_feature mixin
|
5
|
+
module Arturo
|
6
|
+
class Feature < ::ActiveRecord::Base
|
7
|
+
|
8
|
+
include Arturo::SpecialHandling
|
9
|
+
|
10
|
+
Arturo::Feature::SYMBOL_REGEX = /^[a-zA-z][a-zA-Z0-9_]*$/
|
11
|
+
DEFAULT_ATTRIBUTES = { :deployment_percentage => 0 }
|
12
|
+
|
13
|
+
attr_readonly :symbol
|
14
|
+
|
15
|
+
validates_presence_of :symbol, :deployment_percentage
|
16
|
+
validates_uniqueness_of :symbol, :allow_blank => true
|
17
|
+
validates_numericality_of :deployment_percentage,
|
18
|
+
:only_integer => true,
|
19
|
+
:allow_blank => true,
|
20
|
+
:greater_than_or_equal_to => 0,
|
21
|
+
:less_than_or_equal_to => 100
|
22
|
+
|
23
|
+
# Looks up a feature by symbol. Also accepts a Feature as input.
|
24
|
+
# @param [Symbol, Arturo::Feature] feature_or_name a Feature or the Symbol of a Feature
|
25
|
+
# @return [Arturo::Feature, nil] the Feature if found, else nil
|
26
|
+
def self.to_feature(feature_or_symbol)
|
27
|
+
return feature_or_symbol if feature_or_symbol.kind_of?(self)
|
28
|
+
self.find(:first, :conditions => { :symbol => feature_or_symbol.to_s })
|
29
|
+
end
|
30
|
+
|
31
|
+
# Create a new Feature
|
32
|
+
def initialize(attributes = {})
|
33
|
+
super(DEFAULT_ATTRIBUTES.merge(attributes || {}))
|
34
|
+
end
|
35
|
+
|
36
|
+
# @param [Object] feature_recipient a User, Account,
|
37
|
+
# or other model with an #id method
|
38
|
+
# @return [true,false] whether or not this feature is enabled
|
39
|
+
# for feature_recipient
|
40
|
+
# @see Arturo::SpecialHandling#whitelisted?
|
41
|
+
# @see Arturo::SpecialHandling#blacklisted?
|
42
|
+
def enabled_for?(feature_recipient)
|
43
|
+
return false if feature_recipient.nil?
|
44
|
+
return false if blacklisted?(feature_recipient)
|
45
|
+
return true if whitelisted?(feature_recipient)
|
46
|
+
passes_threshold?(feature_recipient)
|
47
|
+
end
|
48
|
+
|
49
|
+
def name
|
50
|
+
return I18n.translate("arturo.feature.nameless") if symbol.blank?
|
51
|
+
I18n.translate("arturo.feature.#{symbol}", :default => symbol.to_s.titleize)
|
52
|
+
end
|
53
|
+
|
54
|
+
def to_s
|
55
|
+
"Feature #{name}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def to_param
|
59
|
+
new_record? ? nil : "#{id}-#{symbol.to_s.parameterize}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
"<Arturo::Feature #{name}, deployed to #{deployment_percentage}%>"
|
64
|
+
end
|
65
|
+
|
66
|
+
def symbol
|
67
|
+
sym = read_attribute(:symbol).to_s
|
68
|
+
sym.blank? ? nil : sym.to_sym
|
69
|
+
end
|
70
|
+
|
71
|
+
def symbol=(sym)
|
72
|
+
write_attribute(:symbol, sym.to_s)
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
def passes_threshold?(feature_recipient)
|
78
|
+
threshold = self.deployment_percentage || 0
|
79
|
+
return false if threshold == 0
|
80
|
+
return true if threshold == 100
|
81
|
+
(((feature_recipient.id + 17) * 13) % 100) < threshold
|
82
|
+
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,5 @@
|
|
1
|
+
<tr class='feature' id="feature_<%= feature.id %>">
|
2
|
+
<td><code class='ruby symbol'><%= feature.symbol %></code></td>
|
3
|
+
<td><%= deployment_percentage_range_and_output_tags("features[#{feature.id}][deployment_percentage]", feature.deployment_percentage) %></td>
|
4
|
+
<td><%= link_to t('.edit'), edit_feature_path(feature), :rel => 'edit', :class => 'edit' %></td>
|
5
|
+
</tr>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<% form_for(feature, :as => 'feature', :url => (feature.new_record? ? features_path : feature_path(feature))) do |form| %>
|
2
|
+
<fieldset>
|
3
|
+
<legend><%= legend %></legend>
|
4
|
+
|
5
|
+
<%= form.label(:symbol) %>
|
6
|
+
<%= form.text_field(:symbol, :required => 'required', :pattern => Arturo::Feature::SYMBOL_REGEX.source, :class => 'symbol') %>
|
7
|
+
<%= error_messages_for(feature, :symbol) %>
|
8
|
+
|
9
|
+
<%= form.label(:deployment_percentage) %>
|
10
|
+
<%= form.range_field(:deployment_percentage, :min => '0', :max => '100', :step => '1', :class => 'deployment_percentage') %>
|
11
|
+
<%= deployment_percentage_output_tag 'feature_deployment_percentage', feature.deployment_percentage %>
|
12
|
+
<%= error_messages_for(feature, :deployment_percentage) %>
|
13
|
+
|
14
|
+
<footer><%= form.submit %></footer>
|
15
|
+
</fieldset>
|
16
|
+
<% end %>
|
@@ -0,0 +1,29 @@
|
|
1
|
+
<h2><%= t('.title') %></h2>
|
2
|
+
<% form_tag(features_path, :method => 'put', 'data-update-path' => feature_path(:id => ':id'), :remote => true) do %>
|
3
|
+
<fieldset>
|
4
|
+
<legend><%= t('.title') %></legend>
|
5
|
+
<table class='features'>
|
6
|
+
<col class='name' />
|
7
|
+
<col class='deployment_percentage' />
|
8
|
+
<col class='edit' />
|
9
|
+
<thead>
|
10
|
+
<tr>
|
11
|
+
<th><%= t('activerecord.attributes.arturo/feature.name') %></th>
|
12
|
+
<th><%= t('activerecord.attributes.arturo/feature.deployment_percentage') %></th>
|
13
|
+
<th> </th>
|
14
|
+
</tr>
|
15
|
+
</thead>
|
16
|
+
<tfoot>
|
17
|
+
<tr><th colspan='4'><%= link_to t('.new'), new_feature_path %> <%= submit_tag %></th></tr>
|
18
|
+
</tfoot>
|
19
|
+
<tbody>
|
20
|
+
<% @features.each do |f| %>
|
21
|
+
<%= render :partial => 'feature', :locals => { :feature => f } %>
|
22
|
+
<% end %>
|
23
|
+
<% if @features.none? %>
|
24
|
+
<tr class='if_no_features'><td colspan='4'><%= t('.none_yet') %></td></tr>
|
25
|
+
<% end %>
|
26
|
+
</tbody>
|
27
|
+
</table>
|
28
|
+
</fieldset>
|
29
|
+
<% end %>
|
@@ -0,0 +1,40 @@
|
|
1
|
+
en:
|
2
|
+
activerecord:
|
3
|
+
models:
|
4
|
+
"arturo/feature": "Feature"
|
5
|
+
attributes:
|
6
|
+
"arturo/feature":
|
7
|
+
symbol: "Symbol"
|
8
|
+
name: "Name"
|
9
|
+
deployment_percentage: "Deployment Percentage"
|
10
|
+
arturo:
|
11
|
+
feature:
|
12
|
+
nameless: "(no name)"
|
13
|
+
features:
|
14
|
+
index:
|
15
|
+
title: 'Features'
|
16
|
+
new: 'New'
|
17
|
+
none_yet: No features yet.
|
18
|
+
new:
|
19
|
+
title: New Feature
|
20
|
+
legend: "New Feature"
|
21
|
+
edit:
|
22
|
+
title: "Edit Feature %{name}"
|
23
|
+
legend: "Edit Feature %{name}"
|
24
|
+
feature:
|
25
|
+
edit: 'Edit'
|
26
|
+
show:
|
27
|
+
title: "Feature %{name}"
|
28
|
+
forbidden:
|
29
|
+
title: Forbidden
|
30
|
+
text: You do not have permission to access that resource.
|
31
|
+
flash:
|
32
|
+
no_such_feature: "No such feature: {id}"
|
33
|
+
error_updating: "Error updating feature #{id}"
|
34
|
+
updated_many: "Updated {count} feature(s)"
|
35
|
+
created: "Created {name}"
|
36
|
+
error_creating: "Sorry, there was an error creating the feature."
|
37
|
+
updated: "Updated {name}"
|
38
|
+
error_updating: "Sorry, there was an error updating {name}"
|
39
|
+
removed: "Removed {name}"
|
40
|
+
error_removing: "Sorry, there was an error removing {name}"
|
data/lib/arturo.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Arturo
|
2
|
+
|
3
|
+
require 'arturo/special_handling'
|
4
|
+
require 'arturo/feature_availability'
|
5
|
+
require 'arturo/feature_management'
|
6
|
+
require 'arturo/controller_filters'
|
7
|
+
require 'arturo/range_form_support'
|
8
|
+
require 'arturo/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 2 && Rails::VERSION::MINOR == 3
|
9
|
+
|
10
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Arturo
|
2
|
+
|
3
|
+
# Adds before filters to controllers for specifying that actions
|
4
|
+
# require features to be enabled for the requester.
|
5
|
+
#
|
6
|
+
# To configure how the controller responds when the feature is
|
7
|
+
# *not* enabled, redefine #on_feature_disabled(feature_name).
|
8
|
+
# It must render or raise an exception.
|
9
|
+
module ControllerFilters
|
10
|
+
|
11
|
+
def self.included(base)
|
12
|
+
base.extend Arturo::ControllerFilters::ClassMethods
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_feature_disabled(feature_name)
|
16
|
+
render :text => 'Forbidden', :status => 403
|
17
|
+
end
|
18
|
+
|
19
|
+
module ClassMethods
|
20
|
+
|
21
|
+
def require_feature(name, options = {})
|
22
|
+
before_filter options do |controller|
|
23
|
+
unless controller.feature_enabled?(name)
|
24
|
+
controller.on_feature_disabled(name)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
ActionController::Base.class_eval do
|
2
|
+
include Arturo::FeatureAvailability
|
3
|
+
helper Arturo::FeatureAvailability
|
4
|
+
include Arturo::ControllerFilters
|
5
|
+
helper Arturo::FeatureManagement
|
6
|
+
helper Arturo::RangeFormSupport::HelperMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
ActionView::Helpers::FormBuilder.instance_eval do
|
10
|
+
include Arturo::RangeFormSupport::FormBuilderMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'rails_generator'
|
14
|
+
generators_path = File.expand_path('../../generators', __FILE__)
|
15
|
+
Rails::Generator::Base.sources << Rails::Generator::PathSource.new(:arturo, generators_path)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Arturo
|
2
|
+
|
3
|
+
# A mixin that provides #feature_enabled? and #if_feature_enabled
|
4
|
+
# methods; to be mixed in by Controllers and Helpers. The including
|
5
|
+
# class must return some "thing that has features" (e.g. a User, Person,
|
6
|
+
# or Account) when Arturo.feature_recipient is bound to an instance
|
7
|
+
# and called.
|
8
|
+
#
|
9
|
+
# @see Arturo.feature_recipient
|
10
|
+
module FeatureAvailability
|
11
|
+
|
12
|
+
def feature_enabled?(symbol_or_feature)
|
13
|
+
feature = ::Arturo::Feature.to_feature(symbol_or_feature)
|
14
|
+
return false if feature.blank?
|
15
|
+
feature.enabled_for?(feature_recipient)
|
16
|
+
end
|
17
|
+
|
18
|
+
def if_feature_enabled(symbol_or_feature, &block)
|
19
|
+
if feature_enabled?(symbol_or_feature)
|
20
|
+
block.call
|
21
|
+
else
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# By default, returns current_user.
|
27
|
+
#
|
28
|
+
# If you would like to change this implementation, it is recommended
|
29
|
+
# you do so in config/initializers/arturo_initializer.rb
|
30
|
+
# @return [Object] the recipient of features.
|
31
|
+
def feature_recipient
|
32
|
+
current_user
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Arturo
|
2
|
+
|
3
|
+
# A mixin that is included by Arturo::FeaturesController and is declared
|
4
|
+
# as a helper for all views. It provides a single method,
|
5
|
+
# may_manage_features?, that returns whether or not the current user
|
6
|
+
# may manage features. By default, it is implemented as follows:
|
7
|
+
#
|
8
|
+
# def may_manage_features?
|
9
|
+
# current_user.present? && current_user.admin?
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# If you would like to change this implementation, it is recommended
|
13
|
+
# you do so in config/initializers/arturo_initializer.rb
|
14
|
+
module FeatureManagement
|
15
|
+
|
16
|
+
# @return [true,false] whether the current user may manage features
|
17
|
+
def may_manage_features?
|
18
|
+
current_user.present? && current_user.admin?
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Arturo
|
2
|
+
module RangeFormSupport
|
3
|
+
|
4
|
+
module HelperMethods
|
5
|
+
def range_field(object_name, method, options = {})
|
6
|
+
ActionView::Helpers::InstanceTag.new(object_name, method, self, options.delete(:object)).to_input_field_tag("range", options)
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
module FormBuilderMethods
|
11
|
+
def range_field(method, options = {})
|
12
|
+
@template.send('range_field', @object_name, method, objectify_options(options))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Arturo
|
2
|
+
|
3
|
+
# Adds whitelist and blacklist support to individual features by name.
|
4
|
+
# Blacklists override whitelists. (In the world of Apache, Features
|
5
|
+
# are "(deny,allow)".)
|
6
|
+
# @example
|
7
|
+
# # allow admins:
|
8
|
+
# Arturo::Feature.whitelist(:some_feature) do |user|
|
9
|
+
# user.is_admin?
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# # disallow for small accounts:
|
13
|
+
# Arturo::Feature.blacklist(:another_feature) do |user|
|
14
|
+
# user.account.small?
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# Blacklists and whitelists can be defined before the feature exists
|
18
|
+
# and are not persisted, so they are best defined in initializers.
|
19
|
+
# This is particularly important if your application runs in several
|
20
|
+
# different processes or on several servers.
|
21
|
+
module SpecialHandling
|
22
|
+
|
23
|
+
def self.included(base)
|
24
|
+
base.extend Arturo::SpecialHandling::ClassMethods
|
25
|
+
end
|
26
|
+
|
27
|
+
module ClassMethods
|
28
|
+
def whitelists
|
29
|
+
@whitelists ||= {}
|
30
|
+
end
|
31
|
+
|
32
|
+
def blacklists
|
33
|
+
@blacklists ||= {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def whitelist(feature_symbol, &block)
|
37
|
+
whitelists[feature_symbol.to_sym] = block
|
38
|
+
end
|
39
|
+
|
40
|
+
def blacklist(feature_symbol, &block)
|
41
|
+
blacklists[feature_symbol.to_sym] = block
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def whitelisted?(feature_recipient)
|
48
|
+
x_listed?(self.class.whitelists, feature_recipient)
|
49
|
+
end
|
50
|
+
|
51
|
+
def blacklisted?(feature_recipient)
|
52
|
+
x_listed?(self.class.blacklists, feature_recipient)
|
53
|
+
end
|
54
|
+
|
55
|
+
def x_listed?(list_map, feature_recipient)
|
56
|
+
list = list_map[self.symbol.to_sym]
|
57
|
+
list.present? && list.call(feature_recipient)
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'rails_generator'
|
2
|
+
|
3
|
+
class ArturoGenerator < Rails::Generator::Base
|
4
|
+
def manifest
|
5
|
+
record do |m|
|
6
|
+
m.file 'initializer.rb', 'config/initializers/arturo_initializer.rb'
|
7
|
+
m.file 'arturo.css', 'public/stylesheets/arturo.css', :collision => :force
|
8
|
+
m.file 'arturo_customizations.css', 'public/stylesheets/arturo_customizations.css', :collision => :skip
|
9
|
+
m.file 'arturo.js', 'public/javascripts/arturo.js'
|
10
|
+
m.file 'semicolon.png', 'public/images/semicolon.png'
|
11
|
+
m.migration_template 'migration.rb', 'db/migrate', :migration_file_name => 'create_features'
|
12
|
+
add_feature_routes(m)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def source_root
|
19
|
+
File.expand_path('../templates', __FILE__)
|
20
|
+
end
|
21
|
+
|
22
|
+
def banner
|
23
|
+
%{Usage: #{$0} #{spec.name}\nCopies an initializer; copies CSS, JS, and PNG assets; generates a migration; adds routes/}
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_feature_routes(manifest)
|
27
|
+
sentinel = 'ActionController::Routing::Routes.draw do |map|'
|
28
|
+
logger.route "map.resources features"
|
29
|
+
unless options[:pretend]
|
30
|
+
manifest.gsub_file 'config/routes.rb', /(#{Regexp.escape(sentinel)})/mi do |match|
|
31
|
+
"#{match}#{feature_routes}\n"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def feature_routes
|
37
|
+
"\n map.resources :features, :controller => 'arturo/features'" +
|
38
|
+
"\n map.features 'features', :controller => 'arturo/features', :action => 'update_all', :conditions => { :method => :put }"
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
/*
|
2
|
+
WARNING:
|
3
|
+
|
4
|
+
Do not edit this file. Any changes you make to this file will be overwritten
|
5
|
+
when you regenerate the arturo assets (which happens when you upgrade the gem).
|
6
|
+
Instead, make customizations to arturo_customizations.css.
|
7
|
+
*/
|
8
|
+
|
9
|
+
.features code.symbol:before { content: ":"; }
|
10
|
+
|
11
|
+
.features { border-collapse: collapse; }
|
12
|
+
|
13
|
+
.features thead tr:last-child th { border-bottom: 1px solid; }
|
14
|
+
.features tfoot tr:first-child th { border-top: 1px solid; }
|
15
|
+
|
16
|
+
.features th, .features td {
|
17
|
+
margin: 0;
|
18
|
+
padding: 0.5em 1.5em;
|
19
|
+
text-align: left;
|
20
|
+
}
|
21
|
+
|
22
|
+
input.deployment_percentage[type=range] { width: 200px; }
|
23
|
+
|
24
|
+
output.deployment_percentage.no_js { display: none; }
|
25
|
+
output.deployment_percentage { margin-left: 1em; }
|
26
|
+
output.deployment_percentage:after { content: "%"; }
|
27
|
+
|
28
|
+
.features a[rel=edit] { visibility: hidden; }
|
29
|
+
.features tr:hover a[rel=edit] { visibility: inherit; }
|
30
|
+
|
31
|
+
.features tfoot th {
|
32
|
+
text-align: right;
|
33
|
+
}
|
34
|
+
|
35
|
+
.features tfoot th * + * {
|
36
|
+
margin-left: 2em;
|
37
|
+
}
|
38
|
+
|
39
|
+
.feature_new label, .feature_edit label { font-weight: bold; }
|
40
|
+
|
41
|
+
.feature_new label, .feature_new .errors,
|
42
|
+
.feature_edit label, .feature_edit .errors {
|
43
|
+
display: block;
|
44
|
+
}
|
45
|
+
|
46
|
+
.feature_new label + input, .feature_new label + textarea, .feature_new label + select,
|
47
|
+
.feature_edit label + input, .feature_edit label + textarea, .feature_edit label + select {
|
48
|
+
margin-top: 0.5em;
|
49
|
+
}
|
50
|
+
|
51
|
+
.feature_new input + label, .feature_new textarea + label, .feature_new select + label,
|
52
|
+
.feature_edit input + label, .feature_edit textarea + label, .feature_edit select + label {
|
53
|
+
margin-top: 1.5em;
|
54
|
+
}
|
55
|
+
|
56
|
+
.feature_new input[type=text], .feature_edit input[type=text] { padding: 0.5em; }
|
57
|
+
|
58
|
+
.feature_new input.symbol, .feature_edit input.symbol {
|
59
|
+
background: transparent url('/images/semicolon.png') no-repeat 3px 4px;
|
60
|
+
font-family: "DejaVu Sans Mono", "Droid Sans Mono", "Mondale", monospace;
|
61
|
+
padding-left: 9px;
|
62
|
+
}
|
63
|
+
|
64
|
+
.feature_new .errors, .feature_edit .errors { color: red; }
|
65
|
+
.feature_new :invalid { border-color: red; }
|
66
|
+
|
67
|
+
.feature_new footer, .feature_edit footer { margin-top: 2em; }
|
@@ -0,0 +1,23 @@
|
|
1
|
+
if (typeof(jQuery) === 'function') {
|
2
|
+
jQuery.arturo = {
|
3
|
+
agentSupportsHTML5Output: ('for' in jQuery('<output />')),
|
4
|
+
|
5
|
+
linkAndShowOutputs: function() {
|
6
|
+
if (jQuery.arturo.agentSupportsHTML5Output) {
|
7
|
+
jQuery('.features output,.feature_new output,.feature_edit output').each(function(i, output) {
|
8
|
+
var output = jQuery(output);
|
9
|
+
var input = jQuery('#' + output.attr('for'));
|
10
|
+
input.change(function() {
|
11
|
+
console.log('input value changed to ' + input.val());
|
12
|
+
output.val(input.val());
|
13
|
+
});
|
14
|
+
output.removeClass('no_js');
|
15
|
+
});
|
16
|
+
}
|
17
|
+
}
|
18
|
+
};
|
19
|
+
|
20
|
+
jQuery(function() {
|
21
|
+
jQuery.arturo.linkAndShowOutputs();
|
22
|
+
});
|
23
|
+
}
|
@@ -0,0 +1 @@
|
|
1
|
+
/* Make any customizations to the Arturo styles here */
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'arturo'
|
2
|
+
|
3
|
+
# Configure who may manage features here.
|
4
|
+
# The following is the default implementation.
|
5
|
+
# Arturo::FeatureManagement.class_eval do
|
6
|
+
# def may_manage_features?
|
7
|
+
# current_user.present? && current_user.admin?
|
8
|
+
# end
|
9
|
+
# end
|
10
|
+
|
11
|
+
# Configure what receives features here.
|
12
|
+
# The following is the default implementation.
|
13
|
+
# Arturo::FeatureAvailability.class_eval do
|
14
|
+
# def feature_recipient
|
15
|
+
# current_user
|
16
|
+
# end
|
17
|
+
# end
|
18
|
+
|
19
|
+
# Whitelists and Blacklists:
|
20
|
+
#
|
21
|
+
# Enable feature one for all admins:
|
22
|
+
# Arturo::Feature.whitelist(:feature_one) do |user|
|
23
|
+
# user.admin?
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Disable feature two for all small accounts:
|
27
|
+
# Arturo::Feature.blacklist(:feature_two) do |user|
|
28
|
+
# user.account.small?
|
29
|
+
# end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'active_support/core_ext'
|
2
|
+
|
3
|
+
class CreateFeatures < ActiveRecord::Migration
|
4
|
+
def self.up
|
5
|
+
create_table :features do |t|
|
6
|
+
t.string :symbol, :null => false
|
7
|
+
t.integer :deployment_percentage, :null => false
|
8
|
+
#Any additional fields here
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.down
|
15
|
+
drop_table :features
|
16
|
+
end
|
17
|
+
end
|
Binary file
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: arturo
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 81
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 2
|
9
|
+
- 3
|
10
|
+
- 1
|
11
|
+
version: 0.2.3.1
|
12
|
+
platform: ruby
|
13
|
+
authors:
|
14
|
+
- James A. Rosen
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-10-31 00:00:00 -07:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: rails
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ~>
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 19
|
31
|
+
segments:
|
32
|
+
- 2
|
33
|
+
- 3
|
34
|
+
- 8
|
35
|
+
version: 2.3.8
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id001
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: mocha
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
hash: 3
|
47
|
+
segments:
|
48
|
+
- 0
|
49
|
+
version: "0"
|
50
|
+
type: :development
|
51
|
+
version_requirements: *id002
|
52
|
+
- !ruby/object:Gem::Dependency
|
53
|
+
name: rake
|
54
|
+
prerelease: false
|
55
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
hash: 3
|
61
|
+
segments:
|
62
|
+
- 0
|
63
|
+
version: "0"
|
64
|
+
type: :development
|
65
|
+
version_requirements: *id003
|
66
|
+
- !ruby/object:Gem::Dependency
|
67
|
+
name: redgreen
|
68
|
+
prerelease: false
|
69
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ~>
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
hash: 11
|
75
|
+
segments:
|
76
|
+
- 1
|
77
|
+
- 2
|
78
|
+
version: "1.2"
|
79
|
+
type: :development
|
80
|
+
version_requirements: *id004
|
81
|
+
- !ruby/object:Gem::Dependency
|
82
|
+
name: sqlite3-ruby
|
83
|
+
prerelease: false
|
84
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
85
|
+
none: false
|
86
|
+
requirements:
|
87
|
+
- - ~>
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
hash: 9
|
90
|
+
segments:
|
91
|
+
- 1
|
92
|
+
- 3
|
93
|
+
version: "1.3"
|
94
|
+
type: :development
|
95
|
+
version_requirements: *id005
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: factory_girl
|
98
|
+
prerelease: false
|
99
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ~>
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
hash: 9
|
105
|
+
segments:
|
106
|
+
- 1
|
107
|
+
- 3
|
108
|
+
version: "1.3"
|
109
|
+
type: :development
|
110
|
+
version_requirements: *id006
|
111
|
+
description: Deploy features incrementally to your users
|
112
|
+
email: james.a.rosen@gmail.com
|
113
|
+
executables: []
|
114
|
+
|
115
|
+
extensions: []
|
116
|
+
|
117
|
+
extra_rdoc_files: []
|
118
|
+
|
119
|
+
files:
|
120
|
+
- lib/arturo/controller_filters.rb
|
121
|
+
- lib/arturo/engine.rb
|
122
|
+
- lib/arturo/feature_availability.rb
|
123
|
+
- lib/arturo/feature_factories.rb
|
124
|
+
- lib/arturo/feature_management.rb
|
125
|
+
- lib/arturo/range_form_support.rb
|
126
|
+
- lib/arturo/special_handling.rb
|
127
|
+
- lib/arturo.rb
|
128
|
+
- lib/generators/arturo/arturo_generator.rb
|
129
|
+
- lib/generators/arturo/templates/arturo.css
|
130
|
+
- lib/generators/arturo/templates/arturo.js
|
131
|
+
- lib/generators/arturo/templates/arturo_customizations.css
|
132
|
+
- lib/generators/arturo/templates/initializer.rb
|
133
|
+
- lib/generators/arturo/templates/migration.rb
|
134
|
+
- lib/generators/arturo/templates/semicolon.png
|
135
|
+
- app/controllers/arturo/features_controller.rb
|
136
|
+
- app/helpers/arturo/features_helper.rb
|
137
|
+
- app/models/arturo/feature.rb
|
138
|
+
- app/views/arturo/features/_feature.html.erb
|
139
|
+
- app/views/arturo/features/_form.html.erb
|
140
|
+
- app/views/arturo/features/edit.html.erb
|
141
|
+
- app/views/arturo/features/forbidden.html.erb
|
142
|
+
- app/views/arturo/features/index.html.erb
|
143
|
+
- app/views/arturo/features/new.html.erb
|
144
|
+
- app/views/arturo/features/show.html.erb
|
145
|
+
- config/locales/en.yml
|
146
|
+
- README.md
|
147
|
+
- HISTORY.md
|
148
|
+
has_rdoc: true
|
149
|
+
homepage: http://github.com/jamesarosen/arturo
|
150
|
+
licenses: []
|
151
|
+
|
152
|
+
post_install_message:
|
153
|
+
rdoc_options: []
|
154
|
+
|
155
|
+
require_paths:
|
156
|
+
- .
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ">="
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
hash: 3
|
164
|
+
segments:
|
165
|
+
- 0
|
166
|
+
version: "0"
|
167
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
168
|
+
none: false
|
169
|
+
requirements:
|
170
|
+
- - ">="
|
171
|
+
- !ruby/object:Gem::Version
|
172
|
+
hash: 3
|
173
|
+
segments:
|
174
|
+
- 0
|
175
|
+
version: "0"
|
176
|
+
requirements: []
|
177
|
+
|
178
|
+
rubyforge_project:
|
179
|
+
rubygems_version: 1.3.7
|
180
|
+
signing_key:
|
181
|
+
specification_version: 2
|
182
|
+
summary: Feature sliders, wrapped up in an engine
|
183
|
+
test_files: []
|
184
|
+
|