arturo 0.2.3.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|