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

Gemfile
... ... @@ -8,6 +8,7 @@ gem 'will_paginate'
8 8 gem 'devise', '~> 1.1.8'
9 9 gem 'lighthouse-api'
10 10 gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git"
  11 +gem 'pivotal-tracker'
11 12  
12 13 platform :ruby do
13 14 gem 'bson_ext', '~> 1.2'
... ...
Gemfile.lock
... ... @@ -54,7 +54,10 @@ GEM
54 54 factory_girl (~> 1.3)
55 55 railties (>= 3.0.0)
56 56 haml (3.0.25)
  57 + happymapper (0.3.2)
  58 + libxml-ruby (~> 1.1.3)
57 59 i18n (0.5.0)
  60 + libxml-ruby (1.1.4)
58 61 lighthouse-api (2.0)
59 62 activeresource (>= 3.0.0)
60 63 activesupport (>= 3.0.0)
... ... @@ -72,6 +75,11 @@ GEM
72 75 tzinfo (~> 0.3.22)
73 76 will_paginate (~> 3.0.pre)
74 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 83 polyglot (0.3.1)
76 84 rack (1.2.2)
77 85 rack-mount (0.6.13)
... ... @@ -92,6 +100,8 @@ GEM
92 100 rake (>= 0.8.7)
93 101 thor (~> 0.14.4)
94 102 rake (0.8.7)
  103 + rest-client (1.5.1)
  104 + mime-types (>= 1.16)
95 105 rspec (2.5.0)
96 106 rspec-core (~> 2.5.0)
97 107 rspec-expectations (~> 2.5.0)
... ... @@ -128,6 +138,7 @@ DEPENDENCIES
128 138 lighthouse-api
129 139 mongoid (~> 2.0.0.rc.7)
130 140 nokogiri
  141 + pivotal-tracker
131 142 rails (= 3.0.5)
132 143 redmine_client!
133 144 rspec (~> 2.5)
... ...
README.md
... ... @@ -92,17 +92,23 @@ for you. Checkout [Hoptoad](http://hoptoadapp.com) from the guys over at
92 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 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 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 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 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 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 112 TODO
107 113 ----
108 114  
... ...
app/helpers/application_helper.rb
... ... @@ -2,4 +2,12 @@ module ApplicationHelper
2 2 def lighthouse_tracker? object
3 3 object.issue_tracker_type == "lighthouseapp"
4 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 13 end
... ...
app/models/app.rb
1 1 class App
2 2 include Mongoid::Document
3 3 include Mongoid::Timestamps
4   -
  4 +
5 5 field :name, :type => String
6 6 field :api_key
7 7 field :resolve_errs_on_deploy, :type => Boolean, :default => false
... ... @@ -21,29 +21,29 @@ class App
21 21 embeds_many :deploys
22 22 embeds_one :issue_tracker
23 23 references_many :errs, :dependent => :destroy
24   -
  24 +
25 25 before_validation :generate_api_key, :on => :create
26   -
  26 +
27 27 validates_presence_of :name, :api_key
28 28 validates_uniqueness_of :name, :allow_blank => true
29 29 validates_uniqueness_of :api_key, :allow_blank => true
30 30 validates_associated :watchers
31 31 validate :check_issue_tracker
32   -
  32 +
33 33 accepts_nested_attributes_for :watchers, :allow_destroy => true,
34 34 :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? }
35 35 accepts_nested_attributes_for :issue_tracker, :allow_destroy => true,
36   - :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) }
37   -
  36 + :reject_if => proc { |attrs| !%w(lighthouseapp redmine pivotal).include?(attrs[:issue_tracker_type]) }
  37 +
38 38 # Mongoid Bug: find(id) on association proxies returns an Enumerator
39 39 def self.find_by_id!(app_id)
40 40 where(:_id => app_id).first || raise(Mongoid::Errors::DocumentNotFound.new(self,app_id))
41 41 end
42   -
  42 +
43 43 def self.find_by_api_key!(key)
44 44 where(:api_key => key).first || raise(Mongoid::Errors::DocumentNotFound.new(self,key))
45 45 end
46   -
  46 +
47 47 def last_deploy_at
48 48 deploys.last && deploys.last.created_at
49 49 end
... ... @@ -58,9 +58,9 @@ class App
58 58 !(self[:notify_on_deploys] == false)
59 59 end
60 60 alias :notify_on_deploys? :notify_on_deploys
61   -
  61 +
62 62 protected
63   -
  63 +
64 64 def generate_api_key
65 65 self.api_key ||= ActiveSupport::SecureRandom.hex
66 66 end
... ...
app/models/issue_tracker.rb
... ... @@ -6,7 +6,7 @@ class IssueTracker
6 6 default_url_options[:host] = Errbit::Application.config.action_mailer.default_url_options[:host]
7 7  
8 8 validate :check_params
9   -
  9 +
10 10 embedded_in :app, :inverse_of => :issue_tracker
11 11  
12 12 field :account, :type => String
... ... @@ -15,8 +15,14 @@ class IssueTracker
15 15 field :issue_tracker_type, :type => String, :default => 'lighthouseapp'
16 16  
17 17 def create_issue err
18   - return create_lighthouseapp_issue err if issue_tracker_type == 'lighthouseapp'
19   - create_redmine_issue err if issue_tracker_type == 'redmine'
  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 26 end
21 27  
22 28 protected
... ... @@ -34,6 +40,14 @@ class IssueTracker
34 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 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 51 def create_lighthouseapp_issue err
38 52 Lighthouse.account = account
39 53 Lighthouse.token = api_token
... ... @@ -56,12 +70,17 @@ class IssueTracker
56 70 end
57 71  
58 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 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 79 "You must specify your Lighthouseapp account, api token and project id"
63   - else
  80 + when 'redmine'
64 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 84 end
66 85 errors.add(:base, message)
67 86 end
... ... @@ -71,9 +90,13 @@ class IssueTracker
71 90 def lighthouseapp_body_template
72 91 @@lighthouseapp_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/lighthouseapp_body.txt.erb").gsub(/^\s*/, ''))
73 92 end
74   -
  93 +
75 94 def redmine_body_template
76 95 @@redmine_body_template ||= ERB.new(File.read(Rails.root + "app/views/errs/redmine_body.txt.erb"))
77 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 101 end
79 102 end
... ...
app/views/apps/_fields.html.haml
... ... @@ -3,7 +3,7 @@
3 3 %div.required
4 4 = f.label :name
5 5 = f.text_field :name
6   -
  6 +
7 7 %div.checkbox
8 8 = f.check_box :notify_on_errs
9 9 = f.label :notify_on_errs, 'Notify on errors'
... ... @@ -39,19 +39,24 @@
39 39 = label_tag :issue_tracker_type_lighthouseapp, 'Lighthouse', :for => label_for_attr(w, 'issue_tracker_type_lighthouseapp')
40 40 = w.radio_button :issue_tracker_type, :redmine
41 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 45 = w.label :account, "Account"
44 46 = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com"
45 47 = w.label :api_token, "API token"
46 48 = w.text_field :api_token, :placeholder => "API Token for your account"
47 49 = w.label :project_id, "Project ID"
48 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 52 = w.label :account, "Redmine URL"
51 53 = w.text_field :account, :placeholder => "like http://www.redmine.org/"
52 54 = w.label :api_token, "API token"
53 55 = w.text_field :api_token, :placeholder => "API Token for your account"
54 56 = w.label :project_id, "Project ID"
55 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 @@
  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 1 $(function(){
2 2 activateNestedForms();
3   -
  3 +
4 4 if($('div.watcher.nested').length)
5 5 activateWatcherTypeSelector();
6 6  
... ... @@ -11,9 +11,9 @@ $(function(){
11 11 function activateNestedForms() {
12 12 $('.nested-wrapper').each(function(){
13 13 var wrapper = $(this);
14   -
  14 +
15 15 makeNestedItemsDestroyable(wrapper);
16   -
  16 +
17 17 var addLink = $('<a/>').text('add another').addClass('add-nested');
18 18 addLink.click(appendNestedItem);
19 19 wrapper.append(addLink);
... ... @@ -35,7 +35,7 @@ function appendNestedItem() {
35 35 var nestedItem = addLink.parent().find('.nested').first().clone().show();
36 36 var timestamp = new Date();
37 37 timestamp = timestamp.valueOf();
38   -
  38 +
39 39 nestedItem.find('input, select').each(function(){
40 40 var input = $(this);
41 41 input.attr('id', input.attr('id').replace(/([_\[])\d+([\]_])/,'$1'+timestamp+'$2'));
... ... @@ -73,17 +73,10 @@ function activateWatcherTypeSelector() {
73 73 }
74 74  
75 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 76 $('div.issue_tracker input[name*=issue_tracker_type]').live('click', function(){
82   - var choosen = $(this).val();
  77 + var chosen = $(this).val();
83 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 83 \ No newline at end of file
... ...
public/stylesheets/application.css
1   -html {
  1 +html {
2 2 margin: 0; padding: 0;
3 3 color: #585858; background-color: #E2E2E2;
4 4 font-size: 62.8%; font-family: Helvetica, "Lucida Grande","Lucida Sans",Arial,sans-serif;
5 5 }
6   -body {
  6 +body {
7 7 margin: 0; padding: 0;
8 8 font-size: 1.3em; line-height: 1.4em;
9 9 }
... ... @@ -34,7 +34,7 @@ a:visited { color: #0069cc;}
34 34 a:hover { color: #0069cc; text-decoration: underline; }
35 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 38 width: 930px;
39 39 margin: 0 auto;
40 40 position: relative;
... ... @@ -98,19 +98,19 @@ a.action { float: right; font-size: 0.9em;}
98 98 margin-bottom: 24px;
99 99 height: 41px;
100 100 }
101   -#nav-bar li {
102   - float: left;
  101 +#nav-bar li {
  102 + float: left;
103 103 margin-right: 18px;
104 104 color: #666;
105 105 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
106 106 border-radius: 50px;
107 107 -moz-border-radius: 50px;
108 108 -webkit-border-radius: 50px;
109   - border: 1px solid #bbb;
  109 + border: 1px solid #bbb;
110 110 }
111 111 #nav-bar li a {
112 112 color: #666;
113   - display: block;
  113 + display: block;
114 114 padding: 0 20px 0 40px;
115 115 font-size: 14px; font-weight: bold; line-height: 39px; text-decoration: none;
116 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 120 #nav-bar li.apps a { background-image: url(images/icons/briefcase.png); }
121 121 #nav-bar li.errs a { background-image: url(images/icons/error.png); }
122 122 #nav-bar li.users a { background-image: url(images/icons/user.png); }
123   -#nav-bar li:hover {
  123 +#nav-bar li:hover {
124 124 box-shadow: 0 0 3px #69c;
125 125 -moz-box-shadow: 0 0 3px #69c;
126 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 130 background-color: #CCC;
131 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 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 141  
142 142 /* Content Title */
143 143 #content-title {
144   - padding: 30px 20px;
  144 + padding: 30px 20px;
145 145 border-top: 1px solid #FFF;
146 146 border-bottom: 1px solid #FFF;
147 147 background-color: #e2e2e2;
148 148 }
149 149 #content-title h1 {
150   - padding: 0; margin: 0;
  150 + padding: 0; margin: 0;
151 151 width: 85%;
152 152 border: none;
153 153 color: #666;
... ... @@ -161,7 +161,7 @@ a.action { float: right; font-size: 0.9em;}
161 161 position: absolute;
162 162 top: 25px; right: 20px;
163 163 }
164   -#action-bar span {
  164 +#action-bar span {
165 165 display: inline-block;
166 166 margin-left: 18px;
167 167 text-decoration: none;
... ... @@ -174,14 +174,14 @@ a.action { float: right; font-size: 0.9em;}
174 174 }
175 175 #action-bar span a {
176 176 color: #666;
177   - display: block;
  177 + display: block;
178 178 padding: 0 20px 0 40px;
179 179 font-size: 14px; font-weight: bold; line-height: 39px; text-decoration: none;
180 180 text-shadow: 1px 1px 0px #FFF; -webkit-text-shadow: 1px 1px 0px #FFF;
181 181 background: transparent 10px 8px no-repeat;
182 182 }
183 183 #action-bar a:hover { text-decoration: none;}
184   -#action-bar span:hover {
  184 +#action-bar span:hover {
185 185 box-shadow: 0 0 3px #69c;
186 186 -moz-box-shadow: 0 0 3px #69c;
187 187 -webkit-box-shadow: 0 0 3px #69c;
... ... @@ -194,14 +194,14 @@ a.action { float: right; font-size: 0.9em;}
194 194 #content {
195 195 padding: 20px; border-top: 1px solid #C6C6C6;
196 196 background-color: #FFF;
197   -}
  197 +}
198 198  
199   -#content a.button {
  199 +#content a.button {
200 200 float: right;
201 201 display: block;
202 202 margin-bottom: 10px;
203 203 }
204   -
  204 +
205 205 /* Footer */
206 206 #footer {
207 207 padding: 20px 0;
... ... @@ -211,22 +211,22 @@ a.action { float: right; font-size: 0.9em;}
211 211  
212 212 /* Flash Messages */
213 213 #flash-messages li {
214   - padding: 13px 45px;
215   - margin-bottom:25px;
  214 + padding: 13px 45px;
  215 + margin-bottom:25px;
216 216 border: 1px solid #C6C6C6;
217 217 background-color: #F9F9F9;
218 218 line-height: 1em;
219 219 }
220 220 #flash-messages li.notice {
221   - padding-left: 20px;
  221 + padding-left: 20px;
222 222 background-color: #b5eeff;
223 223 border: 1px solid #6cf;
224 224 }
225   -#flash-messages li.success {
  225 +#flash-messages li.success {
226 226 background: #cfc url(images/icons/success.png) 16px 50% no-repeat;
227 227 border: 1px solid #6c3;
228 228 }
229   -#flash-messages li.error {
  229 +#flash-messages li.error {
230 230 background: #fcc url(images/icons/error.png) 16px 50% no-repeat;
231 231 border: 1px solid #f99;
232 232 }
... ... @@ -244,13 +244,13 @@ form fieldset {
244 244 padding: 0.8em; margin-bottom: 1em;
245 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 249 color: #555;
250 250 }
251 251 form label {
252 252 font-weight: bold; text-transform: uppercase; line-height: 1.6em;
253   - display: inline-block;
  253 + display: inline-block;
254 254 }
255 255 form label.inline { display: inline; }
256 256 form .checkbox label { display: inline; }
... ... @@ -281,17 +281,17 @@ form input[type=submit] {
281 281 font-size: 1.2em; line-height: 1em; text-transform: uppercase;
282 282 border: none; color: #FFF; background-color: #387fc1;
283 283 }
284   -form div.buttons {
  284 +form div.buttons {
285 285 color: #666;
286 286 background: #FFF url(images/button-bg.png) 0 bottom repeat-x;
287 287 border-radius: 50px;
288 288 -moz-border-radius: 50px;
289 289 -webkit-border-radius: 50px;
290   - border: 1px solid #bbb;
  290 + border: 1px solid #bbb;
291 291 display: inline-block;
292 292 }
293   -form div.buttons:hover {
294   - color: #666;
  293 +form div.buttons:hover {
  294 + color: #666;
295 295 box-shadow: 0 0 3px #69c;
296 296 -moz-box-shadow: 0 0 3px #69c;
297 297 -webkit-box-shadow: 0 0 3px #69c;
... ... @@ -351,10 +351,10 @@ form .error-messages ul {
351 351 }
352 352  
353 353 /* Tables */
354   -table {
355   - width: 100%;
  354 +table {
  355 + width: 100%;
356 356 border: 1px solid #C6C6C6;
357   - margin-bottom: 1.5em;
  357 + margin-bottom: 1.5em;
358 358 border-collapse: separate;
359 359 }
360 360 table thead th {
... ... @@ -364,10 +364,10 @@ table thead th {
364 364 table tbody tr:first-child td {
365 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 372 table th { background-color: #E2E2E2; font-weight: bold; text-transform: uppercase; white-space: nowrap; }
373 373 table tbody tr:nth-child(odd) td { background-color: #F9F9F9; }
... ... @@ -442,8 +442,8 @@ pre {
442 442 background-color: #CCC;
443 443 background-image: none;
444 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 447 -webkit-box-shadow: inset 0 0 5px #999;
448 448 font-style: normal;
449 449 }
... ... @@ -477,11 +477,11 @@ a:hover.button {
477 477 background-color: #eee;
478 478 }
479 479 a.button.active {
480   - border-color: #fff;
  480 + border-color: #fff;
481 481 background-color: #CCC;
482 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 485 -webkit-box-shadow: inset 0 0 5px #999;
486 486 }
487 487  
... ... @@ -502,10 +502,10 @@ a.button.active {
502 502 }
503 503  
504 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 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 509 display: block;
510 510 }
511 511  
... ... @@ -559,7 +559,7 @@ table.errs td.app .environment {
559 559 font-size: 0.8em;
560 560 color: #999;
561 561 }
562   -table.errs td.message a {
  562 +table.errs td.message a {
563 563 width: 420px;
564 564 display: block;
565 565 word-wrap: break-word;
... ... @@ -587,6 +587,10 @@ table.errs tr.resolved td &gt; * {
587 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 594 #action-bar a.lighthouseapp_goto {
591 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 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 606 /* Notices Pagination */
599 607 .notice-pagination {
600 608 float: left;
... ...
spec/controllers/apps_controller_spec.rb
... ... @@ -5,7 +5,7 @@ describe AppsController do
5 5  
6 6 it_requires_authentication
7 7 it_requires_admin_privileges :for => {:new => :get, :edit => :get, :create => :post, :update => :put, :destroy => :delete}
8   -
  8 +
9 9 describe "GET /apps" do
10 10 context 'when logged in as an admin' do
11 11 it 'finds all apps' do
... ... @@ -16,7 +16,7 @@ describe AppsController do
16 16 assigns(:apps).should == apps
17 17 end
18 18 end
19   -
  19 +
20 20 context 'when logged in as a regular user' do
21 21 it 'finds apps the user is watching' do
22 22 sign_in(user = Factory(:user))
... ... @@ -31,7 +31,7 @@ describe AppsController do
31 31 end
32 32 end
33 33 end
34   -
  34 +
35 35 describe "GET /apps/:id" do
36 36 context 'logged in as an admin' do
37 37 before(:each) do
... ... @@ -75,27 +75,27 @@ describe AppsController do
75 75 end
76 76 end
77 77 end
78   -
  78 +
79 79 context 'logged in as a user' do
80 80 it 'finds the app if the user is watching it' do
81 81 pending
82 82 end
83   -
  83 +
84 84 it 'does not find the app if the user is not watching it' do
85 85 sign_in Factory(:user)
86 86 app = Factory(:app)
87   - lambda {
  87 + lambda {
88 88 get :show, :id => app.id
89 89 }.should raise_error(Mongoid::Errors::DocumentNotFound)
90 90 end
91 91 end
92 92 end
93   -
  93 +
94 94 context 'logged in as an admin' do
95 95 before do
96 96 sign_in Factory(:admin)
97 97 end
98   -
  98 +
99 99 describe "GET /apps/new" do
100 100 it 'instantiates a new app with a prebuilt watcher' do
101 101 get :new
... ... @@ -104,7 +104,7 @@ describe AppsController do
104 104 assigns(:app).watchers.should_not be_empty
105 105 end
106 106 end
107   -
  107 +
108 108 describe "GET /apps/:id/edit" do
109 109 it 'finds the correct app' do
110 110 app = Factory(:app)
... ... @@ -112,29 +112,29 @@ describe AppsController do
112 112 assigns(:app).should == app
113 113 end
114 114 end
115   -
  115 +
116 116 describe "POST /apps" do
117 117 before do
118 118 @app = Factory(:app)
119 119 App.stub(:new).and_return(@app)
120 120 end
121   -
  121 +
122 122 context "when the create is successful" do
123 123 before do
124 124 @app.should_receive(:save).and_return(true)
125 125 end
126   -
  126 +
127 127 it "should redirect to the app page" do
128 128 post :create, :app => {}
129 129 response.should redirect_to(app_path(@app))
130 130 end
131   -
  131 +
132 132 it "should display a message" do
133 133 post :create, :app => {}
134 134 request.flash[:success].should match(/success/)
135 135 end
136 136 end
137   -
  137 +
138 138 context "when the create is unsuccessful" do
139 139 it "should render the new page" do
140 140 @app.should_receive(:save).and_return(false)
... ... @@ -143,18 +143,18 @@ describe AppsController do
143 143 end
144 144 end
145 145 end
146   -
  146 +
147 147 describe "PUT /apps/:id" do
148 148 before do
149 149 @app = Factory(:app)
150 150 end
151   -
  151 +
152 152 context "when the update is successful" do
153 153 it "should redirect to the app page" do
154 154 put :update, :id => @app.id, :app => {}
155 155 response.should redirect_to(app_path(@app))
156 156 end
157   -
  157 +
158 158 it "should display a message" do
159 159 put :update, :id => @app.id, :app => {}
160 160 request.flash[:success].should match(/success/)
... ... @@ -168,7 +168,7 @@ describe AppsController do
168 168 response.should redirect_to(app_path(id))
169 169 end
170 170 end
171   -
  171 +
172 172 context "when the update is unsuccessful" do
173 173 it "should render the edit page" do
174 174 put :update, :id => @app.id, :app => { :name => '' }
... ... @@ -179,7 +179,7 @@ describe AppsController do
179 179 context "setting up issue tracker", :cur => true do
180 180 context "unknown tracker type" do
181 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 183 :issue_tracker_type => 'unknown', :project_id => '1234', :api_token => '123123', :account => 'myapp'
184 184 } }
185 185 @app.reload
... ... @@ -211,7 +211,7 @@ describe AppsController do
211 211 @app.reload
212 212  
213 213 @app.issue_tracker.should be_nil
214   - response.body.should match(/You must specify your Lighthouseapp account, api token and project id/)
  214 + response.body.should match(/You must specify your Lighthouseapp account, api token and project id/)
215 215 end
216 216 end
217 217  
... ... @@ -236,38 +236,60 @@ describe AppsController do
236 236 @app.reload
237 237  
238 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 262 end
241 263 end
242 264 end
243 265 end
244   -
  266 +
245 267 describe "DELETE /apps/:id" do
246 268 before do
247 269 @app = Factory(:app)
248 270 App.stub(:find).with(@app.id).and_return(@app)
249 271 end
250   -
  272 +
251 273 it "should find the app" do
252 274 delete :destroy, :id => @app.id
253 275 assigns(:app).should == @app
254 276 end
255   -
  277 +
256 278 it "should destroy the app" do
257 279 @app.should_receive(:destroy)
258 280 delete :destroy, :id => @app.id
259 281 end
260   -
  282 +
261 283 it "should display a message" do
262 284 delete :destroy, :id => @app.id
263 285 request.flash[:success].should match(/success/)
264 286 end
265   -
  287 +
266 288 it "should redirect to the apps page" do
267 289 delete :destroy, :id => @app.id
268 290 response.should redirect_to(apps_path)
269 291 end
270 292 end
271 293 end
272   -
  294 +
273 295 end
... ...
spec/controllers/errs_controller_spec.rb
1 1 require 'spec_helper'
2 2  
3 3 describe ErrsController do
4   -
  4 +
5 5 it_requires_authentication :for => {
6 6 :index => :get, :all => :get, :show => :get, :resolve => :put
7 7 },
8 8 :params => {:app_id => 'dummyid', :id => 'dummyid'}
9   -
  9 +
10 10 let(:app) { Factory(:app) }
11 11 let(:err) { Factory(:err, :app => app) }
12   -
  12 +
13 13 describe "GET /errs" do
14 14 render_views
15 15 context 'when logged in as an admin' do
... ... @@ -31,7 +31,7 @@ describe ErrsController do
31 31 response.should be_success
32 32 response.body.should match(@err.message)
33 33 end
34   -
  34 +
35 35 it "should handle lots of errors" do
36 36 pending "Turning off long running spec"
37 37 1000.times { Factory :notice }
... ... @@ -55,7 +55,7 @@ describe ErrsController do
55 55 end
56 56 end
57 57 end
58   -
  58 +
59 59 context 'when logged in as a user' do
60 60 it 'gets a paginated list of unresolved errs for the users apps' do
61 61 sign_in(user = Factory(:user))
... ... @@ -68,7 +68,7 @@ describe ErrsController do
68 68 end
69 69 end
70 70 end
71   -
  71 +
72 72 describe "GET /errs/all" do
73 73 context 'when logged in as an admin' do
74 74 it "gets a paginated list of all errs" do
... ... @@ -83,7 +83,7 @@ describe ErrsController do
83 83 assigns(:errs).should == errs
84 84 end
85 85 end
86   -
  86 +
87 87 context 'when logged in as a user' do
88 88 it 'gets a paginated list of all errs for the users apps' do
89 89 sign_in(user = Factory(:user))
... ... @@ -96,29 +96,29 @@ describe ErrsController do
96 96 end
97 97 end
98 98 end
99   -
  99 +
100 100 describe "GET /apps/:app_id/errs/:id" do
101 101 render_views
102   -
  102 +
103 103 before do
104 104 3.times { Factory(:notice, :err => err)}
105 105 end
106   -
  106 +
107 107 context 'when logged in as an admin' do
108 108 before do
109 109 sign_in Factory(:admin)
110 110 end
111   -
  111 +
112 112 it "finds the app" do
113 113 get :show, :app_id => app.id, :id => err.id
114 114 assigns(:app).should == app
115 115 end
116   -
  116 +
117 117 it "finds the err" do
118 118 get :show, :app_id => app.id, :id => err.id
119 119 assigns(:err).should == err
120 120 end
121   -
  121 +
122 122 it "successfully render page" do
123 123 get :show, :app_id => app.id, :id => err.id
124 124 response.should be_success
... ... @@ -131,9 +131,9 @@ describe ErrsController do
131 131 err = Factory :err
132 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 135 end
136   -
  136 +
137 137 it "should exist for err's app with issue tracker" do
138 138 tracker = Factory(:lighthouseapp_tracker)
139 139 err = Factory(:err, :app => tracker.app)
... ... @@ -141,7 +141,7 @@ describe ErrsController do
141 141  
142 142 response.body.should button_matcher
143 143 end
144   -
  144 +
145 145 it "should not exist for err with issue_link" do
146 146 tracker = Factory(:lighthouseapp_tracker)
147 147 err = Factory(:err, :app => tracker.app, :issue_link => "http://some.host")
... ... @@ -151,7 +151,7 @@ describe ErrsController do
151 151 end
152 152 end
153 153 end
154   -
  154 +
155 155 context 'when logged in as a user' do
156 156 before do
157 157 sign_in(@user = Factory(:user))
... ... @@ -160,12 +160,12 @@ describe ErrsController do
160 160 @watcher = Factory(:user_watcher, :user => @user, :app => @watched_app)
161 161 @watched_err = Factory(:err, :app => @watched_app)
162 162 end
163   -
  163 +
164 164 it 'finds the err if the user is watching the app' do
165 165 get :show, :app_id => @watched_app.to_param, :id => @watched_err.id
166 166 assigns(:err).should == @watched_err
167 167 end
168   -
  168 +
169 169 it 'raises a DocumentNotFound error if the user is not watching the app' do
170 170 lambda {
171 171 get :show, :app_id => @unwatched_err.app_id, :id => @unwatched_err.id
... ... @@ -173,17 +173,17 @@ describe ErrsController do
173 173 end
174 174 end
175 175 end
176   -
  176 +
177 177 describe "PUT /apps/:app_id/errs/:id/resolve" do
178 178 before do
179 179 sign_in Factory(:admin)
180   -
  180 +
181 181 @err = Factory(:err)
182 182 App.stub(:find).with(@err.app.id).and_return(@err.app)
183 183 @err.app.errs.stub(:find).and_return(@err)
184 184 @err.stub(:resolve!)
185 185 end
186   -
  186 +
187 187 it 'finds the app and the err' do
188 188 App.should_receive(:find).with(@err.app.id).and_return(@err.app)
189 189 @err.app.errs.should_receive(:find).and_return(@err)
... ... @@ -191,17 +191,17 @@ describe ErrsController do
191 191 assigns(:app).should == @err.app
192 192 assigns(:err).should == @err
193 193 end
194   -
  194 +
195 195 it "should resolve the issue" do
196 196 @err.should_receive(:resolve!).and_return(true)
197 197 put :resolve, :app_id => @err.app.id, :id => @err.id
198 198 end
199   -
  199 +
200 200 it "should display a message" do
201 201 put :resolve, :app_id => @err.app.id, :id => @err.id
202 202 request.flash[:success].should match(/Great news/)
203 203 end
204   -
  204 +
205 205 it "should redirect to the app page" do
206 206 put :resolve, :app_id => @err.app.id, :id => @err.id
207 207 response.should redirect_to(app_path(@err.app))
... ... @@ -285,6 +285,39 @@ describe ErrsController do
285 285 err.issue_link.should == @issue_link.sub(/\.xml/, '')
286 286 end
287 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 321 end
289 322  
290 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 2 e.api_token { Factory.next :word }
5 3 e.project_id { Factory.next :word }
6 4 e.association :app, :factory => :app
7 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 13 e.issue_tracker_type 'redmine'
11 14 e.account { "http://#{Factory.next(:word)}.com" }
12   -end
13 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
... ...