enphase-arturo 1.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/HISTORY.md +16 -0
- data/README.md +295 -0
- data/app/controllers/arturo/features_controller.rb +105 -0
- data/app/helpers/arturo/features_helper.rb +37 -0
- data/app/models/arturo/feature.rb +75 -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.rb +37 -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/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 +156 -0
|
@@ -0,0 +1,75 @@
|
|
|
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_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.where(:symbol => feature_or_symbol.to_sym).first
|
|
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
|
+
persisted? ? "#{id}-#{symbol.to_s.parameterize}" : nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inspect
|
|
63
|
+
"<Arturo::Feature #{name}, deployed to #{deployment_percentage}%>"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
protected
|
|
67
|
+
|
|
68
|
+
def passes_threshold?(feature_recipient)
|
|
69
|
+
threshold = self.deployment_percentage || 0
|
|
70
|
+
return false if threshold == 0
|
|
71
|
+
return true if threshold == 100
|
|
72
|
+
(((feature_recipient.id + (self.id || 1) + 17) * 13) % 100) < threshold
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
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
|
data/lib/arturo.rb
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module Arturo
|
|
2
|
+
|
|
3
|
+
require 'arturo/special_handling'
|
|
4
|
+
require 'arturo/feature_availability'
|
|
5
|
+
require 'arturo/feature_management'
|
|
6
|
+
require 'arturo/feature_caching'
|
|
7
|
+
require 'arturo/controller_filters'
|
|
8
|
+
require 'arturo/middleware'
|
|
9
|
+
require 'arturo/engine' if defined?(Rails) && Rails::VERSION::MAJOR == 3
|
|
10
|
+
|
|
11
|
+
class <<self
|
|
12
|
+
|
|
13
|
+
# Quick check for whether a feature is enabled for a recipient.
|
|
14
|
+
# @param [String, Symbol] feature_name
|
|
15
|
+
# @param [#id] recipient
|
|
16
|
+
# @return [true,false] whether the feature exists and is enabled for the recipient
|
|
17
|
+
def feature_enabled_for?(feature_name, recipient)
|
|
18
|
+
f = self::Feature.to_feature(feature_name)
|
|
19
|
+
f && f.enabled_for?(recipient)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
ENABLED_FOR_METHOD_NAME = /^(\w+)_enabled_for\?$/
|
|
23
|
+
|
|
24
|
+
def respond_to?(symbol)
|
|
25
|
+
symbol.to_s =~ ENABLED_FOR_METHOD_NAME || super(symbol)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def method_missing(symbol, *args, &block)
|
|
29
|
+
if (args.length == 1 && match = ENABLED_FOR_METHOD_NAME.match(symbol.to_s))
|
|
30
|
+
feature_enabled_for?(match[1], args[0])
|
|
31
|
+
else
|
|
32
|
+
super(symbol, *args, &block)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
37
|
+
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,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,83 @@
|
|
|
1
|
+
module Arturo
|
|
2
|
+
|
|
3
|
+
# To be extended by Arturo::Feature if you want to enable
|
|
4
|
+
# in-memory caching.
|
|
5
|
+
# NB: Arturo's feature caching only works when using
|
|
6
|
+
# Arturo::Feature.to_feature or when using the helper methods
|
|
7
|
+
# in Arturo and Arturo::FeatureAvailability.
|
|
8
|
+
# NB: if you have multiple application servers, you almost certainly
|
|
9
|
+
# want to clear this cache after each request:
|
|
10
|
+
#
|
|
11
|
+
# class ApplicationController < ActionController::Base
|
|
12
|
+
# after_filter { Arturo::Feature.clear_feature_cache }
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# Alternatively, you could redefine Arturo::Feature.feature_cache
|
|
16
|
+
# to use a shared cache like Memcached.
|
|
17
|
+
module FeatureCaching
|
|
18
|
+
|
|
19
|
+
def self.extended(base)
|
|
20
|
+
class <<base
|
|
21
|
+
alias_method_chain :to_feature, :caching
|
|
22
|
+
attr_accessor :cache_ttl, :feature_cache, :nil_marker
|
|
23
|
+
end
|
|
24
|
+
base.cache_ttl = 0
|
|
25
|
+
base.feature_cache = Arturo::FeatureCaching::Cache.new
|
|
26
|
+
base.nil_marker = Arturo::Feature.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def caches_features?
|
|
30
|
+
self.cache_ttl.to_i > 0
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Wraps Arturo::Feature.to_feature with in-memory caching.
|
|
34
|
+
def to_feature_with_caching(feature_or_symbol)
|
|
35
|
+
if !self.caches_features?
|
|
36
|
+
return to_feature_without_caching(feature_or_symbol)
|
|
37
|
+
elsif (feature_or_symbol.kind_of?(Arturo::Feature))
|
|
38
|
+
feature_cache.write(feature_or_symbol.symbol, feature_or_symbol, :expires_in => cache_ttl)
|
|
39
|
+
feature_or_symbol
|
|
40
|
+
elsif (cached_feature = feature_cache.read(feature_or_symbol.to_sym))
|
|
41
|
+
cached_feature == nil_marker ? nil : cached_feature
|
|
42
|
+
else
|
|
43
|
+
f = to_feature_without_caching(feature_or_symbol)
|
|
44
|
+
feature_cache.write(feature_or_symbol.to_sym, f.nil? ? nil_marker : f, :expires_in => cache_ttl)
|
|
45
|
+
f
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
protected
|
|
50
|
+
|
|
51
|
+
# Quack like a Rails cache.
|
|
52
|
+
class Cache
|
|
53
|
+
def initialize
|
|
54
|
+
@data = {} # of the form {key => [value, expires_at or nil]}
|
|
55
|
+
end
|
|
56
|
+
def read(name, options = nil)
|
|
57
|
+
name = name.to_s
|
|
58
|
+
value, expires_at = *@data[name]
|
|
59
|
+
if value && (expires_at.blank? || expires_at > Time.now)
|
|
60
|
+
value
|
|
61
|
+
else
|
|
62
|
+
nil
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
def write(name, value, options = nil)
|
|
66
|
+
name = name.to_s
|
|
67
|
+
expires_at = if options && options.respond_to?(:[]) && options[:expires_in]
|
|
68
|
+
Time.now + options.delete(:expires_in)
|
|
69
|
+
else
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
value.freeze.tap do |val|
|
|
73
|
+
@data[name] = [value, expires_at]
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
def clear
|
|
77
|
+
@data.clear
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
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
|