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
@@ -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
|
@@ -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
|
Binary file
|