Commit 29490ac8337e1b6cc16ea20d6be529075b4ced9a

Authored by Victor Costa
2 parents 3f675a65 711903e3

Merge branch 'AI3220_proposals' into rails3_stable

Showing 35 changed files with 905 additions and 6 deletions   Show diff stats
app/controllers/admin/features_controller.rb
... ... @@ -93,7 +93,7 @@ class FeaturesController < AdminController
93 93  
94 94 def search_members
95 95 arg = params[:q].downcase
96   - result = environment.people.find(:all, :conditions => ['LOWER(name) LIKE ?', "%#{arg}%"])
  96 + result = environment.people.find(:all, :conditions => ['LOWER(name) LIKE ? OR identifier LIKE ?', "%#{arg}%", "%#{arg}%"])
97 97 render :text => prepare_to_token_input(result).to_json
98 98 end
99 99  
... ...
app/helpers/categories_helper.rb
... ... @@ -12,7 +12,7 @@ module CategoriesHelper
12 12 end
13 13  
14 14 def category_color_style(category)
15   - return '' if category.display_color.blank?
  15 + return '' if category.nil? or category.display_color.blank?
16 16 'background-color: #'+category.display_color+';'
17 17 end
18 18  
... ...
app/models/article.rb
... ... @@ -252,7 +252,7 @@ class Article < ActiveRecord::Base
252 252 }
253 253  
254 254 scope :public,
255   - :conditions => [ "advertise = ? AND published = ? AND profiles.visible = ? AND profiles.public_profile = ?", true, true, true, true ], :joins => [:profile]
  255 + :conditions => [ "articles.advertise = ? AND articles.published = ? AND profiles.visible = ? AND profiles.public_profile = ?", true, true, true, true ], :joins => [:profile]
256 256  
257 257 scope :more_recent,
258 258 :conditions => [ "advertise = ? AND published = ? AND profiles.visible = ? AND profiles.public_profile = ? AND
... ...
features/step_definitions/web_steps.rb
... ... @@ -11,8 +11,14 @@ require File.expand_path(File.join(File.dirname(__FILE__), "..", "support", "pat
11 11  
12 12 module WithinHelpers
13 13 def with_scope(locator)
14   - locator = locator ? first(locator) : locator
15   - locator ? within(locator) { yield } : yield
  14 + if locator
  15 + locator = first(locator) || locator
  16 + within(locator) do
  17 + yield
  18 + end
  19 + else
  20 + yield
  21 + end
16 22 end
17 23 end
18 24 World(WithinHelpers)
... ...
plugins/proposals_discussion/controllers/myprofile/proposals_discussion_plugin_myprofile_controller.rb 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +class ProposalsDiscussionPluginMyprofileController < MyProfileController
  2 +
  3 + before_filter :check_edit_permission_to_proposal, :only => :publish_proposal
  4 +
  5 + def select_topic
  6 + @discussion = profile.articles.find(params[:parent_id])
  7 + render :file => 'select_topic'
  8 + end
  9 +
  10 + def new_proposal
  11 + redirect_to :controller => 'cms', :action => 'new', :type => "ProposalsDiscussionPlugin::Proposal", :parent_id => params[:discussion][:topic]
  12 + end
  13 +
  14 + def publish_proposal
  15 + if @proposal.update_attribute(:published, true)
  16 + session[:notice] = _('Proposal published!')
  17 + else
  18 + session[:notice] = _('Failed to publish your proposal.')
  19 + end
  20 + redirect_to @proposal.topic.view_url
  21 + end
  22 +
  23 + protected
  24 +
  25 + def check_edit_permission_to_proposal
  26 + @proposal = profile.articles.find(params[:proposal_id])
  27 + render_access_denied unless @proposal.allow_edit?(user)
  28 + end
  29 +
  30 +end
... ...
plugins/proposals_discussion/controllers/public/proposals_discussion_plugin_public_controller.rb 0 → 100644
... ... @@ -0,0 +1,43 @@
  1 +class ProposalsDiscussionPluginPublicController < ApplicationController
  2 +
  3 + needs_profile
  4 +
  5 + def load_proposals
  6 + holder = profile.articles.find(params[:holder_id])
  7 + page = (params[:page] || 1).to_i
  8 + set_rand_cookie if page == 1
  9 + order = params[:order]
  10 +
  11 + @proposals = order_proposals(holder.proposals.public, order)
  12 + @proposals = @proposals.page(page).per_page(5)
  13 +
  14 + unless @proposals.empty?
  15 + render :partial => 'content_viewer/proposals_list_content', :locals => {:proposals => @proposals, :holder => holder, :page => page+1, :order => order}
  16 + else
  17 + render :text => ''
  18 + end
  19 + end
  20 +
  21 + private
  22 +
  23 + def order_proposals(proposals, order)
  24 + case order
  25 + when 'alphabetical'
  26 + proposals.reorder('name')
  27 + else
  28 + set_seed
  29 + proposals.reorder('random()')
  30 + end
  31 + end
  32 +
  33 + def set_seed
  34 + #XXX postgresql specific
  35 + seed_val = profile.connection.quote(cookies[:_noosfero_proposals_discussion_rand_seed])
  36 + profile.connection.execute("select setseed(#{seed_val})")
  37 + end
  38 +
  39 + def set_rand_cookie
  40 + cookies[:_noosfero_proposals_discussion_rand_seed] = {value: rand, expires: Time.now + 600}
  41 + end
  42 +
  43 +end
... ...
plugins/proposals_discussion/lib/proposals_discussion_plugin.rb 0 → 100644
... ... @@ -0,0 +1,43 @@
  1 +class ProposalsDiscussionPlugin < Noosfero::Plugin
  2 +
  3 + def self.plugin_name
  4 + 'Discussion of Proposals'
  5 + end
  6 +
  7 + def self.plugin_description
  8 + _("Provide a structured way to promove a discussion over ideas proposed by users.")
  9 + end
  10 +
  11 + def stylesheet?
  12 + true
  13 + end
  14 +
  15 + def content_types
  16 + if context.respond_to?(:params) && context.params
  17 + types = []
  18 + parent_id = context.params[:parent_id]
  19 + parent = parent_id ? context.profile.articles.find(parent_id) : nil
  20 + types << ProposalsDiscussionPlugin::Discussion unless parent
  21 + types << ProposalsDiscussionPlugin::Topic if parent.kind_of?(ProposalsDiscussionPlugin::Discussion)
  22 + types << ProposalsDiscussionPlugin::Proposal if parent.kind_of?(ProposalsDiscussionPlugin::Topic)
  23 + types
  24 + else
  25 + [ProposalsDiscussionPlugin::Discussion,
  26 + ProposalsDiscussionPlugin::Topic,
  27 + ProposalsDiscussionPlugin::Proposal]
  28 + end
  29 + end
  30 +
  31 + def content_remove_new(page)
  32 + page.kind_of?(ProposalsDiscussionPlugin::Discussion) ||
  33 + page.kind_of?(ProposalsDiscussionPlugin::Topic) ||
  34 + page.kind_of?(ProposalsDiscussionPlugin::Proposal)
  35 + end
  36 +
  37 + def content_remove_upload(page)
  38 + page.kind_of?(ProposalsDiscussionPlugin::Discussion) ||
  39 + page.kind_of?(ProposalsDiscussionPlugin::Topic) ||
  40 + page.kind_of?(ProposalsDiscussionPlugin::Proposal)
  41 + end
  42 +
  43 +end
... ...
plugins/proposals_discussion/lib/proposals_discussion_plugin/discussion.rb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +class ProposalsDiscussionPlugin::Discussion < Folder
  2 +
  3 + has_many :topics, :class_name => 'ProposalsDiscussionPlugin::Topic', :foreign_key => 'parent_id'
  4 + has_many :proposals, :class_name => 'ProposalsDiscussionPlugin::Proposal', :through => :children, :source => :children
  5 +
  6 + def self.short_description
  7 + _("Discussion")
  8 + end
  9 +
  10 + def self.description
  11 + _('Container for topics.')
  12 + end
  13 +
  14 + def to_html(options = {})
  15 + proc do
  16 + render :file => 'content_viewer/discussion'
  17 + end
  18 + end
  19 +
  20 +end
... ...
plugins/proposals_discussion/lib/proposals_discussion_plugin/proposal.rb 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +class ProposalsDiscussionPlugin::Proposal < TinyMceArticle
  2 +
  3 + scope :private, lambda {|user| {:conditions => {:last_changed_by_id => user.id, :published => false}}}
  4 +
  5 + alias :topic :parent
  6 +
  7 + def self.short_description
  8 + _("Proposal")
  9 + end
  10 +
  11 + def self.description
  12 + _('Proposal')
  13 + end
  14 +
  15 + validates_presence_of :abstract
  16 +
  17 +
  18 + def to_html(options = {})
  19 + proc do
  20 + render :file => 'content_viewer/proposal'
  21 + end
  22 + end
  23 +
  24 + def allow_edit?(user)
  25 + super || created_by == user
  26 + end
  27 +
  28 +end
... ...
plugins/proposals_discussion/lib/proposals_discussion_plugin/proposal_helper.rb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +module ProposalsDiscussionPlugin::ProposalHelper
  2 +
  3 + def visibility_options(article, tokenized_children)
  4 + article.published = false if article.new_record?
  5 + super
  6 + end
  7 +
  8 +end
... ...
plugins/proposals_discussion/lib/proposals_discussion_plugin/proposals_list_helper.rb 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +module ProposalsDiscussionPlugin::ProposalsListHelper
  2 +
  3 + def more_proposals(text, holder, order, page=1)
  4 + link_to '', url_for({:controller => 'proposals_discussion_plugin_public', :action => 'load_proposals', :holder_id => holder.id, :profile => profile.identifier, :order => order, :page => page })
  5 + end
  6 +
  7 +end
... ...
plugins/proposals_discussion/lib/proposals_discussion_plugin/topic.rb 0 → 100644
... ... @@ -0,0 +1,35 @@
  1 +class ProposalsDiscussionPlugin::Topic < Folder
  2 +
  3 + alias :discussion :parent
  4 +
  5 + has_many :proposals, :class_name => 'ProposalsDiscussionPlugin::Proposal', :foreign_key => 'parent_id'
  6 + has_many :proposals_comments, :class_name => 'Comment', :through => :children, :source => :comments
  7 + has_many :proposals_authors, :class_name => 'Person', :through => :children, :source => :created_by
  8 +
  9 + settings_items :color, :type => :string
  10 +
  11 + attr_accessible :color
  12 +
  13 + def self.short_description
  14 + _("Discussion topic")
  15 + end
  16 +
  17 + def self.description
  18 + _('Container for proposals.')
  19 + end
  20 +
  21 + def most_active_participants
  22 + proposals_authors.group('profiles.id').order('count(articles.id) DESC').includes(:environment, :preferred_domain, :image)
  23 + end
  24 +
  25 + def to_html(options = {})
  26 + proc do
  27 + render :file => 'content_viewer/topic'
  28 + end
  29 + end
  30 +
  31 + def allow_create?(user)
  32 + true
  33 + end
  34 +
  35 +end
... ...
plugins/proposals_discussion/public/images/comments.gif 0 → 100644

1.66 KB

plugins/proposals_discussion/public/jquery.jscroll.min.js 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +/*!
  2 + * jScroll - jQuery Plugin for Infinite Scrolling / Auto-Paging - v2.2.4
  3 + * http://jscroll.com/
  4 + *
  5 + * Copyright 2011-2013, Philip Klauzinski
  6 + * http://klauzinski.com/
  7 + * Dual licensed under the MIT and GPL Version 2 licenses.
  8 + * http://jscroll.com/#license
  9 + * http://www.opensource.org/licenses/mit-license.php
  10 + * http://www.gnu.org/licenses/gpl-2.0.html
  11 + *
  12 + * @author Philip Klauzinski
  13 + * @requires jQuery v1.4.3+
  14 + */
  15 +(function(b){b.jscroll={defaults:{debug:false,autoTrigger:true,autoTriggerUntil:false,loadingHtml:"<small>Loading...</small>",padding:0,nextSelector:"a:last",contentSelector:"",pagingSelector:"",callback:false}};var a=function(e,g){var o=e.data("jscroll"),n=(typeof g==="function")?{callback:g}:g,p=b.extend({},b.jscroll.defaults,n,o||{}),c=(e.css("overflow-y")==="visible"),l=e.find(p.nextSelector).first(),v=b(window),h=b("body"),q=c?v:e,m=b.trim(l.attr("href")+" "+p.contentSelector);e.data("jscroll",b.extend({},o,{initialized:true,waiting:false,nextHref:m}));r();k();t();function k(){var x=b(p.loadingHtml).filter("img").attr("src");if(x){var w=new Image();w.src=x}}function r(){if(!e.find(".jscroll-inner").length){e.contents().wrapAll('<div class="jscroll-inner" />')}}function d(w){if(p.pagingSelector){var x=w.closest(p.pagingSelector).hide()}else{var x=w.parent().not(".jscroll-inner,.jscroll-added").addClass("jscroll-next-parent").hide();if(!x.length){w.wrap('<div class="jscroll-next-parent" />').parent().hide()}}}function j(){return q.unbind(".jscroll").removeData("jscroll").find(".jscroll-inner").children().unwrap().filter(".jscroll-added").children().unwrap()}function i(){r();var D=e.find("div.jscroll-inner").first(),B=e.data("jscroll"),C=parseInt(e.css("borderTopWidth")),y=isNaN(C)?0:C,x=parseInt(e.css("paddingTop"))+y,A=c?q.scrollTop():e.offset().top,z=D.length?D.offset().top:0,w=Math.ceil(A-z+q.height()+x);if(!B.waiting&&w+p.padding>=D.outerHeight()){f("info","jScroll:",D.outerHeight()-w,"from bottom. Loading next request...");return u()}}function s(w){w=w||e.data("jscroll");if(!w||!w.nextHref){f("warn","jScroll: nextSelector not found - destroying");j();return false}else{t();return true}}function t(){var w=e.find(p.nextSelector).first();if(p.autoTrigger&&(p.autoTriggerUntil===false||p.autoTriggerUntil>0)){d(w);if(h.height()<=v.height()){i()}q.unbind(".jscroll").bind("scroll.jscroll",function(){return i()});if(p.autoTriggerUntil>0){p.autoTriggerUntil--}}else{q.unbind(".jscroll");w.bind("click.jscroll",function(){d(w);u();return false})}}function u(){var x=e.find("div.jscroll-inner").first(),w=e.data("jscroll");w.waiting=true;x.append('<div class="jscroll-added" />').children(".jscroll-added").last().html('<div class="jscroll-loading">'+p.loadingHtml+"</div>");return e.animate({scrollTop:x.outerHeight()},0,function(){x.find("div.jscroll-added").last().load(w.nextHref,function(A,z,B){if(z==="error"){return j()}var y=b(this).find(p.nextSelector).first();w.waiting=false;w.nextHref=y.attr("href")?b.trim(y.attr("href")+" "+p.contentSelector):false;b(".jscroll-next-parent",e).remove();s();if(p.callback){p.callback.call(this)}f("dir",w)})})}function f(w){if(p.debug&&typeof console==="object"&&(typeof w==="object"||typeof console[w]==="function")){if(typeof w==="object"){var y=[];for(var x in w){if(typeof console[x]==="function"){y=(w[x].length)?w[x]:[w[x]];console[x].apply(console,y)}else{console.log.apply(console,y)}}}else{console[w].apply(console,Array.prototype.slice.call(arguments,1))}}}b.extend(e.jscroll,{destroy:j});return e};b.fn.jscroll=function(c){return this.each(function(){var f=b(this),e=f.data("jscroll");if(e&&e.initialized){return}var d=new a(f,c)})}})(jQuery);
0 16 \ No newline at end of file
... ...
plugins/proposals_discussion/public/proposals_list.js 0 → 100644
... ... @@ -0,0 +1,31 @@
  1 +function initTwitterButton() {
  2 + !function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0],p=/^http:/.test(d.location)?'http':'https';if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src=p+'://platform.twitter.com/widgets.js';fjs.parentNode.insertBefore(js,fjs);}}(document, 'script', 'twitter-wjs');
  3 +}
  4 +
  5 +jQuery(document).ready(function($) {
  6 + initTwitterButton();
  7 + $('.proposals').on('mouseenter', '.proposal', function() {
  8 + twttr.widgets.load();
  9 + $('.proposal .social').hide();
  10 + $(this).find('.social').show("fast");
  11 + }).on('mouseleave', '.proposal', function() {
  12 + $(this).find('.social').hide("fast");
  13 + });
  14 +
  15 + function proposalsScroll() {
  16 + $('.proposals').data('jscroll', null);
  17 + $('.proposals').jscroll({
  18 + loadingHtml: '<img src="/images/loading.gif" alt="Loading" />Loading...',
  19 + nextSelector: 'div.more a'
  20 + });
  21 + $('.proposals').trigger('scroll.jscroll');
  22 + }
  23 + proposalsScroll();
  24 +
  25 + $('.proposals_list .filters a.order').on('ajax:success', function(event, data, status, xhr) {
  26 + $('.proposals_list .filters a.order').removeClass('selected');
  27 + $(this).addClass('selected');
  28 + $(this).parents('div.proposals_list').find('.proposals').html(data);
  29 + proposalsScroll();
  30 + });
  31 +});
... ...
plugins/proposals_discussion/public/style.css 0 → 100644
... ... @@ -0,0 +1,124 @@
  1 +.private-proposals .proposal {
  2 + background: url(/images/hachure.png);
  3 + opacity: 0.5;
  4 + filter: alpha(opacity=25);
  5 + zoom: 1;
  6 +}
  7 +
  8 +.proposal {
  9 + background: rgb(236, 236, 236);
  10 + width: 100%;
  11 + min-width: 272px;
  12 + vertical-align: top;
  13 + margin: 12px 13px 12px 0;
  14 + box-shadow: 5px 5px 5px -2px #ddd;
  15 + height: 100px;
  16 +}
  17 +
  18 +.article-body-proposals-discussion-plugin_discussion .actions,
  19 +.article-body-proposals-discussion-plugin_topic .actions {
  20 + margin: 10px 0 25px 0;
  21 +}
  22 +
  23 +.proposal .content, .proposal .score, .proposal .topic {
  24 + display: table-cell;
  25 + border-right: 1px solid;
  26 + border-color: rgb(201, 201, 201);
  27 + padding: 5px;
  28 + vertical-align: middle;
  29 + height: 100%;
  30 +}
  31 +.proposal .topic {
  32 + border-right: 0;
  33 + text-align: center;
  34 + width: 24%;
  35 +}
  36 +.proposal .score {
  37 + width: 8%;
  38 + text-align: center;
  39 +}
  40 +
  41 +.proposal .content:hover, .proposal .topic:hover {
  42 + background: rgba(0, 0, 0, 0.1);
  43 +}
  44 +
  45 +.proposal .title {
  46 + font-weight: bold;
  47 + font-size: 15px;
  48 +}
  49 +
  50 +#article .proposal a:visited, #article .proposal a {
  51 + color: rgb(70, 70, 70);
  52 + text-decoration: none;
  53 + width: 100%;
  54 + display: inline-block;
  55 +}
  56 +
  57 +#article .proposal .title a {
  58 + padding: 4px 0px;
  59 +}
  60 +
  61 +.proposal .content {
  62 + width: 68%;
  63 + color: rgb(83, 83, 83);
  64 + vertical-align: top;
  65 + height: 90px;
  66 +}
  67 +
  68 +.proposal .abstract {
  69 + padding-top: 4px;
  70 +}
  71 +
  72 +form .proposals-discussion-plugin textarea {
  73 + width: 98%;
  74 +}
  75 +
  76 +form .proposals-discussion-plugin .abstract textarea {
  77 + height: 60px;
  78 +}
  79 +
  80 +form .proposals-discussion-plugin .body textarea {
  81 + height: 400px;
  82 +}
  83 +
  84 +.topic-color {
  85 + width: 9px;
  86 + float: left;
  87 + height: 100%;
  88 +}
  89 +
  90 +.topics .topic {
  91 + background-color: rgb(233, 233, 233);
  92 + margin: 5px 0;
  93 + height: 25px;
  94 +}
  95 +
  96 +#article .proposal .topic a {
  97 + font-weight: bold;
  98 + color: #888a85;
  99 + width: 100%;
  100 + height: 100%;
  101 +}
  102 +
  103 +#article .topics .topic a {
  104 + text-decoration: none;
  105 + display: inline-block;
  106 + width: 95%;
  107 + height: 100%;
  108 + padding-left: 5px;
  109 + font-weight: bold;
  110 + font-size: 14px;
  111 +}
  112 +
  113 +.proposals_list .filters {
  114 + float: right;
  115 +}
  116 +#article .proposals_list .filters a {
  117 + text-decoration: none;
  118 + border-left: 1px solid rgb(185, 185, 185);
  119 + padding: 0 5px;
  120 + color: #555753;
  121 +}
  122 +.proposals_list .filters a.selected {
  123 + font-weight: bold;
  124 +}
... ...
plugins/proposals_discussion/test/functional/proposals_discussion_plugin_myprofile_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,44 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class ProposalsDiscussionPluginMyprofileControllerTest < ActionController::TestCase
  4 +
  5 + def setup
  6 + @profile = fast_create(Community)
  7 + @discussion = fast_create(ProposalsDiscussionPlugin::Discussion, :profile_id => @profile.id)
  8 + @topic = fast_create(ProposalsDiscussionPlugin::Topic, :parent_id => @discussion.id, :profile_id => @profile.id)
  9 + @person = create_user_with_permission('testinguser', 'post_content')
  10 + login_as :testinguser
  11 + end
  12 +
  13 + attr_reader :profile, :discussion, :topic, :person
  14 +
  15 + should 'list topics for selection' do
  16 + 3.times {fast_create(ProposalsDiscussionPlugin::Topic, :parent_id => discussion.id, :profile_id => profile.id)}
  17 + get :select_topic, :profile => profile.identifier, :parent_id => discussion.id
  18 + assert_equal discussion, assigns(:discussion)
  19 + assert_select 'div#topics' do
  20 + assert_select 'div.content', discussion.topics.count
  21 + assert_select "input[name='discussion[topic]']", discussion.topics.count
  22 + end
  23 + assert_tag :form, :attributes => {:action => "/myprofile/#{profile.identifier}/plugin/proposals_discussion/myprofile/new_proposal"}
  24 + end
  25 +
  26 + should 'new_proposal redirect to cms controller' do
  27 + get :new_proposal, :profile => profile.identifier, :discussion => {:topic => topic.id}
  28 + assert_redirected_to :controller => 'cms', :action => 'new', :type => "ProposalsDiscussionPlugin::Proposal", :parent_id => topic.id
  29 + end
  30 +
  31 + should 'publish a proposal' do
  32 + proposal = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id, :profile_id => profile.id, :published => false, :created_by_id => person.id)
  33 + get :publish_proposal, :proposal_id => proposal.id, :profile => profile.identifier
  34 + assert proposal.reload.published
  35 + end
  36 +
  37 + should 'do not publish if the logged user do not have edition permission' do
  38 + proposal = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id, :profile_id => profile.id, :published => false)
  39 + get :publish_proposal, :proposal_id => proposal.id, :profile => profile.identifier
  40 + assert_response 403
  41 + assert !proposal.reload.published
  42 + end
  43 +
  44 +end
... ...
plugins/proposals_discussion/test/functional/proposals_discussion_plugin_public_controller_test.rb 0 → 100644
... ... @@ -0,0 +1,38 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class ProposalsDiscussionPluginPublicControllerTest < ActionController::TestCase
  4 +
  5 + def setup
  6 + @profile = fast_create(Community)
  7 + @discussion = fast_create(ProposalsDiscussionPlugin::Discussion, :profile_id => @profile.id)
  8 + @topic = fast_create(ProposalsDiscussionPlugin::Topic, :parent_id => @discussion.id, :profile_id => @profile.id)
  9 + end
  10 +
  11 + attr_reader :profile, :discussion, :topic
  12 +
  13 + should 'load proposals' do
  14 + proposals = 3.times.map { fast_create(ProposalsDiscussionPlugin::Proposal, :name => 'proposal title', :abstract => 'proposal abstract', :profile_id => profile.id, :parent_id => topic.id)}
  15 + get :load_proposals, :profile => profile.identifier, :holder_id => discussion.id
  16 + assert_equivalent proposals, assigns(:proposals)
  17 + end
  18 +
  19 + should 'add link to next page' do
  20 + proposal = fast_create(ProposalsDiscussionPlugin::Proposal, :name => 'proposal title', :abstract => 'proposal abstract', :profile_id => profile.id, :parent_id => topic.id)
  21 + get :load_proposals, :profile => profile.identifier, :holder_id => discussion.id
  22 + assert_match /href=.*page=2/, response.body
  23 + end
  24 +
  25 + should 'render blank text if it is the last page' do
  26 + get :load_proposals, :profile => profile.identifier, :holder_id => discussion.id
  27 + assert_equal '', response.body
  28 + end
  29 +
  30 + should 'load proposals with alphabetical order' do
  31 + proposal1 = fast_create(ProposalsDiscussionPlugin::Proposal, :name => 'z proposal', :abstract => 'proposal abstract', :profile_id => profile.id, :parent_id => topic.id)
  32 + proposal2 = fast_create(ProposalsDiscussionPlugin::Proposal, :name => 'abc proposal', :abstract => 'proposal abstract', :profile_id => profile.id, :parent_id => topic.id)
  33 + proposal3 = fast_create(ProposalsDiscussionPlugin::Proposal, :name => 'abd proposal', :abstract => 'proposal abstract', :profile_id => profile.id, :parent_id => topic.id)
  34 + get :load_proposals, :profile => profile.identifier, :holder_id => discussion.id, :order => 'alphabetical'
  35 + assert_equal [proposal2, proposal3, proposal1], assigns(:proposals)
  36 + end
  37 +
  38 +end
... ...
plugins/proposals_discussion/test/test_helper.rb 0 → 100644
... ... @@ -0,0 +1 @@
  1 +require File.dirname(__FILE__) + '/../../../test/test_helper'
... ...
plugins/proposals_discussion/test/unit/discussion_test.rb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class DiscussionTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + @profile = fast_create(Community)
  7 + @discussion = ProposalsDiscussionPlugin::Discussion.new(:name => 'test', :profile => @profile)
  8 + end
  9 +
  10 + attr_reader :profile, :discussion
  11 +
  12 + should 'return list of topics' do
  13 + discussion.save!
  14 + topic1 = fast_create(ProposalsDiscussionPlugin::Topic, :parent_id => discussion.id)
  15 + topic2 = fast_create(ProposalsDiscussionPlugin::Topic, :parent_id => discussion.id)
  16 + assert_equivalent [topic1, topic2], discussion.topics
  17 + end
  18 +
  19 + should 'return list of proposals' do
  20 + discussion.save!
  21 + topic = fast_create(ProposalsDiscussionPlugin::Topic, :parent_id => discussion.id)
  22 + proposal1 = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id)
  23 + proposal2 = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id)
  24 + assert_equivalent [proposal1, proposal2], discussion.proposals
  25 + end
  26 +
  27 +end
... ...
plugins/proposals_discussion/test/unit/proposal_test.rb 0 → 100644
... ... @@ -0,0 +1,32 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class ProposalTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + @profile = fast_create(Community)
  7 + @person = fast_create(Person)
  8 + @proposal = ProposalsDiscussionPlugin::Proposal.new(:name => 'test', :profile => @profile)
  9 + @proposal.created_by = @person
  10 + end
  11 +
  12 + attr_reader :profile, :proposal, :person
  13 +
  14 + should 'save a proposal' do
  15 + proposal.abstract = 'abstract'
  16 + assert proposal.save
  17 + end
  18 +
  19 + should 'do not save a proposal without abstract' do
  20 + proposal.save
  21 + assert proposal.errors['abstract'].present?
  22 + end
  23 +
  24 + should 'allow edition if user is the author' do
  25 + assert proposal.allow_edit?(person)
  26 + end
  27 +
  28 + should 'do not allow edition if user is not the author' do
  29 + assert !proposal.allow_edit?(fast_create(Person))
  30 + end
  31 +
  32 +end
... ...
plugins/proposals_discussion/test/unit/proposals_discussion_plugin_test.rb 0 → 100644
... ... @@ -0,0 +1,76 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class ProposalsDiscussionPluginTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + @plugin = ProposalsDiscussionPlugin.new
  7 + @profile = fast_create(Community)
  8 + @params = {}
  9 + @plugin.stubs(:context).returns(self)
  10 + end
  11 +
  12 + attr_reader :plugin, :profile, :params
  13 +
  14 + should 'has stylesheet' do
  15 + assert @plugin.stylesheet?
  16 + end
  17 +
  18 + should 'return Discussion as a content type' do
  19 + @params[:parent_id] = nil
  20 + assert_includes plugin.content_types, ProposalsDiscussionPlugin::Discussion
  21 + end
  22 +
  23 + should 'do not return Discussion as a content type if it has a parent' do
  24 + parent = fast_create(Folder, :profile_id => @profile.id)
  25 + @params[:parent_id] = parent.id
  26 + assert_not_includes plugin.content_types, ProposalsDiscussionPlugin::Discussion
  27 + end
  28 +
  29 + should 'return Topic as a content type if parent is a Discussion' do
  30 + parent = fast_create(ProposalsDiscussionPlugin::Discussion, :profile_id => @profile.id)
  31 + @params[:parent_id] = parent.id
  32 + assert_includes plugin.content_types, ProposalsDiscussionPlugin::Topic
  33 + end
  34 +
  35 + should 'return Proposal as a content type if parent is a Topic' do
  36 + parent = fast_create(ProposalsDiscussionPlugin::Topic, :profile_id => @profile.id)
  37 + @params[:parent_id] = parent.id
  38 + assert_includes plugin.content_types, ProposalsDiscussionPlugin::Proposal
  39 + end
  40 +
  41 + should 'do not return Proposal as a content type if parent is nil' do
  42 + @params[:parent_id] = nil
  43 + assert_not_includes plugin.content_types, ProposalsDiscussionPlugin::Proposal
  44 + end
  45 +
  46 + should 'remove new button from content page for a discussion' do
  47 + page = fast_create(ProposalsDiscussionPlugin::Discussion, :profile_id => @profile.id)
  48 + assert plugin.content_remove_new(page)
  49 + end
  50 +
  51 + should 'remove upload button from content page for a discussion' do
  52 + page = fast_create(ProposalsDiscussionPlugin::Discussion, :profile_id => @profile.id)
  53 + assert plugin.content_remove_upload(page)
  54 + end
  55 +
  56 + should 'remove new button from content page for a proposal' do
  57 + page = fast_create(ProposalsDiscussionPlugin::Proposal, :profile_id => @profile.id)
  58 + assert plugin.content_remove_new(page)
  59 + end
  60 +
  61 + should 'remove upload button from content page for a proposal' do
  62 + page = fast_create(ProposalsDiscussionPlugin::Proposal, :profile_id => @profile.id)
  63 + assert plugin.content_remove_upload(page)
  64 + end
  65 +
  66 + should 'do not remove new button from content page for others article types' do
  67 + page = fast_create(Article, :profile_id => @profile.id)
  68 + assert !plugin.content_remove_new(page)
  69 + end
  70 +
  71 + should 'do not remove upload button from content page for others article types' do
  72 + page = fast_create(Article, :profile_id => @profile.id)
  73 + assert !plugin.content_remove_upload(page)
  74 + end
  75 +
  76 +end
... ...
plugins/proposals_discussion/test/unit/topic_test.rb 0 → 100644
... ... @@ -0,0 +1,50 @@
  1 +require File.dirname(__FILE__) + '/../test_helper'
  2 +
  3 +class TopicTest < ActiveSupport::TestCase
  4 +
  5 + def setup
  6 + @profile = fast_create(Community)
  7 + @topic = ProposalsDiscussionPlugin::Topic.new(:name => 'test', :profile => @profile)
  8 + end
  9 +
  10 + attr_reader :profile, :topic
  11 +
  12 + should 'return list of proposals' do
  13 + topic.save!
  14 + proposal1 = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id)
  15 + proposal2 = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id)
  16 + assert_equivalent [proposal1, proposal2], topic.proposals
  17 + end
  18 +
  19 + should 'allow any user to create proposals in a topic' do
  20 + assert topic.allow_create?(Person.new)
  21 + end
  22 +
  23 + should 'return list of comments' do
  24 + topic.save!
  25 + proposal = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id)
  26 + comment1 = fast_create(Comment, :source_id => proposal.id)
  27 + comment2 = fast_create(Comment, :source_id => proposal.id)
  28 + assert_equivalent [comment1, comment2], topic.proposals_comments
  29 + end
  30 +
  31 + should 'return list of authors' do
  32 + topic.save!
  33 + author1 = fast_create(Person)
  34 + author2 = fast_create(Person)
  35 + fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id, :created_by_id => author1)
  36 + fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id, :created_by_id => author2)
  37 + assert_equivalent [author1, author2], topic.proposals_authors
  38 + end
  39 +
  40 + should 'return most active participants' do
  41 + topic.save!
  42 + author1 = fast_create(Person)
  43 + author2 = fast_create(Person)
  44 + fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id, :created_by_id => author1)
  45 + fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id, :created_by_id => author2)
  46 + fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id, :created_by_id => author2)
  47 + assert_equal [author2, author1], topic.most_active_participants
  48 + end
  49 +
  50 +end
... ...
plugins/proposals_discussion/views/cms/proposals_discussion_plugin/_proposal.html.erb 0 → 100644
... ... @@ -0,0 +1,28 @@
  1 +<%= required_fields_message %>
  2 +
  3 +<%= render :file => 'shared/tiny_mce' %>
  4 +
  5 +<% title_limit = 70 %>
  6 +<% abstract_limit = 140 %>
  7 +
  8 +<div class="proposals-discussion-plugin">
  9 + <div class="title">
  10 + <%= required labelled_form_field _('Title'), limited_text_area(:article, :name, title_limit, 'title_textarea', :rows => 1) %>
  11 + </div>
  12 +
  13 + <div class="abstract">
  14 + <%= required labelled_form_field _('Abstract'), limited_text_area(:article, :abstract, abstract_limit, 'abstract_textarea') %>
  15 + </div>
  16 +
  17 + <div class="body">
  18 + <% editor_type = 'mceEditor' %>
  19 + <%= labelled_form_field(_('Text'), text_area(:article, :body, :class => editor_type)) %>
  20 + </div>
  21 +</div>
  22 +
  23 +<script>
  24 +jQuery( document ).ready(function( $ ) {
  25 + limited_text_area('title_textarea', <%= title_limit %>);
  26 + limited_text_area('abstract_textarea', <%= abstract_limit %>);
  27 +});
  28 +</script>
... ...
plugins/proposals_discussion/views/cms/proposals_discussion_plugin/_topic.html.erb 0 → 100644
... ... @@ -0,0 +1,13 @@
  1 +<%= stylesheet_link_tag 'spectrum.css' %>
  2 +<%= javascript_include_tag "spectrum.js" %>
  3 +<%= javascript_include_tag "colorpicker-noosfero.js" %>
  4 +
  5 +<%= required_fields_message %>
  6 +
  7 +<%= required f.text_field('name', :size => '64', :maxlength => 150) %>
  8 +<%= render :partial => 'general_fields' %>
  9 +
  10 +<%= labelled_form_field(_('Description:'), text_area(:article, :body, :rows => 3, :cols => 64)) %>
  11 +
  12 +<%= labelled_colorpicker_field(_('Color:'), :article, :color) %>
  13 +<span id="color_preview" class = "color_marker" style="background-color: <%= @article.color %>" ></span>
... ...
plugins/proposals_discussion/views/content_viewer/_proposal_card.html.erb 0 → 100644
... ... @@ -0,0 +1,20 @@
  1 +<div class="proposal">
  2 + <div class="topic-color" style="background-color: <%= proposal_card.topic.color %>;"></div>
  3 + <div class="content">
  4 + <div class="title">
  5 + <%= link_to proposal_card.name, proposal_card.view_url %>
  6 + </div>
  7 + <div class="social">
  8 + <%= render :partial => 'content_viewer/social', :locals => {:proposal => proposal_card} %>
  9 + </div>
  10 + <div class="abstract">
  11 + <%= link_to strip_tags(proposal_card.abstract), proposal_card.view_url %>
  12 + </div>
  13 + </div>
  14 + <div class="score">
  15 + <%= proposal_card.comments_count %>
  16 + </div>
  17 + <div class="topic">
  18 + <%= link_to proposal_card.topic.title, proposal_card.topic.view_url %>
  19 + </div>
  20 +</div>
... ...
plugins/proposals_discussion/views/content_viewer/_proposals_list.html.erb 0 → 100644
... ... @@ -0,0 +1,30 @@
  1 +<script src="/javascripts/plugins/proposals_discussion/jquery.jscroll.min.js" type="text/javascript"></script>
  2 +<%= javascript_include_tag 'plugins/proposals_discussion/proposals_list.js' %>
  3 +
  4 +<% extend ProposalsDiscussionPlugin::ProposalsListHelper %>
  5 +
  6 +<% private_proposals = user ? @page.proposals.private(user) : [] %>
  7 +<% unless private_proposals.empty? %>
  8 +<div class="private-proposals">
  9 + <h5><%= _('My private proposals') %></h5>
  10 + <%= render :partial => 'content_viewer/proposal_card', :collection => private_proposals %>
  11 +</div>
  12 +<% end %>
  13 +
  14 +<% order ||= 'random' %>
  15 +<div class="proposals_list">
  16 + <h5><%= _('Proposals') %></h5>
  17 + <div class="filters">
  18 + <% [[_('Random'), :random], [_('Aplhabetical'), :alphabetical]].each_with_index do |order, i| %>
  19 + <%= link_to order.first, url_for({:controller => 'proposals_discussion_plugin_public', :action => 'load_proposals', :holder_id => holder.id, :profile => profile.identifier, :order => order.second}), :remote => true, :class => "order #{order.second} #{i==0 ? 'selected':''}" %>
  20 + <% end %>
  21 + </div>
  22 + <div class="clear"></div>
  23 +
  24 + <div class="proposals">
  25 + <div class="more">
  26 + <img src="/images/loading.gif" alt="Loading" /><%= _("Loading...") %>
  27 + <%= more_proposals('', holder, order) %>
  28 + </div>
  29 + </div>
  30 +</div>
... ...
plugins/proposals_discussion/views/content_viewer/_proposals_list_content.html.erb 0 → 100644
... ... @@ -0,0 +1,7 @@
  1 +<%= render :partial => 'content_viewer/proposal_card', :collection => proposals %>
  2 +
  3 +<% extend ProposalsDiscussionPlugin::ProposalsListHelper %>
  4 +
  5 +<div class="more">
  6 + <%= more_proposals(_('More'), holder, order, page) %>
  7 +</div>
... ...
plugins/proposals_discussion/views/content_viewer/_social.html.erb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<% if proposal.published? %>
  2 + <a href="https://twitter.com/share" class="twitter-share-button" data-url="<%= url_for proposal.view_url %>" data-text="<%= proposal.title %>" data-count="none"></a>
  3 +<% end %>
... ...
plugins/proposals_discussion/views/content_viewer/discussion.html.erb 0 → 100644
... ... @@ -0,0 +1,29 @@
  1 +<div class="description">
  2 + <%= @page.body %>
  3 +</div>
  4 +
  5 +<% if @page.allow_create?(user) %>
  6 +<div class="actions">
  7 + <%= link_to url_for({:controller => 'cms', :action => 'new', :type => "ProposalsDiscussionPlugin::Topic", :parent_id => @page.id}), :class => 'button with-text icon-add' do %>
  8 + <strong><%= _("New Topic") %></strong>
  9 + <% end %>
  10 +</div>
  11 +<% end %>
  12 +
  13 +<div class="topics">
  14 + <h3><%= _('Discussion Topics') %></h3>
  15 + <% @page.topics.includes(:profile).each do |topic| %>
  16 + <div class="topic">
  17 + <div class="topic-color" style="background-color: <%= topic.color %>;"></div>
  18 + <%= link_to topic.title, topic.view_url %>
  19 + </div>
  20 + <% end %>
  21 +</div>
  22 +
  23 +<div class="actions">
  24 + <%= link_to url_for({:controller => 'proposals_discussion_plugin_myprofile', :action => 'select_topic', :parent_id => @page.id}), :class => 'button with-text icon-add' do %>
  25 + <strong><%= _("Send my proposal") %></strong>
  26 + <% end %>
  27 +</div>
  28 +
  29 +<%= render :partial => 'content_viewer/proposals_list', :locals => {:holder => @page} %>
... ...
plugins/proposals_discussion/views/content_viewer/proposal.html.erb 0 → 100644
... ... @@ -0,0 +1,27 @@
  1 +<div class="topic">
  2 + <h5><%= _('Discussion Topic') %></h5>
  3 + <div class="content"><%= @page.topic.title %></div>
  4 +</div>
  5 +
  6 +<div class="title">
  7 + <h5><%= _('Title') %></h4>
  8 + <div class="content"><%= @page.title %></div>
  9 +</div>
  10 +
  11 +<div class="abstract">
  12 + <h5><%= _('Abstract') %></h4>
  13 + <div class="content"><%= @page.abstract %></div>
  14 +</div>
  15 +
  16 +<div class="body">
  17 + <h5><%= _('Body') %></h4>
  18 + <div class="content"><%= @page.body %></div>
  19 +</div>
  20 +
  21 +<% if @page.allow_edit?(user) && !@page.published %>
  22 +<div class="actions">
  23 + <%= link_to url_for({:controller => 'proposals_discussion_plugin_myprofile', :action => 'publish_proposal', :proposal_id => @page.id}), :class => 'button with-text icon-suggest' do %>
  24 + <strong><%= _("Publish") %></strong>
  25 + <% end %>
  26 +</div>
  27 +<% end %>
... ...
plugins/proposals_discussion/views/content_viewer/topic.html.erb 0 → 100644
... ... @@ -0,0 +1,33 @@
  1 +<div class="description">
  2 + <%= @page.body %>
  3 +</div>
  4 +<h4><%= @page.discussion.title %></h4>
  5 +
  6 +<div class="proposals-count">
  7 + <span class="label"><%= _('Number of Proposals: ') %></span>
  8 + <span class="content"><%= @page.proposals.count %></span>
  9 +</div>
  10 +<div class="participants-count">
  11 + <span class="label"><%= _('Number of Participants: ') %></span>
  12 + <span class="content"><%= @page.proposals_authors.count %></span>
  13 +</div>
  14 +<div class="comments-count">
  15 + <span class="label"><%= _('Number of Comments: ') %></span>
  16 + <span class="content"><%= @page.proposals_comments.count %></span>
  17 +</div>
  18 +<div class="active-participants">
  19 + <span class="label"><%= _('Most active: ') %></span>
  20 + <span class="content">
  21 + <% @page.most_active_participants.each do |author| %>
  22 + <%= link_to profile_image(author, :icon), author.url, :title => author.name %>
  23 + <% end %>
  24 + </span>
  25 +</div>
  26 +
  27 +<div class="actions">
  28 + <%= link_to url_for({:controller => 'cms', :action => 'new', :type => "ProposalsDiscussionPlugin::Proposal", :parent_id => @page.id}), :class => 'button with-text icon-add' do %>
  29 + <strong><%= _("Send my proposal") %></strong>
  30 + <% end %>
  31 +</div>
  32 +
  33 +<%= render :partial => 'content_viewer/proposals_list', :locals => {:holder => @page} %>
... ...
plugins/proposals_discussion/views/select_topic.html.erb 0 → 100644
... ... @@ -0,0 +1,37 @@
  1 +<script>
  2 + jQuery(document).ready(function($){
  3 + $("#topics").accordion();
  4 + $('#topics input[type=radio],label').on('click',function(e){e.stopPropagation();});
  5 + $('#topics h4').click(function(e){
  6 + e.stopPropagation();
  7 + $(this).find('input:radio').prop('checked', true);
  8 + });
  9 + });
  10 +</script>
  11 +
  12 +<div class="proposals-discussion-plugin select-topic">
  13 + <h1><%= @discussion.title %></h1>
  14 + <%= form_for :discussion, :url => {:controller => 'proposals_discussion_plugin_myprofile', :action => 'new_proposal'} do %>
  15 +
  16 + <h3><%= _('Select topic') %></h3>
  17 + <div id="topics">
  18 +
  19 + <% @discussion.children.each do |topic| %>
  20 + <h4>
  21 + <%= radio_button_tag('discussion[topic]', topic.id) %>
  22 + <%= topic.title %>
  23 + </h4>
  24 + <div class="content">
  25 + <%= topic.body %>
  26 + </div>
  27 + <% end %>
  28 +
  29 + <div class="clear"></div>
  30 + </div>
  31 +
  32 + <div class="actions">
  33 + <%= submit_button(:next, _('Next')) %>
  34 + <%= button :cancel, _('Cancel'), @discussion.view_url %>
  35 + </div>
  36 + <% end %>
  37 +</div>
... ...
test/functional/features_controller_test.rb
... ... @@ -146,7 +146,7 @@ class FeaturesControllerTest &lt; ActionController::TestCase
146 146 assert_equal true, e.custom_community_fields['contact_person']['required']
147 147 end
148 148  
149   - should 'search members' do
  149 + should 'search members by name' do
150 150 uses_host 'anhetegua.net'
151 151 person = fast_create(Person, :environment_id => Environment.find(2).id)
152 152 xhr :get, :search_members, :q => person.name[0..2]
... ... @@ -154,4 +154,12 @@ class FeaturesControllerTest &lt; ActionController::TestCase
154 154 assert_includes json_response, {"id"=>person.id, "name"=>person.name}
155 155 end
156 156  
  157 + should 'search members by identifier' do
  158 + uses_host 'anhetegua.net'
  159 + person = fast_create(Person, :name => 'Some Name', :identifier => 'person-identifier', :environment_id => Environment.find(2).id)
  160 + xhr :get, :search_members, :q => person.identifier
  161 + json_response = ActiveSupport::JSON.decode(@response.body)
  162 + assert_includes json_response, {"id"=>person.id, "name"=>person.name}
  163 + end
  164 +
157 165 end
... ...
test/unit/categories_helper_test.rb
... ... @@ -31,4 +31,10 @@ class CategoriesHelperTest &lt; ActiveSupport::TestCase
31 31 assert_equal '', category_color_style(category2)
32 32 end
33 33  
  34 + should 'not return category parent color if category is nil' do
  35 + assert_nothing_raised do
  36 + assert_equal '', category_color_style(nil)
  37 + end
  38 + end
  39 +
34 40 end
... ...