mountain-goat 0.1.8 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (135) hide show
  1. data/README.md +119 -48
  2. data/generators/mg/mg_generator.rb +17 -9
  3. data/generators/mg/templates/create_mountain_goat_tables.rb +61 -25
  4. data/generators/mg/templates/mg.rb +1 -0
  5. data/generators/mg/templates/mountain-goat.yml +22 -3
  6. data/generators/mg/templates/mountain_goat_reports.rake +17 -0
  7. data/generators/mg/templates/update_mountain_goat_tables.rb +104 -0
  8. data/lib/mountain-goat.rb +21 -12
  9. data/lib/mountain-goat/analytics.rb +134 -0
  10. data/lib/mountain-goat/controllers/{mountain_goat/mountain_goat_converts_controller.rb → mg/converts_controller.rb} +12 -11
  11. data/lib/mountain-goat/controllers/{mountain_goat/mountain_goat_metric_variants_controller.rb → mg/metric_variants_controller.rb} +6 -5
  12. data/lib/mountain-goat/controllers/{mountain_goat/mountain_goat_metrics_controller.rb → mg/metrics_controller.rb} +8 -6
  13. data/lib/mountain-goat/controllers/mg/mg.rb +47 -0
  14. data/lib/mountain-goat/controllers/{mountain_goat → mg}/mountain_goat_controller.rb +5 -42
  15. data/lib/mountain-goat/controllers/mg/playground_controller.rb +8 -0
  16. data/lib/mountain-goat/controllers/{mountain_goat/mountain_goat_rallies_controller.rb → mg/rallies_controller.rb} +4 -26
  17. data/lib/mountain-goat/controllers/mg/report_items_controller.rb +82 -0
  18. data/lib/mountain-goat/controllers/mg/reports_controller.rb +90 -0
  19. data/lib/mountain-goat/m_g.rb +64 -0
  20. data/lib/mountain-goat/metric_tracking.rb +192 -74
  21. data/lib/mountain-goat/models/mg/ci_meta.rb +10 -0
  22. data/lib/mountain-goat/models/mg/convert.rb +147 -0
  23. data/lib/mountain-goat/models/mg/convert_meta_type.rb +20 -0
  24. data/lib/mountain-goat/models/mg/cs_meta.rb +10 -0
  25. data/lib/mountain-goat/models/{metric.rb → mg/metric.rb} +3 -5
  26. data/lib/mountain-goat/models/mg/metric_variant.rb +25 -0
  27. data/lib/mountain-goat/models/mg/mountain_goat.rb +3 -0
  28. data/lib/mountain-goat/models/{rally.rb → mg/rally.rb} +4 -3
  29. data/lib/mountain-goat/models/mg/report.rb +11 -0
  30. data/lib/mountain-goat/models/mg/report_item.rb +24 -0
  31. data/lib/mountain-goat/models/mg/report_mailer.rb +18 -0
  32. data/lib/mountain-goat/public/g-bar-min.js +7 -0
  33. data/lib/mountain-goat/public/g-dot-min.js +7 -0
  34. data/lib/mountain-goat/public/g-line-min.js +7 -0
  35. data/lib/mountain-goat/public/g-pie-min.js +1 -0
  36. data/lib/mountain-goat/public/g-raphael-min.js +7 -0
  37. data/lib/mountain-goat/public/jqModel.css +41 -0
  38. data/lib/mountain-goat/public/jqModel.js +69 -0
  39. data/lib/mountain-goat/public/jquery.raphael.js +208 -0
  40. data/lib/mountain-goat/public/mg.css +135 -26
  41. data/lib/mountain-goat/public/mg.js +53 -1
  42. data/lib/mountain-goat/public/raphael-min.js +7 -0
  43. data/lib/mountain-goat/public/utils.js +520 -0
  44. data/lib/mountain-goat/switch_variant.rb +4 -4
  45. data/lib/mountain-goat/version.rb +1 -1
  46. data/lib/mountain-goat/views/mountain_goat/layouts/_pdf.html.erb +15 -0
  47. data/lib/mountain-goat/views/mountain_goat/layouts/mountain_goat.html.erb +17 -5
  48. data/lib/mountain-goat/views/mountain_goat/layouts/xhr.html.erb +2 -0
  49. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_converts → mg/converts}/.tmp_show.html.erb.4433~ +0 -0
  50. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_converts → mg/converts}/_convert_form.html.erb +0 -0
  51. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_converts → mg/converts}/_convert_meta_type_form.html.erb +0 -0
  52. data/lib/mountain-goat/views/mountain_goat/mg/converts/edit.html.erb +13 -0
  53. data/lib/mountain-goat/views/mountain_goat/mg/converts/index.html.erb +25 -0
  54. data/lib/mountain-goat/views/mountain_goat/mg/converts/new.html.erb +13 -0
  55. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_converts → mg/converts}/show.html.erb +3 -28
  56. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metric_variants → mg/metric_variants}/_metric_variant_form.html.erb +0 -4
  57. data/lib/mountain-goat/views/mountain_goat/mg/metric_variants/edit.html.erb +13 -0
  58. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metric_variants → mg/metric_variants}/index.html.erb +3 -3
  59. data/lib/mountain-goat/views/mountain_goat/mg/metric_variants/new.html.erb +15 -0
  60. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metric_variants → mg/metric_variants}/show.html.erb +4 -5
  61. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metrics → mg/metrics}/.tmp_show.html.erb.21270~ +0 -0
  62. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metrics → mg/metrics}/_metric_form.html.erb +0 -2
  63. data/lib/mountain-goat/views/mountain_goat/mg/metrics/edit.html.erb +13 -0
  64. data/lib/mountain-goat/views/mountain_goat/mg/metrics/index.html.erb +14 -0
  65. data/lib/mountain-goat/views/mountain_goat/mg/metrics/new.html.erb +14 -0
  66. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metrics → mg/metrics}/show.html.erb +7 -8
  67. data/lib/mountain-goat/views/mountain_goat/{mountain_goat → mg/mountain_goat}/login.html.erb +0 -0
  68. data/lib/mountain-goat/views/mountain_goat/mg/playground/test.html.erb +14 -0
  69. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/.tmp__rally.html.erb.40484~ +0 -0
  70. data/lib/mountain-goat/views/mountain_goat/mg/rallies/_rallies.html.erb +5 -0
  71. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/_rallies_form.html.erb +0 -0
  72. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/_rally.html.erb +0 -0
  73. data/lib/mountain-goat/views/mountain_goat/mg/rallies/edit.html.erb +13 -0
  74. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/index.html.erb +1 -1
  75. data/lib/mountain-goat/views/mountain_goat/mg/rallies/new.html.erb +13 -0
  76. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/show.html.erb +1 -1
  77. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_chart.html.erb +18 -0
  78. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_report_item_form.html.erb +10 -0
  79. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_report_item_pivot_form.html.erb +14 -0
  80. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_show.html.erb +29 -0
  81. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_svg_chart.html.erb +4 -0
  82. data/lib/mountain-goat/views/mountain_goat/mg/report_items/edit.html.erb +19 -0
  83. data/lib/mountain-goat/views/mountain_goat/mg/report_items/new.html.erb +17 -0
  84. data/lib/mountain-goat/views/mountain_goat/mg/report_mailer/report.html.erb +27 -0
  85. data/lib/mountain-goat/views/mountain_goat/mg/reports/_report.html.erb +22 -0
  86. data/lib/mountain-goat/views/mountain_goat/mg/reports/_report_form.html.erb +21 -0
  87. data/lib/mountain-goat/views/mountain_goat/mg/reports/_report_report_items.html.erb +5 -0
  88. data/lib/mountain-goat/views/mountain_goat/mg/reports/edit.html.erb +36 -0
  89. data/lib/mountain-goat/views/mountain_goat/mg/reports/index.html.erb +26 -0
  90. data/lib/mountain-goat/views/mountain_goat/mg/reports/new.html.erb +17 -0
  91. data/lib/mountain-goat/views/mountain_goat/mg/reports/show.html.erb +21 -0
  92. data/test/fixtures/{ci_metas.yml → mg_ci_metas.yml} +0 -0
  93. data/test/fixtures/{convert_meta_types.yml → mg_convert_meta_types.yml} +0 -0
  94. data/test/fixtures/{converts.yml → mg_converts.yml} +5 -0
  95. data/test/fixtures/{cs_metas.yml → mg_cs_metas.yml} +0 -0
  96. data/test/fixtures/mg_deliveries.yml +9 -0
  97. data/test/fixtures/{metric_variants.yml → mg_metric_variants.yml} +0 -0
  98. data/test/fixtures/{metrics.yml → mg_metrics.yml} +2 -3
  99. data/test/fixtures/{rallies.yml → mg_rallies.yml} +6 -2
  100. data/test/fixtures/mg_report_items.yml +13 -0
  101. data/test/fixtures/mg_reports.yml +15 -0
  102. data/test/mg_convert_test.rb +32 -0
  103. data/test/mg_converts_controller_test.rb +47 -0
  104. data/test/mg_metric_variants_controller_test.rb +46 -0
  105. data/test/mg_metrics_controller_test.rb +52 -0
  106. data/test/mg_mountain_goat_controller_test.rb +45 -0
  107. data/test/mg_mountain_goat_test.rb +392 -0
  108. data/test/mg_playground_controller_test.rb +11 -0
  109. data/test/mg_rallies_controller_test.rb +36 -0
  110. data/test/mg_report_item_test.rb +7 -0
  111. data/test/mg_report_items_controller_test.rb +31 -0
  112. data/test/mg_report_test.rb +18 -0
  113. data/test/mg_reports_controller_test.rb +50 -0
  114. data/test/test_helper.rb +203 -0
  115. metadata +108 -55
  116. data/lib/mountain-goat/models/ci_meta.rb +0 -9
  117. data/lib/mountain-goat/models/convert.rb +0 -70
  118. data/lib/mountain-goat/models/convert_meta_type.rb +0 -19
  119. data/lib/mountain-goat/models/cs_meta.rb +0 -9
  120. data/lib/mountain-goat/models/metric_variant.rb +0 -22
  121. data/lib/mountain-goat/views/mountain_goat/mountain_goat_converts/edit.html.erb +0 -13
  122. data/lib/mountain-goat/views/mountain_goat/mountain_goat_converts/index.html.erb +0 -48
  123. data/lib/mountain-goat/views/mountain_goat/mountain_goat_converts/new.html.erb +0 -13
  124. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metric_variants/edit.html.erb +0 -13
  125. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metric_variants/new.html.erb +0 -15
  126. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metrics/edit.html.erb +0 -13
  127. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metrics/index.html.erb +0 -14
  128. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metrics/new.html.erb +0 -14
  129. data/lib/mountain-goat/views/mountain_goat/mountain_goat_rallies/_rallies.html.erb +0 -5
  130. data/lib/mountain-goat/views/mountain_goat/mountain_goat_rallies/edit.html.erb +0 -13
  131. data/lib/mountain-goat/views/mountain_goat/mountain_goat_rallies/new.html.erb +0 -13
  132. data/test/ocelot_converts_controller_test.rb +0 -45
  133. data/test/ocelot_metric_variants_controller_test.rb +0 -45
  134. data/test/ocelot_metrics_controller_test.rb +0 -45
  135. data/test/ocelot_rallies_controller_test.rb +0 -8
@@ -0,0 +1,47 @@
1
+
2
+ class Mg < ActionController::Base
3
+
4
+ layout 'mountain_goat'
5
+
6
+ before_filter :verify_access
7
+
8
+ self.prepend_view_path File.join([File.dirname(__FILE__), '../../views/mountain_goat/'])
9
+
10
+ private
11
+
12
+ def store_location(url = nil)
13
+ if url.nil?
14
+ if request.method == :post && request.params.count > 0
15
+ session[:mg_return_to] = "#{request.request_uri}?" + encode_parameters(request.params)
16
+ else
17
+ session[:mg_return_to] = request.request_uri
18
+ end
19
+ else
20
+ session[:mg_return_to] = url
21
+ end
22
+
23
+ logger.warn "Storing location: #{session[:mg_return_to]}"
24
+ end
25
+
26
+ def redirect_back_or_default(default)
27
+ redirect_to(session[:mg_return_to] || default)
28
+ session[:mg_return_to] = nil
29
+ end
30
+
31
+ def verify_access
32
+ return if session[:mg_access] == true
33
+ store_location
34
+ redirect_to mg_login_url and return
35
+ end
36
+
37
+ def self.password_digest(password, salt)
38
+ site_key = '1e9532ea39233e1e2786d80fde90d708c0918d2d'
39
+ stretches = 10
40
+ digest = site_key
41
+ stretches.times do
42
+ digest = secure_digest(digest, salt, password, site_key)
43
+ end
44
+ digest
45
+ end
46
+
47
+ end
@@ -1,15 +1,14 @@
1
1
 
2
- class MountainGoatController < ActionController::Base
3
-
4
- layout 'mountain_goat'
2
+ class Mg::MountainGoatController < Mg
5
3
 
6
4
  before_filter :verify_access, :except => [ :login, :login_create, :fetch ]
7
-
8
- self.prepend_view_path File.join([File.dirname(__FILE__), '../../views/mountain_goat/'])
9
5
 
10
6
  def fetch
11
7
  ct = { :png => 'image/png', :css => 'text/css', :html => 'text/html', :js => 'text/javascript' }
12
8
 
9
+ raise ArgumentError, "Invalid fetch file" if params[:file].match(/(([_][_])|([^a-z0-9_]))/ix) #extra security
10
+
11
+ #We will only serve files located in the public directory for security reasons
13
12
  Dir.open(File.join([File.dirname(__FILE__), '../../public/'])).each do |file|
14
13
  if file == params[:file].gsub('_','.')
15
14
  if file =~ /[.]([a-z0-9]+)$/
@@ -21,13 +20,7 @@ class MountainGoatController < ActionController::Base
21
20
  end
22
21
  end
23
22
 
24
- render :file => "#{Rails.root}/public/404.html", :status => :not_found
25
- end
26
-
27
- def verify_access
28
- return if session.has_key?(:mg_access) && session[:mg_access] == true
29
- store_location
30
- redirect_to '/mg/login' and return
23
+ render :file => "#{RAILS_ROOT}/public/404.html", :status => :not_found
31
24
  end
32
25
 
33
26
  def login
@@ -70,34 +63,4 @@ class MountainGoatController < ActionController::Base
70
63
  end
71
64
  end
72
65
 
73
- def store_location(url = nil)
74
- if url.nil?
75
- if request.method == :post && request.params.count > 0
76
- session[:mg_return_to] = "#{request.request_uri}?" + encode_parameters(request.params)
77
- else
78
- session[:mg_return_to] = request.request_uri
79
- end
80
- else
81
- session[:mg_return_to] = url
82
- end
83
-
84
- logger.warn "Storing location: #{session[:mg_return_to]}"
85
- end
86
-
87
- def redirect_back_or_default(default)
88
- redirect_to(session[:mg_return_to] || default)
89
- session[:mg_return_to] = nil
90
- end
91
-
92
- private
93
-
94
- def self.password_digest(password, salt)
95
- site_key = '1e9532ea39233e1e2786d80fde90d708c0918d2d'
96
- stretches = 10
97
- digest = site_key
98
- stretches.times do
99
- digest = secure_digest(digest, salt, password, site_key)
100
- end
101
- digest
102
- end
103
66
  end
@@ -0,0 +1,8 @@
1
+
2
+ class Mg::PlaygroundController < Mg
3
+
4
+ def test
5
+
6
+ end
7
+
8
+ end
@@ -1,9 +1,9 @@
1
- class MountainGoatRalliesController < MountainGoatController
1
+
2
+ class Mg::RalliesController < Mg
2
3
 
3
4
  def index
4
-
5
5
  @page = !params[:page].nil? ? params[:page].to_i : 1
6
- @convert = Convert.find(params[:mountain_goat_convert_id]) if !params[:mountain_goat_convert_id].nil?
6
+ @convert = Convert.find(params[:convert_id]) if !params[:convert_id].nil?
7
7
 
8
8
  if @convert
9
9
  @rallies = @convert.rallies.find(:all, :conditions => { }, :order => "created_at DESC", :limit => 100, :offset => ( @page - 1 ) * 100 )
@@ -28,7 +28,7 @@ class MountainGoatRalliesController < MountainGoatController
28
28
 
29
29
  if @rallies.count > 0
30
30
  render :json => { :success => true,
31
- :result => render_to_string(:partial => 'mountain_goat_rallies/rallies', :locals => { :rallies => @rallies } ),
31
+ :result => render_to_string(:partial => 'mg/rallies/rallies', :locals => { :rallies => @rallies } ),
32
32
  :recent_rally_id => @rallies.first.id }
33
33
  else
34
34
  render :json => { :success => false }
@@ -40,26 +40,4 @@ class MountainGoatRalliesController < MountainGoatController
40
40
  @rally = Rally.find(params[:id])
41
41
  end
42
42
 
43
- def new
44
- if params[:convert_id]
45
- @rally = Rally.new(:convert_id => params[:convert_id])
46
- else
47
- @rally = Rally.new
48
- end
49
-
50
- end
51
-
52
- def create
53
-
54
- end
55
-
56
- def edit
57
- @rally = Rally.find(params[:id])
58
- end
59
-
60
- def update
61
-
62
- end
63
-
64
-
65
43
  end
@@ -0,0 +1,82 @@
1
+ class Mg::ReportItemsController < Mg
2
+
3
+ def get_extra
4
+ ( render :json => { :success => true, :result => "<span></span>" } and return ) if params[:value].blank?
5
+ id, model = params[:value].split('-')
6
+ reportable = model.constantize.find(id)
7
+ render :json => { :success => true, :result => render_to_string( :partial => 'mg/report_items/report_item_pivot_form', :locals => { :reportable => reportable } ) }
8
+ end
9
+
10
+ def new
11
+ @report = Mg::Report.find(params[:report_id])
12
+ raise ArgumentError, "Invalid report" if @report.nil?
13
+
14
+ @report_item = Mg::ReportItem.new
15
+
16
+ render :json => { :success => true,
17
+ :result => render_to_string(:action => :new, :layout => 'xhr') }
18
+ end
19
+
20
+ def create
21
+ @report = Mg::Report.find(params[:report_id])
22
+ raise ArgumentError, "Invalid report" if @report.nil?
23
+
24
+ @report_item = @report.report_items.new(params[:report_item].clone.delete_if { |k, v| k.intern == :reportable || k.intern == :pivot } )
25
+ @report_item.order = @report.report_items.to_a.map { |ri| ri.order }.push(0).max + 1#@report.report_items.maximum(:order) + 1 -- weird sqlite3 bugs
26
+
27
+ if !params[:report_item][:reportable].blank?
28
+ id, model = params[:report_item][:reportable].split('-')
29
+ @report_item.reportable = model.constantize.find(id)
30
+ end
31
+
32
+ if !params[:report_item][:pivot].blank?
33
+ id, model = params[:report_item][:pivot].split('-')
34
+ @report_item.pivot = model.constantize.find(id)
35
+ end
36
+
37
+ if @report_item.save
38
+ render :json => { :success => true,
39
+ :close_dialog => true,
40
+ :result => "<span>Successfully added report item</span>",
41
+ :also => [ { :item => ".report-report-items", :result => render_to_string( :partial => "mg/reports/report_report_items", :locals => { :report => @report_item.report } ) } ] }
42
+ else
43
+ render :json => { :success => true,
44
+ :result => render_to_string(:action => :new, :layout => 'xhr') }
45
+ end
46
+ end
47
+
48
+ def edit
49
+ @report_item = Mg::ReportItem.find(params[:id])
50
+ @report = @report_item.report
51
+
52
+ render :json => { :success => true,
53
+ :result => render_to_string(:action => :edit, :layout => 'xhr') }
54
+ end
55
+
56
+ def update
57
+ @report_item = Mg::ReportItem.find(params[:id])
58
+ @report_item.update_attributes(params[:report_item].clone.delete_if { |k, v| k.intern == :reportable || k.intern == :pivot } )
59
+
60
+ if !params[:report_item][:reportable].blank?
61
+ id, model = params[:report_item][:reportable].split('-')
62
+ @report_item.reportable = model.constantize.find(id)
63
+ end
64
+
65
+ if !params[:report_item][:pivot].blank?
66
+ id, model = params[:report_item][:pivot].split('-')
67
+ @report_item.pivot = model.constantize.find(id)
68
+ end
69
+
70
+ if @report_item.save
71
+ render :json => { :success => true,
72
+ :close_dialog => true,
73
+ :result => "<span>Successfully updated report item</span>",
74
+ :also => [ { :item => ".report-report-items", :result => render_to_string( :partial => "mg/reports/report_report_items", :locals => { :report => @report_item.report }) } ] }
75
+ else
76
+ render :json => { :success => true,
77
+ :result => render_to_string(:action => :edit, :layout => 'xhr') }
78
+ end
79
+ end
80
+
81
+ #TODO: Destroy
82
+ end
@@ -0,0 +1,90 @@
1
+ class Mg::ReportsController < Mg
2
+ # GET /mg_reports
3
+ # GET /mg_reports.xml
4
+ def index
5
+ @reports = Mg::Report.all
6
+
7
+ respond_to do |format|
8
+ format.html # index.html.erb
9
+ format.xml { render :xml => @reports }
10
+ end
11
+ end
12
+
13
+ # GET /mg_reports/1
14
+ # GET /mg_reports/1.xml
15
+ def show
16
+ @report = Mg::Report.find(params[:id])
17
+
18
+ respond_to do |format|
19
+ format.html # show.html.erb
20
+ format.xml { render :xml => @report }
21
+ end
22
+ end
23
+
24
+ def show_svg
25
+ @report = Mg::Report.find(params[:id])
26
+
27
+ render :text => render_to_string(:partial => "mg/reports/report", :layout => '_pdf', :locals => { :report => @report } )
28
+ response.content_type = 'application/xhtml+xml'
29
+ end
30
+
31
+ # GET /mg_reports/new
32
+ # GET /mg_reports/new.xml
33
+ def new
34
+ @report = Mg::Report.new
35
+
36
+ respond_to do |format|
37
+ format.html # new.html.erb
38
+ format.xml { render :xml => @report }
39
+ end
40
+ end
41
+
42
+ # GET /mg_reports/1/edit
43
+ def edit
44
+ @report = Mg::Report.find(params[:id])
45
+ end
46
+
47
+ # POST /mg_reports
48
+ # POST /mg_reports.xml
49
+ def create
50
+ @report = Mg::Report.new(params[:report])
51
+
52
+ respond_to do |format|
53
+ if @report.save
54
+ format.html { flash[:notice] = 'Your report was successfully created, now add some report items.'; redirect_to(edit_mg_report_url @report) }
55
+ format.xml { render :xml => @report, :status => :created, :location => @report }
56
+ else
57
+ format.html { render :action => "new" }
58
+ format.xml { render :xml => @report.errors, :status => :unprocessable_entity }
59
+ end
60
+ end
61
+ end
62
+
63
+ # PUT /mg_reports/1
64
+ # PUT /mg_reports/1.xml
65
+ def update
66
+ @report = Mg::Report.find(params[:id])
67
+
68
+ respond_to do |format|
69
+ if @report.update_attributes(params[:report])
70
+ format.html { redirect_to(@report, :notice => 'Mg::Report was successfully updated.') }
71
+ format.xml { head :ok }
72
+ else
73
+ format.html { render :action => "edit" }
74
+ format.xml { render :xml => @report.errors, :status => :unprocessable_entity }
75
+ end
76
+ end
77
+ end
78
+
79
+ # DELETE /mg_reports/1
80
+ # DELETE /mg_reports/1.xml
81
+ def destroy
82
+ @report = Mg::Report.find(params[:id])
83
+ @report.destroy
84
+
85
+ respond_to do |format|
86
+ format.html { redirect_to(mg_reports_url) }
87
+ format.xml { head :ok }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,64 @@
1
+
2
+ ################
3
+ # Setup PDFKit #
4
+ ################
5
+
6
+ begin
7
+ require 'pdfkit'
8
+ rescue LoadError
9
+ raise "Mountain Goat Reports will not work without the 'pdfkit' gem (please run `gem install pdfkit`)"
10
+ end
11
+
12
+ ######################
13
+ # Setup svg-graph #
14
+ ######################
15
+
16
+ begin
17
+ require 'SVG/Graph/TimeSeries'
18
+ rescue LoadError
19
+ raise "Mountain Goat Reports will not work without the 'svg-graph' gem (please run `gem install svg-graph`)"
20
+ end
21
+
22
+ ############################
23
+ # TODO: Verify Email Setup #
24
+ ############################
25
+
26
+ class MG
27
+ def self.deliver(delivery_set = nil)
28
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
29
+
30
+ if mg_yml.blank? || mg_yml[RAILS_ENV].blank? || mg_yml[RAILS_ENV]['wkhtmltopdf'].blank?
31
+ raise "Please configure wkhtmltopdf settings in #{RAILS_ROOT}/config/mountain-goat.yml"
32
+ end
33
+
34
+ PDFKit.configure do |cz|
35
+ cz.wkhtmltopdf = mg_yml[RAILS_ENV]['wkhtmltopdf']
36
+ cz.default_options = { :'custom-header' => "'Content-Type' 'application/xhtml+xml'" }
37
+ end
38
+
39
+ if delivery_set.nil?
40
+ reports = Mg::Report.all
41
+ else
42
+ reports = Mg::Report.find(:all, :conditions => { :delivery_set => delivery_set.to_s } )
43
+ end
44
+
45
+ #We need to render the report report_show
46
+ reports.each do |report|
47
+ puts "Delivering report: #{report.title}"
48
+ av = ActionView::Base.new
49
+ av.view_paths = File.join([File.dirname(__FILE__), 'views/mountain_goat/'])
50
+ data = av.render(:partial => 'mg/reports/report', :layout => 'layouts/pdf', :locals => { :report => report } )
51
+
52
+ #Oddly, the file extension matters most here
53
+ tmp = Tempfile.new(['chart', '.xhtml'])
54
+ tmp << data
55
+ tmp.flush
56
+
57
+ kit = PDFKit.new(File.new(tmp.path), :page_size => 'Letter')
58
+ pdf = kit.to_pdf
59
+ tmp.close
60
+
61
+ Mg::ReportMailer.deliver_report(report, pdf)
62
+ end
63
+ end
64
+ end
@@ -7,49 +7,165 @@ module MetricTracking
7
7
  define_method :clear!, lambda {}
8
8
  end
9
9
 
10
- #TODO: Namespace?
10
+ Mime::Type.register "application/xhtml+xml", :xhtml
11
+
11
12
  ActionController::Routing::Routes.draw do |map|
12
- map.mg '/mg', :controller => :mountain_goat_converts, :action => :index
13
- map.mg_login '/mg/login', :controller => :mountain_goat, :action => :login
14
- map.mg_login_create '/mg/login/create', :controller => :mountain_goat, :action => :login_create
15
- map.resources :mountain_goat_metric_variants
16
- map.resources :mountain_goat_converts, :has_many => [ :mountain_goat_metrics, :mountain_goat_rallies ]
17
- map.resources :mountain_goat_metrics, :has_many => :mountain_goat_metric_variants
18
- map.resources :mountain_goat_rallies
19
- map.new_rallies '/mg/rallies/new', :controller => :mountain_goat_rallies, :action => :new_rallies
20
- map.fresh_metrics '/fresh-metrics', :controller => :mountain_goat_metrics, :action => :fresh_metrics
21
- map.connect '/mg/public/:file', :controller => :mountain_goat, :action => :fetch
13
+ map.namespace :mg do |mg|
14
+ mg.mg '/mg', :controller => :converts, :action => :index, :path_prefix => ""
15
+ mg.login '/login', :controller => :mountain_goat, :action => :login
16
+ mg.login_create '/login/create', :controller => :mountain_goat, :action => :login_create
17
+ mg.resources :metric_variants
18
+ mg.resources :converts, :has_many => [ :rallies ]
19
+ mg.resources :metrics, :has_many => :metric_variants
20
+ mg.resources :rallies, :collection => { :new_rallies => :get }
21
+ mg.resources :reports, :has_many => :report_items, :member => { :show_svg => :get }
22
+ mg.resources :report_items, :member => { :destroy => :get, :update => :post }, :collection => { :get_extra => :get }
23
+ mg.resources :playground, :collection => { :test => :get }
24
+ mg.new_rallies '/rallies/new', :controller => :rallies, :action => :new_rallies
25
+ mg.fresh_metrics '/fresh-metrics', :controller => :metrics, :action => :fresh_metrics
26
+ mg.connect '/public/:file', :controller => :mountain_goat, :action => :fetch
27
+ end
22
28
  end
23
29
 
24
30
  module Controller
25
31
 
32
+ #This is just for testing
33
+ def mg_rand
34
+ return "(SELECT #{@mg_i.nil? ? 1 : @mg_i.to_f})" if defined?(MOUNTAIN_GOAT_TEST) && MOUNTAIN_GOAT_TEST
35
+ "RAND()"
36
+ end
37
+
38
+ def mg_epsilon
39
+ if @mg_epsilon.nil?
40
+ @mg_epsilon = 0.1 #default
41
+ mg_yml = nil
42
+ begin
43
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
44
+ rescue
45
+ end
46
+ if mg_yml
47
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('epsilon')
48
+ @mg_epsilon = mg_yml[RAILS_ENV]['epsilon'].to_f
49
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('epsilon')
50
+ @mg_epsilon = mg_yml['settings']['epsilon'].to_f
51
+ end
52
+ end
53
+ end
54
+ return @mg_epsilon
55
+ end
56
+
57
+ def mg_strategy
58
+ if @mg_strategy.nil?
59
+ @mg_strategy = 'e-greedy' #default
60
+ mg_yml = nil
61
+ begin
62
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
63
+ rescue
64
+ end
65
+ if mg_yml
66
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('strategy')
67
+ @mg_strategy = mg_yml[RAILS_ENV]['strategy']
68
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('strategy')
69
+ @mg_strategy = mg_yml['settings']['strategy']
70
+ end
71
+ end
72
+ end
73
+ return @mg_strategy
74
+ end
75
+
76
+ def mg_apply_strategy(metric)
77
+ case mg_strategy.downcase
78
+ when 'e-greedy'
79
+ logger.warn Mg::MetricVariant.all(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC, CASE WHEN #{mg_rand} < #{mg_epsilon.to_f} THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC, #{mg_rand} DESC", :conditions => { :metric_id => metric.id } )
80
+ return Mg::MetricVariant.first(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC, CASE WHEN #{mg_rand} < #{mg_epsilon.to_f} THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC, #{mg_rand} DESC", :conditions => { :metric_id => metric.id } )
81
+ when 'e-greedy-decreasing'
82
+ return Mg::MetricVariant.first(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC,
83
+ CASE WHEN #{mg_rand} < #{mg_epsilon.to_f} / ( select sum(served) from mg_metric_variants where metric_id = #{ metric.id.to_i } ) THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC,
84
+ #{mg_rand} DESC", :conditions => { :metric_id => metric.id } ) # * log( ( select sum(served) from mg_metric_variants where metric_id = #{ metric.id.to_i } ) )
85
+ when 'a/b'
86
+ return Mg::MetricVariant.first(:order => "#{mg_rand} DESC", :conditions => { :metric_id => metric.id } )
87
+ else
88
+ raise "Invalid strategy #{mg_strategy}"
89
+ end
90
+ end
91
+
92
+ def mg_storage
93
+ if @mg_storage.nil?
94
+ @mg_storage = defined?(cookies) ? cookies : nil
95
+
96
+ mg_yml = nil
97
+ begin
98
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
99
+ rescue
100
+ end
101
+ if mg_yml
102
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('storage')
103
+ uc = mg_yml[RAILS_ENV]['storage'].strip
104
+ @mg_storage = ( uc == "cookies" && defined?(cookies) ) ? cookies : ( uc == "session" && defined?(session) ) ? session : nil
105
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('storage')
106
+ uc = mg_yml['settings']['storage'].strip
107
+ @mg_storage = ( uc == "cookies" && defined?(cookies) ) ? cookies : ( uc == "session" && defined?(session) ) ? session : nil
108
+ end
109
+ end
110
+ end
111
+ @mg_storage = {} if @mg_storage.nil? #'none'
112
+ return @mg_storage
113
+ end
114
+
26
115
  ######################
27
116
  # Metric Tracking #
28
117
  ######################
29
118
 
30
- def sv(metric_type, convert_type, &block)
119
+ def bds(metric_type, &block)
31
120
  raise ArgumentError, "Switch variant needs block" if !block_given?
32
- metric, convert = get_metric_convert( metric_type, convert_type, true )
33
- block.call(SwitchVariant.new( logger, metric, convert, nil ) )
121
+ metric = get_metric( metric_type, true )
122
+ block.call(SwitchVariant.new( logger, metric, nil ) )
34
123
 
35
- var = get_switch_metric_variant(metric_type, convert_type)
36
- block.call(SwitchVariant.new( logger, metric, convert, var ) )
124
+ var = get_switch_metric_variant( metric_type )
125
+ block.call(SwitchVariant.new( logger, metric, var ) )
126
+ end
127
+
128
+ def bd(metric_type, default, opts = {}, opt = nil)
129
+ return get_metric_variant(metric_type, default, opts, opt)[:value]
130
+ end
131
+
132
+ def bdd(metric_type, default, opts = {}, opt = nil)
133
+ return get_metric_variant(metric_type, default, opts, opt)
134
+ end
135
+
136
+ #Legacy
137
+ def sv(metric_type, convert_type, &block)
138
+ bds(metric_type, &block)
37
139
  end
38
140
 
39
141
  def mv(metric_type, convert_type, default, opts = {}, opt = nil)
40
- return get_metric_variant(metric_type, convert_type, default, opts, opt)[:value]
142
+ bd(metric_type, default, opts, opt)
41
143
  end
42
144
 
43
145
  def mv_detailed(metric_type, convert_type, default, opts = {}, opt = nil)
44
- return get_metric_variant(metric_type, convert_type, default, opts, opt)
146
+ bdd(metric_type, default, opts, opt)
45
147
  end
46
148
 
47
149
  #shorthand
150
+ def rw(convert_type, reward, options = {})
151
+ self.bandit_reward(convert_type, reward, options)
152
+ end
153
+
48
154
  def rc(convert_type, options = {})
49
- self.record_conversion(convert_type, options)
155
+ self.bandit_reward(convert_type, 1, options)
50
156
  end
51
157
 
52
158
  def record_conversion(convert_type, options = {})
159
+ self.bandit_reward(convert_type, 1, options)
160
+ end
161
+
162
+ #allows bandit_reward(convert, options)
163
+ def bandit_reward(convert_type, reward, options = {})
164
+
165
+ if reward.is_a?(Hash) #allow arguments bandit_reward(convert, options)
166
+ options = reward
167
+ reward = 0
168
+ end
53
169
 
54
170
  metrics = {} #for user-defined metrics
55
171
  options = options.with_indifferent_access
@@ -71,18 +187,18 @@ module MetricTracking
71
187
 
72
188
  logger.warn "Recording conversion #{convert_type.to_s} with options #{options.inspect}"
73
189
 
74
- convert = Convert.first( :conditions => { :convert_type => convert_type.to_s } )
190
+ convert = Mg::Convert.first( :conditions => { :convert_type => convert_type.to_s } )
75
191
 
76
192
  #now, we just create the convert if we don't have one
77
- convert = Convert.create!( :convert_type => convert_type.to_s, :name => convert_type.to_s ) if convert.nil?
78
-
193
+ convert = Mg::Convert.create!( :convert_type => convert_type.to_s, :name => convert_type.to_s, :reward => reward ) if convert.nil?
194
+
79
195
  #first, let's tally for the conversion itself
80
196
  #we need to see what meta information we should fill based on the conversion type
81
- Rally.create!( { :convert_id => convert.id } ).set_meta_data(options)
197
+ Mg::Rally.create!( { :convert_id => convert.id } ).set_meta_data(options)
82
198
 
83
199
  #User-defined metric tallies
84
200
  metrics.each do |metric_type, variant_id|
85
- m = Metric.find_by_metric_type(metric_type)
201
+ m = Mg::Metric.find_by_metric_type(metric_type)
86
202
  if m.nil?
87
203
  logger.warn "Missing user-defined metric #{metric_type}"
88
204
  next
@@ -96,27 +212,27 @@ module MetricTracking
96
212
  end
97
213
 
98
214
  logger.warn "Tallying conversion #{convert.name} for #{m.title} - #{v.name} (#{v.value} - #{v.id})"
99
- v.tally_convert
215
+ v.tally_convert(convert, reward)
100
216
  end
101
217
 
102
- if defined?(cookies)
103
- #we just converted, let's tally each of our metrics (from cookies)
104
- convert.metrics.each do |metric|
218
+ if !mg_storage.nil?
219
+ #we just converted, let's tally each of our metrics (from cookies or session)
220
+ Mg::Metric.all.each do |metric|
105
221
  metric_sym = "metric_#{metric.metric_type}".to_sym
106
222
  metric_variant_sym = "metric_#{metric.metric_type}_variant".to_sym
107
223
 
108
- value = cookies[metric_sym]
109
- variant_id = cookies[metric_variant_sym]
224
+ value = mg_storage[metric_sym]
225
+ variant_id = mg_storage[metric_variant_sym]
110
226
 
111
227
  #logger.warn "Value: #{metric_sym} - #{value}"
112
228
  #logger.warn "Value: #{metric_variant_sym} - #{variant_id}"
113
229
 
114
230
  if variant_id.blank? #the user just doesn't have this set
115
- logger.error "No variant found for #{metric.title}"
231
+ #This is now common-case
116
232
  next
117
233
  end
118
234
 
119
- variant = MetricVariant.first(:conditions => { :id => variant_id.to_i } )
235
+ variant = Mg::MetricVariant.first(:conditions => { :id => variant_id.to_i } )
120
236
 
121
237
  if variant.nil?
122
238
  logger.error "Variant #{variant_id} not in metric variants for #{metric.title}"
@@ -128,7 +244,7 @@ module MetricTracking
128
244
  end
129
245
 
130
246
  logger.warn "Tallying conversion #{convert.name} for #{metric.title} - #{variant.name} (#{variant.value} - #{variant.id})"
131
- variant.tally_convert
247
+ variant.tally_convert(convert, reward)
132
248
  end
133
249
  end
134
250
  end
@@ -136,15 +252,15 @@ module MetricTracking
136
252
  private
137
253
 
138
254
  #returns a map { :value => value, :variant_id => id }
139
- def get_metric_variant(metric_type, convert_type, default, opts = {}, opt = nil)
255
+ def get_metric_variant(metric_type, default, opts = {}, opt = nil)
140
256
  metric_sym = "metric_#{metric_type}#{ opt.nil? ? "" : '_' + opt.to_s }".to_sym
141
257
  metric_variant_sym = "metric_#{metric_type}_variant".to_sym
142
258
 
143
259
  #first, we'll check for a cookie value
144
- if defined?(cookies) && cookies[metric_sym] && !cookies[metric_sym].blank?
260
+ if !mg_storage.nil? && mg_storage[metric_sym] && !mg_storage[metric_sym].blank?
145
261
  #we have the cookie
146
- variant_id = cookies[metric_variant_sym]
147
- variant = MetricVariant.first(:conditions => { :id => variant_id.to_i } )
262
+ variant_id = mg_storage[metric_variant_sym]
263
+ variant = Mg::MetricVariant.first(:conditions => { :id => variant_id.to_i } )
148
264
  if !variant.nil?
149
265
  if variant.metric.tally_each_serve
150
266
  variant.tally_serve
@@ -153,46 +269,44 @@ module MetricTracking
153
269
  logger.warn "Serving metric #{metric_type} #{ opt.nil? ? "" : opt.to_s } without finding / tallying variant."
154
270
  end
155
271
 
156
- return { :value => cookies[metric_sym], :variant_id => cookies[metric_variant_sym] } #it's the best we can do
272
+ return { :value => mg_storage[metric_sym], :variant_id => mg_storage[metric_variant_sym] } #it's the best we can do
157
273
  else
158
274
  #we don't have the cookie, let's find a value to set
159
- metric, convert = get_metric_convert( metric_type, convert_type, false )
275
+ metric = get_metric( metric_type, false )
160
276
 
161
- #to use RAND(), let's not use metrics.metric_variants
162
- sum_priority = MetricVariant.first(:select => "SUM(priority) as sum_priority", :conditions => { :metric_id => metric.id } ).sum_priority.to_f
163
-
164
- if sum_priority > 0.0
165
- metric_variant = MetricVariant.first(:order => "RAND() * ( priority / #{sum_priority.to_f} ) DESC", :conditions => { :metric_id => metric.id } )
166
- end
277
+ metric_variant = mg_apply_strategy(metric)
167
278
 
168
279
  if metric_variant.nil?
169
280
  logger.warn "Missing metric variants for #{metric_type}"
170
- metric_variant = MetricVariant.create!( { :metric_id => metric.id, :value => default, :name => default }.merge(opts) )
281
+ metric_variant = Mg::MetricVariant.create!( { :metric_id => metric.id, :value => default, :name => default }.merge(opts) )
282
+ end
283
+
284
+ if metric_variant.metric.tally_each_serve
285
+ metric_variant.tally_serve #donate we served this to a user
171
286
  end
172
287
 
173
- metric_variant.tally_serve #donate we served this to a user
174
288
  value = metric_variant.read_attribute( opt.nil? ? :value : opt )
175
289
  logger.debug "Serving #{metric_variant.name} (#{value}) for #{metric_sym}"
176
290
  #good, we have a variant, let's store it in session
177
291
 
178
- if defined?(cookies)
179
- cookies[metric_sym] = { :value => value } #, :domain => WILD_DOMAIN
180
- cookies[metric_variant_sym] = { :value => metric_variant.id } #, :domain => WILD_DOMAIN
292
+ if !mg_storage.nil?
293
+ mg_storage[metric_sym] = value #, :domain => WILD_DOMAIN
294
+ mg_storage[metric_variant_sym] = metric_variant.id #, :domain => WILD_DOMAIN
181
295
  end
182
296
 
183
297
  return { :value => value, :variant_id => metric_variant.id }
184
298
  end
185
299
  end
186
300
 
187
- def get_switch_metric_variant(metric_type, convert_type)
301
+ def get_switch_metric_variant(metric_type)
188
302
  metric_variant_sym = "metric_#{metric_type}_variant".to_sym
189
303
 
190
304
  #first, we'll check for a cookie selection
191
- if defined?(cookies) && cookies[metric_variant_sym] && !cookies[metric_variant_sym].blank?
305
+ if !mg_storage.nil? && mg_storage[metric_variant_sym] && !mg_storage[metric_variant_sym].blank?
192
306
  #we have the cookie
193
307
 
194
- variant_id = cookies[metric_variant_sym]
195
- variant = MetricVariant.first(:conditions => { :id => variant_id.to_i } )
308
+ variant_id = mg_storage[metric_variant_sym]
309
+ variant = Mg::MetricVariant.first(:conditions => { :id => variant_id.to_i } )
196
310
  if !variant.nil?
197
311
 
198
312
  if variant.metric.tally_each_serve
@@ -208,46 +322,38 @@ module MetricTracking
208
322
  end
209
323
 
210
324
  #we don't have the cookie, let's find a value to set
211
- metric, convert = get_metric_convert( metric_type, convert_type, true )
325
+ metric = get_metric( metric_type, true )
212
326
 
213
- #to use RAND(), let's not use metrics.metric_variants
214
- sum_priority = MetricVariant.first(:select => "SUM(priority) as sum_priority", :conditions => { :metric_id => metric.id } ).sum_priority.to_f
215
-
216
- if sum_priority > 0.0
217
- metric_variant = MetricVariant.first(:order => "RAND() * ( priority / #{sum_priority.to_f} ) DESC", :conditions => { :metric_id => metric.id } )
218
- end
327
+ metric_variant = mg_apply_strategy(metric)
219
328
 
220
329
  if metric_variant.nil?
221
330
  logger.warn "Missing metric variants for #{metric_type}"
222
331
  raise ArgumentError, "Missing variants for switch-type #{metric_type}"
223
332
  end
224
333
 
225
- metric_variant.tally_serve #donate we served this to a user
334
+ if metric_variant.metric.tally_each_serve
335
+ metric_variant.tally_serve #donate we served this to a user
336
+ end
337
+
226
338
  logger.debug "Serving #{metric_variant.name} (#{metric_variant.switch_type}) for #{metric.title} (switch-type)"
227
339
  #good, we have a variant, let's store it in session (not the value, just the selection)
228
- if defined?(cookies)
229
- cookies[metric_variant_sym] = { :value => metric_variant.id } #, :domain => WILD_DOMAIN
340
+ if !mg_storage.nil?
341
+ mg_storage[metric_variant_sym] = metric_variant.id #, :domain => WILD_DOMAIN
230
342
  end
231
343
 
232
344
  return metric_variant
233
345
  end
234
346
 
235
- def get_metric_convert(metric_type, convert_type, is_switch = false)
347
+ def get_metric(metric_type, is_switch = false)
236
348
 
237
- metric = Metric.first(:conditions => { :metric_type => metric_type.to_s } )
349
+ metric = Mg::Metric.first(:conditions => { :metric_type => metric_type.to_s } )
238
350
 
239
- conv = Convert.find_by_convert_type( convert_type.to_s )
240
- if conv.nil?
241
- logger.warn "Missing convert type #{convert_type.to_s} -- creating"
242
- conv = Convert.create( :convert_type => convert_type.to_s, :name => convert_type.to_s ) if conv.nil?
243
- end
244
-
245
351
  if metric.nil? #we don't have a metric of this type
246
352
  logger.warn "Missing metric type #{metric_type.to_s} -- creating"
247
- metric = Metric.create( :metric_type => metric_type.to_s, :title => metric_type.to_s, :convert_id => conv.id, :is_switch => is_switch )
353
+ metric = Mg::Metric.create( :metric_type => metric_type.to_s, :title => metric_type.to_s, :is_switch => is_switch )
248
354
  end
249
355
 
250
- return metric, conv
356
+ return metric
251
357
  end
252
358
  end
253
359
 
@@ -263,6 +369,18 @@ module MetricTracking
263
369
  def sv(*args, &block)
264
370
  @controller.send(:sv, *args, &block)
265
371
  end
372
+
373
+ def bd(*args, &block)
374
+ @controller.send(:bd, *args, &block)
375
+ end
376
+
377
+ def bdd(*args, &block)
378
+ @controller.send(:bdd, *args, &block)
379
+ end
380
+
381
+ def bds(*args, &block)
382
+ @controller.send(:bds, *args, &block)
383
+ end
266
384
  end
267
385
  end
268
386