Commit 49147153c60fa231d7ac0eef1d5e558222ddd73b

Authored by Nick Recobra
2 parents 005eaf22 c5a7cb0e
Exists in master and in 1 other branch production

Merge branch 'tracker' of https://github.com/benlangfeld/errbit into benlangfeld_errbit

Conflicts:
	Gemfile
	app/helpers/application_helper.rb
	app/models/app.rb
1 --colour 1 --colour
  2 +--tty
  3 +--drb
  4 +--format documentation
@@ -10,6 +10,7 @@ gem 'lighthouse-api' @@ -10,6 +10,7 @@ gem 'lighthouse-api'
10 gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git" 10 gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git"
11 gem 'mongoid_rails_migrations' 11 gem 'mongoid_rails_migrations'
12 gem 'useragent', '~> 0.3.1' 12 gem 'useragent', '~> 0.3.1'
  13 +gem 'pivotal-tracker'
13 14
14 platform :ruby do 15 platform :ruby do
15 gem 'bson_ext', '~> 1.2' 16 gem 'bson_ext', '~> 1.2'
@@ -54,7 +54,10 @@ GEM @@ -54,7 +54,10 @@ GEM
54 factory_girl (~> 1.3) 54 factory_girl (~> 1.3)
55 railties (>= 3.0.0) 55 railties (>= 3.0.0)
56 haml (3.0.25) 56 haml (3.0.25)
  57 + happymapper (0.3.2)
  58 + libxml-ruby (~> 1.1.3)
57 i18n (0.5.0) 59 i18n (0.5.0)
  60 + libxml-ruby (1.1.4)
58 lighthouse-api (2.0) 61 lighthouse-api (2.0)
59 activeresource (>= 3.0.0) 62 activeresource (>= 3.0.0)
60 activesupport (>= 3.0.0) 63 activesupport (>= 3.0.0)
@@ -77,6 +80,11 @@ GEM @@ -77,6 +80,11 @@ GEM
77 rails (~> 3.0.0) 80 rails (~> 3.0.0)
78 railties (~> 3.0.0) 81 railties (~> 3.0.0)
79 nokogiri (1.4.4) 82 nokogiri (1.4.4)
  83 + pivotal-tracker (0.2.0)
  84 + builder
  85 + happymapper (>= 0.2.4)
  86 + nokogiri (~> 1.4.1)
  87 + rest-client (~> 1.5.1)
80 polyglot (0.3.1) 88 polyglot (0.3.1)
81 rack (1.2.2) 89 rack (1.2.2)
82 rack-mount (0.6.14) 90 rack-mount (0.6.14)
@@ -97,6 +105,8 @@ GEM @@ -97,6 +105,8 @@ GEM
97 rake (>= 0.8.7) 105 rake (>= 0.8.7)
98 thor (~> 0.14.4) 106 thor (~> 0.14.4)
99 rake (0.8.7) 107 rake (0.8.7)
  108 + rest-client (1.5.1)
  109 + mime-types (>= 1.16)
100 rspec (2.5.0) 110 rspec (2.5.0)
101 rspec-core (~> 2.5.0) 111 rspec-core (~> 2.5.0)
102 rspec-expectations (~> 2.5.0) 112 rspec-expectations (~> 2.5.0)
@@ -135,6 +145,7 @@ DEPENDENCIES @@ -135,6 +145,7 @@ DEPENDENCIES
135 mongoid (= 2.0.0.rc.8) 145 mongoid (= 2.0.0.rc.8)
136 mongoid_rails_migrations 146 mongoid_rails_migrations
137 nokogiri 147 nokogiri
  148 + pivotal-tracker
138 rails (= 3.0.5) 149 rails (= 3.0.5)
139 redmine_client! 150 redmine_client!
140 rspec (~> 2.5) 151 rspec (~> 2.5)
@@ -110,6 +110,12 @@ Redmine integration @@ -110,6 +110,12 @@ Redmine integration
110 * 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. 110 * 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.
111 * Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject 111 * Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject
112 112
  113 +Pivotal Tracker integration
  114 +-------------------------
  115 +
  116 +* Errbit uses token-based authentication. Get your API Key or visit [http://www.pivotaltracker.com/help/api](http://www.pivotaltracker.com/help/api) to learn how to get it.
  117 +* Project id is an identifier of your project, i.e. **24324** for project at http://www.pivotaltracker.com/projects/24324
  118 +
113 TODO 119 TODO
114 ---- 120 ----
115 121
app/helpers/application_helper.rb
@@ -5,7 +5,6 @@ module ApplicationHelper @@ -5,7 +5,6 @@ module ApplicationHelper
5 object.issue_tracker_type == "lighthouseapp" 5 object.issue_tracker_type == "lighthouseapp"
6 end 6 end
7 7
8 -  
9 def user_agent_graph(error) 8 def user_agent_graph(error)
10 tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)} 9 tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)}
11 create_percentage_table(tallies, :total => error.notices.count) 10 create_percentage_table(tallies, :total => error.notices.count)
@@ -15,7 +14,6 @@ module ApplicationHelper @@ -15,7 +14,6 @@ module ApplicationHelper
15 (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}" 14 (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}"
16 end 15 end
17 16
18 -  
19 def tally(collection, &block) 17 def tally(collection, &block)
20 collection.inject({}) do |tallies, item| 18 collection.inject({}) do |tallies, item|
21 value = yield item 19 value = yield item
@@ -24,7 +22,6 @@ module ApplicationHelper @@ -24,7 +22,6 @@ module ApplicationHelper
24 end 22 end
25 end 23 end
26 24
27 -  
28 def create_percentage_table(tallies, options={}) 25 def create_percentage_table(tallies, options={})
29 total = (options[:total] || total_from_tallies(tallies)) 26 total = (options[:total] || total_from_tallies(tallies))
30 percent = 100.0 / total.to_f 27 percent = 100.0 / total.to_f
@@ -33,13 +30,16 @@ module ApplicationHelper @@ -33,13 +30,16 @@ module ApplicationHelper
33 render :partial => "errs/tally_table", :locals => {:rows => rows} 30 render :partial => "errs/tally_table", :locals => {:rows => rows}
34 end 31 end
35 32
36 -  
37 -private  
38 -  
39 -  
40 def total_from_tallies(tallies) 33 def total_from_tallies(tallies)
41 tallies.values.inject(0) {|sum, n| sum + n} 34 tallies.values.inject(0) {|sum, n| sum + n}
42 end 35 end
  36 + private :total_from_tallies
43 37
44 - 38 + def redmine_tracker? object
  39 + object.issue_tracker_type == "redmine"
  40 + end
  41 +
  42 + def pivotal_tracker? object
  43 + object.issue_tracker_type == "pivotal"
  44 + end
45 end 45 end
app/models/app.rb
@@ -33,7 +33,7 @@ class App @@ -33,7 +33,7 @@ class App
33 accepts_nested_attributes_for :watchers, :allow_destroy => true, 33 accepts_nested_attributes_for :watchers, :allow_destroy => true,
34 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } 34 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
35 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, 35 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
36 - :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) } 36 + :reject_if => proc { |attrs| !%w(lighthouseapp redmine pivotal).include?(attrs[:issue_tracker_type]) }
37 37
38 # Mongoid Bug: find(id) on association proxies returns an Enumerator 38 # Mongoid Bug: find(id) on association proxies returns an Enumerator
39 def self.find_by_id!(app_id) 39 def self.find_by_id!(app_id)
app/models/issue_tracker.rb
@@ -6,7 +6,7 @@ class IssueTracker @@ -6,7 +6,7 @@ class IssueTracker
6 default_url_options[:host] = Errbit::Application.config.action_mailer.default_url_options[:host] 6 default_url_options[:host] = Errbit::Application.config.action_mailer.default_url_options[:host]
7 7
8 validate :check_params 8 validate :check_params
9 - 9 +
10 embedded_in :app, :inverse_of => :issue_tracker 10 embedded_in :app, :inverse_of => :issue_tracker
11 11
12 field :account, :type => String 12 field :account, :type => String
@@ -15,8 +15,14 @@ class IssueTracker @@ -15,8 +15,14 @@ class IssueTracker
15 field :issue_tracker_type, :type => String, :default => 'lighthouseapp' 15 field :issue_tracker_type, :type => String, :default => 'lighthouseapp'
16 16
17 def create_issue err 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' 18 + case issue_tracker_type
  19 + when 'lighthouseapp'
  20 + create_lighthouseapp_issue err
  21 + when 'redmine'
  22 + create_redmine_issue err
  23 + when 'pivotal'
  24 + create_pivotal_issue err
  25 + end
20 end 26 end
21 27
22 protected 28 protected
@@ -34,6 +40,14 @@ class IssueTracker @@ -34,6 +40,14 @@ class IssueTracker
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}") 40 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 41 end
36 42
  43 + def create_pivotal_issue err
  44 + PivotalTracker::Client.token = api_token
  45 + PivotalTracker::Client.use_ssl = true
  46 + project = PivotalTracker::Project.find project_id.to_i
  47 + story = project.stories.create :name => issue_title(err), :story_type => 'bug', :description => self.class.pivotal_body_template.result(binding)
  48 + err.update_attribute :issue_link, "https://www.pivotaltracker.com/story/show/#{story.id}"
  49 + end
  50 +
37 def create_lighthouseapp_issue err 51 def create_lighthouseapp_issue err
38 Lighthouse.account = account 52 Lighthouse.account = account
39 Lighthouse.token = api_token 53 Lighthouse.token = api_token
@@ -56,12 +70,17 @@ class IssueTracker @@ -56,12 +70,17 @@ class IssueTracker
56 end 70 end
57 71
58 def check_params 72 def check_params
59 - blank_flags = %w( api_token project_id account ).map {|m| self[m].blank? } 73 + blank_flag_fields = %w(api_token project_id)
  74 + blank_flag_fields << 'account' if %w(lighthouseapp redmine).include? issue_tracker_type
  75 + blank_flags = blank_flag_fields.map {|m| self[m].blank? }
60 if blank_flags.any? && !blank_flags.all? 76 if blank_flags.any? && !blank_flags.all?
61 - message = if issue_tracker_type == 'lighthouseapp' 77 + message = case issue_tracker_type
  78 + when 'lighthouseapp'
62 "You must specify your Lighthouseapp account, api token and project id" 79 "You must specify your Lighthouseapp account, api token and project id"
63 - else 80 + when 'redmine'
64 "You must specify your Redmine url, api token and project id" 81 "You must specify your Redmine url, api token and project id"
  82 + when 'pivotal'
  83 + "You must specify your Pivotal Tracker api token and project id"
65 end 84 end
66 errors.add(:base, message) 85 errors.add(:base, message)
67 end 86 end
@@ -71,9 +90,13 @@ class IssueTracker @@ -71,9 +90,13 @@ class IssueTracker
71 def lighthouseapp_body_template 90 def lighthouseapp_body_template
72 @@lighthouseapp_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, '')) 91 @@lighthouseapp_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, ''))
73 end 92 end
74 - 93 +
75 def redmine_body_template 94 def redmine_body_template
76 @@redmine_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb")) 95 @@redmine_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb"))
77 end 96 end
  97 +
  98 + def pivotal_body_template
  99 + @@pivotal_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/pivotal_body.txt.erb"))
  100 + end
78 end 101 end
79 end 102 end
app/views/apps/_fields.html.haml
@@ -3,7 +3,7 @@ @@ -3,7 +3,7 @@
3 %div.required 3 %div.required
4 = f.label :name 4 = f.label :name
5 = f.text_field :name 5 = f.text_field :name
6 - 6 +
7 %div.checkbox 7 %div.checkbox
8 = f.check_box :notify_on_errs 8 = f.check_box :notify_on_errs
9 = f.label :notify_on_errs, 'Notify on errors' 9 = f.label :notify_on_errs, 'Notify on errors'
@@ -39,19 +39,24 @@ @@ -39,19 +39,24 @@
39 = label_tag :issue_tracker_type_lighthouseapp, 'Lighthouse', :for => label_for_attr(w, 'issue_tracker_type_lighthouseapp') 39 = label_tag :issue_tracker_type_lighthouseapp, 'Lighthouse', :for => label_for_attr(w, 'issue_tracker_type_lighthouseapp')
40 = w.radio_button :issue_tracker_type, :redmine 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') 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} 42 + = w.radio_button :issue_tracker_type, :pivotal
  43 + = label_tag :issue_tracker_type_pivotal, 'Pivotal Tracker', :for => label_for_attr(w, 'issue_tracker_type_pivotal')
  44 + %div.tracker_params.lighthouseapp{:class => lighthouse_tracker?(w.object) ? 'chosen' : nil}
43 = w.label :account, "Account" 45 = w.label :account, "Account"
44 = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com" 46 = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com"
45 = w.label :api_token, "API token" 47 = w.label :api_token, "API token"
46 = w.text_field :api_token, :placeholder => "API Token for your account" 48 = w.text_field :api_token, :placeholder => "API Token for your account"
47 = w.label :project_id, "Project ID" 49 = w.label :project_id, "Project ID"
48 = w.text_field :project_id, :placeholder => "123 from abc from abc.lighthouseapp.com/projects/123" 50 = 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'} 51 + %div.tracker_params.redmine{:class => redmine_tracker?(w.object) ? 'chosen' : nil}
50 = w.label :account, "Redmine URL" 52 = w.label :account, "Redmine URL"
51 = w.text_field :account, :placeholder => "like http://www.redmine.org/" 53 = w.text_field :account, :placeholder => "like http://www.redmine.org/"
52 = w.label :api_token, "API token" 54 = w.label :api_token, "API token"
53 = w.text_field :api_token, :placeholder => "API Token for your account" 55 = w.text_field :api_token, :placeholder => "API Token for your account"
54 = w.label :project_id, "Project ID" 56 = w.label :project_id, "Project ID"
55 = w.text_field :project_id 57 = w.text_field :project_id
56 -  
57 - 58 + %div.tracker_params.pivotal{:class => pivotal_tracker?(w.object) ? 'chosen' : nil}
  59 + = w.label :project_id, "Project ID"
  60 + = w.text_field :project_id
  61 + = w.label :api_token, "API token"
  62 + = w.text_field :api_token, :placeholder => "API Token for your account"
app/views/errs/pivotal_body.txt.erb 0 → 100644
@@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
  1 +See this exception on Errbit: <%= app_err_url err.app, err %>
  2 +<% if notice = err.notices.first %>
  3 + <% if notice.request['url'].present? %>URL: <%= notice.request['url'] %><% end %>
  4 + Where: <%= notice.err.where %>
  5 + Occurred: <%= notice.created_at.to_s :micro %>
  6 + Similar: <%= (notice.err.notices.count - 1).to_s %>
  7 +
  8 + Params:
  9 + <%= pretty_hash notice.params %>
  10 +
  11 + Session:
  12 + <%= pretty_hash notice.session %>
  13 +
  14 + Backtrace:
  15 + <%= notice.backtrace.map { |line| "#{line['number']}: #{line['file'].sub(/^\[PROJECT_ROOT\]/, '')} -> *#{line['method']}*" }.join "\n" %>
  16 +
  17 + Environment:
  18 + <% notice.env_vars.each do |key, val| %>
  19 + <%= "#{key}: #{val}" %>
  20 + <% end %>
  21 +<% end %>
public/javascripts/form.js
1 $(function(){ 1 $(function(){
2 activateNestedForms(); 2 activateNestedForms();
3 - 3 +
4 if($('div.watcher.nested').length) 4 if($('div.watcher.nested').length)
5 activateWatcherTypeSelector(); 5 activateWatcherTypeSelector();
6 6
@@ -11,9 +11,9 @@ $(function(){ @@ -11,9 +11,9 @@ $(function(){
11 function activateNestedForms() { 11 function activateNestedForms() {
12 $('.nested-wrapper').each(function(){ 12 $('.nested-wrapper').each(function(){
13 var wrapper = $(this); 13 var wrapper = $(this);
14 - 14 +
15 makeNestedItemsDestroyable(wrapper); 15 makeNestedItemsDestroyable(wrapper);
16 - 16 +
17 var addLink = $('<a/>').text('add another').addClass('add-nested'); 17 var addLink = $('<a/>').text('add another').addClass('add-nested');
18 addLink.click(appendNestedItem); 18 addLink.click(appendNestedItem);
19 wrapper.append(addLink); 19 wrapper.append(addLink);
@@ -35,7 +35,7 @@ function appendNestedItem() { @@ -35,7 +35,7 @@ function appendNestedItem() {
35 var nestedItem = addLink.parent().find('.nested').first().clone().show(); 35 var nestedItem = addLink.parent().find('.nested').first().clone().show();
36 var timestamp = new Date(); 36 var timestamp = new Date();
37 timestamp = timestamp.valueOf(); 37 timestamp = timestamp.valueOf();
38 - 38 +
39 nestedItem.find('input, select').each(function(){ 39 nestedItem.find('input, select').each(function(){
40 var input = $(this); 40 var input = $(this);
41 input.attr('id', input.attr('id').replace(/([_\[])\d+([\]_])/,'$1'+timestamp+'$2')); 41 input.attr('id', input.attr('id').replace(/([_\[])\d+([\]_])/,'$1'+timestamp+'$2'));
@@ -73,17 +73,10 @@ function activateWatcherTypeSelector() { @@ -73,17 +73,10 @@ function activateWatcherTypeSelector() {
73 } 73 }
74 74
75 function activateIssueTrackerTypeSelector() { 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(){ 76 $('div.issue_tracker input[name*=issue_tracker_type]').live('click', function(){
82 - var choosen = $(this).val(); 77 + var chosen = $(this).val();
83 var wrapper = $(this).closest('.nested'); 78 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; 79 + wrapper.find('div.chosen').removeClass('chosen');
  80 + wrapper.find('div.'+chosen).addClass('chosen');
88 }); 81 });
89 } 82 }
90 \ No newline at end of file 83 \ No newline at end of file
public/stylesheets/application.css
1 -html { 1 +html {
2 margin: 0; padding: 0; 2 margin: 0; padding: 0;
3 color: #585858; background-color: #E2E2E2; 3 color: #585858; background-color: #E2E2E2;
4 font-size: 62.8%; font-family: Helvetica, "Lucida Grande","Lucida Sans",Arial,sans-serif; 4 font-size: 62.8%; font-family: Helvetica, "Lucida Grande","Lucida Sans",Arial,sans-serif;
5 } 5 }
6 -body { 6 +body {
7 margin: 0; padding: 0; 7 margin: 0; padding: 0;
8 font-size: 1.3em; line-height: 1.4em; 8 font-size: 1.3em; line-height: 1.4em;
9 } 9 }
@@ -34,7 +34,7 @@ a:visited { color: #0069cc;} @@ -34,7 +34,7 @@ a:visited { color: #0069cc;}
34 a:hover { color: #0069cc; text-decoration: underline; } 34 a:hover { color: #0069cc; text-decoration: underline; }
35 a.action { float: right; font-size: 0.9em;} 35 a.action { float: right; font-size: 0.9em;}
36 36
37 -#header > div, #nav-bar, #content-wrapper, #footer { 37 +#header > div, #nav-bar, #content-wrapper, #footer {
38 width: 930px; 38 width: 930px;
39 margin: 0 auto; 39 margin: 0 auto;
40 position: relative; 40 position: relative;
@@ -98,19 +98,19 @@ a.action { float: right; font-size: 0.9em;} @@ -98,19 +98,19 @@ a.action { float: right; font-size: 0.9em;}
98 margin-bottom: 24px; 98 margin-bottom: 24px;
99 height: 41px; 99 height: 41px;
100 } 100 }
101 -#nav-bar li {  
102 - float: left; 101 +#nav-bar li {
  102 + float: left;
103 margin-right: 18px; 103 margin-right: 18px;
104 color: #666; 104 color: #666;
105 background: #FFF url(images/button-bg.png) 0 bottom repeat-x; 105 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
106 border-radius: 50px; 106 border-radius: 50px;
107 -moz-border-radius: 50px; 107 -moz-border-radius: 50px;
108 -webkit-border-radius: 50px; 108 -webkit-border-radius: 50px;
109 - border: 1px solid #bbb; 109 + border: 1px solid #bbb;
110 } 110 }
111 #nav-bar li a { 111 #nav-bar li a {
112 color: #666; 112 color: #666;
113 - display: block; 113 + display: block;
114 padding: 0 20px 0 40px; 114 padding: 0 20px 0 40px;
115 font-size: 14px; font-weight: bold; line-height: 39px; text-decoration: none; 115 font-size: 14px; font-weight: bold; line-height: 39px; text-decoration: none;
116 text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF; 116 text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF;
@@ -120,17 +120,17 @@ a.action { float: right; font-size: 0.9em;} @@ -120,17 +120,17 @@ a.action { float: right; font-size: 0.9em;}
120 #nav-bar li.apps a { background-image: url(images/icons/briefcase.png); } 120 #nav-bar li.apps a { background-image: url(images/icons/briefcase.png); }
121 #nav-bar li.errs a { background-image: url(images/icons/error.png); } 121 #nav-bar li.errs a { background-image: url(images/icons/error.png); }
122 #nav-bar li.users a { background-image: url(images/icons/user.png); } 122 #nav-bar li.users a { background-image: url(images/icons/user.png); }
123 -#nav-bar li:hover { 123 +#nav-bar li:hover {
124 box-shadow: 0 0 3px #69c; 124 box-shadow: 0 0 3px #69c;
125 -moz-box-shadow: 0 0 3px #69c; 125 -moz-box-shadow: 0 0 3px #69c;
126 -webkit-box-shadow: 0 0 3px #69c; 126 -webkit-box-shadow: 0 0 3px #69c;
127 } 127 }
128 -#nav-bar li.active {  
129 - border-color: #fff; 128 +#nav-bar li.active {
  129 + border-color: #fff;
130 background-color: #CCC; 130 background-color: #CCC;
131 background-image: none; 131 background-image: none;
132 - box-shadow: inset 0 0 5px #999;  
133 - -moz-box-shadow: inset 0 0 5px #999; 132 + box-shadow: inset 0 0 5px #999;
  133 + -moz-box-shadow: inset 0 0 5px #999;
134 -webkit-box-shadow: inset 0 0 5px #999; 134 -webkit-box-shadow: inset 0 0 5px #999;
135 } 135 }
136 136
@@ -141,13 +141,13 @@ a.action { float: right; font-size: 0.9em;} @@ -141,13 +141,13 @@ a.action { float: right; font-size: 0.9em;}
141 141
142 /* Content Title */ 142 /* Content Title */
143 #content-title { 143 #content-title {
144 - padding: 30px 20px; 144 + padding: 30px 20px;
145 border-top: 1px solid #FFF; 145 border-top: 1px solid #FFF;
146 border-bottom: 1px solid #FFF; 146 border-bottom: 1px solid #FFF;
147 background-color: #e2e2e2; 147 background-color: #e2e2e2;
148 } 148 }
149 #content-title h1 { 149 #content-title h1 {
150 - padding: 0; margin: 0; 150 + padding: 0; margin: 0;
151 width: 85%; 151 width: 85%;
152 border: none; 152 border: none;
153 color: #666; 153 color: #666;
@@ -161,7 +161,7 @@ a.action { float: right; font-size: 0.9em;} @@ -161,7 +161,7 @@ a.action { float: right; font-size: 0.9em;}
161 position: absolute; 161 position: absolute;
162 top: 25px; right: 20px; 162 top: 25px; right: 20px;
163 } 163 }
164 -#action-bar span { 164 +#action-bar span {
165 display: inline-block; 165 display: inline-block;
166 margin-left: 18px; 166 margin-left: 18px;
167 text-decoration: none; 167 text-decoration: none;
@@ -174,14 +174,14 @@ a.action { float: right; font-size: 0.9em;} @@ -174,14 +174,14 @@ a.action { float: right; font-size: 0.9em;}
174 } 174 }
175 #action-bar span a { 175 #action-bar span a {
176 color: #666; 176 color: #666;
177 - display: block; 177 + display: block;
178 padding: 0 20px 0 40px; 178 padding: 0 20px 0 40px;
179 font-size: 14px; font-weight: bold; line-height: 39px; text-decoration: none; 179 font-size: 14px; font-weight: bold; line-height: 39px; text-decoration: none;
180 text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF; 180 text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF;
181 background: transparent 10px 8px no-repeat; 181 background: transparent 10px 8px no-repeat;
182 } 182 }
183 #action-bar a:hover { text-decoration: none;} 183 #action-bar a:hover { text-decoration: none;}
184 -#action-bar span:hover { 184 +#action-bar span:hover {
185 box-shadow: 0 0 3px #69c; 185 box-shadow: 0 0 3px #69c;
186 -moz-box-shadow: 0 0 3px #69c; 186 -moz-box-shadow: 0 0 3px #69c;
187 -webkit-box-shadow: 0 0 3px #69c; 187 -webkit-box-shadow: 0 0 3px #69c;
@@ -194,14 +194,14 @@ a.action { float: right; font-size: 0.9em;} @@ -194,14 +194,14 @@ a.action { float: right; font-size: 0.9em;}
194 #content { 194 #content {
195 padding: 20px; border-top: 1px solid #C6C6C6; 195 padding: 20px; border-top: 1px solid #C6C6C6;
196 background-color: #FFF; 196 background-color: #FFF;
197 -} 197 +}
198 198
199 -#content a.button { 199 +#content a.button {
200 float: right; 200 float: right;
201 display: block; 201 display: block;
202 margin-bottom: 10px; 202 margin-bottom: 10px;
203 } 203 }
204 - 204 +
205 /* Footer */ 205 /* Footer */
206 #footer { 206 #footer {
207 padding: 20px 0; 207 padding: 20px 0;
@@ -211,22 +211,22 @@ a.action { float: right; font-size: 0.9em;} @@ -211,22 +211,22 @@ a.action { float: right; font-size: 0.9em;}
211 211
212 /* Flash Messages */ 212 /* Flash Messages */
213 #flash-messages li { 213 #flash-messages li {
214 - padding: 13px 45px;  
215 - margin-bottom:25px; 214 + padding: 13px 45px;
  215 + margin-bottom:25px;
216 border: 1px solid #C6C6C6; 216 border: 1px solid #C6C6C6;
217 background-color: #F9F9F9; 217 background-color: #F9F9F9;
218 line-height: 1em; 218 line-height: 1em;
219 } 219 }
220 #flash-messages li.notice { 220 #flash-messages li.notice {
221 - padding-left: 20px; 221 + padding-left: 20px;
222 background-color: #b5eeff; 222 background-color: #b5eeff;
223 border: 1px solid #6cf; 223 border: 1px solid #6cf;
224 } 224 }
225 -#flash-messages li.success { 225 +#flash-messages li.success {
226 background: #cfc url(images/icons/success.png) 16px 50% no-repeat; 226 background: #cfc url(images/icons/success.png) 16px 50% no-repeat;
227 border: 1px solid #6c3; 227 border: 1px solid #6c3;
228 } 228 }
229 -#flash-messages li.error { 229 +#flash-messages li.error {
230 background: #fcc url(images/icons/error.png) 16px 50% no-repeat; 230 background: #fcc url(images/icons/error.png) 16px 50% no-repeat;
231 border: 1px solid #f99; 231 border: 1px solid #f99;
232 } 232 }
@@ -244,13 +244,13 @@ form fieldset { @@ -244,13 +244,13 @@ form fieldset {
244 padding: 0.8em; margin-bottom: 1em; 244 padding: 0.8em; margin-bottom: 1em;
245 background-color: #F0F0F0; border: 1px solid #C6C6C6; border-left: none; border-right: none; 245 background-color: #F0F0F0; border: 1px solid #C6C6C6; border-left: none; border-right: none;
246 } 246 }
247 -form fieldset legend {  
248 - font-size: 1.2em; font-weight: bold; text-transform: uppercase; 247 +form fieldset legend {
  248 + font-size: 1.2em; font-weight: bold; text-transform: uppercase;
249 color: #555; 249 color: #555;
250 } 250 }
251 form label { 251 form label {
252 font-weight: bold; text-transform: uppercase; line-height: 1.6em; 252 font-weight: bold; text-transform: uppercase; line-height: 1.6em;
253 - display: inline-block; 253 + display: inline-block;
254 } 254 }
255 form label.inline { display: inline; } 255 form label.inline { display: inline; }
256 form .checkbox label { display: inline; } 256 form .checkbox label { display: inline; }
@@ -281,17 +281,17 @@ form input[type=submit] { @@ -281,17 +281,17 @@ form input[type=submit] {
281 font-size: 1.2em; line-height: 1em; text-transform: uppercase; 281 font-size: 1.2em; line-height: 1em; text-transform: uppercase;
282 border: none; color: #FFF; background-color: #387fc1; 282 border: none; color: #FFF; background-color: #387fc1;
283 } 283 }
284 -form div.buttons { 284 +form div.buttons {
285 color: #666; 285 color: #666;
286 background: #FFF url(images/button-bg.png) 0 bottom repeat-x; 286 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
287 border-radius: 50px; 287 border-radius: 50px;
288 -moz-border-radius: 50px; 288 -moz-border-radius: 50px;
289 -webkit-border-radius: 50px; 289 -webkit-border-radius: 50px;
290 - border: 1px solid #bbb; 290 + border: 1px solid #bbb;
291 display: inline-block; 291 display: inline-block;
292 } 292 }
293 -form div.buttons:hover {  
294 - color: #666; 293 +form div.buttons:hover {
  294 + color: #666;
295 box-shadow: 0 0 3px #69c; 295 box-shadow: 0 0 3px #69c;
296 -moz-box-shadow: 0 0 3px #69c; 296 -moz-box-shadow: 0 0 3px #69c;
297 -webkit-box-shadow: 0 0 3px #69c; 297 -webkit-box-shadow: 0 0 3px #69c;
@@ -351,10 +351,10 @@ form .error-messages ul { @@ -351,10 +351,10 @@ form .error-messages ul {
351 } 351 }
352 352
353 /* Tables */ 353 /* Tables */
354 -table {  
355 - width: 100%; 354 +table {
  355 + width: 100%;
356 border: 1px solid #C6C6C6; 356 border: 1px solid #C6C6C6;
357 - margin-bottom: 1.5em; 357 + margin-bottom: 1.5em;
358 border-collapse: separate; 358 border-collapse: separate;
359 } 359 }
360 table thead th { 360 table thead th {
@@ -364,10 +364,10 @@ table thead th { @@ -364,10 +364,10 @@ table thead th {
364 table tbody tr:first-child td { 364 table tbody tr:first-child td {
365 border-top: 1px solid #C6C6C6; 365 border-top: 1px solid #C6C6C6;
366 } 366 }
367 -table th, table td {  
368 - border-top: 1px solid #C6C6C6;  
369 - padding: 10px 8px;  
370 - text-align: left; 367 +table th, table td {
  368 + border-top: 1px solid #C6C6C6;
  369 + padding: 10px 8px;
  370 + text-align: left;
371 } 371 }
372 table th { background-color: #E2E2E2; font-weight: bold; text-transform: uppercase; white-space: nowrap; } 372 table th { background-color: #E2E2E2; font-weight: bold; text-transform: uppercase; white-space: nowrap; }
373 table tbody tr:nth-child(odd) td { background-color: #F9F9F9; } 373 table tbody tr:nth-child(odd) td { background-color: #F9F9F9; }
@@ -442,8 +442,8 @@ pre { @@ -442,8 +442,8 @@ pre {
442 background-color: #CCC; 442 background-color: #CCC;
443 background-image: none; 443 background-image: none;
444 border-color: #FFF; 444 border-color: #FFF;
445 - box-shadow: inset 0 0 5px #999;  
446 - -moz-box-shadow: inset 0 0 5px #999; 445 + box-shadow: inset 0 0 5px #999;
  446 + -moz-box-shadow: inset 0 0 5px #999;
447 -webkit-box-shadow: inset 0 0 5px #999; 447 -webkit-box-shadow: inset 0 0 5px #999;
448 font-style: normal; 448 font-style: normal;
449 } 449 }
@@ -477,11 +477,11 @@ a:hover.button { @@ -477,11 +477,11 @@ a:hover.button {
477 background-color: #eee; 477 background-color: #eee;
478 } 478 }
479 a.button.active { 479 a.button.active {
480 - border-color: #fff; 480 + border-color: #fff;
481 background-color: #CCC; 481 background-color: #CCC;
482 background-image: none; 482 background-image: none;
483 - box-shadow: inset 0 0 5px #999;  
484 - -moz-box-shadow: inset 0 0 5px #999; 483 + box-shadow: inset 0 0 5px #999;
  484 + -moz-box-shadow: inset 0 0 5px #999;
485 -webkit-box-shadow: inset 0 0 5px #999; 485 -webkit-box-shadow: inset 0 0 5px #999;
486 } 486 }
487 487
@@ -502,10 +502,10 @@ a.button.active { @@ -502,10 +502,10 @@ a.button.active {
502 } 502 }
503 503
504 /* Watchers and Issue Tracker Forms */ 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 { 505 +div.nested.watcher .user, div.nested.watcher .email, div.issue_tracker.nested .lighthouseapp, div.issue_tracker.nested .redmine, div.issue_tracker.nested .pivotal {
506 display: none; 506 display: none;
507 } 507 }
508 -div.nested.watcher .choosen, div.nested.issue_tracker .choosen { 508 +div.nested.watcher .choosen, div.nested.issue_tracker .chosen {
509 display: block; 509 display: block;
510 } 510 }
511 511
@@ -559,7 +559,7 @@ table.errs td.app .environment { @@ -559,7 +559,7 @@ table.errs td.app .environment {
559 font-size: 0.8em; 559 font-size: 0.8em;
560 color: #999; 560 color: #999;
561 } 561 }
562 -table.errs td.message a { 562 +table.errs td.message a {
563 width: 420px; 563 width: 420px;
564 display: block; 564 display: block;
565 word-wrap: break-word; 565 word-wrap: break-word;
@@ -608,6 +608,10 @@ table.tally th.value { @@ -608,6 +608,10 @@ table.tally th.value {
608 background: transparent url(/images/redmine_create.png) 6px 5px no-repeat; 608 background: transparent url(/images/redmine_create.png) 6px 5px no-repeat;
609 } 609 }
610 610
  611 +#action-bar a.pivotal_create {
  612 + background: transparent url(/images/pivotal_create.png) 6px 5px no-repeat;
  613 +}
  614 +
611 #action-bar a.lighthouseapp_goto { 615 #action-bar a.lighthouseapp_goto {
612 background: transparent url(/images/lighthouseapp_goto.png) 6px 5px no-repeat; 616 background: transparent url(/images/lighthouseapp_goto.png) 6px 5px no-repeat;
613 } 617 }
@@ -616,6 +620,10 @@ table.tally th.value { @@ -616,6 +620,10 @@ table.tally th.value {
616 background: transparent url(/images/redmine_goto.png) 6px 5px no-repeat; 620 background: transparent url(/images/redmine_goto.png) 6px 5px no-repeat;
617 } 621 }
618 622
  623 +#action-bar a.pivotal_goto {
  624 + background: transparent url(/images/pivotal_goto.png) 6px 5px no-repeat;
  625 +}
  626 +
619 /* Notices Pagination */ 627 /* Notices Pagination */
620 .notice-pagination { 628 .notice-pagination {
621 float: left; 629 float: left;
spec/controllers/apps_controller_spec.rb
@@ -5,7 +5,7 @@ describe AppsController do @@ -5,7 +5,7 @@ describe AppsController do
5 5
6 it_requires_authentication 6 it_requires_authentication
7 it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete} 7 it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete}
8 - 8 +
9 describe "GET /apps" do 9 describe "GET /apps" do
10 context 'when logged in as an admin' do 10 context 'when logged in as an admin' do
11 it 'finds all apps' do 11 it 'finds all apps' do
@@ -16,7 +16,7 @@ describe AppsController do @@ -16,7 +16,7 @@ describe AppsController do
16 assigns(:apps).should == apps 16 assigns(:apps).should == apps
17 end 17 end
18 end 18 end
19 - 19 +
20 context 'when logged in as a regular user' do 20 context 'when logged in as a regular user' do
21 it 'finds apps the user is watching' do 21 it 'finds apps the user is watching' do
22 sign_in(user = Factory(:user)) 22 sign_in(user = Factory(:user))
@@ -31,7 +31,7 @@ describe AppsController do @@ -31,7 +31,7 @@ describe AppsController do
31 end 31 end
32 end 32 end
33 end 33 end
34 - 34 +
35 describe "GET /apps/:id" do 35 describe "GET /apps/:id" do
36 context 'logged in as an admin' do 36 context 'logged in as an admin' do
37 before(:each) do 37 before(:each) do
@@ -75,27 +75,27 @@ describe AppsController do @@ -75,27 +75,27 @@ describe AppsController do
75 end 75 end
76 end 76 end
77 end 77 end
78 - 78 +
79 context 'logged in as a user' do 79 context 'logged in as a user' do
80 it 'finds the app if the user is watching it' do 80 it 'finds the app if the user is watching it' do
81 pending 81 pending
82 end 82 end
83 - 83 +
84 it 'does not find the app if the user is not watching it' do 84 it 'does not find the app if the user is not watching it' do
85 sign_in Factory(:user) 85 sign_in Factory(:user)
86 app = Factory(:app) 86 app = Factory(:app)
87 - lambda { 87 + lambda {
88 get :show, :id => app.id 88 get :show, :id => app.id
89 }.should raise_error(Mongoid::Errors::DocumentNotFound) 89 }.should raise_error(Mongoid::Errors::DocumentNotFound)
90 end 90 end
91 end 91 end
92 end 92 end
93 - 93 +
94 context 'logged in as an admin' do 94 context 'logged in as an admin' do
95 before do 95 before do
96 sign_in Factory(:admin) 96 sign_in Factory(:admin)
97 end 97 end
98 - 98 +
99 describe "GET /apps/new" do 99 describe "GET /apps/new" do
100 it 'instantiates a new app with a prebuilt watcher' do 100 it 'instantiates a new app with a prebuilt watcher' do
101 get :new 101 get :new
@@ -104,7 +104,7 @@ describe AppsController do @@ -104,7 +104,7 @@ describe AppsController do
104 assigns(:app).watchers.should_not be_empty 104 assigns(:app).watchers.should_not be_empty
105 end 105 end
106 end 106 end
107 - 107 +
108 describe "GET /apps/:id/edit" do 108 describe "GET /apps/:id/edit" do
109 it 'finds the correct app' do 109 it 'finds the correct app' do
110 app = Factory(:app) 110 app = Factory(:app)
@@ -112,29 +112,29 @@ describe AppsController do @@ -112,29 +112,29 @@ describe AppsController do
112 assigns(:app).should == app 112 assigns(:app).should == app
113 end 113 end
114 end 114 end
115 - 115 +
116 describe "POST /apps" do 116 describe "POST /apps" do
117 before do 117 before do
118 @app = Factory(:app) 118 @app = Factory(:app)
119 App.stub(:new).and_return(@app) 119 App.stub(:new).and_return(@app)
120 end 120 end
121 - 121 +
122 context "when the create is successful" do 122 context "when the create is successful" do
123 before do 123 before do
124 @app.should_receive(:save).and_return(true) 124 @app.should_receive(:save).and_return(true)
125 end 125 end
126 - 126 +
127 it "should redirect to the app page" do 127 it "should redirect to the app page" do
128 post :create, :app => {} 128 post :create, :app => {}
129 response.should redirect_to(app_path(@app)) 129 response.should redirect_to(app_path(@app))
130 end 130 end
131 - 131 +
132 it "should display a message" do 132 it "should display a message" do
133 post :create, :app => {} 133 post :create, :app => {}
134 request.flash[:success].should match(/success/) 134 request.flash[:success].should match(/success/)
135 end 135 end
136 end 136 end
137 - 137 +
138 context "when the create is unsuccessful" do 138 context "when the create is unsuccessful" do
139 it "should render the new page" do 139 it "should render the new page" do
140 @app.should_receive(:save).and_return(false) 140 @app.should_receive(:save).and_return(false)
@@ -143,18 +143,18 @@ describe AppsController do @@ -143,18 +143,18 @@ describe AppsController do
143 end 143 end
144 end 144 end
145 end 145 end
146 - 146 +
147 describe "PUT /apps/:id" do 147 describe "PUT /apps/:id" do
148 before do 148 before do
149 @app = Factory(:app) 149 @app = Factory(:app)
150 end 150 end
151 - 151 +
152 context "when the update is successful" do 152 context "when the update is successful" do
153 it "should redirect to the app page" do 153 it "should redirect to the app page" do
154 put :update, :id => @app.id, :app => {} 154 put :update, :id => @app.id, :app => {}
155 response.should redirect_to(app_path(@app)) 155 response.should redirect_to(app_path(@app))
156 end 156 end
157 - 157 +
158 it "should display a message" do 158 it "should display a message" do
159 put :update, :id => @app.id, :app => {} 159 put :update, :id => @app.id, :app => {}
160 request.flash[:success].should match(/success/) 160 request.flash[:success].should match(/success/)
@@ -168,7 +168,7 @@ describe AppsController do @@ -168,7 +168,7 @@ describe AppsController do
168 response.should redirect_to(app_path(id)) 168 response.should redirect_to(app_path(id))
169 end 169 end
170 end 170 end
171 - 171 +
172 context "when the update is unsuccessful" do 172 context "when the update is unsuccessful" do
173 it "should render the edit page" do 173 it "should render the edit page" do
174 put :update, :id => @app.id, :app => { :name => '' } 174 put :update, :id => @app.id, :app => { :name => '' }
@@ -179,7 +179,7 @@ describe AppsController do @@ -179,7 +179,7 @@ describe AppsController do
179 context "setting up issue tracker", :cur => true do 179 context "setting up issue tracker", :cur => true do
180 context "unknown tracker type" do 180 context "unknown tracker type" do
181 before(:each) do 181 before(:each) do
182 - put :update, :id => @app.id, :app => { :issue_tracker_attributes => { 182 + put :update, :id => @app.id, :app => { :issue_tracker_attributes => {
183 :issue_tracker_type => 'unknown', :project_id => '1234', :api_token => '123123', :account => 'myapp' 183 :issue_tracker_type => 'unknown', :project_id => '1234', :api_token => '123123', :account => 'myapp'
184 } } 184 } }
185 @app.reload 185 @app.reload
@@ -211,7 +211,7 @@ describe AppsController do @@ -211,7 +211,7 @@ describe AppsController do
211 @app.reload 211 @app.reload
212 212
213 @app.issue_tracker.should be_nil 213 @app.issue_tracker.should be_nil
214 - response.body.should match(/You must specify your Lighthouseapp account, api token and project id/) 214 + response.body.should match(/You must specify your Lighthouseapp account, api token and project id/)
215 end 215 end
216 end 216 end
217 217
@@ -236,38 +236,60 @@ describe AppsController do @@ -236,38 +236,60 @@ describe AppsController do
236 @app.reload 236 @app.reload
237 237
238 @app.issue_tracker.should be_nil 238 @app.issue_tracker.should be_nil
239 - response.body.should match(/You must specify your Redmine url, api token and project id/) 239 + response.body.should match(/You must specify your Redmine url, api token and project id/)
  240 + end
  241 + end
  242 +
  243 + context "pivotal" do
  244 + it "should save tracker params" do
  245 + put :update, :id => @app.id, :app => { :issue_tracker_attributes => {
  246 + :issue_tracker_type => 'pivotal', :project_id => '1234', :api_token => '123123' } }
  247 + @app.reload
  248 +
  249 + tracker = @app.issue_tracker
  250 + tracker.issue_tracker_type.should == 'pivotal'
  251 + tracker.project_id.should == '1234'
  252 + tracker.api_token.should == '123123'
  253 + end
  254 +
  255 + it "should show validation notice when sufficient params are not present" do
  256 + put :update, :id => @app.id, :app => { :issue_tracker_attributes => {
  257 + :issue_tracker_type => 'pivotal', :project_id => '1234' } }
  258 + @app.reload
  259 +
  260 + @app.issue_tracker.should be_nil
  261 + response.body.should match(/You must specify your Pivotal Tracker api token and project id/)
240 end 262 end
241 end 263 end
242 end 264 end
243 end 265 end
244 - 266 +
245 describe "DELETE /apps/:id" do 267 describe "DELETE /apps/:id" do
246 before do 268 before do
247 @app = Factory(:app) 269 @app = Factory(:app)
248 App.stub(:find).with(@app.id).and_return(@app) 270 App.stub(:find).with(@app.id).and_return(@app)
249 end 271 end
250 - 272 +
251 it "should find the app" do 273 it "should find the app" do
252 delete :destroy, :id => @app.id 274 delete :destroy, :id => @app.id
253 assigns(:app).should == @app 275 assigns(:app).should == @app
254 end 276 end
255 - 277 +
256 it "should destroy the app" do 278 it "should destroy the app" do
257 @app.should_receive(:destroy) 279 @app.should_receive(:destroy)
258 delete :destroy, :id => @app.id 280 delete :destroy, :id => @app.id
259 end 281 end
260 - 282 +
261 it "should display a message" do 283 it "should display a message" do
262 delete :destroy, :id => @app.id 284 delete :destroy, :id => @app.id
263 request.flash[:success].should match(/success/) 285 request.flash[:success].should match(/success/)
264 end 286 end
265 - 287 +
266 it "should redirect to the apps page" do 288 it "should redirect to the apps page" do
267 delete :destroy, :id => @app.id 289 delete :destroy, :id => @app.id
268 response.should redirect_to(apps_path) 290 response.should redirect_to(apps_path)
269 end 291 end
270 end 292 end
271 end 293 end
272 - 294 +
273 end 295 end
spec/controllers/errs_controller_spec.rb
@@ -285,6 +285,39 @@ describe ErrsController do @@ -285,6 +285,39 @@ describe ErrsController do
285 err.issue_link.should == @issue_link.sub(/\.xml/, '') 285 err.issue_link.should == @issue_link.sub(/\.xml/, '')
286 end 286 end
287 end 287 end
  288 +
  289 + context "redmine tracker" do
  290 + let(:notice) { Factory :notice }
  291 + let(:tracker) { Factory :pivotal_tracker, :app => notice.err.app }
  292 + let(:err) { notice.err }
  293 +
  294 + before(:each) do
  295 + pending
  296 + number = 5
  297 + @issue_link = "#{tracker.account}/issues/#{number}.xml?project_id=#{tracker.project_id}"
  298 + body = "<issue><subject>my subject</subject><id>#{number}</id></issue>"
  299 + stub_request(:post, "#{tracker.account}/issues.xml").to_return(:status => 201, :headers => {'Location' => @issue_link}, :body => body )
  300 +
  301 + post :create_issue, :app_id => err.app.id, :id => err.id
  302 + err.reload
  303 + end
  304 +
  305 + it "should make request to Pivotal Tracker with err params" do
  306 + requested = have_requested(:post, "#{tracker.account}/issues.xml")
  307 + WebMock.should requested.with(:headers => {'X-Redmine-API-Key' => tracker.api_token})
  308 + WebMock.should requested.with(:body => /<project-id>#{tracker.project_id}<\/project-id>/)
  309 + WebMock.should requested.with(:body => /<subject>\[#{ err.environment }\]\[#{err.where}\] #{err.message.to_s.truncate(100)}<\/subject>/)
  310 + WebMock.should requested.with(:body => /<description>.+<\/description>/m)
  311 + end
  312 +
  313 + it "should redirect to err page" do
  314 + response.should redirect_to( app_err_path(err.app, err) )
  315 + end
  316 +
  317 + it "should create issue link for err" do
  318 + err.issue_link.should == @issue_link.sub(/\.xml/, '')
  319 + end
  320 + end
288 end 321 end
289 322
290 context "absent issue tracker" do 323 context "absent issue tracker" do
spec/factories/issue_tracker_factories.rb
1 -Factory.define :lighthouseapp_tracker, :class => IssueTracker do |e|  
2 - e.issue_tracker_type 'lighthouseapp'  
3 - e.account { Factory.next :word } 1 +Factory.define :generic_tracker, :class => IssueTracker do |e|
4 e.api_token { Factory.next :word } 2 e.api_token { Factory.next :word }
5 e.project_id { Factory.next :word } 3 e.project_id { Factory.next :word }
6 e.association :app, :factory => :app 4 e.association :app, :factory => :app
7 end 5 end
8 6
9 -Factory.define :redmine_tracker, :parent => :lighthouseapp_tracker do |e| 7 +Factory.define :lighthouseapp_tracker, :parent => :generic_tracker do |e|
  8 + e.issue_tracker_type 'lighthouseapp'
  9 + e.account { Factory.next :word }
  10 +end
  11 +
  12 +Factory.define :redmine_tracker, :parent => :generic_tracker do |e|
10 e.issue_tracker_type 'redmine' 13 e.issue_tracker_type 'redmine'
11 e.account { "http://#{Factory.next(:word)}.com" } 14 e.account { "http://#{Factory.next(:word)}.com" }
12 -end  
13 \ No newline at end of file 15 \ No newline at end of file
  16 +end
  17 +
  18 +Factory.define :pivotal_tracker, :parent => :generic_tracker do |e|
  19 + e.issue_tracker_type 'pivotal'
  20 +end