green_flag 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (136) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/Gemfile +17 -0
  4. data/Gemfile.lock +138 -0
  5. data/MIT-LICENSE +20 -0
  6. data/README.md +43 -0
  7. data/Rakefile +38 -0
  8. data/app/assets/images/green_flag/.gitkeep +0 -0
  9. data/app/assets/javascripts/green_flag/admin/features.js +353 -0
  10. data/app/assets/javascripts/green_flag/admin/rules.js +2 -0
  11. data/app/assets/javascripts/green_flag/application.js +14 -0
  12. data/app/assets/stylesheets/green_flag/admin/features.css.scss +36 -0
  13. data/app/assets/stylesheets/green_flag/admin/rules.css.scss +3 -0
  14. data/app/assets/stylesheets/green_flag/application.css +13 -0
  15. data/app/controllers/green_flag/admin/feature_decision_summaries_controller.rb +33 -0
  16. data/app/controllers/green_flag/admin/features_controller.rb +32 -0
  17. data/app/controllers/green_flag/admin/rule_lists_controller.rb +28 -0
  18. data/app/controllers/green_flag/admin/white_list_users_controller.rb +39 -0
  19. data/app/controllers/green_flag/site_visitor_management.rb +51 -0
  20. data/app/helpers/green_flag/application_helper.rb +4 -0
  21. data/app/models/green_flag/feature.rb +84 -0
  22. data/app/models/green_flag/feature_decision.rb +77 -0
  23. data/app/models/green_flag/feature_event.rb +9 -0
  24. data/app/models/green_flag/rule.rb +66 -0
  25. data/app/models/green_flag/site_visitor.rb +49 -0
  26. data/app/models/green_flag/user_group.rb +8 -0
  27. data/app/models/green_flag/visitor_group.rb +60 -0
  28. data/app/views/green_flag/admin/features/index.html.erb +14 -0
  29. data/app/views/green_flag/admin/features/show.html.erb +144 -0
  30. data/app/views/layouts/green_flag/application.html.erb +25 -0
  31. data/config/routes.rb +12 -0
  32. data/db/migrate/20140502112602_create_green_flag_site_visitors.rb +9 -0
  33. data/db/migrate/20140502221059_create_green_flag_features.rb +10 -0
  34. data/db/migrate/20140502221423_create_green_flag_feature_decisions.rb +13 -0
  35. data/db/migrate/20140505204611_add_visitor_code_to_site_visitors.rb +8 -0
  36. data/db/migrate/20140511045110_create_green_flag_rules.rb +12 -0
  37. data/db/migrate/20140513203728_set_default_percentage_in_green_flag_rules.rb +10 -0
  38. data/db/migrate/20140514202337_require_ordering_for_green_flag_rules.rb +13 -0
  39. data/db/migrate/20140516214909_add_restrictions_to_green_flag_rules.rb +13 -0
  40. data/db/migrate/20150211214159_create_green_flag_feature_events.rb +13 -0
  41. data/db/migrate/20150213191101_add_rule_id_to_green_flag_feature_decisions.rb +5 -0
  42. data/db/migrate/20150218035000_add_version_number_to_green_flag_rules.rb +9 -0
  43. data/db/migrate/20150218035805_add_version_number_to_green_flag_features.rb +9 -0
  44. data/db/migrate/20150218171852_add_version_number_to_green_flag_rules_indices.rb +19 -0
  45. data/green_flag.gemspec +34 -0
  46. data/green_flag.png +0 -0
  47. data/green_flag_small.png +0 -0
  48. data/lib/green_flag/engine.rb +16 -0
  49. data/lib/green_flag/version.rb +3 -0
  50. data/lib/green_flag.rb +4 -0
  51. data/lib/tasks/green_flag_tasks.rake +4 -0
  52. data/script/rails +8 -0
  53. data/spec/controllers/admin/feature_decision_summaries_controller_spec.rb +17 -0
  54. data/spec/controllers/admin/features_controller_spec.rb +21 -0
  55. data/spec/controllers/admin/rule_lists_controller_spec.rb +25 -0
  56. data/spec/controllers/admin/white_list_users_controller_spec.rb +15 -0
  57. data/spec/controllers/site_visitor_management_spec.rb +54 -0
  58. data/spec/dummy/README.rdoc +261 -0
  59. data/spec/dummy/Rakefile +7 -0
  60. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  61. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  62. data/spec/dummy/app/controllers/application_controller.rb +7 -0
  63. data/spec/dummy/app/controllers/feature_check_controller.rb +10 -0
  64. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  65. data/spec/dummy/app/mailers/.gitkeep +0 -0
  66. data/spec/dummy/app/models/.gitkeep +0 -0
  67. data/spec/dummy/app/models/user.rb +3 -0
  68. data/spec/dummy/app/views/layouts/application.html.erb +14 -0
  69. data/spec/dummy/config/application.rb +65 -0
  70. data/spec/dummy/config/boot.rb +10 -0
  71. data/spec/dummy/config/database.yml +43 -0
  72. data/spec/dummy/config/environment.rb +5 -0
  73. data/spec/dummy/config/environments/development.rb +37 -0
  74. data/spec/dummy/config/environments/production.rb +67 -0
  75. data/spec/dummy/config/environments/test.rb +37 -0
  76. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  77. data/spec/dummy/config/initializers/inflections.rb +15 -0
  78. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  79. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  80. data/spec/dummy/config/initializers/session_store.rb +8 -0
  81. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  82. data/spec/dummy/config/locales/en.yml +5 -0
  83. data/spec/dummy/config/routes.rb +6 -0
  84. data/spec/dummy/config.ru +4 -0
  85. data/spec/dummy/db/migrate/20150726195118_create_users.rb +8 -0
  86. data/spec/dummy/db/migrate/20150726204409_add_email_to_users.rb +5 -0
  87. data/spec/dummy/db/schema.rb +78 -0
  88. data/spec/dummy/lib/assets/.gitkeep +0 -0
  89. data/spec/dummy/log/.gitkeep +0 -0
  90. data/spec/dummy/log/development.log +8341 -0
  91. data/spec/dummy/log/test.log +34578 -0
  92. data/spec/dummy/public/404.html +26 -0
  93. data/spec/dummy/public/422.html +26 -0
  94. data/spec/dummy/public/500.html +25 -0
  95. data/spec/dummy/public/favicon.ico +0 -0
  96. data/spec/dummy/script/rails +6 -0
  97. data/spec/dummy/tmp/cache/assets/C47/3D0/sprockets%2F8f17c33229239b023190617bf2e915a3 +0 -0
  98. data/spec/dummy/tmp/cache/assets/C81/770/sprockets%2Fdbcf34796b062155788f0b550808541a +0 -0
  99. data/spec/dummy/tmp/cache/assets/C89/D60/sprockets%2F73cd073739a0655341b7278fae57518f +0 -0
  100. data/spec/dummy/tmp/cache/assets/CB6/8F0/sprockets%2F5ea0f1f2583683678e122a9a9391a80f +0 -0
  101. data/spec/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
  102. data/spec/dummy/tmp/cache/assets/CE7/FF0/sprockets%2Fe45f3a7675a8c5a5b064117792bf5e28 +0 -0
  103. data/spec/dummy/tmp/cache/assets/D07/670/sprockets%2F761d03a66b753d628feccd12072c814c +0 -0
  104. data/spec/dummy/tmp/cache/assets/D10/860/sprockets%2F4582878dbb5b72bfa76615d31b34ed51 +0 -0
  105. data/spec/dummy/tmp/cache/assets/D13/270/sprockets%2F701a30cd450ae3cfa114092bafc16004 +0 -0
  106. data/spec/dummy/tmp/cache/assets/D15/C10/sprockets%2F6f42f843c916d7864a0dfa912fb3194a +0 -0
  107. data/spec/dummy/tmp/cache/assets/D18/860/sprockets%2Fce00769800ae939cebb28501947ea96f +0 -0
  108. data/spec/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  109. data/spec/dummy/tmp/cache/assets/D4A/970/sprockets%2F2a7d3b403cbdd8b59d57d26964ea5768 +0 -0
  110. data/spec/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
  111. data/spec/dummy/tmp/cache/assets/D59/090/sprockets%2F88788ba6a64e6279624e5c2ff7eead53 +0 -0
  112. data/spec/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
  113. data/spec/dummy/tmp/cache/assets/D5C/330/sprockets%2Fd1b1c9a53f4a8a5827e5b02bf0e100e8 +0 -0
  114. data/spec/dummy/tmp/cache/assets/D68/760/sprockets%2Fea24808c41a3dff75b995b0f090e1a6a +0 -0
  115. data/spec/dummy/tmp/cache/assets/D6A/580/sprockets%2F18c12847aa1bb46ce9b5661f0e9e5fb0 +0 -0
  116. data/spec/dummy/tmp/cache/assets/DA1/210/sprockets%2F25a2979e392c8bc3adce7075cf19ab4b +0 -0
  117. data/spec/dummy/tmp/cache/assets/DA9/2C0/sprockets%2Ff95d82b2bbb6db8ffe1a87f67b415291 +0 -0
  118. data/spec/dummy/tmp/cache/assets/DB2/0C0/sprockets%2F95cf35cd3e97774df3c41ee0ef564a8d +0 -0
  119. data/spec/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
  120. data/spec/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  121. data/spec/dummy/tmp/cache/assets/E07/200/sprockets%2F82a8ce7f5bcfb07f773df4cbfeb04762 +0 -0
  122. data/spec/dummy/tmp/cache/assets/E34/D30/sprockets%2F99c2d0bbd78f1b867beeb3a2eefda618 +0 -0
  123. data/spec/factories/green_flag/feature.rb +7 -0
  124. data/spec/factories/green_flag/rule.rb +9 -0
  125. data/spec/features/admin_spec.rb +16 -0
  126. data/spec/features/visitor_spec.rb +36 -0
  127. data/spec/models/green_flag/feature_decision_spec.rb +102 -0
  128. data/spec/models/green_flag/feature_event_spec.rb +4 -0
  129. data/spec/models/green_flag/feature_spec.rb +135 -0
  130. data/spec/models/green_flag/rule_spec.rb +107 -0
  131. data/spec/models/green_flag/site_visitor_spec.rb +95 -0
  132. data/spec/models/green_flag/user_group_spec.rb +24 -0
  133. data/spec/models/green_flag/visitor_group_spec.rb +81 -0
  134. data/spec/spec_helper.rb +20 -0
  135. data/spec/support/controller_helpers.rb +7 -0
  136. metadata +359 -0
@@ -0,0 +1,13 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require_self
12
+ *= require_tree .
13
+ */
@@ -0,0 +1,33 @@
1
+ class GreenFlag::Admin::FeatureDecisionSummariesController < ApplicationController
2
+
3
+ def show
4
+ feature_id = params[:feature_id]
5
+ feature = GreenFlag::Feature.find(feature_id)
6
+
7
+ render :json => summary(feature)
8
+ end
9
+
10
+ def update
11
+ feature_id = params[:feature_id]
12
+ feature = GreenFlag::Feature.find(feature_id)
13
+
14
+ if params[:forget_enabled]
15
+ feature.forget_non_manual_decisions!(true)
16
+ end
17
+ if params[:forget_disabled]
18
+ feature.forget_non_manual_decisions!(false)
19
+ end
20
+
21
+ render :json => summary(feature)
22
+ end
23
+
24
+ private
25
+
26
+ def summary(feature)
27
+ {
28
+ enabled: feature.enabled_count,
29
+ disabled: feature.disabled_count,
30
+ }
31
+ end
32
+
33
+ end
@@ -0,0 +1,32 @@
1
+ class GreenFlag::Admin::FeaturesController < ApplicationController
2
+
3
+ layout 'green_flag/application'
4
+
5
+ def index
6
+ @features = GreenFlag::Feature.all
7
+ end
8
+
9
+ def show
10
+ @feature = GreenFlag::Feature.find(params[:id])
11
+ @visitor_groups = GreenFlag::VisitorGroup.all.map { |group| { key: group.key, description: group.description } }
12
+ end
13
+
14
+ def current_visitor_status
15
+ @feature = GreenFlag::Feature.find(params[:id])
16
+ fd = GreenFlag::FeatureDecision.for_feature(@feature.id).where(site_visitor_id: current_site_visitor.id).first
17
+ render :json => { status: status_text(fd) }
18
+ end
19
+
20
+ private
21
+
22
+ def status_text(feature_decison)
23
+ if feature_decison.nil? || feature_decison.undecided?
24
+ "Undecided"
25
+ elsif feature_decison.enabled?
26
+ "Enabled"
27
+ else
28
+ "Disabled"
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,28 @@
1
+ class GreenFlag::Admin::RuleListsController < ApplicationController
2
+
3
+ def show
4
+ feature_id = params[:feature_id]
5
+ feature = GreenFlag::Feature.find(feature_id)
6
+ rules = feature.rules
7
+
8
+ render :json => rules.to_json(methods: :group_description)
9
+ end
10
+
11
+ def update
12
+ feature_id = params[:feature_id].to_i
13
+ rule_array = params['_json'] || []
14
+
15
+ filter_rules(rule_array)
16
+
17
+ rules = GreenFlag::Rule.set_rules!(feature_id, rule_array)
18
+ render :json => rules.to_json(methods: :group_description)
19
+ end
20
+
21
+ private
22
+
23
+ def filter_rules(rule_array)
24
+ rule_array.each do |rule|
25
+ rule.delete('group_description')
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,39 @@
1
+ class GreenFlag::Admin::WhiteListUsersController < ApplicationController
2
+
3
+ def index
4
+ feature_id = params[:feature_id]
5
+ users = GreenFlag::FeatureDecision.whitelisted_users(feature_id)
6
+
7
+ users.each { |u| u.include_root_in_json = false }
8
+
9
+ respond_to do |format|
10
+ format.js { render :json => users.to_json }
11
+ end
12
+ end
13
+
14
+ def create
15
+ feature_id = params[:feature_id]
16
+ feature = GreenFlag::Feature.find(feature_id)
17
+
18
+ user = User.where(email: params[:email]).first
19
+ GreenFlag::FeatureDecision.whitelist_user!(feature.code, user)
20
+
21
+ user.include_root_in_json = false
22
+
23
+ respond_to do |format|
24
+ format.js { render :json => user.to_json }
25
+ end
26
+ end
27
+
28
+ def destroy
29
+ feature_id = params[:feature_id]
30
+ user_id = params[:id]
31
+
32
+ fd = GreenFlag::FeatureDecision.for_user(user_id).for_feature(feature_id).first
33
+ fd.destroy
34
+
35
+ respond_to do |format|
36
+ format.js { render :json => '' }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,51 @@
1
+ module GreenFlag::SiteVisitorManagement
2
+ COOKIE_NAME = 'green_flag_site_visitor'
3
+
4
+ def self.included(base)
5
+ base.before_filter :set_site_visitor
6
+ base.helper_method :feature_enabled?
7
+ end
8
+
9
+ # Ensure we have a cookie
10
+ def set_site_visitor
11
+ ensure_code_cookie
12
+ record_login(current_user) if current_user
13
+ end
14
+
15
+ # Make sure the current SiteVisitor is the correct visitor for this user
16
+ def record_login(user)
17
+ self.current_site_visitor = GreenFlag::SiteVisitor.for_user!(user, current_site_visitor)
18
+ end
19
+
20
+ # Finds or creates a GreenFlag::SiteVisitor
21
+ def current_site_visitor
22
+ @current_site_visitor ||= begin
23
+ code = ensure_code_cookie
24
+ GreenFlag::SiteVisitor.for_visitor_code!(code)
25
+ end
26
+ end
27
+
28
+ def feature_enabled?(feature_code)
29
+ @features_enabled ||= {}
30
+ if @features_enabled[feature_code].nil?
31
+ visitor_id = current_site_visitor.id
32
+ @features_enabled[feature_code] = GreenFlag::FeatureDecision.feature_enabled?(feature_code, visitor_id)
33
+ end
34
+ @features_enabled[feature_code]
35
+ end
36
+
37
+ private
38
+
39
+ def current_site_visitor=(visitor)
40
+ @current_site_visitor = visitor
41
+ cookies.permanent[COOKIE_NAME] = current_site_visitor.visitor_code
42
+ end
43
+
44
+ def ensure_code_cookie
45
+ unless cookies[COOKIE_NAME]
46
+ cookies.permanent[COOKIE_NAME] = GreenFlag::SiteVisitor.new_code
47
+ end
48
+ cookies[COOKIE_NAME]
49
+ end
50
+
51
+ end
@@ -0,0 +1,4 @@
1
+ module GreenFlag
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,84 @@
1
+ class GreenFlag::Feature < ActiveRecord::Base
2
+ attr_protected #none
3
+
4
+ has_many :rules, :conditions => proc { "version_number = #{self.version_number}" }, order: 'order_by'
5
+ has_many :feature_decisions
6
+ has_many :feature_events
7
+
8
+ validates_presence_of :code
9
+
10
+ self.include_root_in_json = false
11
+
12
+ def self.for_code!(code)
13
+ feature = where(code: code.to_s).first
14
+ unless feature
15
+ feature = create!(code: code.to_s)
16
+ end
17
+ feature
18
+ rescue ActiveRecord::RecordNotUnique, PG::Error => ex
19
+ # Race condition
20
+ where(code: code.to_s).first || raise
21
+ end
22
+
23
+ def decide_if_enabled_for_visitor(site_visitor_id)
24
+ fd_query = GreenFlag::FeatureDecision.where(feature_id: id, site_visitor_id: site_visitor_id)
25
+ feature_decision = fd_query.first_or_initialize
26
+
27
+ if feature_decision.undecided?
28
+ decide_feature_decision(feature_decision)
29
+ end
30
+
31
+ feature_decision
32
+ end
33
+
34
+ def enabled_count
35
+ feature_decisions.enabled.count
36
+ end
37
+
38
+ def disabled_count
39
+ feature_decisions.disabled.count
40
+ end
41
+
42
+ def forget_non_manual_decisions!(enabled)
43
+ non_manual_fds = feature_decisions.non_manual.where(enabled: enabled)
44
+
45
+ if non_manual_fds.count > 0
46
+ create_feature_event(enabled, non_manual_fds.count)
47
+ non_manual_fds.delete_all
48
+ end
49
+ end
50
+
51
+ def latest_version
52
+ last_rule = GreenFlag::Rule.order('version_number DESC').where(feature_id: id).first
53
+
54
+ last_rule.present? ? last_rule.version_number : version_number
55
+ end
56
+
57
+ private
58
+
59
+ def decide_feature_decision(feature_decision)
60
+ matched_rule = rules.find { |rule| rule.applies_to?(feature_decision.site_visitor) }
61
+
62
+ if matched_rule
63
+ feature_decision.rule_id = matched_rule.id
64
+ feature_decision.enabled = !!matched_rule.decision?
65
+ end
66
+
67
+ saved_feature_decision(feature_decision)
68
+ end
69
+
70
+ def saved_feature_decision(feature_decision)
71
+ save_result = feature_decision.safe_save!
72
+
73
+ if save_result == true
74
+ feature_decision
75
+ else
76
+ save_result
77
+ end
78
+ end
79
+
80
+ def create_feature_event(enabled, count)
81
+ event_type_code = (enabled == true ? GreenFlag::FeatureEvent::ENABLED_DECISIONS_FORGOTTEN : GreenFlag::FeatureEvent::DISABLED_DECISIONS_FORGOTTEN)
82
+ GreenFlag::FeatureEvent.create!(feature_id: id, event_type_code: event_type_code, count: count)
83
+ end
84
+ end
@@ -0,0 +1,77 @@
1
+ class GreenFlag::FeatureDecision < ActiveRecord::Base
2
+
3
+ attr_protected #none
4
+
5
+ validates_presence_of :feature_id, :site_visitor_id
6
+
7
+ belongs_to :feature
8
+ belongs_to :site_visitor
9
+ belongs_to :rule
10
+
11
+ has_one :user, through: :site_visitor
12
+
13
+ scope :enabled, -> { where(enabled: true) }
14
+ scope :disabled, -> { where(enabled: false) }
15
+ scope :whitelisted, ->(feature_id) {
16
+ for_feature(feature_id).enabled.where(manual: true).order(:created_at)
17
+ }
18
+ scope :for_user, ->(user_id) {
19
+ joins(:site_visitor).where('green_flag_site_visitors.user_id = ?', user_id)
20
+ }
21
+ scope :for_feature, ->(feature_id) {
22
+ where(feature_id: feature_id)
23
+ }
24
+ scope :non_manual, -> { where('manual is not true') }
25
+
26
+ # Check if the feature is enabled, AND store the result as a FeatureDecision
27
+ def self.feature_enabled?(feature_code, site_visitor_id)
28
+ feature = GreenFlag::Feature.for_code!(feature_code)
29
+ fd = feature.decide_if_enabled_for_visitor(site_visitor_id)
30
+
31
+ fd.enabled
32
+ end
33
+
34
+ def self.feature_enabled_for_user?(feature_code, user)
35
+ feature_enabled? feature_code, GreenFlag::SiteVisitor.for_user!(user).id
36
+ end
37
+
38
+ # Force the given user to have the feature enabled
39
+ def self.whitelist_user!(feature_code, user)
40
+ ensure_feature_enabled(feature_code, user, true)
41
+ end
42
+
43
+ def self.ensure_feature_enabled(feature_code, user, manual=false)
44
+ feature = GreenFlag::Feature.for_code!(feature_code)
45
+ visitor = GreenFlag::SiteVisitor.for_user!(user)
46
+
47
+ fd = GreenFlag::FeatureDecision.where(feature_id: feature.id,
48
+ site_visitor_id: visitor.id).first_or_initialize
49
+
50
+ unless fd.enabled? && fd.manual == manual
51
+ fd.enabled = true
52
+ fd.manual = manual
53
+ fd.save!
54
+ end
55
+ fd
56
+ end
57
+
58
+ def self.whitelisted_users(feature_id)
59
+ # inefficient, but shouldn't matter.
60
+ feature_decisions = whitelisted(feature_id).joins(:site_visitor => :user).all
61
+ users = feature_decisions.map(&:user)
62
+ end
63
+
64
+ def undecided?
65
+ self.enabled.nil?
66
+ end
67
+
68
+ def safe_save!
69
+ begin
70
+ save!
71
+ true
72
+ rescue ActiveRecord::RecordNotUnique, PG::Error => ex
73
+ # Race condition
74
+ self.class.where(feature_id: feature_id, site_visitor_id: site_visitor_id).first || raise
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,9 @@
1
+ class GreenFlag::FeatureEvent < ActiveRecord::Base
2
+ attr_accessible :event_type_code, :feature_id, :count
3
+
4
+ validates :event_type_code, presence: true
5
+ validates :feature_id, presence: true
6
+
7
+ ENABLED_DECISIONS_FORGOTTEN = 0
8
+ DISABLED_DECISIONS_FORGOTTEN = 1
9
+ end
@@ -0,0 +1,66 @@
1
+ class GreenFlag::Rule < ActiveRecord::Base
2
+ attr_protected # none
3
+
4
+ validates :feature_id, presence: true
5
+ validates :group_key, presence: true
6
+ validates :order_by, presence: true
7
+ validates :percentage, presence: true,
8
+ numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
9
+
10
+ self.include_root_in_json = false
11
+
12
+ class << self
13
+ public
14
+
15
+ def set_rules!(feature_id, rules_array)
16
+ return [] if rules_array.empty?
17
+
18
+ rules = create_new_rules(feature_id, rules_array)
19
+
20
+ transaction { rules.each(&:save!) }
21
+
22
+ rules
23
+ end
24
+
25
+ private
26
+
27
+ def create_new_rules(feature_id, rules_array)
28
+ new_version_number = increment_feature_version(feature_id)
29
+
30
+ rules_array.map do |rule_attributes|
31
+ rule_attributes[:feature_id] = feature_id
32
+ create_new_rule_version(new_version_number, rule_attributes)
33
+ end
34
+ end
35
+
36
+ def increment_feature_version(feature_id)
37
+ feature = GreenFlag::Feature.find(feature_id)
38
+
39
+ feature.update_attributes(version_number: feature.latest_version + 1)
40
+ feature.version_number
41
+ end
42
+
43
+ def create_new_rule_version(new_version_number, rule_attributes)
44
+ GreenFlag::Rule.new(rule_attributes.except(:id).merge(version_number: new_version_number))
45
+ end
46
+ end
47
+
48
+ def applies_to?(visitor)
49
+ visitor_group.includes_visitor?(visitor, self)
50
+ end
51
+
52
+ def decision?
53
+ r = Random.rand(100)
54
+ r < percentage
55
+ end
56
+
57
+ def group_description
58
+ visitor_group.description
59
+ end
60
+
61
+ private
62
+
63
+ def visitor_group
64
+ GreenFlag::VisitorGroup.for_key(group_key)
65
+ end
66
+ end
@@ -0,0 +1,49 @@
1
+ # A SiteVisitor is a unique visitor to the site.
2
+ class GreenFlag::SiteVisitor < ActiveRecord::Base
3
+
4
+ attr_protected # none
5
+
6
+ belongs_to :user
7
+ has_many :feature_decisions
8
+
9
+ # Finds, updates, or creates a SiteVisitor for the given user.
10
+ # If the user has an existing SiteVisitor, return that
11
+ # Otherwise, check if the given SiteVisitor can be assigned to the user.
12
+ # Otherwise, create a brand new SiteVisitor
13
+ def self.for_user!(user, visitor_to_check=nil)
14
+ where(user_id: user.id).first ||
15
+ assign_visitor_if_available(visitor_to_check, user) ||
16
+ create!(user_id: user.id, visitor_code: new_code)
17
+ rescue ActiveRecord::RecordNotUnique, PG::Error => ex
18
+ # Race condition - some other request created/updated a SiteVisitor with our user's id
19
+ where(user_id: user.id).first || raise
20
+ end
21
+
22
+ def self.for_visitor_code!(code)
23
+ where(visitor_code: code).first_or_create!
24
+ rescue ActiveRecord::RecordNotUnique, PG::Error => ex
25
+ # Race condition
26
+ where(visitor_code: code).first || raise
27
+ end
28
+
29
+ def self.new_code
30
+ SecureRandom.uuid
31
+ end
32
+
33
+ # If GreenFlag is added to an existing app,
34
+ # then some users might be older than their corresponding SiteVisitors
35
+ def first_visited_at
36
+ [created_at, user.try(:created_at), DateTime.now].compact.min
37
+ end
38
+
39
+ private
40
+
41
+ def self.assign_visitor_if_available(visitor, user)
42
+ if visitor && visitor.user_id.nil?
43
+ visitor.update_attribute(:user_id, user.id)
44
+ return visitor
45
+ end
46
+ nil
47
+ end
48
+
49
+ end
@@ -0,0 +1,8 @@
1
+ class GreenFlag::UserGroup < GreenFlag::VisitorGroup
2
+
3
+ def includes_visitor?(visitor, rule=nil)
4
+ user_exists = !!visitor.user
5
+ user_exists && evaluator.call(visitor.user, rule)
6
+ end
7
+
8
+ end
@@ -0,0 +1,60 @@
1
+ class GreenFlag::VisitorGroup
2
+
3
+ class MultipleGroupsError < StandardError; end
4
+ MUTEX = Mutex.new
5
+
6
+ attr_reader :key, :description
7
+
8
+ def self.define(&block)
9
+ instance_eval(&block)
10
+ end
11
+
12
+ def self.group(key, description=nil, type=GreenFlag::VisitorGroup, &block)
13
+ new_group = nil
14
+ MUTEX.synchronize do
15
+ key_exists = for_key(key)
16
+ if key_exists
17
+ raise MultipleGroupsError.new "Two groups with key :#{key} were defined. Rename one of them!"
18
+ end
19
+ new_group = type.new(key, description, &block)
20
+ @groups ||= []
21
+ @groups << new_group
22
+ end
23
+ new_group
24
+ end
25
+
26
+ def self.user_group(key, description=nil, &block)
27
+ group(key, description, GreenFlag::UserGroup, &block)
28
+ end
29
+
30
+ def self.for_key(key)
31
+ all.find { |g| g.key.to_s == key.to_s }
32
+ end
33
+
34
+ def self.clear!
35
+ @groups = []
36
+ end
37
+
38
+ def self.all
39
+ @groups || []
40
+ end
41
+
42
+ def self.keys
43
+ all.map(&:key)
44
+ end
45
+
46
+ def initialize(key, description=nil, &block)
47
+ self.key = key.to_s
48
+ self.description = description
49
+ self.evaluator = block
50
+ end
51
+
52
+ def includes_visitor?(visitor, rule=nil)
53
+ evaluator.call(visitor, rule)
54
+ end
55
+
56
+ private
57
+ attr_writer :key, :description, :evaluator
58
+ attr_reader :evaluator
59
+
60
+ end
@@ -0,0 +1,14 @@
1
+
2
+ <div class="header clearfix">
3
+ <h1><%= link_to 'Green Flag', admin_features_path %></h1>
4
+ </div>
5
+
6
+ <div class="row">
7
+ <% @features.each do |feature| %>
8
+ <div class="col-md-6">
9
+ <h4><%= link_to feature.code, admin_feature_path(feature) %></h4>
10
+ <p><%= feature.description %></p>
11
+ </div>
12
+ <% end %>
13
+
14
+ </div>