detour 0.0.3 → 0.0.5
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/README.md +2 -25
- data/app/assets/javascripts/detour/add_fields.js +7 -0
- data/app/assets/javascripts/detour/delete_feature.js +9 -11
- data/app/assets/javascripts/detour/delete_flag.js +9 -11
- data/app/assets/javascripts/detour/delete_group.js +5 -0
- data/app/assets/javascripts/detour/feature_lines.js +23 -0
- data/app/assets/javascripts/detour/modals.js +1 -0
- data/app/assets/stylesheets/detour/main.css +12 -0
- data/app/controllers/detour/features_controller.rb +3 -2
- data/app/controllers/detour/flaggable_flags_controller.rb +9 -77
- data/app/controllers/detour/groups_controller.rb +39 -0
- data/app/helpers/detour/application_helper.rb +11 -0
- data/app/helpers/detour/flaggable_flags_helper.rb +20 -0
- data/app/helpers/detour/flags_helper.rb +5 -1
- data/app/models/detour/concerns/keepable.rb +21 -0
- data/app/models/detour/concerns/matchers.rb +28 -9
- data/app/models/detour/database_group_flag.rb +30 -0
- data/app/models/detour/defined_group.rb +21 -0
- data/app/models/detour/defined_group_flag.rb +28 -0
- data/app/models/detour/feature.rb +4 -3
- data/app/models/detour/flag_in_flag.rb +1 -8
- data/app/models/detour/flaggable_flag.rb +24 -0
- data/app/models/detour/group.rb +17 -0
- data/app/models/detour/membership.rb +41 -0
- data/app/models/detour/opt_out_flag.rb +1 -8
- data/app/views/detour/flaggable_flags/_flaggable_flag_fields.html.erb +19 -0
- data/app/views/detour/flaggable_flags/index.html.erb +13 -26
- data/app/views/detour/flags/_feature_form.html.erb +12 -3
- data/app/views/detour/flags/index.html.erb +7 -2
- data/app/views/detour/groups/_group.html.erb +3 -0
- data/app/views/detour/groups/_membership_fields.html.erb +19 -0
- data/app/views/detour/groups/index.html.erb +21 -0
- data/app/views/detour/groups/show.html.erb +41 -0
- data/app/views/detour/memberships/_membership.html.erb +4 -0
- data/app/views/detour/{features → shared}/_errors.html.erb +2 -2
- data/app/views/detour/shared/_nav.html.erb +1 -0
- data/app/views/detour/shared/error.js.erb +5 -0
- data/config/locales/en.yml +11 -0
- data/config/routes.rb +8 -7
- data/detour.gemspec +1 -0
- data/lib/detour/acts_as_flaggable.rb +19 -4
- data/lib/detour/configuration.rb +1 -1
- data/lib/detour/flag_form.rb +53 -34
- data/lib/detour/flaggable.rb +0 -19
- data/lib/detour/version.rb +1 -1
- data/lib/generators/templates/migration.rb +21 -1
- data/lib/tasks/.gitkeep +0 -0
- data/spec/controllers/detour/flaggable_flags_controller_spec.rb +30 -67
- data/spec/controllers/detour/groups_controller_spec.rb +107 -0
- data/spec/dummy/db/migrate/20131221052201_setup_detour.rb +21 -1
- data/spec/dummy/db/schema.rb +20 -1
- data/spec/factories/database_group_flag.rb +7 -0
- data/spec/factories/{group_flag.rb → defined_group_flag.rb} +1 -1
- data/spec/factories/group.rb +10 -0
- data/spec/factories/membership.rb +6 -0
- data/spec/features/database_group_flags_spec.rb +50 -0
- data/spec/features/database_groups_spec.rb +174 -0
- data/spec/features/defined_group_flags_spec.rb +67 -0
- data/spec/features/features_spec.rb +44 -0
- data/spec/features/flag_in_flags_spec.rb +22 -60
- data/spec/features/opt_out_flags_spec.rb +34 -59
- data/spec/integration/group_rollout_spec.rb +2 -2
- data/spec/lib/detour/acts_as_flaggable_spec.rb +12 -3
- data/spec/lib/detour/configuration_spec.rb +6 -2
- data/spec/lib/detour/flag_form_spec.rb +0 -11
- data/spec/lib/detour/flaggable_spec.rb +1 -54
- data/spec/models/detour/database_group_flag_spec.rb +29 -0
- data/spec/models/detour/defined_group_spec.rb +21 -0
- data/spec/models/detour/feature_spec.rb +57 -119
- data/spec/models/detour/flag_in_flag_spec.rb +1 -4
- data/spec/models/detour/flaggable_flag_spec.rb +25 -0
- data/spec/models/detour/group_flag_spec.rb +1 -1
- data/spec/models/detour/membership_spec.rb +58 -0
- data/spec/models/detour/opt_out_flag_spec.rb +1 -4
- data/spec/spec_helper.rb +4 -0
- metadata +97 -81
- data/app/models/detour/concerns/flag_actions.rb +0 -141
- data/app/models/detour/group_flag.rb +0 -13
- data/app/views/detour/features/_success.html.erb +0 -1
- data/app/views/detour/features/error.js.erb +0 -5
- data/lib/tasks/detour.rake +0 -119
- data/spec/features/group_flags_spec.rb +0 -49
- data/spec/integration/flag_rollout_spec.rb +0 -27
- data/spec/lib/tasks/detour_spec.rb +0 -162
- /data/app/views/detour/{features → shared}/success.js.erb +0 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
class Detour::Membership < ActiveRecord::Base
|
2
|
+
validates :group_id, presence: true
|
3
|
+
validates :member_id, presence: true, uniqueness: { scope: :group_id }
|
4
|
+
validates :member_key, presence: true, unless: -> { member_id }
|
5
|
+
validates :member_type, presence: true
|
6
|
+
validate :validate_member_type
|
7
|
+
|
8
|
+
attr_accessor :member_key
|
9
|
+
attr_accessible :group_id
|
10
|
+
attr_accessible :member_key
|
11
|
+
attr_accessible :member_type
|
12
|
+
|
13
|
+
default_scope { order("member_type ASC") }
|
14
|
+
|
15
|
+
belongs_to :group
|
16
|
+
belongs_to :member, polymorphic: true
|
17
|
+
|
18
|
+
before_validation :set_member
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def member_class
|
23
|
+
group.flaggable_class
|
24
|
+
end
|
25
|
+
|
26
|
+
def set_member
|
27
|
+
unless member || !member_key
|
28
|
+
self.member_type = group.flaggable_type
|
29
|
+
self.member_id = member_class.flaggable_find!(member_key).id
|
30
|
+
end
|
31
|
+
rescue ActiveRecord::RecordNotFound
|
32
|
+
errors.add(member_type, "\"#{member_key}\" could not be found")
|
33
|
+
false
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_member_type
|
37
|
+
unless group && member_type == group.flaggable_type
|
38
|
+
errors.add :member_type, "must match the group's flaggable_type"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,12 +1,5 @@
|
|
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
|
-
class Detour::OptOutFlag < Detour::
|
4
|
-
include Detour::Concerns::CountableFlag
|
5
|
-
|
6
|
-
belongs_to :flaggable, polymorphic: true
|
7
|
-
|
8
|
-
validates_presence_of :flaggable
|
3
|
+
class Detour::OptOutFlag < Detour::FlaggableFlag
|
9
4
|
validates_uniqueness_of :feature_id, scope: [:flaggable_type, :flaggable_id]
|
10
|
-
|
11
|
-
attr_accessible :flaggable
|
12
5
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<hr>
|
2
|
+
|
3
|
+
<div class="row">
|
4
|
+
<%= f.label :flaggable_key, flaggable_type, class: "col-sm-1" %>
|
5
|
+
|
6
|
+
<div class="col-sm-3">
|
7
|
+
<% if f.object.persisted? %>
|
8
|
+
<%= text_field_tag "flaggable_identifier", f.object.flaggable.send(f.object.flaggable.class.detour_flaggable_find_by), class: "form-control", disabled: "disabled" %>
|
9
|
+
<% else %>
|
10
|
+
<%= f.text_field :flaggable_key, class: "form-control" %>
|
11
|
+
<% end %>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<div class="col-sm-2 checkbox">
|
15
|
+
<%= f.label :_destroy do %>
|
16
|
+
<%= f.check_box "_destroy" %> Remove <%= flag_noun.capitalize %>
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
</div>
|
@@ -1,34 +1,21 @@
|
|
1
1
|
<h1><%= flaggable_type.capitalize %> <%= flag_verb %> <%= feature_name %></h1>
|
2
2
|
|
3
|
-
|
4
|
-
<
|
5
|
-
|
6
|
-
<
|
7
|
-
|
8
|
-
|
9
|
-
</thead>
|
10
|
-
|
11
|
-
<%= render partial: "flaggable_flag", collection: @flags, as: :flag %>
|
3
|
+
<% if @feature.errors.any? %>
|
4
|
+
<ul>
|
5
|
+
<% @feature.errors.full_messages.each do |msg| %>
|
6
|
+
<li><%= msg %></li>
|
7
|
+
<% end %>
|
8
|
+
</ul>
|
12
9
|
<% end %>
|
13
10
|
|
14
|
-
<%=
|
15
|
-
|
16
|
-
<%=
|
17
|
-
<%= form_tag request.path, remote: true do |form| %>
|
18
|
-
<%= text_field_tag :ids, "", class: "form-control" %>
|
19
|
-
|
20
|
-
<%= modal_footer do %>
|
21
|
-
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
22
|
-
<%= submit_tag "Create #{flag_title}", class: "btn btn-default btn-primary" %>
|
23
|
-
<% end %>
|
11
|
+
<%= form_for @feature, url: request.path do |form| %>
|
12
|
+
<%= form.fields_for "#{flaggable_type}_#{flag_type.pluralize}" do |flaggable_flag_form| %>
|
13
|
+
<%= render "flaggable_flag_fields", f: flaggable_flag_form %>
|
24
14
|
<% end %>
|
25
|
-
<% end %>
|
26
15
|
|
27
|
-
<%=
|
28
|
-
<p>Are you sure you want to delete <%= feature_name %> <%= flag_type.dasherize %> for <%= flaggable_class %> <span class="flaggable-identifier"></span>?</p>
|
16
|
+
<%= link_to_add_fields "Add #{flag_noun.capitalize}", form, "#{flaggable_type}_#{flag_type.pluralize}", :flaggable_flag_fields %>
|
29
17
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
<% end %>
|
18
|
+
<hr>
|
19
|
+
|
20
|
+
<%= form.submit "Update #{flag_noun.capitalize.pluralize}", class: "btn btn-primary pull-left" %>
|
34
21
|
<% end %>
|
@@ -10,12 +10,21 @@
|
|
10
10
|
|
11
11
|
<td><%= feature %></td>
|
12
12
|
|
13
|
+
<td>
|
14
|
+
<% if feature.lines.any? %>
|
15
|
+
<i class="feature-lines glyphicon glyphicon-ok" data-prefix="<%= github_prefix %>" data-lines="<%= feature.lines.join(",") %>"></i>
|
16
|
+
(<%= pluralize(feature.lines.count, "use") %>)
|
17
|
+
<% else %>
|
18
|
+
<i class="glyphicon glyphicon-ban-circle"></i>
|
19
|
+
<% end %>
|
20
|
+
</td>
|
21
|
+
|
13
22
|
<%= render partial: "detour/shared/spacer_cells", locals: { tag: "td" } %>
|
14
23
|
|
15
|
-
<% @flag_form.group_flags_for(feature).each do |
|
16
|
-
<%= feature_form.fields_for "#{params[:flaggable_type]}_group_flags_attributes[#{
|
24
|
+
<% @flag_form.group_flags_for(feature).each do |flag| %>
|
25
|
+
<%= feature_form.fields_for "#{params[:flaggable_type]}_#{flag.group_type}_group_flags_attributes[#{flag.group_name}]", flag do |group_flag_form| %>
|
17
26
|
<%= group_flag_form.hidden_field :id %>
|
18
|
-
<td><%= group_flag_form.check_box :to_keep, data: { toggle: "tooltip", placement: "top", original_title: "#{
|
27
|
+
<td><%= group_flag_form.check_box :to_keep, data: { toggle: "tooltip", placement: "top", original_title: "#{flag.group_name}" } %></td>
|
19
28
|
<% end %>
|
20
29
|
<% end %>
|
21
30
|
|
@@ -22,13 +22,18 @@
|
|
22
22
|
<tr>
|
23
23
|
<th></th>
|
24
24
|
<th>Feature</th>
|
25
|
+
<th>In Code?</th>
|
25
26
|
|
26
27
|
<%= render partial: "detour/shared/spacer_cells", locals: { tag: "th" } %>
|
27
28
|
|
28
|
-
<% @flag_form.
|
29
|
+
<% @flag_form.groups.each do |group| %>
|
29
30
|
<th>
|
30
31
|
<span class="group-header">
|
31
|
-
|
32
|
+
<% if group.is_a? Detour::Group %>
|
33
|
+
<%= link_to group, group %>
|
34
|
+
<% else %>
|
35
|
+
<%= group %>
|
36
|
+
<% end %>
|
32
37
|
</span>
|
33
38
|
</th>
|
34
39
|
<% end %>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<hr>
|
2
|
+
|
3
|
+
<div class="row">
|
4
|
+
<%= f.label :member_key, @group.flaggable_type.constantize.detour_flaggable_find_by.to_s.titleize, class: "col-sm-1" %>
|
5
|
+
|
6
|
+
<div class="col-sm-3">
|
7
|
+
<% if f.object.persisted? %>
|
8
|
+
<%= text_field_tag "member_identifier", f.object.member.send(f.object.member.class.detour_flaggable_find_by), class: "form-control", disabled: "disabled" %>
|
9
|
+
<% else %>
|
10
|
+
<%= f.text_field :member_key, class: "form-control" %>
|
11
|
+
<% end %>
|
12
|
+
</div>
|
13
|
+
|
14
|
+
<div class="col-sm-2 checkbox">
|
15
|
+
<%= f.label :_destroy do %>
|
16
|
+
<%= f.check_box "_destroy" %> Remove member
|
17
|
+
<% end %>
|
18
|
+
</div>
|
19
|
+
</div>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<% Detour.config.flaggable_types.each do |type| %>
|
2
|
+
<h1><%= type %> Groups</h1>
|
3
|
+
|
4
|
+
<%= content_tag :ul, class: "groups" do %>
|
5
|
+
<%= render @groups.select { |group| group.flaggable_type == type } %>
|
6
|
+
<% end %>
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<%= content_tag :span, "Create a Group", class: "btn btn-default", data: { toggle: "modal", target: "#create-group" } %>
|
10
|
+
|
11
|
+
<%= modal title: "Create a Group", id: "create-group", fade: true do %>
|
12
|
+
<%= form_for Detour::Group.new, remote: true do |form| %>
|
13
|
+
<%= form.text_field :name, class: "form-control", placeholder: "Group Name" %>
|
14
|
+
<%= form.select :flaggable_type, Detour.config.flaggable_types, class: "form-control" %>
|
15
|
+
|
16
|
+
<%= modal_footer do %>
|
17
|
+
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
18
|
+
<%= form.submit "Create Group", class: "btn btn-primary" %>
|
19
|
+
<% end %>
|
20
|
+
<% end %>
|
21
|
+
<% end %>
|
@@ -0,0 +1,41 @@
|
|
1
|
+
<h1><%= @group %></h1>
|
2
|
+
|
3
|
+
<% if @group.errors.any? %>
|
4
|
+
<ul>
|
5
|
+
<% @group.errors.full_messages.each do |msg| %>
|
6
|
+
<li><%= msg %></li>
|
7
|
+
<% end %>
|
8
|
+
</ul>
|
9
|
+
<% end %>
|
10
|
+
|
11
|
+
<%= form_for @group do |form| %>
|
12
|
+
<div class="row">
|
13
|
+
<%= form.label :name, class: "control-label col-sm-1" %>
|
14
|
+
|
15
|
+
<div class="col-sm-3">
|
16
|
+
<%= form.text_field :name, class: "form-control" %>
|
17
|
+
</div>
|
18
|
+
</div>
|
19
|
+
|
20
|
+
<%= form.fields_for :memberships do |membership_form| %>
|
21
|
+
<%= render "membership_fields", f: membership_form %>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
<%= link_to_add_fields "Add Member", form, :memberships %>
|
25
|
+
|
26
|
+
|
27
|
+
<hr>
|
28
|
+
|
29
|
+
<%= form.submit class: "btn btn-primary pull-left" %>
|
30
|
+
<% end %>
|
31
|
+
|
32
|
+
<%= button_to "Delete Group", group_path(@group), class: "btn btn-danger delete-group", method: :delete %>
|
33
|
+
|
34
|
+
<%= modal title: "Delete Group", id: "delete-group" do %>
|
35
|
+
<p>Are you sure you want to delete <%= @group.name %>?</p>
|
36
|
+
|
37
|
+
<%= modal_footer do %>
|
38
|
+
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
39
|
+
<%= link_to "Delete Group", group_path(@group), method: :delete, class: "btn btn-danger" %>
|
40
|
+
<% end %>
|
41
|
+
<% end %>
|
@@ -1,9 +1,9 @@
|
|
1
1
|
<div class="panel panel-danger">
|
2
|
-
<div class="panel-heading">Whoops! There were some errors saving your
|
2
|
+
<div class="panel-heading">Whoops! There were some errors saving your <%= model.class.model_name.singular %>:</div>
|
3
3
|
|
4
4
|
<div class="panel-body">
|
5
5
|
<ul>
|
6
|
-
<%
|
6
|
+
<% model.errors.full_messages.each do |msg| %>
|
7
7
|
<li><%= msg %></li>
|
8
8
|
<% end %>
|
9
9
|
</ul>
|
@@ -13,6 +13,7 @@
|
|
13
13
|
|
14
14
|
<div class="collapse navbar-collapse navbar-right" id="feature-flags-nav">
|
15
15
|
<ul class="nav navbar-nav">
|
16
|
+
<li><%= link_to "Groups", groups_path %></li>
|
16
17
|
<li class="dropdown">
|
17
18
|
<a href="javascript:void(0)" data-toggle="dropdown">Feature Flags <b class="caret"></b></a>
|
18
19
|
|
@@ -0,0 +1,5 @@
|
|
1
|
+
if ($(".modal-body:visible .panel-danger").length) {
|
2
|
+
$(".modal-body:visible .panel-danger").replaceWith("<%= j render partial: "detour/shared/errors", locals: { model: @model } %>");
|
3
|
+
} else {
|
4
|
+
$(".modal-body:visible").prepend("<%= j render partial: "detour/shared/errors", locals: { model: @model } %>");
|
5
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Sample localization file for English. Add more files in this directory for other locales.
|
2
|
+
# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
|
3
|
+
|
4
|
+
en:
|
5
|
+
activerecord:
|
6
|
+
errors:
|
7
|
+
models:
|
8
|
+
detour/feature:
|
9
|
+
attributes:
|
10
|
+
name:
|
11
|
+
invalid: "must be composed of letters, numbers, underscores, and dashes"
|
data/config/routes.rb
CHANGED
@@ -3,14 +3,15 @@ Detour::Engine.routes.draw do
|
|
3
3
|
post "/flags/:flaggable_type" => "flags#update"
|
4
4
|
|
5
5
|
resources :features, only: [:create, :destroy]
|
6
|
+
resources :groups, only: [:index, :show, :create, :update, :destroy]
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
8
|
+
%w[flag-ins opt-outs].each do |flag_type|
|
9
|
+
scope "/#{flag_type}/:feature_name" do
|
10
|
+
get ":flaggable_type" => "flaggable_flags#index", as: "#{flag_type.underscore.singularize}_flags"
|
11
|
+
put ":flaggable_type" => "flaggable_flags#update"
|
12
|
+
delete ":flaggable_type/:id" => "flaggable_flags#destroy", as: "#{flag_type.underscore.singularize}_flag"
|
13
|
+
end
|
14
|
+
end
|
14
15
|
|
15
16
|
root to: "application#index"
|
16
17
|
end
|
data/detour.gemspec
CHANGED
@@ -24,6 +24,7 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_development_dependency "capybara"
|
25
25
|
spec.add_development_dependency "database_cleaner", "~> 1.2.0"
|
26
26
|
spec.add_development_dependency "factory_girl_rails"
|
27
|
+
spec.add_development_dependency "launchy"
|
27
28
|
spec.add_development_dependency "poltergeist"
|
28
29
|
spec.add_development_dependency "pry-nav"
|
29
30
|
spec.add_development_dependency "rspec-rails"
|
@@ -23,14 +23,23 @@ module Detour::ActsAsFlaggable
|
|
23
23
|
update_only: true,
|
24
24
|
reject_if: proc { |attrs| attrs[:percentage].blank? }
|
25
25
|
|
26
|
-
has_many :#{table_name}
|
27
|
-
class_name: "Detour::
|
26
|
+
has_many :#{table_name}_defined_group_flags,
|
27
|
+
class_name: "Detour::DefinedGroupFlag",
|
28
28
|
inverse_of: :feature,
|
29
29
|
dependent: :destroy,
|
30
30
|
conditions: { flaggable_type: "#{self}" }
|
31
31
|
|
32
|
-
attr_accessible :#{table_name}
|
33
|
-
accepts_nested_attributes_for :#{table_name}
|
32
|
+
attr_accessible :#{table_name}_defined_group_flags_attributes
|
33
|
+
accepts_nested_attributes_for :#{table_name}_defined_group_flags, allow_destroy: true
|
34
|
+
|
35
|
+
has_many :#{table_name}_database_group_flags,
|
36
|
+
class_name: "Detour::DatabaseGroupFlag",
|
37
|
+
inverse_of: :feature,
|
38
|
+
dependent: :destroy,
|
39
|
+
conditions: { flaggable_type: "#{self}" }
|
40
|
+
|
41
|
+
attr_accessible :#{table_name}_database_group_flags_attributes
|
42
|
+
accepts_nested_attributes_for :#{table_name}_database_group_flags, allow_destroy: true
|
34
43
|
|
35
44
|
has_many :#{table_name}_flag_ins,
|
36
45
|
class_name: "Detour::FlagInFlag",
|
@@ -38,11 +47,17 @@ module Detour::ActsAsFlaggable
|
|
38
47
|
dependent: :destroy,
|
39
48
|
conditions: { flaggable_type: "#{self}" }
|
40
49
|
|
50
|
+
attr_accessible :#{table_name}_flag_ins_attributes
|
51
|
+
accepts_nested_attributes_for :#{table_name}_flag_ins, allow_destroy: true
|
52
|
+
|
41
53
|
has_many :#{table_name}_opt_outs,
|
42
54
|
class_name: "Detour::OptOutFlag",
|
43
55
|
inverse_of: :feature,
|
44
56
|
dependent: :destroy,
|
45
57
|
conditions: { flaggable_type: "#{self}" }
|
58
|
+
|
59
|
+
attr_accessible :#{table_name}_opt_outs_attributes
|
60
|
+
accepts_nested_attributes_for :#{table_name}_opt_outs, allow_destroy: true
|
46
61
|
EOF
|
47
62
|
|
48
63
|
class_eval do
|
data/lib/detour/configuration.rb
CHANGED
@@ -30,6 +30,6 @@ class Detour::Configuration
|
|
30
30
|
|
31
31
|
def define_group_for_class(klass, group_name, &block)
|
32
32
|
@defined_groups[klass] ||= {}
|
33
|
-
@defined_groups[klass][group_name] = block
|
33
|
+
@defined_groups[klass][group_name] = Detour::DefinedGroup.new(group_name, block)
|
34
34
|
end
|
35
35
|
end
|
data/lib/detour/flag_form.rb
CHANGED
@@ -1,28 +1,25 @@
|
|
1
1
|
class Detour::FlagForm
|
2
2
|
def initialize(flaggable_type)
|
3
|
-
@flaggable_type = flaggable_type
|
3
|
+
@flaggable_type = flaggable_type.classify.constantize
|
4
4
|
end
|
5
5
|
|
6
6
|
def features
|
7
|
-
@features ||= Detour::Feature.includes("#{
|
7
|
+
@features ||= Detour::Feature.includes("#{flaggable_collection}_percentage_flag", "#{flaggable_collection}_database_group_flags", "#{flaggable_collection}_defined_group_flags").with_lines
|
8
8
|
end
|
9
9
|
|
10
10
|
def errors?
|
11
11
|
features.any? { |feature| feature.errors.any? }
|
12
12
|
end
|
13
13
|
|
14
|
-
def
|
15
|
-
@
|
16
|
-
all_names = features.collect { |feature| feature.send("#{@flaggable_type}_group_flags").collect(&:group_name) }.uniq.flatten
|
17
|
-
defined_group_names = Detour.config.defined_groups.fetch(@flaggable_type.classify, {}).keys.map(&:to_s)
|
18
|
-
(all_names | defined_group_names).sort
|
19
|
-
end
|
14
|
+
def groups
|
15
|
+
@groups ||= (database_groups + defined_groups).sort_by { |group| group.name.downcase }
|
20
16
|
end
|
21
17
|
|
22
|
-
def group_flags_for(feature,
|
23
|
-
|
24
|
-
flags
|
25
|
-
|
18
|
+
def group_flags_for(feature, types = %w[defined database])
|
19
|
+
Array.wrap(types).inject([]) do |flags, type|
|
20
|
+
flags.concat _group_flags_for(feature, type)
|
21
|
+
end.sort_by do |flag|
|
22
|
+
flag.group.name.downcase
|
26
23
|
end
|
27
24
|
end
|
28
25
|
|
@@ -33,7 +30,7 @@ class Detour::FlagForm
|
|
33
30
|
next unless feature_params
|
34
31
|
|
35
32
|
check_percentage_flag_for_deletion(feature, feature_params)
|
36
|
-
|
33
|
+
process_group_flags(feature, feature_params)
|
37
34
|
|
38
35
|
feature.assign_attributes feature_params
|
39
36
|
feature.save if feature.changed_for_autosave?
|
@@ -49,14 +46,29 @@ class Detour::FlagForm
|
|
49
46
|
|
50
47
|
private
|
51
48
|
|
49
|
+
def _group_flags_for(feature, type)
|
50
|
+
send("#{type}_groups").map do |group|
|
51
|
+
flags = feature.send("#{flaggable_collection}_#{type}_group_flags")
|
52
|
+
if flag = flags.detect { |flag| flag.group.id == group.id }
|
53
|
+
next flag
|
54
|
+
else
|
55
|
+
if type == "database"
|
56
|
+
flags.new(group_id: group.id)
|
57
|
+
else
|
58
|
+
flags.new(group_name: group.name)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
52
64
|
def check_percentage_flag_for_deletion(feature, params)
|
53
|
-
key = :"#{
|
54
|
-
flag = feature.send("#{
|
65
|
+
key = :"#{flaggable_collection}_percentage_flag_attributes"
|
66
|
+
flag = feature.send("#{flaggable_collection}_percentage_flag")
|
55
67
|
flag_params = params[key]
|
56
68
|
|
57
69
|
if flag.present? && flag_params[:percentage].blank?
|
58
|
-
feature.send("#{
|
59
|
-
feature.send("#{
|
70
|
+
feature.send("#{flaggable_collection}_percentage_flag").mark_for_destruction
|
71
|
+
feature.send("#{flaggable_collection}_percentage_flag=", nil)
|
60
72
|
end
|
61
73
|
|
62
74
|
if flag.present? && flag_params[:percentage].to_i == flag.percentage
|
@@ -64,23 +76,30 @@ class Detour::FlagForm
|
|
64
76
|
end
|
65
77
|
end
|
66
78
|
|
67
|
-
def
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
79
|
+
def database_groups
|
80
|
+
@database_groups ||= Detour::Group.where(flaggable_type: @flaggable_type)
|
81
|
+
end
|
82
|
+
|
83
|
+
def defined_groups
|
84
|
+
@defined_groups ||= begin
|
85
|
+
(Detour::DefinedGroupFlag.where(flaggable_type: @flaggable_type).map { |flag|
|
86
|
+
flag.group
|
87
|
+
} + Detour::DefinedGroup.by_type(@flaggable_type).values).uniq(&:name)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def flaggable_collection
|
92
|
+
@flaggable_type.table_name
|
93
|
+
end
|
94
|
+
|
95
|
+
def process_group_flags(feature, params)
|
96
|
+
%w[defined database].each do |type|
|
97
|
+
key = :"#{flaggable_collection}_#{type}_group_flags_attributes"
|
98
|
+
flags_params = params[key] || {}
|
99
|
+
params.delete key
|
100
|
+
|
101
|
+
group_flags_for(feature, type).each do |flag|
|
102
|
+
flag.keep_or_destroy(flags_params[flag.group_name])
|
84
103
|
end
|
85
104
|
end
|
86
105
|
end
|
data/lib/detour/flaggable.rb
CHANGED
@@ -12,17 +12,7 @@ module Detour::Flaggable
|
|
12
12
|
# Returns whether or not the object has access to the given feature. If given
|
13
13
|
# a block, it will call the block if the user has access to the feature.
|
14
14
|
#
|
15
|
-
# If an exception is raised in the block, it will increment the
|
16
|
-
# `failure_count` of the feature and raise the exception.
|
17
|
-
#
|
18
|
-
# @example
|
19
|
-
# # Exceptions will be tracked in the `failure_count` of :new_user_interface.
|
20
|
-
# user.has_feature?(:new_user_interface) do
|
21
|
-
# # ...
|
22
|
-
# end
|
23
|
-
#
|
24
15
|
# @example
|
25
|
-
# # Exceptions will *not* be tracked in the `failure_count` of :new_user_interface.
|
26
16
|
# if user.has_feature?(:new_user_interface)
|
27
17
|
# # ...
|
28
18
|
# end
|
@@ -48,15 +38,6 @@ module Detour::Flaggable
|
|
48
38
|
end
|
49
39
|
end
|
50
40
|
|
51
|
-
if match && block_given?
|
52
|
-
begin
|
53
|
-
yield
|
54
|
-
rescue => e
|
55
|
-
feature.increment! :failure_count
|
56
|
-
raise e
|
57
|
-
end
|
58
|
-
end
|
59
|
-
|
60
41
|
match
|
61
42
|
end
|
62
43
|
|
data/lib/detour/version.rb
CHANGED
@@ -2,7 +2,6 @@ class SetupDetour < ActiveRecord::Migration
|
|
2
2
|
def change
|
3
3
|
create_table :detour_features do |t|
|
4
4
|
t.string :name
|
5
|
-
t.integer :failure_count, default: 0
|
6
5
|
t.text :flag_in_counts, default: "{}"
|
7
6
|
t.text :opt_out_counts, default: "{}"
|
8
7
|
t.timestamps
|
@@ -15,6 +14,7 @@ class SetupDetour < ActiveRecord::Migration
|
|
15
14
|
t.integer :feature_id
|
16
15
|
t.integer :flaggable_id
|
17
16
|
t.string :flaggable_type
|
17
|
+
t.integer :group_id
|
18
18
|
t.string :group_name
|
19
19
|
t.integer :percentage
|
20
20
|
t.timestamps
|
@@ -22,11 +22,31 @@ class SetupDetour < ActiveRecord::Migration
|
|
22
22
|
|
23
23
|
add_index :detour_flags, :type
|
24
24
|
add_index :detour_flags, :feature_id
|
25
|
+
add_index :detour_flags, :group_id
|
26
|
+
add_index :detour_flags,
|
27
|
+
[:type, :feature_id, :group_id]
|
25
28
|
add_index :detour_flags,
|
26
29
|
[:type, :feature_id, :flaggable_type, :flaggable_id],
|
27
30
|
name: "flag_type_feature_flaggable_type_id"
|
28
31
|
add_index :detour_flags,
|
29
32
|
[:type, :feature_id, :flaggable_type],
|
30
33
|
name: "flag_type_feature_flaggable_type"
|
34
|
+
|
35
|
+
create_table :detour_groups do |t|
|
36
|
+
t.string :name
|
37
|
+
t.string :flaggable_type
|
38
|
+
t.timestamps
|
39
|
+
end
|
40
|
+
|
41
|
+
create_table :detour_memberships do |t|
|
42
|
+
t.integer :group_id
|
43
|
+
t.string :member_type
|
44
|
+
t.integer :member_id
|
45
|
+
t.timestamps
|
46
|
+
end
|
47
|
+
|
48
|
+
add_index :detour_memberships, [:group_id, :member_type, :member_id],
|
49
|
+
name: :detour_memberships_membership_index,
|
50
|
+
unique: true
|
31
51
|
end
|
32
52
|
end
|
data/lib/tasks/.gitkeep
ADDED
File without changes
|