detour 0.0.3 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +2 -25
  3. data/app/assets/javascripts/detour/add_fields.js +7 -0
  4. data/app/assets/javascripts/detour/delete_feature.js +9 -11
  5. data/app/assets/javascripts/detour/delete_flag.js +9 -11
  6. data/app/assets/javascripts/detour/delete_group.js +5 -0
  7. data/app/assets/javascripts/detour/feature_lines.js +23 -0
  8. data/app/assets/javascripts/detour/modals.js +1 -0
  9. data/app/assets/stylesheets/detour/main.css +12 -0
  10. data/app/controllers/detour/features_controller.rb +3 -2
  11. data/app/controllers/detour/flaggable_flags_controller.rb +9 -77
  12. data/app/controllers/detour/groups_controller.rb +39 -0
  13. data/app/helpers/detour/application_helper.rb +11 -0
  14. data/app/helpers/detour/flaggable_flags_helper.rb +20 -0
  15. data/app/helpers/detour/flags_helper.rb +5 -1
  16. data/app/models/detour/concerns/keepable.rb +21 -0
  17. data/app/models/detour/concerns/matchers.rb +28 -9
  18. data/app/models/detour/database_group_flag.rb +30 -0
  19. data/app/models/detour/defined_group.rb +21 -0
  20. data/app/models/detour/defined_group_flag.rb +28 -0
  21. data/app/models/detour/feature.rb +4 -3
  22. data/app/models/detour/flag_in_flag.rb +1 -8
  23. data/app/models/detour/flaggable_flag.rb +24 -0
  24. data/app/models/detour/group.rb +17 -0
  25. data/app/models/detour/membership.rb +41 -0
  26. data/app/models/detour/opt_out_flag.rb +1 -8
  27. data/app/views/detour/flaggable_flags/_flaggable_flag_fields.html.erb +19 -0
  28. data/app/views/detour/flaggable_flags/index.html.erb +13 -26
  29. data/app/views/detour/flags/_feature_form.html.erb +12 -3
  30. data/app/views/detour/flags/index.html.erb +7 -2
  31. data/app/views/detour/groups/_group.html.erb +3 -0
  32. data/app/views/detour/groups/_membership_fields.html.erb +19 -0
  33. data/app/views/detour/groups/index.html.erb +21 -0
  34. data/app/views/detour/groups/show.html.erb +41 -0
  35. data/app/views/detour/memberships/_membership.html.erb +4 -0
  36. data/app/views/detour/{features → shared}/_errors.html.erb +2 -2
  37. data/app/views/detour/shared/_nav.html.erb +1 -0
  38. data/app/views/detour/shared/error.js.erb +5 -0
  39. data/config/locales/en.yml +11 -0
  40. data/config/routes.rb +8 -7
  41. data/detour.gemspec +1 -0
  42. data/lib/detour/acts_as_flaggable.rb +19 -4
  43. data/lib/detour/configuration.rb +1 -1
  44. data/lib/detour/flag_form.rb +53 -34
  45. data/lib/detour/flaggable.rb +0 -19
  46. data/lib/detour/version.rb +1 -1
  47. data/lib/generators/templates/migration.rb +21 -1
  48. data/lib/tasks/.gitkeep +0 -0
  49. data/spec/controllers/detour/flaggable_flags_controller_spec.rb +30 -67
  50. data/spec/controllers/detour/groups_controller_spec.rb +107 -0
  51. data/spec/dummy/db/migrate/20131221052201_setup_detour.rb +21 -1
  52. data/spec/dummy/db/schema.rb +20 -1
  53. data/spec/factories/database_group_flag.rb +7 -0
  54. data/spec/factories/{group_flag.rb → defined_group_flag.rb} +1 -1
  55. data/spec/factories/group.rb +10 -0
  56. data/spec/factories/membership.rb +6 -0
  57. data/spec/features/database_group_flags_spec.rb +50 -0
  58. data/spec/features/database_groups_spec.rb +174 -0
  59. data/spec/features/defined_group_flags_spec.rb +67 -0
  60. data/spec/features/features_spec.rb +44 -0
  61. data/spec/features/flag_in_flags_spec.rb +22 -60
  62. data/spec/features/opt_out_flags_spec.rb +34 -59
  63. data/spec/integration/group_rollout_spec.rb +2 -2
  64. data/spec/lib/detour/acts_as_flaggable_spec.rb +12 -3
  65. data/spec/lib/detour/configuration_spec.rb +6 -2
  66. data/spec/lib/detour/flag_form_spec.rb +0 -11
  67. data/spec/lib/detour/flaggable_spec.rb +1 -54
  68. data/spec/models/detour/database_group_flag_spec.rb +29 -0
  69. data/spec/models/detour/defined_group_spec.rb +21 -0
  70. data/spec/models/detour/feature_spec.rb +57 -119
  71. data/spec/models/detour/flag_in_flag_spec.rb +1 -4
  72. data/spec/models/detour/flaggable_flag_spec.rb +25 -0
  73. data/spec/models/detour/group_flag_spec.rb +1 -1
  74. data/spec/models/detour/membership_spec.rb +58 -0
  75. data/spec/models/detour/opt_out_flag_spec.rb +1 -4
  76. data/spec/spec_helper.rb +4 -0
  77. metadata +97 -81
  78. data/app/models/detour/concerns/flag_actions.rb +0 -141
  79. data/app/models/detour/group_flag.rb +0 -13
  80. data/app/views/detour/features/_success.html.erb +0 -1
  81. data/app/views/detour/features/error.js.erb +0 -5
  82. data/lib/tasks/detour.rake +0 -119
  83. data/spec/features/group_flags_spec.rb +0 -49
  84. data/spec/integration/flag_rollout_spec.rb +0 -27
  85. data/spec/lib/tasks/detour_spec.rb +0 -162
  86. /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::Flag
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
- <%= table do %>
4
- <thead>
5
- <tr>
6
- <th></th>
7
- <th><%= flaggable_class.detour_flaggable_find_by %></th>
8
- </tr>
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
- <%= content_tag :span, "Create #{flag_title.indefinitize}", class: "btn btn-default pull-left", data: { toggle: "modal", target: "#create-flaggable-flag" } %>
15
-
16
- <%= modal title: "Create #{flag_title.indefinitize}", id: "create-flaggable-flag", fade: true do %>
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
- <%= modal title: "Delete #{flag_title}", id: "delete-flag" do %>
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
- <%= modal_footer do %>
31
- <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
32
- <%= link_to "Delete #{flag_title}", "javascript:void(0)", method: :delete, class: "btn btn-danger" %>
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 |group_flag| %>
16
- <%= feature_form.fields_for "#{params[:flaggable_type]}_group_flags_attributes[#{group_flag.group_name}]", group_flag do |group_flag_form| %>
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: "#{group_flag.group_name}" } %></td>
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.group_names.each do |group_name| %>
29
+ <% @flag_form.groups.each do |group| %>
29
30
  <th>
30
31
  <span class="group-header">
31
- <%= group_name %>
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,3 @@
1
+ <%= content_tag_for :li, group do %>
2
+ <%= link_to group, group %>
3
+ <% 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 %>
@@ -0,0 +1,4 @@
1
+ <%= content_tag_for :tr, membership do %>
2
+ <td><%= membership.member.class %></td>
3
+ <td><%= membership.member.send membership.member.class.detour_flaggable_find_by %></td>
4
+ <% end %>
@@ -1,9 +1,9 @@
1
1
  <div class="panel panel-danger">
2
- <div class="panel-heading">Whoops! There were some errors saving your feature:</div>
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
- <% @feature.errors.full_messages.each do |msg| %>
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
- get "/flag-ins/:feature_name/:flaggable_type" => "flaggable_flags#index", as: "flag_in_flags"
8
- post "/flag-ins/:feature_name/:flaggable_type" => "flaggable_flags#create"
9
- delete "/flag-ins/:feature_name/:flaggable_type/:id" => "flaggable_flags#destroy", as: "flag_in_flag"
10
-
11
- get "/opt-outs/:feature_name/:flaggable_type" => "flaggable_flags#index", as: "opt_out_flags"
12
- post "/opt-outs/:feature_name/:flaggable_type" => "flaggable_flags#create"
13
- delete "/opt-outs/:feature_name/:flaggable_type/:id" => "flaggable_flags#destroy", as: "opt_out_flag"
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}_group_flags,
27
- class_name: "Detour::GroupFlag",
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}_group_flags_attributes
33
- accepts_nested_attributes_for :#{table_name}_group_flags, allow_destroy: true
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
@@ -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
@@ -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("#{@flaggable_type}_percentage_flag", "#{@flaggable_type}_group_flags").with_lines
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 group_names
15
- @group_names ||= begin
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, initialize = true)
23
- group_names.map do |group_name|
24
- flags = feature.send("#{@flaggable_type}_group_flags")
25
- flags.detect { |flag| flag.group_name == group_name } || (flags.new(group_name: group_name) if initialize)
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
- set_group_flag_params(feature, feature_params)
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 = :"#{@flaggable_type}_percentage_flag_attributes"
54
- flag = feature.send("#{@flaggable_type}_percentage_flag")
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("#{@flaggable_type}_percentage_flag").mark_for_destruction
59
- feature.send("#{@flaggable_type}_percentage_flag=", nil)
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 set_group_flag_params(feature, params)
68
- key = :"#{@flaggable_type}_group_flags_attributes"
69
- flags_params = params[key]
70
- params.delete key
71
-
72
- group_names.zip(group_flags_for(feature, false)).each do |name, flag|
73
- flag_params = flags_params[name]
74
- to_keep = flag_params["to_keep"] == "1"
75
- flags_params.delete name
76
-
77
- if flag && to_keep
78
- flag.to_keep = true
79
- elsif flag && !to_keep
80
- flag.mark_for_destruction
81
- elsif !flag && to_keep
82
- flag = feature.send("#{@flaggable_type}_group_flags").new group_name: name
83
- flag.to_keep = true
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
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module Detour
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.5"
3
3
  end
@@ -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
File without changes