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 | ... | ... |