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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ae9c93918889c5377f07579be0dcb784a5ed6007
4
+ data.tar.gz: c0b1be203e73c179a460a508b0e8ef1c520c1b57
5
+ SHA512:
6
+ metadata.gz: c349a69e7e806ac938dc1857817eec6a9a89ea4542f638374b1194102684bf85e420ae683cadd976ec1d316385d770e60619983096253fb173335f265b1ec9b3
7
+ data.tar.gz: 2320eba53839ee27c664ef1b1407a140ce21e88ad233bf82f0b8c35fa5431cc660beee97124a6f3aa8226f38cda3b24f1960d8235ab7d9f122a0abc145d421fe
data/README.md CHANGED
@@ -107,20 +107,8 @@ flagged into a specific feature. The `#has_feature?` method provided by
107
107
 
108
108
  ### Determining if a record is flagged into a feature
109
109
 
110
- `#has_feature?` has two methods of use. The first one, where it is passed a
111
- block, will increment a `failure_count` on the given feature if the block
112
- raises an exception (the exception is again raised after incrementing). This
113
- currently does not alter the behavior of the feature, but it services a metrics
114
- purpose:
115
-
116
- ```ruby
117
- current_user.has_feature? :new_user_interface do
118
- render_new_user_interface
119
- end
120
- ```
121
-
122
- When not given a block, it simply returns a boolean, and does not watch for
123
- exceptions:
110
+ Call the `#has_feature?` method on an instance of your class that implements
111
+ `acts_as_flaggable`.
124
112
 
125
113
  ```ruby
126
114
  if current_user.has_feature? :new_user_interface
@@ -128,17 +116,6 @@ if current_user.has_feature? :new_user_interface
128
116
  end
129
117
  ```
130
118
 
131
- Want to make use of both? `#has_feature?` returns a boolean even when passed
132
- a block:
133
-
134
- ```ruby
135
- if current_user.has_feature? :new_user_interface do
136
- render_new_user_interface
137
- end; else
138
- render_old_user_interface
139
- end
140
- ```
141
-
142
119
  ### Defining programmatic groups
143
120
 
144
121
  A specific group of records matching a given block can be flagged into a
@@ -0,0 +1,7 @@
1
+ $(document).on('click', 'form .add-fields', function(e) {
2
+ e.preventDefault();
3
+ var time = new Date().getTime(),
4
+ regexp = new RegExp($(this).data('id'), 'g');
5
+
6
+ $(this).before($(this).data('fields').replace(regexp, time));
7
+ });
@@ -1,13 +1,11 @@
1
- $(function () {
2
- $('.delete-feature').on('click', function (e) {
3
- var href = $(e.currentTarget).data('path'),
4
- feature = $(e.currentTarget).closest('td').next().text(),
5
- $modal = $('#delete-feature'),
6
- $name = $modal.find('.feature-name'),
7
- $link = $modal.find('a');
1
+ $(document).on('click', '.delete-feature', function (e) {
2
+ var href = $(e.currentTarget).data('path'),
3
+ feature = $(e.currentTarget).closest('td').next().text(),
4
+ $modal = $('#delete-feature'),
5
+ $name = $modal.find('.feature-name'),
6
+ $link = $modal.find('a');
8
7
 
9
- $link.attr('href', href);
10
- $name.text(feature.trim());
11
- $modal.modal('show');
12
- });
8
+ $link.attr('href', href);
9
+ $name.text(feature.trim());
10
+ $modal.modal('show');
13
11
  });
@@ -1,13 +1,11 @@
1
- $(function () {
2
- $('.delete-flag').on('click', function (e) {
3
- var href = $(e.currentTarget).data('path'),
4
- id = $(e.currentTarget).closest('td').next().text(),
5
- $modal = $('#delete-flag'),
6
- $id = $modal.find('.flaggable-identifier'),
7
- $link = $modal.find('a');
1
+ $(document).on('click', '.delete-flag', function (e) {
2
+ var href = $(e.currentTarget).data('path'),
3
+ id = $(e.currentTarget).closest('td').next().text(),
4
+ $modal = $('#delete-flag'),
5
+ $id = $modal.find('.flaggable-identifier'),
6
+ $link = $modal.find('a');
8
7
 
9
- $link.attr('href', href);
10
- $id.text(id.trim());
11
- $modal.modal('show');
12
- });
8
+ $link.attr('href', href);
9
+ $id.text(id.trim());
10
+ $modal.modal('show');
13
11
  });
@@ -0,0 +1,5 @@
1
+ $(document).on('click', '.delete-group', function (e) {
2
+ e.preventDefault();
3
+ var $modal = $('#delete-group');
4
+ $modal.modal('show');
5
+ });
@@ -0,0 +1,23 @@
1
+ $(function() {
2
+ $('.feature-lines').each(function() {
3
+ var lines = $(this).data('lines').split(','),
4
+ prefix = $(this).data('prefix');
5
+
6
+ $(this).popover({
7
+ html : true,
8
+ content : function() { return lineList(prefix, lines) },
9
+ placement: 'bottom'
10
+ });
11
+ });
12
+
13
+ function lineList(prefix, lines) {
14
+ var str = '<ul>';
15
+
16
+ lines.forEach(function(line) {
17
+ line = '<li><a href="' + prefix + line + '">' + line + '</a></li>';
18
+ str += line;
19
+ });
20
+
21
+ return str;
22
+ }
23
+ });
@@ -1,3 +1,4 @@
1
1
  $(document).on('hidden.bs.modal', '.modal', function () {
2
2
  $('.panel', this).remove();
3
+ $('form', this)[0].reset();
3
4
  });
@@ -4,6 +4,14 @@
4
4
  src: url('/assets/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('/assets/glyphicons-halflings-regular.woff') format('woff'), url('/assets/glyphicons-halflings-regular.ttf') format('truetype'), url('/assets/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
5
5
  }
6
6
 
7
+ .popover {
8
+ max-width: none;
9
+ }
10
+
11
+ .popover ul {
12
+ padding-left: 10px;
13
+ }
14
+
7
15
  body {
8
16
  padding-bottom: 40px;
9
17
  }
@@ -47,3 +55,7 @@ table.table tbody tr td {
47
55
  .counter-header {
48
56
  width: 80px;
49
57
  }
58
+
59
+ .delete-group {
60
+ margin-left: 0.5em;
61
+ }
@@ -4,9 +4,10 @@ class Detour::FeaturesController < Detour::ApplicationController
4
4
 
5
5
  if @feature.save
6
6
  flash[:notice] = "Your feature has been successfully created."
7
- render :success
7
+ render "detour/shared/success"
8
8
  else
9
- render :error
9
+ @model = @feature
10
+ render "detour/shared/error"
10
11
  end
11
12
  end
12
13
 
@@ -1,90 +1,22 @@
1
1
  require "indefinite_article"
2
2
 
3
3
  class Detour::FlaggableFlagsController < Detour::ApplicationController
4
+ include Detour::FlaggableFlagsHelper
5
+
4
6
  before_filter :ensure_flaggable_type_exists
5
7
 
6
8
  def index
7
- feature = Detour::Feature.find_by_name!(feature_name)
8
- @flags = feature.send("#{flag_type}_flags").where(flaggable_type: flaggable_class.to_s)
9
+ @feature = Detour::Feature.find_by_name!(params[:feature_name])
9
10
  end
10
11
 
11
- def create
12
- @feature = Detour::Feature.find_by_name! feature_name
13
- ids = params[:ids].split(",")
14
- @errors = []
15
-
16
- Detour::Feature.transaction do
17
- begin
18
- ids.each do |id|
19
- flaggable = flaggable_class.flaggable_find! id
20
- flag = @feature.send("#{flaggable_type}_#{flag_type.pluralize}").new flaggable: flaggable
21
-
22
- unless flag.save
23
- @errors.concat flag.errors.full_messages
24
- end
25
- end
26
- rescue ActiveRecord::RecordNotFound => e
27
- @errors << e.message
28
- end
12
+ def update
13
+ @feature = Detour::Feature.find_by_name!(params[:feature_name])
29
14
 
30
- if @errors.any?
31
- raise ActiveRecord::Rollback
32
- end
33
- end
34
-
35
- if @errors.empty?
36
- flash[:notice] = success_message
37
- render :success
15
+ if @feature.update_attributes(params[:feature])
16
+ flash[:notice] = "Your #{flag_noun.pluralize} have been updated"
17
+ redirect_to send("#{flag_type}_flags_path", feature_name: params[:feature_name], flaggable_type: params[:flaggable_type])
38
18
  else
39
- render :error
19
+ render :index
40
20
  end
41
21
  end
42
-
43
- def destroy
44
- feature = Detour::Feature.find_by_name!(feature_name)
45
- @flag = feature.send("#{flag_type}_flags").find(params[:id])
46
- @flag.destroy
47
- flash[:notice] = "#{feature_name} #{flag_noun} for #{flaggable_class} #{@flag.flaggable.send flaggable_class.detour_flaggable_find_by} has been deleted."
48
- redirect_to send("#{flag_type}_flags_path", feature.name, flaggable_type)
49
- end
50
-
51
- private
52
-
53
- def feature_name
54
- params[:feature_name]
55
- end
56
- helper_method :feature_name
57
-
58
- def flaggable_type
59
- params[:flaggable_type]
60
- end
61
- helper_method :flaggable_type
62
-
63
- def flaggable_class
64
- flaggable_type.classify.constantize
65
- end
66
- helper_method :flaggable_class
67
-
68
- def flag_type
69
- request.path.split("/")[2].underscore.singularize
70
- end
71
- helper_method :flag_type
72
-
73
- def flag_verb
74
- flag_type == "flag_in" ? "flagged in to" : "opted out of"
75
- end
76
- helper_method :flag_verb
77
-
78
- def flag_noun
79
- flag_type.dasherize
80
- end
81
- helper_method :flag_noun
82
-
83
- def success_message
84
- plural = params[:ids].split(",").length > 1
85
- klass = plural ? flaggable_class.to_s.pluralize : flaggable_class
86
- has = plural ? "have" : "has"
87
-
88
- flash[:notice] = "#{klass} #{params[:ids].split(",").join(", ")} #{has} been #{flag_verb} #{@feature.name}"
89
- end
90
22
  end
@@ -0,0 +1,39 @@
1
+ class Detour::GroupsController < Detour::ApplicationController
2
+ def index
3
+ @groups = Detour::Group.all
4
+ end
5
+
6
+ def show
7
+ @group = Detour::Group.find(params[:id])
8
+ end
9
+
10
+ def create
11
+ @group = Detour::Group.new(params[:group])
12
+
13
+ if @group.save
14
+ flash[:notice] = "Your group has been successfully created."
15
+ render "detour/shared/success"
16
+ else
17
+ @model = @group
18
+ render "detour/shared/error"
19
+ end
20
+ end
21
+
22
+ def update
23
+ @group = Detour::Group.find(params[:id])
24
+
25
+ if @group.update_attributes(params[:group])
26
+ flash[:notice] = "Your group has been successfully updated."
27
+ redirect_to group_path @group
28
+ else
29
+ render :show
30
+ end
31
+ end
32
+
33
+ def destroy
34
+ @group = Detour::Group.find(params[:id])
35
+ @group.destroy
36
+ flash[:notice] = "Group \"#{@group.name}\" has been deleted."
37
+ redirect_to groups_path
38
+ end
39
+ end
@@ -1,4 +1,15 @@
1
1
  module Detour::ApplicationHelper
2
+ def link_to_add_fields(name, f, association, template = nil)
3
+ new_object = f.object.send(association).klass.new
4
+ template ||= "#{association.to_s.singularize}_fields"
5
+ id = new_object.object_id
6
+ fields = f.fields_for(association, new_object, child_index: id) do |builder|
7
+ render("#{template}", f: builder)
8
+ end
9
+
10
+ link_to name, "javascript:void(0)", class: "add-fields btn btn-default", data: { id: id, fields: fields.gsub("\n", "") }
11
+ end
12
+
2
13
  def table(&block)
3
14
  content_tag :div, class: "table-responsive" do
4
15
  content_tag :table, class: "table table-striped" do
@@ -1,5 +1,25 @@
1
1
  module Detour::FlaggableFlagsHelper
2
+ def feature_name
3
+ params[:feature_name]
4
+ end
5
+
6
+ def flag_noun
7
+ flag_type.dasherize
8
+ end
9
+
2
10
  def flag_title
3
11
  flag_noun.capitalize
4
12
  end
13
+
14
+ def flag_type
15
+ request.path.split("/")[2].underscore.singularize
16
+ end
17
+
18
+ def flag_verb
19
+ flag_type == "flag_in" ? "flagged in to" : "opted out of"
20
+ end
21
+
22
+ def flaggable_type
23
+ params[:flaggable_type]
24
+ end
5
25
  end
@@ -1,6 +1,10 @@
1
1
  module Detour::FlagsHelper
2
+ def github_prefix
3
+ "https://github.com/#{ENV["DETOUR_GITHUB_REPO"]}/blob/#{ENV["DETOUR_GITHUB_BRANCH"]}/"
4
+ end
5
+
2
6
  def spacer_count
3
- names_count = @flag_form.group_names.length
7
+ names_count = @flag_form.groups.length
4
8
  difference = 10 - names_count
5
9
  count = difference < 0 ? 0 : difference
6
10
  end
@@ -0,0 +1,21 @@
1
+ module Detour::Concerns
2
+ module Keepable
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ attr_writer :to_keep
7
+ end
8
+
9
+ def to_keep
10
+ @to_keep || (!marked_for_destruction? && !new_record?)
11
+ end
12
+
13
+ def keep_or_destroy(params = {})
14
+ if params["to_keep"] == "1"
15
+ self.to_keep = true
16
+ else
17
+ mark_for_destruction
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,7 +1,8 @@
1
1
  module Detour::Concerns
2
2
  module Matchers
3
3
  # Determines whether or not the given instance has had the feature rolled out
4
- # to it either via direct flagging-in, percentage, or by group membership.
4
+ # to it either via direct flagging-in, percentage, or by database or defined
5
+ # group membership.
5
6
  #
6
7
  # @example
7
8
  # feature.match?(current_user)
@@ -11,7 +12,10 @@ module Detour::Concerns
11
12
  #
12
13
  # @return Whether or not the given instance has the feature rolled out to it.
13
14
  def match?(instance)
14
- match_id?(instance) || match_percentage?(instance) || match_groups?(instance)
15
+ match_id?(instance) ||
16
+ match_percentage?(instance) ||
17
+ match_database_groups?(instance) ||
18
+ match_defined_groups?(instance)
15
19
  end
16
20
 
17
21
  # Determines whether or not the given instance has had the feature rolled out
@@ -48,25 +52,40 @@ module Detour::Concerns
48
52
  end
49
53
 
50
54
  # Determines whether or not the given instance has had the feature rolled out
51
- # to it via group membership.
55
+ # to it via database group membership.
52
56
  #
53
57
  # @example
54
- # feature.match_groups?(current_user)
58
+ # feature.match_database_groups?(current_user)
59
+ #
60
+ # @param [ActiveRecord::Base] instance A record to be tested for feature
61
+ # rollout.
62
+ #
63
+ # @return Whether or not the given instance has the feature rolled out to it
64
+ # via direct database group membership.
65
+ def match_database_groups?(instance)
66
+ database_group_flags.where(flaggable_type: instance.class).map(&:members).flatten.uniq.include? instance
67
+ end
68
+
69
+ # Determines whether or not the given instance has had the feature rolled out
70
+ # to it via defined group membership.
71
+ #
72
+ # @example
73
+ # feature.match_defined_groups?(current_user)
55
74
  #
56
75
  # @param [ActiveRecord::Base] instance A record to be tested for feature
57
76
  # rollout.
58
77
  #
59
78
  # @return Whether or not the given instance has the feature rolled out to it
60
79
  # via direct group membership.
61
- def match_groups?(instance)
80
+ def match_defined_groups?(instance)
62
81
  klass = instance.class.to_s
63
82
 
64
- return unless Detour.config.defined_groups[klass]
83
+ return unless Detour::DefinedGroup.by_type(klass).any?
65
84
 
66
- group_names = group_flags.find_all_by_flaggable_type(klass).collect(&:group_name)
85
+ group_names = defined_group_flags.find_all_by_flaggable_type(klass).collect(&:group_name)
67
86
 
68
- Detour.config.defined_groups[klass].collect { |group_name, block|
69
- block.call(instance) if group_names.include? group_name.to_s
87
+ Detour::DefinedGroup.by_type(klass).collect { |name, group|
88
+ group.test(instance) if group_names.include? group.name
70
89
  }.any?
71
90
  end
72
91
  end
@@ -0,0 +1,30 @@
1
+ class Detour::DatabaseGroupFlag < Detour::Flag
2
+ include Detour::Concerns::Keepable
3
+
4
+ validates_presence_of :group_id
5
+ validates_presence_of :flaggable_type
6
+ validates_uniqueness_of :feature_id, scope: :group_id
7
+
8
+ attr_accessible :group_id
9
+
10
+ belongs_to :group
11
+ has_many :memberships, through: :group
12
+
13
+ def members
14
+ flaggable_class.joins(%Q{INNER JOIN "detour_memberships" ON "#{flaggable_type.downcase.pluralize}"."id" = "detour_memberships"."member_id"}).where(detour_memberships: { group_id: group.id })
15
+ end
16
+
17
+ def group_name
18
+ group.name
19
+ end
20
+
21
+ def group_type
22
+ "database"
23
+ end
24
+
25
+ private
26
+
27
+ def flaggable_class
28
+ flaggable_type.constantize
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ class Detour::DefinedGroup
2
+ attr_reader :name
3
+ alias :id :name
4
+
5
+ def initialize(name, test)
6
+ @name = name.to_s
7
+ @test = test
8
+ end
9
+
10
+ def to_s
11
+ name
12
+ end
13
+
14
+ def test(arg)
15
+ @test.call(arg)
16
+ end
17
+
18
+ def self.by_type(type)
19
+ Detour.config.defined_groups.fetch(type.to_s, {}).with_indifferent_access
20
+ end
21
+ end
@@ -0,0 +1,28 @@
1
+ # A group of flaggable records of a given class may be flagged into a feature
2
+ # with this class.
3
+ class Detour::DefinedGroupFlag < Detour::Flag
4
+ include Detour::Concerns::Keepable
5
+
6
+ validates_presence_of :group_name
7
+ validates_uniqueness_of :feature_id, scope: [:flaggable_type, :group_name]
8
+
9
+ attr_accessible :group_name
10
+
11
+ def group
12
+ find_group || build_group
13
+ end
14
+
15
+ def group_type
16
+ "defined"
17
+ end
18
+
19
+ private
20
+
21
+ def find_group
22
+ Detour::DefinedGroup.by_type(flaggable_type)[group_name]
23
+ end
24
+
25
+ def build_group
26
+ Detour::DefinedGroup.new(group_name, ->{})
27
+ end
28
+ end
@@ -2,7 +2,6 @@
2
2
  # via individual flags, percentages, or defined groups.
3
3
  class Detour::Feature < ActiveRecord::Base
4
4
  include Detour::Concerns::Matchers
5
- include Detour::Concerns::FlagActions
6
5
 
7
6
  self.table_name = :detour_features
8
7
 
@@ -10,13 +9,15 @@ class Detour::Feature < ActiveRecord::Base
10
9
  serialize :opt_out_counts, JSON
11
10
 
12
11
  has_many :flag_in_flags
13
- has_many :group_flags
12
+ has_many :defined_group_flags
13
+ has_many :database_group_flags
14
14
  has_many :percentage_flags
15
15
  has_many :opt_out_flags
16
16
  has_many :flags, dependent: :destroy
17
17
 
18
18
  validates_presence_of :name
19
19
  validates_uniqueness_of :name
20
+ validates_format_of :name, with: /\A[\w-]+\Z/
20
21
 
21
22
  attr_accessible :name
22
23
 
@@ -69,7 +70,7 @@ class Detour::Feature < ActiveRecord::Base
69
70
 
70
71
  File.open path do |file|
71
72
  file.each_line.with_index(1) do |line, i|
72
- line.scan(/\.has_feature\?\s*\(*:(\w+)/).each do |match, _|
73
+ line.scan(/\.has_feature\?\s*\(?[:"]([\w-]+)/).each do |match, _|
73
74
  (hash[match] ||= new(name: match)).lines << "#{path}#L#{i}"
74
75
  end
75
76
  end
@@ -1,12 +1,5 @@
1
1
  # An individual record of a certain type may be flagged into a feature with
2
2
  # this class.
3
- class Detour::FlagInFlag < Detour::Flag
4
- include Detour::Concerns::CountableFlag
5
-
6
- belongs_to :flaggable, polymorphic: true
7
-
8
- validates_presence_of :flaggable
3
+ class Detour::FlagInFlag < 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,24 @@
1
+ class Detour::FlaggableFlag < Detour::Flag
2
+ include Detour::Concerns::CountableFlag
3
+
4
+ belongs_to :flaggable, polymorphic: true
5
+
6
+ validates_presence_of :flaggable
7
+
8
+ attr_accessor :flaggable_key
9
+ attr_accessible :flaggable
10
+ attr_accessible :flaggable_key
11
+
12
+ before_validation :set_flaggable
13
+
14
+ private
15
+
16
+ def set_flaggable
17
+ unless flaggable || !flaggable_key
18
+ self.flaggable_id = flaggable_type.constantize.flaggable_find!(flaggable_key).id
19
+ end
20
+ rescue ActiveRecord::RecordNotFound
21
+ errors.add(flaggable_type, "\"#{flaggable_key}\" could not be found")
22
+ false
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ class Detour::Group < ActiveRecord::Base
2
+ validates :name, presence: true, uniqueness: { scope: :flaggable_type }
3
+ validates :flaggable_type, presence: true, inclusion: { in: Detour.config.flaggable_types }
4
+ has_many :memberships, dependent: :destroy
5
+
6
+ accepts_nested_attributes_for :memberships, allow_destroy: true
7
+
8
+ attr_accessible :name, :flaggable_type, :memberships_attributes
9
+
10
+ def to_s
11
+ name
12
+ end
13
+
14
+ def flaggable_class
15
+ flaggable_type.constantize
16
+ end
17
+ end