green_flag 0.1.0

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