Commit 96ab0e3fa5bd3788f9ec32d748811c93c1add22a
Exists in
master
and in
1 other branch
Merge branch 'thoughtworks_mingle'
Conflicts: app/helpers/application_helper.rb app/views/apps/_fields.html.haml
Showing
10 changed files
with
164 additions
and
29 deletions
Show diff stats
app/helpers/application_helper.rb
1 | 1 | module ApplicationHelper |
2 | 2 | |
3 | - | |
4 | - def lighthouse_tracker? object | |
5 | - object.issue_tracker_type == "lighthouseapp" | |
6 | - end | |
7 | - | |
8 | 3 | def user_agent_graph(error) |
9 | 4 | tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)} |
10 | 5 | create_percentage_table(tallies, :total => error.notices.count) |
... | ... | @@ -39,16 +34,11 @@ module ApplicationHelper |
39 | 34 | object.issue_tracker_type == "none" |
40 | 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 | 41 | end |
49 | 42 | |
50 | - def fogbugz_tracker? object | |
51 | - object.issue_tracker_type == 'fogbugz' | |
52 | - end | |
53 | 43 | end |
54 | 44 | ... | ... |
app/models/app.rb
... | ... | @@ -36,7 +36,7 @@ class App |
36 | 36 | accepts_nested_attributes_for :watchers, :allow_destroy => true, |
37 | 37 | :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } |
38 | 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 | 41 | # Mongoid Bug: find(id) on association proxies returns an Enumerator |
42 | 42 | def self.find_by_id!(app_id) | ... | ... |
app/models/issue_tracker.rb
... | ... | @@ -12,6 +12,7 @@ class IssueTracker |
12 | 12 | field :account, :type => String |
13 | 13 | field :api_token, :type => String |
14 | 14 | field :project_id, :type => String |
15 | + field :ticket_properties, :type => String | |
15 | 16 | field :username, :type => String |
16 | 17 | field :password, :type => String |
17 | 18 | field :issue_tracker_type, :type => String, :default => 'none' |
... | ... | @@ -26,6 +27,17 @@ class IssueTracker |
26 | 27 | create_pivotal_issue err |
27 | 28 | when 'fogbugz' |
28 | 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 | 41 | end |
30 | 42 | end |
31 | 43 | |
... | ... | @@ -84,29 +96,54 @@ class IssueTracker |
84 | 96 | err.update_attribute :issue_link, "https://#{account}.fogbugz.com/default.asp?#{fb_resp['case']['ixBug']}" |
85 | 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 | 116 | def issue_title err |
88 | 117 | "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}" |
89 | 118 | end |
90 | 119 | |
91 | 120 | def check_params |
92 | 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 | 123 | blank_flag_fields += %w(username password) |
95 | 124 | else |
96 | 125 | blank_flag_fields << 'api_token' |
97 | 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 | 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 | 135 | if blank_flags.any? && !blank_flags.all? |
101 | 136 | message = case issue_tracker_type |
102 | 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 | 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 | 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 | 143 | when 'fogbugz' |
109 | 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 | 147 | end |
111 | 148 | errors.add(:base, message) |
112 | 149 | end |
... | ... | @@ -128,6 +165,11 @@ class IssueTracker |
128 | 165 | def fogbugz_body_template |
129 | 166 | @@fogbugz_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/fogbugz_body.txt.erb")) |
130 | 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 | 173 | end |
132 | 174 | end |
133 | 175 | ... | ... |
app/views/apps/_fields.html.haml
... | ... | @@ -62,15 +62,17 @@ |
62 | 62 | = label_tag :issue_tracker_type_pivotal, 'Pivotal Tracker', :for => label_for_attr(w, 'issue_tracker_type_pivotal') |
63 | 63 | = w.radio_button :issue_tracker_type, :fogbugz |
64 | 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 | 67 | %div.tracker_params.none{:class => no_tracker?(w.object) ? 'chosen' : nil} |
66 | 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 | 70 | = w.label :account, "Account" |
69 | 71 | = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com" |
70 | 72 | = w.label :api_token, "API token" |
71 | 73 | = w.text_field :api_token, :placeholder => "API Token for your account" |
72 | 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 | 76 | %div.tracker_params.redmine{:class => redmine_tracker?(w.object) ? 'chosen' : nil} |
75 | 77 | = w.label :account, "Redmine URL" |
76 | 78 | = w.text_field :account, :placeholder => "like http://www.redmine.org/" |
... | ... | @@ -88,8 +90,19 @@ |
88 | 90 | = w.text_field :project_id |
89 | 91 | = w.label :account, "FogBugz URL" |
90 | 92 | = w.text_field :account, :placeholder => "abc from http://abc.fogbugz.com/" |
91 | - = w.label :username, 'account username' | |
93 | + = w.label :username, 'Username' | |
92 | 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 | 107 | = w.password_field :password, :placeholder => 'Password for your account' |
95 | 108 | ... | ... |
... | ... | @@ -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 | 525 | } |
526 | 526 | |
527 | 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 | 529 | display: none; |
530 | 530 | } |
531 | 531 | div.nested.watcher .choosen, div.nested.issue_tracker .chosen { | ... | ... |
spec/controllers/apps_controller_spec.rb
... | ... | @@ -297,7 +297,7 @@ describe AppsController do |
297 | 297 | @app.reload |
298 | 298 | |
299 | 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 | 301 | end |
302 | 302 | end |
303 | 303 | |
... | ... | @@ -322,7 +322,7 @@ describe AppsController do |
322 | 322 | @app.reload |
323 | 323 | |
324 | 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 | 326 | end |
327 | 327 | end |
328 | 328 | |
... | ... | @@ -344,7 +344,7 @@ describe AppsController do |
344 | 344 | @app.reload |
345 | 345 | |
346 | 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 | 348 | end |
349 | 349 | end |
350 | 350 | |
... | ... | @@ -375,6 +375,37 @@ describe AppsController do |
375 | 375 | end |
376 | 376 | end |
377 | 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 | 409 | end |
379 | 410 | end |
380 | 411 | ... | ... |
spec/controllers/errs_controller_spec.rb
... | ... | @@ -330,7 +330,7 @@ describe ErrsController do |
330 | 330 | end |
331 | 331 | end |
332 | 332 | |
333 | - context "redmine tracker" do | |
333 | + context "pivotal tracker" do | |
334 | 334 | let(:notice) { Factory :notice } |
335 | 335 | let(:tracker) { Factory :pivotal_tracker, :app => notice.err.app } |
336 | 336 | let(:err) { notice.err } |
... | ... | @@ -362,6 +362,40 @@ describe ErrsController do |
362 | 362 | err.issue_link.should == @issue_link.sub(/\.xml/, '') |
363 | 363 | end |
364 | 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 | 399 | end |
366 | 400 | |
367 | 401 | context "absent issue tracker" do | ... | ... |
spec/factories/issue_tracker_factories.rb
... | ... | @@ -17,3 +17,12 @@ end |
17 | 17 | Factory.define :pivotal_tracker, :parent => :generic_tracker do |e| |
18 | 18 | e.issue_tracker_type 'pivotal' |
19 | 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 | + | ... | ... |