Commit 49147153c60fa231d7ac0eef1d5e558222ddd73b
Exists in
master
and in
1 other branch
Merge branch 'tracker' of https://github.com/benlangfeld/errbit into benlangfeld_errbit
Conflicts: Gemfile app/helpers/application_helper.rb app/models/app.rb
Showing
14 changed files
with
247 additions
and
114 deletions
Show diff stats
.rspec
Gemfile
... | ... | @@ -10,6 +10,7 @@ gem 'lighthouse-api' |
10 | 10 | gem 'redmine_client', :git => "git://github.com/oruen/redmine_client.git" |
11 | 11 | gem 'mongoid_rails_migrations' |
12 | 12 | gem 'useragent', '~> 0.3.1' |
13 | +gem 'pivotal-tracker' | |
13 | 14 | |
14 | 15 | platform :ruby do |
15 | 16 | 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) |
... | ... | @@ -77,6 +80,11 @@ GEM |
77 | 80 | rails (~> 3.0.0) |
78 | 81 | railties (~> 3.0.0) |
79 | 82 | nokogiri (1.4.4) |
83 | + pivotal-tracker (0.2.0) | |
84 | + builder | |
85 | + happymapper (>= 0.2.4) | |
86 | + nokogiri (~> 1.4.1) | |
87 | + rest-client (~> 1.5.1) | |
80 | 88 | polyglot (0.3.1) |
81 | 89 | rack (1.2.2) |
82 | 90 | rack-mount (0.6.14) |
... | ... | @@ -97,6 +105,8 @@ GEM |
97 | 105 | rake (>= 0.8.7) |
98 | 106 | thor (~> 0.14.4) |
99 | 107 | rake (0.8.7) |
108 | + rest-client (1.5.1) | |
109 | + mime-types (>= 1.16) | |
100 | 110 | rspec (2.5.0) |
101 | 111 | rspec-core (~> 2.5.0) |
102 | 112 | rspec-expectations (~> 2.5.0) |
... | ... | @@ -135,6 +145,7 @@ DEPENDENCIES |
135 | 145 | mongoid (= 2.0.0.rc.8) |
136 | 146 | mongoid_rails_migrations |
137 | 147 | nokogiri |
148 | + pivotal-tracker | |
138 | 149 | rails (= 3.0.5) |
139 | 150 | redmine_client! |
140 | 151 | rspec (~> 2.5) | ... | ... |
README.md
... | ... | @@ -110,6 +110,12 @@ Redmine integration |
110 | 110 | * Errbit uses token-based authentication. Get your API Key or visit [http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication](http://www.redmine.org/projects/redmine/wiki/Rest_api#Authentication) to learn how to get it. |
111 | 111 | * Project id is an identifier of your project, i.e. **chilliproject** for project at http://www.redmine.org/projects/chilliproject |
112 | 112 | |
113 | +Pivotal Tracker integration | |
114 | +------------------------- | |
115 | + | |
116 | +* Errbit uses token-based authentication. Get your API Key or visit [http://www.pivotaltracker.com/help/api](http://www.pivotaltracker.com/help/api) to learn how to get it. | |
117 | +* Project id is an identifier of your project, i.e. **24324** for project at http://www.pivotaltracker.com/projects/24324 | |
118 | + | |
113 | 119 | TODO |
114 | 120 | ---- |
115 | 121 | ... | ... |
app/helpers/application_helper.rb
... | ... | @@ -5,7 +5,6 @@ module ApplicationHelper |
5 | 5 | object.issue_tracker_type == "lighthouseapp" |
6 | 6 | end |
7 | 7 | |
8 | - | |
9 | 8 | def user_agent_graph(error) |
10 | 9 | tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)} |
11 | 10 | create_percentage_table(tallies, :total => error.notices.count) |
... | ... | @@ -15,7 +14,6 @@ module ApplicationHelper |
15 | 14 | (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}" |
16 | 15 | end |
17 | 16 | |
18 | - | |
19 | 17 | def tally(collection, &block) |
20 | 18 | collection.inject({}) do |tallies, item| |
21 | 19 | value = yield item |
... | ... | @@ -24,7 +22,6 @@ module ApplicationHelper |
24 | 22 | end |
25 | 23 | end |
26 | 24 | |
27 | - | |
28 | 25 | def create_percentage_table(tallies, options={}) |
29 | 26 | total = (options[:total] || total_from_tallies(tallies)) |
30 | 27 | percent = 100.0 / total.to_f |
... | ... | @@ -33,13 +30,16 @@ module ApplicationHelper |
33 | 30 | render :partial => "errs/tally_table", :locals => {:rows => rows} |
34 | 31 | end |
35 | 32 | |
36 | - | |
37 | -private | |
38 | - | |
39 | - | |
40 | 33 | def total_from_tallies(tallies) |
41 | 34 | tallies.values.inject(0) {|sum, n| sum + n} |
42 | 35 | end |
36 | + private :total_from_tallies | |
43 | 37 | |
44 | - | |
38 | + def redmine_tracker? object | |
39 | + object.issue_tracker_type == "redmine" | |
40 | + end | |
41 | + | |
42 | + def pivotal_tracker? object | |
43 | + object.issue_tracker_type == "pivotal" | |
44 | + end | |
45 | 45 | end | ... | ... |
app/models/app.rb
... | ... | @@ -33,7 +33,7 @@ class App |
33 | 33 | accepts_nested_attributes_for :watchers, :allow_destroy => true, |
34 | 34 | :reject_if => proc { |attrs| attrs[:user_id].blank? && attrs[:email].blank? } |
35 | 35 | accepts_nested_attributes_for :issue_tracker, :allow_destroy => true, |
36 | - :reject_if => proc { |attrs| !%w( lighthouseapp redmine ).include?(attrs[:issue_tracker_type]) } | |
36 | + :reject_if => proc { |attrs| !%w(lighthouseapp redmine pivotal).include?(attrs[:issue_tracker_type]) } | |
37 | 37 | |
38 | 38 | # Mongoid Bug: find(id) on association proxies returns an Enumerator |
39 | 39 | def self.find_by_id!(app_id) | ... | ... |
app/models/issue_tracker.rb
... | ... | @@ -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; |
... | ... | @@ -608,6 +608,10 @@ table.tally th.value { |
608 | 608 | background: transparent url(/images/redmine_create.png) 6px 5px no-repeat; |
609 | 609 | } |
610 | 610 | |
611 | +#action-bar a.pivotal_create { | |
612 | + background: transparent url(/images/pivotal_create.png) 6px 5px no-repeat; | |
613 | +} | |
614 | + | |
611 | 615 | #action-bar a.lighthouseapp_goto { |
612 | 616 | background: transparent url(/images/lighthouseapp_goto.png) 6px 5px no-repeat; |
613 | 617 | } |
... | ... | @@ -616,6 +620,10 @@ table.tally th.value { |
616 | 620 | background: transparent url(/images/redmine_goto.png) 6px 5px no-repeat; |
617 | 621 | } |
618 | 622 | |
623 | +#action-bar a.pivotal_goto { | |
624 | + background: transparent url(/images/pivotal_goto.png) 6px 5px no-repeat; | |
625 | +} | |
626 | + | |
619 | 627 | /* Notices Pagination */ |
620 | 628 | .notice-pagination { |
621 | 629 | 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' } } | |
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
... | ... | @@ -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 | ... | ... |