Commit a02dacae093789471b601c378f6015690a527fc5
1 parent
6858ff32
Exists in
master
and in
1 other branch
Add support for Pivotal Tracker issue creation
Showing
13 changed files
with
280 additions
and
142 deletions
Show diff stats
Gemfile
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" | ... | ... |
... | ... | @@ -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 > * { |
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 > * { |
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 | ... | ... |