Commit 8367790f4e542a600174fc7183ddba723baa4b51
1 parent
4a1146c7
Exists in
master
and in
1 other branch
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.
Showing
9 changed files
with
161 additions
and
15 deletions
Show diff stats
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 |
@@ -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 | + |