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