detour 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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'