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,7 +99,7 @@ class ContentViewerController < ApplicationController | ||
99 | @images = @images.paginate(:per_page => per_page, :page => params[:npage]) unless params[:slideshow] | 99 | @images = @images.paginate(:per_page => per_page, :page => params[:npage]) unless params[:slideshow] |
100 | end | 100 | end |
101 | 101 | ||
102 | - @comments = @page.comments(true) | 102 | + @comments = @page.comments(true).as_thread |
103 | if params[:slideshow] | 103 | if params[:slideshow] |
104 | render :action => 'slideshow', :layout => 'slideshow' | 104 | render :action => 'slideshow', :layout => 'slideshow' |
105 | end | 105 | end |
@@ -116,7 +116,7 @@ class ContentViewerController < ApplicationController | @@ -116,7 +116,7 @@ class ContentViewerController < ApplicationController | ||
116 | @comment = nil # clear the comment form | 116 | @comment = nil # clear the comment form |
117 | redirect_to :action => 'view_page', :profile => params[:profile], :page => @page.explode_path, :view => params[:view] | 117 | redirect_to :action => 'view_page', :profile => params[:profile], :page => @page.explode_path, :view => params[:view] |
118 | else | 118 | else |
119 | - @form_div = 'opened' | 119 | + @form_div = 'opened' if params[:comment][:reply_of_id].blank? |
120 | end | 120 | end |
121 | end | 121 | end |
122 | 122 |
app/models/comment.rb
@@ -5,6 +5,8 @@ class Comment < ActiveRecord::Base | @@ -5,6 +5,8 @@ class Comment < ActiveRecord::Base | ||
5 | validates_presence_of :title, :body | 5 | validates_presence_of :title, :body |
6 | belongs_to :article, :counter_cache => true | 6 | belongs_to :article, :counter_cache => true |
7 | belongs_to :author, :class_name => 'Person', :foreign_key => 'author_id' | 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 | # unauthenticated authors: | 11 | # unauthenticated authors: |
10 | validates_presence_of :name, :if => (lambda { |record| !record.email.blank? }) | 12 | validates_presence_of :name, :if => (lambda { |record| !record.email.blank? }) |
@@ -46,7 +48,7 @@ class Comment < ActiveRecord::Base | @@ -46,7 +48,7 @@ class Comment < ActiveRecord::Base | ||
46 | end | 48 | end |
47 | 49 | ||
48 | def message | 50 | def message |
49 | - author_id ? _('(removed user)') : ('<br />' + _('(unauthenticated user)')) | 51 | + author_id ? _('(removed user)') : _('(unauthenticated user)') |
50 | end | 52 | end |
51 | 53 | ||
52 | def removed_user_image | 54 | def removed_user_image |
@@ -73,6 +75,25 @@ class Comment < ActiveRecord::Base | @@ -73,6 +75,25 @@ class Comment < ActiveRecord::Base | ||
73 | end | 75 | end |
74 | end | 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 | class Notifier < ActionMailer::Base | 97 | class Notifier < ActionMailer::Base |
77 | def mail(comment) | 98 | def mail(comment) |
78 | profile = comment.article.profile | 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 | <% if comment.author %> | 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 | :class => 'comment-picture', | 10 | :class => 'comment-picture', |
7 | - :style => 'background-image:url(%s)' % profile_icon(comment.author, :minor) | 11 | + :title => comment.author_name |
8 | %> | 12 | %> |
9 | <% else %> | 13 | <% else %> |
10 | <%# unauthenticated user: display gravatar icon %> | 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 | :class => 'comment-picture', | 19 | :class => 'comment-picture', |
14 | - :style => 'background-image:url(%s)' % url_image | 20 | + :title => '%s %s' % [comment.author_name, comment.message] |
15 | %> | 21 | %> |
16 | <% end %> | 22 | <% end %> |
17 | 23 | ||
@@ -32,6 +38,35 @@ | @@ -32,6 +38,35 @@ | ||
32 | <%= txt2html comment.body %> | 38 | <%= txt2html comment.body %> |
33 | </div> | 39 | </div> |
34 | </div> | 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 | <% end %> | 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 | <% focus_on = logged_in? ? 'title' : 'name' %> | 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 | <% @form_div ||= 'closed' %> | 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 | <%= icaptcha_field() %> | 23 | <%= icaptcha_field() %> |
22 | <%= hidden_field_tag(:confirm, 'false') %> | 24 | <%= hidden_field_tag(:confirm, 'false') %> |
23 | 25 | ||
@@ -37,7 +39,8 @@ | @@ -37,7 +39,8 @@ | ||
37 | <%= required labelled_form_field(_('Title'), text_field(:comment, :title)) %> | 39 | <%= required labelled_form_field(_('Title'), text_field(:comment, :title)) %> |
38 | <%= required labelled_form_field(_('Enter your comment'), text_area(:comment, :body, :rows => 5)) %> | 40 | <%= required labelled_form_field(_('Enter your comment'), text_area(:comment, :body, :rows => 5)) %> |
39 | <% button_bar do %> | 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 | <% end %> | 44 | <% end %> |
42 | <% end %> | 45 | <% end %> |
43 | 46 |
app/views/content_viewer/view_page.rhtml
@@ -120,14 +120,15 @@ | @@ -120,14 +120,15 @@ | ||
120 | </div> | 120 | </div> |
121 | <% end %> | 121 | <% end %> |
122 | 122 | ||
123 | -<div class="comments"> | ||
124 | - <a name="comments_list"></a> | 123 | +<div class="comments" id="comments_list"> |
125 | <% if @page.accept_comments? %> | 124 | <% if @page.accept_comments? %> |
126 | <h3 <%= 'class="no-comments-yet"' if @comments.size == 0 %>> | 125 | <h3 <%= 'class="no-comments-yet"' if @comments.size == 0 %>> |
127 | <%= number_of_comments(@page) %> | 126 | <%= number_of_comments(@page) %> |
128 | </h3> | 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 | <% end %> | 132 | <% end %> |
132 | </div><!-- end class="comments" --> | 133 | </div><!-- end class="comments" --> |
133 | 134 |
@@ -0,0 +1,90 @@ | @@ -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,3 +307,13 @@ Given /^the articles of "(.+)" are moderated$/ do |organization| | ||
307 | organization.moderated_articles = true | 307 | organization.moderated_articles = true |
308 | organization.save | 308 | organization.save |
309 | end | 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,6 +66,20 @@ When /^I select window "([^\"]*)"$/ do |selector| | ||
66 | selenium.select_window(selector) | 66 | selenium.select_window(selector) |
67 | end | 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 | #### Noosfero specific steps #### | 83 | #### Noosfero specific steps #### |
70 | 84 | ||
71 | Then /^the select for category "([^\"]*)" should be visible$/ do |name| | 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,8 +125,12 @@ Then /^I should see "([^\"]*)"$/ do |text| | ||
125 | end | 125 | end |
126 | 126 | ||
127 | Then /^I should see "([^\"]*)" within "([^\"]*)"$/ do |text, selector| | 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 | end | 134 | end |
131 | end | 135 | end |
132 | 136 | ||
@@ -147,8 +151,12 @@ Then /^I should not see "([^\"]*)"$/ do |text| | @@ -147,8 +151,12 @@ Then /^I should not see "([^\"]*)"$/ do |text| | ||
147 | end | 151 | end |
148 | 152 | ||
149 | Then /^I should not see "([^\"]*)" within "([^\"]*)"$/ do |text, selector| | 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 | end | 160 | end |
153 | end | 161 | end |
154 | 162 | ||
@@ -187,4 +195,3 @@ end | @@ -187,4 +195,3 @@ end | ||
187 | Then /^show me the page$/ do | 195 | Then /^show me the page$/ do |
188 | save_and_open_page | 196 | save_and_open_page |
189 | end | 197 | end |
190 | - |
public/designs/icons/tango/style.css
@@ -84,3 +84,5 @@ | @@ -84,3 +84,5 @@ | ||
84 | .icon-gallery { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } | 84 | .icon-gallery { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } |
85 | .icon-newgallery { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } | 85 | .icon-newgallery { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } |
86 | .icon-locale { background-image: url(Tango/16x16/apps/preferences-desktop-locale.png) } | 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,22 +1111,22 @@ hr.pre-posts, hr.sep-posts { | ||
1111 | background: url(imgs/comment-owner-bg-NO.png) 0% 0% no-repeat; | 1111 | background: url(imgs/comment-owner-bg-NO.png) 0% 0% no-repeat; |
1112 | } | 1112 | } |
1113 | 1113 | ||
1114 | - | ||
1115 | .comment-created-at { | 1114 | .comment-created-at { |
1116 | position: relative; | 1115 | position: relative; |
1117 | - padding-right: 10px; | 1116 | + padding-right: 9px; |
1118 | } | 1117 | } |
1118 | + | ||
1119 | .comment-from-owner .comment-created-at { | 1119 | .comment-from-owner .comment-created-at { |
1120 | color: #333; | 1120 | color: #333; |
1121 | } | 1121 | } |
1122 | 1122 | ||
1123 | - | ||
1124 | .article-comment .button-bar { | 1123 | .article-comment .button-bar { |
1125 | position: relative; | 1124 | position: relative; |
1126 | - top: 3px; | ||
1127 | - right: 1px; | 1125 | + top: 9px; |
1126 | + right: 8px; | ||
1128 | z-index: 10px; | 1127 | z-index: 10px; |
1129 | } | 1128 | } |
1129 | + | ||
1130 | .article-comment .button-bar a { | 1130 | .article-comment .button-bar a { |
1131 | position: relative; | 1131 | position: relative; |
1132 | } | 1132 | } |
@@ -1136,8 +1136,6 @@ hr.pre-posts, hr.sep-posts { | @@ -1136,8 +1136,6 @@ hr.pre-posts, hr.sep-posts { | ||
1136 | padding: 7px 12px 3px 26px; | 1136 | padding: 7px 12px 3px 26px; |
1137 | } | 1137 | } |
1138 | 1138 | ||
1139 | - | ||
1140 | - | ||
1141 | /* ==> controllers.css <== */ | 1139 | /* ==> controllers.css <== */ |
1142 | 1140 | ||
1143 | /******** controller-friends action-friends-index ********/ | 1141 | /******** controller-friends action-friends-index ********/ |
178 Bytes
308 Bytes
public/javascripts/application.js
@@ -649,3 +649,20 @@ jQuery(function($) { | @@ -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,11 +1025,6 @@ code input { | ||
1025 | padding: 4px; | 1025 | padding: 4px; |
1026 | } | 1026 | } |
1027 | 1027 | ||
1028 | -.comment-from-owner { | ||
1029 | - border: 1px solid #888; | ||
1030 | - background: #eee; | ||
1031 | -} | ||
1032 | - | ||
1033 | #article .article-comment h4 { | 1028 | #article .article-comment h4 { |
1034 | font-size: 13px; | 1029 | font-size: 13px; |
1035 | margin: 0px; | 1030 | margin: 0px; |
@@ -1042,7 +1037,6 @@ code input { | @@ -1042,7 +1037,6 @@ code input { | ||
1042 | width: 70px; | 1037 | width: 70px; |
1043 | background-repeat: no-repeat; | 1038 | background-repeat: no-repeat; |
1044 | background-position: 0% 0%; | 1039 | background-position: 0% 0%; |
1045 | - padding-top: 50px; | ||
1046 | } | 1040 | } |
1047 | 1041 | ||
1048 | a.comment-picture { | 1042 | a.comment-picture { |
@@ -1061,8 +1055,8 @@ a.comment-picture { | @@ -1061,8 +1055,8 @@ a.comment-picture { | ||
1061 | display: inline; | 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 | .comment-from-owner .comment-info { | 1062 | .comment-from-owner .comment-info { |
@@ -1071,12 +1065,265 @@ a.comment-picture { | @@ -1071,12 +1065,265 @@ a.comment-picture { | ||
1071 | 1065 | ||
1072 | .comment-text { | 1066 | .comment-text { |
1073 | font-size: 11px; | 1067 | font-size: 11px; |
1068 | + padding-right: 10px; | ||
1074 | } | 1069 | } |
1075 | 1070 | ||
1076 | .comment-logged-out .comment-text { | 1071 | .comment-logged-out .comment-text { |
1077 | color: #888; | 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 | /* * * Comment Box * * */ | 1327 | /* * * Comment Box * * */ |
1081 | 1328 | ||
1082 | .post_comment_box { | 1329 | .post_comment_box { |
@@ -1107,6 +1354,10 @@ a.comment-picture { | @@ -1107,6 +1354,10 @@ a.comment-picture { | ||
1107 | margin: -10px 0px 0px 0px; | 1354 | margin: -10px 0px 0px 0px; |
1108 | } | 1355 | } |
1109 | 1356 | ||
1357 | +.post_comment_box.opened form { | ||
1358 | + padding-bottom: 15px; | ||
1359 | +} | ||
1360 | + | ||
1110 | .post_comment_box .formfield * { | 1361 | .post_comment_box .formfield * { |
1111 | width: 99%; | 1362 | width: 99%; |
1112 | } | 1363 | } |
@@ -1117,7 +1368,7 @@ a.comment-picture { | @@ -1117,7 +1368,7 @@ a.comment-picture { | ||
1117 | 1368 | ||
1118 | .post_comment_box input.button { | 1369 | .post_comment_box input.button { |
1119 | position: relative; | 1370 | position: relative; |
1120 | - float: none; | 1371 | + float: left; |
1121 | margin: auto; | 1372 | margin: auto; |
1122 | } | 1373 | } |
1123 | 1374 | ||
@@ -1133,6 +1384,30 @@ a.comment-picture { | @@ -1133,6 +1384,30 @@ a.comment-picture { | ||
1133 | display: block; | 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 | /* * * addThis button * * */ | 1411 | /* * * addThis button * * */ |
1137 | 1412 | ||
1138 | .msie #addThis { | 1413 | .msie #addThis { |
test/factories.rb
@@ -431,4 +431,9 @@ module Noosfero::Factory | @@ -431,4 +431,9 @@ module Noosfero::Factory | ||
431 | { :name => 'Sender', :email => 'sender@example.com', :article_name => 'Some title', :article_body => 'some body text', :article_abstract => 'some abstract text'} | 431 | { :name => 'Sender', :email => 'sender@example.com', :article_name => 'Some title', :article_body => 'some body text', :article_abstract => 'some abstract text'} |
432 | end | 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 | end | 439 | end |
test/functional/content_viewer_controller_test.rb
@@ -242,7 +242,7 @@ class ContentViewerControllerTest < Test::Unit::TestCase | @@ -242,7 +242,7 @@ class ContentViewerControllerTest < Test::Unit::TestCase | ||
242 | 242 | ||
243 | get :view_page, :profile => profile.identifier, :page => [ 'myarticle' ] | 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 | end | 246 | end |
247 | 247 | ||
248 | should "display current article's tags" do | 248 | should "display current article's tags" do |
@@ -928,7 +928,7 @@ class ContentViewerControllerTest < Test::Unit::TestCase | @@ -928,7 +928,7 @@ class ContentViewerControllerTest < Test::Unit::TestCase | ||
928 | 928 | ||
929 | get :view_page, :profile => profile.identifier, :page => article.explode_path | 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 | end | 932 | end |
933 | 933 | ||
934 | should 'show comment form opened on error' do | 934 | should 'show comment form opened on error' do |
@@ -1245,4 +1245,75 @@ class ContentViewerControllerTest < Test::Unit::TestCase | @@ -1245,4 +1245,75 @@ class ContentViewerControllerTest < Test::Unit::TestCase | ||
1245 | assert_redirected_to :profile => @profile.identifier, :page => page.explode_path | 1245 | assert_redirected_to :profile => @profile.identifier, :page => page.explode_path |
1246 | end | 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 | end | 1319 | end |
test/unit/comment_test.rb
@@ -238,4 +238,83 @@ class CommentTest < Test::Unit::TestCase | @@ -238,4 +238,83 @@ class CommentTest < Test::Unit::TestCase | ||
238 | assert_equal owner, ta.target | 238 | assert_equal owner, ta.target |
239 | end | 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 | end | 320 | end |