Commit e79cae05fe7949cbadef300e1d3f09cd2ba4f914
Exists in
master
and in
1 other branch
Merge branch 'comments_on_errors'
Showing
14 changed files
with
242 additions
and
18 deletions
Show diff stats
app/controllers/errs_controller.rb
... | ... | @@ -27,6 +27,7 @@ class ErrsController < ApplicationController |
27 | 27 | page = 1 if page.to_i.zero? |
28 | 28 | @notices = @err.notices.ordered.paginate(:page => page, :per_page => 1) |
29 | 29 | @notice = @notices.first |
30 | + @comment = Comment.new | |
30 | 31 | end |
31 | 32 | |
32 | 33 | def create_issue |
... | ... | @@ -62,6 +63,30 @@ class ErrsController < ApplicationController |
62 | 63 | redirect_to app_path(@app) |
63 | 64 | end |
64 | 65 | |
66 | + | |
67 | + def create_comment | |
68 | + @comment = Comment.new(params[:comment].merge(:user_id => current_user.id)) | |
69 | + if @comment.valid? | |
70 | + @err.comments << @comment | |
71 | + @err.save | |
72 | + flash[:success] = "Comment saved!" | |
73 | + else | |
74 | + flash[:error] = "I'm sorry, your comment was blank! Try again?" | |
75 | + end | |
76 | + redirect_to app_err_path(@app, @err) | |
77 | + end | |
78 | + | |
79 | + def destroy_comment | |
80 | + @comment = Comment.find(params[:comment_id]) | |
81 | + if @comment.destroy | |
82 | + flash[:success] = "Comment deleted!" | |
83 | + else | |
84 | + flash[:error] = "Sorry, I couldn't delete your comment for some reason. I hope you don't have any sensitive information in there!" | |
85 | + end | |
86 | + redirect_to app_err_path(@app, @err) | |
87 | + end | |
88 | + | |
89 | + | |
65 | 90 | protected |
66 | 91 | |
67 | 92 | def find_app |
... | ... | @@ -83,3 +108,4 @@ class ErrsController < ApplicationController |
83 | 108 | end |
84 | 109 | |
85 | 110 | end |
111 | + | ... | ... |
app/helpers/application_helper.rb
1 | 1 | module ApplicationHelper |
2 | - | |
3 | - | |
2 | + | |
3 | + | |
4 | 4 | def lighthouse_tracker? object |
5 | 5 | object.issue_tracker_type == "lighthouseapp" |
6 | 6 | end |
7 | - | |
7 | + | |
8 | 8 | def user_agent_graph(error) |
9 | 9 | tallies = tally(error.notices) {|notice| pretty_user_agent(notice.user_agent)} |
10 | 10 | create_percentage_table(tallies, :total => error.notices.count) |
11 | 11 | end |
12 | - | |
12 | + | |
13 | 13 | def pretty_user_agent(user_agent) |
14 | 14 | (user_agent.nil? || user_agent.none?) ? "N/A" : "#{user_agent.browser} #{user_agent.version}" |
15 | 15 | end |
16 | - | |
16 | + | |
17 | 17 | def tally(collection, &block) |
18 | 18 | collection.inject({}) do |tallies, item| |
19 | 19 | value = yield item |
... | ... | @@ -21,7 +21,7 @@ module ApplicationHelper |
21 | 21 | tallies |
22 | 22 | end |
23 | 23 | end |
24 | - | |
24 | + | |
25 | 25 | def create_percentage_table(tallies, options={}) |
26 | 26 | total = (options[:total] || total_from_tallies(tallies)) |
27 | 27 | percent = 100.0 / total.to_f |
... | ... | @@ -29,12 +29,16 @@ module ApplicationHelper |
29 | 29 | .sort {|a, b| a[0] <=> b[0]} |
30 | 30 | render :partial => "errs/tally_table", :locals => {:rows => rows} |
31 | 31 | end |
32 | - | |
32 | + | |
33 | 33 | def total_from_tallies(tallies) |
34 | 34 | tallies.values.inject(0) {|sum, n| sum + n} |
35 | 35 | end |
36 | 36 | private :total_from_tallies |
37 | - | |
37 | + | |
38 | + def no_tracker? object | |
39 | + object.issue_tracker_type == "none" | |
40 | + end | |
41 | + | |
38 | 42 | def redmine_tracker? object |
39 | 43 | object.issue_tracker_type == "redmine" |
40 | 44 | end |
... | ... | @@ -47,3 +51,4 @@ module ApplicationHelper |
47 | 51 | object.issue_tracker_type == 'fogbugz' |
48 | 52 | end |
49 | 53 | end |
54 | + | ... | ... |
app/models/err.rb
app/views/apps/_fields.html.haml
... | ... | @@ -53,7 +53,7 @@ |
53 | 53 | %div.issue_tracker.nested |
54 | 54 | %div.choose |
55 | 55 | = w.radio_button :issue_tracker_type, :none |
56 | - = label_tag :issue_tracker_type_lighthouseapp, '(None)', :for => label_for_attr(w, 'issue_tracker_type_none') | |
56 | + = label_tag :issue_tracker_type_none, '(None)', :for => label_for_attr(w, 'issue_tracker_type_none') | |
57 | 57 | = w.radio_button :issue_tracker_type, :lighthouseapp |
58 | 58 | = label_tag :issue_tracker_type_lighthouseapp, 'Lighthouse', :for => label_for_attr(w, 'issue_tracker_type_lighthouseapp') |
59 | 59 | = w.radio_button :issue_tracker_type, :redmine |
... | ... | @@ -62,6 +62,8 @@ |
62 | 62 | = label_tag :issue_tracker_type_pivotal, 'Pivotal Tracker', :for => label_for_attr(w, 'issue_tracker_type_pivotal') |
63 | 63 | = w.radio_button :issue_tracker_type, :fogbugz |
64 | 64 | = label_tag :issue_tracker_type_fogbugz, 'FogBugz', :for => label_for_attr(w, 'issue_tracker_type_fogbugz') |
65 | + %div.tracker_params.none{:class => no_tracker?(w.object) ? 'chosen' : nil} | |
66 | + %p When no issue tracker has been configured, you will be able to leave comments on errors. | |
65 | 67 | %div.tracker_params.lighthouseapp{:class => lighthouse_tracker?(w.object) ? 'chosen' : nil} |
66 | 68 | = w.label :account, "Account" |
67 | 69 | = w.text_field :account, :placeholder => "abc from abc.lighthouseapp.com" | ... | ... |
app/views/errs/show.html.haml
... | ... | @@ -20,6 +20,25 @@ |
20 | 20 | - if @err.unresolved? |
21 | 21 | %span= link_to 'resolve', resolve_app_err_path(@app, @err), :method => :put, :confirm => err_confirm, :class => 'resolve' |
22 | 22 | |
23 | +- if !@app.issue_tracker_configured? || @err.comments.any? | |
24 | + - content_for :comments do | |
25 | + %h3 Comments on this Err | |
26 | + - @err.comments.each do |comment| | |
27 | + .window | |
28 | + %table.comment | |
29 | + %tr | |
30 | + %th | |
31 | + %span= link_to '✘'.html_safe, destroy_comment_app_err_path(@app, @err) << "?comment_id=#{comment.id}", :method => :delete, :confirm => "Are sure you don't need this comment?", :class => "destroy-comment" | |
32 | + = time_ago_in_words(comment.created_at, true) << " ago by " | |
33 | + = link_to comment.user.email, user_path(comment.user) | |
34 | + %tr | |
35 | + %td= comment.body.gsub("\n", "<br>").html_safe | |
36 | + - unless @app.issue_tracker_configured? | |
37 | + = form_for @comment, :url => create_comment_app_err_path(@app, @err) do |comment_form| | |
38 | + %p Add a comment | |
39 | + = comment_form.text_area :body, :style => "width: 420px; height: 80px;" | |
40 | + = comment_form.submit "Save Comment" | |
41 | + | |
23 | 42 | %h4= @notice.try(:message) |
24 | 43 | |
25 | 44 | = will_paginate @notices, :param_name => :notice, :page_links => false, :class => 'notice-pagination' |
... | ... | @@ -53,3 +72,4 @@ viewing occurrence #{@notices.current_page} of #{@notices.total_pages} |
53 | 72 | #session |
54 | 73 | %h3 Session |
55 | 74 | = render 'notices/session', :notice => @notice |
75 | + | ... | ... |
app/views/layouts/application.html.haml
... | ... | @@ -28,5 +28,9 @@ |
28 | 28 | #content |
29 | 29 | = render :partial => 'shared/flash_messages' |
30 | 30 | = yield |
31 | + - if content_for?(:comments) | |
32 | + #content-comments | |
33 | + = yield :comments | |
31 | 34 | #footer= "Powered by #{link_to 'Errbit', 'http://github.com/jdpace/errbit', :target => '_blank'}: the open source error catcher.".html_safe |
32 | 35 | = yield :scripts |
36 | + | ... | ... |
config/routes.rb
... | ... | @@ -22,6 +22,8 @@ Errbit::Application.routes.draw do |
22 | 22 | put :resolve |
23 | 23 | post :create_issue |
24 | 24 | delete :unlink_issue |
25 | + post :create_comment | |
26 | + delete :destroy_comment | |
25 | 27 | end |
26 | 28 | end |
27 | 29 | |
... | ... | @@ -33,3 +35,4 @@ Errbit::Application.routes.draw do |
33 | 35 | root :to => 'apps#index' |
34 | 36 | |
35 | 37 | end |
38 | + | ... | ... |
public/stylesheets/application.css
... | ... | @@ -139,14 +139,17 @@ a.action { float: right; font-size: 0.9em;} |
139 | 139 | border: 1px solid #C6C6C6; |
140 | 140 | } |
141 | 141 | |
142 | -/* Content Title */ | |
143 | -#content-title { | |
142 | +/* Content Title and Comments */ | |
143 | +#content-title, #content-comments { | |
144 | 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 | -#content-title h1 { | |
149 | +#content-comments { | |
150 | + background-color: #ffffff; | |
151 | +} | |
152 | +#content-title h1, #content-comments h3 { | |
150 | 153 | padding: 0; margin: 0; |
151 | 154 | width: 85%; |
152 | 155 | border: none; |
... | ... | @@ -154,6 +157,11 @@ a.action { float: right; font-size: 0.9em;} |
154 | 157 | font-size: 2em; line-height: 1em; font-weight: bold; font-family: arial, sans-serif; |
155 | 158 | word-wrap: break-word; |
156 | 159 | } |
160 | +#content-comments h3 { | |
161 | + font-size: 1.5em; | |
162 | + margin-bottom: 14px; | |
163 | +} | |
164 | + | |
157 | 165 | #content-title .meta { font-size: 0.9em; color: #787878; } |
158 | 166 | |
159 | 167 | /* Action Bar */ |
... | ... | @@ -610,6 +618,7 @@ table.tally th.value { |
610 | 618 | text-transform:none; |
611 | 619 | } |
612 | 620 | |
621 | + | |
613 | 622 | /* Resolve Errs */ |
614 | 623 | #action-bar a.resolve { |
615 | 624 | background: transparent url(images/icons/thumbs-up.png) 6px 5px no-repeat; |
... | ... | @@ -694,3 +703,28 @@ span.click_span { |
694 | 703 | display: none; |
695 | 704 | } |
696 | 705 | |
706 | +/* Comments */ | |
707 | +#content-comments form p { | |
708 | + margin: 30px 0 0 0; | |
709 | + text-transform: uppercase; | |
710 | +} | |
711 | +table.comment tbody th { | |
712 | + text-transform: none; | |
713 | + font-weight: normal; | |
714 | + height: 20px; | |
715 | + line-height: 0.5em; | |
716 | +} | |
717 | +table.comment tbody td { | |
718 | + background-color: #F9F9F9; | |
719 | +} | |
720 | +#content-comments a.destroy-comment { | |
721 | + color: #EE0000; | |
722 | + margin-right: 5px; | |
723 | +} | |
724 | +#content-comments a.destroy-comment:hover { | |
725 | + text-decoration: none; | |
726 | +} | |
727 | +#content-comments #comment_submit { | |
728 | + margin-top: 15px; | |
729 | +} | |
730 | + | ... | ... |
spec/controllers/errs_controller_spec.rb
... | ... | @@ -437,4 +437,60 @@ describe ErrsController do |
437 | 437 | end |
438 | 438 | end |
439 | 439 | end |
440 | + | |
441 | + | |
442 | + describe "POST /apps/:app_id/errs/:id/create_comment" do | |
443 | + render_views | |
444 | + | |
445 | + before(:each) do | |
446 | + sign_in Factory(:admin) | |
447 | + end | |
448 | + | |
449 | + context "successful comment creation" do | |
450 | + let(:err) { Factory(:err) } | |
451 | + let(:user) { Factory(:user) } | |
452 | + | |
453 | + before(:each) do | |
454 | + post :create_comment, :app_id => err.app.id, :id => err.id, | |
455 | + :comment => { :body => "One test comment", :user_id => user.id } | |
456 | + err.reload | |
457 | + end | |
458 | + | |
459 | + it "should create the comment" do | |
460 | + err.comments.size.should == 1 | |
461 | + end | |
462 | + | |
463 | + it "should redirect to err page" do | |
464 | + response.should redirect_to( app_err_path(err.app, err) ) | |
465 | + end | |
466 | + end | |
467 | + end | |
468 | + | |
469 | + describe "DELETE /apps/:app_id/errs/:id/destroy_comment" do | |
470 | + render_views | |
471 | + | |
472 | + before(:each) do | |
473 | + sign_in Factory(:admin) | |
474 | + end | |
475 | + | |
476 | + context "successful comment deletion" do | |
477 | + let(:err) { Factory :err_with_comments } | |
478 | + let(:comment) { err.comments.first } | |
479 | + | |
480 | + before(:each) do | |
481 | + delete :destroy_comment, :app_id => err.app.id, :id => err.id, :comment_id => comment.id | |
482 | + err.reload | |
483 | + end | |
484 | + | |
485 | + it "should delete the comment" do | |
486 | + err.comments.detect{|c| c.id.to_s == comment.id }.should == nil | |
487 | + end | |
488 | + | |
489 | + it "should redirect to err page" do | |
490 | + response.should redirect_to( app_err_path(err.app, err) ) | |
491 | + end | |
492 | + end | |
493 | + end | |
494 | + | |
440 | 495 | end |
496 | + | ... | ... |
spec/factories/err_factories.rb
... | ... | @@ -4,6 +4,11 @@ Factory.define :err do |e| |
4 | 4 | e.component 'foo' |
5 | 5 | e.action 'bar' |
6 | 6 | e.environment 'production' |
7 | + e.comments [] | |
8 | +end | |
9 | + | |
10 | +Factory.define(:err_with_comments, :parent => :err) do |ec| | |
11 | + ec.comments { (1..3).map{Factory(:comment)} } | |
7 | 12 | end |
8 | 13 | |
9 | 14 | Factory.define :notice do |n| |
... | ... | @@ -22,4 +27,5 @@ def random_backtrace |
22 | 27 | 'method' => ActiveSupport.methods.shuffle.first |
23 | 28 | }} |
24 | 29 | backtrace |
25 | -end | |
26 | 30 | \ No newline at end of file |
31 | +end | |
32 | + | ... | ... |
... | ... | @@ -0,0 +1,12 @@ |
1 | +require 'spec_helper' | |
2 | + | |
3 | +describe Comment do | |
4 | + context 'validations' do | |
5 | + it 'should require a body' do | |
6 | + comment = Factory.build(:comment, :body => nil) | |
7 | + comment.should_not be_valid | |
8 | + comment.errors[:body].should include("can't be blank") | |
9 | + end | |
10 | + end | |
11 | +end | |
12 | + | ... | ... |
spec/views/errs/show.html.haml_spec.rb
1 | 1 | require 'spec_helper' |
2 | 2 | |
3 | -describe "errs/show.html.erb" do | |
4 | - before do | |
3 | +describe "errs/show.html.erb" do | |
4 | + before do | |
5 | 5 | err = Factory(:err) |
6 | + comment = Factory(:comment) | |
6 | 7 | assign :err, err |
8 | + assign :comment, comment | |
7 | 9 | assign :app, err.app |
8 | 10 | assign :notices, err.notices.ordered.paginate(:page => 1, :per_page => 1) |
9 | 11 | assign :notice, err.notices.first |
... | ... | @@ -12,7 +14,7 @@ describe "errs/show.html.erb" do |
12 | 14 | describe "content_for :action_bar" do |
13 | 15 | |
14 | 16 | it "should confirm the 'resolve' link by default" do |
15 | - render | |
17 | + render | |
16 | 18 | action_bar = String.new(view.instance_variable_get(:@_content_for)[:action_bar]) |
17 | 19 | resolve_link = action_bar.match(/(<a href.*?(class="resolve").*?>)/)[0] |
18 | 20 | resolve_link.should =~ /data-confirm="Seriously\?"/ |
... | ... | @@ -20,7 +22,7 @@ describe "errs/show.html.erb" do |
20 | 22 | |
21 | 23 | it "should confirm the 'resolve' link if configuration is unset" do |
22 | 24 | Errbit::Config.stub(:confirm_resolve_err).and_return(nil) |
23 | - render | |
25 | + render | |
24 | 26 | action_bar = String.new(view.instance_variable_get(:@_content_for)[:action_bar]) |
25 | 27 | resolve_link = action_bar.match(/(<a href.*?(class="resolve").*?>)/)[0] |
26 | 28 | resolve_link.should =~ /data-confirm="Seriously\?"/ |
... | ... | @@ -28,7 +30,7 @@ describe "errs/show.html.erb" do |
28 | 30 | |
29 | 31 | it "should not confirm the 'resolve' link if configured not to" do |
30 | 32 | Errbit::Config.stub(:confirm_resolve_err).and_return(false) |
31 | - render | |
33 | + render | |
32 | 34 | action_bar = String.new(view.instance_variable_get(:@_content_for)[:action_bar]) |
33 | 35 | resolve_link = action_bar.match(/(<a href.*?(class="resolve").*?>)/)[0] |
34 | 36 | resolve_link.should_not =~ /data-confirm=/ |
... | ... | @@ -36,4 +38,38 @@ describe "errs/show.html.erb" do |
36 | 38 | |
37 | 39 | end |
38 | 40 | |
41 | + describe "content_for :comments" do | |
42 | + it 'should display comments and new comment form when no issue tracker' do | |
43 | + err = Factory(:err_with_comments) | |
44 | + assign :err, err | |
45 | + assign :app, err.app | |
46 | + render | |
47 | + comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments]) | |
48 | + comments_section.should =~ /Test comment/ | |
49 | + comments_section.should =~ /Add a comment/ | |
50 | + end | |
51 | + | |
52 | + context "with issue tracker" do | |
53 | + def with_issue_tracker(err) | |
54 | + err.app.issue_tracker = IssueTracker.new :issue_tracker_type => "lighthouseapp", :project_id => "1234" | |
55 | + assign :err, err | |
56 | + assign :app, err.app | |
57 | + end | |
58 | + | |
59 | + it 'should not display the comments section' do | |
60 | + with_issue_tracker(Factory(:err)) | |
61 | + render | |
62 | + view.instance_variable_get(:@_content_for)[:comments].should be_blank | |
63 | + end | |
64 | + | |
65 | + it 'should display existing comments' do | |
66 | + with_issue_tracker(Factory(:err_with_comments)) | |
67 | + render | |
68 | + comments_section = String.new(view.instance_variable_get(:@_content_for)[:comments]) | |
69 | + comments_section.should =~ /Test comment/ | |
70 | + comments_section.should_not =~ /Add a comment/ | |
71 | + end | |
72 | + end | |
73 | + end | |
39 | 74 | end |
75 | + | ... | ... |