Commit a2ecebd84ea929b92b0829fb2289628131956eea
1 parent
8eba02a9
Exists in
master
and in
11 other branches
proposals_discussion: added discussion phases
Showing
14 changed files
with
259 additions
and
13 deletions
Show diff stats
| ... | ... | @@ -0,0 +1,15 @@ |
| 1 | +require_dependency 'models/vote' | |
| 2 | + | |
| 3 | +class Vote | |
| 4 | + | |
| 5 | + validate :proposals_discussion_plugin_modify_vote | |
| 6 | + before_destroy :proposals_discussion_plugin_modify_vote | |
| 7 | + | |
| 8 | + def proposals_discussion_plugin_modify_vote | |
| 9 | + if voteable.kind_of?(ProposalsDiscussionPlugin::Proposal) && !voteable.allow_vote? | |
| 10 | + errors.add(:base, _("Can't vote in this discussion anymore.")) | |
| 11 | + false | |
| 12 | + end | |
| 13 | + end | |
| 14 | + | |
| 15 | +end | ... | ... |
lib/proposals_discussion_plugin/discussion.rb
| ... | ... | @@ -19,8 +19,11 @@ class ProposalsDiscussionPlugin::Discussion < ProposalsDiscussionPlugin::Proposa |
| 19 | 19 | |
| 20 | 20 | settings_items :custom_body_label, :type => :string, :default => _('Body') |
| 21 | 21 | settings_items :allow_topics, :type => :boolean, :default => false |
| 22 | + settings_items :phase, :type => :string, :default => :proposals | |
| 22 | 23 | |
| 23 | - attr_accessible :custom_body_label, :allow_topics | |
| 24 | + attr_accessible :custom_body_label, :allow_topics, :phase | |
| 25 | + | |
| 26 | + AVAILABLE_PHASES = {:proposals => _('Proposals'), :vote => 'Vote', :finish => 'Announcement'} | |
| 24 | 27 | |
| 25 | 28 | def self.short_description |
| 26 | 29 | _("Discussion") |
| ... | ... | @@ -30,6 +33,14 @@ class ProposalsDiscussionPlugin::Discussion < ProposalsDiscussionPlugin::Proposa |
| 30 | 33 | _('Container for topics.') |
| 31 | 34 | end |
| 32 | 35 | |
| 36 | + def available_phases | |
| 37 | + AVAILABLE_PHASES | |
| 38 | + end | |
| 39 | + | |
| 40 | + def allow_new_proposals? | |
| 41 | + phase.to_sym == :proposals | |
| 42 | + end | |
| 43 | + | |
| 33 | 44 | def to_html(options = {}) |
| 34 | 45 | discussion = self |
| 35 | 46 | proc do | ... | ... |
| ... | ... | @@ -0,0 +1,22 @@ |
| 1 | +module ProposalsDiscussionPlugin::DiscussionHelper | |
| 2 | + | |
| 3 | + def link_to_new_proposal(discussion) | |
| 4 | + return '' unless discussion.allow_new_proposals? | |
| 5 | + | |
| 6 | + url = {:parent_id => discussion.id, :profile => discussion.profile.identifier} | |
| 7 | + if discussion.allow_topics | |
| 8 | + url.merge!(:controller => 'proposals_discussion_plugin_myprofile', :action => 'select_topic') | |
| 9 | + else | |
| 10 | + url.merge!(:controller => 'cms', :action => 'new', :type => "ProposalsDiscussionPlugin::Proposal") | |
| 11 | + end | |
| 12 | + link_to _("Send your proposal!"), url_for(url), :class => 'button with-text icon-add' | |
| 13 | + end | |
| 14 | + | |
| 15 | + def discussion_phases(discussion) | |
| 16 | + discussion.available_phases.map do |phase| | |
| 17 | + active = discussion.phase.to_sym == phase.first ? ' active' : '' | |
| 18 | + content_tag 'span', phase.second, :class => "phase #{phase.first}#{active}" | |
| 19 | + end.join | |
| 20 | + end | |
| 21 | + | |
| 22 | +end | ... | ... |
lib/proposals_discussion_plugin/proposal.rb
| ... | ... | @@ -15,8 +15,18 @@ class ProposalsDiscussionPlugin::Proposal < TinyMceArticle |
| 15 | 15 | |
| 16 | 16 | validates_presence_of :abstract |
| 17 | 17 | |
| 18 | + validate :discussion_phase_proposals | |
| 19 | + | |
| 20 | + def discussion_phase_proposals | |
| 21 | + errors.add(:base, _("Can't create a proposal at this phase.")) unless discussion.allow_new_proposals? | |
| 22 | + end | |
| 23 | + | |
| 24 | + def allow_vote? | |
| 25 | + discussion.phase.to_sym != :finish | |
| 26 | + end | |
| 27 | + | |
| 18 | 28 | def discussion |
| 19 | - parent.kind_of?(ProposalsDiscussionPlugin::Discussion) ? parent : parent.discussion | |
| 29 | + @discussion ||= parent.kind_of?(ProposalsDiscussionPlugin::Discussion) ? parent : parent.discussion | |
| 20 | 30 | end |
| 21 | 31 | |
| 22 | 32 | def to_html(options = {}) | ... | ... |
public/style.css
| 1 | +.article-body-proposals-discussion-plugin_discussion .phases { | |
| 2 | + margin: 20px; | |
| 3 | + color: white; | |
| 4 | +} | |
| 5 | +.article-body-proposals-discussion-plugin_discussion .phases .phase { | |
| 6 | + padding: 8px; | |
| 7 | + background-color: gray; | |
| 8 | + min-width: 70px; | |
| 9 | + display: inline-block; | |
| 10 | + text-align: center; | |
| 11 | + font-size: 14px; | |
| 12 | + font-weight: bold; | |
| 13 | + opacity: 0.3; | |
| 14 | +} | |
| 15 | +.article-body-proposals-discussion-plugin_discussion .phases .proposals { | |
| 16 | + background-color: rgb(55, 186, 211); | |
| 17 | +} | |
| 18 | +.article-body-proposals-discussion-plugin_discussion .phases .vote { | |
| 19 | + background-color: rgb(236, 141, 52); | |
| 20 | +} | |
| 21 | +.article-body-proposals-discussion-plugin_discussion .phases .finish { | |
| 22 | + background-color: rgb(57, 175, 142); | |
| 23 | +} | |
| 24 | +.article-body-proposals-discussion-plugin_discussion .phases .active { | |
| 25 | + opacity: 1; | |
| 26 | +} | |
| 27 | + | |
| 1 | 28 | .proposals_list .proposal .abstract { |
| 2 | 29 | color: rgb(160, 160, 160); |
| 3 | 30 | margin-bottom: 15px; | ... | ... |
test/functional/cms_controller_test.rb
| ... | ... | @@ -30,4 +30,9 @@ class CmsControllerTest < ActionController::TestCase |
| 30 | 30 | assert_tag :tag => 'label', :attributes => {:class => 'formlabel'}, :content => 'My Custom Label' |
| 31 | 31 | end |
| 32 | 32 | |
| 33 | + should 'display available phases when edit a proposal' do | |
| 34 | + get :edit, :id => discussion.id, :profile => profile.identifier | |
| 35 | + assert_tag :tag => 'select', :attributes => {:id => 'article_phase'} | |
| 36 | + end | |
| 37 | + | |
| 33 | 38 | end | ... | ... |
| ... | ... | @@ -0,0 +1,28 @@ |
| 1 | +require_relative '../test_helper' | |
| 2 | + | |
| 3 | +class DiscussionHelperTest < ActionView::TestCase | |
| 4 | + | |
| 5 | + def setup | |
| 6 | + @profile = fast_create(Community) | |
| 7 | + @discussion = ProposalsDiscussionPlugin::Discussion.create!(:name => 'discussion', :profile => @profile, :name => 'discussion') | |
| 8 | + end | |
| 9 | + | |
| 10 | + include ProposalsDiscussionPlugin::DiscussionHelper | |
| 11 | + | |
| 12 | + attr_reader :profile, :discussion | |
| 13 | + | |
| 14 | + should 'display new proposal link when discussion is in proposals phase' do | |
| 15 | + assert !link_to_new_proposal(discussion).blank? | |
| 16 | + end | |
| 17 | + | |
| 18 | + should 'not display new proposal link when discussion is in vote phase' do | |
| 19 | + discussion.update_attribute(:phase, :vote) | |
| 20 | + assert link_to_new_proposal(discussion).blank? | |
| 21 | + end | |
| 22 | + | |
| 23 | + should 'not display new proposal link when discussion is in finish phase' do | |
| 24 | + discussion.update_attribute(:phase, :finish) | |
| 25 | + assert link_to_new_proposal(discussion).blank? | |
| 26 | + end | |
| 27 | + | |
| 28 | +end | ... | ... |
test/unit/discussion_test.rb
| ... | ... | @@ -34,4 +34,19 @@ class DiscussionTest < ActiveSupport::TestCase |
| 34 | 34 | assert_equal 10, discussion.max_score |
| 35 | 35 | end |
| 36 | 36 | |
| 37 | + should 'allow new proposals if discussion phase is proposals' do | |
| 38 | + discussion.phase = :proposals | |
| 39 | + assert discussion.allow_new_proposals? | |
| 40 | + end | |
| 41 | + | |
| 42 | + should 'not allow new proposals if discussion phase is vote' do | |
| 43 | + discussion.phase = :vote | |
| 44 | + assert !discussion.allow_new_proposals? | |
| 45 | + end | |
| 46 | + | |
| 47 | + should 'not allow new proposals if discussion phase is finish' do | |
| 48 | + discussion.phase = :finish | |
| 49 | + assert !discussion.allow_new_proposals? | |
| 50 | + end | |
| 51 | + | |
| 37 | 52 | end | ... | ... |
test/unit/proposal_test.rb
| ... | ... | @@ -5,11 +5,12 @@ class ProposalTest < ActiveSupport::TestCase |
| 5 | 5 | def setup |
| 6 | 6 | @profile = fast_create(Community) |
| 7 | 7 | @person = fast_create(Person) |
| 8 | - @proposal = ProposalsDiscussionPlugin::Proposal.new(:name => 'test', :profile => @profile) | |
| 8 | + @discussion = ProposalsDiscussionPlugin::Discussion.create!(:name => 'discussion', :profile => person, :name => 'discussion') | |
| 9 | + @proposal = ProposalsDiscussionPlugin::Proposal.new(:name => 'test', :abstract => 'abstract', :profile => @profile, :parent => @discussion) | |
| 9 | 10 | @proposal.created_by = @person |
| 10 | 11 | end |
| 11 | 12 | |
| 12 | - attr_reader :profile, :proposal, :person | |
| 13 | + attr_reader :profile, :proposal, :person, :discussion | |
| 13 | 14 | |
| 14 | 15 | should 'save a proposal' do |
| 15 | 16 | proposal.abstract = 'abstract' |
| ... | ... | @@ -17,6 +18,7 @@ class ProposalTest < ActiveSupport::TestCase |
| 17 | 18 | end |
| 18 | 19 | |
| 19 | 20 | should 'do not save a proposal without abstract' do |
| 21 | + proposal.abstract = nil | |
| 20 | 22 | proposal.save |
| 21 | 23 | assert proposal.errors['abstract'].present? |
| 22 | 24 | end |
| ... | ... | @@ -42,7 +44,6 @@ class ProposalTest < ActiveSupport::TestCase |
| 42 | 44 | end |
| 43 | 45 | |
| 44 | 46 | should 'return proposals by discussion' do |
| 45 | - discussion = fast_create(ProposalsDiscussionPlugin::Discussion) | |
| 46 | 47 | topic = fast_create(ProposalsDiscussionPlugin::Topic, :parent_id => discussion.id) |
| 47 | 48 | proposal1 = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id) |
| 48 | 49 | proposal2 = fast_create(ProposalsDiscussionPlugin::Proposal) |
| ... | ... | @@ -51,8 +52,17 @@ class ProposalTest < ActiveSupport::TestCase |
| 51 | 52 | assert_equivalent [proposal1, proposal3], ProposalsDiscussionPlugin::Proposal.from_discussion(discussion) |
| 52 | 53 | end |
| 53 | 54 | |
| 55 | + should 'return discussion associated with a proposal' do | |
| 56 | + assert_equal discussion, proposal.discussion | |
| 57 | + end | |
| 58 | + | |
| 59 | + should 'return discussion associated with a proposal topic' do | |
| 60 | + topic = fast_create(ProposalsDiscussionPlugin::Topic, :parent_id => discussion.id) | |
| 61 | + proposal = fast_create(ProposalsDiscussionPlugin::Proposal, :parent_id => topic.id) | |
| 62 | + assert_equal discussion, proposal.discussion | |
| 63 | + end | |
| 64 | + | |
| 54 | 65 | should 'return normalized score' do |
| 55 | - discussion = ProposalsDiscussionPlugin::Discussion.create!(:profile => person, :name => 'discussion') | |
| 56 | 66 | proposal1 = ProposalsDiscussionPlugin::Proposal.create!(:parent => discussion, :profile => profile, :name => "proposal1", :abstract => 'abstract') |
| 57 | 67 | proposal2 = ProposalsDiscussionPlugin::Proposal.create!(:parent => discussion, :profile => profile, :name => "proposal2", :abstract => 'abstract') |
| 58 | 68 | 10.times { Comment.create!(:source => proposal1, :body => "comment", :author => person) } |
| ... | ... | @@ -61,4 +71,50 @@ class ProposalTest < ActiveSupport::TestCase |
| 61 | 71 | assert_equal 0.5, proposal2.reload.normalized_score |
| 62 | 72 | end |
| 63 | 73 | |
| 74 | + should 'create a new proposal if the current phase is proposals' do | |
| 75 | + discussion.update_attribute(:phase, :proposals) | |
| 76 | + assert proposal.save | |
| 77 | + end | |
| 78 | + | |
| 79 | + should 'do not create a new proposal if the current phase is vote' do | |
| 80 | + discussion.update_attribute(:phase, :vote) | |
| 81 | + assert !proposal.save | |
| 82 | + end | |
| 83 | + | |
| 84 | + should 'do not create a new proposal if the current phase is finish' do | |
| 85 | + discussion.update_attribute(:phase, :finish) | |
| 86 | + assert !proposal.save | |
| 87 | + end | |
| 88 | + | |
| 89 | + should 'do not create a new proposal if the current phase is invalid' do | |
| 90 | + discussion.update_attribute(:phase, '') | |
| 91 | + assert !proposal.save | |
| 92 | + end | |
| 93 | + | |
| 94 | + should 'do not update a proposal if a discussion is not in proposals phase' do | |
| 95 | + discussion.update_attribute(:phase, :vote) | |
| 96 | + proposal.body = "changed" | |
| 97 | + assert !proposal.save | |
| 98 | + end | |
| 99 | + | |
| 100 | + should 'allow update of proposals if a discussion is in proposals phase' do | |
| 101 | + proposal.body = "changed" | |
| 102 | + assert proposal.save | |
| 103 | + end | |
| 104 | + | |
| 105 | + should 'allow vote if discussion phase is vote' do | |
| 106 | + discussion.update_attribute(:phase, :vote) | |
| 107 | + assert proposal.allow_vote? | |
| 108 | + end | |
| 109 | + | |
| 110 | + should 'allow vote if discussion phase is proposals' do | |
| 111 | + discussion.update_attribute(:phase, :proposals) | |
| 112 | + assert proposal.allow_vote? | |
| 113 | + end | |
| 114 | + | |
| 115 | + should 'not allow vote if discussion phase is finish' do | |
| 116 | + discussion.update_attribute(:phase, :finish) | |
| 117 | + assert !proposal.allow_vote? | |
| 118 | + end | |
| 119 | + | |
| 64 | 120 | end | ... | ... |
test/unit/topic_test.rb
| ... | ... | @@ -3,8 +3,9 @@ require File.dirname(__FILE__) + '/../test_helper' |
| 3 | 3 | class TopicTest < ActiveSupport::TestCase |
| 4 | 4 | |
| 5 | 5 | def setup |
| 6 | + @discussion = fast_create(ProposalsDiscussionPlugin::Discussion) | |
| 6 | 7 | @profile = fast_create(Community) |
| 7 | - @topic = ProposalsDiscussionPlugin::Topic.new(:name => 'test', :profile => @profile) | |
| 8 | + @topic = ProposalsDiscussionPlugin::Topic.new(:name => 'test', :profile => @profile, :parent => @discussion) | |
| 8 | 9 | end |
| 9 | 10 | |
| 10 | 11 | attr_reader :profile, :topic | ... | ... |
| ... | ... | @@ -0,0 +1,53 @@ |
| 1 | +require_relative '../test_helper' | |
| 2 | + | |
| 3 | +class VoteTest < ActiveSupport::TestCase | |
| 4 | + | |
| 5 | + def setup | |
| 6 | + @person = fast_create(Person) | |
| 7 | + @profile = fast_create(Community) | |
| 8 | + @discussion = ProposalsDiscussionPlugin::Discussion.create!(:name => 'discussion', :profile => @person, :name => 'discussion') | |
| 9 | + @proposal = ProposalsDiscussionPlugin::Proposal.create!(:name => 'test', :abstract => 'abstract', :profile => @profile, :parent => @discussion) | |
| 10 | + end | |
| 11 | + | |
| 12 | + attr_reader :profile, :proposal, :person, :discussion | |
| 13 | + | |
| 14 | + should 'vote for articles that are not proposals' do | |
| 15 | + article = fast_create(Article) | |
| 16 | + vote = Vote.new(:voteable => article, :voter => person, :vote => 1) | |
| 17 | + assert vote.save | |
| 18 | + end | |
| 19 | + | |
| 20 | + should 'vote for a proposal of a discussion in proposals phase' do | |
| 21 | + proposal.discussion.phase = :proposals | |
| 22 | + vote = Vote.new(:voteable => proposal, :voter => person, :vote => 1) | |
| 23 | + assert vote.save | |
| 24 | + end | |
| 25 | + | |
| 26 | + should 'vote for a proposal of a discussion in vote phase' do | |
| 27 | + proposal.discussion.phase = :vote | |
| 28 | + vote = Vote.new(:voteable => proposal, :voter => person, :vote => 1) | |
| 29 | + assert vote.save | |
| 30 | + end | |
| 31 | + | |
| 32 | + should 'not vote for a proposal of a finished discussion' do | |
| 33 | + proposal.discussion.phase = :finish | |
| 34 | + vote = Vote.new(:voteable => proposal, :voter => person, :vote => 1) | |
| 35 | + assert !vote.save | |
| 36 | + end | |
| 37 | + | |
| 38 | + should 'not destroy a proposal vote of a finished discussion' do | |
| 39 | + proposal.discussion.phase = :vote | |
| 40 | + vote = Vote.new(:voteable => proposal, :voter => person, :vote => 1) | |
| 41 | + assert vote.save | |
| 42 | + proposal.discussion.phase = :finish | |
| 43 | + assert !vote.destroy | |
| 44 | + end | |
| 45 | + | |
| 46 | + should 'destroy a proposal vote of a discussion in vote phase' do | |
| 47 | + proposal.discussion.phase = :vote | |
| 48 | + vote = Vote.new(:voteable => proposal, :voter => person, :vote => 1) | |
| 49 | + assert vote.save | |
| 50 | + assert vote.destroy | |
| 51 | + end | |
| 52 | + | |
| 53 | +end | ... | ... |
views/cms/proposals_discussion_plugin/_discussion.html.erb
| ... | ... | @@ -6,4 +6,5 @@ |
| 6 | 6 | <%= labelled_form_field(_('Description:'), text_area(:article, :body, :rows => 3, :cols => 64)) %> |
| 7 | 7 | |
| 8 | 8 | <%= f.text_field(:custom_body_label) %> |
| 9 | +<%= labelled_form_field _('Current Phase'), f.select(:phase, ProposalsDiscussionPlugin::Discussion::AVAILABLE_PHASES.map{|k,v| [v,k]} ) %> | |
| 9 | 10 | <%= labelled_form_field check_box(:article, :allow_topics) + _('Allow topics'), '' %> | ... | ... |
views/content_viewer/discussion.html.erb
| 1 | +<% extend ProposalsDiscussionPlugin::DiscussionHelper %> | |
| 1 | 2 | <%= javascript_include_tag 'plugins/proposals_discussion/proposals_list.js' %> |
| 2 | 3 | |
| 3 | 4 | <%= add_rss_feed_to_head(discussion.name, discussion.feed.url) if discussion.feed %> |
| ... | ... | @@ -6,6 +7,10 @@ |
| 6 | 7 | <%= discussion.body %> |
| 7 | 8 | </div> |
| 8 | 9 | |
| 10 | +<div class="phases"> | |
| 11 | + <%= discussion_phases(discussion) %> | |
| 12 | +</div> | |
| 13 | + | |
| 9 | 14 | <% if discussion.allow_create?(user) %> |
| 10 | 15 | <div class="actions"> |
| 11 | 16 | <%= link_to({:controller => :proposals_discussion_plugin_profile, :action => :export, :format => :csv, :article_id => discussion.id}, :class => 'button with-text icon-spread') do %> |
| ... | ... | @@ -21,9 +26,7 @@ |
| 21 | 26 | </div> |
| 22 | 27 | |
| 23 | 28 | <div class="new-proposal"> |
| 24 | - <%= link_to url_for({:controller => 'cms', :action => 'new', :type => "ProposalsDiscussionPlugin::Proposal", :parent_id => discussion.id}), :class => 'button with-text icon-add' do %> | |
| 25 | - <strong><%= _("Send your proposal!") %></strong> | |
| 26 | - <% end %> | |
| 29 | + <%= link_to_new_proposal(discussion) %> | |
| 27 | 30 | </div> |
| 28 | 31 | |
| 29 | 32 | <%= render :partial => 'content_viewer/proposals_list', :locals => {:holder => discussion} %> | ... | ... |
views/content_viewer/discussion_topics.html.erb
| 1 | +<% extend ProposalsDiscussionPlugin::DiscussionHelper %> | |
| 1 | 2 | <%= javascript_include_tag 'plugins/proposals_discussion/proposals_list.js' %> |
| 2 | 3 | |
| 3 | 4 | <%= add_rss_feed_to_head(discussion.name, discussion.feed.url) if discussion.feed %> |
| ... | ... | @@ -20,9 +21,7 @@ |
| 20 | 21 | <div class="topics js-masonry" data-masonry-options='{ "itemSelector": ".topic-item", "columnWidth": 200 }'> |
| 21 | 22 | <div class="actions topic-item"> |
| 22 | 23 | <div class="topic-color"></div> |
| 23 | - <%= link_to url_for({:controller => 'proposals_discussion_plugin_myprofile', :action => 'select_topic', :parent_id => discussion.id}), :class => 'button with-text icon-add' do %> | |
| 24 | - <strong><%= _("Send your proposal!") %></strong> | |
| 25 | - <% end %> | |
| 24 | + <%= link_to_new_proposal(discussion) %> | |
| 26 | 25 | </div> |
| 27 | 26 | <% discussion.topics.includes(:profile).each do |topic| %> |
| 28 | 27 | <div class="topic-item" id="topic-<%= topic.id %>"> | ... | ... |