ee_arturo 1.3.4
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.
- checksums.yaml +15 -0
- data/HISTORY.md +16 -0
- data/README.md +295 -0
- data/app/controllers/arturo/features_controller.rb +105 -0
- data/app/helpers/arturo/features_helper.rb +27 -0
- data/app/models/arturo/feature.rb +76 -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/config/routes.rb +14 -0
- data/lib/arturo/controller_filters.rb +33 -0
- data/lib/arturo/engine.rb +10 -0
- data/lib/arturo/feature_availability.rb +37 -0
- data/lib/arturo/feature_caching.rb +83 -0
- data/lib/arturo/feature_factories.rb +4 -0
- data/lib/arturo/feature_management.rb +23 -0
- data/lib/arturo/middleware.rb +60 -0
- data/lib/arturo/special_handling.rb +62 -0
- data/lib/arturo/test_support.rb +30 -0
- data/lib/arturo.rb +37 -0
- 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 +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 +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NjI3NWU5NDQ3N2EwZGY5Y2RjZjA2M2ZkNzIzMDUzZjllN2FjMDM5MA==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MjkzYzk0NDQxMDJjYjI4YjJmYjFjYjAwM2JjYzk5ZmUzNDQwZDUyMg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
ZjNjM2QzMDg5MjRkNTcxYzRhMjZiNDUxZDI4NTZjODMzNjczNzU0N2YwN2Y0
|
10
|
+
ZDRmYjRjOTk4YTk5NjI1Yjk5NjU0N2ZlYjFiOWY1NDZjMGRiOTM1MDUwMjE2
|
11
|
+
Y2JlNTA5YmFiOWExYjM0MjFkM2MyODIxYTY5NGI5MTExYmRlMDg=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YmEyYjFiZGZiYWM4NDk1MTcxOWIyNTgyZTZjMGFjMDQ1ZTJjNDczYjVlMjM3
|
14
|
+
ZmFmOGZhZmRjZWZlY2I4ZmNjZjMxYWVmOWFlZTUyYmMxYWRkNjc1YWEzYjU4
|
15
|
+
ZTJlM2Y1Y2EzNjE3MmNlYzc1YmE1M2UyOWE2Y2EyYmQ2YmEyMmQ=
|
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,295 @@
|
|
1
|
+
## What
|
2
|
+
|
3
|
+
Arturo provides feature sliders for Rails. It lets you turn features on and off
|
4
|
+
just like
|
5
|
+
[feature flippers](http://code.flickr.com/blog/2009/12/02/flipping-out/),
|
6
|
+
but offers more fine-grained control. It supports deploying features only for
|
7
|
+
a given percent* of your users and whitelisting and blacklisting users based
|
8
|
+
on any criteria you can express in Ruby.
|
9
|
+
|
10
|
+
* The selection isn't random. It's not even pseudo-random. It's completely
|
11
|
+
deterministic. This assures that if a user has a feature on Monday, the
|
12
|
+
user will still have it on Tuesday (unless, of course, you *decrease*
|
13
|
+
the feature's deployment percentage or change its white- or blacklist
|
14
|
+
settings).
|
15
|
+
|
16
|
+
### A quick example
|
17
|
+
|
18
|
+
Trish, a developer is working on a new feature: a live feed of recent postings
|
19
|
+
in the user's city that shows up in the user's sidebar. First, she uses Arturo's
|
20
|
+
view helpers to control who sees the sidebar widget:
|
21
|
+
|
22
|
+
<%# in app/views/layout/_sidebar.html.erb: %>
|
23
|
+
<% if_feature_enabled(:live_postings) do %>
|
24
|
+
<div class='widget'>
|
25
|
+
<h3>Recent Postings</h3>
|
26
|
+
<ol id='live_postings'>
|
27
|
+
</ol>
|
28
|
+
</div>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
Then Trish writes some Javascript that will poll the server for recent
|
32
|
+
postings and put them in the sidebar widget:
|
33
|
+
|
34
|
+
// in public/javascript/live_postings.js:
|
35
|
+
$(function() {
|
36
|
+
var livePostingsList = $('#live_postings');
|
37
|
+
if (livePostingsList.length > 0) {
|
38
|
+
var updatePostingsList = function() {
|
39
|
+
livePostingsList.load('/listings/recent');
|
40
|
+
setTimeout(updatePostingsList, 30);
|
41
|
+
}
|
42
|
+
updatePostingsList();
|
43
|
+
}
|
44
|
+
});
|
45
|
+
|
46
|
+
Trish uses Arturo's Controller filters to control who has access to
|
47
|
+
the feature:
|
48
|
+
|
49
|
+
# in app/controllers/postings_controller:
|
50
|
+
class PostingsController < ApplicationController
|
51
|
+
require_feature :live_postings, :only => :recent
|
52
|
+
# ...
|
53
|
+
end
|
54
|
+
|
55
|
+
Trish then deploys this code to production. Nobody will see the feature yet,
|
56
|
+
since it's not on for anyone. (In fact, the feature doesn't yet exist
|
57
|
+
in the database, which is the same as being deployed to 0% of users.) A week
|
58
|
+
later, when the company is ready to start deploying the feature to a few
|
59
|
+
people, the product manager, Vijay, signs in to their site and navigates
|
60
|
+
to `/features`, adds a new feature called "live_postings" and sets its
|
61
|
+
deployment percentage to 3%. After a few days, the operations team decides
|
62
|
+
that the increase in traffic is not going to overwhelm their servers, and
|
63
|
+
Vijay can bump the deployment percentage up to 50%. A few more days go by
|
64
|
+
and they clean up the last few bugs they found with the "live_postings"
|
65
|
+
feature and deploy it to all users.
|
66
|
+
|
67
|
+
## Installation
|
68
|
+
|
69
|
+
### In Rails 3, with Bundler
|
70
|
+
|
71
|
+
gem 'arturo', '~> 1.0'
|
72
|
+
|
73
|
+
### In Rails 3, without Bundler
|
74
|
+
|
75
|
+
$ gem install arturo --version="~> 1.0"
|
76
|
+
|
77
|
+
### In Rails 2.3
|
78
|
+
|
79
|
+
For Rails 2.3 support, see the
|
80
|
+
[`rails_2_3` branch](http://github.com/jamesarosen/arturo/tree/rails_2_3)
|
81
|
+
of Arturo.
|
82
|
+
|
83
|
+
## Configuration
|
84
|
+
|
85
|
+
### In Rails
|
86
|
+
|
87
|
+
#### Run the generators:
|
88
|
+
|
89
|
+
$ rails g arturo:migration
|
90
|
+
$ rails g arturo:initializer
|
91
|
+
$ rails g arturo:route
|
92
|
+
$ rails g arturo:assets
|
93
|
+
|
94
|
+
#### Run the migration:
|
95
|
+
|
96
|
+
$ rake db:migrate
|
97
|
+
|
98
|
+
#### Edit the generated migration as necessary
|
99
|
+
|
100
|
+
#### Edit the configuration
|
101
|
+
|
102
|
+
##### Initializer
|
103
|
+
|
104
|
+
Open up the newly-generated `config/initializers/arturo_initializer.rb`.
|
105
|
+
There are configuration options for the following:
|
106
|
+
|
107
|
+
* the method that determines whether a user has permission to manage features
|
108
|
+
(see [admin permissions](#adminpermissions))
|
109
|
+
* the method that returns the object that has features
|
110
|
+
(e.g. User, Person, or Account; see
|
111
|
+
[feature recipients](#featurerecipients))
|
112
|
+
* whitelists and blacklists for features
|
113
|
+
(see [white- and blacklisting](#wblisting))
|
114
|
+
|
115
|
+
##### CSS
|
116
|
+
|
117
|
+
Open up the newly-generated `public/stylehseets/arturo_customizations.css`.
|
118
|
+
You can add any overrides you like to the feature configuration page styles
|
119
|
+
here. **Do not** edit `public/stylehseets/arturo.css` as that file may be
|
120
|
+
overwritten in future updates to Arturo.
|
121
|
+
|
122
|
+
### In other frameworks
|
123
|
+
|
124
|
+
Arturo is a Rails engine. I want to promote reuse on other frameworks by
|
125
|
+
extracting key pieces into mixins, though this isn't done yet. Open an
|
126
|
+
[issue](http://github.com/jamesarosen/arturo/issues) and I'll be happy to
|
127
|
+
work with you on support for your favorite framework.
|
128
|
+
|
129
|
+
## Deep-Dive
|
130
|
+
|
131
|
+
### <span id='adminpermissions'>Admin Permissions</span>
|
132
|
+
|
133
|
+
`Arturo::FeatureManagement#may_manage_features?` is a method that is run in
|
134
|
+
the context of a Controller or View instance. It should return `true` if
|
135
|
+
and only if the current user may manage permissions. The default implementation
|
136
|
+
is as follows:
|
137
|
+
|
138
|
+
current_user.present? && current_user.admin?
|
139
|
+
|
140
|
+
You can change the implementation in
|
141
|
+
`config/initializers/arturo_initializer.rb`. A reasonable implementation
|
142
|
+
might be
|
143
|
+
|
144
|
+
Arturo.permit_management do
|
145
|
+
signed_in? && current_user.can?(:manage_features)
|
146
|
+
end
|
147
|
+
|
148
|
+
### <span id='featurerecipients'>Feature Recipients</span>
|
149
|
+
|
150
|
+
Clients of Arturo may want to deploy new features on a per-user, per-project,
|
151
|
+
per-account, or other basis. For example, it is likely Twitter deployed
|
152
|
+
"#newtwitter" on a per-user basis. Conversely, Facebook -- at least in its
|
153
|
+
early days -- may have deployed features on a per-university basis. It wouldn't
|
154
|
+
make much sense to deploy a feature to one user of a Basecamp project but not
|
155
|
+
to others, so 37Signals would probably want a per-project or per-account basis.
|
156
|
+
|
157
|
+
`Arturo::FeatureAvailability#feature_recipient` is intended to support these
|
158
|
+
many use cases. It is a method that returns the current "thing" (a user, account,
|
159
|
+
project, university, ...) that is a member of the category that is the basis for
|
160
|
+
deploying new features. It should return an `Object` that responds to `#id`.
|
161
|
+
|
162
|
+
The default implementation simply returns `current_user`. Like
|
163
|
+
`Arturo::FeatureManagement#may_manage_features?`, this method can be configured
|
164
|
+
in `config/initializers/arturo_initializer.rb`. If you want to deploy features
|
165
|
+
on a per-account basis, a reasonable implementation might be
|
166
|
+
|
167
|
+
Arturo.feature_recipient do
|
168
|
+
current_account
|
169
|
+
end
|
170
|
+
|
171
|
+
or
|
172
|
+
|
173
|
+
Arturo.feature_recipient do
|
174
|
+
current_user.account
|
175
|
+
end
|
176
|
+
|
177
|
+
If the block returns `nil`, the feature will be disabled.
|
178
|
+
|
179
|
+
### <span id='wblisting'>Whitelists & Blacklists</span>
|
180
|
+
|
181
|
+
Whitelists and blacklists allow you to control exactly which users or accounts
|
182
|
+
will have a feature. For example, if all premium users should have the
|
183
|
+
`:awesome` feature, place the following in
|
184
|
+
`config/initializers/arturo_initializer.rb`:
|
185
|
+
|
186
|
+
Arturo::Feature.whitelist(:awesome) do |user|
|
187
|
+
user.account.premium?
|
188
|
+
end
|
189
|
+
|
190
|
+
If, on the other hand, no users on the free plan should have the
|
191
|
+
`:awesome` feature, place the following in
|
192
|
+
`config/initializers/arturo_initializer.rb`:
|
193
|
+
|
194
|
+
Arturo::Feature.blacklist(:awesome) do |user|
|
195
|
+
user.account.free?
|
196
|
+
end
|
197
|
+
|
198
|
+
### Feature Conditionals
|
199
|
+
|
200
|
+
All that configuration is just a waste of time if Arturo didn't modify the
|
201
|
+
behavior of your application based on feature availability. There are a few
|
202
|
+
ways to do so.
|
203
|
+
|
204
|
+
#### Controller Filters
|
205
|
+
|
206
|
+
If an action should only be available to those with a feature enabled,
|
207
|
+
use a before filter. The following will raise a 403 Forbidden error for
|
208
|
+
every action within `BookHoldsController` that is invoked by a user who
|
209
|
+
does not have the `:hold_book` feature.
|
210
|
+
|
211
|
+
class BookHoldsController < ApplicationController
|
212
|
+
require_feature :hold_book
|
213
|
+
end
|
214
|
+
|
215
|
+
`require_feature` accepts as a second argument a `Hash` that it passes on
|
216
|
+
to `before_filter`, so you can use `:only` and `:except` to specify exactly
|
217
|
+
which actions are filtered.
|
218
|
+
|
219
|
+
If you want to customize the page that is rendered on 403 Forbidden
|
220
|
+
responses, put the view in
|
221
|
+
`RAILS_ROOT/app/views/arturo/features/forbidden.html.erb`. Rails will
|
222
|
+
check there before falling back on Arturo's forbidden page.
|
223
|
+
|
224
|
+
#### Conditional Evaluation
|
225
|
+
|
226
|
+
Both controllers and views have access to the `if_feature_enabled` and
|
227
|
+
`feature_enabled?` methods. The former is used like so:
|
228
|
+
|
229
|
+
<% if_feature_enabled?(:reserve_table) %>
|
230
|
+
<%= link_to 'Reserve a table', new_restaurant_reservation_path(:restaurant_id => @restaurant) %>
|
231
|
+
<% end %>
|
232
|
+
|
233
|
+
The latter can be used like so:
|
234
|
+
|
235
|
+
def widgets_for_sidebar
|
236
|
+
widgets = []
|
237
|
+
widgets << twitter_widget if feature_enabled?(:twitter_integration)
|
238
|
+
...
|
239
|
+
widgets
|
240
|
+
end
|
241
|
+
|
242
|
+
#### Rack Middleware
|
243
|
+
|
244
|
+
require 'arturo'
|
245
|
+
use Arturo::Middleware, :feature => :my_feature
|
246
|
+
|
247
|
+
#### Outside a Controller
|
248
|
+
|
249
|
+
If you want to check availability outside of a controller or view (really
|
250
|
+
outside of something that has `Arturo::FeatureAvailability` mixed in), you
|
251
|
+
can ask either
|
252
|
+
|
253
|
+
Arturo.feature_enabled_for?(:foo, recipient)
|
254
|
+
|
255
|
+
or the slightly fancier
|
256
|
+
|
257
|
+
Arturo.foo_enabled_for?(recipient)
|
258
|
+
|
259
|
+
Both check whether the `foo` feature exists and is enabled for `recipient`.
|
260
|
+
|
261
|
+
#### Caching
|
262
|
+
|
263
|
+
**Note**: Arturo does not yet have caching support. Be very careful when
|
264
|
+
caching actions or pages that involve feature detection as you will get
|
265
|
+
strange behavior when a user who has access to a feature requests a page
|
266
|
+
just after one who does not (and vice versa). The following is the
|
267
|
+
**intended** support for caching.
|
268
|
+
|
269
|
+
Both the `require_feature` before filter and the `if_feature_enabled` block
|
270
|
+
evaluation automatically append a string based on the feature's
|
271
|
+
`last_modified` timestamp to cache keys that Rails generates. Thus, you don't
|
272
|
+
have to worry about expiring caches when you increase a feature's deployment
|
273
|
+
percentage. See `Arturo::CacheSupport` for more information.
|
274
|
+
|
275
|
+
## The Name
|
276
|
+
|
277
|
+
Arturo gets its name from
|
278
|
+
[Professor Maximillian Arturo](http://en.wikipedia.org/wiki/Maximillian_Arturo)
|
279
|
+
on [Sliders](http://en.wikipedia.org/wiki/Sliders).
|
280
|
+
|
281
|
+
## Contributing ##
|
282
|
+
|
283
|
+
For bug reports, open an [issue](https://github.com/jamesarosen/Timecop.js/issues)
|
284
|
+
on GitHub.
|
285
|
+
|
286
|
+
Timecop.js has a ‘commit-bit’ policy, much like the Rubinius project
|
287
|
+
and Gemcutter. Submit a patch that is accepted, and you can get full
|
288
|
+
commit access to the project. All you have to do is open an issue
|
289
|
+
asking for access and I'll add you as a collaborator.
|
290
|
+
Feel free to fork the project though and have fun in your own sandbox.
|
291
|
+
|
292
|
+
## Authors ##
|
293
|
+
|
294
|
+
* [https://github.com/jamesarosen](James A. Rosen)
|
295
|
+
* [https://github.com/plukevdh](Luke van der Hoeven)
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'action_controller'
|
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?
|
6
|
+
module Arturo
|
7
|
+
|
8
|
+
# Handles all Feature actions. Clients of the Arturo engine
|
9
|
+
# should redefine Arturo::FeaturesController#permitted? to
|
10
|
+
# return true only for users who are permitted to manage features.
|
11
|
+
class FeaturesController < ApplicationController
|
12
|
+
include Arturo::FeatureManagement
|
13
|
+
|
14
|
+
unloadable
|
15
|
+
respond_to :html, :json, :xml
|
16
|
+
before_filter :require_permission
|
17
|
+
before_filter :load_feature, :only => [ :show, :edit, :update, :destroy ]
|
18
|
+
|
19
|
+
def index
|
20
|
+
@features = Arturo::Feature.all
|
21
|
+
respond_with @features
|
22
|
+
end
|
23
|
+
|
24
|
+
def update_all
|
25
|
+
updated_count = 0
|
26
|
+
errors = []
|
27
|
+
features_params = params[:features] || {}
|
28
|
+
features_params.each do |id, attributes|
|
29
|
+
feature = Arturo::Feature.find_by_id(id)
|
30
|
+
if feature.blank?
|
31
|
+
errors << t('arturo.features.flash.no_such_feature', :id => id)
|
32
|
+
elsif feature.update_attributes(attributes)
|
33
|
+
updated_count += 1
|
34
|
+
else
|
35
|
+
errors << t('arturo.features.flash.error_updating', :id => id)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
if errors.any?
|
39
|
+
flash[:error] = errors
|
40
|
+
else
|
41
|
+
flash[:success] = t('arturo.features.flash.updated_many', :count => updated_count)
|
42
|
+
end
|
43
|
+
redirect_to features_path
|
44
|
+
end
|
45
|
+
|
46
|
+
def show
|
47
|
+
respond_with @feature
|
48
|
+
end
|
49
|
+
|
50
|
+
def new
|
51
|
+
@feature = Arturo::Feature.new(params[:feature])
|
52
|
+
respond_with @feature
|
53
|
+
end
|
54
|
+
|
55
|
+
def create
|
56
|
+
@feature = Arturo::Feature.new(params[:feature])
|
57
|
+
if @feature.save
|
58
|
+
flash[:notice] = t('arturo.features.flash.created', :name => @feature.to_s)
|
59
|
+
redirect_to features_path
|
60
|
+
else
|
61
|
+
flash[:alert] = t('arturo.features.flash.error_creating', :name => @feature.to_s)
|
62
|
+
render :action => 'new'
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def edit
|
67
|
+
respond_with @feature
|
68
|
+
end
|
69
|
+
|
70
|
+
def update
|
71
|
+
if @feature.update_attributes(params[:feature])
|
72
|
+
flash[:notice] = t('arturo.features.flash.updated', :name => @feature.to_s)
|
73
|
+
redirect_to feature_path(@feature)
|
74
|
+
else
|
75
|
+
flash[:alert] = t('arturo.features.flash.error_updating', :name => @feature.to_s)
|
76
|
+
render :action => 'edit'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def destroy
|
81
|
+
if @feature.destroy
|
82
|
+
flash[:notice] = t('arturo.features.flash.removed', :name => @feature.to_s)
|
83
|
+
else
|
84
|
+
flash[:alert] = t('arturo.features.flash.error_removing', :name => @feature.to_s)
|
85
|
+
end
|
86
|
+
redirect_to features_path
|
87
|
+
end
|
88
|
+
|
89
|
+
protected
|
90
|
+
|
91
|
+
def require_permission
|
92
|
+
unless may_manage_features?
|
93
|
+
render :action => 'forbidden', :status => 403
|
94
|
+
return false
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def load_feature
|
99
|
+
@feature ||= Arturo::Feature.find(params[:id])
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
|
104
|
+
end
|
105
|
+
|
@@ -0,0 +1,27 @@
|
|
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
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,76 @@
|
|
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 }.with_indifferent_access
|
12
|
+
|
13
|
+
attr_accessible :symbol, :deployment_percentage if ActiveRecord::VERSION::MAJOR < 4
|
14
|
+
attr_readonly :symbol
|
15
|
+
|
16
|
+
validates_presence_of :symbol, :deployment_percentage
|
17
|
+
validates_uniqueness_of :symbol, :allow_blank => true
|
18
|
+
validates_numericality_of :deployment_percentage,
|
19
|
+
:only_integer => true,
|
20
|
+
:allow_blank => true,
|
21
|
+
:greater_than_or_equal_to => 0,
|
22
|
+
:less_than_or_equal_to => 100
|
23
|
+
|
24
|
+
# Looks up a feature by symbol. Also accepts a Feature as input.
|
25
|
+
# @param [Symbol, Arturo::Feature] feature_or_name a Feature or the Symbol of a Feature
|
26
|
+
# @return [Arturo::Feature, nil] the Feature if found, else nil
|
27
|
+
def self.to_feature(feature_or_symbol)
|
28
|
+
return feature_or_symbol if feature_or_symbol.kind_of?(self)
|
29
|
+
self.where(:symbol => feature_or_symbol.to_sym).first
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create a new Feature
|
33
|
+
def initialize(attributes = {})
|
34
|
+
super(DEFAULT_ATTRIBUTES.merge(attributes || {}))
|
35
|
+
end
|
36
|
+
|
37
|
+
# @param [Object] feature_recipient a User, Account,
|
38
|
+
# or other model with an #id method
|
39
|
+
# @return [true,false] whether or not this feature is enabled
|
40
|
+
# for feature_recipient
|
41
|
+
# @see Arturo::SpecialHandling#whitelisted?
|
42
|
+
# @see Arturo::SpecialHandling#blacklisted?
|
43
|
+
def enabled_for?(feature_recipient)
|
44
|
+
return false if feature_recipient.nil?
|
45
|
+
return false if blacklisted?(feature_recipient)
|
46
|
+
return true if whitelisted?(feature_recipient)
|
47
|
+
passes_threshold?(feature_recipient)
|
48
|
+
end
|
49
|
+
|
50
|
+
def name
|
51
|
+
return I18n.translate("arturo.feature.nameless") if symbol.blank?
|
52
|
+
I18n.translate("arturo.feature.#{symbol}", :default => symbol.to_s.titleize)
|
53
|
+
end
|
54
|
+
|
55
|
+
def to_s
|
56
|
+
"Feature #{name}"
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_param
|
60
|
+
persisted? ? "#{id}-#{symbol.to_s.parameterize}" : nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def inspect
|
64
|
+
"<Arturo::Feature #{name}, deployed to #{deployment_percentage}%>"
|
65
|
+
end
|
66
|
+
|
67
|
+
protected
|
68
|
+
|
69
|
+
def passes_threshold?(feature_recipient)
|
70
|
+
threshold = self.deployment_percentage || 0
|
71
|
+
return false if threshold == 0
|
72
|
+
return true if threshold == 100
|
73
|
+
(((feature_recipient.id + (self.id || 1) + 17) * 13) % 100) < threshold
|
74
|
+
end
|
75
|
+
end
|
76
|
+
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/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
|