diff --git a/.gitignore b/.gitignore index c1f6c385d..7d4f3c5ef 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ dump.rdb #Rubinius's JIT *.rbc + +#IDE +diaspora.iml diff --git a/Changelog.md b/Changelog.md index e9111f660..6aa69902f 100644 --- a/Changelog.md +++ b/Changelog.md @@ -40,6 +40,7 @@ * Added comment count to statistic to enable calculations of posts/comments ratios [#4799](https://github.com/diaspora/diaspora/pull/4799) * Add filters to notifications controller [#4814](https://github.com/diaspora/diaspora/pull/4814) * Activate hovercards in SPV and conversations [#4870](https://github.com/diaspora/diaspora/pull/4870) +* Added possibility to conduct polls [#4861](https://github.com/diaspora/diaspora/pull/4861) # 0.3.0.3 @@ -58,6 +59,7 @@ ## Bug fixes * Fix regression caused by using after_commit with nested '#save' which lead to an infinite recursion [#4715](https://github.com/diaspora/diaspora/issues/4715) * Save textarea value before rendering comments when clicked 'show more...' [#4514](https://github.com/diaspora/diaspora/issues/4514) + # 0.3.0.0 ## Pod statistics diff --git a/app/assets/javascripts/app/models/poll_participation.js b/app/assets/javascripts/app/models/poll_participation.js new file mode 100644 index 000000000..a31c35779 --- /dev/null +++ b/app/assets/javascripts/app/models/poll_participation.js @@ -0,0 +1,5 @@ +app.models.PollParticipation = Backbone.Model.extend({ + url : function(){ + "/poll_participations" + } +}); \ No newline at end of file diff --git a/app/assets/javascripts/app/models/status_message.js b/app/assets/javascripts/app/models/status_message.js index 07f8f2f53..bce96553d 100644 --- a/app/assets/javascripts/app/models/status_message.js +++ b/app/assets/javascripts/app/models/status_message.js @@ -13,7 +13,8 @@ app.models.StatusMessage = app.models.Post.extend({ status_message : _.clone(this.attributes), aspect_ids : this.get("aspect_ids"), photos : this.photos && this.photos.pluck("id"), - services : this.get("services") + services : this.get("services"), + poll : this.get("poll") } } }); diff --git a/app/assets/javascripts/app/views/poll_view.js b/app/assets/javascripts/app/views/poll_view.js new file mode 100644 index 000000000..9a1df2a88 --- /dev/null +++ b/app/assets/javascripts/app/views/poll_view.js @@ -0,0 +1,94 @@ +app.views.Poll = app.views.Base.extend({ + templateName : "poll", + + events : { + "click .submit" : "vote", + "click .toggle_result" : "toggleResult" + }, + + initialize : function(options) { + this.poll = this.model.attributes.poll; + this.progressBarFactor = 3; + this.toggleMode = 0; + }, + + postRenderTemplate : function() { + if(this.poll) { + this.setProgressBar(); + } + }, + + removeForm : function() { + var cnt = this.$("form").contents(); + this.$("form").replaceWith(cnt); + this.$('input').remove(); + this.$('submit').remove(); + this.$('.toggle_result_wrapper').remove(); + }, + + setProgressBar : function() { + var answers = this.poll.poll_answers; + for(index = 0; index < answers.length; ++index) { + var percentage = 0; + if(this.poll.participation_count != 0) { + percentage = Math.round(answers[index].vote_count / this.poll.participation_count * 100); + } + var progressBar = this.$(".poll_progress_bar[data-answerid="+answers[index].id+"]"); + progressBar.parent().next().html(" - " + percentage + "%"); + var width = percentage * this.progressBarFactor; + progressBar.css("width", width + "px"); + } + }, + + toggleResult : function(e) { + this.$('.poll_progress_bar_wrapper').toggle(); + this.$('.percentage').toggle(); + if(this.toggleMode == 0) { + this.$('.toggle_result').html(Diaspora.I18n.t("poll.close_result")); + this.toggleMode = 1; + }else{ + this.$('.toggle_result').html(Diaspora.I18n.t("poll.show_result")); + this.toggleMode = 0; + } + return false; + }, + + refreshResult : function(answerId) { + this.updateCounter(answerId); + this.setProgressBar(); + }, + + updateCounter : function(answerId) { + this.poll.participation_count++; + this.$('.poll_statistic').html(Diaspora.I18n.t("poll.count", {"count" : this.poll.participation_count})); + var answers = this.poll.poll_answers; + for(index = 0; index < answers.length; ++index) { + if(answers[index].id == answerId) { + answers[index].vote_count++; + return; + } + } + }, + + vote : function(evt){ + var result = parseInt($(evt.target).parent().find("input[name=vote]:checked").val()); + var pollParticipation = new app.models.PollParticipation(); + var parent = this; + pollParticipation.save({ + "poll_answer_id" : result, + "poll_id" : this.poll.poll_id + },{ + url : "/posts/"+this.poll.post_id+"/poll_participations", + success : function(model, response) { + parent.removeForm(); + parent.refreshResult(result); + if(parent.toggleMode == 0) { + parent.toggleResult(null); + } + + } + }); + return false; + } + +}); \ No newline at end of file diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js index 675f4d4fd..295281bcc 100644 --- a/app/assets/javascripts/app/views/publisher_view.js +++ b/app/assets/javascripts/app/views/publisher_view.js @@ -23,12 +23,16 @@ app.views.Publisher = Backbone.View.extend({ "click .post_preview_button" : "createPostPreview", "textchange #status_message_fake_text": "handleTextchange", "click #locator" : "showLocation", + "click #poll_creator" : "showPollCreator", + "click #add_poll_answer" : "addPollAnswer", + "click .remove_poll_answer" : "removePollAnswer", "click #hide_location" : "destroyLocation", "keypress #location_address" : "avoidEnter" }, initialize : function(opts){ this.standalone = opts ? opts.standalone : false; + this.option_counter = 1; // init shortcut references to the various elements this.el_input = this.$('#status_message_fake_text'); @@ -37,6 +41,8 @@ app.views.Publisher = Backbone.View.extend({ this.el_submit = this.$('input[type=submit], button#submit'); this.el_preview = this.$('button.post_preview_button'); this.el_photozone = this.$('#photodropzone'); + this.el_poll_creator = this.$('#poll_creator_wrapper'); + this.el_poll_answer = this.$('#poll_creator_wrapper .poll_answer'); // init mentions plugin Mentions.initialize(this.el_input); @@ -69,7 +75,7 @@ app.views.Publisher = Backbone.View.extend({ }); this.initSubviews(); - + this.addPollAnswer(); return this; }, @@ -136,7 +142,9 @@ app.views.Publisher = Backbone.View.extend({ "photos" : serializedForm["photos[]"], "services" : serializedForm["services[]"], "location_address" : $("#location_address").val(), - "location_coords" : serializedForm["location[coords]"] + "location_coords" : serializedForm["location[coords]"], + "poll_question" : serializedForm["poll_question"], + "poll_answers" : serializedForm["poll_answers[]"] }, { url : "/status_messages", success : function() { @@ -171,6 +179,36 @@ app.views.Publisher = Backbone.View.extend({ } }, + showPollCreator: function(){ + this.el_poll_creator.toggle(); + }, + + addPollAnswer: function(){ + if($(".poll_answer").size() == 1) { + $(".remove_poll_answer").css("visibility","visible"); + } + + this.option_counter++; + var clone = this.el_poll_answer.clone(); + + var answer = clone.find('.poll_answer_input'); + answer.val(""); + + var placeholder = answer.attr("placeholder"); + var expression = /[^0-9]+/; + answer.attr("placeholder", expression.exec(placeholder) + this.option_counter); + + $('#poll_creator_wrapper .poll_answer').last().after(clone); + }, + + removePollAnswer: function(evt){ + $(evt.currentTarget).parent().remove(); + if($(".poll_answer").size() == 1) { + $(".remove_poll_answer").css("visibility","hidden");; + } + + return false; + }, // avoid submitting form when pressing Enter key avoidEnter: function(evt){ if(evt.keyCode == 13) @@ -295,6 +333,9 @@ app.views.Publisher = Backbone.View.extend({ // clear location this.destroyLocation(); + // clear poll form + this.clearPollForm(); + // force textchange plugin to update lastValue this.el_input.data('lastValue', ''); this.el_hiddenInput.data('lastValue', ''); @@ -302,6 +343,11 @@ app.views.Publisher = Backbone.View.extend({ return this; }, + clearPollForm : function(){ + this.$('#poll_question').val(''); + this.$('.poll_answer_input').val(''); + }, + tryClose : function(){ // if it is not submittable, close it. if( !this._submittable() ){ @@ -323,7 +369,7 @@ app.views.Publisher = Backbone.View.extend({ $(this.el).addClass("closed"); this.el_wrapper.removeClass("active"); this.el_input.css('height', ''); - + this.el_poll_creator.hide(); return this; }, diff --git a/app/assets/javascripts/app/views/stream_post_views.js b/app/assets/javascripts/app/views/stream_post_views.js index d8a9551e0..fb1d846d4 100644 --- a/app/assets/javascripts/app/views/stream_post_views.js +++ b/app/assets/javascripts/app/views/stream_post_views.js @@ -9,6 +9,7 @@ app.views.StreamPost = app.views.Post.extend({ ".post-content" : "postContentView", ".oembed" : "oEmbedView", ".opengraph" : "openGraphView", + ".poll" : "pollView", ".status-message-location" : "postLocationStreamView" }, @@ -31,6 +32,7 @@ app.views.StreamPost = app.views.Post.extend({ this.commentStreamView = new app.views.CommentStream({model : this.model}); this.oEmbedView = new app.views.OEmbed({model : this.model}); this.openGraphView = new app.views.OpenGraph({model : this.model}); + this.pollView = new app.views.Poll({model : this.model}); }, diff --git a/app/assets/stylesheets/application.css.sass b/app/assets/stylesheets/application.css.sass index c0555449e..08b7a445b 100644 --- a/app/assets/stylesheets/application.css.sass +++ b/app/assets/stylesheets/application.css.sass @@ -10,6 +10,7 @@ @import 'header' @import 'footer' @import 'opengraph' +@import 'poll' @import 'help' @import 'profile' @import 'publisher_blueprint' diff --git a/app/assets/stylesheets/poll.css.scss b/app/assets/stylesheets/poll.css.scss new file mode 100644 index 000000000..dd5c89f4f --- /dev/null +++ b/app/assets/stylesheets/poll.css.scss @@ -0,0 +1,45 @@ +.poll_form { + display: block; + margin: 10px 0px 10px 0px; + border-top: solid 1px $border-grey; + border-bottom: solid 1px $border-grey; + padding: 10px 0px 5px 0px; + overflow: hidden; + width: 100%; +} + +.poll_form input[type="radio"] { + display:inline !important; +} + +.poll_result { + width:100%px; +} + +.poll_progress_bar { + position:absolute; + width:0px; + height:15px; + top:-12px; + z-index:-1; + background-color:$background-grey; +} + +.poll_statistic{ + float:right; +} + +.poll_progress_bar_wrapper { + position: relative; + width: 0; + height: 0; + display:inline-block; +} + +.poll_answer_entry{ + width:100%; +} + +.percentage { + display:inline; +} diff --git a/app/assets/stylesheets/publisher.css.scss b/app/assets/stylesheets/publisher.css.scss index 0fe934716..550221777 100644 --- a/app/assets/stylesheets/publisher.css.scss +++ b/app/assets/stylesheets/publisher.css.scss @@ -82,6 +82,16 @@ &.with_attachments .row-fluid#photodropzone_container { border-top: 1px dashed $border-grey; } + + #poll_creator_wrapper { + display:none; + border-top: 1px dashed $border-grey; + padding:4px 6px 4px 6px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + } + &.with_location .row-fluid#location_container { height: 30px; #hide_location { display: none !important; } @@ -162,6 +172,7 @@ margin-right: 5px; #file-upload, #locator, + #poll_creator, #hide_location { text-decoration: none !important; font-size: 16px; diff --git a/app/assets/stylesheets/publisher_blueprint.css.scss b/app/assets/stylesheets/publisher_blueprint.css.scss index 23e3a94ca..9865e7997 100644 --- a/app/assets/stylesheets/publisher_blueprint.css.scss +++ b/app/assets/stylesheets/publisher_blueprint.css.scss @@ -309,8 +309,75 @@ } } } + #poll_creator { + bottom: 1px !important; + display: inline-block; + margin: 0; + cursor: pointer; + position: absolute !important; + right: 55px; + i { + @include opacity(0.4); + } + &:hover { + color: $text-dark-grey; + cursor: pointer; + i { + @include opacity(1); + } + } + } + .btn { height: 19px; width: 19px; } } + +#poll_creator_wrapper { + display:none; + border: 1px solid $border-dark-grey; + padding:5px; + margin-top:1em; + @include border-radius(2px); +} + +.remove_poll_answer { + visibility:hidden; + float:right; + display: table-cell; + + .icons-deletelabel { + height: 14px; + width: 14px; + margin-top:5px; + } +} + +.poll_answer_input { + width:100%; +} + +#add_poll_answer_wrapper { + padding:5px 0 5px 0; + display:block; +} + +#poll_question_wrapper { +} + +#poll_question { + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} + +.poll_answer { + display: table; + width: 100%; +} + +.poll_answer_input_wrapper { + display: table-cell; +} \ No newline at end of file diff --git a/app/assets/templates/poll_tpl.jst.hbs b/app/assets/templates/poll_tpl.jst.hbs new file mode 100644 index 000000000..ed9e18e7c --- /dev/null +++ b/app/assets/templates/poll_tpl.jst.hbs @@ -0,0 +1,32 @@ +{{#if poll}} +
+

{{t "poll.count" count=poll.participation_count}}

+ {{poll.question}}
+ {{#unless already_participated_in_poll}} +
+ {{#poll.poll_answers}} + + + {{answer}} + +
+ {{/poll.poll_answers}} + +
+

+
{{t "poll.show_result"}}
+

+ {{else}} + {{#poll.poll_answers}} +
+
+
+ {{answer}} +

+
+ {{/poll.poll_answers}} + {{/unless}} +
+{{/if}} \ No newline at end of file diff --git a/app/assets/templates/status-message_tpl.jst.hbs b/app/assets/templates/status-message_tpl.jst.hbs index 166c8cb1d..761dc57c6 100644 --- a/app/assets/templates/status-message_tpl.jst.hbs +++ b/app/assets/templates/status-message_tpl.jst.hbs @@ -18,4 +18,5 @@ {{{text}}}
+
diff --git a/app/controllers/poll_participations_controller.rb b/app/controllers/poll_participations_controller.rb new file mode 100644 index 000000000..2fd07c76e --- /dev/null +++ b/app/controllers/poll_participations_controller.rb @@ -0,0 +1,28 @@ +class PollParticipationsController < ApplicationController + include ApplicationHelper + before_filter :authenticate_user! + + def create + answer = PollAnswer.find(params[:poll_answer_id]) + poll_participation = current_user.participate_in_poll!(target, answer) if target + respond_to do |format| + format.html { redirect_to :back } + format.mobile { redirect_to stream_path } + format.json { render json: poll_participation, :status => 201 } + end + rescue ActiveRecord::RecordInvalid + respond_to do |format| + format.html { redirect_to :back } + format.mobile { redirect_to stream_path } + format.json { render :nothing => true, :status => 403 } + end + end + + private + + def target + @target ||= if params[:post_id] + current_user.find_visible_shareable_by_id(Post, params[:post_id]) || raise(ActiveRecord::RecordNotFound.new) + end + end +end \ No newline at end of file diff --git a/app/controllers/status_messages_controller.rb b/app/controllers/status_messages_controller.rb index 83f92f3ac..4ed6dd531 100644 --- a/app/controllers/status_messages_controller.rb +++ b/app/controllers/status_messages_controller.rb @@ -49,6 +49,14 @@ class StatusMessagesController < ApplicationController @status_message = current_user.build_post(:status_message, params[:status_message]) @status_message.build_location(:address => params[:location_address], :coordinates => params[:location_coords]) if params[:location_address].present? + if params[:poll_question].present? + @status_message.build_poll(:question => params[:poll_question]) + [*params[:poll_answers]].each do |poll_answer| + @status_message.poll.poll_answers.build(:answer => poll_answer) + end + end + + @status_message.attach_photos_by_ids(params[:photos]) if @status_message.save @@ -78,7 +86,7 @@ class StatusMessagesController < ApplicationController respond_to do |format| format.html { redirect_to :back } format.mobile { redirect_to stream_path } - format.json { render :nothing => true , :status => 403 } + format.json { render :nothing => true, :status => 403 } end end end @@ -117,4 +125,4 @@ class StatusMessagesController < ApplicationController def remove_getting_started current_user.disable_getting_started end -end +end \ No newline at end of file diff --git a/app/models/poll.rb b/app/models/poll.rb new file mode 100644 index 000000000..d43fcec77 --- /dev/null +++ b/app/models/poll.rb @@ -0,0 +1,40 @@ +class Poll < ActiveRecord::Base + include Diaspora::Federated::Base + include Diaspora::Guid + attr_accessible :question, :poll_answers + belongs_to :status_message + has_many :poll_answers + has_many :poll_participations + + xml_attr :question + xml_attr :poll_answers, :as => [PollAnswer] + + #forward some requests to status message, because a poll is just attached to a status message and is not sharable itself + delegate :author, :author_id, :public?, :subscribers, to: :status_message + + validate :enough_poll_answers + + self.include_root_in_json = false + + def enough_poll_answers + errors.add(:poll_answers, I18n.t("activerecord.errors.models.poll.attributes.poll_answers.not_enough_poll_answers")) if poll_answers.size < 2 + end + + def as_json(options={}) + { + :poll_id => self.id, + :post_id => self.status_message.id, + :question => self.question, + :poll_answers => self.poll_answers, + :participation_count => self.participation_count, + } + end + + def participation_count + poll_answers.sum("vote_count") + end + + def already_participated?(user) + poll_participations.where(:author_id => user.person.id).present? + end +end diff --git a/app/models/poll_answer.rb b/app/models/poll_answer.rb new file mode 100644 index 000000000..e3cc13269 --- /dev/null +++ b/app/models/poll_answer.rb @@ -0,0 +1,13 @@ +class PollAnswer < ActiveRecord::Base + + include Diaspora::Federated::Base + include Diaspora::Guid + + belongs_to :poll + has_many :poll_participations + + xml_attr :answer + + self.include_root_in_json = false + +end diff --git a/app/models/poll_participation.rb b/app/models/poll_participation.rb new file mode 100644 index 000000000..590a62830 --- /dev/null +++ b/app/models/poll_participation.rb @@ -0,0 +1,66 @@ +class PollParticipation < ActiveRecord::Base + + include Diaspora::Federated::Base + + include Diaspora::Guid + include Diaspora::Relayable + belongs_to :poll + belongs_to :poll_answer, counter_cache: :vote_count + belongs_to :author, :class_name => 'Person', :foreign_key => :author_id + xml_attr :diaspora_handle + xml_attr :poll_answer_guid + xml_convention :underscore + validate :not_already_participated + + def parent_class + Poll + end + + def parent + self.poll + end + + def poll_answer_guid + poll_answer.guid + end + + def poll_answer_guid= new_poll_answer_guid + self.poll_answer = PollAnswer.where(:guid => new_poll_answer_guid).first + end + + def parent= parent + self.poll = parent + end + + def diaspora_handle + self.author.diaspora_handle + end + + def diaspora_handle= nh + self.author = Webfinger.new(nh).fetch + end + + def not_already_participated + return if poll.nil? + + other_participations = PollParticipation.where(author_id: self.author.id, poll_id: self.poll.id).to_a-[self] + if other_participations.present? + self.errors.add(:poll, I18n.t("activerecord.errors.models.poll_participations.attributes.poll.already_participated")) + end + end + + class Generator < Federated::Generator + def self.federated_class + PollParticipation + end + + def initialize(person, target, poll_answer) + @poll_answer = poll_answer + super(person, target) + end + + def relayable_options + {:poll => @target.poll, :poll_answer => @poll_answer} + end + end +end diff --git a/app/models/post.rb b/app/models/post.rb index b2eda3d06..0cbc9f07d 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -72,6 +72,9 @@ class Post < ActiveRecord::Base def address end + def poll + end + def self.excluding_blocks(user) people = user.blocks.map{|b| b.person_id} scope = scoped diff --git a/app/models/status_message.rb b/app/models/status_message.rb index 4e0dfd127..6e73ecb48 100644 --- a/app/models/status_message.rb +++ b/app/models/status_message.rb @@ -20,10 +20,13 @@ class StatusMessage < Post xml_attr :raw_message xml_attr :photos, :as => [Photo] xml_attr :location, :as => Location + xml_attr :poll, :as => Poll has_many :photos, :dependent => :destroy, :foreign_key => :status_message_guid, :primary_key => :guid has_one :location + has_one :poll, autosave: true + # a StatusMessage is federated before its photos are so presence_of_content() fails erroneously if no text is present # therefore, we put the validation in a before_destory callback instead of a validation diff --git a/app/models/user/social_actions.rb b/app/models/user/social_actions.rb index 6bb547f40..ccc30b484 100644 --- a/app/models/user/social_actions.rb +++ b/app/models/user/social_actions.rb @@ -13,6 +13,11 @@ module User::SocialActions Like::Generator.new(self, target).create!(opts) end + def participate_in_poll!(target, answer, opts={}) + find_or_create_participation!(target) + PollParticipation::Generator.new(self, target, answer).create!(opts) + end + def reshare!(target, opts={}) find_or_create_participation!(target) reshare = build_post(:reshare, :root_guid => target.guid) diff --git a/app/presenters/post_presenter.rb b/app/presenters/post_presenter.rb index fa179250a..d797da86c 100644 --- a/app/presenters/post_presenter.rb +++ b/app/presenters/post_presenter.rb @@ -35,6 +35,8 @@ class PostPresenter :root => root, :title => title, :address => @post.address, + :poll => @post.poll(), + :already_participated_in_poll => already_participated_in_poll, :interactions => { :likes => [user_like].compact, @@ -72,6 +74,14 @@ class PostPresenter @current_user.present? end + private + + def already_participated_in_poll + if @post.poll + @post.poll.already_participated?(current_user) + end + end + end class PostInteractionPresenter diff --git a/app/views/publisher/_publisher.mobile.haml b/app/views/publisher/_publisher.mobile.haml index a1556a32c..dca0f8cf2 100644 --- a/app/views/publisher/_publisher.mobile.haml +++ b/app/views/publisher/_publisher.mobile.haml @@ -39,4 +39,4 @@ #publisher_mobile = submit_tag t('shared.publisher.share'), :class => 'btn primary', :id => "submit_new_message" - #publisher_photo_upload + #publisher_photo_upload \ No newline at end of file diff --git a/app/views/publisher/_publisher_blueprint.html.haml b/app/views/publisher/_publisher_blueprint.html.haml index b99d55958..75918f3fe 100644 --- a/app/views/publisher/_publisher_blueprint.html.haml +++ b/app/views/publisher/_publisher_blueprint.html.haml @@ -28,12 +28,26 @@ %span#publisher-images %span.markdownIndications != t('shared.publisher.formatWithMarkdown', markdown_link: link_to(t('help.markdown'), 'https://diasporafoundation.org/formatting', target: :blank)) + #poll_creator.btn{:title => t('shared.publisher.poll.add_a_poll')} + %i.entypo.bar-graph{:class => "publisher_image"} + #locator.btn{:title => t('shared.publisher.get_location')} = image_tag 'icons/marker.png', :alt => t('shared.publisher.get_location').titleize, :class => 'publisher_image' #file-upload.btn{:title => t('shared.publisher.upload_photos')} = image_tag 'icons/camera.png', :alt => t('shared.publisher.upload_photos').titleize, :class => 'publisher_image' = hidden_field :location, :coords #location_container + #poll_creator_wrapper + #poll_question_wrapper + %input{:id => 'poll_question', :placeholder => t('shared.publisher.poll.question'), :name => 'poll_question'} + .poll_answer + %span{:class => 'poll_answer_input_wrapper'} + %input{:class => 'poll_answer_input', :placeholder => t('shared.publisher.poll.option'), :name => 'poll_answers[]'} + %a{:class => 'remove_poll_answer', :title => t('shared.publisher.poll.remove_poll_answer')} + .icons-deletelabel + #add_poll_answer_wrapper + #add_poll_answer{:class => 'button creation'} + = t('shared.publisher.poll.add_poll_answer') - if publisher_public = hidden_field_tag 'aspect_ids[]', "public" diff --git a/app/views/publisher/_publisher_bootstrap.html.haml b/app/views/publisher/_publisher_bootstrap.html.haml index 9df147931..a35f22d36 100644 --- a/app/views/publisher/_publisher_bootstrap.html.haml +++ b/app/views/publisher/_publisher_bootstrap.html.haml @@ -22,8 +22,20 @@ %ul#photodropzone .row-fluid#location_container = hidden_field :location, :coords + .row-fluid#poll_creator_wrapper + #poll_question_wrapper{:class => "input-block-level"} + %input{:id => 'poll_question', :placeholder => t('shared.publisher.poll.question'), :name => 'poll_question', :class=> "form-control"} + .poll_answer + %input{:class => 'form-control poll_answer_input', :placeholder => t('shared.publisher.poll.option'), :name => 'poll_answers[]'} + .remove_poll_answer.btn.btn-link{:title => t('shared.publisher.poll.remove_poll_answer')} + %i.entypo.trash + #add_poll_answer_wrapper + #add_poll_answer{:class => 'btn btn-default'} + = t('shared.publisher.poll.add_poll_answer') .row-fluid#button_container #publisher-images.pull-right + #poll_creator.btn.btn-link{:title => t('shared.publisher.poll.add_a_poll')} + %i.entypo.bar-graph #file-upload.btn.btn-link{:title => t('shared.publisher.upload_photos')} %i.entypo.camera.publisher_image #locator.btn.btn-link{:title => t('shared.publisher.get_location')} diff --git a/config/locales/diaspora/en-AU.yml b/config/locales/diaspora/en-AU.yml index 7713db61b..f34d29a1f 100644 --- a/config/locales/diaspora/en-AU.yml +++ b/config/locales/diaspora/en-AU.yml @@ -26,3 +26,11 @@ attributes: from_id: taken: "is a duplicate of a pre-existing request." + poll: + attributes: + poll_answers: + not_enough_poll_answers: "Not enough poll options provided." + poll_participation: + attributes: + poll: + already_participated: "You've already participated in this poll!" diff --git a/config/locales/diaspora/en-GB.yml b/config/locales/diaspora/en-GB.yml index afd094c31..d6ae6e10e 100644 --- a/config/locales/diaspora/en-GB.yml +++ b/config/locales/diaspora/en-GB.yml @@ -26,3 +26,11 @@ attributes: from_id: taken: "is a duplicate of a pre-existing request." + poll: + attributes: + poll_answers: + not_enough_poll_answers: "Not enough poll options provided." + poll_participation: + attributes: + poll: + already_participated: "You've already participated in this poll!" diff --git a/config/locales/diaspora/en-US.yml b/config/locales/diaspora/en-US.yml index e2a634aa0..c527a04bb 100644 --- a/config/locales/diaspora/en-US.yml +++ b/config/locales/diaspora/en-US.yml @@ -26,4 +26,11 @@ attributes: from_id: taken: "is a duplicate of a pre-existing request." - + poll: + attributes: + poll_answers: + not_enough_poll_answers: "Not enough poll options provided." + poll_participation: + attributes: + poll: + already_participated: "You've already participated in this poll!" diff --git a/config/locales/diaspora/en.yml b/config/locales/diaspora/en.yml index 08424ab0c..ed21bf4c9 100644 --- a/config/locales/diaspora/en.yml +++ b/config/locales/diaspora/en.yml @@ -77,6 +77,14 @@ en: attributes: root_guid: taken: "That good, huh? You've already reshared that post!" + poll: + attributes: + poll_answers: + not_enough_poll_answers: "Not enough poll options provided." + poll_participation: + attributes: + poll: + already_participated: "You've already participated in this poll!" error_messages: helper: invalid_fields: "Invalid Fields" @@ -1037,6 +1045,12 @@ en: hello: "Hey everyone, I'm #%{new_user_tag}. " i_like: "I'm interested in %{tags}. " invited_by: "Thanks for the invite, " + poll: + remove_poll_answer: "Remove option" + add_poll_answer: "Add option" + add_a_poll: "Add a poll" + question: "Question" + option: "Option 1" add_contact: enter_a_diaspora_username: "Enter a diaspora* username:" your_diaspora_username_is: "Your diaspora* username is: %{diaspora_handle}" diff --git a/config/locales/javascript/javascript.en.yml b/config/locales/javascript/javascript.en.yml index 0cf179ba4..91527ffc1 100644 --- a/config/locales/javascript/javascript.en.yml +++ b/config/locales/javascript/javascript.en.yml @@ -172,3 +172,12 @@ en: reshared: "Reshared" comment: "Comment" home: "HOME" + + poll: + vote: "Vote" + result: "Result" + count: + one: "1 vote so far" + other: "<%=count%> votes so far" + show_result: "Show result" + close_result: "Hide result" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 38405a721..39b66432e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -31,11 +31,15 @@ Diaspora::Application.routes.draw do get :interactions end + resources :poll_participations, :only => [:create] + resources :likes, :only => [:create, :destroy, :index ] resources :participations, :only => [:create, :destroy, :index] resources :comments, :only => [:new, :create, :destroy, :index] end + + get 'p/:id' => 'posts#show', :as => 'short_post' get 'posts/:id/iframe' => 'posts#iframe', :as => 'iframe' diff --git a/db/migrate/20140308154022_create_polls.rb b/db/migrate/20140308154022_create_polls.rb new file mode 100644 index 000000000..f2d84ba6a --- /dev/null +++ b/db/migrate/20140308154022_create_polls.rb @@ -0,0 +1,38 @@ +class CreatePolls < ActiveRecord::Migration + def up + create_table :polls do |t| + t.string :question, :null => false + t.belongs_to :status_message, :null => false + t.boolean :status + t.string :guid + t.timestamps + end + add_index :polls, :status_message_id + + create_table :poll_answers do |t| + t.string :answer, :null => false + t.belongs_to :poll, :null => false + t.string :guid + t.integer :vote_count, :default => 0 + end + add_index :poll_answers, :poll_id + + create_table :poll_participations do |t| + t.belongs_to :poll_answer, :null => false + t.belongs_to :author, :null => false + t.belongs_to :poll, :null => false + t.string :guid + t.text :author_signature + t.text :parent_author_signature + + t.timestamps + end + add_index :poll_participations, :poll_id + end + + def down + drop_table :polls + drop_table :poll_answers + drop_table :poll_participations + end +end diff --git a/db/schema.rb b/db/schema.rb index 60fab83be..97f7cb6c2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140222162826) do +ActiveRecord::Schema.define(:version => 20140308154022) do create_table "account_deletions", :force => true do |t| t.string "diaspora_handle" @@ -283,6 +283,39 @@ ActiveRecord::Schema.define(:version => 20140222162826) do t.datetime "updated_at", :null => false end + create_table "poll_answers", :force => true do |t| + t.string "answer", :null => false + t.integer "poll_id", :null => false + t.string "guid" + t.integer "vote_count", :default => 0 + end + + add_index "poll_answers", ["poll_id"], :name => "index_poll_answers_on_poll_id" + + create_table "poll_participations", :force => true do |t| + t.integer "poll_answer_id", :null => false + t.integer "author_id", :null => false + t.integer "poll_id", :null => false + t.string "guid" + t.text "author_signature" + t.text "parent_author_signature" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "poll_participations", ["poll_id"], :name => "index_poll_participations_on_poll_id" + + create_table "polls", :force => true do |t| + t.string "question", :null => false + t.integer "status_message_id", :null => false + t.boolean "status" + t.string "guid" + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + + add_index "polls", ["status_message_id"], :name => "index_polls_on_status_message_id" + create_table "post_reports", :force => true do |t| t.integer "post_id", :null => false t.string "user_id" @@ -502,36 +535,36 @@ ActiveRecord::Schema.define(:version => 20140222162826) do add_index "users", ["invitation_token"], :name => "index_users_on_invitation_token" add_index "users", ["username"], :name => "index_users_on_username", :unique => true - add_foreign_key "aspect_memberships", "aspects", :name => "aspect_memberships_aspect_id_fk", :dependent => :delete - add_foreign_key "aspect_memberships", "contacts", :name => "aspect_memberships_contact_id_fk", :dependent => :delete + add_foreign_key "aspect_memberships", "aspects", name: "aspect_memberships_aspect_id_fk", dependent: :delete + add_foreign_key "aspect_memberships", "contacts", name: "aspect_memberships_contact_id_fk", dependent: :delete - add_foreign_key "aspect_visibilities", "aspects", :name => "aspect_visibilities_aspect_id_fk", :dependent => :delete + add_foreign_key "aspect_visibilities", "aspects", name: "aspect_visibilities_aspect_id_fk", dependent: :delete - add_foreign_key "comments", "people", :name => "comments_author_id_fk", :column => "author_id", :dependent => :delete + add_foreign_key "comments", "people", name: "comments_author_id_fk", column: "author_id", dependent: :delete - add_foreign_key "contacts", "people", :name => "contacts_person_id_fk", :dependent => :delete + add_foreign_key "contacts", "people", name: "contacts_person_id_fk", dependent: :delete - add_foreign_key "conversation_visibilities", "conversations", :name => "conversation_visibilities_conversation_id_fk", :dependent => :delete - add_foreign_key "conversation_visibilities", "people", :name => "conversation_visibilities_person_id_fk", :dependent => :delete + add_foreign_key "conversation_visibilities", "conversations", name: "conversation_visibilities_conversation_id_fk", dependent: :delete + add_foreign_key "conversation_visibilities", "people", name: "conversation_visibilities_person_id_fk", dependent: :delete - add_foreign_key "conversations", "people", :name => "conversations_author_id_fk", :column => "author_id", :dependent => :delete + add_foreign_key "conversations", "people", name: "conversations_author_id_fk", column: "author_id", dependent: :delete - add_foreign_key "invitations", "users", :name => "invitations_recipient_id_fk", :column => "recipient_id", :dependent => :delete - add_foreign_key "invitations", "users", :name => "invitations_sender_id_fk", :column => "sender_id", :dependent => :delete + add_foreign_key "invitations", "users", name: "invitations_recipient_id_fk", column: "recipient_id", dependent: :delete + add_foreign_key "invitations", "users", name: "invitations_sender_id_fk", column: "sender_id", dependent: :delete - add_foreign_key "likes", "people", :name => "likes_author_id_fk", :column => "author_id", :dependent => :delete + add_foreign_key "likes", "people", name: "likes_author_id_fk", column: "author_id", dependent: :delete - add_foreign_key "messages", "conversations", :name => "messages_conversation_id_fk", :dependent => :delete - add_foreign_key "messages", "people", :name => "messages_author_id_fk", :column => "author_id", :dependent => :delete + add_foreign_key "messages", "conversations", name: "messages_conversation_id_fk", dependent: :delete + add_foreign_key "messages", "people", name: "messages_author_id_fk", column: "author_id", dependent: :delete - add_foreign_key "notification_actors", "notifications", :name => "notification_actors_notification_id_fk", :dependent => :delete + add_foreign_key "notification_actors", "notifications", name: "notification_actors_notification_id_fk", dependent: :delete - add_foreign_key "posts", "people", :name => "posts_author_id_fk", :column => "author_id", :dependent => :delete + add_foreign_key "posts", "people", name: "posts_author_id_fk", column: "author_id", dependent: :delete - add_foreign_key "profiles", "people", :name => "profiles_person_id_fk", :dependent => :delete + add_foreign_key "profiles", "people", name: "profiles_person_id_fk", dependent: :delete - add_foreign_key "services", "users", :name => "services_user_id_fk", :dependent => :delete + add_foreign_key "services", "users", name: "services_user_id_fk", dependent: :delete - add_foreign_key "share_visibilities", "contacts", :name => "post_visibilities_contact_id_fk", :dependent => :delete + add_foreign_key "share_visibilities", "contacts", name: "post_visibilities_contact_id_fk", dependent: :delete end diff --git a/features/desktop/post_with_a_poll.feature b/features/desktop/post_with_a_poll.feature new file mode 100644 index 000000000..5d8b7b277 --- /dev/null +++ b/features/desktop/post_with_a_poll.feature @@ -0,0 +1,65 @@ +@javascript +Feature: posting with a poll + + Background: + Given following users exist: + | username | + | bob | + And I sign in as "bob@bob.bob" + And I am on the home page + + Scenario: expanding the publisher + Given "#poll_creator_wrapper" is hidden + When I expand the publisher + Then I should see an element "#poll_creator" + + Scenario: expanding the poll creator + Given "#poll_creator_wrapper" is hidden + When I expand the publisher + And I press the element "#poll_creator" + Then I should see an element "#poll_creator_wrapper" + + Scenario: adding option to poll + Given "#poll_creator_wrapper" is hidden + When I expand the publisher + And I press the element "#poll_creator" + And I press the element "#add_poll_answer" + Then I should see 3 options + + Scenario: delete an option + Given "#poll_creator_wrapper" is hidden + When I expand the publisher + And I press the element "#poll_creator" + And I delete the first option + Then I should see 1 option + And I should not see a remove icon + + Scenario: post with an attached poll + Given I expand the publisher + And I press the element "#poll_creator" + When I fill in the following: + | status_message_fake_text | I am eating yogurt | + | poll_question | What kind of yogurt do you like? | + And I fill in the following for the options: + | normal | + | not normal | + And I press "Share" + Then I should see a ".poll_form" within ".stream_element" + And I should see a "form" within ".stream_element" + + Scenario: vote for an option + Given I expand the publisher + And I press the element "#poll_creator" + When I fill in the following: + | status_message_fake_text | I am eating yogurt | + | poll_question | What kind of yogurt do you like? | + And I fill in the following for the options: + | normal | + | not normal | + And I press "Share" + + And I check the first option + And I press "Vote" within ".stream_element" + Then I should see an element ".poll_progress_bar" + And I should see an element ".percentage" + And I should see "1 vote so far" within ".poll_statistic" \ No newline at end of file diff --git a/features/step_definitions/post_with_poll_steps.rb b/features/step_definitions/post_with_poll_steps.rb new file mode 100644 index 000000000..2ae5a78ba --- /dev/null +++ b/features/step_definitions/post_with_poll_steps.rb @@ -0,0 +1,32 @@ +Then /^I should see ([1-9]+) options?$/ do |number| + find("#poll_creator_wrapper").all(".poll_answer").count.should eql(number.to_i) +end + +And /^I delete the first option$/ do + find("#poll_creator_wrapper").all(".poll_answer .remove_poll_answer").first.click +end + +And /^I should not see a remove icon$/ do + page.should_not have_css(".remove_poll_answer") +end + +When /^I fill in the following for the options:$/ do |table| + i = 0 + table.raw.flatten.each do |value| + all(".poll_answer_input")[i].set(value) + i+=1 + end +end + +When /^I check the first option$/ do + sleep 1 + first(".poll_form input").click +end + +And /^I press the element "([^"]*)"$/ do |selector| + find(selector).click +end + +Then /^I should see an element "([^"]*)"$/ do |selector| + page.should have_css(selector) +end \ No newline at end of file diff --git a/lib/federated/generator.rb b/lib/federated/generator.rb index 96fe9006c..ab6025ef2 100644 --- a/lib/federated/generator.rb +++ b/lib/federated/generator.rb @@ -11,8 +11,6 @@ module Federated FEDERATION_LOGGER.info("user:#{@user.id} dispatching #{relayable.class}:#{relayable.guid}") Postzord::Dispatcher.defer_build_and_post(@user, relayable) relayable - else - false end end diff --git a/spec/factories.rb b/spec/factories.rb index fcede7e1b..ac040e185 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -92,6 +92,12 @@ FactoryGirl.define do end end + factory(:status_message_with_poll, :parent => :status_message) do + after(:build) do |sm| + FactoryGirl.create(:poll, :status_message => sm) + end + end + factory(:status_message_with_photo, :parent => :status_message) do sequence(:text) { |n| "There are #{n} ninjas in this photo." } after(:build) do |sm| @@ -107,6 +113,18 @@ FactoryGirl.define do end end + factory(:poll) do + sequence(:question) { |n| "What do you think about #{n} ninjas?" } + after(:build) do |p| + p.poll_answers << FactoryGirl.build(:poll_answer) + p.poll_answers << FactoryGirl.build(:poll_answer) + end + end + + factory(:poll_answer) do + sequence(:answer) { |n| "#{n} questionmarks" } + end + factory(:photo) do sequence(:random_string) {|n| SecureRandom.hex(10) } association :author, :factory => :person diff --git a/spec/javascripts/app/views/poll_view_spec.js b/spec/javascripts/app/views/poll_view_spec.js new file mode 100644 index 000000000..4ee1a2ef8 --- /dev/null +++ b/spec/javascripts/app/views/poll_view_spec.js @@ -0,0 +1,45 @@ +describe("app.views.Poll", function(){ + beforeEach(function() { + loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}}); + this.view = new app.views.Poll({ "model" : factory.postWithPoll()}); + this.view.render(); + }); + + describe("setProgressBar", function(){ + it("sets the progress bar according to the voting result", function(){ + var percentage = (this.view.poll.poll_answers[0].vote_count / this.view.poll.participation_count)*100; + expect(this.view.$('.poll_progress_bar:first').css('width')).toBe(this.view.progressBarFactor * percentage+"px"); + expect(this.view.$(".percentage:first").text()).toBe(" - " + percentage + "%"); + }) + }); + + describe("toggleResult", function(){ + it("toggles the progress bar and result", function(){ + expect(this.view.$('.poll_progress_bar_wrapper:first').css('display')).toBe("none"); + this.view.toggleResult(null); + expect(this.view.$('.poll_progress_bar_wrapper:first').css('display')).toBe("block"); + }) + }); + + describe("updateCounter", function(){ + it("updates the counter after a vote", function(){ + var pc = this.view.poll.participation_count; + var answerCount = this.view.poll.poll_answers[0].vote_count; + this.view.updateCounter(1); + expect(this.view.poll.participation_count).toBe(pc+1); + expect(this.view.poll.poll_answers[0].vote_count).toBe(answerCount+1); + }) + }); + + describe("vote", function(){ + it("checks the ajax call for voting", function(){ + spyOn($, "ajax"); + var radio = this.view.$('input[name="vote"]:first'); + radio.attr('checked', true); + this.view.vote({'target' : radio}); + var obj = JSON.parse($.ajax.mostRecentCall.args[0].data); + expect(obj.poll_id).toBe(this.view.poll.poll_id); + expect(obj.poll_answer_id).toBe(this.view.poll.poll_answers[0].id); + }) + }) +}); diff --git a/spec/javascripts/app/views/publisher_view_spec.js b/spec/javascripts/app/views/publisher_view_spec.js index fb753f01f..4d7a809ad 100644 --- a/spec/javascripts/app/views/publisher_view_spec.js +++ b/spec/javascripts/app/views/publisher_view_spec.js @@ -277,6 +277,40 @@ describe("app.views.Publisher", function() { }); + context("poll", function(){ + beforeEach(function() { + loginAs({name: "alice", avatar : {small : "http://avatar.com/photo.jpg"}}); + spec.loadFixture("aspects_index"); + $("#poll_creator_wrapper").hide(); //css not loaded? :-/ + this.view = new app.views.Publisher(); + }); + + describe('#showPollCreator', function(){ + it("Shows the poll creator", function(){ + expect($("#poll_creator_wrapper").is(":visible")).toBe(false); + this.view.showPollCreator(); + expect($("#poll_creator_wrapper").is(":visible")).toBe(true); + }) + }); + + describe("#addPollAnswer", function(){ + it("should add a poll answer if clicked", function(){ + expect($("#poll_creator_wrapper .poll_answer").length).toBe(2); + this.view.addPollAnswer(); + expect($("#poll_creator_wrapper .poll_answer").length).toBe(3); + }) + }); + + describe("#removePollAnswer", function(){ + it("should remove a poll answer if clicked", function(){ + var answer_count = $('.poll_answer').length; + var evt = {'currentTarget' : $("#poll_creator_wrapper .poll_answer:first .remove_poll_answer")}; + this.view.removePollAnswer(evt); + expect($("#poll_creator_wrapper .poll_answer").length).toBe(answer_count-1); + }) + }); + }); + context("locator", function() { beforeEach(function() { // should be jasmine helper diff --git a/spec/javascripts/helpers/factory.js b/spec/javascripts/helpers/factory.js index a3c8692ac..b5dd63068 100644 --- a/spec/javascripts/helpers/factory.js +++ b/spec/javascripts/helpers/factory.js @@ -124,11 +124,31 @@ factory = { return new app.models.Post(_.extend(defaultAttrs, overrides)) }, + postWithPoll : function(overrides) { + defaultAttrs = _.extend(factory.postAttrs(), {"author" : this.author()}); + defaultAttrs = _.extend(defaultAttrs, {"already_participated_in_poll" : false}); + defaultAttrs = _.extend(defaultAttrs, {"poll" : factory.poll()}); + return new app.models.Post(_.extend(defaultAttrs, overrides)); + }, + statusMessage : function(overrides){ //intentionally doesn't have an author to mirror creation process, maybe we should change the creation process return new app.models.StatusMessage(_.extend(factory.postAttrs(), overrides)) }, + poll: function(overrides){ + return { + "question" : "This is an awesome question", + "created_at" : "2012-01-03T19:53:13Z", + "author" : this.author(), + "post_id" : 1, + "poll_answers" : [{"answer" : "yes", "id" : 1, "vote_count" : 9}, {"answer" : "no", "id" : 2, "vote_count" : 1}], + "guid" : this.guid(), + "poll_id": this.id.next(), + "participation_count" : 10 + } + }, + comment: function(overrides) { var defaultAttrs = { "text" : "This is an awesome comment!", diff --git a/spec/models/poll_answer_spec.rb b/spec/models/poll_answer_spec.rb new file mode 100644 index 000000000..aa53d5261 --- /dev/null +++ b/spec/models/poll_answer_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe PollAnswer do + before do + @status = FactoryGirl.create(:status_message_with_poll) + @user = alice + @answer = @status.poll.poll_answers.first + end + + describe 'counter cache' do + it 'increments the counter cache on the answer' do + lambda { + alice.participate_in_poll!(@status, @answer) + }.should change{ + @answer.reload.vote_count + }.by(1) + end + + end + +end \ No newline at end of file diff --git a/spec/models/poll_participation_spec.rb b/spec/models/poll_participation_spec.rb new file mode 100644 index 000000000..13a9e0f63 --- /dev/null +++ b/spec/models/poll_participation_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' +require Rails.root.join("spec", "shared_behaviors", "relayable") + +describe PollParticipation do + + before do + @alices_aspect = alice.aspects.first + @status = bob.post(:status_message, :text => "hello", :to => bob.aspects.first.id) + @poll = Poll.new(:question => 'Who is in charge?') + @poll.poll_answers.build(:answer => "a") + @poll.poll_answers.build(:answer => "b") + @status.poll = @poll + end + + describe 'validation' do + it 'forbids multiple participations in the same poll' do + expect { + 2.times do |run| + bob.participate_in_poll!(@status, @poll.poll_answers.first) + end + }.to raise_error + end + + it 'allows a one time participation in a poll' do + expect { + bob.participate_in_poll!(@status, @poll.poll_answers.first) + }.to_not raise_error + end + + end + + describe 'xml' do + before do + @poll_participant = FactoryGirl.create(:user) + @poll_participant_aspect = @poll_participant.aspects.create(:name => "bruisers") + connect_users(alice, @alices_aspect, @poll_participant, @poll_participant_aspect) + @poll = Poll.new(:question => "hi") + @poll.poll_answers.build(:answer => "a") + @poll.poll_answers.build(:answer => "b") + @post = alice.post :status_message, :text => "hello", :to => @alices_aspect.id + @post.poll = @poll + @poll_participation = @poll_participant.participate_in_poll!(@post, @poll.poll_answers.first) + @xml = @poll_participation.to_xml.to_s + end + + it 'serializes the class name' do + @xml.include?(PollParticipation.name.underscore.to_s).should be_true + end + + it 'serializes the sender handle' do + @xml.include?(@poll_participation.diaspora_handle).should be_true + end + + it 'serializes the poll_guid' do + @xml.should include(@poll.guid) + end + + it 'serializes the poll_answer_guid' do + @xml.should include(@poll_participation.poll_answer.guid) + end + + describe 'marshalling' do + before do + @marshalled_poll_participation = PollParticipation.from_xml(@xml) + end + + it 'marshals the author' do + @marshalled_poll_participation.author.should == @poll_participant.person + end + + it 'marshals the answer' do + @marshalled_poll_participation.poll_answer.should == @poll_participation.poll_answer + end + + it 'marshals the poll' do + @marshalled_poll_participation.poll.should == @poll + end + end + end + + describe 'it is relayable' do + before do + @local_luke, @local_leia, @remote_raphael = set_up_friends + @remote_parent = FactoryGirl.build(:status_message_with_poll, :author => @remote_raphael) + + @local_parent = @local_luke.post :status_message, :text => "hi", :to => @local_luke.aspects.first + @poll2 = Poll.new(:question => 'Who is now in charge?') + @poll2.poll_answers.build(:answer => "a") + @poll2.poll_answers.build(:answer => "b") + @local_parent.poll = @poll2 + + @object_by_parent_author = @local_luke.participate_in_poll!(@local_parent, @poll2.poll_answers.first) + @object_by_recipient = @local_leia.participate_in_poll!(@local_parent, @poll2.poll_answers.first) + @dup_object_by_parent_author = @object_by_parent_author.dup + + @object_on_remote_parent = @local_luke.participate_in_poll!(@remote_parent, @remote_parent.poll.poll_answers.first) + end + + let(:build_object) { PollParticipation::Generator.new(alice, @status, @poll.poll_answers.first).build } + it_should_behave_like 'it is relayable' + end +end diff --git a/spec/models/poll_spec.rb b/spec/models/poll_spec.rb new file mode 100644 index 000000000..c94c08e33 --- /dev/null +++ b/spec/models/poll_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Poll do + before do + @poll = Poll.new(:question => "What do you think about apples?") + end + + describe 'validation' do + it 'should not create a poll when it has less than two answers' do + @poll.poll_answers.build(:answer => '1') + @poll.should_not be_valid + end + + it 'should create a poll when it has more than two answers' do + @poll.poll_answers.build(:answer => '1') + @poll.poll_answers.build(:answer => '2') + @poll.should be_valid + end + end +end \ No newline at end of file diff --git a/spec/models/status_message_spec.rb b/spec/models/status_message_spec.rb index 7ee8b4bc5..297a3f269 100644 --- a/spec/models/status_message_spec.rb +++ b/spec/models/status_message_spec.rb @@ -350,6 +350,35 @@ STR end end end + + context 'with a poll' do + before do + @message.poll = FactoryGirl.create(:poll, :status_message => @message) + @xml = @message.to_xml.to_s + end + + it 'serializes the poll' do + @xml.should include "poll" + @xml.should include "question" + @xml.should include "poll_answer" + end + + describe ".from_xml" do + before do + @marshalled = StatusMessage.from_xml(@xml) + end + + it 'marshals the poll' do + @marshalled.poll.should be_present + end + + it 'marshals the poll answers' do + @marshalled.poll.poll_answers.size.should == 2 + end + end + end + + end describe '#after_dispatch' do diff --git a/spec/models/user/social_actions_spec.rb b/spec/models/user/social_actions_spec.rb index 33c99e675..de486010d 100644 --- a/spec/models/user/social_actions_spec.rb +++ b/spec/models/user/social_actions_spec.rb @@ -16,7 +16,7 @@ describe User::SocialActions do alice.participations.last.target.should == @status end - it "creates the like" do + it "creates the comment" do lambda{ alice.comment!(@status, "bro") }.should change(Comment, :count).by(1) end @@ -83,4 +83,31 @@ describe User::SocialActions do @status.reload.likes.should == likes end end + + describe 'User#participate_in_poll!' do + before do + @bobs_aspect = bob.aspects.where(:name => "generic").first + @status = bob.post(:status_message, :text => "hello", :to => @bobs_aspect.id) + @poll = FactoryGirl.create(:poll, :status_message => @status) + @answer = @poll.poll_answers.first + end + + it "federates" do + Participation::Generator.any_instance.stub(:create!) + Postzord::Dispatcher.should_receive(:defer_build_and_post) + alice.participate_in_poll!(@status, @answer) + end + + it "creates a partcipation" do + lambda{ alice.participate_in_poll!(@status, @answer) }.should change(Participation, :count).by(1) + end + + it "creates the poll participation" do + lambda{ alice.participate_in_poll!(@status, @answer) }.should change(PollParticipation, :count).by(1) + end + + it "sets the poll answer id" do + alice.participate_in_poll!(@status, @answer).poll_answer.should == @answer + end + end end \ No newline at end of file