From 17673f21873397a15449f70589758298b338621f Mon Sep 17 00:00:00 2001 From: Caio SBA Date: Sun, 9 Jan 2011 15:35:07 -0300 Subject: [PATCH] Nested comments --- app/controllers/public/content_viewer_controller.rb | 4 ++-- app/models/comment.rb | 23 ++++++++++++++++++++++- app/views/content_viewer/_comment.rhtml | 51 +++++++++++++++++++++++++++++++++++++++++++-------- app/views/content_viewer/_comment_form.rhtml | 31 +++++++++++++++++-------------- app/views/content_viewer/view_page.rhtml | 9 +++++---- db/migrate/20101221134544_add_reply_to_comments.rb | 9 +++++++++ features/comment_reply.feature | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ features/step_definitions/noosfero_steps.rb | 10 ++++++++++ features/step_definitions/selenium_steps.rb | 14 ++++++++++++++ features/step_definitions/webrat_steps.rb | 17 ++++++++++++----- public/designs/icons/tango/style.css | 2 ++ public/designs/themes/base/style.css | 12 +++++------- public/images/black-alpha-pixel-5.png | Bin 0 -> 178 bytes public/images/comment-reply-owner-bg.png | Bin 0 -> 308 bytes public/javascripts/application.js | 17 +++++++++++++++++ public/stylesheets/application.css | 293 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------- test/factories.rb | 5 +++++ test/functional/content_viewer_controller_test.rb | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- test/unit/comment_test.rb | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 19 files changed, 689 insertions(+), 52 deletions(-) create mode 100644 db/migrate/20101221134544_add_reply_to_comments.rb create mode 100644 features/comment_reply.feature create mode 100644 public/images/black-alpha-pixel-5.png create mode 100644 public/images/comment-reply-owner-bg.png diff --git a/app/controllers/public/content_viewer_controller.rb b/app/controllers/public/content_viewer_controller.rb index 27723b5..47dec71 100644 --- a/app/controllers/public/content_viewer_controller.rb +++ b/app/controllers/public/content_viewer_controller.rb @@ -99,7 +99,7 @@ class ContentViewerController < ApplicationController @images = @images.paginate(:per_page => per_page, :page => params[:npage]) unless params[:slideshow] end - @comments = @page.comments(true) + @comments = @page.comments(true).as_thread if params[:slideshow] render :action => 'slideshow', :layout => 'slideshow' end @@ -116,7 +116,7 @@ class ContentViewerController < ApplicationController @comment = nil # clear the comment form redirect_to :action => 'view_page', :profile => params[:profile], :page => @page.explode_path, :view => params[:view] else - @form_div = 'opened' + @form_div = 'opened' if params[:comment][:reply_of_id].blank? end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 0bd61d7..0eb87b3 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -5,6 +5,8 @@ class Comment < ActiveRecord::Base validates_presence_of :title, :body belongs_to :article, :counter_cache => true belongs_to :author, :class_name => 'Person', :foreign_key => 'author_id' + has_many :children, :class_name => 'Comment', :foreign_key => 'reply_of_id', :dependent => :destroy + belongs_to :reply_of, :class_name => 'Comment', :foreign_key => 'reply_of_id' # unauthenticated authors: validates_presence_of :name, :if => (lambda { |record| !record.email.blank? }) @@ -46,7 +48,7 @@ class Comment < ActiveRecord::Base end def message - author_id ? _('(removed user)') : ('
' + _('(unauthenticated user)')) + author_id ? _('(removed user)') : _('(unauthenticated user)') end def removed_user_image @@ -73,6 +75,25 @@ class Comment < ActiveRecord::Base end end + def replies + @replies || children + end + + def replies=(comments_list) + @replies = comments_list + end + + def self.as_thread + result = {} + root = [] + all.each do |c| + c.replies = [] + result[c.id] ||= c + c.reply_of_id.nil? ? root << c : result[c.reply_of_id].replies << c + end + root + end + class Notifier < ActionMailer::Base def mail(comment) profile = comment.article.profile diff --git a/app/views/content_viewer/_comment.rhtml b/app/views/content_viewer/_comment.rhtml index d2cfe1a..240ae7d 100644 --- a/app/views/content_viewer/_comment.rhtml +++ b/app/views/content_viewer/_comment.rhtml @@ -1,17 +1,23 @@ -<%= content_tag('a', '', :name => comment.anchor) %> -
+
  • +
    + +
    <% if comment.author %> - <%= link_to content_tag( 'span', comment.author_name, :class => 'comment-info' ), comment.author.url, + <%= link_to image_tag(profile_icon(comment.author, :minor)) + + content_tag('span', comment.author_name, :class => 'comment-info'), + comment.author.url, :class => 'comment-picture', - :style => 'background-image:url(%s)' % profile_icon(comment.author, :minor) + :title => comment.author_name %> <% else %> <%# unauthenticated user: display gravatar icon %> - <% url_image = comment.author_id ? comment.removed_user_image : str_gravatar_url_for( comment.email, :size => 50 ) %> - <%= content_tag 'span', content_tag('span', comment.author_name + comment.message, :class => 'comment-info'), + <% 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'] %> + <%= content_tag 'span', image_tag(url_image) + + content_tag('span', comment.author_name, :class => 'comment-info') + + content_tag('span', comment.message, :class => 'comment-user-status ' + status_class), :class => 'comment-picture', - :style => 'background-image:url(%s)' % url_image + :title => '%s %s' % [comment.author_name, comment.message] %> <% end %> @@ -32,6 +38,35 @@ <%= txt2html comment.body %>
    + +
    + <% if @comment && @comment.errors.any? && @comment.reply_of_id.to_i == comment.id %> + <%= error_messages_for :comment %> + + <% end %> + <%= link_to_function _('Reply'), + "var f = add_comment_reply_form(this, %s); f.find('input[name=comment[title]], textarea').val(''); return false" % comment.id, + :class => 'comment-reply-link', + :id => 'comment-reply-to-' + comment.id.to_s + %> +
    + + <% end %> + +
  • + + <% unless comment.replies.blank? %> + <% end %> - + + diff --git a/app/views/content_viewer/_comment_form.rhtml b/app/views/content_viewer/_comment_form.rhtml index f6ed11a..6cd9286 100644 --- a/app/views/content_viewer/_comment_form.rhtml +++ b/app/views/content_viewer/_comment_form.rhtml @@ -1,23 +1,25 @@ -<% - comment_form_id = 'comment_form'+ rand(9999).to_s -%> - <% focus_on = logged_in? ? 'title' : 'name' %> -<%= error_messages_for :comment %> +<% if @comment && @comment.errors.any? && @comment.reply_of_id.blank? %> + <%= error_messages_for :comment %> + +<% end %> <% @form_div ||= 'closed' %> -
    +
    -

    <%= content_tag('a', '', :name => 'comment_form') + _('Post a comment') %>

    +

    + <%= content_tag('a', '', :name => 'comment_form') + _('Post a comment') %> +

    -<% form_tag( url_for(@page.view_url.merge({:only_path => true})), { :id => comment_form_id } ) do %> +<% form_tag( url_for(@page.view_url.merge({:only_path => true})), { :class => 'comment_form' } ) do %> <%= icaptcha_field() %> <%= hidden_field_tag(:confirm, 'false') %> @@ -37,7 +39,8 @@ <%= required labelled_form_field(_('Title'), text_field(:comment, :title)) %> <%= required labelled_form_field(_('Enter your comment'), text_area(:comment, :body, :rows => 5)) %> <% button_bar do %> - <%= submit_button('add', _('Post comment'), :onclick => "$('confirm').value = 'true'; this.disabled = true; this.form.submit(); return true;") %> + <%= submit_button('add', _('Post comment'), :onclick => "this.form.confirm.value = 'true'; this.disabled = true; this.form.submit(); return true;") %> + <%= button_to_function :cancel, _('Cancel'), "f=jQuery(this).parents('.post_comment_box'); f.removeClass('opened'); f.addClass('closed'); return false" %> <% end %> <% end %> diff --git a/app/views/content_viewer/view_page.rhtml b/app/views/content_viewer/view_page.rhtml index ba98f70..0288bc8 100644 --- a/app/views/content_viewer/view_page.rhtml +++ b/app/views/content_viewer/view_page.rhtml @@ -120,14 +120,15 @@
    <% end %> -
    - +
    <% if @page.accept_comments? %>

    > <%= number_of_comments(@page) %>

    - <%= render :partial => 'comment', :collection => @comments %> - <%= render :partial => 'comment_form' %> +
      + <%= render :partial => 'comment', :collection => @comments %> +
    +
    <%= render :partial => 'comment_form' %>
    <% end %>
    diff --git a/db/migrate/20101221134544_add_reply_to_comments.rb b/db/migrate/20101221134544_add_reply_to_comments.rb new file mode 100644 index 0000000..cd4ff6a --- /dev/null +++ b/db/migrate/20101221134544_add_reply_to_comments.rb @@ -0,0 +1,9 @@ +class AddReplyToComments < ActiveRecord::Migration + def self.up + add_column :comments, :reply_of_id, :integer + end + + def self.down + remove_column :comments, :reply_of_id + end +end diff --git a/features/comment_reply.feature b/features/comment_reply.feature new file mode 100644 index 0000000..46f083e --- /dev/null +++ b/features/comment_reply.feature @@ -0,0 +1,90 @@ +Feature: comment + As a visitor + I want to reply comments + + Background: + Given the following users + | login | + | booking | + And the following articles + | owner | name | + | booking | article to comment | + | booking | another article | + And the following comments + | article | author | title | body | + | article to comment | booking | root comment | this comment is not a reply | + | another article | booking | some comment | this is my very own comment | + + Scenario: not post a comment without javascript + Given I am on /booking/article-to-comment + When I follow "Reply" within ".comment-balloon" + Then I should not see "Enter your comment" within "div.comment-balloon" + + Scenario: not show any reply form by default + When I go to /booking/article-to-comment + Then I should not see "Enter your comment" within "div.comment-balloon" + And I should see "Reply" within "div.comment-balloon" + + @selenium + Scenario: show error messages when make a blank comment reply + Given I am logged in as "booking" + And I go to /booking/article-to-comment + And I follow "Reply" within ".comment-balloon" + When I press "Post comment" within ".comment-balloon" + Then I should see "Title can't be blank" within "div.comment_reply" + And I should see "Body can't be blank" within "div.comment_reply" + + @selenium + Scenario: not show any reply form by default + When I go to /booking/article-to-comment + Then I should not see "Enter your comment" within "div.comment-balloon" + And I should see "Reply" within "div.comment-balloon" + + @selenium + Scenario: render reply form + Given I am on /booking/article-to-comment + When I follow "Reply" within ".comment-balloon" + Then I should see "Enter your comment" within "div.comment_reply.opened" + + @selenium + Scenario: cancel comment reply + Given I am on /booking/article-to-comment + When I follow "Reply" within ".comment-balloon" + And I follow "Cancel" within ".comment-balloon" + Then I should see "Enter your comment" within "div.comment_reply.closed" + + @selenium + Scenario: not render same reply form twice + Given I am on /booking/article-to-comment + When I follow "Reply" within ".comment-balloon" + And I follow "Cancel" within ".comment-balloon" + And I follow "Reply" within ".comment-balloon" + Then there should be 1 "comment_form" within "comment_reply" + And I should see "Enter your comment" within "div.comment_reply.opened" + + @selenium + Scenario: reply a comment + Given I am logged in as "booking" + And I go to /booking/another-article + And I follow "Reply" within ".comment-balloon" + And I fill in "Title" within "comment-balloon" with "Hey ho, let's go!" + And I fill in "Enter your comment" within "comment-balloon" with "Hey ho, let's go!" + When I press "Post comment" within ".comment-balloon" + Then I should see "Hey ho, let's go" within "ul.comment-replies" + And there should be 1 "comment-replies" within "article-comment" + + @selenium + Scenario: redirect to right place after reply a picture comment + Given the following files + | owner | file | mime | + | booking | rails.png | image/png | + And the following comment + | article | author | title | body | + | rails.png | booking | root comment | this comment is not a reply | + Given I am logged in as "booking" + And I go to /booking/rails.png?view=true + And I follow "Reply" within ".comment-balloon" + And I fill in "Title" within "comment-balloon" with "Hey ho, let's go!" + And I fill in "Enter your comment" within "comment-balloon" with "Hey ho, let's go!" + When I press "Post comment" within ".comment-balloon" + Then I should be exactly on /booking/rails.png?view=true diff --git a/features/step_definitions/noosfero_steps.rb b/features/step_definitions/noosfero_steps.rb index f5404f1..1a51376 100644 --- a/features/step_definitions/noosfero_steps.rb +++ b/features/step_definitions/noosfero_steps.rb @@ -307,3 +307,13 @@ Given /^the articles of "(.+)" are moderated$/ do |organization| organization.moderated_articles = true organization.save end + +Given /^the following comments?$/ do |table| + table.hashes.each do |item| + data = item.dup + article = Article.find_by_name(data.delete("article")) + author = Profile[data.delete("author")] + comment = article.comments.build(:author => author, :title => data.delete("title"), :body => data.delete("body")) + comment.save! + end +end diff --git a/features/step_definitions/selenium_steps.rb b/features/step_definitions/selenium_steps.rb index 3550ff9..30accda 100644 --- a/features/step_definitions/selenium_steps.rb +++ b/features/step_definitions/selenium_steps.rb @@ -66,6 +66,20 @@ When /^I select window "([^\"]*)"$/ do |selector| selenium.select_window(selector) end +When /^I fill in "([^\"]*)" within "([^\"]*)" with "([^\"]*)"$/ do |field_label, parent_class, value| + selenium.type("xpath=//*[contains(@class, '#{parent_class}')]//*[@id=//label[contains(., '#{field_label}')]/@for]", value) +end + +When /^I press "([^\"]*)" within "([^\"]*)"$/ do |button_value, selector| + selenium.click("css=#{selector} input[value=#{button_value}]") + selenium.wait_for_page_to_load(10000) +end + +Then /^there should be ([1-9][0-9]*) "([^\"]*)" within "([^\"]*)"$/ do |number, child_class, parent_class| + # Using xpath is the only way to count + response.selenium.get_xpath_count("//*[contains(@class,'#{parent_class}')]//*[contains(@class,'#{child_class}')]").to_i.should be(number.to_i) +end + #### Noosfero specific steps #### Then /^the select for category "([^\"]*)" should be visible$/ do |name| diff --git a/features/step_definitions/webrat_steps.rb b/features/step_definitions/webrat_steps.rb index 429953d..c79a1ec 100644 --- a/features/step_definitions/webrat_steps.rb +++ b/features/step_definitions/webrat_steps.rb @@ -125,8 +125,12 @@ Then /^I should see "([^\"]*)"$/ do |text| end Then /^I should see "([^\"]*)" within "([^\"]*)"$/ do |text, selector| - within(selector) do |content| - content.should contain(text) + if response.class.to_s == 'Webrat::SeleniumResponse' + response.selenium.text('css=' + selector).should include(text) + else + within(selector) do |content| + content.should contain(text) + end end end @@ -147,8 +151,12 @@ Then /^I should not see "([^\"]*)"$/ do |text| end Then /^I should not see "([^\"]*)" within "([^\"]*)"$/ do |text, selector| - within(selector) do |content| - content.should_not contain(text) + if response.class.to_s == 'Webrat::SeleniumResponse' + response.selenium.text('css=' + selector).should_not include(text) + else + within(selector) do |content| + content.should_not contain(text) + end end end @@ -187,4 +195,3 @@ end Then /^show me the page$/ do save_and_open_page end - diff --git a/public/designs/icons/tango/style.css b/public/designs/icons/tango/style.css index fc355b5..0f5b9d6 100644 --- a/public/designs/icons/tango/style.css +++ b/public/designs/icons/tango/style.css @@ -84,3 +84,5 @@ .icon-gallery { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } .icon-newgallery { background-image: url(Tango/16x16/mimetypes/image-x-generic.png) } .icon-locale { background-image: url(Tango/16x16/apps/preferences-desktop-locale.png) } +.icon-user-removed { background-image: url(Tango/16x16/actions/gtk-cancel.png) } +.icon-user-unknown { background-image: url(Tango/16x16/status/dialog-error.png) } diff --git a/public/designs/themes/base/style.css b/public/designs/themes/base/style.css index 4cec199..c8c9771 100644 --- a/public/designs/themes/base/style.css +++ b/public/designs/themes/base/style.css @@ -1111,22 +1111,22 @@ hr.pre-posts, hr.sep-posts { background: url(imgs/comment-owner-bg-NO.png) 0% 0% no-repeat; } - .comment-created-at { position: relative; - padding-right: 10px; + padding-right: 9px; } + .comment-from-owner .comment-created-at { color: #333; } - .article-comment .button-bar { position: relative; - top: 3px; - right: 1px; + top: 9px; + right: 8px; z-index: 10px; } + .article-comment .button-bar a { position: relative; } @@ -1136,8 +1136,6 @@ hr.pre-posts, hr.sep-posts { padding: 7px 12px 3px 26px; } - - /* ==> controllers.css <== */ /******** controller-friends action-friends-index ********/ diff --git a/public/images/black-alpha-pixel-5.png b/public/images/black-alpha-pixel-5.png new file mode 100644 index 0000000..a728b36 Binary files /dev/null and b/public/images/black-alpha-pixel-5.png differ diff --git a/public/images/comment-reply-owner-bg.png b/public/images/comment-reply-owner-bg.png new file mode 100644 index 0000000..0224a60 Binary files /dev/null and b/public/images/comment-reply-owner-bg.png differ diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 805ceac..c9065f3 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -649,3 +649,20 @@ jQuery(function($) { }) } }); + +function add_comment_reply_form(button, comment_id) { + var container = jQuery(button).parents('.comment_reply'); + var f = container.find('.comment_form'); + if (f.length == 0) { + f = jQuery('#page-comment-form .comment_form').clone(); + f.find('.fieldWithErrors').map(function() { jQuery(this).replaceWith(jQuery(this).contents()); }); + f.prepend(''); + container.append(f); + } + if (container.hasClass('closed')) { + container.removeClass('closed'); + container.addClass('opened'); + container.find('.comment_form input[type=text]:visible:first').focus(); + } + return f; +} diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index 9d83744..de862a4 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -1025,11 +1025,6 @@ code input { padding: 4px; } -.comment-from-owner { - border: 1px solid #888; - background: #eee; -} - #article .article-comment h4 { font-size: 13px; margin: 0px; @@ -1042,7 +1037,6 @@ code input { width: 70px; background-repeat: no-repeat; background-position: 0% 0%; - padding-top: 50px; } a.comment-picture { @@ -1061,8 +1055,8 @@ a.comment-picture { display: inline; } -.comment-details { - margin-left: 100px; +.comment-info { + display: block; } .comment-from-owner .comment-info { @@ -1071,12 +1065,265 @@ a.comment-picture { .comment-text { font-size: 11px; + padding-right: 10px; } .comment-logged-out .comment-text { color: #888; } +.comment-created-at { + padding-right: 9px; +} + +#content .comment-from-owner input.button, +#content .comment-from-owner a.button { + border-color: #646464; +} + +.article-comment .button-bar { + top: 9px; + right: 8px; +} + +.msie7 .article-comments-list .comment-balloon { + margin-top: -15px; +} + +.article-comments-list .comment-balloon br { + line-height: 0; +} + +.article-comments-list .comment-balloon div#errorExplanation h2 { + margin-left: 20px; + font-size: 14px; +} + +.article-comments-list .comment-balloon div#errorExplanation p, +.article-comments-list .comment-balloon div#errorExplanation ul, +.article-comments-list .comment-balloon div#errorExplanation li { + margin: 0; + margin-left: 20px; + text-align: left; +} + +.article-comments-list .comment-balloon div#errorExplanation li { + list-style: circle; +} + +.comment-user-status { + font-size: 9px; + text-indent: -5000em; + width: 16px; + height: 16px; + display: block; + position: absolute; + top: 33px; + left: 33px; + background-repeat: no-repeat; +} + +#article .article-comments-list, +#article .article-comments-list ul, +#article .article-comments-list li { + padding: 0; + margin: 0; + margin-bottom: 10px; + list-style: none; +} + +.article-comment .button-bar { + margin: 0; +} + +.article-comment .comment-details { + margin: 0px; + padding: 7px 1px 3px 26px; +} + +#article .comment-reply-link { + font-size: 10px; + text-decoration: none; + color: #000; +} + +#article .comment-reply-link:hover { + text-decoration: underline; +} + +#article .opened .comment-reply-link { + visibility: hidden; +} + +.comment_form div.fieldWithErrors input { + border-color: #999; + border-width: 0 1px 1px 0; + background: transparent url("../images/input-bg.gif") no-repeat top left; +} + +.comment_form div.fieldWithErrors { + background: transparent; +} + +/* * * Comment Replies * * */ + +.comment-replies .comment-wrapper-1 { + margin-left: 45px; +} + +.comment-replies .comment-wrapper-1, +.comment-replies .comment-wrapper-2, +.comment-replies .comment-wrapper-3, +.comment-replies .comment-wrapper-4, +.comment-replies .comment-wrapper-5, +.comment-replies .comment-wrapper-6, +.comment-replies .comment-wrapper-7, +.comment-replies .comment-wrapper-8, +.comment-replies .comment-from-owner .comment-wrapper-1, +.comment-replies .comment-from-owner .comment-wrapper-2, +.comment-replies .comment-from-owner .comment-wrapper-3, +.comment-replies .comment-from-owner .comment-wrapper-4, +.comment-replies .comment-from-owner .comment-wrapper-5, +.comment-replies .comment-from-owner .comment-wrapper-6, +.comment-replies .comment-from-owner .comment-wrapper-7, +.comment-replies .comment-from-owner .comment-wrapper-8 { + background: transparent; +} + +.comment-replies .comment-from-owner.comment-content { + background: transparent url(/images/comment-reply-owner-bg.png) left top repeat-x; +} + +.comment-replies .comment-user-status { + top: 15px; + left: 15px; +} + +#article .article-comments-list .comment-replies li { + margin-bottom: 2px; +} + +.comment-replies .comment-balloon div#errorExplanation h2 { + margin-left: 0; +} + +.comment-replies .comment-text, +.comment-replies .comment-created-at { + padding-right: 7px; +} + +.comment-replies .comment-picture { + width: 40px; + height: 57px; + overflow: hidden; +} + +.comment-replies .comment-picture img { + width: 32px; + height: 32px; +} + +.article-comment .comment-replies .button-bar { + right: 7px; + top: 2px; +} + +.article-comment form .button-bar, +.article-comment .comment-replies form .button-bar { + right: 0; + top: 0; +} + +.comment-replies .comment-details { + padding-top: 0; + padding-left: 0; +} + +#article .article-comments-list .comment-replies { + padding-left: 74px; + margin-top: 2px; +} + +#article .comment-replies .comment-replies { + padding-left: 10px; +} + +#article .comment-replies .comment-replies .article-comment, +#article .comment-replies .comment-replies .article-comment-inner { + border-right: 0; + margin-right: -1px; + padding-right: 1px; +} + +.comment-replies .comment-content { + padding: 4px 0 0 4px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; + border-radius: 3px; +} + +#article .comment-replies .article-comment { + border: 1px solid #808080; + padding: 0; + margin-bottom: 10px; + background: transparent url(/images/black-alpha-pixel-5.png) left top repeat; + -moz-border-radius: 5px; + -webkit-border-radius: 5px; + border-radius: 5px; +} + +.comment-replies .article-comment-inner { + border: 1px solid #fff; + padding: 0; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; +} + +#article .comment-replies .comment-replies .article-comment { + -moz-border-radius: 5px 0 0 5px; + -webkit-border-radius: 5px 0 0 5px; + border-radius: 5px 0 0 5px; +} + +.comment-replies .comment-replies .article-comment-inner { + -moz-border-radius: 4px 0 0 4px; + -webkit-border-radius: 4px 0 0 4px; + border-radius: 4px 0 0 4px; +} + +.comment-replies .comment-replies .comment-content { + -moz-border-radius: 3px 0 0 3px; + -webkit-border-radius: 3px 0 0 3px; + border-radius: 3px 0 0 3px; +} + +#content .comment-replies a.button, +#content .comment-replies input.button { + border-color: #808080; +} + +.comment-replies .comment_reply { + padding-right: 9px; +} + +.comment-replies .comment-info, +.comment-replies .comment-created-at, +.comment-replies .comment-logged-out .comment-text, +.comment-logged-out h4 { + color: #000; +} + +.comment-replies .comment-created-at { + opacity: 0.4; +} + +.comment-replies .comment-logged-out .comment-text, +.comment-logged-out .comment-picture, +.comment-logged-out h4 { + opacity: 0.6; +} + /* * * Comment Box * * */ .post_comment_box { @@ -1107,6 +1354,10 @@ a.comment-picture { margin: -10px 0px 0px 0px; } +.post_comment_box.opened form { + padding-bottom: 15px; +} + .post_comment_box .formfield * { width: 99%; } @@ -1117,7 +1368,7 @@ a.comment-picture { .post_comment_box input.button { position: relative; - float: none; + float: left; margin: auto; } @@ -1133,6 +1384,30 @@ a.comment-picture { display: block; } +.post_comment_box.comment_reply { + margin: 0; + text-align: right; + padding: 0 11px 5px 0; +} + +.comment_reply.post_comment_box.opened { + background: transparent; +} + +.comment_reply.post_comment_box form { + margin: 0; + padding-bottom: 27px; + padding-left: 26px; +} + +.comment-replies .comment_reply.post_comment_box form { + padding-left: 0; +} + +.post_comment_box.comment_reply #comment_title { + width: 100%; +} + /* * * addThis button * * */ .msie #addThis { diff --git a/test/factories.rb b/test/factories.rb index d21216b..1416b5a 100644 --- a/test/factories.rb +++ b/test/factories.rb @@ -431,4 +431,9 @@ module Noosfero::Factory { :name => 'Sender', :email => 'sender@example.com', :article_name => 'Some title', :article_body => 'some body text', :article_abstract => 'some abstract text'} end + def defaults_for_comment(params = {}) + name = "comment_#{rand(1000)}" + { :title => name, :body => "my own comment", :article_id => 1 }.merge(params) + end + end diff --git a/test/functional/content_viewer_controller_test.rb b/test/functional/content_viewer_controller_test.rb index 7c57e2a..1bb54cb 100644 --- a/test/functional/content_viewer_controller_test.rb +++ b/test/functional/content_viewer_controller_test.rb @@ -242,7 +242,7 @@ class ContentViewerControllerTest < Test::Unit::TestCase get :view_page, :profile => profile.identifier, :page => [ 'myarticle' ] - assert_tag :tag => 'form', :attributes => { :id => /^comment_form/, :action => '/person/article' } + assert_tag :tag => 'form', :attributes => { :class => /^comment_form/, :action => '/person/article' } end should "display current article's tags" do @@ -928,7 +928,7 @@ class ContentViewerControllerTest < Test::Unit::TestCase get :view_page, :profile => profile.identifier, :page => article.explode_path - assert_tag :tag => 'span', :content => '(removed user)', :attributes => {:class => 'comment-info'} + assert_tag :tag => 'span', :content => '(removed user)', :attributes => {:class => 'comment-user-status icon-user-removed'} end should 'show comment form opened on error' do @@ -1245,4 +1245,75 @@ class ContentViewerControllerTest < Test::Unit::TestCase assert_redirected_to :profile => @profile.identifier, :page => page.explode_path end + should 'display reply to comment button if authenticated' do + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + comment = article.comments.build(:author => profile, :title => 'a comment', :body => 'lalala') + comment.save! + login_as 'testuser' + get :view_page, :profile => 'testuser', :page => [ 'test' ] + assert_tag :tag => 'a', :attributes => { :class => /comment-reply-link/ } + end + + should 'display reply to comment button if not authenticated' do + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + comment = article.comments.build(:author => profile, :title => 'a comment', :body => 'lalala') + comment.save! + get :view_page, :profile => 'testuser', :page => [ 'test' ] + assert_tag :tag => 'a', :attributes => { :class => /comment-reply-link/ } + end + + should 'display replies if comment has replies' do + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + comment1 = article.comments.build(:author => profile, :title => 'a comment', :body => 'lalala') + comment1.save! + comment2 = article.comments.build(:author => profile, :title => 'a comment', :body => 'replying to lalala', :reply_of_id => comment1.id) + comment2.save! + get :view_page, :profile => 'testuser', :page => [ 'test' ] + assert_tag :tag => 'ul', :attributes => { :class => 'comment-replies' } + end + + should 'not display replies if comment does not have replies' do + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + comment = article.comments.build(:author => profile, :title => 'a comment', :body => 'lalala') + comment.save! + get :view_page, :profile => 'testuser', :page => [ 'test' ] + assert_no_tag :tag => 'ul', :attributes => { :class => 'comment-replies' } + end + + should 'show reply error' do + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + comment = article.comments.build(:author => profile, :title => 'root', :body => 'root') + comment.save! + login_as 'testuser' + post :view_page, :profile => profile.identifier, :page => ['test'], :comment => { :title => '', :body => '', :reply_of_id => comment.id }, :confirm => 'true' + assert_tag :tag => 'div', :attributes => { :class => /comment_reply/ }, :descendant => {:tag => 'div', :attributes => {:class => 'errorExplanation'} } + assert_no_tag :tag => 'div', :attributes => { :id => 'page-comment-form' }, :descendant => {:tag => 'div', :attributes => {:class => 'errorExplanation'} } + assert_tag :tag => 'div', :attributes => { :id => 'page-comment-form' }, :descendant => { :tag => 'div', :attributes => { :class => /post_comment_box closed/ } } + end + + should 'show comment error' do + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + comment1 = article.comments.build(:author => profile, :title => 'root', :body => 'root') + comment1.save! + comment2 = article.comments.build(:author => profile, :title => 'root', :body => 'root', :reply_of_id => comment1.id) + comment2.save! + login_as 'testuser' + post :view_page, :profile => profile.identifier, :page => ['test'], :comment => { :title => '', :body => '' }, :confirm => 'true' + assert_no_tag :tag => 'div', :attributes => { :class => /comment_reply/ }, :descendant => {:tag => 'div', :attributes => {:class => 'errorExplanation'} } + assert_tag :tag => 'div', :attributes => { :id => 'page-comment-form' }, :descendant => {:tag => 'div', :attributes => {:class => 'errorExplanation'} } + assert_tag :tag => 'div', :attributes => { :id => 'page-comment-form' }, :descendant => { :tag => 'div', :attributes => { :class => /post_comment_box opened/ } } + end + end diff --git a/test/unit/comment_test.rb b/test/unit/comment_test.rb index 86f6d6a..606fe44 100644 --- a/test/unit/comment_test.rb +++ b/test/unit/comment_test.rb @@ -238,4 +238,83 @@ class CommentTest < Test::Unit::TestCase assert_equal owner, ta.target end + should "get children of a comment" do + c = fast_create(Comment) + c1 = fast_create(Comment, :reply_of_id => c.id) + c2 = fast_create(Comment) + c3 = fast_create(Comment, :reply_of_id => c.id) + assert_equal [c1,c3], c.children + end + + should "get parent of a comment" do + c = fast_create(Comment) + c1 = fast_create(Comment, :reply_of_id => c.id) + c2 = fast_create(Comment, :reply_of_id => c1.id) + c3 = fast_create(Comment, :reply_of_id => c.id) + c4 = fast_create(Comment) + assert_equal c, c1.reply_of + assert_equal c, c3.reply_of + assert_equal c1, c2.reply_of + assert_nil c4.reply_of + end + + should 'destroy replies when comment is removed' do + Comment.delete_all + owner = create_user('testuser').person + article = owner.articles.create!(:name => 'test', :body => '...') + c = article.comments.create!(:article => article, :name => 'foo', :title => 'bar', :body => 'my comment', :email => 'cracker@test.org') + c1 = article.comments.create!(:article => article, :name => 'foo', :title => 'bar', :body => 'my comment', :email => 'cracker@test.org', :reply_of_id => c.id) + c2 = article.comments.create!(:article => article, :name => 'foo', :title => 'bar', :body => 'my comment', :email => 'cracker@test.org') + c3 = article.comments.create!(:article => article, :name => 'foo', :title => 'bar', :body => 'my comment', :email => 'cracker@test.org', :reply_of_id => c.id) + assert_equal 4, Comment.count + c.destroy + assert_equal [c2], Comment.all + end + + should "get children if replies are not loaded" do + c = fast_create(Comment) + c1 = fast_create(Comment, :reply_of_id => c.id) + c2 = fast_create(Comment) + c3 = fast_create(Comment, :reply_of_id => c.id) + assert_nil c.instance_variable_get('@replies') + assert_equal [c1,c3], c.replies + end + + should "get replies if they are loaded" do + c = fast_create(Comment) + c1 = fast_create(Comment, :reply_of_id => c.id) + c2 = fast_create(Comment) + c3 = fast_create(Comment, :reply_of_id => c.id) + c.replies = [c2] + assert_not_nil c.instance_variable_get('@replies') + assert_equal [c2], c.replies + end + + should "set replies" do + c = fast_create(Comment) + c1 = fast_create(Comment, :reply_of_id => c.id) + c2 = fast_create(Comment) + c3 = fast_create(Comment, :reply_of_id => c.id) + c.replies = [] + c.replies << c2 + assert_equal [c2], c.instance_variable_get('@replies') + assert_equal [c2], c.replies + assert_equal [c1,c3], c.reload.children + end + + should "return comments as a thread" do + a = fast_create(Article) + c0 = fast_create(Comment, :article_id => a.id) + c1 = fast_create(Comment, :reply_of_id => c0.id, :article_id => a.id) + c2 = fast_create(Comment, :reply_of_id => c1.id, :article_id => a.id) + c3 = fast_create(Comment, :reply_of_id => c0.id, :article_id => a.id) + c4 = fast_create(Comment, :article_id => a.id) + result = a.comments.as_thread + assert_equal c0.id, result[0].id + assert_equal [c1.id, c3.id], result[0].replies.map(&:id) + assert_equal [c2.id], result[0].replies[0].replies.map(&:id) + assert_equal c4.id, result[1].id + assert result[1].replies.empty? + end + end -- libgit2 0.21.2