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 | + | ... | ... |