Commit 17673f21873397a15449f70589758298b338621f
Committed by
Antonio Terceiro
1 parent
8c237770
Exists in
master
and in
23 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 | ... | ... |