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 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  
... ...
config/initializers/issue_trackers.rb 0 → 100644
... ... @@ -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 @@
  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 +
... ...