arturo 0.2.3.8 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +58 -79
- data/app/controllers/arturo/features_controller.rb +20 -25
- data/app/helpers/arturo/features_helper.rb +10 -0
- data/app/models/arturo/feature.rb +6 -18
- data/app/views/arturo/features/_form.html.erb +3 -7
- data/app/views/arturo/features/index.html.erb +1 -1
- data/config/routes.rb +14 -0
- data/lib/arturo.rb +2 -31
- data/lib/arturo/configuration.rb +43 -0
- data/lib/arturo/controller_filters.rb +1 -1
- data/lib/arturo/engine.rb +8 -14
- data/lib/arturo/feature_availability.rb +2 -10
- data/lib/generators/arturo/assets_generator.rb +18 -0
- data/lib/generators/arturo/initializer_generator.rb +13 -0
- data/lib/generators/arturo/migration_generator.rb +27 -0
- data/lib/generators/arturo/routes_generator.rb +15 -0
- data/lib/generators/arturo/templates/arturo.css +2 -2
- data/lib/generators/arturo/templates/arturo.js +1 -1
- data/lib/generators/arturo/templates/initializer.rb +10 -18
- metadata +107 -154
- data/HISTORY.md +0 -16
- data/lib/arturo/feature_caching.rb +0 -141
- data/lib/arturo/feature_management.rb +0 -23
- data/lib/arturo/middleware.rb +0 -60
- data/lib/arturo/range_form_support.rb +0 -17
- data/lib/arturo/test_support.rb +0 -32
- data/lib/generators/arturo/arturo_generator.rb +0 -40
- data/rails/init.rb +0 -7
data/README.md
CHANGED
@@ -1,10 +1,3 @@
|
|
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
1
|
## What
|
9
2
|
|
10
3
|
Arturo provides feature sliders for Rails. It lets you turn features on and off
|
@@ -55,7 +48,7 @@ the feature:
|
|
55
48
|
|
56
49
|
# in app/controllers/postings_controller:
|
57
50
|
class PostingsController < ApplicationController
|
58
|
-
require_feature :live_postings, :only => :recent
|
51
|
+
require_feature! :live_postings, :only => :recent
|
59
52
|
# ...
|
60
53
|
end
|
61
54
|
|
@@ -71,27 +64,25 @@ Vijay can bump the deployment percentage up to 50%. A few more days go by
|
|
71
64
|
and they clean up the last few bugs they found with the "live_postings"
|
72
65
|
feature and deploy it to all users.
|
73
66
|
|
74
|
-
##
|
67
|
+
## Installation
|
75
68
|
|
76
|
-
|
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.
|
69
|
+
### In Rails 3, with Bundler
|
80
70
|
|
81
|
-
|
71
|
+
gem 'arturo', '~> 1.0'
|
82
72
|
|
83
|
-
In
|
73
|
+
### In Rails 3, without Bundler
|
84
74
|
|
85
|
-
gem
|
86
|
-
|
75
|
+
$ gem install arturo --version="~> 1.0"
|
76
|
+
|
77
|
+
**Note**: the following two sections describe the **intended** use of
|
78
|
+
Arturo with Rails 2. Arturo does not yet have Rails 2 support.
|
87
79
|
|
88
|
-
|
89
|
-
(and thus not pick up its "engine-ness") without **also** declaring
|
90
|
-
it as a Rails gem. Thus, in `config/environment.rb`:
|
80
|
+
### In Rails 2, with Bundler
|
91
81
|
|
92
|
-
|
82
|
+
gem 'arturo', :git => 'git://github.com/jamesarosen/arturo.git',
|
83
|
+
:tag => 'rails_2_3'
|
93
84
|
|
94
|
-
### In Rails 2
|
85
|
+
### In Rails 2, without Bundler
|
95
86
|
|
96
87
|
Put the `rails_2_3` branch of `git://github.com/jamesarosen/arturo.git` into
|
97
88
|
your `vendor/plugins/` directory. You can use Git submodules or a simple
|
@@ -101,18 +92,12 @@ checkout.
|
|
101
92
|
|
102
93
|
### In Rails
|
103
94
|
|
104
|
-
#### Run the
|
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:
|
95
|
+
#### Run the migrations:
|
114
96
|
|
115
|
-
$
|
97
|
+
$ rails g arturo:migration
|
98
|
+
$ rails g arturo:initializer
|
99
|
+
$ rails g arturo:route
|
100
|
+
$ rails g arturo:assets
|
116
101
|
|
117
102
|
#### Edit the configuration
|
118
103
|
|
@@ -121,10 +106,10 @@ checkout.
|
|
121
106
|
Open up the newly-generated `config/initializers/arturo_initializer.rb`.
|
122
107
|
There are configuration options for the following:
|
123
108
|
|
124
|
-
* the
|
109
|
+
* the block that determines whether a user has permission to manage features
|
125
110
|
(see [admin permissions](#adminpermissions))
|
126
|
-
* the
|
127
|
-
(
|
111
|
+
* the block that yields the object that has features
|
112
|
+
(a User, Person, or Account, see
|
128
113
|
[feature recipients](#featurerecipients))
|
129
114
|
* whitelists and blacklists for features
|
130
115
|
(see [white- and blacklisting](#wblisting))
|
@@ -147,19 +132,20 @@ work with you on support for your favorite framework.
|
|
147
132
|
|
148
133
|
### <span id='adminpermissions'>Admin Permissions</span>
|
149
134
|
|
150
|
-
`Arturo
|
151
|
-
|
152
|
-
|
153
|
-
is as follows:
|
154
|
-
|
155
|
-
current_user.present? && current_user.admin?
|
156
|
-
|
157
|
-
You can change the implementation in
|
135
|
+
`Arturo.permit_management` is a block that is run in the context of
|
136
|
+
a Controller instance. It should return `true` iff the current user
|
137
|
+
can manage permissions. Configure the block in
|
158
138
|
`config/initializers/arturo_initializer.rb`. A reasonable implementation
|
159
139
|
might be
|
160
140
|
|
161
141
|
Arturo.permit_management do
|
162
|
-
|
142
|
+
current_user.admin?
|
143
|
+
end
|
144
|
+
|
145
|
+
or
|
146
|
+
|
147
|
+
Arturo.permit_management do
|
148
|
+
signed_in? && signed_in_person.can?(:manage_features)
|
163
149
|
end
|
164
150
|
|
165
151
|
### <span id='featurerecipients'>Feature Recipients</span>
|
@@ -171,23 +157,35 @@ early days -- may have deployed features on a per-university basis. It wouldn't
|
|
171
157
|
make much sense to deploy a feature to one user of a Basecamp project but not
|
172
158
|
to others, so 37Signals would probably want a per-project or per-account basis.
|
173
159
|
|
174
|
-
`Arturo
|
175
|
-
|
176
|
-
|
177
|
-
|
160
|
+
`Arturo.feature_recipient` is intended to support these many use cases. It is a
|
161
|
+
block that returns the current "thing" (a user, account, project, university,
|
162
|
+
...) that is a member of the category that is the basis for deploying new
|
163
|
+
features. Like `Arturo.permit_management`, it is configured in
|
164
|
+
`config/initializers/arturo_initializer.rb`. It should return an `Object` that
|
165
|
+
responds to `#id`. If you want to deploy features on a per-user basis, a
|
166
|
+
reasonable implementation might be
|
178
167
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
168
|
+
Arturo.thing_that_has_features do
|
169
|
+
current_user
|
170
|
+
end
|
171
|
+
|
172
|
+
or
|
173
|
+
|
174
|
+
Arturo.thing_that_has_features do
|
175
|
+
signed_in_person
|
176
|
+
end
|
177
|
+
|
178
|
+
If, on the other hand, you have accounts that have many users and you
|
179
|
+
want to deploy features on a per-account basis, a reasonable implementation
|
180
|
+
might be
|
183
181
|
|
184
|
-
Arturo.
|
182
|
+
Arturo.thing_that_has_features do
|
185
183
|
current_account
|
186
184
|
end
|
187
185
|
|
188
186
|
or
|
189
187
|
|
190
|
-
Arturo.
|
188
|
+
Arturo.thing_that_has_features do
|
191
189
|
current_user.account
|
192
190
|
end
|
193
191
|
|
@@ -226,10 +224,10 @@ every action within `BookHoldsController` that is invoked by a user who
|
|
226
224
|
does not have the `:hold_book` feature.
|
227
225
|
|
228
226
|
class BookHoldsController < ApplicationController
|
229
|
-
require_feature :hold_book
|
227
|
+
require_feature! :hold_book
|
230
228
|
end
|
231
229
|
|
232
|
-
`require_feature
|
230
|
+
`require_feature!` accepts as a second argument a `Hash` that it passes on
|
233
231
|
to `before_filter`, so you can use `:only` and `:except` to specify exactly
|
234
232
|
which actions are filtered.
|
235
233
|
|
@@ -256,34 +254,15 @@ The latter can be used like so:
|
|
256
254
|
widgets
|
257
255
|
end
|
258
256
|
|
259
|
-
#### Rack Middleware
|
260
|
-
|
261
|
-
require 'arturo'
|
262
|
-
use Arturo::Middleware, :feature => :my_feature
|
263
|
-
|
264
|
-
#### Outside a Controller
|
265
|
-
|
266
|
-
If you want to check availability outside of a controller or view (really
|
267
|
-
outside of something that has `Arturo::FeatureAvailability` mixed in), you
|
268
|
-
can ask either
|
269
|
-
|
270
|
-
Arturo.feature_enabled_for?(:foo, recipient)
|
271
|
-
|
272
|
-
or the slightly fancier
|
273
|
-
|
274
|
-
Arturo.foo_enabled_for?(recipient)
|
275
|
-
|
276
|
-
Both check whether the `foo` feature exists and is enabled for `recipient`.
|
277
|
-
|
278
257
|
#### Caching
|
279
258
|
|
280
259
|
**Note**: Arturo does not yet have caching support. Be very careful when
|
281
260
|
caching actions or pages that involve feature detection as you will get
|
282
|
-
strange behavior when a
|
261
|
+
strange behavior when a use who has access to a feature requests a page
|
283
262
|
just after one who does not (and vice versa). The following is the
|
284
263
|
**intended** support for caching.
|
285
264
|
|
286
|
-
Both the `require_feature
|
265
|
+
Both the `require_feature!` before filter and the `if_feature_enabled` block
|
287
266
|
evaluation automatically append a string based on the feature's
|
288
267
|
`last_modified` timestamp to cache keys that Rails generates. Thus, you don't
|
289
268
|
have to worry about expiring caches when you increase a feature's deployment
|
@@ -293,4 +272,4 @@ percentage. See `Arturo::CacheSupport` for more information.
|
|
293
272
|
|
294
273
|
Arturo gets its name from
|
295
274
|
[Professor Maximillian Arturo](http://en.wikipedia.org/wiki/Maximillian_Arturo)
|
296
|
-
on [Sliders](http://en.wikipedia.org/wiki/Sliders).
|
275
|
+
on [Sliders](http://en.wikipedia.org/wiki/Sliders).
|
@@ -1,23 +1,30 @@
|
|
1
1
|
require 'action_controller'
|
2
2
|
|
3
|
+
# TODO: this doesn't do anything radically out of the ordinary.
|
4
|
+
# Are there Rails 3 patterns/mixins/methods I can use
|
5
|
+
# to clean it up a bit?
|
3
6
|
module Arturo
|
4
7
|
|
8
|
+
begin
|
9
|
+
require 'application_controller'
|
10
|
+
rescue LoadError
|
11
|
+
# do nothing
|
12
|
+
end
|
13
|
+
|
14
|
+
base = Object.const_defined?(:ApplicationController) ? ApplicationController : ActionController::Base
|
15
|
+
|
5
16
|
# Handles all Feature actions. Clients of the Arturo engine
|
6
17
|
# should redefine Arturo::FeaturesController#permitted? to
|
7
18
|
# return true only for users who are permitted to manage features.
|
8
|
-
class FeaturesController <
|
9
|
-
|
10
|
-
|
19
|
+
class FeaturesController < base
|
20
|
+
unloadable
|
21
|
+
respond_to :html, :json, :xml
|
11
22
|
before_filter :require_permission
|
12
23
|
before_filter :load_feature, :only => [ :show, :edit, :update, :destroy ]
|
13
24
|
|
14
25
|
def index
|
15
26
|
@features = Arturo::Feature.all
|
16
|
-
|
17
|
-
format.html { }
|
18
|
-
format.json { render :json => @features }
|
19
|
-
format.xml { render :xml => @features }
|
20
|
-
end
|
27
|
+
respond_with @features
|
21
28
|
end
|
22
29
|
|
23
30
|
def update_all
|
@@ -43,20 +50,12 @@ module Arturo
|
|
43
50
|
end
|
44
51
|
|
45
52
|
def show
|
46
|
-
|
47
|
-
format.html { }
|
48
|
-
format.json { render :json => @feature }
|
49
|
-
format.xml { render :xml => @feature }
|
50
|
-
end
|
53
|
+
respond_with @feature
|
51
54
|
end
|
52
55
|
|
53
56
|
def new
|
54
57
|
@feature = Arturo::Feature.new(params[:feature])
|
55
|
-
|
56
|
-
format.html { }
|
57
|
-
format.json { render :json => @feature }
|
58
|
-
format.xml { render :xml => @feature }
|
59
|
-
end
|
58
|
+
respond_with @feature
|
60
59
|
end
|
61
60
|
|
62
61
|
def create
|
@@ -71,11 +70,7 @@ module Arturo
|
|
71
70
|
end
|
72
71
|
|
73
72
|
def edit
|
74
|
-
|
75
|
-
format.html { }
|
76
|
-
format.json { render :json => @feature }
|
77
|
-
format.xml { render :xml => @feature }
|
78
|
-
end
|
73
|
+
respond_with @feature
|
79
74
|
end
|
80
75
|
|
81
76
|
def update
|
@@ -83,7 +78,7 @@ module Arturo
|
|
83
78
|
flash[:notice] = t('arturo.features.flash.updated', :name => @feature.to_s)
|
84
79
|
redirect_to feature_path(@feature)
|
85
80
|
else
|
86
|
-
|
81
|
+
flash[:alert] = t('arturo.features.flash.error_updating', :name => @feature.to_s)
|
87
82
|
render :action => 'edit'
|
88
83
|
end
|
89
84
|
end
|
@@ -100,7 +95,7 @@ module Arturo
|
|
100
95
|
protected
|
101
96
|
|
102
97
|
def require_permission
|
103
|
-
unless
|
98
|
+
unless Arturo.permit_management?(self)
|
104
99
|
render :action => 'forbidden', :status => 403
|
105
100
|
return false
|
106
101
|
end
|
@@ -23,5 +23,15 @@ module Arturo
|
|
23
23
|
def deployment_percentage_output_tag(id, value)
|
24
24
|
content_tag(:output, value, { 'for' => id, 'class' => 'deployment_percentage no_js' })
|
25
25
|
end
|
26
|
+
|
27
|
+
def error_messages_for(feature, attribute)
|
28
|
+
if feature.errors[attribute].any?
|
29
|
+
content_tag(:ul, :class => 'errors') do
|
30
|
+
feature.errors[attribute].map { |msg| content_tag(:li, msg, :class => 'error') }.join(''.html_safe)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
''
|
34
|
+
end
|
35
|
+
end
|
26
36
|
end
|
27
37
|
end
|
@@ -8,7 +8,7 @@ module Arturo
|
|
8
8
|
include Arturo::SpecialHandling
|
9
9
|
|
10
10
|
Arturo::Feature::SYMBOL_REGEX = /^[a-zA-z][a-zA-Z0-9_]*$/
|
11
|
-
DEFAULT_ATTRIBUTES = { :deployment_percentage => 0 }
|
11
|
+
DEFAULT_ATTRIBUTES = { :deployment_percentage => 0 }
|
12
12
|
|
13
13
|
attr_readonly :symbol
|
14
14
|
|
@@ -25,11 +25,7 @@ module Arturo
|
|
25
25
|
# @return [Arturo::Feature, nil] the Feature if found, else nil
|
26
26
|
def self.to_feature(feature_or_symbol)
|
27
27
|
return feature_or_symbol if feature_or_symbol.kind_of?(self)
|
28
|
-
self.
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.find_feature(*args)
|
32
|
-
to_feature(*args)
|
28
|
+
self.where(:symbol => feature_or_symbol.to_sym).first
|
33
29
|
end
|
34
30
|
|
35
31
|
# Create a new Feature
|
@@ -60,29 +56,21 @@ module Arturo
|
|
60
56
|
end
|
61
57
|
|
62
58
|
def to_param
|
63
|
-
|
59
|
+
persisted? ? "#{id}-#{symbol.to_s.parameterize}" : nil
|
64
60
|
end
|
65
61
|
|
66
62
|
def inspect
|
67
63
|
"<Arturo::Feature #{name}, deployed to #{deployment_percentage}%>"
|
68
64
|
end
|
69
65
|
|
70
|
-
def symbol
|
71
|
-
sym = read_attribute(:symbol).to_s
|
72
|
-
sym.blank? ? nil : sym.to_sym
|
73
|
-
end
|
74
|
-
|
75
|
-
def symbol=(sym)
|
76
|
-
write_attribute(:symbol, sym.to_s)
|
77
|
-
end
|
78
|
-
|
79
66
|
protected
|
80
67
|
|
81
68
|
def passes_threshold?(feature_recipient)
|
82
69
|
threshold = self.deployment_percentage || 0
|
83
|
-
return false if threshold == 0
|
70
|
+
return false if threshold == 0
|
84
71
|
return true if threshold == 100
|
85
|
-
(((feature_recipient.id +
|
72
|
+
(((feature_recipient.id + 17) * 13) % 100) < threshold
|
73
|
+
|
86
74
|
end
|
87
75
|
end
|
88
76
|
end
|
@@ -1,19 +1,15 @@
|
|
1
|
-
|
2
|
-
:url => (feature.new_record? ? features_path : feature_path(feature)),
|
3
|
-
:html => {
|
4
|
-
:method => (feature.new_record? ? :post : :put)
|
5
|
-
}) do |form| %>
|
1
|
+
<%= form_for(feature, :as => 'feature', :url => (feature.new_record? ? features_path : feature_path(feature))) do |form| %>
|
6
2
|
<fieldset>
|
7
3
|
<legend><%= legend %></legend>
|
8
4
|
|
9
|
-
<%= error_messages_for(:feature, :object => feature) %>
|
10
|
-
|
11
5
|
<%= form.label(:symbol) %>
|
12
6
|
<%= form.text_field(:symbol, :required => 'required', :pattern => Arturo::Feature::SYMBOL_REGEX.source, :class => 'symbol') %>
|
7
|
+
<%= error_messages_for(feature, :symbol) %>
|
13
8
|
|
14
9
|
<%= form.label(:deployment_percentage) %>
|
15
10
|
<%= form.range_field(:deployment_percentage, :min => '0', :max => '100', :step => '1', :class => 'deployment_percentage') %>
|
16
11
|
<%= deployment_percentage_output_tag 'feature_deployment_percentage', feature.deployment_percentage %>
|
12
|
+
<%= error_messages_for(feature, :deployment_percentage) %>
|
17
13
|
|
18
14
|
<footer><%= form.submit %></footer>
|
19
15
|
</fieldset>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<h2><%= t('.title') %></h2>
|
2
|
-
|
2
|
+
<%= form_tag(features_path, :method => 'put', 'data-update-path' => feature_path(:id => ':id'), :remote => true) do %>
|
3
3
|
<fieldset>
|
4
4
|
<legend><%= t('.title') %></legend>
|
5
5
|
<table class='features'>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# In Rails edge, the engine can have its own route set
|
2
|
+
# and be mounted within an application at a sub-URL.
|
3
|
+
# In 3.0.1, this is not yet available.
|
4
|
+
|
5
|
+
# TODO replace this with the commented-out version below
|
6
|
+
Rails.application.routes.draw do
|
7
|
+
resources :features, :controller => 'arturo/features'
|
8
|
+
put 'features', :to => 'arturo/features#update_all', :as => 'features'
|
9
|
+
end
|
10
|
+
|
11
|
+
# Arturo::Engine.routes.draw do
|
12
|
+
# resources :features, :controller => 'arturo/features'
|
13
|
+
# put 'features', :to => 'arturo/features#update_all', :as => 'features'
|
14
|
+
# end
|
data/lib/arturo.rb
CHANGED
@@ -1,38 +1,9 @@
|
|
1
1
|
module Arturo
|
2
2
|
|
3
|
+
require 'arturo/configuration'
|
3
4
|
require 'arturo/special_handling'
|
4
5
|
require 'arturo/feature_availability'
|
5
|
-
require 'arturo/feature_management'
|
6
|
-
require 'arturo/feature_caching'
|
7
6
|
require 'arturo/controller_filters'
|
8
|
-
require 'arturo/
|
9
|
-
require 'arturo/middleware'
|
10
|
-
require 'arturo/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 2 && Rails::VERSION::MINOR == 3
|
7
|
+
require 'arturo/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
|
11
8
|
|
12
|
-
class <<self
|
13
|
-
|
14
|
-
# Quick check for whether a feature is enabled for a recipient.
|
15
|
-
# @param [String, Symbol] feature_name
|
16
|
-
# @param [#id] recipient
|
17
|
-
# @return [true,false] whether the feature exists and is enabled for the recipient
|
18
|
-
def feature_enabled_for?(feature_name, recipient)
|
19
|
-
f = self::Feature.to_feature(feature_name)
|
20
|
-
f && f.enabled_for?(recipient)
|
21
|
-
end
|
22
|
-
|
23
|
-
ENABLED_FOR_METHOD_NAME = /^(\w+)_enabled_for\?$/
|
24
|
-
|
25
|
-
def respond_to?(symbol)
|
26
|
-
symbol.to_s =~ ENABLED_FOR_METHOD_NAME || super(symbol)
|
27
|
-
end
|
28
|
-
|
29
|
-
def method_missing(symbol, *args, &block)
|
30
|
-
if (args.length == 1 && match = ENABLED_FOR_METHOD_NAME.match(symbol.to_s))
|
31
|
-
feature_enabled_for?(match[1], args[0])
|
32
|
-
else
|
33
|
-
super(symbol, *args, &block)
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
end
|
38
9
|
end
|