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.
Files changed (35) hide show
  1. data/HISTORY.md +16 -0
  2. data/README.md +295 -0
  3. data/app/controllers/arturo/features_controller.rb +105 -0
  4. data/app/helpers/arturo/features_helper.rb +37 -0
  5. data/app/models/arturo/feature.rb +75 -0
  6. data/app/views/arturo/features/_feature.html.erb +5 -0
  7. data/app/views/arturo/features/_form.html.erb +16 -0
  8. data/app/views/arturo/features/edit.html.erb +2 -0
  9. data/app/views/arturo/features/forbidden.html.erb +2 -0
  10. data/app/views/arturo/features/index.html.erb +29 -0
  11. data/app/views/arturo/features/new.html.erb +2 -0
  12. data/app/views/arturo/features/show.html.erb +2 -0
  13. data/config/locales/en.yml +40 -0
  14. data/config/routes.rb +14 -0
  15. data/lib/arturo.rb +37 -0
  16. data/lib/arturo/controller_filters.rb +33 -0
  17. data/lib/arturo/engine.rb +10 -0
  18. data/lib/arturo/feature_availability.rb +37 -0
  19. data/lib/arturo/feature_caching.rb +83 -0
  20. data/lib/arturo/feature_factories.rb +4 -0
  21. data/lib/arturo/feature_management.rb +23 -0
  22. data/lib/arturo/middleware.rb +60 -0
  23. data/lib/arturo/special_handling.rb +62 -0
  24. data/lib/arturo/test_support.rb +30 -0
  25. data/lib/generators/arturo/assets_generator.rb +18 -0
  26. data/lib/generators/arturo/initializer_generator.rb +13 -0
  27. data/lib/generators/arturo/migration_generator.rb +27 -0
  28. data/lib/generators/arturo/routes_generator.rb +15 -0
  29. data/lib/generators/arturo/templates/arturo.css +67 -0
  30. data/lib/generators/arturo/templates/arturo.js +23 -0
  31. data/lib/generators/arturo/templates/arturo_customizations.css +1 -0
  32. data/lib/generators/arturo/templates/initializer.rb +29 -0
  33. data/lib/generators/arturo/templates/migration.rb +17 -0
  34. data/lib/generators/arturo/templates/semicolon.png +0 -0
  35. 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,2 @@
1
+ <h2><%= t('.title', :name => @feature.name) %></h2>
2
+ <%= render :partial => 'form', :locals => { :feature => @feature, :legend => t('.legend', :name => @feature.name) } %>
@@ -0,0 +1,2 @@
1
+ <h2><%= t('.title') %></h2>
2
+ <p><%= t('.text') %></p>
@@ -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>&nbsp;</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,2 @@
1
+ <h2><%= t('.title') %></h2>
2
+ <%= render :partial => 'form', :locals => { :feature => @feature, :legend => t('.legend') } %>
@@ -0,0 +1,2 @@
1
+ <h2><%= t('.title', :name => @feature.name) %></h2>
2
+ <p>Deployment percentage: <%= @feature.deployment_percentage %></p>
@@ -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,10 @@
1
+ module Arturo
2
+ class Engine < ::Rails::Engine
3
+ ActiveSupport.on_load(:action_controller) do
4
+ include Arturo::FeatureAvailability
5
+ helper Arturo::FeatureAvailability
6
+ include Arturo::ControllerFilters
7
+ helper Arturo::FeatureManagement
8
+ end
9
+ end
10
+ 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,4 @@
1
+ Factory.define :feature, :class => Arturo::Feature do |f|
2
+ f.sequence(:symbol) { |n| "feature_#{n}".to_sym }
3
+ f.deployment_percentage { |_| rand(101) }
4
+ 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