Commit bd17fa780fb7f38eb201c96fc4a87f9f8e597a55

Authored by Nick Recobra
2 parents d95174a2 dd383c8f
Exists in master and in 1 other branch production

Merge branch 'feature/redmine'

Gemfile
... ... @@ -7,6 +7,7 @@ gem 'haml'
7 7 gem 'will_paginate'
8 8 gem 'devise', '~> 1.1.8'
9 9 gem 'lighthouse-api'
  10 +gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git"
10 11  
11 12 platform :ruby do
12 13 gem 'bson_ext', '~> 1.2'
... ...
Gemfile.lock
  1 +GIT
  2 + remote: git://github.com/oruen/redmine_client.git
  3 + revision: 0df20a8b695869b03cfa129560b938a0af346add
  4 + specs:
  5 + redmine_client (0.0.1)
  6 + activeresource (>= 2.3.0)
  7 +
1 8 GEM
2 9 remote: http://rubygems.org/
3 10 specs:
... ... @@ -122,6 +129,7 @@ DEPENDENCIES
122 129 mongoid (~> 2.0.0.rc.7)
123 130 nokogiri
124 131 rails (= 3.0.5)
  132 + redmine_client!
125 133 rspec (~> 2.5)
126 134 rspec-rails (~> 2.5)
127 135 webmock
... ...
README.md
... ... @@ -94,7 +94,14 @@ Lighthouseapp integration
94 94  
95 95 * Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview
96 96 * Errbit uses token-based authentication. Get your API Token or visit [http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token](http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token) to learn how to get it.
97   -* Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview
  97 +* Project id is number identifier of your project, i.e. **73466** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview
  98 +
  99 +Redmine integration
  100 +-------------------------
  101 +
  102 +* Account is the host of your redmine installation, i.e. **http://redmine.org**
  103 +* Errbit uses token-based authentication. Get your API Key or visit [http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication](http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication) to learn how to get it.
  104 +* Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject
98 105  
99 106 TODO
100 107 ----
... ...
app/controllers/errs_controller.rb
... ... @@ -36,7 +36,8 @@ class ErrsController < ApplicationController
36 36 flash[:error] = "This up has no issue tracker setup."
37 37 end
38 38 redirect_to app_err_path(@app, @err)
39   - rescue ActiveResource::ConnectionError
  39 + rescue ActiveResource::ConnectionError => e
  40 + Rails.logger.error e.to_s
40 41 flash[:error] = "There was an error during issue creation. Check your tracker settings or try again later."
41 42 redirect_to app_err_path(@app, @err)
42 43 end
... ...
app/helpers/application_helper.rb
1 1 module ApplicationHelper
  2 + def lighthouse_tracker? object
  3 + object.issue_tracker_type == "lighthouseapp"
  4 + end
2 5 end
... ...
app/models/app.rb
... ... @@ -33,7 +33,7 @@ class App
33 33 accepts_nested_attributes_for :watchers, :allow_destroy => true,
34 34 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
35 35 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
36   - :reject_if => proc { |attrs| !%w( lighthouseapp ).include?(attrs[:issue_tracker_type]) }
  36 + :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) }
37 37  
38 38 # Mongoid Bug: find(id) on association proxies returns an Enumerator
39 39 def self.find_by_id!(app_id)
... ...
app/models/issue_tracker.rb
... ... @@ -5,7 +5,7 @@ class IssueTracker
5 5 include Rails.application.routes.url_helpers
6 6 default_url_options[:host] = Errbit::Application.config.action_mailer.default_url_options[:host]
7 7  
8   - validate :check_lighthouseapp_params
  8 + validate :check_params
9 9  
10 10 embedded_in :app, :inverse_of => :issue_tracker
11 11  
... ... @@ -15,6 +15,26 @@ class IssueTracker
15 15 field :issue_tracker_type, :type => String, :default => 'lighthouseapp'
16 16  
17 17 def create_issue err
  18 + return create_lighthouseapp_issue err if issue_tracker_type == 'lighthouseapp'
  19 + create_redmine_issue err if issue_tracker_type == 'redmine'
  20 + end
  21 +
  22 + protected
  23 + def create_redmine_issue err
  24 + token = api_token
  25 + acc = account
  26 + RedmineClient::Base.configure do
  27 + self.token = token
  28 + self.site = acc
  29 + end
  30 + issue = RedmineClient::Issue.new(:project_id => project_id)
  31 + issue.subject = issue_title err
  32 + issue.description = self.class.redmine_body_template.result(binding)
  33 + issue.save!
  34 + err.update_attribute :issue_link, "#{RedmineClient::Issue.site.to_s.sub(/#{RedmineClient::Issue.site.path}$/, '')}#{RedmineClient::Issue.element_path(issue.id, :project_id => project_id)}".sub(/\.xml\?project_id=#{project_id}$/, "\?project_id=#{project_id}")
  35 + end
  36 +
  37 + def create_lighthouseapp_issue err
18 38 Lighthouse.account = account
19 39 Lighthouse.token = api_token
20 40  
... ... @@ -22,75 +42,38 @@ class IssueTracker
22 42 Lighthouse::Ticket.site
23 43  
24 44 ticket = Lighthouse::Ticket.new(:project_id => project_id)
25   - ticket.title = "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}"
  45 + ticket.title = issue_title err
26 46  
27   - ticket.body = ""
28   - ticket.body += "[See this exception on Errbit](#{ app_err_url err.app, err } \"See this exception on Errbit\")"
29   - ticket.body += "\n"
30   - if notice = err.notices.first
31   - ticket.body += "# #{notice.message} #"
32   - ticket.body += "\n"
33   - ticket.body += "## Summary ##"
34   - ticket.body += "\n"
35   - if notice.request['url'].present?
36   - ticket.body += "### URL ###"
37   - ticket.body += "\n"
38   - ticket.body += "[#{notice.request['url']}](#{notice.request['url']})"
39   - ticket.body += "\n"
40   - end
41   - ticket.body += "### Where ###"
42   - ticket.body += "\n"
43   - ticket.body += notice.err.where
44   - ticket.body += "\n"
45   -
46   - ticket.body += "### Occured ###"
47   - ticket.body += "\n"
48   - ticket.body += notice.created_at.to_s(:micro)
49   - ticket.body += "\n"
50   -
51   - ticket.body += "### Similar ###"
52   - ticket.body += "\n"
53   - ticket.body += (notice.err.notices.count - 1).to_s
54   - ticket.body += "\n"
55   -
56   - ticket.body += "## Params ##"
57   - ticket.body += "\n"
58   - ticket.body += "<code>#{pretty_hash(notice.params)}</code>"
59   - ticket.body += "\n"
60   -
61   - ticket.body += "## Session ##"
62   - ticket.body += "\n"
63   - ticket.body += "<code>#{pretty_hash(notice.session)}</code>"
64   - ticket.body += "\n"
65   -
66   - ticket.body += "## Backtrace ##"
67   - ticket.body += "\n"
68   - ticket.body += "<code>"
69   - for line in notice.backtrace
70   - ticket.body += "#{line['number']}: #{line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> **#{line['method']}**"
71   - ticket.body += "\n"
72   - end
73   - ticket.body += "</code>"
74   - ticket.body += "\n"
75   -
76   - ticket.body += "## Environment ##"
77   - ticket.body += "\n"
78   - for key, val in notice.env_vars
79   - ticket.body += "#{key}: #{val}"
80   - end
81   - ticket.body += "\n"
82   - end
  47 + ticket.body = self.class.lighthouseapp_body_template.result(binding)
83 48  
84 49 ticket.tags << "errbit"
85 50 ticket.save!
86 51 err.update_attribute :issue_link, "#{Lighthouse::Ticket.site.to_s.sub(/#{Lighthouse::Ticket.site.path}$/, '')}#{Lighthouse::Ticket.element_path(ticket.id, :project_id => project_id)}".sub(/\.xml$/, '')
87 52 end
88 53  
89   - protected
90   - def check_lighthouseapp_params
  54 + def issue_title err
  55 + "[#{ err.environment }][#{ err.where }] #{err.message.to_s.truncate(100)}"
  56 + end
  57 +
  58 + def check_params
91 59 blank_flags = %w( api_token project_id account ).map {|m| self[m].blank? }
92 60 if blank_flags.any? && !blank_flags.all?
93   - errors.add(:base, "You must specify your Lighthouseapp account, token and project id")
  61 + message = if issue_tracker_type == 'lighthouseapp'
  62 + "You must specify your Lighthouseapp account, api token and project id"
  63 + else
  64 + "You must specify your Redmine url, api token and project id"
  65 + end
  66 + errors.add(:base, message)
  67 + end
  68 + end
  69 +
  70 + class << self
  71 + def lighthouseapp_body_template
  72 + @@lighthouseapp_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, ''))
  73 + end
  74 +
  75 + def redmine_body_template
  76 + @@redmine_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb"))
94 77 end
95 78 end
96 79 end
... ...
app/views/apps/_fields.html.haml
... ... @@ -33,15 +33,24 @@
33 33 %fieldset
34 34 %legend Issue tracker
35 35 = f.fields_for :issue_tracker do |w|
36   - %div.watcher.nested
  36 + %div.issue_tracker.nested
37 37 %div.choose
38 38 = w.radio_button :issue_tracker_type, :lighthouseapp
39 39 = label_tag :issue_tracker_type_lighthouseapp, 'Lighthouse', :for => label_for_attr(w, 'issue_tracker_type_lighthouseapp')
40   - %div.lighthouseapp{:class => 'choosen'}
  40 + = w.radio_button :issue_tracker_type, :redmine
  41 + = label_tag :issue_tracker_type_redmine, 'Redmine', :for => label_for_attr(w, 'issue_tracker_type_redmine')
  42 + %div.tracker_params{:class => lighthouse_tracker?(w.object) ? 'choosen' : nil}
41 43 = w.label :account, "Account"
42   - = w.text_field :account
  44 + = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com"
  45 + = w.label :api_token, "API token"
  46 + = w.text_field :api_token, :placeholder => "API Token for your account"
  47 + = w.label :project_id, "Project ID"
  48 + = w.text_field :project_id, :placeholder => "123 from abc from abc.lighthouseapp.com/projects/123"
  49 + %div.tracker_params{:class => lighthouse_tracker?(w.object) ? nil : 'choosen'}
  50 + = w.label :account, "Redmine URL"
  51 + = w.text_field :account, :placeholder => "like http://www.redmine.org/"
43 52 = w.label :api_token, "API token"
44   - = w.text_field :api_token
  53 + = w.text_field :api_token, :placeholder => "API Token for your account"
45 54 = w.label :project_id, "Project ID"
46 55 = w.text_field :project_id
47 56  
... ...
app/views/errs/lighthouseapp_body.txt.erb 0 → 100644
... ... @@ -0,0 +1,34 @@
  1 +[See this exception on Errbit](<%= app_err_url err.app, err %> "See this exception on Errbit")
  2 +<% if notice = err.notices.first %>
  3 + # <%= notice.message %> #
  4 + ## Summary ##
  5 + <% if notice.request['url'].present? %>
  6 + ### URL ###
  7 + [<%= notice.request['url'] %>](<%= notice.request['url'] %>)"
  8 + <% end %>
  9 + ### Where ###
  10 + <%= notice.err.where %>
  11 +
  12 + ### Occured ###
  13 + <%= notice.created_at.to_s(:micro) %>
  14 +
  15 + ### Similar ###
  16 + <%= (notice.err.notices.count - 1).to_s %>
  17 +
  18 + ## Params ##
  19 + <code><%= pretty_hash(notice.params) %></code>
  20 +
  21 + ## Session ##
  22 + <code><%= pretty_hash(notice.session) %></code>
  23 +
  24 + ## Backtrace ##
  25 + <code>
  26 + <% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].sub(/^\[PROJECT_ROOT\]/, '') %> -> **<%= line['method'] %>**
  27 + <% end %>
  28 + </code>
  29 +
  30 + ## Environment ##
  31 + <% for key, val in notice.env_vars %>
  32 + <%= key %>: <%= val %>
  33 + <% end %>
  34 +<% end %>
... ...
app/views/errs/redmine_body.txt.erb 0 → 100644
... ... @@ -0,0 +1,45 @@
  1 +"See this exception on Errbit":<%= app_err_url err.app, err %>
  2 +<% if notice = err.notices.first %>
  3 +h1. <%= notice.message %>
  4 +
  5 +h2. Summary
  6 +<% if notice.request['url'].present? %>
  7 +h3. URL
  8 +
  9 +"<%= notice.request['url'] %>":<%= notice.request['url'] %>
  10 +<% end %>
  11 +h3. Where
  12 +
  13 +<%= notice.err.where %>
  14 +
  15 +h3. Occured
  16 +
  17 +<%= notice.created_at.to_s(:micro) %>
  18 +
  19 +h3. Similar
  20 +
  21 +<%= (notice.err.notices.count - 1).to_s %>
  22 +
  23 +h2. Params
  24 +
  25 +<pre><%= pretty_hash(notice.params) %></pre>
  26 +
  27 +h2. Session
  28 +
  29 +<pre><%= pretty_hash(notice.session) %></pre>
  30 +
  31 +h2. Backtrace
  32 +
  33 +<pre>
  34 +<% for line in notice.backtrace %><%= line['number'] %>: <%= line['file'].sub(/^\[PROJECT_ROOT\]/, '') %> -> *<%= line['method'] %>*
  35 +<% end %>
  36 +</pre>
  37 +
  38 +h2. Environment
  39 +
  40 +<pre>
  41 +<% for key, val in notice.env_vars %>
  42 +<%= key %>: <%= val %>
  43 +<% end %>
  44 +</pre>
  45 +<% end %>
... ...
app/views/errs/show.html.haml
... ... @@ -12,10 +12,10 @@
12 12 - content_for :action_bar do
13 13 - if @err.app.issue_tracker
14 14 - if @err.issue_link.blank?
15   - %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => 'create-issue'
  15 + %span= link_to 'create issue', create_issue_app_err_path(@app, @err), :method => :post, :class => "#{@app.issue_tracker.issue_tracker_type}_create create-issue"
16 16 - else
17   - %span= link_to 'go to issue', @err.issue_link, :class => 'goto-issue'
18   - = link_to 'clear issue', clear_issue_app_err_path(@app, @err), :method => :delete, :confirm => "Clear err issues?", :class => 'clear-issue'
  17 + %span= link_to 'go to issue', @err.issue_link, :class => "#{@app.issue_tracker.issue_tracker_type}_create goto-issue"
  18 + = link_to 'clear issue', clear_issue_app_err_path(@app, @err), :method => :delete, :confirm => "Clear err issues?", :class => "clear-issue"
19 19 - if @err.unresolved?
20 20 %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve'
21 21  
... ...
public/images/lighthouseapp_create.png 0 → 100644

1.57 KB

public/images/lighthousehouseapp_goto.png 0 → 100644

1.61 KB

public/images/redmine_create.png 0 → 100644

1.47 KB

public/images/redmine_goto.png 0 → 100644

1.52 KB

public/javascripts/form.js
... ... @@ -3,6 +3,9 @@ $(function(){
3 3  
4 4 if($('div.watcher.nested').length)
5 5 activateWatcherTypeSelector();
  6 +
  7 + if($('div.issue_tracker.nested').length)
  8 + activateIssueTrackerTypeSelector();
6 9 });
7 10  
8 11 function activateNestedForms() {
... ... @@ -67,4 +70,20 @@ function activateWatcherTypeSelector() {
67 70 wrapper.find('div.choosen').removeClass('choosen');
68 71 wrapper.find('div.'+choosen).addClass('choosen');
69 72 });
  73 +}
  74 +
  75 +function activateIssueTrackerTypeSelector() {
  76 + var not_choosen = $("div.tracker_params").filter(function () {
  77 + return !$(this).hasClass("choosen");
  78 + });
  79 + window.hiddenTracker = not_choosen.html();
  80 + not_choosen.remove();
  81 + $('div.issue_tracker input[name*=issue_tracker_type]').live('click', function(){
  82 + var choosen = $(this).val();
  83 + var wrapper = $(this).closest('.nested');
  84 + var tmp;
  85 + tmp = wrapper.find('div.choosen').html();
  86 + wrapper.find('div.choosen').html(window.hiddenTracker);
  87 + window.hiddenTracker = tmp;
  88 + });
70 89 }
71 90 \ No newline at end of file
... ...
public/stylesheets/application.css
... ... @@ -501,14 +501,15 @@ a.button.active {
501 501 margin-right: 14px;
502 502 }
503 503  
504   -/* Watchers Form */
505   -div.nested.watcher .user, div.nested.watcher .email {
  504 +/* Watchers and Issue Tracker Forms */
  505 +div.nested.watcher .user, div.nested.watcher .email, div.issue_tracker.nested .lighthouseapp, div.issue_tracker.nested .redmine {
506 506 display: none;
507 507 }
508   -div.nested.watcher .choosen {
  508 +div.nested.watcher .choosen, div.nested.issue_tracker .choosen {
509 509 display: block;
510 510 }
511   -div.nested.watcher .choose {
  511 +
  512 +div.nested.watcher .choose, div.nested.issue_tracker .choose {
512 513 margin-bottom: 0.5em;
513 514 }
514 515  
... ... @@ -578,6 +579,22 @@ table.errs tr.resolved td &gt; * {
578 579 background: transparent url(images/icons/thumbs-up.png) 6px 5px no-repeat;
579 580 }
580 581  
  582 +#action-bar a.lighthouseapp_create {
  583 + background: transparent url(/images/lighthouseapp_create.png) 6px 5px no-repeat;
  584 +}
  585 +
  586 +#action-bar a.redmine_create {
  587 + background: transparent url(/images/redmine_create.png) 6px 5px no-repeat;
  588 +}
  589 +
  590 +#action-bar a.lighthouseapp_goto {
  591 + background: transparent url(/images/lighthouseapp_goto.png) 6px 5px no-repeat;
  592 +}
  593 +
  594 +#action-bar a.redmine_goto {
  595 + background: transparent url(/images/redmine_goto.png) 6px 5px no-repeat;
  596 +}
  597 +
581 598 /* Notices Pagination */
582 599 .notice-pagination {
583 600 float: left;
... ...
spec/controllers/apps_controller_spec.rb
... ... @@ -211,17 +211,32 @@ describe AppsController do
211 211 @app.reload
212 212  
213 213 @app.issue_tracker.should be_nil
214   - response.body.should match(/You must specify your Lighthouseapp account, token and project id/)
  214 + response.body.should match(/You must specify your Lighthouseapp account, api token and project id/)
  215 + end
  216 + end
  217 +
  218 + context "redmine" do
  219 + it "should save tracker params" do
  220 + put :update, :id => @app.id, :app => { :issue_tracker_attributes => {
  221 + :issue_tracker_type => 'redmine', :project_id => '1234', :api_token => '123123', :account => 'http://myapp.com'
  222 + } }
  223 + @app.reload
  224 +
  225 + tracker = @app.issue_tracker
  226 + tracker.issue_tracker_type.should == 'redmine'
  227 + tracker.project_id.should == '1234'
  228 + tracker.api_token.should == '123123'
  229 + tracker.account.should == 'http://myapp.com'
215 230 end
216 231  
217 232 it "should show validation notice when sufficient params are not present" do
218 233 put :update, :id => @app.id, :app => { :issue_tracker_attributes => {
219   - :issue_tracker_type => 'lighthouseapp', :project_id => '1234', :api_token => '123123'
  234 + :issue_tracker_type => 'redmine', :project_id => '1234', :api_token => '123123'
220 235 } }
221 236 @app.reload
222 237  
223 238 @app.issue_tracker.should be_nil
224   - response.body.should match(/You must specify your Lighthouseapp account, token and project id/)
  239 + response.body.should match(/You must specify your Redmine url, api token and project id/)
225 240 end
226 241 end
227 242 end
... ...
spec/controllers/errs_controller_spec.rb
... ... @@ -253,6 +253,38 @@ describe ErrsController do
253 253 err.issue_link.should == @issue_link.sub(/\.xml$/, '')
254 254 end
255 255 end
  256 +
  257 + context "redmine tracker" do
  258 + let(:notice) { Factory :notice }
  259 + let(:tracker) { Factory :redmine_tracker, :app => notice.err.app }
  260 + let(:err) { notice.err }
  261 +
  262 + before(:each) do
  263 + number = 5
  264 + @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}"
  265 + body = "<issue><subject>my subject</subject><id>#{number}</id></issue>"
  266 + stub_request(:post, "#{tracker.account}/issues.xml").to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
  267 +
  268 + post :create_issue, :app_id => err.app.id, :id => err.id
  269 + err.reload
  270 + end
  271 +
  272 + it "should make request to Redmine with err params" do
  273 + requested = have_requested(:post, "#{tracker.account}/issues.xml")
  274 + WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token})
  275 + WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/)
  276 + WebMock.should requested.with(:body => /<subject>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/subject>/)
  277 + WebMock.should requested.with(:body => /<description>.+<\/description>/m)
  278 + end
  279 +
  280 + it "should redirect to err page" do
  281 + response.should redirect_to( app_err_path(err.app, err) )
  282 + end
  283 +
  284 + it "should create issue link for err" do
  285 + err.issue_link.should == @issue_link.sub(/\.xml/, '')
  286 + end
  287 + end
256 288 end
257 289  
258 290 context "absent issue tracker" do
... ...
spec/factories/issue_tracker_factories.rb
... ... @@ -4,4 +4,9 @@ Factory.define :lighthouseapp_tracker, :class =&gt; IssueTracker do |e|
4 4 e.api_token { Factory.next :word }
5 5 e.project_id { Factory.next :word }
6 6 e.association :app, :factory => :app
  7 +end
  8 +
  9 +Factory.define :redmine_tracker, :parent => :lighthouseapp_tracker do |e|
  10 + e.issue_tracker_type 'redmine'
  11 + e.account { "http://#{Factory.next(:word)}.com" }
7 12 end
8 13 \ No newline at end of file
... ...