diff --git a/app/controllers/public/comment_controller.rb b/app/controllers/public/comment_controller.rb new file mode 100644 index 0000000..f8d27b6 --- /dev/null +++ b/app/controllers/public/comment_controller.rb @@ -0,0 +1,177 @@ +class CommentController < ApplicationController + + needs_profile + + def create + begin + @page = profile.articles.find(params[:id]) + rescue + @page = nil + end + + # page not found, give error + if @page.nil? + respond_to do |format| + format.js do + render :json => { :msg => _('Page not found.')} + end + end + return + end + + unless @page.accept_comments? + respond_to do |format| + format.js do + render :json => { :msg => _('Comment not allowed in this article')} + end + end + return + end + + @comment = Comment.new(params[:comment]) + @comment.author = user if logged_in? + @comment.article = @page + @comment.ip_address = request.remote_ip + @comment.user_agent = request.user_agent + @comment.referrer = request.referrer + plugins_filter_comment(@comment) + + if @comment.rejected? + respond_to do |format| + format.js do + render :json => { :msg => _('Comment was rejected')} + end + end + return + end + + if !@comment.valid? || (not pass_without_comment_captcha? and not verify_recaptcha(:model => @comment, :message => _('Please type the words correctly'))) + respond_to do |format| + format.js do + render :json => { + :render_target => 'form', + :html => render_to_string(:partial => 'comment_form', :object => @comment, :locals => {:comment => @comment, :display_link => true, :show_form => true}) + } + end + end + return + end + + if @comment.article.moderate_comments? && !(@comment.author && @comment.author_id == @comment.article.author_id) + @comment.created_at = Time.now + ApproveComment.create!(:requestor => @comment.author, :target => profile, :comment_attributes => @comment.attributes.to_json) + + respond_to do |format| + format.js do + render :json => { :render_target => nil, :msg => _('Your comment is waiting for approval.') } + end + end + return + end + + @comment.save + + respond_to do |format| + format.js do + comment_to_render = @comment.comment_root + render :json => { + :render_target => comment_to_render.anchor, + :html => render_to_string(:partial => 'comment', :locals => {:comment => comment_to_render, :display_link => true}), + :msg => _('Comment successfully created.') + } + end + end + end + + def destroy + comment = profile.comments_received.find(params[:id]) + + could_remove = (user == comment.author || user == comment.profile || user.has_permission?(:moderate_comments, comment.profile)) + if comment && could_remove && comment.destroy + render :text => {'ok' => true}.to_json, :content_type => 'application/json' + else + session[:notice] = _("The comment was not removed.") + render :text => {'ok' => false}.to_json, :content_type => 'application/json' + end + end + + def mark_as_spam + comment = profile.comments_received.find(params[:id]) + could_mark_as_spam = (user == comment.profile || user.has_permission?(:moderate_comments, comment.profile)) + + if logged_in? && could_mark_as_spam + comment.spam! + render :text => {'ok' => true}.to_json, :content_type => 'application/json' + else + session[:notice] = _("You couldn't mark this comment as spam.") + render :text => {'ok' => false}.to_json, :content_type => 'application/json' + end + end + + def edit + begin + @comment = profile.comments_received.find(params[:id]) + rescue ActiveRecord::RecordNotFound + @comment = nil + end + + if @comment.nil? + render_not_found + return + end + + display_link = params[:reply_of_id].present? && !params[:reply_of_id].empty? + + render :partial => "comment_form", :locals => {:comment => @comment, :display_link => display_link, :edition_mode => true, :show_form => true} + end + + def update + begin + @comment = profile.comments_received.find(params[:id]) + rescue ActiveRecord::RecordNotFound + @comment = nil + end + + if @comment.nil? or user != @comment.author + render_not_found + return + end + + if @comment.update_attributes(params[:comment]) + respond_to do |format| + format.js do + comment_to_render = @comment.comment_root + render :json => { + :ok => true, + :render_target => comment_to_render.anchor, + :html => render_to_string(:partial => 'comment', :locals => {:comment => comment_to_render}) + } + end + end + else + respond_to do |format| + format.js do + render :json => { + :ok => false, + :render_target => 'form', + :html => render_to_string(:partial => 'comment_form', :object => @comment, :locals => {:comment => @comment, :display_link => false, :edition_mode => true}) + } + end + end + end + end + + protected + + def plugins_filter_comment(comment) + @plugins.each do |plugin| + plugin.filter_comment(comment) + end + end + + def pass_without_comment_captcha? + logged_in? && !environment.enabled?('captcha_for_logged_users') + end + helper_method :pass_without_comment_captcha? + +end diff --git a/app/controllers/public/content_viewer_controller.rb b/app/controllers/public/content_viewer_controller.rb index 602eea8..6a017fb 100644 --- a/app/controllers/public/content_viewer_controller.rb +++ b/app/controllers/public/content_viewer_controller.rb @@ -2,8 +2,6 @@ class ContentViewerController < ApplicationController needs_profile - before_filter :comment_author, :only => :edit_comment - helper ProfileHelper helper TagsHelper @@ -70,24 +68,8 @@ class ContentViewerController < ApplicationController @form_div = params[:form] - if params[:comment] && params[:confirm] == 'true' - @comment = Comment.new(params[:comment]) - if request.post? && @page.accept_comments? - add_comment - end - else - @comment = Comment.new - end - - if request.post? - if params[:remove_comment] - remove_comment - return - elsif params[:mark_comment_as_spam] - mark_comment_as_spam - return - end - end + #FIXME see a better way to do this. It's not need to pass this variable anymore + @comment = Comment.new if @page.has_posts? posts = if params[:year] and params[:month] @@ -125,81 +107,8 @@ class ContentViewerController < ApplicationController end end - def edit_comment - path = params[:page].join('/') - @page = profile.articles.find_by_path(path) - @form_div = 'opened' - @comment = @page.comments.find_by_id(params[:id]) - if @comment - if request.post? - begin - @comment.update_attributes(params[:comment]) - session[:notice] = _('Comment succesfully updated') - redirect_to :action => 'view_page', :profile => profile.identifier, :page => @comment.article.explode_path - rescue - session[:notice] = _('Comment could not be updated') - end - end - else - redirect_to @page.view_url - session[:notice] = _('Could not find the comment in the article') - end - end - protected - def add_comment - @comment.author = user if logged_in? - @comment.article = @page - @comment.ip_address = request.remote_ip - @comment.user_agent = request.user_agent - @comment.referrer = request.referrer - plugins_filter_comment(@comment) - return if @comment.rejected? - if (pass_without_comment_captcha? || verify_recaptcha(:model => @comment, :message => _('Please type the words correctly'))) && @comment.save - @page.touch - @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' if params[:comment][:reply_of_id].blank? - end - end - - def plugins_filter_comment(comment) - @plugins.each do |plugin| - plugin.filter_comment(comment) - end - end - - def pass_without_comment_captcha? - logged_in? && !environment.enabled?('captcha_for_logged_users') - end - helper_method :pass_without_comment_captcha? - - def remove_comment - @comment = @page.comments.find(params[:remove_comment]) - if (user == @comment.author || user == @page.profile || user.has_permission?(:moderate_comments, @page.profile)) - @comment.destroy - end - finish_comment_handling - end - - def mark_comment_as_spam - @comment = @page.comments.find(params[:mark_comment_as_spam]) - if logged_in? && (user == @page.profile || user.has_permission?(:moderate_comments, @page.profile)) - @comment.spam! - end - finish_comment_handling - end - - def finish_comment_handling - if request.xhr? - render :text => {'ok' => true}.to_json, :content_type => 'application/json' - else - redirect_to :action => 'view_page', :profile => params[:profile], :page => @page.explode_path, :view => params[:view] - end - end - def per_page 12 end @@ -223,13 +132,9 @@ class ContentViewerController < ApplicationController end end - def comment_author - comment = Comment.find_by_id(params[:id]) - if comment - render_access_denied if comment.author.blank? || comment.author != user - else - render_not_found - end + def pass_without_comment_captcha? + logged_in? && !environment.enabled?('captcha_for_logged_users') end + helper_method :pass_without_comment_captcha? end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index db14797..2c3b35a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1403,12 +1403,14 @@ module ApplicationHelper end def expirable_button(content, action, text, url, options = {}) - options[:class] = "button with-text icon-#{action.to_s}" + #FIXME Leandro see if it's needed the options class parameter + options[:class] = "button with-text icon-#{action.to_s}" + (options[:class].nil? ? '' : " " + options[:class]) expirable_content_reference content, action, text, url, options end def expirable_comment_link(content, action, text, url, options = {}) - options[:class] = "comment-footer comment-footer-link comment-footer-hide" + #FIXME Leandro see if it's needed the options class parameter + options[:class] = "comment-footer comment-footer-link comment-footer-hide" + (options[:class].nil? ? '' : " " + options[:class]) expirable_content_reference content, action, text, url, options end diff --git a/app/helpers/article_helper.rb b/app/helpers/article_helper.rb index d286384..c532393 100644 --- a/app/helpers/article_helper.rb +++ b/app/helpers/article_helper.rb @@ -35,7 +35,13 @@ module ArticleHelper 'div', check_box(:article, :notify_comments) + content_tag('label', _('I want to receive a notification of each comment written by e-mail'), :for => 'article_notify_comments') + - observe_field(:article_accept_comments, :function => "$('article_notify_comments').disabled = ! $('article_accept_comments').checked") + observe_field(:article_accept_comments, :function => "$('article_notify_comments').disabled = ! $('article_accept_comments').checked;$('article_moderate_comments').disabled = ! $('article_accept_comments').checked") + ) + + + content_tag( + 'div', + check_box(:article, :moderate_comments) + + content_tag('label', _('I want to approve comments on this article'), :for => 'article_moderate_comments') ) + (article.can_display_hits? ? diff --git a/app/helpers/comment_helper.rb b/app/helpers/comment_helper.rb new file mode 100644 index 0000000..d3d1454 --- /dev/null +++ b/app/helpers/comment_helper.rb @@ -0,0 +1,25 @@ +module CommentHelper + + def article_title(article, args = {}) + title = article.display_title if article.kind_of?(UploadedFile) && article.image? + title = article.title if title.blank? + title = content_tag('h1', h(title), :class => 'title') + if article.belongs_to_blog? + unless args[:no_link] + title = content_tag('h1', link_to(article.name, article.url), :class => 'title') + end + comments = '' + unless args[:no_comments] || !article.accept_comments + comments = (" - %s") % link_to_comments(article) + end + title << content_tag('span', + content_tag('span', show_date(article.published_at), :class => 'date') + + content_tag('span', [_(", by %s") % link_to(article.author_name, article.author_url)], :class => 'author') + + content_tag('span', comments, :class => 'comments'), + :class => 'created-at' + ) + end + title + end + +end diff --git a/app/helpers/content_viewer_helper.rb b/app/helpers/content_viewer_helper.rb index f6ba368..8de2b05 100644 --- a/app/helpers/content_viewer_helper.rb +++ b/app/helpers/content_viewer_helper.rb @@ -2,14 +2,17 @@ module ContentViewerHelper include BlogHelper include ForumHelper + + def display_number_of_comments(n) + base_str = "#{amount_str}" + end def number_of_comments(article) - n = article.comments.without_spam.count - if n == 0 - _('No comments yet') - else - n_('One comment', '%{comments} comments', n) % { :comments => n } - end + display_number_of_comments(article.comments.without_spam.count) end def article_title(article, args = {}) diff --git a/app/models/approve_comment.rb b/app/models/approve_comment.rb new file mode 100644 index 0000000..361de45 --- /dev/null +++ b/app/models/approve_comment.rb @@ -0,0 +1,101 @@ +class ApproveComment < Task + validates_presence_of :target_id + + settings_items :comment_attributes, :closing_statment + + validates_presence_of :comment_attributes + + def comment + @comment ||= Comment.new(JSON.parse(self.comment_attributes)) unless self.comment_attributes.nil? + @comment + end + + def requestor_name + requestor ? requestor.name : _('Anonymous') + end + + def article + Article.find_by_id comment.source_id unless self.comment.nil? + end + + def article_name + article ? article.name : _("Article removed.") + end + + + def perform + comment.save! + end + + def title + _("New comment to article") + end + + def icon + result = {:type => :defined_image, :src => '/images/icons-app/article-minor.png'} + result.merge({:url => article.url}) if article + return result + end + + def linked_subject + {:text => article_name, :url => article.url} if article + end + + def information + if article + {:message => _('%{requestor} commented on the the article: %{linked_subject}.') % {:requestor => requestor_name} } + else + {:message => _("The article was removed.")} + end + end + + def accept_details + true + end + + def reject_details + true + end + + def default_decision + if article + 'skip' + else + 'reject' + end + end + + def accept_disabled? + article.blank? + end + + def target_notification_description + if article + _('%{requestor} wants to comment the article: %{article}.') % {:requestor => requestor_name, :article => article.name} + else + _('%{requestor} wanted to comment the article but it was removed.') % {:requestor => requestor_name} + end + end + + def target_notification_message + target_notification_description + "\n\n" + + _('You need to login on %{system} in order to approve or reject this comment.') % { :system => target.environment.name } + end + + def task_finished_message + if !closing_statment.blank? + _("Your comment to the article \"%{article}\" was approved. Here is the comment left by the admin who approved your comment:\n\n%{comment} ") % {:article => article_name, :comment => closing_statment} + else + _('Your request for comment the article "%{article}" was approved.') % {:article => article_name} + end + end + + def task_cancelled_message + message = _('Your request for commenting the article "%{article}" was rejected.') % {:article => article_name} + if !reject_explanation.blank? + message += " " + _("Here is the reject explanation left by the administrator who rejected your comment: \n\n%{reject_explanation}") % {:reject_explanation => reject_explanation} + end + message + end + +end diff --git a/app/models/article.rb b/app/models/article.rb index 57d3781..ba9687b 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -2,6 +2,8 @@ require 'hpricot' class Article < ActiveRecord::Base +include ActionController::UrlWriter + # use for internationalizable human type names in search facets # reimplement on subclasses def self.type_name @@ -42,6 +44,7 @@ class Article < ActiveRecord::Base settings_items :display_hits, :type => :boolean, :default => true settings_items :author_name, :type => :string, :default => "" settings_items :allow_members_to_edit, :type => :boolean, :default => false + settings_items :moderate_comments, :type => :boolean, :default => false settings_items :followers, :type => Array, :default => [] belongs_to :reference_article, :class_name => "Article", :foreign_key => 'reference_article_id' @@ -309,6 +312,14 @@ class Article < ActiveRecord::Base @view_url ||= image? ? url.merge(:view => true) : url end + def comment_url_structure(comment, action = :edit) + if comment.new_record? + profile.url.merge(:page => path.split("/"), :controller => :comment, :action => :create) + else + profile.url.merge(:page => path.split("/"), :controller => :comment, :action => action || :edit, :id => comment.id) + end + end + def allow_children? true end @@ -471,6 +482,10 @@ class Article < ActiveRecord::Base allow_post_content?(user) || user && allow_members_to_edit && user.is_member_of?(profile) end + def moderate_comments? + moderate_comments == true + end + def comments_updated solr_save end diff --git a/app/models/comment.rb b/app/models/comment.rb index ed99a97..0671597 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -28,7 +28,14 @@ class Comment < ActiveRecord::Base xss_terminate :only => [ :body, :title, :name ], :on => 'validation' - delegate :environment, :to => :source + #FIXME make this test + def comment_root + if(self.reply_of.present?) + self.reply_of.comment_root + else + self + end + end def action_tracker_target self.article.profile diff --git a/app/models/task.rb b/app/models/task.rb index 8a6acf1..b3656c5 100644 --- a/app/models/task.rb +++ b/app/models/task.rb @@ -74,7 +74,7 @@ class Task < ActiveRecord::Base end def self.all_types - %w[Invitation EnterpriseActivation AddMember Ticket SuggestArticle AddFriend CreateCommunity AbuseComplaint ApproveArticle CreateEnterprise ChangePassword EmailActivation InviteFriend InviteMember] + %w[Invitation EnterpriseActivation AddMember Ticket SuggestArticle AddFriend CreateCommunity AbuseComplaint ApproveComment ApproveArticle CreateEnterprise ChangePassword EmailActivation InviteFriend InviteMember] end # this method finished the task. It calls #perform, which must be overriden diff --git a/app/views/comment/_comment.rhtml b/app/views/comment/_comment.rhtml new file mode 100644 index 0000000..9a2d4c4 --- /dev/null +++ b/app/views/comment/_comment.rhtml @@ -0,0 +1,99 @@ +
" + + amount_str = n == 0 ? _('no comments yet') : (n == 1 ? _('One comment') : _('%s comments') % n) + + base_str + "+ <%= _('If you are a registered user, you can login and be automatically recognized.') %> +
+ + <% end %> + + <% if !edition_mode && !pass_without_comment_captcha? %> + <%= hidden_field_tag(:recaptcha_response_field, nil, :id => nil) %> + <%= hidden_field_tag(:recaptcha_challenge_field, nil, :id => nil) %> + <% end %> + + <%= labelled_form_field(_('Title'), f.text_field(:title)) %> + <%= required labelled_form_field(_('Enter your comment'), f.text_area(:body, :rows => 5)) %> + <%= f.hidden_field(:reply_of_id) %> + + <% button_bar do %> + <%= submit_button('add', _('Post comment'), :onclick => "if(check_captcha(this)) { save_comment(this) } else { check_captcha(this, save_comment)};return false;") %> + <%= button_to_function :cancel, _('Cancel'), "jQuery.colorbox.close();f=jQuery(this).parents('.post_comment_box'); f.removeClass('opened'); f.addClass('closed'); return false" %> + <% end %> +<% end %> + + +- <%= _('If you are a registered user, you can login and be automatically recognized.') %> -
- - <% end %> - - <% unless pass_without_comment_captcha? %> - <%= hidden_field_tag(:recaptcha_response_field, nil, :id => nil) %> - <%= hidden_field_tag(:recaptcha_challenge_field, nil, :id => nil) %> - <% end %> - - <%= 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 => "submit_comment_form(this); return false") %> - <% if cancel_triggers_hide %> - <%= button_to_function :cancel, _('Cancel'), "f=jQuery(this).parents('.post_comment_box'); f.removeClass('opened'); f.addClass('closed'); return false" %> - <% else %> - <%= button('cancel', _('Cancel'), {:action => 'view_page', :profile => profile.identifier, :page => @comment.article.explode_path})%> - <% end %> - <% end %> -<% end %> - -+ <%= _('Title: ') %> + <%= task.comment.title %> +
++ <%= task.comment.body %> +
diff --git a/config/routes.rb b/config/routes.rb index 57f172d..27ff0e4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -76,6 +76,9 @@ ActionController::Routing::Routes.draw do |map| # profile search map.profile_search 'profile/:profile/search', :controller => 'profile_search', :action => 'index', :profile => /#{Noosfero.identifier_format}/ + # comments + map.comment 'profile/:profile/comment/:action/:id', :controller => 'comment', :profile => /#{Noosfero.identifier_format}/ + # public profile information map.profile 'profile/:profile/:action/:id', :controller => 'profile', :action => 'index', :id => /[^\/]*/, :profile => /#{Noosfero.identifier_format}/ @@ -122,7 +125,6 @@ ActionController::Routing::Routes.draw do |map| # cache stuff - hack map.cache 'public/:action/:id', :controller => 'public' - map.connect ':profile/edit_comment/:id/*page', :controller => 'content_viewer', :action => 'edit_comment', :profile => /#{Noosfero.identifier_format}/ # match requests for profiles that don't have a custom domain map.homepage ':profile/*page', :controller => 'content_viewer', :action => 'view_page', :profile => /#{Noosfero.identifier_format}/, :conditions => { :if => lambda { |env| !Domain.hosting_profile_at(env[:host]) } } diff --git a/plugins/require_auth_to_comment/public/jquery.livequery.min.js b/plugins/require_auth_to_comment/public/jquery.livequery.min.js new file mode 100644 index 0000000..5be66e7 --- /dev/null +++ b/plugins/require_auth_to_comment/public/jquery.livequery.min.js @@ -0,0 +1,9 @@ +/* Copyright (c) 2010 Brandon Aaron (http://brandonaaron.net) + * Dual licensed under the MIT (MIT_LICENSE.txt) + * and GPL Version 2 (GPL_LICENSE.txt) licenses. + * + * Version: 1.1.1 + * Requires jQuery 1.3+ + * Docs: http://docs.jquery.com/Plugins/livequery + */ +(function(a){a.extend(a.fn,{livequery:function(e,d,c){var b=this,f;if(a.isFunction(e)){c=d,d=e,e=undefined}a.each(a.livequery.queries,function(g,h){if(b.selector==h.selector&&b.context==h.context&&e==h.type&&(!d||d.$lqguid==h.fn.$lqguid)&&(!c||c.$lqguid==h.fn2.$lqguid)){return(f=h)&&false}});f=f||new a.livequery(this.selector,this.context,e,d,c);f.stopped=false;f.run();return this},expire:function(e,d,c){var b=this;if(a.isFunction(e)){c=d,d=e,e=undefined}a.each(a.livequery.queries,function(f,g){if(b.selector==g.selector&&b.context==g.context&&(!e||e==g.type)&&(!d||d.$lqguid==g.fn.$lqguid)&&(!c||c.$lqguid==g.fn2.$lqguid)&&!this.stopped){a.livequery.stop(g.id)}});return this}});a.livequery=function(b,d,f,e,c){this.selector=b;this.context=d;this.type=f;this.fn=e;this.fn2=c;this.elements=[];this.stopped=false;this.id=a.livequery.queries.push(this)-1;e.$lqguid=e.$lqguid||a.livequery.guid++;if(c){c.$lqguid=c.$lqguid||a.livequery.guid++}return this};a.livequery.prototype={stop:function(){var b=this;if(this.type){this.elements.unbind(this.type,this.fn)}else{if(this.fn2){this.elements.each(function(c,d){b.fn2.apply(d)})}}this.elements=[];this.stopped=true},run:function(){if(this.stopped){return}var d=this;var e=this.elements,c=a(this.selector,this.context),b=c.not(e);this.elements=c;if(this.type){b.bind(this.type,this.fn);if(e.length>0){a.each(e,function(f,g){if(a.inArray(g,c)<0){a.event.remove(g,d.type,d.fn)}})}}else{b.each(function(){d.fn.apply(this)});if(this.fn2&&e.length>0){a.each(e,function(f,g){if(a.inArray(g,c)<0){d.fn2.apply(g)}})}}}};a.extend(a.livequery,{guid:0,queries:[],queue:[],running:false,timeout:null,checkQueue:function(){if(a.livequery.running&&a.livequery.queue.length){var b=a.livequery.queue.length;while(b--){a.livequery.queries[a.livequery.queue.shift()].run()}}},pause:function(){a.livequery.running=false},play:function(){a.livequery.running=true;a.livequery.run()},registerPlugin:function(){a.each(arguments,function(c,d){if(!a.fn[d]){return}var b=a.fn[d];a.fn[d]=function(){var e=b.apply(this,arguments);a.livequery.run();return e}})},run:function(b){if(b!=undefined){if(a.inArray(b,a.livequery.queue)<0){a.livequery.queue.push(b)}}else{a.each(a.livequery.queries,function(c){if(a.inArray(c,a.livequery.queue)<0){a.livequery.queue.push(c)}})}if(a.livequery.timeout){clearTimeout(a.livequery.timeout)}a.livequery.timeout=setTimeout(a.livequery.checkQueue,20)},stop:function(b){if(b!=undefined){a.livequery.queries[b].stop()}else{a.each(a.livequery.queries,function(c){a.livequery.queries[c].stop()})}}});a.livequery.registerPlugin("append","prepend","after","before","wrap","attr","removeAttr","addClass","removeClass","toggleClass","empty","remove","html");a(function(){a.livequery.play()})})(jQuery); \ No newline at end of file diff --git a/public/javascripts/application.js b/public/javascripts/application.js index 9c271ee..b8b4d75 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -60,6 +60,27 @@ function updateUrlField(name_field, id) { } } + + +jQuery.fn.centerInForm = function () { + var $ = jQuery; + var form = $(this).parent('form'); + this.css("position", "absolute"); + this.css("top", (form.height() - this.height())/ 2 + form.scrollTop() + "px"); + this.css("left", (form.width() - this.width()) / 2 + form.scrollLeft() + "px"); + this.css("width", form.width() + "px"); + this.css("height", form.height() + "px"); + return this; +} + +jQuery.fn.center = function () { + var $ = jQuery; + this.css("position", "absolute"); + this.css("top", ($(window).height() - this.height())/ 2 + $(window).scrollTop() + "px"); + this.css("left", ($(window).width() - this.width()) / 2 + $(window).scrollLeft() + "px"); + return this; +} + function show_warning(field, message) { new Effect.Highlight(field, {duration:3}); $(message).show(); @@ -127,8 +148,9 @@ function loading_done(element_id) { $(element_id).removeClassName('small-loading-dark'); } function open_loading(message) { - jQuery('body').append(" "); + jQuery('body').prepend(" "); jQuery('#overlay_loading').show(); + jQuery('#overlay_loading_modal').center(); jQuery('#overlay_loading_modal').fadeIn('slow'); } function close_loading() { @@ -668,11 +690,13 @@ 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(''); + comments_div = jQuery(button).parents('.comments'); + f = comments_div.find('.comment_form').first().clone(); + f.find('.errorExplanation').remove(); + f.append(''); container.append(f); } if (container.hasClass('closed')) { @@ -680,9 +704,95 @@ function add_comment_reply_form(button, comment_id) { container.addClass('opened'); container.find('.comment_form input[type=text]:visible:first').focus(); } + container.addClass('page-comment-form'); return f; } +function update_comment_count(element, new_count) { + var $ = jQuery; + var content = ''; + var parent_element = element.parent(); + + write_out = parent_element.find('.comment-count-write-out'); + + element.html(new_count); + + if(new_count == 0) { + content = NO_COMMENT_YET; + parent_element.addClass("no-comments-yet"); + } else if(new_count == 1) { + parent_element.removeClass("no-comments-yet"); + content = ONE_COMMENT; + } else { + content = new_count + ' ' + COMMENT_PLURAL; + } + + if(write_out){ + write_out.html(content); + } + +} + +function save_comment(button) { + var $ = jQuery; + open_loading(DEFAULT_LOADING_MESSAGE); + var $button = $(button); + var form = $(button).parents("form"); + var post_comment_box = $(button).parents('.post_comment_box'); + var comment_div = $button.parents('.comments'); + $button.addClass('comment-button-loading'); + $.post(form.attr("action"), form.serialize(), function(data) { + + if(data.render_target == null) { + //Comment for approval + form.find("input[type='text']").add('textarea').each(function() { + this.value = ''; + }); + form.find('.errorExplanation').remove(); + + } else if(data.render_target == 'form') { + //Comment with errors + $(button).parents('.page-comment-form').html(data.html); + + } else if($('#' + data.render_target).size() > 0) { + //Comment of reply + $('#'+ data.render_target).replaceWith(data.html); + $('#' + data.render_target).effect("highlight", {}, 3000); + + } else { + //New comment of article + comment_div.find('.article-comments-list').append(data.html); + + form.find("input[type='text']").add('textarea').each(function() { + this.value = ''; + }); + + form.find('.errorExplanation').remove(); + + } + + comment_div.find('.comment-count').add('#article-header .comment-count').each(function() { + var count = parseInt($(this).html()); + update_comment_count($(this), count + 1); + }); + + if(jQuery('#recaptcha_response_field').val()){ + Recaptcha.reload(); + } + + if(data.msg != null) { + display_notice(data.msg); + } + + $.colorbox.close(); + close_loading(); + post_comment_box.removeClass('opened'); + post_comment_box.addClass('closed'); + $button.removeClass('comment-button-loading'); + $button.enable(); + }, 'json'); +} + function remove_comment(button, url, msg) { var $ = jQuery; var $button = $(button); @@ -695,17 +805,27 @@ function remove_comment(button, url, msg) { if (data.ok) { var $comment = $button.closest('.article-comment'); var $replies = $comment.find('.comment-replies .article-comment'); - $comment.slideUp(); + + var $comments_div = $button.closest('.comments'); + var comments_removed = 1; - if ($button.hasClass('remove-children')) { - comments_removed = 1 + $replies.size(); - } else { - $replies.appendTo('.article-comments-list'); - } - $('.comment-count').each(function() { - var count = parseInt($(this).html()); - $(this).html(count - comments_removed); + $comment.slideUp(400, function() { + if ($button.hasClass('remove-children')) { + comments_removed = 1 + $replies.size(); + } else { + $replies.appendTo('.article-comments-list'); + } + + $comments_div.find('.comment-count').add('#article-header .comment-count').each(function() { + var count = parseInt($(this).html()); + update_comment_count($(this), count - comments_removed); + }); + $(this).remove(); }); + + }else{ + $button.removeClass('comment-button-loading'); + return; } }); } diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css index e8c733d..c9eb2de 100644 --- a/public/stylesheets/application.css +++ b/public/stylesheets/application.css @@ -528,11 +528,15 @@ code input { background: transparent url(../images/loading-small-dark.gif) no-repeat 10% center; } #overlay_loading { - z-index: 100; - cursor: progress; + z-index: 10000; + top: 0; + left: 0; + position: fixed; + width: 100%; + height: 100%; } #overlay_loading_modal { - z-index: 101; + z-index: 10001; width: 160px; height: 120px; border: 1px solid #000; @@ -998,6 +1002,9 @@ code input { .comments { } +span.comment-count.hide{ + display: none; +} #content .no-comments-yet { text-align: center; font-size: 80%; @@ -1018,7 +1025,7 @@ code input { margin-bottom: 10px; padding: 4px; } -#article .article-comment h4 { +#article .article-comment .comment-details h4 { font-size: 13px; margin: 0px; display: inline; @@ -1288,6 +1295,10 @@ a.comment-picture { border: 1px solid #888; cursor: pointer; } +.post_comment_box.opened h4 { + border: none; + cursor: default; +} .post_comment_box.opened { border: 1px solid #888; background: #eee; @@ -1338,6 +1349,11 @@ a.comment-picture { .post_comment_box.comment_reply #comment_title { width: 100%; } + +#page-comment-form-template { + display:none; +} + #page-comment-form .post_comment_box { text-align: left; padding-left: 0; diff --git a/test/functional/comment_controller_test.rb b/test/functional/comment_controller_test.rb new file mode 100644 index 0000000..02022de --- /dev/null +++ b/test/functional/comment_controller_test.rb @@ -0,0 +1,512 @@ +require File.dirname(__FILE__) + '/../test_helper' +require 'comment_controller' + +# Re-raise errors caught by the controller. +class CommentController; def rescue_action(e) raise e end; end + +class CommentControllerTest < ActionController::TestCase + + def setup + @controller = CommentController.new + @request = ActionController::TestRequest.new + @response = ActionController::TestResponse.new + + @profile = create_user('testinguser').person + @environment = @profile.environment + end + attr_reader :profile, :environment + + should "not be able to remove other people's comments if not moderator or admin" do + create_user('normaluser') + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + + commenter = create_user('otheruser').person + comment = fast_create(Comment, :source_id => article, :title => 'a comment', :body => 'lalala') + + login_as 'normaluser' # normaluser cannot remove other people's comments + assert_no_difference Comment, :count do + post :destroy, :profile => profile.identifier, :id => comment.id + end + end + + should "not be able to remove other people's comments if not moderator or admin and return json if is an ajax request" do + create_user('normaluser') + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + + commenter = create_user('otheruser').person + comment = fast_create(Comment, :source_id => article, :author_id => commenter, :title => 'a comment', :body => 'lalala') + + login_as 'normaluser' # normaluser cannot remove other people's comments + assert_no_difference Comment, :count do + xhr :post, :destroy, :profile => profile.identifier, :id => comment.id + assert_response :success + end + assert_match /\{\"ok\":false\}/, @response.body + end + + should 'be able to remove comments on their articles' do + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + + commenter = create_user('otheruser').person + comment = fast_create(Comment, :source_id => article, :author_id => commenter, :title => 'a comment', :body => 'lalala') + + login_as 'testuser' # testuser must be able to remove comments in his articles + assert_difference Comment, :count, -1 do + xhr :post, :destroy, :profile => profile.identifier, :id => comment.id + assert_response :success + end + assert_match /\{\"ok\":true\}/, @response.body + end + + should 'be able to remove comments of their images' do + profile = create_user('testuser').person + + image = UploadedFile.create!(:profile => profile, :uploaded_data => fixture_file_upload('/files/rails.png', 'image/png')) + image.save! + + commenter = create_user('otheruser').person + comment = fast_create(Comment, :source_id => image, :author_id => commenter, :title => 'a comment', :body => 'lalala') + + login_as 'testuser' # testuser must be able to remove comments in his articles + assert_difference Comment, :count, -1 do + xhr :post, :destroy, :profile => profile.identifier, :id => comment.id + assert_response :success + end + end + + should 'be able to remove comments if is moderator' do + commenter = create_user('commenter_user').person + community = Community.create!(:name => 'Community test', :identifier => 'community-test') + article = community.articles.create!(:name => 'test', :profile => community) + comment = fast_create(Comment, :source_id => article, :author_id => commenter, :title => 'a comment', :body => 'lalala') + community.add_moderator(profile) + login_as profile.identifier + assert_difference Comment, :count, -1 do + xhr :post, :destroy, :profile => community.identifier, :id => comment.id + assert_response :success + end + assert_match /\{\"ok\":true\}/, @response.body + end + + should 'be able to remove comment' do + profile = create_user('testuser').person + article = profile.articles.build(:name => 'test') + article.save! + comment = fast_create(Comment, :source_id => article, :author_id => profile, :title => 'a comment', :body => 'lalala') + + login_as 'testuser' + assert_difference Comment, :count, -1 do + xhr :post, :destroy, :profile => profile.identifier, :id => comment.id + assert_response :success + end + end + + should 'display not found page if a user should try to make a cross comment' do + page = profile.articles.create!(:name => 'myarticle', :body => 'the body of the text') + + other_person = create_user('otheruser').person + other_page = other_person.articles.create!(:name => 'myarticle', :body => 'the body of the text') + + assert_no_difference Comment, :count do + xhr :post, :create, :profile => profile.identifier, :id => other_page.id, :comment => { :title => 'crap!', :body => 'I think that this article is crap' } + end + assert_match /not found/, @response.body + end + + should 'not be able to post comment if article do not accept it' do + page = profile.articles.create!(:name => 'myarticle', :body => 'the body of the text', :accept_comments => false) + + assert_no_difference Comment, :count do + xhr :post, :create, :profile => profile.identifier, :id => page.id, :comment => { :title => 'crap!', :body => 'I think that this article is crap' } + end + assert_match /Comment not allowed in this article/, @response.body + end + + should "the author's comment be the logged user" do + page = profile.articles.create!(:name => 'myarticle', :body => 'the body of the text') + + login_as profile.identifier + + xhr :post, :create, :profile => profile.identifier, :id => page.id, :comment => { :title => 'crap!', :body => 'I think that this article is crap' } + assert_equal profile, assigns(:comment).author + end + + should "the articles's comment be the article passed as parameter" do + page = profile.articles.create!(:name => 'myarticle', :body => 'the body of the text') + + login_as profile.identifier + + xhr :post, :create, :profile => profile.identifier, :id => page.id, :comment => { :title => 'crap!', :body => 'I think that this article is crap' } + assert_equal page, assigns(:comment).article + end + + should 'show comment form opened on error' do + login_as profile.identifier + page = profile.articles.create!(:name => 'myarticle', :body => 'the body of the text') + xhr :post, :create, :profile => @profile.identifier, :id => page.id, :comment => { :title => '', :body => '' }, :confirm => 'true' + response = JSON.parse @response.body + assert_match /
<%= comment.title.blank? && ' ' || comment.title %>
+