Commit 17673f21873397a15449f70589758298b338621f
Committed by
Antonio Terceiro
1 parent
8c237770
Exists in
master
and in
29 other branches
Nested comments
(ActionItem1771)
Showing
19 changed files
with
689 additions
and
52 deletions
Show diff stats
app/controllers/public/content_viewer_controller.rb
... | ... | @@ -99,7 +99,7 @@ class ContentViewerController < ApplicationController |
99 | 99 | @images = @images.paginate(:per_page => per_page, :page => params[:npage]) unless params[:slideshow] |
100 | 100 | end |
101 | 101 | |
102 | - @comments = @page.comments(true) | |
102 | + @comments = @page.comments(true).as_thread | |
103 | 103 | if params[:slideshow] |
104 | 104 | render :action => 'slideshow', :layout => 'slideshow' |
105 | 105 | end |
... | ... | @@ -116,7 +116,7 @@ class ContentViewerController < ApplicationController |
116 | 116 | @comment = nil # clear the comment form |
117 | 117 | redirect_to :action => 'view_page', :profile => params[:profile], :page => @page.explode_path, :view => params[:view] |
118 | 118 | else |
119 | - @form_div = 'opened' | |
119 | + @form_div = 'opened' if params[:comment][:reply_of_id].blank? | |
120 | 120 | end |
121 | 121 | end |
122 | 122 | ... | ... |
app/models/comment.rb
... | ... | @@ -5,6 +5,8 @@ class Comment < ActiveRecord::Base |
5 | 5 | validates_presence_of :title, :body |
6 | 6 | belongs_to :article, :counter_cache => true |
7 | 7 | belongs_to :author, :class_name => 'Person', :foreign_key => 'author_id' |
8 | + has_many :children, :class_name => 'Comment', :foreign_key => 'reply_of_id', :dependent => :destroy | |
9 | + belongs_to :reply_of, :class_name => 'Comment', :foreign_key => 'reply_of_id' | |
8 | 10 | |
9 | 11 | # unauthenticated authors: |
10 | 12 | validates_presence_of :name, :if => (lambda { |record| !record.email.blank? }) |
... | ... | @@ -46,7 +48,7 @@ class Comment < ActiveRecord::Base |
46 | 48 | end |
47 | 49 | |
48 | 50 | def message |
49 | - author_id ? _('(removed user)') : ('<br />' + _('(unauthenticated user)')) | |
51 | + author_id ? _('(removed user)') : _('(unauthenticated user)') | |
50 | 52 | end |
51 | 53 | |
52 | 54 | def removed_user_image |
... | ... | @@ -73,6 +75,25 @@ class Comment < ActiveRecord::Base |
73 | 75 | end |
74 | 76 | end |
75 | 77 | |
78 | + def replies | |
79 | + @replies || children | |
80 | + end | |
81 | + | |
82 | + def replies=(comments_list) | |
83 | + @replies = comments_list | |
84 | + end | |
85 | + | |
86 | + def self.as_thread | |
87 | + result = {} | |
88 | + root = [] | |
89 | + all.each do |c| | |
90 | + c.replies = [] | |
91 | + result[c.id] ||= c | |
92 | + c.reply_of_id.nil? ? root << c : result[c.reply_of_id].replies << c | |
93 | + end | |
94 | + root | |
95 | + end | |
96 | + | |
76 | 97 | class Notifier < ActionMailer::Base |
77 | 98 | def mail(comment) |
78 | 99 | profile = comment.article.profile | ... | ... |
app/views/content_viewer/_comment.rhtml
1 | -<%= content_tag('a', '', :name => comment.anchor) %> | |
2 | -<div class="article-comment<%= ' comment-from-owner' if ( comment.author && (@page.profile.name == comment.author.name) ) %> comment-logged-<%= comment.author ? 'in' : 'out' %>"> | |
1 | +<li id="<%= comment.anchor %>" class="article-comment"> | |
2 | + <div class="article-comment-inner"> | |
3 | + | |
4 | + <div class="comment-content comment-logged-<%= comment.author ? 'in' : 'out' %> <%= 'comment-from-owner' if ( comment.author && (@page.profile.name == comment.author.name) ) %>"> | |
3 | 5 | |
4 | 6 | <% if comment.author %> |
5 | - <%= link_to content_tag( 'span', comment.author_name, :class => 'comment-info' ), comment.author.url, | |
7 | + <%= link_to image_tag(profile_icon(comment.author, :minor)) + | |
8 | + content_tag('span', comment.author_name, :class => 'comment-info'), | |
9 | + comment.author.url, | |
6 | 10 | :class => 'comment-picture', |
7 | - :style => 'background-image:url(%s)' % profile_icon(comment.author, :minor) | |
11 | + :title => comment.author_name | |
8 | 12 | %> |
9 | 13 | <% else %> |
10 | 14 | <%# unauthenticated user: display gravatar icon %> |
11 | - <% url_image = comment.author_id ? comment.removed_user_image : str_gravatar_url_for( comment.email, :size => 50 ) %> | |
12 | - <%= content_tag 'span', content_tag('span', comment.author_name + comment.message, :class => 'comment-info'), | |
15 | + <% url_image, status_class = comment.author_id ? [comment.removed_user_image, 'icon-user-removed'] : [str_gravatar_url_for( comment.email, :size => 50 ), 'icon-user-unknown'] %> | |
16 | + <%= content_tag 'span', image_tag(url_image) + | |
17 | + content_tag('span', comment.author_name, :class => 'comment-info') + | |
18 | + content_tag('span', comment.message, :class => 'comment-user-status ' + status_class), | |
13 | 19 | :class => 'comment-picture', |
14 | - :style => 'background-image:url(%s)' % url_image | |
20 | + :title => '%s %s' % [comment.author_name, comment.message] | |
15 | 21 | %> |
16 | 22 | <% end %> |
17 | 23 | |
... | ... | @@ -32,6 +38,35 @@ |
32 | 38 | <%= txt2html comment.body %> |
33 | 39 | </div> |
34 | 40 | </div> |
41 | + | |
42 | + <div class="comment_reply post_comment_box closed"> | |
43 | + <% if @comment && @comment.errors.any? && @comment.reply_of_id.to_i == comment.id %> | |
44 | + <%= error_messages_for :comment %> | |
45 | + <script type="text/javascript"> | |
46 | + jQuery(function() { | |
47 | + document.location.href = '#<%= comment.anchor %>'; | |
48 | + add_comment_reply_form('#comment-reply-to-<%= comment.id %>', <%= comment.id %>); | |
49 | + }); | |
50 | + </script> | |
51 | + <% end %> | |
52 | + <%= link_to_function _('Reply'), | |
53 | + "var f = add_comment_reply_form(this, %s); f.find('input[name=comment[title]], textarea').val(''); return false" % comment.id, | |
54 | + :class => 'comment-reply-link', | |
55 | + :id => 'comment-reply-to-' + comment.id.to_s | |
56 | + %> | |
57 | + </div> | |
58 | + | |
59 | + <% end %> | |
60 | + | |
61 | + </div> | |
62 | + | |
63 | + <% unless comment.replies.blank? %> | |
64 | + <ul class="comment-replies"> | |
65 | + <% comment.replies.each do |reply| %> | |
66 | + <%= render :partial => 'comment', :locals => { :comment => reply } %> | |
67 | + <% end %> | |
68 | + </ul> | |
35 | 69 | <% end %> |
36 | 70 | |
37 | -</div> | |
71 | + </div> | |
72 | +</li> | ... | ... |
app/views/content_viewer/_comment_form.rhtml
1 | -<% | |
2 | - comment_form_id = 'comment_form'+ rand(9999).to_s | |
3 | -%> | |
4 | - | |
5 | 1 | <% focus_on = logged_in? ? 'title' : 'name' %> |
6 | 2 | |
7 | -<%= error_messages_for :comment %> | |
3 | +<% if @comment && @comment.errors.any? && @comment.reply_of_id.blank? %> | |
4 | + <%= error_messages_for :comment %> | |
5 | + <script type="text/javascript">jQuery(function() { document.location.href = '#page-comment-form'; });</script> | |
6 | +<% end %> | |
8 | 7 | |
9 | 8 | <% @form_div ||= 'closed' %> |
10 | 9 | |
11 | -<div | |
12 | - class="post_comment_box <%= @form_div %>" | |
13 | - onclick="f=$('<%= comment_form_id %>'); | |
14 | - this.className = this.className.replace(/closed/,'opened'); | |
15 | - f.commit.focus(); f['comment[<%= focus_on %>]'].focus(); | |
16 | - this.onclick=null"> | |
10 | +<div class="post_comment_box <%= @form_div %>"> | |
17 | 11 | |
18 | -<h4><%= content_tag('a', '', :name => 'comment_form') + _('Post a comment') %></h4> | |
12 | +<h4 onclick="var d = jQuery(this).parent('.post_comment_box'); | |
13 | + if (d.hasClass('closed')) { | |
14 | + d.removeClass('closed'); | |
15 | + d.addClass('opened'); | |
16 | + d.find('input[name=comment[title]], textarea').val(''); | |
17 | + d.find('.comment_form input[name=comment[<%= focus_on %>]]').focus(); | |
18 | + }"> | |
19 | + <%= content_tag('a', '', :name => 'comment_form') + _('Post a comment') %> | |
20 | +</h4> | |
19 | 21 | |
20 | -<% form_tag( url_for(@page.view_url.merge({:only_path => true})), { :id => comment_form_id } ) do %> | |
22 | +<% form_tag( url_for(@page.view_url.merge({:only_path => true})), { :class => 'comment_form' } ) do %> | |
21 | 23 | <%= icaptcha_field() %> |
22 | 24 | <%= hidden_field_tag(:confirm, 'false') %> |
23 | 25 | |
... | ... | @@ -37,7 +39,8 @@ |
37 | 39 | <%= required labelled_form_field(_('Title'), text_field(:comment, :title)) %> |
38 | 40 | <%= required labelled_form_field(_('Enter your comment'), text_area(:comment, :body, :rows => 5)) %> |
39 | 41 | <% button_bar do %> |
40 | - <%= submit_button('add', _('Post comment'), :onclick => "$('confirm').value = 'true'; this.disabled = true; this.form.submit(); return true;") %> | |
42 | + <%= submit_button('add', _('Post comment'), :onclick => "this.form.confirm.value = 'true'; this.disabled = true; this.form.submit(); return true;") %> | |
43 | + <%= button_to_function :cancel, _('Cancel'), "f=jQuery(this).parents('.post_comment_box'); f.removeClass('opened'); f.addClass('closed'); return false" %> | |
41 | 44 | <% end %> |
42 | 45 | <% end %> |
43 | 46 | ... | ... |
app/views/content_viewer/view_page.rhtml
... | ... | @@ -120,14 +120,15 @@ |
120 | 120 | </div> |
121 | 121 | <% end %> |
122 | 122 | |
123 | -<div class="comments"> | |
124 | - <a name="comments_list"></a> | |
123 | +<div class="comments" id="comments_list"> | |
125 | 124 | <% if @page.accept_comments? %> |
126 | 125 | <h3 <%= 'class="no-comments-yet"' if @comments.size == 0 %>> |
127 | 126 | <%= number_of_comments(@page) %> |
128 | 127 | </h3> |
129 | - <%= render :partial => 'comment', :collection => @comments %> | |
130 | - <%= render :partial => 'comment_form' %> | |
128 | + <ul class="article-comments-list"> | |
129 | + <%= render :partial => 'comment', :collection => @comments %> | |
130 | + </ul> | |
131 | + <div id="page-comment-form"><%= render :partial => 'comment_form' %></div> | |
131 | 132 | <% end %> |
132 | 133 | </div><!-- end class="comments" --> |
133 | 134 | ... | ... |
... | ... | @@ -0,0 +1,90 @@ |
1 | +Feature: comment | |
2 | + As a visitor | |
3 | + I want to reply comments | |
4 | + | |
5 | + Background: | |
6 | + Given the following users | |
7 | + | login | | |
8 | + | booking | | |
9 | + And the following articles | |
10 | + | owner | name | | |
11 | + | booking | article to comment | | |
12 | + | booking | another article | | |
13 | + And the following comments | |
14 | + | article | author | title | body | | |
15 | + | article to comment | booking | root comment | this comment is not a reply | | |
16 | + | another article | booking | some comment | this is my very own comment | | |
17 | + | |
18 | + Scenario: not post a comment without javascript | |
19 | + Given I am on /booking/article-to-comment | |
20 | + When I follow "Reply" within ".comment-balloon" | |
21 | + Then I should not see "Enter your comment" within "div.comment-balloon" | |
22 | + | |
23 | + Scenario: not show any reply form by default | |
24 | + When I go to /booking/article-to-comment | |
25 | + Then I should not see "Enter your comment" within "div.comment-balloon" | |
26 | + And I should see "Reply" within "div.comment-balloon" | |
27 | + | |
28 | + @selenium | |
29 | + Scenario: show error messages when make a blank comment reply | |
30 | + Given I am logged in as "booking" | |
31 | + And I go to /booking/article-to-comment | |
32 | + And I follow "Reply" within ".comment-balloon" | |
33 | + When I press "Post comment" within ".comment-balloon" | |
34 | + Then I should see "Title can't be blank" within "div.comment_reply" | |
35 | + And I should see "Body can't be blank" within "div.comment_reply" | |
36 | + | |
37 | + @selenium | |
38 | + Scenario: not show any reply form by default | |
39 | + When I go to /booking/article-to-comment | |
40 | + Then I should not see "Enter your comment" within "div.comment-balloon" | |
41 | + And I should see "Reply" within "div.comment-balloon" | |
42 | + | |
43 | + @selenium | |
44 | + Scenario: render reply form | |
45 | + Given I am on /booking/article-to-comment | |
46 | + When I follow "Reply" within ".comment-balloon" | |
47 | + Then I should see "Enter your comment" within "div.comment_reply.opened" | |
48 | + | |
49 | + @selenium | |
50 | + Scenario: cancel comment reply | |
51 | + Given I am on /booking/article-to-comment | |
52 | + When I follow "Reply" within ".comment-balloon" | |
53 | + And I follow "Cancel" within ".comment-balloon" | |
54 | + Then I should see "Enter your comment" within "div.comment_reply.closed" | |
55 | + | |
56 | + @selenium | |
57 | + Scenario: not render same reply form twice | |
58 | + Given I am on /booking/article-to-comment | |
59 | + When I follow "Reply" within ".comment-balloon" | |
60 | + And I follow "Cancel" within ".comment-balloon" | |
61 | + And I follow "Reply" within ".comment-balloon" | |
62 | + Then there should be 1 "comment_form" within "comment_reply" | |
63 | + And I should see "Enter your comment" within "div.comment_reply.opened" | |
64 | + | |
65 | + @selenium | |
66 | + Scenario: reply a comment | |
67 | + Given I am logged in as "booking" | |
68 | + And I go to /booking/another-article | |
69 | + And I follow "Reply" within ".comment-balloon" | |
70 | + And I fill in "Title" within "comment-balloon" with "Hey ho, let's go!" | |
71 | + And I fill in "Enter your comment" within "comment-balloon" with "Hey ho, let's go!" | |
72 | + When I press "Post comment" within ".comment-balloon" | |
73 | + Then I should see "Hey ho, let's go" within "ul.comment-replies" | |
74 | + And there should be 1 "comment-replies" within "article-comment" | |
75 | + | |
76 | + @selenium | |
77 | + Scenario: redirect to right place after reply a picture comment | |
78 | + Given the following files | |
79 | + | owner | file | mime | | |
80 | + | booking | rails.png | image/png | | |
81 | + And the following comment | |
82 | + | article | author | title | body | | |
83 | + | rails.png | booking | root comment | this comment is not a reply | | |
84 | + Given I am logged in as "booking" | |
85 | + And I go to /booking/rails.png?view=true | |
86 | + And I follow "Reply" within ".comment-balloon" | |
87 | + And I fill in "Title" within "comment-balloon" with "Hey ho, let's go!" | |
88 | + And I fill in "Enter your comment" within "comment-balloon" with "Hey ho, let's go!" | |
89 | + When I press "Post comment" within ".comment-balloon" | |
90 | + Then I should be exactly on /booking/rails.png?view=true | ... | ... |
features/step_definitions/noosfero_steps.rb
... | ... | @@ -307,3 +307,13 @@ Given /^the articles of "(.+)" are moderated$/ do |organization| |
307 | 307 | organization.moderated_articles = true |
308 | 308 | organization.save |
309 | 309 | end |
310 | + | |
311 | +Given /^the following comments?$/ do |table| | |
312 | + table.hashes.each do |item| | |
313 | + data = item.dup | |
314 | + article = Article.find_by_name(data.delete("article")) | |
315 | + author = Profile[data.delete("author")] | |
316 | + comment = article.comments.build(:author => author, :title => data.delete("title"), :body => data.delete("body")) | |
317 | + comment.save! | |
318 | + end | |
319 | +end | ... | ... |
features/step_definitions/selenium_steps.rb
... | ... | @@ -66,6 +66,20 @@ When /^I select window "([^\"]*)"$/ do |selector| |
66 | 66 | selenium.select_window(selector) |
67 | 67 | end |
68 | 68 | |
69 | +When /^I fill in "([^\"]*)" within "([^\"]*)" with "([^\"]*)"$/ do |field_label, parent_class, value| | |
70 | + selenium.type("xpath=//*[contains(@class, '#{parent_class}')]//*[@id=//label[contains(., '#{field_label}')]/@for]", value) | |
71 | +end | |
72 | + | |
73 | +When /^I press "([^\"]*)" within "([^\"]*)"$/ do |button_value, selector| | |
74 | + selenium.click("css=#{selector} input[value=#{button_value}]") | |
75 | + selenium.wait_for_page_to_load(10000) | |
76 | +end | |
77 | + | |
78 | +Then /^there should be ([1-9][0-9]*) "([^\"]*)" within "([^\"]*)"$/ do |number, child_class, parent_class| | |
79 | + # Using xpath is the only way to count | |
80 | + response.selenium.get_xpath_count("//*[contains(@class,'#{parent_class}')]//*[contains(@class,'#{child_class}')]").to_i.should be(number.to_i) | |
81 | +end | |
82 | + | |
69 | 83 | #### Noosfero specific steps #### |
70 | 84 | |
71 | 85 | Then /^the select for category "([^\"]*)" should be visible$/ do |name| | ... | ... |
features/step_definitions/webrat_steps.rb
... | ... | @@ -125,8 +125,12 @@ Then /^I should see "([^\"]*)"$/ do |text| |
125 | 125 | end |
126 | 126 | |
127 | 127 | Then /^I should see "([^\"]*)" within "([^\"]*)"$/ do |text, selector| |
128 | - within(selector) do |content| | |
129 | - content.should contain(text) | |
128 | + if response.class.to_s == 'Webrat::SeleniumResponse' | |
129 | + response.selenium.text('css=' + selector).should include(text) | |
130 | + else | |
131 | + within(selector) do |content| | |
132 | + content.should contain(text) | |
133 | + end | |
130 | 134 | end |
131 | 135 | end |
132 | 136 | |
... | ... | @@ -147,8 +151,12 @@ Then /^I should not see "([^\"]*)"$/ do |text| |
147 | 151 | end |
148 | 152 | |
149 | 153 | Then /^I should not see "([^\"]*)" within "([^\"]*)"$/ do |text, selector| |
150 | - within(selector) do |content| | |
151 | - content.should_not contain(text) | |
154 | + if response.class.to_s == 'Webrat::SeleniumResponse' | |
155 | + response.selenium.text('css=' + selector).should_not include(text) | |
156 | + else | |
157 | + within(selector) do |content| | |
158 | + content.should_not contain(text) | |
159 | + end | |
152 | 160 | end |
153 | 161 | end |
154 | 162 | |
... | ... | @@ -187,4 +195,3 @@ end |
187 | 195 | Then /^show me the page$/ do |
188 | 196 | save_and_open_page |
189 | 197 | end |
190 | - | ... | ... |
public/designs/icons/tango/style.css
... | ... | @@ -84,3 +84,5 @@ |
84 | 84 | .icon-gallery { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } |
85 | 85 | .icon-newgallery { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } |
86 | 86 | .icon-locale { background-image: url(Tango/16x16/apps/preferences-desktop-locale.png) } |
87 | +.icon-user-removed { background-image: url(Tango/16x16/actions/gtk-cancel.png) } | |
88 | +.icon-user-unknown { background-image: url(Tango/16x16/status/dialog-error.png) } | ... | ... |
public/designs/themes/base/style.css
... | ... | @@ -1111,22 +1111,22 @@ hr.pre-posts, hr.sep-posts { |
1111 | 1111 | background: url(imgs/comment-owner-bg-NO.png) 0% 0% no-repeat; |
1112 | 1112 | } |
1113 | 1113 | |
1114 | - | |
1115 | 1114 | .comment-created-at { |
1116 | 1115 | position: relative; |
1117 | - padding-right: 10px; | |
1116 | + padding-right: 9px; | |
1118 | 1117 | } |
1118 | + | |
1119 | 1119 | .comment-from-owner .comment-created-at { |
1120 | 1120 | color: #333; |
1121 | 1121 | } |
1122 | 1122 | |
1123 | - | |
1124 | 1123 | .article-comment .button-bar { |
1125 | 1124 | position: relative; |
1126 | - top: 3px; | |
1127 | - right: 1px; | |
1125 | + top: 9px; | |
1126 | + right: 8px; | |
1128 | 1127 | z-index: 10px; |
1129 | 1128 | } |
1129 | + | |
1130 | 1130 | .article-comment .button-bar a { |
1131 | 1131 | position: relative; |
1132 | 1132 | } |
... | ... | @@ -1136,8 +1136,6 @@ hr.pre-posts, hr.sep-posts { |
1136 | 1136 | padding: 7px 12px 3px 26px; |
1137 | 1137 | } |
1138 | 1138 | |
1139 | - | |
1140 | - | |
1141 | 1139 | /* ==> controllers.css <== */ |
1142 | 1140 | |
1143 | 1141 | /******** controller-friends action-friends-index ********/ | ... | ... |
178 Bytes
308 Bytes
public/javascripts/application.js
... | ... | @@ -649,3 +649,20 @@ jQuery(function($) { |
649 | 649 | }) |
650 | 650 | } |
651 | 651 | }); |
652 | + | |
653 | +function add_comment_reply_form(button, comment_id) { | |
654 | + var container = jQuery(button).parents('.comment_reply'); | |
655 | + var f = container.find('.comment_form'); | |
656 | + if (f.length == 0) { | |
657 | + f = jQuery('#page-comment-form .comment_form').clone(); | |
658 | + f.find('.fieldWithErrors').map(function() { jQuery(this).replaceWith(jQuery(this).contents()); }); | |
659 | + f.prepend('<input type="hidden" name="comment[reply_of_id]" value="' + comment_id + '" />'); | |
660 | + container.append(f); | |
661 | + } | |
662 | + if (container.hasClass('closed')) { | |
663 | + container.removeClass('closed'); | |
664 | + container.addClass('opened'); | |
665 | + container.find('.comment_form input[type=text]:visible:first').focus(); | |
666 | + } | |
667 | + return f; | |
668 | +} | ... | ... |
public/stylesheets/application.css
... | ... | @@ -1025,11 +1025,6 @@ code input { |
1025 | 1025 | padding: 4px; |
1026 | 1026 | } |
1027 | 1027 | |
1028 | -.comment-from-owner { | |
1029 | - border: 1px solid #888; | |
1030 | - background: #eee; | |
1031 | -} | |
1032 | - | |
1033 | 1028 | #article .article-comment h4 { |
1034 | 1029 | font-size: 13px; |
1035 | 1030 | margin: 0px; |
... | ... | @@ -1042,7 +1037,6 @@ code input { |
1042 | 1037 | width: 70px; |
1043 | 1038 | background-repeat: no-repeat; |
1044 | 1039 | background-position: 0% 0%; |
1045 | - padding-top: 50px; | |
1046 | 1040 | } |
1047 | 1041 | |
1048 | 1042 | a.comment-picture { |
... | ... | @@ -1061,8 +1055,8 @@ a.comment-picture { |
1061 | 1055 | display: inline; |
1062 | 1056 | } |
1063 | 1057 | |
1064 | -.comment-details { | |
1065 | - margin-left: 100px; | |
1058 | +.comment-info { | |
1059 | + display: block; | |
1066 | 1060 | } |
1067 | 1061 | |
1068 | 1062 | .comment-from-owner .comment-info { |
... | ... | @@ -1071,12 +1065,265 @@ a.comment-picture { |
1071 | 1065 | |
1072 | 1066 | .comment-text { |
1073 | 1067 | font-size: 11px; |
1068 | + padding-right: 10px; | |
1074 | 1069 | } |
1075 | 1070 | |
1076 | 1071 | .comment-logged-out .comment-text { |
1077 | 1072 | color: #888; |
1078 | 1073 | } |
1079 | 1074 | |
1075 | +.comment-created-at { | |
1076 | + padding-right: 9px; | |
1077 | +} | |
1078 | + | |
1079 | +#content .comment-from-owner input.button, | |
1080 | +#content .comment-from-owner a.button { | |
1081 | + border-color: #646464; | |
1082 | +} | |
1083 | + | |
1084 | +.article-comment .button-bar { | |
1085 | + top: 9px; | |
1086 | + right: 8px; | |
1087 | +} | |
1088 | + | |
1089 | +.msie7 .article-comments-list .comment-balloon { | |
1090 | + margin-top: -15px; | |
1091 | +} | |
1092 | + | |
1093 | +.article-comments-list .comment-balloon br { | |
1094 | + line-height: 0; | |
1095 | +} | |
1096 | + | |
1097 | +.article-comments-list .comment-balloon div#errorExplanation h2 { | |
1098 | + margin-left: 20px; | |
1099 | + font-size: 14px; | |
1100 | +} | |
1101 | + | |
1102 | +.article-comments-list .comment-balloon div#errorExplanation p, | |
1103 | +.article-comments-list .comment-balloon div#errorExplanation ul, | |
1104 | +.article-comments-list .comment-balloon div#errorExplanation li { | |
1105 | + margin: 0; | |
1106 | + margin-left: 20px; | |
1107 | + text-align: left; | |
1108 | +} | |
1109 | + | |
1110 | +.article-comments-list .comment-balloon div#errorExplanation li { | |
1111 | + list-style: circle; | |
1112 | +} | |
1113 | + | |
1114 | +.comment-user-status { | |
1115 | + font-size: 9px; | |
1116 | + text-indent: -5000em; | |
1117 | + width: 16px; | |
1118 | + height: 16px; | |
1119 | + display: block; | |
1120 | + position: absolute; | |
1121 | + top: 33px; | |
1122 | + left: 33px; | |
1123 | + background-repeat: no-repeat; | |
1124 | +} | |
1125 | + | |
1126 | +#article .article-comments-list, | |
1127 | +#article .article-comments-list ul, | |
1128 | +#article .article-comments-list li { | |
1129 | + padding: 0; | |
1130 | + margin: 0; | |
1131 | + margin-bottom: 10px; | |
1132 | + list-style: none; | |
1133 | +} | |
1134 | + | |
1135 | +.article-comment .button-bar { | |
1136 | + margin: 0; | |
1137 | +} | |
1138 | + | |
1139 | +.article-comment .comment-details { | |
1140 | + margin: 0px; | |
1141 | + padding: 7px 1px 3px 26px; | |
1142 | +} | |
1143 | + | |
1144 | +#article .comment-reply-link { | |
1145 | + font-size: 10px; | |
1146 | + text-decoration: none; | |
1147 | + color: #000; | |
1148 | +} | |
1149 | + | |
1150 | +#article .comment-reply-link:hover { | |
1151 | + text-decoration: underline; | |
1152 | +} | |
1153 | + | |
1154 | +#article .opened .comment-reply-link { | |
1155 | + visibility: hidden; | |
1156 | +} | |
1157 | + | |
1158 | +.comment_form div.fieldWithErrors input { | |
1159 | + border-color: #999; | |
1160 | + border-width: 0 1px 1px 0; | |
1161 | + background: transparent url("../images/input-bg.gif") no-repeat top left; | |
1162 | +} | |
1163 | + | |
1164 | +.comment_form div.fieldWithErrors { | |
1165 | + background: transparent; | |
1166 | +} | |
1167 | + | |
1168 | +/* * * Comment Replies * * */ | |
1169 | + | |
1170 | +.comment-replies .comment-wrapper-1 { | |
1171 | + margin-left: 45px; | |
1172 | +} | |
1173 | + | |
1174 | +.comment-replies .comment-wrapper-1, | |
1175 | +.comment-replies .comment-wrapper-2, | |
1176 | +.comment-replies .comment-wrapper-3, | |
1177 | +.comment-replies .comment-wrapper-4, | |
1178 | +.comment-replies .comment-wrapper-5, | |
1179 | +.comment-replies .comment-wrapper-6, | |
1180 | +.comment-replies .comment-wrapper-7, | |
1181 | +.comment-replies .comment-wrapper-8, | |
1182 | +.comment-replies .comment-from-owner .comment-wrapper-1, | |
1183 | +.comment-replies .comment-from-owner .comment-wrapper-2, | |
1184 | +.comment-replies .comment-from-owner .comment-wrapper-3, | |
1185 | +.comment-replies .comment-from-owner .comment-wrapper-4, | |
1186 | +.comment-replies .comment-from-owner .comment-wrapper-5, | |
1187 | +.comment-replies .comment-from-owner .comment-wrapper-6, | |
1188 | +.comment-replies .comment-from-owner .comment-wrapper-7, | |
1189 | +.comment-replies .comment-from-owner .comment-wrapper-8 { | |
1190 | + background: transparent; | |
1191 | +} | |
1192 | + | |
1193 | +.comment-replies .comment-from-owner.comment-content { | |
1194 | + background: transparent url(/images/comment-reply-owner-bg.png) left top repeat-x; | |
1195 | +} | |
1196 | + | |
1197 | +.comment-replies .comment-user-status { | |
1198 | + top: 15px; | |
1199 | + left: 15px; | |
1200 | +} | |
1201 | + | |
1202 | +#article .article-comments-list .comment-replies li { | |
1203 | + margin-bottom: 2px; | |
1204 | +} | |
1205 | + | |
1206 | +.comment-replies .comment-balloon div#errorExplanation h2 { | |
1207 | + margin-left: 0; | |
1208 | +} | |
1209 | + | |
1210 | +.comment-replies .comment-text, | |
1211 | +.comment-replies .comment-created-at { | |
1212 | + padding-right: 7px; | |
1213 | +} | |
1214 | + | |
1215 | +.comment-replies .comment-picture { | |
1216 | + width: 40px; | |
1217 | + height: 57px; | |
1218 | + overflow: hidden; | |
1219 | +} | |
1220 | + | |
1221 | +.comment-replies .comment-picture img { | |
1222 | + width: 32px; | |
1223 | + height: 32px; | |
1224 | +} | |
1225 | + | |
1226 | +.article-comment .comment-replies .button-bar { | |
1227 | + right: 7px; | |
1228 | + top: 2px; | |
1229 | +} | |
1230 | + | |
1231 | +.article-comment form .button-bar, | |
1232 | +.article-comment .comment-replies form .button-bar { | |
1233 | + right: 0; | |
1234 | + top: 0; | |
1235 | +} | |
1236 | + | |
1237 | +.comment-replies .comment-details { | |
1238 | + padding-top: 0; | |
1239 | + padding-left: 0; | |
1240 | +} | |
1241 | + | |
1242 | +#article .article-comments-list .comment-replies { | |
1243 | + padding-left: 74px; | |
1244 | + margin-top: 2px; | |
1245 | +} | |
1246 | + | |
1247 | +#article .comment-replies .comment-replies { | |
1248 | + padding-left: 10px; | |
1249 | +} | |
1250 | + | |
1251 | +#article .comment-replies .comment-replies .article-comment, | |
1252 | +#article .comment-replies .comment-replies .article-comment-inner { | |
1253 | + border-right: 0; | |
1254 | + margin-right: -1px; | |
1255 | + padding-right: 1px; | |
1256 | +} | |
1257 | + | |
1258 | +.comment-replies .comment-content { | |
1259 | + padding: 4px 0 0 4px; | |
1260 | + -moz-border-radius: 3px; | |
1261 | + -webkit-border-radius: 3px; | |
1262 | + border-radius: 3px; | |
1263 | +} | |
1264 | + | |
1265 | +#article .comment-replies .article-comment { | |
1266 | + border: 1px solid #808080; | |
1267 | + padding: 0; | |
1268 | + margin-bottom: 10px; | |
1269 | + background: transparent url(/images/black-alpha-pixel-5.png) left top repeat; | |
1270 | + -moz-border-radius: 5px; | |
1271 | + -webkit-border-radius: 5px; | |
1272 | + border-radius: 5px; | |
1273 | +} | |
1274 | + | |
1275 | +.comment-replies .article-comment-inner { | |
1276 | + border: 1px solid #fff; | |
1277 | + padding: 0; | |
1278 | + -moz-border-radius: 4px; | |
1279 | + -webkit-border-radius: 4px; | |
1280 | + border-radius: 4px; | |
1281 | +} | |
1282 | + | |
1283 | +#article .comment-replies .comment-replies .article-comment { | |
1284 | + -moz-border-radius: 5px 0 0 5px; | |
1285 | + -webkit-border-radius: 5px 0 0 5px; | |
1286 | + border-radius: 5px 0 0 5px; | |
1287 | +} | |
1288 | + | |
1289 | +.comment-replies .comment-replies .article-comment-inner { | |
1290 | + -moz-border-radius: 4px 0 0 4px; | |
1291 | + -webkit-border-radius: 4px 0 0 4px; | |
1292 | + border-radius: 4px 0 0 4px; | |
1293 | +} | |
1294 | + | |
1295 | +.comment-replies .comment-replies .comment-content { | |
1296 | + -moz-border-radius: 3px 0 0 3px; | |
1297 | + -webkit-border-radius: 3px 0 0 3px; | |
1298 | + border-radius: 3px 0 0 3px; | |
1299 | +} | |
1300 | + | |
1301 | +#content .comment-replies a.button, | |
1302 | +#content .comment-replies input.button { | |
1303 | + border-color: #808080; | |
1304 | +} | |
1305 | + | |
1306 | +.comment-replies .comment_reply { | |
1307 | + padding-right: 9px; | |
1308 | +} | |
1309 | + | |
1310 | +.comment-replies .comment-info, | |
1311 | +.comment-replies .comment-created-at, | |
1312 | +.comment-replies .comment-logged-out .comment-text, | |
1313 | +.comment-logged-out h4 { | |
1314 | + color: #000; | |
1315 | +} | |
1316 | + | |
1317 | +.comment-replies .comment-created-at { | |
1318 | + opacity: 0.4; | |
1319 | +} | |
1320 | + | |
1321 | +.comment-replies .comment-logged-out .comment-text, | |
1322 | +.comment-logged-out .comment-picture, | |
1323 | +.comment-logged-out h4 { | |
1324 | + opacity: 0.6; | |
1325 | +} | |
1326 | + | |
1080 | 1327 | /* * * Comment Box * * */ |
1081 | 1328 | |
1082 | 1329 | .post_comment_box { |
... | ... | @@ -1107,6 +1354,10 @@ a.comment-picture { |
1107 | 1354 | margin: -10px 0px 0px 0px; |
1108 | 1355 | } |
1109 | 1356 | |
1357 | +.post_comment_box.opened form { | |
1358 | + padding-bottom: 15px; | |
1359 | +} | |
1360 | + | |
1110 | 1361 | .post_comment_box .formfield * { |
1111 | 1362 | width: 99%; |
1112 | 1363 | } |
... | ... | @@ -1117,7 +1368,7 @@ a.comment-picture { |
1117 | 1368 | |
1118 | 1369 | .post_comment_box input.button { |
1119 | 1370 | position: relative; |
1120 | - float: none; | |
1371 | + float: left; | |
1121 | 1372 | margin: auto; |
1122 | 1373 | } |
1123 | 1374 | |
... | ... | @@ -1133,6 +1384,30 @@ a.comment-picture { |
1133 | 1384 | display: block; |
1134 | 1385 | } |
1135 | 1386 | |
1387 | +.post_comment_box.comment_reply { | |
1388 | + margin: 0; | |
1389 | + text-align: right; | |
1390 | + padding: 0 11px 5px 0; | |
1391 | +} | |
1392 | + | |
1393 | +.comment_reply.post_comment_box.opened { | |
1394 | + background: transparent; | |
1395 | +} | |
1396 | + | |
1397 | +.comment_reply.post_comment_box form { | |
1398 | + margin: 0; | |
1399 | + padding-bottom: 27px; | |
1400 | + padding-left: 26px; | |
1401 | +} | |
1402 | + | |
1403 | +.comment-replies .comment_reply.post_comment_box form { | |
1404 | + padding-left: 0; | |
1405 | +} | |
1406 | + | |
1407 | +.post_comment_box.comment_reply #comment_title { | |
1408 | + width: 100%; | |
1409 | +} | |
1410 | + | |
1136 | 1411 | /* * * addThis button * * */ |
1137 | 1412 | |
1138 | 1413 | .msie #addThis { | ... | ... |
test/factories.rb
... | ... | @@ -431,4 +431,9 @@ module Noosfero::Factory |
431 | 431 | { :name => 'Sender', :email => 'sender@example.com', :article_name => 'Some title', :article_body => 'some body text', :article_abstract => 'some abstract text'} |
432 | 432 | end |
433 | 433 | |
434 | + def defaults_for_comment(params = {}) | |
435 | + name = "comment_#{rand(1000)}" | |
436 | + { :title => name, :body => "my own comment", :article_id => 1 }.merge(params) | |
437 | + end | |
438 | + | |
434 | 439 | end | ... | ... |
test/functional/content_viewer_controller_test.rb
... | ... | @@ -242,7 +242,7 @@ class ContentViewerControllerTest < Test::Unit::TestCase |
242 | 242 | |
243 | 243 | get :view_page, :profile => profile.identifier, :page => [ 'myarticle' ] |
244 | 244 | |
245 | - assert_tag :tag => 'form', :attributes => { :id => /^comment_form/, :action => '/person/article' } | |
245 | + assert_tag :tag => 'form', :attributes => { :class => /^comment_form/, :action => '/person/article' } | |
246 | 246 | end |
247 | 247 | |
248 | 248 | should "display current article's tags" do |
... | ... | @@ -928,7 +928,7 @@ class ContentViewerControllerTest < Test::Unit::TestCase |
928 | 928 | |
929 | 929 | get :view_page, :profile => profile.identifier, :page => article.explode_path |
930 | 930 | |
931 | - assert_tag :tag => 'span', :content => '(removed user)', :attributes => {:class => 'comment-info'} | |
931 | + assert_tag :tag => 'span', :content => '(removed user)', :attributes => {:class => 'comment-user-status icon-user-removed'} | |
932 | 932 | end |
933 | 933 | |
934 | 934 | should 'show comment form opened on error' do |
... | ... | @@ -1245,4 +1245,75 @@ class ContentViewerControllerTest < Test::Unit::TestCase |
1245 | 1245 | assert_redirected_to :profile => @profile.identifier, :page => page.explode_path |
1246 | 1246 | end |
1247 | 1247 | |
1248 | + should 'display reply to comment button if authenticated' do | |
1249 | + profile = create_user('testuser').person | |
1250 | + article = profile.articles.build(:name => 'test') | |
1251 | + article.save! | |
1252 | + comment = article.comments.build(:author => profile, :title => 'a comment', :body => 'lalala') | |
1253 | + comment.save! | |
1254 | + login_as 'testuser' | |
1255 | + get :view_page, :profile => 'testuser', :page => [ 'test' ] | |
1256 | + assert_tag :tag => 'a', :attributes => { :class => /comment-reply-link/ } | |
1257 | + end | |
1258 | + | |
1259 | + should 'display reply to comment button if not authenticated' do | |
1260 | + profile = create_user('testuser').person | |
1261 | + article = profile.articles.build(:name => 'test') | |
1262 | + article.save! | |
1263 | + comment = article.comments.build(:author => profile, :title => 'a comment', :body => 'lalala') | |
1264 | + comment.save! | |
1265 | + get :view_page, :profile => 'testuser', :page => [ 'test' ] | |
1266 | + assert_tag :tag => 'a', :attributes => { :class => /comment-reply-link/ } | |
1267 | + end | |
1268 | + | |
1269 | + should 'display replies if comment has replies' do | |
1270 | + profile = create_user('testuser').person | |
1271 | + article = profile.articles.build(:name => 'test') | |
1272 | + article.save! | |
1273 | + comment1 = article.comments.build(:author => profile, :title => 'a comment', :body => 'lalala') | |
1274 | + comment1.save! | |
1275 | + comment2 = article.comments.build(:author => profile, :title => 'a comment', :body => 'replying to lalala', :reply_of_id => comment1.id) | |
1276 | + comment2.save! | |
1277 | + get :view_page, :profile => 'testuser', :page => [ 'test' ] | |
1278 | + assert_tag :tag => 'ul', :attributes => { :class => 'comment-replies' } | |
1279 | + end | |
1280 | + | |
1281 | + should 'not display replies if comment does not have replies' do | |
1282 | + profile = create_user('testuser').person | |
1283 | + article = profile.articles.build(:name => 'test') | |
1284 | + article.save! | |
1285 | + comment = article.comments.build(:author => profile, :title => 'a comment', :body => 'lalala') | |
1286 | + comment.save! | |
1287 | + get :view_page, :profile => 'testuser', :page => [ 'test' ] | |
1288 | + assert_no_tag :tag => 'ul', :attributes => { :class => 'comment-replies' } | |
1289 | + end | |
1290 | + | |
1291 | + should 'show reply error' do | |
1292 | + profile = create_user('testuser').person | |
1293 | + article = profile.articles.build(:name => 'test') | |
1294 | + article.save! | |
1295 | + comment = article.comments.build(:author => profile, :title => 'root', :body => 'root') | |
1296 | + comment.save! | |
1297 | + login_as 'testuser' | |
1298 | + post :view_page, :profile => profile.identifier, :page => ['test'], :comment => { :title => '', :body => '', :reply_of_id => comment.id }, :confirm => 'true' | |
1299 | + assert_tag :tag => 'div', :attributes => { :class => /comment_reply/ }, :descendant => {:tag => 'div', :attributes => {:class => 'errorExplanation'} } | |
1300 | + assert_no_tag :tag => 'div', :attributes => { :id => 'page-comment-form' }, :descendant => {:tag => 'div', :attributes => {:class => 'errorExplanation'} } | |
1301 | + assert_tag :tag => 'div', :attributes => { :id => 'page-comment-form' }, :descendant => { :tag => 'div', :attributes => { :class => /post_comment_box closed/ } } | |
1302 | + end | |
1303 | + | |
1304 | + should 'show comment error' do | |
1305 | + profile = create_user('testuser').person | |
1306 | + article = profile.articles.build(:name => 'test') | |
1307 | + article.save! | |
1308 | + comment1 = article.comments.build(:author => profile, :title => 'root', :body => 'root') | |
1309 | + comment1.save! | |
1310 | + comment2 = article.comments.build(:author => profile, :title => 'root', :body => 'root', :reply_of_id => comment1.id) | |
1311 | + comment2.save! | |
1312 | + login_as 'testuser' | |
1313 | + post :view_page, :profile => profile.identifier, :page => ['test'], :comment => { :title => '', :body => '' }, :confirm => 'true' | |
1314 | + assert_no_tag :tag => 'div', :attributes => { :class => /comment_reply/ }, :descendant => {:tag => 'div', :attributes => {:class => 'errorExplanation'} } | |
1315 | + assert_tag :tag => 'div', :attributes => { :id => 'page-comment-form' }, :descendant => {:tag => 'div', :attributes => {:class => 'errorExplanation'} } | |
1316 | + assert_tag :tag => 'div', :attributes => { :id => 'page-comment-form' }, :descendant => { :tag => 'div', :attributes => { :class => /post_comment_box opened/ } } | |
1317 | + end | |
1318 | + | |
1248 | 1319 | end | ... | ... |
test/unit/comment_test.rb
... | ... | @@ -238,4 +238,83 @@ class CommentTest < Test::Unit::TestCase |
238 | 238 | assert_equal owner, ta.target |
239 | 239 | end |
240 | 240 | |
241 | + should "get children of a comment" do | |
242 | + c = fast_create(Comment) | |
243 | + c1 = fast_create(Comment, :reply_of_id => c.id) | |
244 | + c2 = fast_create(Comment) | |
245 | + c3 = fast_create(Comment, :reply_of_id => c.id) | |
246 | + assert_equal [c1,c3], c.children | |
247 | + end | |
248 | + | |
249 | + should "get parent of a comment" do | |
250 | + c = fast_create(Comment) | |
251 | + c1 = fast_create(Comment, :reply_of_id => c.id) | |
252 | + c2 = fast_create(Comment, :reply_of_id => c1.id) | |
253 | + c3 = fast_create(Comment, :reply_of_id => c.id) | |
254 | + c4 = fast_create(Comment) | |
255 | + assert_equal c, c1.reply_of | |
256 | + assert_equal c, c3.reply_of | |
257 | + assert_equal c1, c2.reply_of | |
258 | + assert_nil c4.reply_of | |
259 | + end | |
260 | + | |
261 | + should 'destroy replies when comment is removed' do | |
262 | + Comment.delete_all | |
263 | + owner = create_user('testuser').person | |
264 | + article = owner.articles.create!(:name => 'test', :body => '...') | |
265 | + c = article.comments.create!(:article => article, :name => 'foo', :title => 'bar', :body => 'my comment', :email => 'cracker@test.org') | |
266 | + c1 = article.comments.create!(:article => article, :name => 'foo', :title => 'bar', :body => 'my comment', :email => 'cracker@test.org', :reply_of_id => c.id) | |
267 | + c2 = article.comments.create!(:article => article, :name => 'foo', :title => 'bar', :body => 'my comment', :email => 'cracker@test.org') | |
268 | + c3 = article.comments.create!(:article => article, :name => 'foo', :title => 'bar', :body => 'my comment', :email => 'cracker@test.org', :reply_of_id => c.id) | |
269 | + assert_equal 4, Comment.count | |
270 | + c.destroy | |
271 | + assert_equal [c2], Comment.all | |
272 | + end | |
273 | + | |
274 | + should "get children if replies are not loaded" do | |
275 | + c = fast_create(Comment) | |
276 | + c1 = fast_create(Comment, :reply_of_id => c.id) | |
277 | + c2 = fast_create(Comment) | |
278 | + c3 = fast_create(Comment, :reply_of_id => c.id) | |
279 | + assert_nil c.instance_variable_get('@replies') | |
280 | + assert_equal [c1,c3], c.replies | |
281 | + end | |
282 | + | |
283 | + should "get replies if they are loaded" do | |
284 | + c = fast_create(Comment) | |
285 | + c1 = fast_create(Comment, :reply_of_id => c.id) | |
286 | + c2 = fast_create(Comment) | |
287 | + c3 = fast_create(Comment, :reply_of_id => c.id) | |
288 | + c.replies = [c2] | |
289 | + assert_not_nil c.instance_variable_get('@replies') | |
290 | + assert_equal [c2], c.replies | |
291 | + end | |
292 | + | |
293 | + should "set replies" do | |
294 | + c = fast_create(Comment) | |
295 | + c1 = fast_create(Comment, :reply_of_id => c.id) | |
296 | + c2 = fast_create(Comment) | |
297 | + c3 = fast_create(Comment, :reply_of_id => c.id) | |
298 | + c.replies = [] | |
299 | + c.replies << c2 | |
300 | + assert_equal [c2], c.instance_variable_get('@replies') | |
301 | + assert_equal [c2], c.replies | |
302 | + assert_equal [c1,c3], c.reload.children | |
303 | + end | |
304 | + | |
305 | + should "return comments as a thread" do | |
306 | + a = fast_create(Article) | |
307 | + c0 = fast_create(Comment, :article_id => a.id) | |
308 | + c1 = fast_create(Comment, :reply_of_id => c0.id, :article_id => a.id) | |
309 | + c2 = fast_create(Comment, :reply_of_id => c1.id, :article_id => a.id) | |
310 | + c3 = fast_create(Comment, :reply_of_id => c0.id, :article_id => a.id) | |
311 | + c4 = fast_create(Comment, :article_id => a.id) | |
312 | + result = a.comments.as_thread | |
313 | + assert_equal c0.id, result[0].id | |
314 | + assert_equal [c1.id, c3.id], result[0].replies.map(&:id) | |
315 | + assert_equal [c2.id], result[0].replies[0].replies.map(&:id) | |
316 | + assert_equal c4.id, result[1].id | |
317 | + assert result[1].replies.empty? | |
318 | + end | |
319 | + | |
241 | 320 | end | ... | ... |