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.
Files changed (36) hide show
  1. checksums.yaml +15 -0
  2. data/HISTORY.md +16 -0
  3. data/README.md +295 -0
  4. data/app/controllers/arturo/features_controller.rb +105 -0
  5. data/app/helpers/arturo/features_helper.rb +27 -0
  6. data/app/models/arturo/feature.rb +76 -0
  7. data/app/views/arturo/features/_feature.html.erb +5 -0
  8. data/app/views/arturo/features/_form.html.erb +16 -0
  9. data/app/views/arturo/features/edit.html.erb +2 -0
  10. data/app/views/arturo/features/forbidden.html.erb +2 -0
  11. data/app/views/arturo/features/index.html.erb +29 -0
  12. data/app/views/arturo/features/new.html.erb +2 -0
  13. data/app/views/arturo/features/show.html.erb +2 -0
  14. data/config/locales/en.yml +40 -0
  15. data/config/routes.rb +14 -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/arturo.rb +37 -0
  26. data/lib/generators/arturo/assets_generator.rb +18 -0
  27. data/lib/generators/arturo/initializer_generator.rb +13 -0
  28. data/lib/generators/arturo/migration_generator.rb +27 -0
  29. data/lib/generators/arturo/routes_generator.rb +15 -0
  30. data/lib/generators/arturo/templates/arturo.css +67 -0
  31. data/lib/generators/arturo/templates/arturo.js +23 -0
  32. data/lib/generators/arturo/templates/arturo_customizations.css +1 -0
  33. data/lib/generators/arturo/templates/initializer.rb +29 -0
  34. data/lib/generators/arturo/templates/migration.rb +17 -0
  35. data/lib/generators/arturo/templates/semicolon.png +0 -0
  36. metadata +175 -0
@@ -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
@@ -0,0 +1,60 @@
1
+ module Arturo
2
+ # A Rack middleware that requires a feature to be present. By default,
3
+ # checks feature availability against an `arturo.recipient` object
4
+ # in the `env`. If that object is missing, this middleware always fails,
5
+ # even if the feature is available for everyone.
6
+ #
7
+ # ## Usage
8
+ #
9
+ # use Arturo::Middleware, :feature => :foo
10
+ #
11
+ # ## Options
12
+ #
13
+ # * feature -- the name of the feature to require, as a Symbol; required
14
+ #
15
+ # * recipient -- the key in the `env` hash under which the feature
16
+ # recipient can be found; defaults to "arturo.recipient".
17
+ # * on_unavailable -- a Rack-like object
18
+ # (has `#call(Hash) -> [status, headers, body]`) that
19
+ # is called when the feature is unavailable; defaults
20
+ # to returning `[ 404, {}, ['Not Found'] ]`.
21
+ class Middleware
22
+
23
+ MISSING_FEATURE_ERROR = "Cannot create an Arturo::Middleware without a :feature"
24
+
25
+ DEFAULT_RECIPIENT_KEY = 'arturo.recipient'
26
+
27
+ DEFAULT_ON_UNAVAILABLE = lambda { |env| [ 404, {}, ['Not Found'] ] }
28
+
29
+ def initialize(app, options = {})
30
+ @app = app
31
+ @feature = options[:feature]
32
+ raise ArgumentError.new(MISSING_FEATURE_ERROR) unless @feature
33
+ @recipient_key = options[:recipient] || DEFAULT_RECIPIENT_KEY
34
+ @on_unavailable = options[:on_unavailable] || DEFAULT_ON_UNAVAILABLE
35
+ end
36
+
37
+ def call(env)
38
+ if enabled_for_recipient?(env)
39
+ @app.call(env)
40
+ else
41
+ fail(env)
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def enabled_for_recipient?(env)
48
+ ::Arturo.feature_enabled_for?(@feature, recipient(env))
49
+ end
50
+
51
+ def recipient(env)
52
+ env[@recipient_key]
53
+ end
54
+
55
+ def fail(env)
56
+ @on_unavailable.call(env)
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,62 @@
1
+ module Arturo
2
+
3
+ # Adds whitelist and blacklist support to individual features by name.
4
+ # Blacklists override whitelists. (In the world of Apache, Features
5
+ # are "(deny,allow)".)
6
+ # @example
7
+ # # allow admins:
8
+ # Arturo::Feature.whitelist(:some_feature) do |user|
9
+ # user.is_admin?
10
+ # end
11
+ #
12
+ # # disallow for small accounts:
13
+ # Arturo::Feature.blacklist(:another_feature) do |user|
14
+ # user.account.small?
15
+ # end
16
+ #
17
+ # Blacklists and whitelists can be defined before the feature exists
18
+ # and are not persisted, so they are best defined in initializers.
19
+ # This is particularly important if your application runs in several
20
+ # different processes or on several servers.
21
+ module SpecialHandling
22
+
23
+ def self.included(base)
24
+ base.extend Arturo::SpecialHandling::ClassMethods
25
+ end
26
+
27
+ module ClassMethods
28
+ def whitelists
29
+ @whitelists ||= {}
30
+ end
31
+
32
+ def blacklists
33
+ @blacklists ||= {}
34
+ end
35
+
36
+ def whitelist(feature_symbol, &block)
37
+ whitelists[feature_symbol.to_sym] = block
38
+ end
39
+
40
+ def blacklist(feature_symbol, &block)
41
+ blacklists[feature_symbol.to_sym] = block
42
+ end
43
+ end
44
+
45
+ protected
46
+
47
+ def whitelisted?(feature_recipient)
48
+ x_listed?(self.class.whitelists, feature_recipient)
49
+ end
50
+
51
+ def blacklisted?(feature_recipient)
52
+ x_listed?(self.class.blacklists, feature_recipient)
53
+ end
54
+
55
+ def x_listed?(list_map, feature_recipient)
56
+ list = list_map[self.symbol.to_sym]
57
+ list.present? && list.call(feature_recipient)
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,30 @@
1
+ Arturo.instance_eval do
2
+
3
+ # Enable a feature; create it if necessary.
4
+ # For use in testing. Not auto-required on load. To load,
5
+ #
6
+ # require 'arturo/test_support'
7
+ #
8
+ # @param [Symbol, String] name the feature name
9
+ def enable_feature!(name)
10
+ feature = Arturo::Feature.to_feature(name)
11
+ if feature
12
+ feature.update_attributes(:deployment_percentage => 100)
13
+ else
14
+ Arturo::Feature.create!(:symbol => name, :deployment_percentage => 100)
15
+ end
16
+ end
17
+
18
+ # Disable a feature if it exists.
19
+ # For use in testing. Not auto-required on load. To load,
20
+ #
21
+ # require 'arturo/test_support'
22
+ #
23
+ # @param [Symbol, String] name the feature name
24
+ def disable_feature!(name)
25
+ if (feature = Arturo::Feature.to_feature(name))
26
+ feature.update_attributes(:deployment_percentage => 0)
27
+ end
28
+ end
29
+
30
+ 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,18 @@
1
+ require 'rails/generators'
2
+
3
+ module Arturo
4
+ class AssetsGenerator < Rails::Generators::Base
5
+ def self.source_root
6
+ File.join(File.dirname(__FILE__), 'templates')
7
+ end
8
+
9
+ def copy_assets
10
+ copy_file 'arturo.css', 'public/stylesheets/arturo.css', :force => true
11
+ copy_file 'arturo_customizations.css', 'public/stylesheets/arturo_customizations.css', :skip => true
12
+ copy_file 'arturo.js', 'public/javascripts/arturo.js'
13
+ copy_file 'semicolon.png', 'public/images/semicolon.png'
14
+ end
15
+
16
+ end
17
+ end
18
+
@@ -0,0 +1,13 @@
1
+ require 'rails/generators'
2
+
3
+ module Arturo
4
+ class InitializerGenerator < Rails::Generators::Base
5
+ def self.source_root
6
+ File.join(File.dirname(__FILE__), 'templates')
7
+ end
8
+
9
+ def copy_initializer_file
10
+ copy_file "initializer.rb", "config/initializers/arturo_initializer.rb"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,27 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ module Arturo
5
+ class MigrationGenerator < Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+
8
+ def self.source_root
9
+ File.join(File.dirname(__FILE__), 'templates')
10
+ end
11
+
12
+ # Implement the required interface for Rails::Generators::Migration.
13
+ # taken from
14
+ # http://github.com/rails/rails/blob/master/activerecord/lib/generators/active_record.rb
15
+ def self.next_migration_number(dirname) #:nodoc:
16
+ if ActiveRecord::Base.timestamped_migrations
17
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
18
+ else
19
+ "%.3d" % (current_migration_number(dirname) + 1)
20
+ end
21
+ end
22
+
23
+ def create_migration_file
24
+ migration_template 'migration.rb', 'db/migrate/create_features.rb'
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ require 'rails/generators'
2
+
3
+ module Arturo
4
+ class RoutesGenerator < Rails::Generators::Base
5
+
6
+ def add_mount
7
+ if Arturo::Engine.respond_to?(:routes)
8
+ route "mount Arturo::Engine => ''"
9
+ else
10
+ puts "This version of Rails doesn't support Engine-specific routing. Nothing to do."
11
+ end
12
+ end
13
+
14
+ end
15
+ end
@@ -0,0 +1,67 @@
1
+ /*
2
+ WARNING:
3
+
4
+ Do not edit this file. Any changes you make to this file will be overwritten
5
+ when you regenerate the arturo assets (which happens when you upgrade the gem).
6
+ Instead, make customizations to arturo_customizations.css.
7
+ */
8
+
9
+ .features code.symbol:before { content: ":"; }
10
+
11
+ .features { border-collapse: collapse; }
12
+
13
+ .features thead tr:last-child th { border-bottom: 1px solid; }
14
+ .features tfoot tr:first-child th { border-top: 1px solid; }
15
+
16
+ .features th, .features td {
17
+ margin: 0;
18
+ padding: 0.5em 1.5em;
19
+ text-align: left;
20
+ }
21
+
22
+ input.deployment_percentage[type=range] { width: 200px; }
23
+
24
+ output.deployment_percentage.no_js { display: none; }
25
+ output.deployment_percentage { margin-left: 1em; }
26
+ output.deployment_percentage:after { content: "%"; }
27
+
28
+ .features a[rel=edit] { visibility: hidden; }
29
+ .features tr:hover a[rel=edit] { visibility: inherit; }
30
+
31
+ .features tfoot th {
32
+ text-align: right;
33
+ }
34
+
35
+ .features tfoot th * + * {
36
+ margin-left: 2em;
37
+ }
38
+
39
+ .feature_new label, .feature_edit label { font-weight: bold; }
40
+
41
+ .feature_new label, .feature_new .errors,
42
+ .feature_edit label, .feature_edit .errors {
43
+ display: block;
44
+ }
45
+
46
+ .feature_new label + input, .feature_new label + textarea, .feature_new label + select,
47
+ .feature_edit label + input, .feature_edit label + textarea, .feature_edit label + select {
48
+ margin-top: 0.5em;
49
+ }
50
+
51
+ .feature_new input + label, .feature_new textarea + label, .feature_new select + label,
52
+ .feature_edit input + label, .feature_edit textarea + label, .feature_edit select + label {
53
+ margin-top: 1.5em;
54
+ }
55
+
56
+ .feature_new input[type=text], .feature_edit input[type=text] { padding: 0.5em; }
57
+
58
+ .feature_new input.symbol, .feature_edit input.symbol {
59
+ background: transparent url('/images/semicolon.png') no-repeat 3px 4px;
60
+ font-family: "DejaVu Sans Mono", "Droid Sans Mono", "Mondale", monospace;
61
+ padding-left: 9px;
62
+ }
63
+
64
+ .feature_new .errors, .feature_edit .errors { color: red; }
65
+ .feature_new :invalid { border-color: red; }
66
+
67
+ .feature_new footer, .feature_edit footer { margin-top: 2em; }
@@ -0,0 +1,23 @@
1
+ if (typeof(jQuery) === 'function') {
2
+ jQuery.arturo = {
3
+ agentSupportsHTML5Output: ('for' in jQuery('<output />')),
4
+
5
+ linkAndShowOutputs: function() {
6
+ if (jQuery.arturo.agentSupportsHTML5Output) {
7
+ jQuery('.features output,.feature_new output,.feature_edit output').each(function(i, output) {
8
+ var output = jQuery(output);
9
+ var input = jQuery('#' + output.attr('for'));
10
+ input.change(function() {
11
+ console.log('input value changed to ' + input.val());
12
+ output.val(input.val());
13
+ });
14
+ output.removeClass('no_js');
15
+ });
16
+ }
17
+ }
18
+ };
19
+
20
+ jQuery(function() {
21
+ jQuery.arturo.linkAndShowOutputs();
22
+ });
23
+ }
@@ -0,0 +1 @@
1
+ /* Make any customizations to the Arturo styles here */
@@ -0,0 +1,29 @@
1
+ require 'arturo'
2
+
3
+ # Configure who may manage features here.
4
+ # The following is the default implementation.
5
+ # Arturo::FeatureManagement.class_eval do
6
+ # def may_manage_features?
7
+ # current_user.present? && current_user.admin?
8
+ # end
9
+ # end
10
+
11
+ # Configure what receives features here.
12
+ # The following is the default implementation.
13
+ # Arturo::FeatureAvailability.class_eval do
14
+ # def feature_recipient
15
+ # current_user
16
+ # end
17
+ # end
18
+
19
+ # Whitelists and Blacklists:
20
+ #
21
+ # Enable feature one for all admins:
22
+ # Arturo::Feature.whitelist(:feature_one) do |user|
23
+ # user.admin?
24
+ # end
25
+ #
26
+ # Disable feature two for all small accounts:
27
+ # Arturo::Feature.blacklist(:feature_two) do |user|
28
+ # user.account.small?
29
+ # end
@@ -0,0 +1,17 @@
1
+ require 'active_support/core_ext'
2
+
3
+ class CreateFeatures < ActiveRecord::Migration
4
+ def self.up
5
+ create_table :features do |t|
6
+ t.string :symbol, :null => false
7
+ t.integer :deployment_percentage, :null => false
8
+ #Any additional fields here
9
+
10
+ t.timestamps
11
+ end
12
+ end
13
+
14
+ def self.down
15
+ drop_table :features
16
+ end
17
+ end