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}}
+
+{{/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