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.
Files changed (147) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -1
  3. data/.travis.yml +6 -2
  4. data/Gemfile +5 -0
  5. data/README.md +41 -132
  6. data/Rake.md +108 -0
  7. data/Rakefile +21 -1
  8. data/app/assets/fonts/glyphicons-halflings-regular.eot +0 -0
  9. data/app/assets/fonts/glyphicons-halflings-regular.svg +229 -0
  10. data/app/assets/fonts/glyphicons-halflings-regular.ttf +0 -0
  11. data/app/assets/fonts/glyphicons-halflings-regular.woff +0 -0
  12. data/app/assets/images/detour/.gitkeep +0 -0
  13. data/app/assets/javascripts/detour/application.js +16 -0
  14. data/app/assets/javascripts/detour/bootstrap.js +2006 -0
  15. data/app/assets/javascripts/detour/delete_feature.js +13 -0
  16. data/app/assets/javascripts/detour/delete_flag.js +13 -0
  17. data/app/assets/javascripts/detour/modals.js +3 -0
  18. data/app/assets/javascripts/detour/tooltips.js +3 -0
  19. data/app/assets/stylesheets/detour/application.css +14 -0
  20. data/app/assets/stylesheets/detour/bootstrap.css +7112 -0
  21. data/app/assets/stylesheets/detour/main.css +49 -0
  22. data/app/controllers/detour/application_controller.rb +12 -0
  23. data/app/controllers/detour/features_controller.rb +19 -0
  24. data/app/controllers/detour/flaggable_flags_controller.rb +90 -0
  25. data/app/controllers/detour/flags_controller.rb +18 -0
  26. data/app/helpers/detour/application_helper.rb +32 -0
  27. data/app/helpers/detour/flaggable_flags_helper.rb +5 -0
  28. data/app/helpers/detour/flags_helper.rb +7 -0
  29. data/app/models/detour/concerns/countable_flag.rb +19 -0
  30. data/app/models/detour/concerns/flag_actions.rb +141 -0
  31. data/app/models/detour/concerns/matchers.rb +73 -0
  32. data/app/models/detour/feature.rb +81 -0
  33. data/{lib → app/models}/detour/flag.rb +8 -2
  34. data/app/models/detour/flag_in_flag.rb +12 -0
  35. data/app/models/detour/group_flag.rb +13 -0
  36. data/{lib → app/models}/detour/opt_out_flag.rb +4 -2
  37. data/app/models/detour/percentage_flag.rb +9 -0
  38. data/app/views/detour/application/index.html.erb +0 -0
  39. data/app/views/detour/features/_errors.html.erb +11 -0
  40. data/app/views/detour/features/_success.html.erb +1 -0
  41. data/app/views/detour/features/error.js.erb +5 -0
  42. data/app/views/detour/features/success.js.erb +1 -0
  43. data/app/views/detour/flaggable_flags/_errors.html.erb +11 -0
  44. data/app/views/detour/flaggable_flags/_flaggable_flag.html.erb +11 -0
  45. data/app/views/detour/flaggable_flags/error.js.erb +5 -0
  46. data/app/views/detour/flaggable_flags/index.html.erb +34 -0
  47. data/app/views/detour/flaggable_flags/success.js.erb +1 -0
  48. data/app/views/detour/flags/_feature_form.html.erb +38 -0
  49. data/app/views/detour/flags/index.html.erb +76 -0
  50. data/app/views/detour/shared/_nav.html.erb +28 -0
  51. data/app/views/detour/shared/_spacer_cells.html.erb +3 -0
  52. data/app/views/layouts/detour/application.html.erb +29 -0
  53. data/config/routes.rb +16 -0
  54. data/detour.gemspec +15 -14
  55. data/lib/detour/acts_as_flaggable.rb +42 -3
  56. data/lib/detour/configuration.rb +35 -0
  57. data/lib/detour/engine.rb +5 -0
  58. data/lib/detour/flag_form.rb +87 -0
  59. data/lib/detour/version.rb +1 -1
  60. data/lib/detour.rb +10 -14
  61. data/lib/generators/templates/migration.rb +2 -0
  62. data/lib/tasks/detour.rake +16 -16
  63. data/script/rails +8 -0
  64. data/spec/controllers/detour/application_controller_spec.rb +15 -0
  65. data/spec/controllers/detour/features_controller_spec.rb +51 -0
  66. data/spec/controllers/detour/flaggable_flags_controller_spec.rb +100 -0
  67. data/spec/controllers/detour/flags_controller_spec.rb +77 -0
  68. data/spec/dummy/README.rdoc +261 -0
  69. data/spec/dummy/Rakefile +7 -0
  70. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  71. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  72. data/spec/dummy/app/controllers/application_controller.rb +7 -0
  73. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  74. data/spec/dummy/app/mailers/.gitkeep +0 -0
  75. data/spec/dummy/app/models/.gitkeep +0 -0
  76. data/spec/dummy/app/models/user.rb +5 -0
  77. data/spec/dummy/app/models/widget.rb +5 -0
  78. data/spec/dummy/app/views/application/index.html.erb +22 -0
  79. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  80. data/spec/dummy/config/application.rb +65 -0
  81. data/spec/dummy/config/boot.rb +10 -0
  82. data/spec/dummy/config/database.yml +19 -0
  83. data/spec/dummy/config/environment.rb +5 -0
  84. data/spec/dummy/config/environments/development.rb +37 -0
  85. data/spec/dummy/config/environments/production.rb +67 -0
  86. data/spec/dummy/config/environments/test.rb +37 -0
  87. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  88. data/spec/dummy/config/initializers/detour.rb +36 -0
  89. data/spec/dummy/config/initializers/inflections.rb +15 -0
  90. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  91. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  92. data/spec/dummy/config/initializers/session_store.rb +8 -0
  93. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  94. data/spec/dummy/config/locales/en.yml +5 -0
  95. data/spec/dummy/config/routes.rb +4 -0
  96. data/spec/dummy/config.ru +4 -0
  97. data/spec/dummy/db/migrate/20131218015844_create_users.rb +10 -0
  98. data/spec/dummy/db/migrate/20131218023124_create_widgets.rb +9 -0
  99. data/spec/dummy/db/migrate/20131218055352_add_user_id_to_widgets.rb +6 -0
  100. data/spec/dummy/db/migrate/20131221052201_setup_detour.rb +32 -0
  101. data/spec/dummy/db/schema.rb +59 -0
  102. data/spec/dummy/db/seeds.rb +10 -0
  103. data/spec/dummy/lib/assets/.gitkeep +0 -0
  104. data/spec/dummy/log/.gitkeep +0 -0
  105. data/spec/dummy/public/404.html +26 -0
  106. data/spec/dummy/public/422.html +26 -0
  107. data/spec/dummy/public/500.html +25 -0
  108. data/spec/dummy/public/favicon.ico +0 -0
  109. data/spec/dummy/script/rails +6 -0
  110. data/spec/factories/feature.rb +5 -0
  111. data/spec/factories/flag_in_flag.rb +6 -0
  112. data/spec/factories/group_flag.rb +7 -0
  113. data/spec/factories/opt_out_flag.rb +6 -0
  114. data/spec/factories/percentage_flag.rb +7 -0
  115. data/spec/factories/user.rb +6 -0
  116. data/spec/factories/widget.rb +5 -0
  117. data/spec/features/features_spec.rb +70 -0
  118. data/spec/features/flag_in_flags_spec.rb +118 -0
  119. data/spec/features/group_flags_spec.rb +49 -0
  120. data/spec/features/home_page_spec.rb +11 -0
  121. data/spec/features/opt_out_flags_spec.rb +105 -0
  122. data/spec/features/percentage_flags_spec.rb +63 -0
  123. data/spec/integration/group_rollout_spec.rb +1 -1
  124. data/spec/lib/detour/acts_as_flaggable_spec.rb +45 -0
  125. data/spec/lib/detour/configuration_spec.rb +23 -0
  126. data/spec/lib/detour/flag_form_spec.rb +84 -0
  127. data/spec/lib/{active_record/rollout → detour}/flaggable_spec.rb +19 -19
  128. data/spec/lib/tasks/{detour_rake_spec.rb → detour_spec.rb} +54 -54
  129. data/spec/{lib/active_record/rollout → models/detour}/feature_spec.rb +93 -67
  130. data/spec/models/detour/flag_in_flag_spec.rb +38 -0
  131. data/spec/{lib/active_record/rollout → models/detour}/flag_spec.rb +1 -1
  132. data/spec/models/detour/opt_out_flag_spec.rb +38 -0
  133. data/spec/{lib/active_record/rollout → models/detour}/percentage_flag_spec.rb +1 -1
  134. data/spec/spec_helper.rb +41 -21
  135. data/spec/support/shared_contexts/rake.rb +4 -14
  136. metadata +278 -95
  137. data/lib/detour/feature.rb +0 -312
  138. data/lib/detour/flaggable_flag.rb +0 -10
  139. data/lib/detour/group_flag.rb +0 -8
  140. data/lib/detour/percentage_flag.rb +0 -11
  141. data/spec/lib/active_record/rollout/acts_as_flaggable_spec.rb +0 -31
  142. data/spec/lib/active_record/rollout/flaggable_flag_spec.rb +0 -9
  143. data/spec/lib/active_record/rollout/opt_out_flag_spec.rb +0 -9
  144. data/spec/support/schema.rb +0 -13
  145. /data/lib/generators/{active_record_rollout_generator.rb → detour_generator.rb} +0 -0
  146. /data/lib/generators/templates/{active_record_rollout.rb → detour.rb} +0 -0
  147. /data/spec/{lib/active_record/rollout → models/detour}/group_flag_spec.rb +0 -0
@@ -0,0 +1,11 @@
1
+ <tr>
2
+ <td>
3
+ <button type="button" class="btn btn-danger btn-xs delete-flag" data-path="<%= send("#{flag_type}_flag_path", feature_name, flaggable_type, flag.id) %>">
4
+ <span class="glyphicon glyphicon-remove"></span>
5
+ </button>
6
+ </td>
7
+
8
+ <td>
9
+ <%= flag.flaggable.send flaggable_class.detour_flaggable_find_by %>
10
+ </td>
11
+ </tr>
@@ -0,0 +1,5 @@
1
+ if ($(".modal-body .panel-danger").length) {
2
+ $("#create-flaggable-flag .modal-body .panel-danger").replaceWith("<%= j render "errors" %>");
3
+ } else {
4
+ $("#create-flaggable-flag .modal-body").prepend("<%= j render "errors" %>");
5
+ }
@@ -0,0 +1,34 @@
1
+ <h1><%= flaggable_type.capitalize %> <%= flag_verb %> <%= feature_name %></h1>
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 %>
12
+ <% end %>
13
+
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 %>
24
+ <% end %>
25
+ <% end %>
26
+
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>
29
+
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 %>
34
+ <% end %>
@@ -0,0 +1 @@
1
+ location.reload();
@@ -0,0 +1,38 @@
1
+ <%= content_tag_for :tr, feature do %>
2
+ <%= fields_for "features[#{feature}]", feature do |feature_form| %>
3
+ <td>
4
+ <% unless feature.new_record? %>
5
+ <button type="button" class="btn btn-danger btn-xs delete-feature" data-path="<%= feature_path(feature.id, params: { flaggable_type: params[:flaggable_type] }) %>">
6
+ <span class="glyphicon glyphicon-remove"></span>
7
+ </button>
8
+ <% end %>
9
+ </td>
10
+
11
+ <td><%= feature %></td>
12
+
13
+ <%= render partial: "detour/shared/spacer_cells", locals: { tag: "td" } %>
14
+
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| %>
17
+ <%= 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>
19
+ <% end %>
20
+ <% end %>
21
+
22
+ <%= feature_form.fields_for "#{params[:flaggable_type]}_percentage_flag_attributes", feature.send("#{params[:flaggable_type]}_percentage_flag") do |percentage_flag_form| %>
23
+ <td><%= percentage_flag_form.text_field :percentage, class: "form-control input-sm" %></td>
24
+ <% end %>
25
+
26
+ <td class="flag-in-count">
27
+ <%= link_to flag_in_flags_path(feature.name, params[:flaggable_type]) do %>
28
+ <%= feature.flag_in_count_for(params[:flaggable_type]) %>
29
+ <% end %>
30
+ </td>
31
+
32
+ <td class="opt-out-count">
33
+ <%= link_to opt_out_flags_path(feature.name, params[:flaggable_type]) do %>
34
+ <%= feature.opt_out_count_for(params[:flaggable_type]) %>
35
+ <% end %>
36
+ </td>
37
+ <% end %>
38
+ <% end %>
@@ -0,0 +1,76 @@
1
+ <h1><%= params[:flaggable_type].classify %> Flags</h1>
2
+
3
+ <% if @flag_form.errors? %>
4
+ <div class="panel panel-danger">
5
+ <div class="panel-heading">Whoops! There were some errors saving your flags:</div>
6
+
7
+ <div class="panel-body">
8
+ <ul>
9
+ <% @flag_form.features.each do |feature| %>
10
+ <% feature.errors.full_messages.each do |msg| %>
11
+ <li><strong><%= feature.name %>:</strong> <%= msg %></li>
12
+ <% end %>
13
+ <% end %>
14
+ </ul>
15
+ </div>
16
+ </div>
17
+ <% end %>
18
+
19
+ <%= form_tag do %>
20
+ <%= table do %>
21
+ <thead>
22
+ <tr>
23
+ <th></th>
24
+ <th>Feature</th>
25
+
26
+ <%= render partial: "detour/shared/spacer_cells", locals: { tag: "th" } %>
27
+
28
+ <% @flag_form.group_names.each do |group_name| %>
29
+ <th>
30
+ <span class="group-header">
31
+ <%= group_name %>
32
+ </span>
33
+ </th>
34
+ <% end %>
35
+
36
+ <th class="percentage-header">Percentage</th>
37
+ <th class="counter-header">Flag-ins</th>
38
+ <th class="counter-header">Opt-outs</th>
39
+ </tr>
40
+ </thead>
41
+
42
+ <tbody>
43
+ <%= render partial: "feature_form", collection: @flag_form.features, as: :feature %>
44
+ </tbody>
45
+ <% end %>
46
+
47
+ <%= content_tag :span, "Create a Feature", class: "btn btn-default pull-left", data: { toggle: "modal", target: "#create-feature" } %>
48
+
49
+ <div class="btn-group pull-right">
50
+ <% if @flag_form.errors? %>
51
+ <%= link_to "Cancel", request.path, class: "btn btn-default" %>
52
+ <% end %>
53
+
54
+ <%= submit_tag "Save Changes", class: "btn btn-primary" %>
55
+ </div>
56
+ <% end %>
57
+
58
+ <%= modal title: "Delete Feature", id: "delete-feature" do %>
59
+ <p>Are you sure you want to delete <span class="feature-name"></span>?</p>
60
+
61
+ <%= modal_footer do %>
62
+ <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
63
+ <%= link_to "Delete Feature", "javascript:void(0)", method: :delete, class: "btn btn-danger" %>
64
+ <% end %>
65
+ <% end %>
66
+
67
+ <%= modal title: "Create a Feature", id: "create-feature", fade: true do %>
68
+ <%= form_for Detour::Feature.new, remote: true do |form| %>
69
+ <%= form.text_field :name, class: "form-control", placeholder: "feature_name" %>
70
+
71
+ <%= modal_footer do %>
72
+ <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
73
+ <%= form.submit "Create Feature", class: "btn btn-primary" %>
74
+ <% end %>
75
+ <% end %>
76
+ <% end %>
@@ -0,0 +1,28 @@
1
+ <nav class="navbar navbar-default" role="navigation">
2
+ <div class="container">
3
+ <div class="navbar-header">
4
+ <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#feature-flags-nav">
5
+ <span class="sr-only">Toggle navigation</span>
6
+ <span class="icon-bar"></span>
7
+ <span class="icon-bar"></span>
8
+ <span class="icon-bar"></span>
9
+ </button>
10
+
11
+ <%= link_to "Detour", root_path, class: "navbar-brand" %>
12
+ </div>
13
+
14
+ <div class="collapse navbar-collapse navbar-right" id="feature-flags-nav">
15
+ <ul class="nav navbar-nav">
16
+ <li class="dropdown">
17
+ <a href="javascript:void(0)" data-toggle="dropdown">Feature Flags <b class="caret"></b></a>
18
+
19
+ <ul class="dropdown-menu">
20
+ <% Detour.config.flaggable_types.each do |type| %>
21
+ <li><%= link_to "#{type} Flags", flags_path(type.tableize) %></li>
22
+ <% end %>
23
+ </ul>
24
+ </li>
25
+ </ul>
26
+ </div>
27
+ </div>
28
+ </nav>
@@ -0,0 +1,3 @@
1
+ <% spacer_count.times do %>
2
+ <<%= tag %> class="spacer-cell"></<%= tag %>>
3
+ <% end %>
@@ -0,0 +1,29 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+
7
+ <title>Detour</title>
8
+ <%= stylesheet_link_tag "detour/application", :media => "all" %>
9
+ <%= javascript_include_tag "detour/application" %>
10
+ <%= csrf_meta_tags %>
11
+ </head>
12
+ <body>
13
+
14
+ <%= render "detour/shared/nav" %>
15
+
16
+ <div class="container">
17
+ <% if flash.notice %>
18
+ <div class="alert alert-success"><%= flash.notice %></div>
19
+ <% end %>
20
+
21
+ <% if flash.alert %>
22
+ <div class="alert alert-danger"><%= flash.alert %></div>
23
+ <% end %>
24
+
25
+ <%= yield %>
26
+ </div>
27
+
28
+ </body>
29
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,16 @@
1
+ Detour::Engine.routes.draw do
2
+ get "/flags/:flaggable_type" => "flags#index", as: "flags"
3
+ post "/flags/:flaggable_type" => "flags#update"
4
+
5
+ resources :features, only: [:create, :destroy]
6
+
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"
14
+
15
+ root to: "application#index"
16
+ end
data/detour.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
- lib = File.expand_path('../lib', __FILE__)
3
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
4
  require "detour/version"
5
5
 
6
6
  Gem::Specification.new do |spec|
@@ -8,26 +8,27 @@ Gem::Specification.new do |spec|
8
8
  spec.version = Detour::VERSION
9
9
  spec.authors = ["Jonathan Clem"]
10
10
  spec.email = ["jonathan@heroku.com"]
11
- spec.summary = %q{Rollouts (feature flags) for ActiveRecord models.}
12
- spec.description = %q{Rollouts (feature flags) for ActiveRecord models.}
13
- spec.homepage = "https://github.com/jclem/detour"
11
+ spec.summary = %q{Rollouts (feature flags) for Rails.}
12
+ spec.description = %q{Rollouts (feature flags) for Rails.}
13
+ spec.homepage = "https://github.com/heroku/detour"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
- spec.require_paths = ["lib"]
17
+ spec.test_files = spec.files.grep(%r{^(spec)/})
20
18
 
21
19
  spec.required_ruby_version = ">= 1.9.3"
22
20
 
23
- spec.add_dependency "activerecord", "~> 3.2"
24
- spec.add_dependency "rails", "~> 3.2"
25
- spec.add_development_dependency "bundler", "~> 1.3"
26
- spec.add_development_dependency "rake"
21
+ spec.add_dependency "rails", "~> 3.2", ">= 3.2.16"
22
+ spec.add_dependency "indefinite_article", "~> 0.2", ">= 0.2.0"
23
+
24
+ spec.add_development_dependency "capybara"
25
+ spec.add_development_dependency "database_cleaner", "~> 1.2.0"
26
+ spec.add_development_dependency "factory_girl_rails"
27
+ spec.add_development_dependency "poltergeist"
28
+ spec.add_development_dependency "pry-nav"
27
29
  spec.add_development_dependency "rspec-rails"
28
- spec.add_development_dependency "fakefs"
30
+ spec.add_development_dependency "selenium-webdriver"
29
31
  spec.add_development_dependency "shoulda-matchers"
30
32
  spec.add_development_dependency "sqlite3-ruby"
31
- spec.add_development_dependency "pry-debugger"
32
33
  spec.add_development_dependency "yard"
33
34
  end
@@ -10,25 +10,64 @@ module Detour::ActsAsFlaggable
10
10
  # @option options [Symbol] :find_by The field to find the record by when
11
11
  # running rake tasks. Defaults to :id.
12
12
  def acts_as_flaggable(options = {})
13
+ Detour::Feature.class_eval <<-EOF
14
+ has_one :#{table_name}_percentage_flag,
15
+ class_name: "Detour::PercentageFlag",
16
+ inverse_of: :feature,
17
+ dependent: :destroy,
18
+ conditions: { flaggable_type: "#{self}" }
19
+
20
+ attr_accessible :#{table_name}_percentage_flag_attributes
21
+
22
+ accepts_nested_attributes_for :#{table_name}_percentage_flag,
23
+ update_only: true,
24
+ reject_if: proc { |attrs| attrs[:percentage].blank? }
25
+
26
+ has_many :#{table_name}_group_flags,
27
+ class_name: "Detour::GroupFlag",
28
+ inverse_of: :feature,
29
+ dependent: :destroy,
30
+ conditions: { flaggable_type: "#{self}" }
31
+
32
+ attr_accessible :#{table_name}_group_flags_attributes
33
+ accepts_nested_attributes_for :#{table_name}_group_flags, allow_destroy: true
34
+
35
+ has_many :#{table_name}_flag_ins,
36
+ class_name: "Detour::FlagInFlag",
37
+ inverse_of: :feature,
38
+ dependent: :destroy,
39
+ conditions: { flaggable_type: "#{self}" }
40
+
41
+ has_many :#{table_name}_opt_outs,
42
+ class_name: "Detour::OptOutFlag",
43
+ inverse_of: :feature,
44
+ dependent: :destroy,
45
+ conditions: { flaggable_type: "#{self}" }
46
+ EOF
47
+
13
48
  class_eval do
14
49
  @detour_flaggable_find_by = :id
15
50
 
16
- has_many :flaggable_flags,
51
+ has_many :flag_in_flags,
17
52
  as: :flaggable,
18
- class_name: "Detour::FlaggableFlag"
53
+ class_name: "Detour::FlagInFlag"
19
54
 
20
55
  has_many :opt_out_flags,
21
56
  as: :flaggable,
22
57
  class_name: "Detour::OptOutFlag"
23
58
 
24
59
  has_many :features,
25
- through: :flaggable_flags,
60
+ through: :flag_in_flags,
26
61
  class_name: "Detour::Feature"
27
62
 
28
63
  if options[:find_by]
29
64
  @detour_flaggable_find_by = options[:find_by]
30
65
  end
31
66
 
67
+ def self.detour_flaggable_find_by
68
+ @detour_flaggable_find_by
69
+ end
70
+
32
71
  extend Detour::Flaggable::ClassMethods
33
72
  include Detour::Flaggable
34
73
  end
@@ -0,0 +1,35 @@
1
+ class Detour::Configuration
2
+ attr_reader :defined_groups
3
+ attr_accessor :default_flaggable_class_name
4
+ attr_accessor :flaggable_types
5
+ attr_accessor :grep_dirs
6
+
7
+ def initialize
8
+ @defined_groups = {}
9
+ @flaggable_types = []
10
+ @grep_dirs = []
11
+ end
12
+
13
+ # Allows for methods of the form `define_user_group` that call the private
14
+ # method `define_group_for_class`. A new group for any `User` records will
15
+ # be created that rollouts can be attached to.
16
+ #
17
+ # @example
18
+ # Detour.config.define_user_group :admins do |user|
19
+ # user.admin?
20
+ # end
21
+ def method_missing(method, *args, &block)
22
+ if /^define_(?<klass>[a-z0-9_]+)_group/ =~ method
23
+ define_group_for_class(klass.classify, args[0], &block)
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def define_group_for_class(klass, group_name, &block)
32
+ @defined_groups[klass] ||= {}
33
+ @defined_groups[klass][group_name] = block
34
+ end
35
+ end
@@ -0,0 +1,5 @@
1
+ module Detour
2
+ class Engine < ::Rails::Engine
3
+ isolate_namespace Detour
4
+ end
5
+ end
@@ -0,0 +1,87 @@
1
+ class Detour::FlagForm
2
+ def initialize(flaggable_type)
3
+ @flaggable_type = flaggable_type
4
+ end
5
+
6
+ def features
7
+ @features ||= Detour::Feature.includes("#{@flaggable_type}_percentage_flag", "#{@flaggable_type}_group_flags").with_lines
8
+ end
9
+
10
+ def errors?
11
+ features.any? { |feature| feature.errors.any? }
12
+ end
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
20
+ end
21
+
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)
26
+ end
27
+ end
28
+
29
+ def update_attributes(params)
30
+ Detour::Feature.transaction do |transaction|
31
+ features.map do |feature|
32
+ feature_params = params[:features][feature.name]
33
+ next unless feature_params
34
+
35
+ check_percentage_flag_for_deletion(feature, feature_params)
36
+ set_group_flag_params(feature, feature_params)
37
+
38
+ feature.assign_attributes feature_params
39
+ feature.save if feature.changed_for_autosave?
40
+ end
41
+
42
+ if features.any? { |feature| feature.errors.any? }
43
+ raise ActiveRecord::Rollback
44
+ else
45
+ true
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def check_percentage_flag_for_deletion(feature, params)
53
+ key = :"#{@flaggable_type}_percentage_flag_attributes"
54
+ flag = feature.send("#{@flaggable_type}_percentage_flag")
55
+ flag_params = params[key]
56
+
57
+ 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)
60
+ end
61
+
62
+ if flag.present? && flag_params[:percentage].to_i == flag.percentage
63
+ params.delete key
64
+ end
65
+ end
66
+
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
84
+ end
85
+ end
86
+ end
87
+ end
@@ -1,3 +1,3 @@
1
1
  module Detour
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  end
data/lib/detour.rb CHANGED
@@ -1,13 +1,8 @@
1
- require "active_record"
2
- require "detour/version"
3
- require "detour/feature"
4
- require "detour/flag"
5
- require "detour/flaggable_flag"
6
- require "detour/group_flag"
7
- require "detour/percentage_flag"
8
- require "detour/opt_out_flag"
9
- require "detour/flaggable"
1
+ require "detour/engine"
10
2
  require "detour/acts_as_flaggable"
3
+ require "detour/flaggable"
4
+ require "detour/flag_form"
5
+ require "detour/configuration"
11
6
 
12
7
  module Detour
13
8
  # Allows for configuration of Detour::Feature, mostly intended
@@ -20,13 +15,14 @@ module Detour
20
15
  # end
21
16
  # end
22
17
  def self.configure(&block)
23
- yield Detour::Feature
18
+ ActionDispatch::Reloader.to_prepare do
19
+ yield Detour.config
20
+ Detour.config.flaggable_types.each { |f| f.constantize }
21
+ end
24
22
  end
25
- end
26
23
 
27
- class Detour::Task < Rails::Railtie
28
- rake_tasks do
29
- Dir[File.join(File.dirname(__FILE__), '../tasks/*.rake')].each { |f| load f }
24
+ def self.config
25
+ @config ||= Detour::Configuration.new
30
26
  end
31
27
  end
32
28
 
@@ -3,6 +3,8 @@ class SetupDetour < ActiveRecord::Migration
3
3
  create_table :detour_features do |t|
4
4
  t.string :name
5
5
  t.integer :failure_count, default: 0
6
+ t.text :flag_in_counts, default: "{}"
7
+ t.text :opt_out_counts, default: "{}"
6
8
  t.timestamps
7
9
  end
8
10
 
@@ -12,8 +12,8 @@ namespace :detour do
12
12
 
13
13
  desc "Activate a feature for a record"
14
14
  task :activate, [:feature, :flaggable_type, :flaggable_id] => :environment do |task, args|
15
- if args.to_a.length < 3 && Detour::Feature.default_flaggable_class_name
16
- klass = Detour::Feature.default_flaggable_class_name.constantize
15
+ if args.to_a.length < 3 && Detour.config.default_flaggable_class_name
16
+ klass = Detour.config.default_flaggable_class_name.constantize
17
17
  record_locator = args[:flaggable_type]
18
18
  else
19
19
  klass = args[:flaggable_type].constantize
@@ -27,8 +27,8 @@ namespace :detour do
27
27
 
28
28
  desc "Deactivate a feature for a record"
29
29
  task :deactivate, [:feature, :flaggable_type, :flaggable_id] => :environment do |task, args|
30
- if args.to_a.length < 3 && Detour::Feature.default_flaggable_class_name
31
- klass = Detour::Feature.default_flaggable_class_name.constantize
30
+ if args.to_a.length < 3 && Detour.config.default_flaggable_class_name
31
+ klass = Detour.config.default_flaggable_class_name.constantize
32
32
  record_locator = args[:flaggable_type]
33
33
  else
34
34
  klass = args[:flaggable_type].constantize
@@ -41,8 +41,8 @@ namespace :detour do
41
41
 
42
42
  desc "Opt a record out of a feature"
43
43
  task :opt_out, [:feature, :flaggable_type, :flaggable_id] => :environment do |task, args|
44
- if args.to_a.length < 3 && Detour::Feature.default_flaggable_class_name
45
- klass = Detour::Feature.default_flaggable_class_name.constantize
44
+ if args.to_a.length < 3 && Detour.config.default_flaggable_class_name
45
+ klass = Detour.config.default_flaggable_class_name.constantize
46
46
  record_locator = args[:flaggable_type]
47
47
  else
48
48
  klass = args[:flaggable_type].constantize
@@ -55,8 +55,8 @@ namespace :detour do
55
55
 
56
56
  desc "Remove an opt out of a record from a feature"
57
57
  task :un_opt_out, [:feature, :flaggable_type, :flaggable_id] => :environment do |task, args|
58
- if args.to_a.length < 3 && Detour::Feature.default_flaggable_class_name
59
- klass = Detour::Feature.default_flaggable_class_name.constantize
58
+ if args.to_a.length < 3 && Detour.config.default_flaggable_class_name
59
+ klass = Detour.config.default_flaggable_class_name.constantize
60
60
  record_locator = args[:flaggable_type]
61
61
  else
62
62
  klass = args[:flaggable_type].constantize
@@ -69,8 +69,8 @@ namespace :detour do
69
69
 
70
70
  desc "Activate a feature for a group"
71
71
  task :activate_group, [:feature, :flaggable_type, :group_name] => :environment do |task, args|
72
- if args.to_a.length < 3 && Detour::Feature.default_flaggable_class_name
73
- klass = Detour::Feature.default_flaggable_class_name
72
+ if args.to_a.length < 3 && Detour.config.default_flaggable_class_name
73
+ klass = Detour.config.default_flaggable_class_name
74
74
  group_name = args[:flaggable_type]
75
75
  else
76
76
  klass = args[:flaggable_type]
@@ -82,8 +82,8 @@ namespace :detour do
82
82
 
83
83
  desc "Deactivate a feature for a group"
84
84
  task :deactivate_group, [:feature, :flaggable_type, :group_name] => :environment do |task, args|
85
- if args.to_a.length < 3 && Detour::Feature.default_flaggable_class_name
86
- klass = Detour::Feature.default_flaggable_class_name
85
+ if args.to_a.length < 3 && Detour.config.default_flaggable_class_name
86
+ klass = Detour.config.default_flaggable_class_name
87
87
  group_name = args[:flaggable_type]
88
88
  else
89
89
  klass = args[:flaggable_type]
@@ -95,8 +95,8 @@ namespace :detour do
95
95
 
96
96
  desc "Activate a feature for a percentage"
97
97
  task :activate_percentage, [:feature, :flaggable_type, :percentage] => :environment do |task, args|
98
- if args.to_a.length < 3 && Detour::Feature.default_flaggable_class_name
99
- klass = Detour::Feature.default_flaggable_class_name
98
+ if args.to_a.length < 3 && Detour.config.default_flaggable_class_name
99
+ klass = Detour.config.default_flaggable_class_name
100
100
  percentage = args[:flaggable_type]
101
101
  else
102
102
  klass = args[:flaggable_type]
@@ -108,8 +108,8 @@ namespace :detour do
108
108
 
109
109
  desc "Deactivate a feature for a percentage"
110
110
  task :deactivate_percentage, [:feature, :flaggable_type] => :environment do |task, args|
111
- if args.to_a.length < 3 && Detour::Feature.default_flaggable_class_name
112
- klass = Detour::Feature.default_flaggable_class_name
111
+ if args.to_a.length < 3 && Detour.config.default_flaggable_class_name
112
+ klass = Detour.config.default_flaggable_class_name
113
113
  else
114
114
  klass = args[:flaggable_type]
115
115
  end
data/script/rails ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ ENGINE_ROOT = File.expand_path('../..', __FILE__)
5
+ ENGINE_PATH = File.expand_path('../../lib/detour/engine', __FILE__)
6
+
7
+ require 'rails/all'
8
+ require 'rails/engine/commands'