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