arturo 0.2.3.8 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.md +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
|