detour 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +2 -25
- data/app/assets/javascripts/detour/add_fields.js +7 -0
- data/app/assets/javascripts/detour/delete_feature.js +9 -11
- data/app/assets/javascripts/detour/delete_flag.js +9 -11
- data/app/assets/javascripts/detour/delete_group.js +5 -0
- data/app/assets/javascripts/detour/feature_lines.js +23 -0
- data/app/assets/javascripts/detour/modals.js +1 -0
- data/app/assets/stylesheets/detour/main.css +12 -0
- data/app/controllers/detour/features_controller.rb +3 -2
- data/app/controllers/detour/flaggable_flags_controller.rb +9 -77
- data/app/controllers/detour/groups_controller.rb +39 -0
- data/app/helpers/detour/application_helper.rb +11 -0
- data/app/helpers/detour/flaggable_flags_helper.rb +20 -0
- data/app/helpers/detour/flags_helper.rb +5 -1
- data/app/models/detour/concerns/keepable.rb +21 -0
- data/app/models/detour/concerns/matchers.rb +28 -9
- data/app/models/detour/database_group_flag.rb +30 -0
- data/app/models/detour/defined_group.rb +21 -0
- data/app/models/detour/defined_group_flag.rb +28 -0
- data/app/models/detour/feature.rb +4 -3
- data/app/models/detour/flag_in_flag.rb +1 -8
- data/app/models/detour/flaggable_flag.rb +24 -0
- data/app/models/detour/group.rb +17 -0
- data/app/models/detour/membership.rb +41 -0
- data/app/models/detour/opt_out_flag.rb +1 -8
- data/app/views/detour/flaggable_flags/_flaggable_flag_fields.html.erb +19 -0
- data/app/views/detour/flaggable_flags/index.html.erb +13 -26
- data/app/views/detour/flags/_feature_form.html.erb +12 -3
- data/app/views/detour/flags/index.html.erb +7 -2
- data/app/views/detour/groups/_group.html.erb +3 -0
- data/app/views/detour/groups/_membership_fields.html.erb +19 -0
- data/app/views/detour/groups/index.html.erb +21 -0
- data/app/views/detour/groups/show.html.erb +41 -0
- data/app/views/detour/memberships/_membership.html.erb +4 -0
- data/app/views/detour/{features → shared}/_errors.html.erb +2 -2
- data/app/views/detour/shared/_nav.html.erb +1 -0
- data/app/views/detour/shared/error.js.erb +5 -0
- data/config/locales/en.yml +11 -0
- data/config/routes.rb +8 -7
- data/detour.gemspec +1 -0
- data/lib/detour/acts_as_flaggable.rb +19 -4
- data/lib/detour/configuration.rb +1 -1
- data/lib/detour/flag_form.rb +53 -34
- data/lib/detour/flaggable.rb +0 -19
- data/lib/detour/version.rb +1 -1
- data/lib/generators/templates/migration.rb +21 -1
- data/lib/tasks/.gitkeep +0 -0
- data/spec/controllers/detour/flaggable_flags_controller_spec.rb +30 -67
- data/spec/controllers/detour/groups_controller_spec.rb +107 -0
- data/spec/dummy/db/migrate/20131221052201_setup_detour.rb +21 -1
- data/spec/dummy/db/schema.rb +20 -1
- data/spec/factories/database_group_flag.rb +7 -0
- data/spec/factories/{group_flag.rb → defined_group_flag.rb} +1 -1
- data/spec/factories/group.rb +10 -0
- data/spec/factories/membership.rb +6 -0
- data/spec/features/database_group_flags_spec.rb +50 -0
- data/spec/features/database_groups_spec.rb +174 -0
- data/spec/features/defined_group_flags_spec.rb +67 -0
- data/spec/features/features_spec.rb +44 -0
- data/spec/features/flag_in_flags_spec.rb +22 -60
- data/spec/features/opt_out_flags_spec.rb +34 -59
- data/spec/integration/group_rollout_spec.rb +2 -2
- data/spec/lib/detour/acts_as_flaggable_spec.rb +12 -3
- data/spec/lib/detour/configuration_spec.rb +6 -2
- data/spec/lib/detour/flag_form_spec.rb +0 -11
- data/spec/lib/detour/flaggable_spec.rb +1 -54
- data/spec/models/detour/database_group_flag_spec.rb +29 -0
- data/spec/models/detour/defined_group_spec.rb +21 -0
- data/spec/models/detour/feature_spec.rb +57 -119
- data/spec/models/detour/flag_in_flag_spec.rb +1 -4
- data/spec/models/detour/flaggable_flag_spec.rb +25 -0
- data/spec/models/detour/group_flag_spec.rb +1 -1
- data/spec/models/detour/membership_spec.rb +58 -0
- data/spec/models/detour/opt_out_flag_spec.rb +1 -4
- data/spec/spec_helper.rb +4 -0
- metadata +97 -81
- data/app/models/detour/concerns/flag_actions.rb +0 -141
- data/app/models/detour/group_flag.rb +0 -13
- data/app/views/detour/features/_success.html.erb +0 -1
- data/app/views/detour/features/error.js.erb +0 -5
- data/lib/tasks/detour.rake +0 -119
- data/spec/features/group_flags_spec.rb +0 -49
- data/spec/integration/flag_rollout_spec.rb +0 -27
- data/spec/lib/tasks/detour_spec.rb +0 -162
- /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?`
|
111
|
-
|
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
|
@@ -1,13 +1,11 @@
|
|
1
|
-
$(function () {
|
2
|
-
$(
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
});
|
8
|
+
$link.attr('href', href);
|
9
|
+
$name.text(feature.trim());
|
10
|
+
$modal.modal('show');
|
13
11
|
});
|
@@ -1,13 +1,11 @@
|
|
1
|
-
$(function () {
|
2
|
-
$(
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
10
|
-
|
11
|
-
|
12
|
-
});
|
8
|
+
$link.attr('href', href);
|
9
|
+
$id.text(id.trim());
|
10
|
+
$modal.modal('show');
|
13
11
|
});
|
@@ -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
|
+
});
|
@@ -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
|
7
|
+
render "detour/shared/success"
|
8
8
|
else
|
9
|
-
|
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
|
12
|
-
@feature = Detour::Feature.find_by_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
|
-
|
31
|
-
|
32
|
-
|
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 :
|
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.
|
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
|
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)
|
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.
|
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
|
80
|
+
def match_defined_groups?(instance)
|
62
81
|
klass = instance.class.to_s
|
63
82
|
|
64
|
-
return unless Detour.
|
83
|
+
return unless Detour::DefinedGroup.by_type(klass).any?
|
65
84
|
|
66
|
-
group_names =
|
85
|
+
group_names = defined_group_flags.find_all_by_flaggable_type(klass).collect(&:group_name)
|
67
86
|
|
68
|
-
Detour.
|
69
|
-
|
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 :
|
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*\(
|
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::
|
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
|