detour 0.0.1 → 0.0.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.
- checksums.yaml +7 -0
- data/.gitignore +2 -1
- data/.travis.yml +6 -2
- data/Gemfile +5 -0
- data/README.md +41 -132
- data/Rake.md +108 -0
- data/Rakefile +21 -1
- data/app/assets/fonts/glyphicons-halflings-regular.eot +0 -0
- data/app/assets/fonts/glyphicons-halflings-regular.svg +229 -0
- data/app/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/app/assets/fonts/glyphicons-halflings-regular.woff +0 -0
- data/app/assets/images/detour/.gitkeep +0 -0
- data/app/assets/javascripts/detour/application.js +16 -0
- data/app/assets/javascripts/detour/bootstrap.js +2006 -0
- data/app/assets/javascripts/detour/delete_feature.js +13 -0
- data/app/assets/javascripts/detour/delete_flag.js +13 -0
- data/app/assets/javascripts/detour/modals.js +3 -0
- data/app/assets/javascripts/detour/tooltips.js +3 -0
- data/app/assets/stylesheets/detour/application.css +14 -0
- data/app/assets/stylesheets/detour/bootstrap.css +7112 -0
- data/app/assets/stylesheets/detour/main.css +49 -0
- data/app/controllers/detour/application_controller.rb +12 -0
- data/app/controllers/detour/features_controller.rb +19 -0
- data/app/controllers/detour/flaggable_flags_controller.rb +90 -0
- data/app/controllers/detour/flags_controller.rb +18 -0
- data/app/helpers/detour/application_helper.rb +32 -0
- data/app/helpers/detour/flaggable_flags_helper.rb +5 -0
- data/app/helpers/detour/flags_helper.rb +7 -0
- data/app/models/detour/concerns/countable_flag.rb +19 -0
- data/app/models/detour/concerns/flag_actions.rb +141 -0
- data/app/models/detour/concerns/matchers.rb +73 -0
- data/app/models/detour/feature.rb +81 -0
- data/{lib → app/models}/detour/flag.rb +8 -2
- data/app/models/detour/flag_in_flag.rb +12 -0
- data/app/models/detour/group_flag.rb +13 -0
- data/{lib → app/models}/detour/opt_out_flag.rb +4 -2
- data/app/models/detour/percentage_flag.rb +9 -0
- data/app/views/detour/application/index.html.erb +0 -0
- data/app/views/detour/features/_errors.html.erb +11 -0
- data/app/views/detour/features/_success.html.erb +1 -0
- data/app/views/detour/features/error.js.erb +5 -0
- data/app/views/detour/features/success.js.erb +1 -0
- data/app/views/detour/flaggable_flags/_errors.html.erb +11 -0
- data/app/views/detour/flaggable_flags/_flaggable_flag.html.erb +11 -0
- data/app/views/detour/flaggable_flags/error.js.erb +5 -0
- data/app/views/detour/flaggable_flags/index.html.erb +34 -0
- data/app/views/detour/flaggable_flags/success.js.erb +1 -0
- data/app/views/detour/flags/_feature_form.html.erb +38 -0
- data/app/views/detour/flags/index.html.erb +76 -0
- data/app/views/detour/shared/_nav.html.erb +28 -0
- data/app/views/detour/shared/_spacer_cells.html.erb +3 -0
- data/app/views/layouts/detour/application.html.erb +29 -0
- data/config/routes.rb +16 -0
- data/detour.gemspec +15 -14
- data/lib/detour/acts_as_flaggable.rb +42 -3
- data/lib/detour/configuration.rb +35 -0
- data/lib/detour/engine.rb +5 -0
- data/lib/detour/flag_form.rb +87 -0
- data/lib/detour/version.rb +1 -1
- data/lib/detour.rb +10 -14
- data/lib/generators/templates/migration.rb +2 -0
- data/lib/tasks/detour.rake +16 -16
- data/script/rails +8 -0
- data/spec/controllers/detour/application_controller_spec.rb +15 -0
- data/spec/controllers/detour/features_controller_spec.rb +51 -0
- data/spec/controllers/detour/flaggable_flags_controller_spec.rb +100 -0
- data/spec/controllers/detour/flags_controller_spec.rb +77 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/stylesheets/application.css +13 -0
- data/spec/dummy/app/controllers/application_controller.rb +7 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/user.rb +5 -0
- data/spec/dummy/app/models/widget.rb +5 -0
- data/spec/dummy/app/views/application/index.html.erb +22 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +19 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/detour.rb +36 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +4 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/db/migrate/20131218015844_create_users.rb +10 -0
- data/spec/dummy/db/migrate/20131218023124_create_widgets.rb +9 -0
- data/spec/dummy/db/migrate/20131218055352_add_user_id_to_widgets.rb +6 -0
- data/spec/dummy/db/migrate/20131221052201_setup_detour.rb +32 -0
- data/spec/dummy/db/schema.rb +59 -0
- data/spec/dummy/db/seeds.rb +10 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/factories/feature.rb +5 -0
- data/spec/factories/flag_in_flag.rb +6 -0
- data/spec/factories/group_flag.rb +7 -0
- data/spec/factories/opt_out_flag.rb +6 -0
- data/spec/factories/percentage_flag.rb +7 -0
- data/spec/factories/user.rb +6 -0
- data/spec/factories/widget.rb +5 -0
- data/spec/features/features_spec.rb +70 -0
- data/spec/features/flag_in_flags_spec.rb +118 -0
- data/spec/features/group_flags_spec.rb +49 -0
- data/spec/features/home_page_spec.rb +11 -0
- data/spec/features/opt_out_flags_spec.rb +105 -0
- data/spec/features/percentage_flags_spec.rb +63 -0
- data/spec/integration/group_rollout_spec.rb +1 -1
- data/spec/lib/detour/acts_as_flaggable_spec.rb +45 -0
- data/spec/lib/detour/configuration_spec.rb +23 -0
- data/spec/lib/detour/flag_form_spec.rb +84 -0
- data/spec/lib/{active_record/rollout → detour}/flaggable_spec.rb +19 -19
- data/spec/lib/tasks/{detour_rake_spec.rb → detour_spec.rb} +54 -54
- data/spec/{lib/active_record/rollout → models/detour}/feature_spec.rb +93 -67
- data/spec/models/detour/flag_in_flag_spec.rb +38 -0
- data/spec/{lib/active_record/rollout → models/detour}/flag_spec.rb +1 -1
- data/spec/models/detour/opt_out_flag_spec.rb +38 -0
- data/spec/{lib/active_record/rollout → models/detour}/percentage_flag_spec.rb +1 -1
- data/spec/spec_helper.rb +41 -21
- data/spec/support/shared_contexts/rake.rb +4 -14
- metadata +278 -95
- data/lib/detour/feature.rb +0 -312
- data/lib/detour/flaggable_flag.rb +0 -10
- data/lib/detour/group_flag.rb +0 -8
- data/lib/detour/percentage_flag.rb +0 -11
- data/spec/lib/active_record/rollout/acts_as_flaggable_spec.rb +0 -31
- data/spec/lib/active_record/rollout/flaggable_flag_spec.rb +0 -9
- data/spec/lib/active_record/rollout/opt_out_flag_spec.rb +0 -9
- data/spec/support/schema.rb +0 -13
- /data/lib/generators/{active_record_rollout_generator.rb → detour_generator.rb} +0 -0
- /data/lib/generators/templates/{active_record_rollout.rb → detour.rb} +0 -0
- /data/spec/{lib/active_record/rollout → models/detour}/group_flag_spec.rb +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
@font-face {
|
|
2
|
+
font-family: 'Glyphicons Halflings';
|
|
3
|
+
src: url('/assets/glyphicons-halflings-regular.eot');
|
|
4
|
+
src: url('/assets/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('/assets/glyphicons-halflings-regular.woff') format('woff'), url('/assets/glyphicons-halflings-regular.ttf') format('truetype'), url('/assets/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
body {
|
|
8
|
+
padding-bottom: 40px;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.panel-danger {
|
|
12
|
+
margin-top: 20px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.panel-danger ul {
|
|
16
|
+
margin-bottom: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.modal-body .panel-danger {
|
|
20
|
+
margin-top: 0;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.table-responsive {
|
|
24
|
+
padding-top: 60px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
table.table tbody tr td {
|
|
28
|
+
vertical-align: middle;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.group-header {
|
|
32
|
+
display: block;
|
|
33
|
+
width: 10px;
|
|
34
|
+
white-space: nowrap;
|
|
35
|
+
|
|
36
|
+
-webkit-transform: rotate(-30deg);
|
|
37
|
+
-moz-transform: rotate(-30deg);
|
|
38
|
+
-ms-transform: rotate(-30deg);
|
|
39
|
+
-o-transform: rotate(-30deg);
|
|
40
|
+
transform: rotate(-30deg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.percentage-header {
|
|
44
|
+
width: 120px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.counter-header {
|
|
48
|
+
width: 80px;
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
class Detour::ApplicationController < ActionController::Base
|
|
2
|
+
def index
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
private
|
|
6
|
+
|
|
7
|
+
def ensure_flaggable_type_exists
|
|
8
|
+
unless Detour.config.flaggable_types.map(&:tableize).include? params[:flaggable_type]
|
|
9
|
+
raise ActionController::RoutingError.new("Not Found")
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
class Detour::FeaturesController < Detour::ApplicationController
|
|
2
|
+
def create
|
|
3
|
+
@feature = Detour::Feature.new(params[:feature])
|
|
4
|
+
|
|
5
|
+
if @feature.save
|
|
6
|
+
flash[:notice] = "Your feature has been successfully created."
|
|
7
|
+
render :success
|
|
8
|
+
else
|
|
9
|
+
render :error
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def destroy
|
|
14
|
+
@feature = Detour::Feature.find(params[:id])
|
|
15
|
+
@feature.destroy
|
|
16
|
+
flash[:notice] = "Feature #{@feature.name} has been deleted."
|
|
17
|
+
redirect_to flags_path(params[:flaggable_type])
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require "indefinite_article"
|
|
2
|
+
|
|
3
|
+
class Detour::FlaggableFlagsController < Detour::ApplicationController
|
|
4
|
+
before_filter :ensure_flaggable_type_exists
|
|
5
|
+
|
|
6
|
+
def index
|
|
7
|
+
feature = Detour::Feature.find_by_name!(feature_name)
|
|
8
|
+
@flags = feature.send("#{flag_type}_flags").where(flaggable_type: flaggable_class.to_s)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def create
|
|
12
|
+
@feature = Detour::Feature.find_by_name! feature_name
|
|
13
|
+
ids = params[:ids].split(",")
|
|
14
|
+
@errors = []
|
|
15
|
+
|
|
16
|
+
Detour::Feature.transaction do
|
|
17
|
+
begin
|
|
18
|
+
ids.each do |id|
|
|
19
|
+
flaggable = flaggable_class.flaggable_find! id
|
|
20
|
+
flag = @feature.send("#{flaggable_type}_#{flag_type.pluralize}").new flaggable: flaggable
|
|
21
|
+
|
|
22
|
+
unless flag.save
|
|
23
|
+
@errors.concat flag.errors.full_messages
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
27
|
+
@errors << e.message
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if @errors.any?
|
|
31
|
+
raise ActiveRecord::Rollback
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if @errors.empty?
|
|
36
|
+
flash[:notice] = success_message
|
|
37
|
+
render :success
|
|
38
|
+
else
|
|
39
|
+
render :error
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def destroy
|
|
44
|
+
feature = Detour::Feature.find_by_name!(feature_name)
|
|
45
|
+
@flag = feature.send("#{flag_type}_flags").find(params[:id])
|
|
46
|
+
@flag.destroy
|
|
47
|
+
flash[:notice] = "#{feature_name} #{flag_noun} for #{flaggable_class} #{@flag.flaggable.send flaggable_class.detour_flaggable_find_by} has been deleted."
|
|
48
|
+
redirect_to send("#{flag_type}_flags_path", feature.name, flaggable_type)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def feature_name
|
|
54
|
+
params[:feature_name]
|
|
55
|
+
end
|
|
56
|
+
helper_method :feature_name
|
|
57
|
+
|
|
58
|
+
def flaggable_type
|
|
59
|
+
params[:flaggable_type]
|
|
60
|
+
end
|
|
61
|
+
helper_method :flaggable_type
|
|
62
|
+
|
|
63
|
+
def flaggable_class
|
|
64
|
+
flaggable_type.classify.constantize
|
|
65
|
+
end
|
|
66
|
+
helper_method :flaggable_class
|
|
67
|
+
|
|
68
|
+
def flag_type
|
|
69
|
+
request.path.split("/")[2].underscore.singularize
|
|
70
|
+
end
|
|
71
|
+
helper_method :flag_type
|
|
72
|
+
|
|
73
|
+
def flag_verb
|
|
74
|
+
flag_type == "flag_in" ? "flagged in to" : "opted out of"
|
|
75
|
+
end
|
|
76
|
+
helper_method :flag_verb
|
|
77
|
+
|
|
78
|
+
def flag_noun
|
|
79
|
+
flag_type.dasherize
|
|
80
|
+
end
|
|
81
|
+
helper_method :flag_noun
|
|
82
|
+
|
|
83
|
+
def success_message
|
|
84
|
+
plural = params[:ids].split(",").length > 1
|
|
85
|
+
klass = plural ? flaggable_class.to_s.pluralize : flaggable_class
|
|
86
|
+
has = plural ? "have" : "has"
|
|
87
|
+
|
|
88
|
+
flash[:notice] = "#{klass} #{params[:ids].split(",").join(", ")} #{has} been #{flag_verb} #{@feature.name}"
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class Detour::FlagsController < Detour::ApplicationController
|
|
2
|
+
before_filter :ensure_flaggable_type_exists
|
|
3
|
+
|
|
4
|
+
def index
|
|
5
|
+
@flag_form = Detour::FlagForm.new(params[:flaggable_type])
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def update
|
|
9
|
+
@flag_form = Detour::FlagForm.new(params[:flaggable_type])
|
|
10
|
+
|
|
11
|
+
if @flag_form.update_attributes(params)
|
|
12
|
+
flash[:notice] = "Your flags have been successfully updated."
|
|
13
|
+
redirect_to flags_path
|
|
14
|
+
else
|
|
15
|
+
render :index
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Detour::ApplicationHelper
|
|
2
|
+
def table(&block)
|
|
3
|
+
content_tag :div, class: "table-responsive" do
|
|
4
|
+
content_tag :table, class: "table table-striped" do
|
|
5
|
+
yield
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def modal(options, &block)
|
|
11
|
+
content_tag :div, id: options[:id], class: "modal #{options[:fade].present?}", tabindex: "-1", role: "dialog", aria_labbeledby: "#{options[:id]}-modal-label", aria_hidden: "true" do
|
|
12
|
+
content_tag :div, class: "modal-dialog" do
|
|
13
|
+
content_tag :div, class: "modal-content" do
|
|
14
|
+
content_tag(:div, class: "modal-header") do
|
|
15
|
+
content_tag :button, "×", class: "close", data_dismiss: "modal", aria_hidden: "true"
|
|
16
|
+
content_tag :h4, options[:title], id: "#{options[:id]}-modal-label"
|
|
17
|
+
end +
|
|
18
|
+
|
|
19
|
+
content_tag(:div, class: "modal-body") do
|
|
20
|
+
yield
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def modal_footer(&block)
|
|
28
|
+
content_tag :div, class: "modal-footer" do
|
|
29
|
+
yield
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Detour::Concerns
|
|
2
|
+
module CountableFlag
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
included do
|
|
6
|
+
after_save :step_count
|
|
7
|
+
after_destroy :step_count
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def step_count
|
|
12
|
+
count = feature.send("#{flag_type}_count_for", flaggable_type.tableize)
|
|
13
|
+
count = destroyed? ? count - 1 : count + 1
|
|
14
|
+
feature.send("#{flag_type}_counts")[flaggable_type.tableize] = count
|
|
15
|
+
feature.save!
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
module Detour::Concerns
|
|
2
|
+
module FlagActions
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
module ClassMethods
|
|
6
|
+
# Add a record to the given feature. If the feature is not found, an
|
|
7
|
+
# ActiveRecord::RecordNotFound will be raised.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# Detour::Feature.add_record_to_feature user, :new_ui
|
|
11
|
+
#
|
|
12
|
+
# @param [ActiveRecord::Base] record A record to add the feature to.
|
|
13
|
+
# @param [String,Symbol] feature_name The feature to be added to the record.
|
|
14
|
+
#
|
|
15
|
+
# @return [Detour::Flag] The
|
|
16
|
+
# {Detour::Flag Flag} created.
|
|
17
|
+
def add_record_to_feature(record, feature_name)
|
|
18
|
+
feature = find_by_name!(feature_name)
|
|
19
|
+
feature.flag_in_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).first_or_create!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Remove a record from the given feature. If the feature is not found, an
|
|
23
|
+
# ActiveRecord::RecordNotFound will be raised.
|
|
24
|
+
#
|
|
25
|
+
# @example
|
|
26
|
+
# Detour::Feature.remove_record_from_feature user, :new_ui
|
|
27
|
+
#
|
|
28
|
+
# @param [ActiveRecord::Base] record A record to remove the feature from.
|
|
29
|
+
# @param [String,Symbol] feature_name The feature to be removed from the
|
|
30
|
+
# record.
|
|
31
|
+
def remove_record_from_feature(record, feature_name)
|
|
32
|
+
feature = find_by_name!(feature_name)
|
|
33
|
+
feature.flag_in_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).destroy_all
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Opt the given record out of a feature. If the feature is not found, an
|
|
37
|
+
# ActiveRecord::RecordNotFound will be raised. An opt out ensures that no
|
|
38
|
+
# matter what, `record.rollout?(:rollout)` will always return false for any
|
|
39
|
+
# opted-out-of features.
|
|
40
|
+
#
|
|
41
|
+
# @param [ActiveRecord::Base] record A record to opt out of the feature.
|
|
42
|
+
# @param [String,Symbol] feature_name The feature to be opted out of.
|
|
43
|
+
#
|
|
44
|
+
# @example
|
|
45
|
+
# Detour::Feature.opt_record_out_of_feature user, :new_ui
|
|
46
|
+
#
|
|
47
|
+
# @return [Detour::OptOut] The
|
|
48
|
+
# {Detour::OptOut OptOut} created.
|
|
49
|
+
def opt_record_out_of_feature(record, feature_name)
|
|
50
|
+
feature = find_by_name!(feature_name)
|
|
51
|
+
feature.opt_out_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).first_or_create!
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Remove any opt out for the given record out of a feature. If the feature
|
|
55
|
+
# is not found, an ActiveRecord::RecordNotFound will be raised.
|
|
56
|
+
#
|
|
57
|
+
# @example
|
|
58
|
+
# Detour::Feature.un_opt_record_out_of_feature user, :new_ui
|
|
59
|
+
#
|
|
60
|
+
# @param [ActiveRecord::Base] record A record to un-opt-out of the feature.
|
|
61
|
+
# @param [String,Symbol] feature_name The feature to be un-opted-out of.
|
|
62
|
+
def un_opt_record_out_of_feature(record, feature_name)
|
|
63
|
+
feature = find_by_name!(feature_name)
|
|
64
|
+
feature.opt_out_flags.where(flaggable_type: record.class.to_s, flaggable_id: record.id).destroy_all
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Add a group to the given feature. If the feature is not found, an
|
|
68
|
+
# ActiveRecord::RecordNotFound will be raised.
|
|
69
|
+
#
|
|
70
|
+
# @example
|
|
71
|
+
# Detour::Feature.add_group_to_feature "User", "admin", :delete_records
|
|
72
|
+
#
|
|
73
|
+
# @param [String] flaggable_type The class (as a string) that the group
|
|
74
|
+
# should be associated with.
|
|
75
|
+
# @param [String] group_name The name of the group to have the feature
|
|
76
|
+
# added to it.
|
|
77
|
+
# @param [String,Symbol] feature_name The feature to be added to the group.
|
|
78
|
+
#
|
|
79
|
+
# @return [Detour::Flag] The
|
|
80
|
+
# {Detour::Flag Flag} created.
|
|
81
|
+
def add_group_to_feature(flaggable_type, group_name, feature_name)
|
|
82
|
+
feature = find_by_name!(feature_name)
|
|
83
|
+
feature.group_flags.where(flaggable_type: flaggable_type, group_name: group_name).first_or_create!
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Remove a group from agiven feature. If the feature is not found, an
|
|
87
|
+
# ActiveRecord::RecordNotFound will be raised.
|
|
88
|
+
#
|
|
89
|
+
# @example
|
|
90
|
+
# Detour::Feature.remove_group_from_feature "User", "admin", :delete_records
|
|
91
|
+
#
|
|
92
|
+
# @param [String] flaggable_type The class (as a string) that the group should
|
|
93
|
+
# be removed from.
|
|
94
|
+
# @param [String] group_name The name of the group to have the feature
|
|
95
|
+
# removed from it.
|
|
96
|
+
# @param [String,Symbol] feature_name The feature to be removed from the
|
|
97
|
+
# group.
|
|
98
|
+
def remove_group_from_feature(flaggable_type, group_name, feature_name)
|
|
99
|
+
feature = find_by_name!(feature_name)
|
|
100
|
+
feature.group_flags.where(flaggable_type: flaggable_type, group_name: group_name).destroy_all
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Add a percentage of records to the given feature. If the feature is not
|
|
104
|
+
# found, an ActiveRecord::RecordNotFound will be raised.
|
|
105
|
+
#
|
|
106
|
+
# @example
|
|
107
|
+
# Detour::Feature.add_percentage_to_feature "User", 75, :delete_records
|
|
108
|
+
#
|
|
109
|
+
# @param [String] flaggable_type The class (as a string) that the percetnage
|
|
110
|
+
# should be associated with.
|
|
111
|
+
# @param [Integer] percentage The percentage of `flaggable_type` records
|
|
112
|
+
# that the feature will be available for.
|
|
113
|
+
# @param [String,Symbol] feature_name The feature to be added to the
|
|
114
|
+
# percentage of records.
|
|
115
|
+
#
|
|
116
|
+
# @return [Detour::Flag] The
|
|
117
|
+
# {Detour::Flag Flag} created.
|
|
118
|
+
def add_percentage_to_feature(flaggable_type, percentage, feature_name)
|
|
119
|
+
feature = find_by_name!(feature_name)
|
|
120
|
+
|
|
121
|
+
flag = feature.percentage_flags.where(flaggable_type: flaggable_type).first_or_initialize
|
|
122
|
+
flag.update_attributes!(percentage: percentage)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Remove any percentage flags for the given feature. If the feature is not
|
|
126
|
+
# found, an ActiveRecord::RecordNotFound will be raised.
|
|
127
|
+
#
|
|
128
|
+
# @example
|
|
129
|
+
# Detour::Feature.remove_percentage_from_feature "User", delete_records
|
|
130
|
+
#
|
|
131
|
+
# @param [String] flaggable_type The class (as a string) that the percetnage
|
|
132
|
+
# should be removed from.
|
|
133
|
+
# @param [String,Symbol] feature_name The feature to have the percentage
|
|
134
|
+
# flag removed from.
|
|
135
|
+
def remove_percentage_from_feature(flaggable_type, feature_name)
|
|
136
|
+
feature = find_by_name!(feature_name)
|
|
137
|
+
feature.percentage_flags.where(flaggable_type: flaggable_type).destroy_all
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module Detour::Concerns
|
|
2
|
+
module Matchers
|
|
3
|
+
# Determines whether or not the given instance has had the feature rolled out
|
|
4
|
+
# to it either via direct flagging-in, percentage, or by group membership.
|
|
5
|
+
#
|
|
6
|
+
# @example
|
|
7
|
+
# feature.match?(current_user)
|
|
8
|
+
#
|
|
9
|
+
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
|
10
|
+
# rollout.
|
|
11
|
+
#
|
|
12
|
+
# @return Whether or not the given instance has the feature rolled out to it.
|
|
13
|
+
def match?(instance)
|
|
14
|
+
match_id?(instance) || match_percentage?(instance) || match_groups?(instance)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Determines whether or not the given instance has had the feature rolled out
|
|
18
|
+
# to it via direct flagging-in.
|
|
19
|
+
#
|
|
20
|
+
# @example
|
|
21
|
+
# feature.match_id?(current_user)
|
|
22
|
+
#
|
|
23
|
+
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
|
24
|
+
# rollout.
|
|
25
|
+
#
|
|
26
|
+
# @return Whether or not the given instance has the feature rolled out to it
|
|
27
|
+
# via direct flagging-in.
|
|
28
|
+
def match_id?(instance)
|
|
29
|
+
flag_in_flags.where(flaggable_type: instance.class.to_s, flaggable_id: instance.id).any?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Determines whether or not the given instance has had the feature rolled out
|
|
33
|
+
# to it via percentage.
|
|
34
|
+
#
|
|
35
|
+
# @example
|
|
36
|
+
# feature.match_percentage?(current_user)
|
|
37
|
+
#
|
|
38
|
+
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
|
39
|
+
# rollout.
|
|
40
|
+
#
|
|
41
|
+
# @return Whether or not the given instance has the feature rolled out to it
|
|
42
|
+
# via direct percentage.
|
|
43
|
+
def match_percentage?(instance)
|
|
44
|
+
flag = percentage_flags.find(:first, conditions: ["flaggable_type = ?", instance.class.to_s])
|
|
45
|
+
percentage = flag ? flag.percentage : 0
|
|
46
|
+
|
|
47
|
+
instance.id % 10 < percentage / 10
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Determines whether or not the given instance has had the feature rolled out
|
|
51
|
+
# to it via group membership.
|
|
52
|
+
#
|
|
53
|
+
# @example
|
|
54
|
+
# feature.match_groups?(current_user)
|
|
55
|
+
#
|
|
56
|
+
# @param [ActiveRecord::Base] instance A record to be tested for feature
|
|
57
|
+
# rollout.
|
|
58
|
+
#
|
|
59
|
+
# @return Whether or not the given instance has the feature rolled out to it
|
|
60
|
+
# via direct group membership.
|
|
61
|
+
def match_groups?(instance)
|
|
62
|
+
klass = instance.class.to_s
|
|
63
|
+
|
|
64
|
+
return unless Detour.config.defined_groups[klass]
|
|
65
|
+
|
|
66
|
+
group_names = group_flags.find_all_by_flaggable_type(klass).collect(&:group_name)
|
|
67
|
+
|
|
68
|
+
Detour.config.defined_groups[klass].collect { |group_name, block|
|
|
69
|
+
block.call(instance) if group_names.include? group_name
|
|
70
|
+
}.any?
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Represents an individual feature that may be rolled out to a set of records
|
|
2
|
+
# via individual flags, percentages, or defined groups.
|
|
3
|
+
class Detour::Feature < ActiveRecord::Base
|
|
4
|
+
include Detour::Concerns::Matchers
|
|
5
|
+
include Detour::Concerns::FlagActions
|
|
6
|
+
|
|
7
|
+
self.table_name = :detour_features
|
|
8
|
+
|
|
9
|
+
serialize :flag_in_counts, JSON
|
|
10
|
+
serialize :opt_out_counts, JSON
|
|
11
|
+
|
|
12
|
+
has_many :flag_in_flags
|
|
13
|
+
has_many :group_flags
|
|
14
|
+
has_many :percentage_flags
|
|
15
|
+
has_many :opt_out_flags
|
|
16
|
+
has_many :flags, dependent: :destroy
|
|
17
|
+
|
|
18
|
+
validates_presence_of :name
|
|
19
|
+
validates_uniqueness_of :name
|
|
20
|
+
|
|
21
|
+
attr_accessible :name
|
|
22
|
+
|
|
23
|
+
# Returns an instance variable intended to hold an array of the lines of code
|
|
24
|
+
# that this feature appears on.
|
|
25
|
+
#
|
|
26
|
+
# @return [Array<String>] The lines that this rollout appears on (if
|
|
27
|
+
# {Detour::Feature.with_lines} has already been called).
|
|
28
|
+
def lines
|
|
29
|
+
@lines ||= []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def to_s
|
|
33
|
+
name
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Returns the number of flag-ins for a given type.
|
|
37
|
+
#
|
|
38
|
+
# @example
|
|
39
|
+
# feature.flag_in_count_for("users")
|
|
40
|
+
#
|
|
41
|
+
# @return [Fixnum] The number of flag-ins for the given type.
|
|
42
|
+
def flag_in_count_for(type)
|
|
43
|
+
flag_in_counts[type] || 0
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the number of opt-outs for a given type.
|
|
47
|
+
#
|
|
48
|
+
# @example
|
|
49
|
+
# feature.opt_out_count_for("users")
|
|
50
|
+
#
|
|
51
|
+
# @return [Fixnum] The number of opt-outs for the given type.
|
|
52
|
+
def opt_out_count_for(type)
|
|
53
|
+
opt_out_counts[type] || 0
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Return an array of both every feature in the database as well as every
|
|
57
|
+
# feature that is checked for in `@grep_dirs`. Features that are checked
|
|
58
|
+
# for but not persisted will be returned as unpersisted instances of this
|
|
59
|
+
# class. Each instance returned will have its `@lines` set to an array
|
|
60
|
+
# containing every line in `@grep_dirs` where it is checked for.
|
|
61
|
+
#
|
|
62
|
+
# @return [Array<Detour::Feature>] Every persisted and
|
|
63
|
+
# checked-for feature.
|
|
64
|
+
def self.with_lines
|
|
65
|
+
hash = all.each_with_object({}) { |feature, hash| hash[feature.name] = feature }
|
|
66
|
+
|
|
67
|
+
Dir[*Detour.config.grep_dirs].each do |path|
|
|
68
|
+
next unless File.file? path
|
|
69
|
+
|
|
70
|
+
File.open path do |file|
|
|
71
|
+
file.each_line.with_index(1) do |line, i|
|
|
72
|
+
line.scan(/\.has_feature\?\s*\(*:(\w+)/).each do |match, _|
|
|
73
|
+
(hash[match] ||= new(name: match)).lines << "#{path}#L#{i}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
hash.values.sort_by(&:name)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -6,8 +6,14 @@ class Detour::Flag < ActiveRecord::Base
|
|
|
6
6
|
|
|
7
7
|
belongs_to :feature
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
validates_presence_of :feature
|
|
10
|
+
validates_presence_of :flaggable_type
|
|
11
11
|
|
|
12
12
|
attr_accessible :flaggable_type
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def flag_type
|
|
17
|
+
self.class.to_s.underscore.gsub("detour/", "").split("_")[0..-2].join("_")
|
|
18
|
+
end
|
|
13
19
|
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# An individual record of a certain type may be flagged into a feature with
|
|
2
|
+
# this class.
|
|
3
|
+
class Detour::FlagInFlag < Detour::Flag
|
|
4
|
+
include Detour::Concerns::CountableFlag
|
|
5
|
+
|
|
6
|
+
belongs_to :flaggable, polymorphic: true
|
|
7
|
+
|
|
8
|
+
validates_presence_of :flaggable
|
|
9
|
+
validates_uniqueness_of :feature_id, scope: [:flaggable_type, :flaggable_id]
|
|
10
|
+
|
|
11
|
+
attr_accessible :flaggable
|
|
12
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# A group of flaggable records of a given class may be flagged into a feature
|
|
2
|
+
# with this class.
|
|
3
|
+
class Detour::GroupFlag < Detour::Flag
|
|
4
|
+
validates_presence_of :group_name
|
|
5
|
+
validates_uniqueness_of :feature_id, scope: [:flaggable_type, :group_name]
|
|
6
|
+
|
|
7
|
+
attr_writer :to_keep
|
|
8
|
+
attr_accessible :group_name
|
|
9
|
+
|
|
10
|
+
def to_keep
|
|
11
|
+
@to_keep || (!marked_for_destruction? && !new_record?)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
# Ensures that a feature will never be available to the associated record,
|
|
2
2
|
# even in the case of, for example, a 100% flag.
|
|
3
3
|
class Detour::OptOutFlag < Detour::Flag
|
|
4
|
+
include Detour::Concerns::CountableFlag
|
|
5
|
+
|
|
4
6
|
belongs_to :flaggable, polymorphic: true
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
+
validates_presence_of :flaggable
|
|
9
|
+
validates_uniqueness_of :feature_id, scope: [:flaggable_type, :flaggable_id]
|
|
8
10
|
|
|
9
11
|
attr_accessible :flaggable
|
|
10
12
|
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# A percentage of flaggable records of a given class may be flagged into a feature
|
|
2
|
+
# with this class.
|
|
3
|
+
class Detour::PercentageFlag < Detour::Flag
|
|
4
|
+
validates_presence_of :percentage
|
|
5
|
+
validates_numericality_of :percentage, greater_than: 0, less_than_or_equal_to: 100
|
|
6
|
+
validates_uniqueness_of :feature_id, scope: :flaggable_type
|
|
7
|
+
|
|
8
|
+
attr_accessible :percentage
|
|
9
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="panel panel-danger">
|
|
2
|
+
<div class="panel-heading">Whoops! There were some errors saving your feature:</div>
|
|
3
|
+
|
|
4
|
+
<div class="panel-body">
|
|
5
|
+
<ul>
|
|
6
|
+
<% @feature.errors.full_messages.each do |msg| %>
|
|
7
|
+
<li><%= msg %></li>
|
|
8
|
+
<% end %>
|
|
9
|
+
</ul>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<div class="alert alert-success"><%= flash.notice %></div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
location.reload();
|