Commit a02dacae093789471b601c378f6015690a527fc5

Authored by Ben Langfeld
1 parent 6858ff32
Exists in master and in 1 other branch production

Add support for Pivotal Tracker issue creation

@@ -8,6 +8,7 @@ gem 'will_paginate' @@ -8,6 +8,7 @@ gem 'will_paginate'
8 gem 'devise', '~> 1.1.8' 8 gem 'devise', '~> 1.1.8'
9 gem 'lighthouse-api' 9 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 'pivotal-tracker'
11 12
12 platform :ruby do 13 platform :ruby do
13 gem 'bson_ext', '~> 1.2' 14 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)
@@ -72,6 +75,11 @@ GEM @@ -72,6 +75,11 @@ GEM
72 tzinfo (~> 0.3.22) 75 tzinfo (~> 0.3.22)
73 will_paginate (~> 3.0.pre) 76 will_paginate (~> 3.0.pre)
74 nokogiri (1.4.4) 77 nokogiri (1.4.4)
  78 + pivotal-tracker (0.2.0)
  79 + builder
  80 + happymapper (>= 0.2.4)
  81 + nokogiri (~> 1.4.1)
  82 + rest-client (~> 1.5.1)
75 polyglot (0.3.1) 83 polyglot (0.3.1)
76 rack (1.2.2) 84 rack (1.2.2)
77 rack-mount (0.6.13) 85 rack-mount (0.6.13)
@@ -92,6 +100,8 @@ GEM @@ -92,6 +100,8 @@ GEM
92 rake (>= 0.8.7) 100 rake (>= 0.8.7)
93 thor (~> 0.14.4) 101 thor (~> 0.14.4)
94 rake (0.8.7) 102 rake (0.8.7)
  103 + rest-client (1.5.1)
  104 + mime-types (>= 1.16)
95 rspec (2.5.0) 105 rspec (2.5.0)
96 rspec-core (~> 2.5.0) 106 rspec-core (~> 2.5.0)
97 rspec-expectations (~> 2.5.0) 107 rspec-expectations (~> 2.5.0)
@@ -128,6 +138,7 @@ DEPENDENCIES @@ -128,6 +138,7 @@ DEPENDENCIES
128 lighthouse-api 138 lighthouse-api
129 mongoid (~> 2.0.0.rc.7) 139 mongoid (~> 2.0.0.rc.7)
130 nokogiri 140 nokogiri
  141 + pivotal-tracker
131 rails (= 3.0.5) 142 rails (= 3.0.5)
132 redmine_client! 143 redmine_client!
133 rspec (~> 2.5) 144 rspec (~> 2.5)
@@ -92,17 +92,23 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at @@ -92,17 +92,23 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at
92 Lighthouseapp integration 92 Lighthouseapp integration
93 ------------------------- 93 -------------------------
94 94
95 -* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview 95 +* Account is the name of your subdomain, i.e. **litcafe** for project at http://litcafe.lighthouseapp.com/projects/73466-face/overview
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. 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 98
99 Redmine integration 99 Redmine integration
100 ------------------------- 100 -------------------------
101 101
102 -* Account is the host of your redmine installation, i.e. **http://redmine.org** 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. 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 104 * Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject
105 105
  106 +Pivotal Tracker integration
  107 +-------------------------
  108 +
  109 +* 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.
  110 +* Project id is an identifier of your project, i.e. **24324** for project at http://www.pivotaltracker.com/projects/24324
  111 +
106 TODO 112 TODO
107 ---- 113 ----
108 114
app/helpers/application_helper.rb
@@ -2,4 +2,12 @@ module ApplicationHelper @@ -2,4 +2,12 @@ module ApplicationHelper
2 def lighthouse_tracker? object 2 def lighthouse_tracker? object
3 object.issue_tracker_type == "lighthouseapp" 3 object.issue_tracker_type == "lighthouseapp"
4 end 4 end
  5 +
  6 + def redmine_tracker? object
  7 + object.issue_tracker_type == "redmine"
  8 + end
  9 +
  10 + def pivotal_tracker? object
  11 + object.issue_tracker_type == "pivotal"
  12 + end
5 end 13 end
app/models/app.rb
1 class App 1 class App
2 include Mongoid::Document 2 include Mongoid::Document
3 include Mongoid::Timestamps 3 include Mongoid::Timestamps
4 - 4 +
5 field :name, :type => String 5 field :name, :type => String
6 field :api_key 6 field :api_key
7 field :resolve_errs_on_deploy, :type => Boolean, :default => false 7 field :resolve_errs_on_deploy, :type => Boolean, :default => false
@@ -21,29 +21,29 @@ class App @@ -21,29 +21,29 @@ class App
21 embeds_many :deploys 21 embeds_many :deploys
22 embeds_one :issue_tracker 22 embeds_one :issue_tracker
23 references_many :errs, :dependent => :destroy 23 references_many :errs, :dependent => :destroy
24 - 24 +
25 before_validation :generate_api_key, :on => :create 25 before_validation :generate_api_key, :on => :create
26 - 26 +
27 validates_presence_of :name, :api_key 27 validates_presence_of :name, :api_key
28 validates_uniqueness_of :name, :allow_blank => true 28 validates_uniqueness_of :name, :allow_blank => true
29 validates_uniqueness_of :api_key, :allow_blank => true 29 validates_uniqueness_of :api_key, :allow_blank => true
30 validates_associated :watchers 30 validates_associated :watchers
31 validate :check_issue_tracker 31 validate :check_issue_tracker
32 - 32 +
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]) }  
37 - 36 + :reject_if => proc { |attrs| !%w(lighthouseapp redmine pivotal).include?(attrs[:issue_tracker_type]) }
  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)
40 where(:_id => app_id).first || raise(Mongoid::Errors::DocumentNotFound.new(self,app_id)) 40 where(:_id => app_id).first || raise(Mongoid::Errors::DocumentNotFound.new(self,app_id))
41 end 41 end
42 - 42 +
43 def self.find_by_api_key!(key) 43 def self.find_by_api_key!(key)
44 where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key)) 44 where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key))
45 end 45 end
46 - 46 +
47 def last_deploy_at 47 def last_deploy_at
48 deploys.last && deploys.last.created_at 48 deploys.last && deploys.last.created_at
49 end 49 end
@@ -58,9 +58,9 @@ class App @@ -58,9 +58,9 @@ class App
58 !(self[:notify_on_deploys] == false) 58 !(self[:notify_on_deploys] == false)
59 end 59 end
60 alias :notify_on_deploys? :notify_on_deploys 60 alias :notify_on_deploys? :notify_on_deploys
61 - 61 +
62 protected 62 protected
63 - 63 +
64 def generate_api_key 64 def generate_api_key
65 self.api_key ||= ActiveSupport::SecureRandom.hex 65 self.api_key ||= ActiveSupport::SecureRandom.hex
66 end 66 end
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;
@@ -587,6 +587,10 @@ table.errs tr.resolved td &gt; * { @@ -587,6 +587,10 @@ table.errs tr.resolved td &gt; * {
587 background: transparent url(/images/redmine_create.png) 6px 5px no-repeat; 587 background: transparent url(/images/redmine_create.png) 6px 5px no-repeat;
588 } 588 }
589 589
  590 +#action-bar a.pivotal_create {
  591 + background: transparent url(/images/pivotal_create.png) 6px 5px no-repeat;
  592 +}
  593 +
590 #action-bar a.lighthouseapp_goto { 594 #action-bar a.lighthouseapp_goto {
591 background: transparent url(/images/lighthouseapp_goto.png) 6px 5px no-repeat; 595 background: transparent url(/images/lighthouseapp_goto.png) 6px 5px no-repeat;
592 } 596 }
@@ -595,6 +599,10 @@ table.errs tr.resolved td &gt; * { @@ -595,6 +599,10 @@ table.errs tr.resolved td &gt; * {
595 background: transparent url(/images/redmine_goto.png) 6px 5px no-repeat; 599 background: transparent url(/images/redmine_goto.png) 6px 5px no-repeat;
596 } 600 }
597 601
  602 +#action-bar a.pivotal_goto {
  603 + background: transparent url(/images/pivotal_goto.png) 6px 5px no-repeat;
  604 +}
  605 +
598 /* Notices Pagination */ 606 /* Notices Pagination */
599 .notice-pagination { 607 .notice-pagination {
600 float: left; 608 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', :api_token => '123123' } }
  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
1 require 'spec_helper' 1 require 'spec_helper'
2 2
3 describe ErrsController do 3 describe ErrsController do
4 - 4 +
5 it_requires_authentication :for => { 5 it_requires_authentication :for => {
6 :index => :get, :all => :get, :show => :get, :resolve => :put 6 :index => :get, :all => :get, :show => :get, :resolve => :put
7 }, 7 },
8 :params => {:app_id => 'dummyid', :id => 'dummyid'} 8 :params => {:app_id => 'dummyid', :id => 'dummyid'}
9 - 9 +
10 let(:app) { Factory(:app) } 10 let(:app) { Factory(:app) }
11 let(:err) { Factory(:err, :app => app) } 11 let(:err) { Factory(:err, :app => app) }
12 - 12 +
13 describe "GET /errs" do 13 describe "GET /errs" do
14 render_views 14 render_views
15 context 'when logged in as an admin' do 15 context 'when logged in as an admin' do
@@ -31,7 +31,7 @@ describe ErrsController do @@ -31,7 +31,7 @@ describe ErrsController do
31 response.should be_success 31 response.should be_success
32 response.body.should match(@err.message) 32 response.body.should match(@err.message)
33 end 33 end
34 - 34 +
35 it "should handle lots of errors" do 35 it "should handle lots of errors" do
36 pending "Turning off long running spec" 36 pending "Turning off long running spec"
37 1000.times { Factory :notice } 37 1000.times { Factory :notice }
@@ -55,7 +55,7 @@ describe ErrsController do @@ -55,7 +55,7 @@ describe ErrsController do
55 end 55 end
56 end 56 end
57 end 57 end
58 - 58 +
59 context 'when logged in as a user' do 59 context 'when logged in as a user' do
60 it 'gets a paginated list of unresolved errs for the users apps' do 60 it 'gets a paginated list of unresolved errs for the users apps' do
61 sign_in(user = Factory(:user)) 61 sign_in(user = Factory(:user))
@@ -68,7 +68,7 @@ describe ErrsController do @@ -68,7 +68,7 @@ describe ErrsController do
68 end 68 end
69 end 69 end
70 end 70 end
71 - 71 +
72 describe "GET /errs/all" do 72 describe "GET /errs/all" do
73 context 'when logged in as an admin' do 73 context 'when logged in as an admin' do
74 it "gets a paginated list of all errs" do 74 it "gets a paginated list of all errs" do
@@ -83,7 +83,7 @@ describe ErrsController do @@ -83,7 +83,7 @@ describe ErrsController do
83 assigns(:errs).should == errs 83 assigns(:errs).should == errs
84 end 84 end
85 end 85 end
86 - 86 +
87 context 'when logged in as a user' do 87 context 'when logged in as a user' do
88 it 'gets a paginated list of all errs for the users apps' do 88 it 'gets a paginated list of all errs for the users apps' do
89 sign_in(user = Factory(:user)) 89 sign_in(user = Factory(:user))
@@ -96,29 +96,29 @@ describe ErrsController do @@ -96,29 +96,29 @@ describe ErrsController do
96 end 96 end
97 end 97 end
98 end 98 end
99 - 99 +
100 describe "GET /apps/:app_id/errs/:id" do 100 describe "GET /apps/:app_id/errs/:id" do
101 render_views 101 render_views
102 - 102 +
103 before do 103 before do
104 3.times { Factory(:notice, :err => err)} 104 3.times { Factory(:notice, :err => err)}
105 end 105 end
106 - 106 +
107 context 'when logged in as an admin' do 107 context 'when logged in as an admin' do
108 before do 108 before do
109 sign_in Factory(:admin) 109 sign_in Factory(:admin)
110 end 110 end
111 - 111 +
112 it "finds the app" do 112 it "finds the app" do
113 get :show, :app_id => app.id, :id => err.id 113 get :show, :app_id => app.id, :id => err.id
114 assigns(:app).should == app 114 assigns(:app).should == app
115 end 115 end
116 - 116 +
117 it "finds the err" do 117 it "finds the err" do
118 get :show, :app_id => app.id, :id => err.id 118 get :show, :app_id => app.id, :id => err.id
119 assigns(:err).should == err 119 assigns(:err).should == err
120 end 120 end
121 - 121 +
122 it "successfully render page" do 122 it "successfully render page" do
123 get :show, :app_id => app.id, :id => err.id 123 get :show, :app_id => app.id, :id => err.id
124 response.should be_success 124 response.should be_success
@@ -131,9 +131,9 @@ describe ErrsController do @@ -131,9 +131,9 @@ describe ErrsController do
131 err = Factory :err 131 err = Factory :err
132 get :show, :app_id => err.app.id, :id => err.id 132 get :show, :app_id => err.app.id, :id => err.id
133 133
134 - response.body.should_not button_matcher 134 + response.body.should_not button_matcher
135 end 135 end
136 - 136 +
137 it "should exist for err's app with issue tracker" do 137 it "should exist for err's app with issue tracker" do
138 tracker = Factory(:lighthouseapp_tracker) 138 tracker = Factory(:lighthouseapp_tracker)
139 err = Factory(:err, :app => tracker.app) 139 err = Factory(:err, :app => tracker.app)
@@ -141,7 +141,7 @@ describe ErrsController do @@ -141,7 +141,7 @@ describe ErrsController do
141 141
142 response.body.should button_matcher 142 response.body.should button_matcher
143 end 143 end
144 - 144 +
145 it "should not exist for err with issue_link" do 145 it "should not exist for err with issue_link" do
146 tracker = Factory(:lighthouseapp_tracker) 146 tracker = Factory(:lighthouseapp_tracker)
147 err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host") 147 err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host")
@@ -151,7 +151,7 @@ describe ErrsController do @@ -151,7 +151,7 @@ describe ErrsController do
151 end 151 end
152 end 152 end
153 end 153 end
154 - 154 +
155 context 'when logged in as a user' do 155 context 'when logged in as a user' do
156 before do 156 before do
157 sign_in(@user = Factory(:user)) 157 sign_in(@user = Factory(:user))
@@ -160,12 +160,12 @@ describe ErrsController do @@ -160,12 +160,12 @@ describe ErrsController do
160 @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app) 160 @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app)
161 @watched_err = Factory(:err, :app => @watched_app) 161 @watched_err = Factory(:err, :app => @watched_app)
162 end 162 end
163 - 163 +
164 it 'finds the err if the user is watching the app' do 164 it 'finds the err if the user is watching the app' do
165 get :show, :app_id => @watched_app.to_param, :id => @watched_err.id 165 get :show, :app_id => @watched_app.to_param, :id => @watched_err.id
166 assigns(:err).should == @watched_err 166 assigns(:err).should == @watched_err
167 end 167 end
168 - 168 +
169 it 'raises a DocumentNotFound error if the user is not watching the app' do 169 it 'raises a DocumentNotFound error if the user is not watching the app' do
170 lambda { 170 lambda {
171 get :show, :app_id => @unwatched_err.app_id, :id => @unwatched_err.id 171 get :show, :app_id => @unwatched_err.app_id, :id => @unwatched_err.id
@@ -173,17 +173,17 @@ describe ErrsController do @@ -173,17 +173,17 @@ describe ErrsController do
173 end 173 end
174 end 174 end
175 end 175 end
176 - 176 +
177 describe "PUT /apps/:app_id/errs/:id/resolve" do 177 describe "PUT /apps/:app_id/errs/:id/resolve" do
178 before do 178 before do
179 sign_in Factory(:admin) 179 sign_in Factory(:admin)
180 - 180 +
181 @err = Factory(:err) 181 @err = Factory(:err)
182 App.stub(:find).with(@err.app.id).and_return(@err.app) 182 App.stub(:find).with(@err.app.id).and_return(@err.app)
183 @err.app.errs.stub(:find).and_return(@err) 183 @err.app.errs.stub(:find).and_return(@err)
184 @err.stub(:resolve!) 184 @err.stub(:resolve!)
185 end 185 end
186 - 186 +
187 it 'finds the app and the err' do 187 it 'finds the app and the err' do
188 App.should_receive(:find).with(@err.app.id).and_return(@err.app) 188 App.should_receive(:find).with(@err.app.id).and_return(@err.app)
189 @err.app.errs.should_receive(:find).and_return(@err) 189 @err.app.errs.should_receive(:find).and_return(@err)
@@ -191,17 +191,17 @@ describe ErrsController do @@ -191,17 +191,17 @@ describe ErrsController do
191 assigns(:app).should == @err.app 191 assigns(:app).should == @err.app
192 assigns(:err).should == @err 192 assigns(:err).should == @err
193 end 193 end
194 - 194 +
195 it "should resolve the issue" do 195 it "should resolve the issue" do
196 @err.should_receive(:resolve!).and_return(true) 196 @err.should_receive(:resolve!).and_return(true)
197 put :resolve, :app_id => @err.app.id, :id => @err.id 197 put :resolve, :app_id => @err.app.id, :id => @err.id
198 end 198 end
199 - 199 +
200 it "should display a message" do 200 it "should display a message" do
201 put :resolve, :app_id => @err.app.id, :id => @err.id 201 put :resolve, :app_id => @err.app.id, :id => @err.id
202 request.flash[:success].should match(/Great news/) 202 request.flash[:success].should match(/Great news/)
203 end 203 end
204 - 204 +
205 it "should redirect to the app page" do 205 it "should redirect to the app page" do
206 put :resolve, :app_id => @err.app.id, :id => @err.id 206 put :resolve, :app_id => @err.app.id, :id => @err.id
207 response.should redirect_to(app_path(@err.app)) 207 response.should redirect_to(app_path(@err.app))
@@ -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