Commit 96ab0e3fa5bd3788f9ec32d748811c93c1add22a

Authored by Nathan Broadbent
2 parents e79cae05 8367790f
Exists in master and in 1 other branch production

Merge branch 'thoughtworks_mingle'

Conflicts:
	app/helpers/application_helper.rb
	app/views/apps/_fields.html.haml
app/helpers/application_helper.rb
1 module ApplicationHelper 1 module ApplicationHelper
2 2
3 -  
4 - def lighthouse_tracker? object  
5 - object.issue_tracker_type == "lighthouseapp"  
6 - end  
7 -  
8 def user_agent_graph(error) 3 def user_agent_graph(error)
9 tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)} 4 tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)}
10 create_percentage_table(tallies, :total => error.notices.count) 5 create_percentage_table(tallies, :total => error.notices.count)
@@ -39,16 +34,11 @@ module ApplicationHelper @@ -39,16 +34,11 @@ module ApplicationHelper
39 object.issue_tracker_type == "none" 34 object.issue_tracker_type == "none"
40 end 35 end
41 36
42 - def redmine_tracker? object  
43 - object.issue_tracker_type == "redmine"  
44 - end  
45 -  
46 - def pivotal_tracker? object  
47 - object.issue_tracker_type == "pivotal" 37 + %w(lighthouseapp redmine pivotal fogbugz mingle).each do |tracker|
  38 + define_method("#{tracker}_tracker?".to_sym) do |object|
  39 + object.issue_tracker_type == tracker
  40 + end
48 end 41 end
49 42
50 - def fogbugz_tracker? object  
51 - object.issue_tracker_type == 'fogbugz'  
52 - end  
53 end 43 end
54 44
app/models/app.rb
@@ -36,7 +36,7 @@ class App @@ -36,7 +36,7 @@ class App
36 accepts_nested_attributes_for :watchers, :allow_destroy => true, 36 accepts_nested_attributes_for :watchers, :allow_destroy => true,
37 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } 37 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
38 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, 38 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
39 - :reject_if => proc { |attrs| !%w(none lighthouseapp redmine pivotal fogbugz).include?(attrs[:issue_tracker_type]) } 39 + :reject_if => proc { |attrs| !%w(none lighthouseapp redmine pivotal fogbugz mingle).include?(attrs[:issue_tracker_type]) }
40 40
41 # Mongoid Bug: find(id) on association proxies returns an Enumerator 41 # Mongoid Bug: find(id) on association proxies returns an Enumerator
42 def self.find_by_id!(app_id) 42 def self.find_by_id!(app_id)
app/models/issue_tracker.rb
@@ -12,6 +12,7 @@ class IssueTracker @@ -12,6 +12,7 @@ class IssueTracker
12 field :account, :type => String 12 field :account, :type => String
13 field :api_token, :type => String 13 field :api_token, :type => String
14 field :project_id, :type => String 14 field :project_id, :type => String
  15 + field :ticket_properties, :type => String
15 field :username, :type => String 16 field :username, :type => String
16 field :password, :type => String 17 field :password, :type => String
17 field :issue_tracker_type, :type => String, :default => 'none' 18 field :issue_tracker_type, :type => String, :default => 'none'
@@ -26,6 +27,17 @@ class IssueTracker @@ -26,6 +27,17 @@ class IssueTracker
26 create_pivotal_issue err 27 create_pivotal_issue err
27 when 'fogbugz' 28 when 'fogbugz'
28 create_fogbugz_issue err 29 create_fogbugz_issue err
  30 + when 'mingle'
  31 + create_mingle_issue err
  32 + end
  33 + end
  34 +
  35 + def ticket_properties_hash
  36 + # Parses 'key=value, key2=value2' from user input into a ruby hash.
  37 + self.ticket_properties.split(",").inject({}) do |hash, pair|
  38 + key, value = pair.split("=").map(&:strip)
  39 + hash[key] = value
  40 + hash
29 end 41 end
30 end 42 end
31 43
@@ -84,29 +96,54 @@ class IssueTracker @@ -84,29 +96,54 @@ class IssueTracker
84 err.update_attribute :issue_link, "https://#{account}.fogbugz.com/default.asp?#{fb_resp['case']['ixBug']}" 96 err.update_attribute :issue_link, "https://#{account}.fogbugz.com/default.asp?#{fb_resp['case']['ixBug']}"
85 end 97 end
86 98
  99 + def create_mingle_issue err
  100 + properties = ticket_properties_hash
  101 + basic_auth = account.gsub(/https?:\/\//, "https://#{username}:#{password}@")
  102 + Mingle.set_site "#{basic_auth}/api/v1/projects/#{project_id}/"
  103 +
  104 + card = Mingle::Card.new
  105 + card.card_type_name = properties.delete("card_type")
  106 + card.name = issue_title(err)
  107 + card.description = self.class.mingle_body_template.result(binding)
  108 + properties.each do |property, value|
  109 + card.send("cp_#{property}=", value)
  110 + end
  111 +
  112 + card.save!
  113 + err.update_attribute :issue_link, URI.parse("#{account}/projects/#{project_id}/cards/#{card.id}").to_s
  114 + end
  115 +
87 def issue_title err 116 def issue_title err
88 "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}" 117 "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}"
89 end 118 end
90 119
91 def check_params 120 def check_params
92 blank_flag_fields = %w(project_id) 121 blank_flag_fields = %w(project_id)
93 - if(%w(fogbugz).include?(issue_tracker_type)) 122 + if %w(fogbugz mingle).include?(issue_tracker_type)
94 blank_flag_fields += %w(username password) 123 blank_flag_fields += %w(username password)
95 else 124 else
96 blank_flag_fields << 'api_token' 125 blank_flag_fields << 'api_token'
97 end 126 end
98 - blank_flag_fields << 'account' if(%w(fogbugz lighthouseapp redmine).include?(issue_tracker_type)) 127 + blank_flag_fields << 'account' if(%w(fogbugz lighthouseapp redmine mingle).include?(issue_tracker_type))
99 blank_flags = blank_flag_fields.map {|m| self[m].blank? } 128 blank_flags = blank_flag_fields.map {|m| self[m].blank? }
  129 +
  130 + if issue_tracker_type == "mingle"
  131 + # Check that mingle was given a 'card_type' in the ticket_properties
  132 + blank_flags << "card_type" unless ticket_properties_hash["card_type"]
  133 + end
  134 +
100 if blank_flags.any? && !blank_flags.all? 135 if blank_flags.any? && !blank_flags.all?
101 message = case issue_tracker_type 136 message = case issue_tracker_type
102 when 'lighthouseapp' 137 when 'lighthouseapp'
103 - 'You must specify your Lighthouseapp account, api token and project id' 138 + 'You must specify your Lighthouseapp account, API token and Project ID'
104 when 'redmine' 139 when 'redmine'
105 - 'You must specify your Redmine url, api token and project id' 140 + 'You must specify your Redmine URL, API token and Project ID'
106 when 'pivotal' 141 when 'pivotal'
107 - 'You must specify your Pivotal Tracker api token and project id' 142 + 'You must specify your Pivotal Tracker API token and Project ID'
108 when 'fogbugz' 143 when 'fogbugz'
109 'You must specify your FogBugz Area Name, Username, and Password' 144 'You must specify your FogBugz Area Name, Username, and Password'
  145 + when 'mingle'
  146 + 'You must specify your Mingle URL, Project ID, Card Type (in default card properties), Sign-in name, and Password'
110 end 147 end
111 errors.add(:base, message) 148 errors.add(:base, message)
112 end 149 end
@@ -128,6 +165,11 @@ class IssueTracker @@ -128,6 +165,11 @@ class IssueTracker
128 def fogbugz_body_template 165 def fogbugz_body_template
129 @@fogbugz_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/fogbugz_body.txt.erb")) 166 @@fogbugz_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/fogbugz_body.txt.erb"))
130 end 167 end
  168 +
  169 + def mingle_body_template
  170 + # Mingle also uses textile markup, so the redmine template is perfect.
  171 + redmine_body_template
  172 + end
131 end 173 end
132 end 174 end
133 175
app/views/apps/_fields.html.haml
@@ -62,15 +62,17 @@ @@ -62,15 +62,17 @@
62 = label_tag :issue_tracker_type_pivotal, 'Pivotal Tracker', :for => label_for_attr(w, 'issue_tracker_type_pivotal') 62 = label_tag :issue_tracker_type_pivotal, 'Pivotal Tracker', :for => label_for_attr(w, 'issue_tracker_type_pivotal')
63 = w.radio_button :issue_tracker_type, :fogbugz 63 = w.radio_button :issue_tracker_type, :fogbugz
64 = label_tag :issue_tracker_type_fogbugz, 'FogBugz', :for => label_for_attr(w, 'issue_tracker_type_fogbugz') 64 = label_tag :issue_tracker_type_fogbugz, 'FogBugz', :for => label_for_attr(w, 'issue_tracker_type_fogbugz')
  65 + = w.radio_button :issue_tracker_type, :mingle
  66 + = label_tag :issue_tracker_type_fogbugz, 'Mingle', :for => label_for_attr(w, 'issue_tracker_type_mingle')
65 %div.tracker_params.none{:class => no_tracker?(w.object) ? 'chosen' : nil} 67 %div.tracker_params.none{:class => no_tracker?(w.object) ? 'chosen' : nil}
66 %p When no issue tracker has been configured, you will be able to leave comments on errors. 68 %p When no issue tracker has been configured, you will be able to leave comments on errors.
67 - %div.tracker_params.lighthouseapp{:class => lighthouse_tracker?(w.object) ? 'chosen' : nil} 69 + %div.tracker_params.lighthouseapp{:class => lighthouseapp_tracker?(w.object) ? 'chosen' : nil}
68 = w.label :account, "Account" 70 = w.label :account, "Account"
69 = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com" 71 = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com"
70 = w.label :api_token, "API token" 72 = w.label :api_token, "API token"
71 = w.text_field :api_token, :placeholder => "API Token for your account" 73 = w.text_field :api_token, :placeholder => "API Token for your account"
72 = w.label :project_id, "Project ID" 74 = w.label :project_id, "Project ID"
73 - = w.text_field :project_id, :placeholder => "123 from abc.lighthouseapp.com/projects/123" 75 + = w.text_field :project_id
74 %div.tracker_params.redmine{:class => redmine_tracker?(w.object) ? 'chosen' : nil} 76 %div.tracker_params.redmine{:class => redmine_tracker?(w.object) ? 'chosen' : nil}
75 = w.label :account, "Redmine URL" 77 = w.label :account, "Redmine URL"
76 = w.text_field :account, :placeholder => "like http://www.redmine.org/" 78 = w.text_field :account, :placeholder => "like http://www.redmine.org/"
@@ -88,8 +90,19 @@ @@ -88,8 +90,19 @@
88 = w.text_field :project_id 90 = w.text_field :project_id
89 = w.label :account, "FogBugz URL" 91 = w.label :account, "FogBugz URL"
90 = w.text_field :account, :placeholder => "abc from http://abc.fogbugz.com/" 92 = w.text_field :account, :placeholder => "abc from http://abc.fogbugz.com/"
91 - = w.label :username, 'account username' 93 + = w.label :username, 'Username'
92 = w.text_field :username, :placeholder => 'Username/Email for your account' 94 = w.text_field :username, :placeholder => 'Username/Email for your account'
93 - = w.label :password, 'account password' 95 + = w.label :password, 'Password'
  96 + = w.password_field :password, :placeholder => 'Password for your account'
  97 + %div.tracker_params.mingle{:class => mingle_tracker?(w.object) ? 'chosen' : nil}
  98 + = w.label :account, "Mingle URL"
  99 + = w.text_field :account, :placeholder => "http://mingle.yoursite.com/"
  100 + = w.label :project_id, "Project ID"
  101 + = w.text_field :project_id
  102 + = w.label :ticket_properties, "Card Properties (comma separated key=value pairs)"
  103 + = w.text_field :ticket_properties, :placeholder => "card_type = Defect, defect_status = Open, priority = Essential"
  104 + = w.label :username, 'Sign-in name'
  105 + = w.text_field :username, :placeholder => 'Sign-in name for your account'
  106 + = w.label :password, 'Password'
94 = w.password_field :password, :placeholder => 'Password for your account' 107 = w.password_field :password, :placeholder => 'Password for your account'
95 108
config/initializers/issue_trackers.rb 0 → 100644
@@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
  1 +require Rails.root.join('lib/issue_trackers/mingle.rb')
  2 +
lib/issue_trackers/mingle.rb 0 → 100644
@@ -0,0 +1,14 @@ @@ -0,0 +1,14 @@
  1 +module Mingle
  2 + class Card < ActiveResource::Base
  3 + # site template ~> "https://username:password@mingle.example.com/api/v1/projects/:project_id/"
  4 + end
  5 + def self.set_site(site)
  6 + # ActiveResource seems to clone and freeze the @site variable
  7 + # after the first use. It seems that the only way to change @site
  8 + # is to drop the subclass, and then reload it.
  9 + Mingle.send(:remove_const, :Card)
  10 + load File.join(Rails.root,'lib','issue_trackers','mingle.rb')
  11 + Mingle::Card.site = site
  12 + end
  13 +end
  14 +
public/stylesheets/application.css
@@ -525,7 +525,7 @@ a.button.active { @@ -525,7 +525,7 @@ a.button.active {
525 } 525 }
526 526
527 /* Watchers and Issue Tracker Forms */ 527 /* Watchers and Issue Tracker Forms */
528 -div.nested.watcher .user, div.nested.watcher .email, div.issue_tracker.nested .lighthouseapp, div.issue_tracker.nested .redmine, div.issue_tracker.nested .pivotal, div.issue_tracker.nested .fogbugz { 528 +div.nested.watcher .user, div.nested.watcher .email, div.issue_tracker.nested .tracker_params {
529 display: none; 529 display: none;
530 } 530 }
531 div.nested.watcher .choosen, div.nested.issue_tracker .chosen { 531 div.nested.watcher .choosen, div.nested.issue_tracker .chosen {
spec/controllers/apps_controller_spec.rb
@@ -297,7 +297,7 @@ describe AppsController do @@ -297,7 +297,7 @@ describe AppsController do
297 @app.reload 297 @app.reload
298 298
299 @app.issue_tracker.should be_nil 299 @app.issue_tracker.should be_nil
300 - response.body.should match(/You must specify your Lighthouseapp account, api token and project id/) 300 + response.body.should match(/You must specify your Lighthouseapp account, API token and Project ID/)
301 end 301 end
302 end 302 end
303 303
@@ -322,7 +322,7 @@ describe AppsController do @@ -322,7 +322,7 @@ describe AppsController do
322 @app.reload 322 @app.reload
323 323
324 @app.issue_tracker.should be_nil 324 @app.issue_tracker.should be_nil
325 - response.body.should match(/You must specify your Redmine url, api token and project id/) 325 + response.body.should match(/You must specify your Redmine URL, API token and Project ID/)
326 end 326 end
327 end 327 end
328 328
@@ -344,7 +344,7 @@ describe AppsController do @@ -344,7 +344,7 @@ describe AppsController do
344 @app.reload 344 @app.reload
345 345
346 @app.issue_tracker.should be_nil 346 @app.issue_tracker.should be_nil
347 - response.body.should match(/You must specify your Pivotal Tracker api token and project id/) 347 + response.body.should match(/You must specify your Pivotal Tracker API token and Project ID/)
348 end 348 end
349 end 349 end
350 350
@@ -375,6 +375,37 @@ describe AppsController do @@ -375,6 +375,37 @@ describe AppsController do
375 end 375 end
376 end 376 end
377 end 377 end
  378 +
  379 + context "mingle" do
  380 + context 'with correct params' do
  381 + before do
  382 + put :update, :id => @app.id, :app => { :issue_tracker_attributes => {
  383 + :issue_tracker_type => 'mingle', :project_id => 'test', :account => 'http://mingle.example.com',
  384 + :username => '1234', :password => '123123', :ticket_properties => "card_type = Defect"
  385 + } }
  386 + @app.reload
  387 + end
  388 +
  389 + subject {@app.issue_tracker}
  390 + its(:issue_tracker_type) {should == 'mingle'}
  391 + its(:project_id) {should == 'test'}
  392 + its(:username) {should == '1234'}
  393 + its(:password) {should == '123123'}
  394 + end
  395 +
  396 + it "should show validation notice when sufficient params are not present" do
  397 + put :update, :id => @app.id, :app => { :issue_tracker_attributes => {
  398 + :issue_tracker_type => 'mingle', :project_id => 'test', :account => 'http://mingle.example.com',
  399 + :username => '1234', :password => '1234', :ticket_properties => "cards_type = Defect"
  400 + } }
  401 + @app.reload
  402 +
  403 + @app.issue_tracker.should be_nil
  404 + response.body.should match(/You must specify your Mingle URL, Project ID, Card Type \(in default card properties\), Sign-in name, and Password/)
  405 + end
  406 + end
  407 +
  408 +
378 end 409 end
379 end 410 end
380 411
spec/controllers/errs_controller_spec.rb
@@ -330,7 +330,7 @@ describe ErrsController do @@ -330,7 +330,7 @@ describe ErrsController do
330 end 330 end
331 end 331 end
332 332
333 - context "redmine tracker" do 333 + context "pivotal tracker" do
334 let(:notice) { Factory :notice } 334 let(:notice) { Factory :notice }
335 let(:tracker) { Factory :pivotal_tracker, :app => notice.err.app } 335 let(:tracker) { Factory :pivotal_tracker, :app => notice.err.app }
336 let(:err) { notice.err } 336 let(:err) { notice.err }
@@ -362,6 +362,40 @@ describe ErrsController do @@ -362,6 +362,40 @@ describe ErrsController do
362 err.issue_link.should == @issue_link.sub(/\.xml/, '') 362 err.issue_link.should == @issue_link.sub(/\.xml/, '')
363 end 363 end
364 end 364 end
  365 +
  366 + context "mingle tracker" do
  367 + let(:notice) { Factory :notice }
  368 + let(:tracker) { Factory :mingle_tracker, :app => notice.err.app }
  369 + let(:err) { notice.err }
  370 +
  371 + before(:each) do
  372 + number = 5
  373 + @issue_link = "#{tracker.account}/projects/#{tracker.project_id}/cards/#{number}.xml"
  374 + @basic_auth = tracker.account.gsub("https://", "https://#{tracker.username}:#{tracker.password}@")
  375 + body = "<card><id type=\"integer\">#{number}</id></card>"
  376 + stub_request(:post, "#{@basic_auth}/api/v1/projects/#{tracker.project_id}/cards.xml").
  377 + to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
  378 +
  379 + post :create_issue, :app_id => err.app.id, :id => err.id
  380 + err.reload
  381 + end
  382 +
  383 + it "should make request to Mingle with err params" do
  384 + requested = have_requested(:post, "#{@basic_auth}/api/v1/projects/#{tracker.project_id}/cards.xml")
  385 + WebMock.should requested.with(:headers => {'Content-Type' => 'application/xml'})
  386 + WebMock.should requested.with(:body => /FooError: Too Much Bar/)
  387 + WebMock.should requested.with(:body => /See this exception on Errbit/)
  388 + WebMock.should requested.with(:body => /<card-type-name>Defect<\/card-type-name>/)
  389 + end
  390 +
  391 + it "should redirect to err page" do
  392 + response.should redirect_to( app_err_path(err.app, err) )
  393 + end
  394 +
  395 + it "should create issue link for err" do
  396 + err.issue_link.should == @issue_link.sub(/\.xml$/, '')
  397 + end
  398 + end
365 end 399 end
366 400
367 context "absent issue tracker" do 401 context "absent issue tracker" do
spec/factories/issue_tracker_factories.rb
@@ -17,3 +17,12 @@ end @@ -17,3 +17,12 @@ end
17 Factory.define :pivotal_tracker, :parent => :generic_tracker do |e| 17 Factory.define :pivotal_tracker, :parent => :generic_tracker do |e|
18 e.issue_tracker_type 'pivotal' 18 e.issue_tracker_type 'pivotal'
19 end 19 end
  20 +
  21 +Factory.define :mingle_tracker, :parent => :generic_tracker do |t|
  22 + t.issue_tracker_type 'mingle'
  23 + t.account "https://mingle.example.com"
  24 + t.ticket_properties 'card_type = Defect, defect_status = open, priority = essential'
  25 + t.username "test_user"
  26 + t.password "test_password"
  27 +end
  28 +