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.
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