Commit 8367790f4e542a600174fc7183ddba723baa4b51

Authored by Nathan Broadbent
1 parent 4a1146c7
Exists in master and in 1 other branch production

Added ThoughtWorks Mingle issue tracker. (http://www.thoughtworks-studios.com/mi…

…ngle-agile-project-management). Also did some general tidy up of messages/css/etc relating to issue trackers.
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,13 +62,15 @@ @@ -62,13 +62,15 @@
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 - %div.tracker_params.lighthouseapp{:class => lighthouse_tracker?(w.object) ? 'chosen' : nil} 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')
  67 + %div.tracker_params.lighthouseapp{:class => lighthouseapp_tracker?(w.object) ? 'chosen' : nil}
66 = w.label :account, "Account" 68 = w.label :account, "Account"
67 = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com" 69 = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com"
68 = w.label :api_token, "API token" 70 = w.label :api_token, "API token"
69 = w.text_field :api_token, :placeholder => "API Token for your account" 71 = w.text_field :api_token, :placeholder => "API Token for your account"
70 = w.label :project_id, "Project ID" 72 = w.label :project_id, "Project ID"
71 - = w.text_field :project_id, :placeholder => "123 from abc.lighthouseapp.com/projects/123" 73 + = w.text_field :project_id
72 %div.tracker_params.redmine{:class => redmine_tracker?(w.object) ? 'chosen' : nil} 74 %div.tracker_params.redmine{:class => redmine_tracker?(w.object) ? 'chosen' : nil}
73 = w.label :account, "Redmine URL" 75 = w.label :account, "Redmine URL"
74 = w.text_field :account, :placeholder => "like http://www.redmine.org/" 76 = w.text_field :account, :placeholder => "like http://www.redmine.org/"
@@ -86,8 +88,19 @@ @@ -86,8 +88,19 @@
86 = w.text_field :project_id 88 = w.text_field :project_id
87 = w.label :account, "FogBugz URL" 89 = w.label :account, "FogBugz URL"
88 = w.text_field :account, :placeholder => "abc from http://abc.fogbugz.com/" 90 = w.text_field :account, :placeholder => "abc from http://abc.fogbugz.com/"
89 - = w.label :username, 'account username' 91 + = w.label :username, 'Username'
90 = w.text_field :username, :placeholder => 'Username/Email for your account' 92 = w.text_field :username, :placeholder => 'Username/Email for your account'
91 - = w.label :password, 'account password' 93 + = w.label :password, 'Password'
  94 + = w.password_field :password, :placeholder => 'Password for your account'
  95 + %div.tracker_params.mingle{:class => mingle_tracker?(w.object) ? 'chosen' : nil}
  96 + = w.label :account, "Mingle URL"
  97 + = w.text_field :account, :placeholder => "http://mingle.yoursite.com/"
  98 + = w.label :project_id, "Project ID"
  99 + = w.text_field :project_id
  100 + = w.label :ticket_properties, "Card Properties (comma separated key=value pairs)"
  101 + = w.text_field :ticket_properties, :placeholder => "card_type = Defect, defect_status = Open, priority = Essential"
  102 + = w.label :username, 'Sign-in name'
  103 + = w.text_field :username, :placeholder => 'Sign-in name for your account'
  104 + = w.label :password, 'Password'
92 = w.password_field :password, :placeholder => 'Password for your account' 105 = w.password_field :password, :placeholder => 'Password for your account'
93 106
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
@@ -518,7 +518,7 @@ a.button.active { @@ -518,7 +518,7 @@ a.button.active {
518 } 518 }
519 519
520 /* Watchers and Issue Tracker Forms */ 520 /* Watchers and Issue Tracker Forms */
521 -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 { 521 +div.nested.watcher .user, div.nested.watcher .email, div.issue_tracker.nested .tracker_params {
522 display: none; 522 display: none;
523 } 523 }
524 div.nested.watcher .choosen, div.nested.issue_tracker .chosen { 524 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
@@ -438,3 +472,4 @@ describe ErrsController do @@ -438,3 +472,4 @@ describe ErrsController do
438 end 472 end
439 end 473 end
440 end 474 end
  475 +
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 +