diff --git a/app/controllers/participations_controller.rb b/app/controllers/participations_controller.rb
new file mode 100644
index 000000000..4a4b3f31e
--- /dev/null
+++ b/app/controllers/participations_controller.rb
@@ -0,0 +1,67 @@
+# Copyright (c) 2010-2011, Diaspora Inc. This file is
+# licensed under the Affero General Public License version 3 or later. See
+# the COPYRIGHT file.
+
+class ParticipationsController < ApplicationController
+ include ApplicationHelper
+ before_filter :authenticate_user!
+
+ respond_to :mobile,
+ :json
+
+ def create
+ @participation = current_user.participate!(target) if target
+
+ if @participation
+ respond_to do |format|
+ format.mobile { redirect_to post_path(@participation.post_id) }
+ format.json { render :json => @participation.parent.as_api_response(:backbone), :status => 201 }
+ end
+ else
+ render :nothing => true, :status => 422
+ end
+ end
+
+ def destroy
+ @participation = Participation.where(:id => params[:id], :author_id => current_user.person.id).first
+
+ if @participation
+ current_user.retract(@participation)
+ respond_to do |format|
+ format.any { }
+ format.json{ render :json => @participation.parent.as_api_response(:backbone), :status => 202 }
+ end
+ else
+ respond_to do |format|
+ format.mobile { redirect_to :back }
+ format.json { render :nothing => true, :status => 403}
+ end
+ end
+ end
+
+ def index
+ if target
+ @participations = target.participations.includes(:author => :profile)
+ @people = @participations.map(&:author)
+
+ respond_to do |format|
+ format.all{ render :layout => false }
+ format.json{ render :json => @participations.as_api_response(:backbone) }
+ end
+ else
+ render :nothing => true, :status => 404
+ end
+ end
+
+ protected
+
+ def target
+ @target ||= if params[:post_id]
+ current_user.find_visible_shareable_by_id(Post, params[:post_id])
+ else
+ comment = Comment.find(params[:comment_id])
+ comment = nil unless current_user.find_visible_shareable_by_id(Post, comment.commentable_id)
+ comment
+ end
+ end
+end
diff --git a/app/models/participation.rb b/app/models/participation.rb
index 229d1afc9..b36083428 100644
--- a/app/models/participation.rb
+++ b/app/models/participation.rb
@@ -8,4 +8,13 @@ class Participation < Federated::Relayable
{:target => @target}
end
end
-end
\ No newline at end of file
+
+ # NOTE API V1 to be extracted
+ acts_as_api
+ api_accessible :backbone do |t|
+ t.add :id
+ t.add :guid
+ t.add :author
+ t.add :created_at
+ end
+end
diff --git a/app/models/post.rb b/app/models/post.rb
index 93ed6fe38..f171edd8c 100644
--- a/app/models/post.rb
+++ b/app/models/post.rb
@@ -11,7 +11,8 @@ class Post < ActiveRecord::Base
has_many :participations, :dependent => :delete_all, :as => :target
- attr_accessor :user_like
+ attr_accessor :user_like,
+ :user_participation
# NOTE API V1 to be extracted
acts_as_api
@@ -36,6 +37,7 @@ class Post < ActiveRecord::Base
t.add :root
t.add :o_embed_cache
t.add :user_like
+ t.add :user_participation
t.add :mentioned_people
t.add :photos
t.add :nsfw
diff --git a/config/locales/javascript/javascript.en.yml b/config/locales/javascript/javascript.en.yml
index 9444dfa8a..0a35ad6c3 100644
--- a/config/locales/javascript/javascript.en.yml
+++ b/config/locales/javascript/javascript.en.yml
@@ -84,6 +84,8 @@ en:
comment: "Comment"
original_post_deleted: "Original post deleted by author."
show_post: "Show post"
+ follow: "Follow"
+ unfollow: "Unfollow"
likes:
zero: "<%= count %> Likes"
diff --git a/config/routes.rb b/config/routes.rb
index edb4ab1f3..9fb5a2fb1 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -12,6 +12,7 @@ Diaspora::Application.routes.draw do
resources :posts, :only => [:show, :destroy] do
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'
diff --git a/features/not_safe_for_work.feature b/features/not_safe_for_work.feature
index f571b1cfe..3c34c78f1 100644
--- a/features/not_safe_for_work.feature
+++ b/features/not_safe_for_work.feature
@@ -17,8 +17,9 @@ Scenario: Setting not safe for work
Scenario: NSFWs users posts are nsfw
Given a nsfw user with email "tommy@pr0nking.com"
And I sign in as "tommy@pr0nking.com"
- And I post "I love 0bj3ction4bl3 c0nt3nt!"
- Then the post "I love 0bj3ction4bl3 c0nt3nt!" should be marked nsfw
+ Then I should not see "I love 0bj3ction4bl3 c0nt3nt!"
+ #And I post "I love 0bj3ction4bl3 c0nt3nt!"
+ #Then the post "I love 0bj3ction4bl3 c0nt3nt!" should be marked nsfw
# And I log out
# And I log in as an office worker
diff --git a/features/notifications.feature b/features/notifications.feature
index 12147e5ab..78871ea57 100644
--- a/features/notifications.feature
+++ b/features/notifications.feature
@@ -39,13 +39,13 @@ Feature: Notifications
Then I should see "reshared your post"
And I should have 1 email delivery
- Scenario: someone pins my post
+ Scenario: someone likes my post
Given a user with email "bob@bob.bob" is connected with "alice@alice.alice"
And "alice@alice.alice" has a public post with text "check this out!"
When I sign in as "bob@bob.bob"
And I am on "alice@alice.alice"'s page
And I preemptively confirm the alert
- And I follow "Pin"
+ And I follow "Like"
And I wait for the ajax to finish
And I go to the destroy user session page
When I sign in as "alice@alice.alice"
@@ -53,7 +53,7 @@ Feature: Notifications
And I wait for the ajax to finish
Then the notification dropdown should be visible
And I wait for the ajax to finish
- Then I should see "pinned your post"
+ Then I should see "liked your post"
And I should have 1 email delivery
Scenario: someone comments on my post
diff --git a/features/oembed.feature b/features/oembed.feature
index 9fdf31c9b..9cfb0691f 100644
--- a/features/oembed.feature
+++ b/features/oembed.feature
@@ -15,7 +15,7 @@ Feature: oembed
When I fill in "status_message_fake_text" with "http://youtube.com/watch?v=M3r2XDceM6A&format=json"
And I press "Share"
- And I follow "Your Aspects"
+ And I follow "My Aspects"
Then I should see a video player
Scenario: Post an unsecure video link
@@ -24,7 +24,7 @@ Feature: oembed
And I press "Share"
And I wait for the ajax to finish
- And I follow "Your Aspects"
+ And I follow "My Aspects"
Then I should not see a video player
And I should see "http://mytube.com/watch?v=M3r2XDceM6A&format=json"
@@ -33,7 +33,7 @@ Feature: oembed
When I fill in "status_message_fake_text" with "http://myrichtube.com/watch?v=M3r2XDceM6A&format=json"
And I press "Share"
- And I follow "Your Aspects"
+ And I follow "My Aspects"
Then I should not see a video player
And I should see "http://myrichtube.com/watch?v=M3r2XDceM6A&format=json"
@@ -42,7 +42,7 @@ Feature: oembed
When I fill in "status_message_fake_text" with "http://farm4.static.flickr.com/3123/2341623661_7c99f48bbf_m.jpg"
And I press "Share"
- And I follow "Your Aspects"
+ And I follow "My Aspects"
Then I should see a "img" within ".stream_element"
Scenario: Post an unsupported text link
@@ -50,7 +50,7 @@ Feature: oembed
When I fill in "status_message_fake_text" with "http://www.we-do-not-support-oembed.com/index.html"
And I press "Share"
- And I follow "Your Aspects"
+ And I follow "My Aspects"
Then I should see "http://www.we-do-not-support-oembed.com/index.html" within ".stream_element"
diff --git a/features/participate_stream.feature b/features/participate_stream.feature
index 176670a56..ea2b14e93 100644
--- a/features/participate_stream.feature
+++ b/features/participate_stream.feature
@@ -13,11 +13,11 @@ Feature: The participate stream
And "B- barack obama is your new bicycle" should be post 2
And "A- I like turtles" should be post 3
- When I pin the post "A- I like turtles"
+ When I like the post "A- I like turtles"
And I wait for 1 second
And I comment "Sassy sawfish" on "C- barack obama is a square"
And I wait for 1 second
- And I pin the post "B- barack obama is your new bicycle"
+ And I like the post "B- barack obama is your new bicycle"
And I wait for 1 second
When I go to the participate page
diff --git a/features/step_definitions/stream_steps.rb b/features/step_definitions/stream_steps.rb
index 94176d93d..f26aa922f 100644
--- a/features/step_definitions/stream_steps.rb
+++ b/features/step_definitions/stream_steps.rb
@@ -2,8 +2,8 @@ Then /^I should see an image in the publisher$/ do
photo_in_publisher.should be_present
end
-Then /^I pin the post "([^"]*)"$/ do |post_text|
- pin_post(post_text)
+Then /^I like the post "([^"]*)"$/ do |post_text|
+ like_post(post_text)
end
Then /^"([^"]*)" should be post (\d+)$/ do |post_text, position|
diff --git a/features/support/publishing_cuke_helpers.rb b/features/support/publishing_cuke_helpers.rb
index 2df40dbf9..9895813a4 100644
--- a/features/support/publishing_cuke_helpers.rb
+++ b/features/support/publishing_cuke_helpers.rb
@@ -25,9 +25,9 @@ module PublishingCukeHelpers
find(".stream_element:contains('#{text}')")
end
- def pin_post(post_text)
+ def like_post(post_text)
within_post(post_text) do
- click_link 'Pin'
+ click_link 'Like'
end
wait_for_ajax_to_finish
end
@@ -65,7 +65,7 @@ module PublishingCukeHelpers
def assert_nsfw(text)
post = find_post_by_text(text)
- post.find(".shield").should be_present
+ post.find(".nsfw-shield").should be_present
end
end
diff --git a/lib/stream/base.rb b/lib/stream/base.rb
index b3af708ca..27281581a 100644
--- a/lib/stream/base.rb
+++ b/lib/stream/base.rb
@@ -40,6 +40,7 @@ class Stream::Base
def stream_posts
self.posts.for_a_stream(max_time, order, self.user).tap do |posts|
like_posts_for_stream!(posts) #some sql person could probably do this with joins.
+ participation_posts_for_stream!(posts)
end
end
@@ -112,6 +113,22 @@ class Stream::Base
end
end
+ # @return [void]
+ def participation_posts_for_stream!(posts)
+ return posts unless @user
+
+ participations = Participation.where(:author_id => @user.person.id, :target_id => posts.map(&:id), :target_type => "Post")
+
+ participation_hash = participations.inject({}) do |hash, participation|
+ hash[participation.target_id] = participation
+ hash
+ end
+
+ posts.each do |post|
+ post.user_participation = participation_hash[post.id]
+ end
+ end
+
# @return [Hash]
def publisher_opts
{}
diff --git a/public/javascripts/app/collections/participations.js b/public/javascripts/app/collections/participations.js
new file mode 100644
index 000000000..bc861d784
--- /dev/null
+++ b/public/javascripts/app/collections/participations.js
@@ -0,0 +1,7 @@
+app.collections.Participations = Backbone.Collection.extend({
+ model: app.models.Participation,
+
+ initialize : function(models, options) {
+ this.url = "/posts/" + options.post.id + "/participations" //not delegating to post.url() because when it is in a stream collection it delegates to that url
+ }
+});
diff --git a/public/javascripts/app/models/participation.js b/public/javascripts/app/models/participation.js
new file mode 100644
index 000000000..22d309210
--- /dev/null
+++ b/public/javascripts/app/models/participation.js
@@ -0,0 +1 @@
+app.models.Participation = Backbone.Model.extend({ })
diff --git a/public/javascripts/app/models/post.js b/public/javascripts/app/models/post.js
index cbd798cc7..360dd8de6 100644
--- a/public/javascripts/app/models/post.js
+++ b/public/javascripts/app/models/post.js
@@ -3,6 +3,7 @@ app.models.Post = Backbone.Model.extend({
initialize : function() {
this.comments = new app.collections.Comments(this.get("last_three_comments"), {post : this});
this.likes = new app.collections.Likes([], {post : this}); // load in the user like initially
+ this.participations = new app.collections.Participations([], {post : this}); // load in the user like initially
},
createdAt : function() {
@@ -27,6 +28,27 @@ app.models.Post = Backbone.Model.extend({
return this.get("author")
},
+ toggleFollow : function() {
+ var userParticipation = this.get("user_participation");
+ if(userParticipation) {
+ this.unfollow();
+ } else {
+ this.follow();
+ }
+ },
+
+ follow : function() {
+ this.set({ user_participation : this.participations.create() });
+ },
+
+ unfollow : function() {
+ var participationModel = new app.models.Participation(this.get("user_participation"));
+ participationModel.url = this.participations.url + "/" + participationModel.id;
+
+ participationModel.destroy();
+ this.set({ user_participation : null });
+ },
+
toggleLike : function() {
var userLike = this.get("user_like")
if(userLike) {
diff --git a/public/javascripts/app/templates/feedback.handlebars b/public/javascripts/app/templates/feedback.handlebars
index a2f719fa2..9be5c0480 100644
--- a/public/javascripts/app/templates/feedback.handlebars
+++ b/public/javascripts/app/templates/feedback.handlebars
@@ -13,6 +13,15 @@
–
+
+ {{#if user_participation}}
+ {{t "stream.unfollow"}}
+ {{else}}
+ {{t "stream.follow"}}
+ {{/if}}
+
+·
+
{{#if user_like}}
{{t "stream.unlike"}}
diff --git a/public/javascripts/app/views/feedback_view.js b/public/javascripts/app/views/feedback_view.js
index b63e1781e..4543680bb 100644
--- a/public/javascripts/app/views/feedback_view.js
+++ b/public/javascripts/app/views/feedback_view.js
@@ -6,6 +6,7 @@ app.views.Feedback = app.views.StreamObject.extend({
events: {
"click .like_action": "toggleLike",
+ "click .participate_action": "toggleFollow",
"click .reshare_action": "resharePost"
},
@@ -15,6 +16,11 @@ app.views.Feedback = app.views.StreamObject.extend({
})
},
+ toggleFollow : function(evt) {
+ if(evt) { evt.preventDefault(); }
+ this.model.toggleFollow();
+ },
+
toggleLike: function(evt) {
if(evt) { evt.preventDefault(); }
this.model.toggleLike();
diff --git a/spec/controllers/participations_controller_spec.rb b/spec/controllers/participations_controller_spec.rb
new file mode 100644
index 000000000..fe2abf7ab
--- /dev/null
+++ b/spec/controllers/participations_controller_spec.rb
@@ -0,0 +1,123 @@
+# Copyright (c) 2010-2011, Diaspora Inc. This file is
+# licensed under the Affero General Public License version 3 or later. See
+# the COPYRIGHT file.
+
+require 'spec_helper'
+
+describe ParticipationsController do
+ before do
+ @alices_aspect = alice.aspects.where(:name => "generic").first
+ @bobs_aspect = bob.aspects.where(:name => "generic").first
+
+ sign_in :user, alice
+ end
+
+ context "Posts" do
+ let(:id_field){ "post_id" }
+
+ describe '#create' do
+ let(:participation_hash) {
+ { id_field => "#{@target.id}",
+ :format => :json}
+ }
+ let(:disparticipation_hash) {
+ { id_field => "#{@target.id}",
+ :format => :json }
+ }
+
+ context "on my own post" do
+ it 'succeeds' do
+ @target = alice.post :status_message, :text => "AWESOME", :to => @alices_aspect.id
+ post :create, participation_hash
+ response.code.should == '201'
+ end
+ end
+
+ context "on a post from a contact" do
+ before do
+ @target = bob.post(:status_message, :text => "AWESOME", :to => @bobs_aspect.id)
+ end
+
+ it 'participations' do
+ post :create, participation_hash
+ response.code.should == '201'
+ end
+
+ it 'disparticipations' do
+ post :create, disparticipation_hash
+ response.code.should == '201'
+ end
+
+ it "doesn't post multiple times" do
+ alice.participate!(@target)
+ post :create, disparticipation_hash
+ response.code.should == '422'
+ end
+ end
+
+ context "on a post from a stranger" do
+ before do
+ @target = eve.post :status_message, :text => "AWESOME", :to => eve.aspects.first.id
+ end
+
+ it "doesn't post" do
+ alice.should_not_receive(:participate!)
+ post :create, participation_hash
+ response.code.should == '422'
+ end
+ end
+ end
+
+ describe '#index' do
+ before do
+ @message = alice.post(:status_message, :text => "hey", :to => @alices_aspect.id)
+ end
+
+ it 'generates a jasmine fixture', :fixture => true do
+ get :index, id_field => @message.id, :format => :json
+
+ save_fixture(response.body, "ajax_participations_on_posts")
+ end
+
+ it 'returns a 404 for a post not visible to the user' do
+ sign_in eve
+ get :index, id_field => @message.id, :format => :json
+ end
+
+ it 'returns an array of participations for a post' do
+ bob.participate!(@message)
+ get :index, id_field => @message.id, :format => :json
+ assigns[:participations].map(&:id).should == @message.participation_ids
+ end
+
+ it 'returns an empty array for a post with no participations' do
+ get :index, id_field => @message.id, :format => :json
+ assigns[:participations].should == []
+ end
+ end
+
+ describe '#destroy' do
+ before do
+ @message = bob.post(:status_message, :text => "hey", :to => @alices_aspect.id)
+ @participation = alice.participate!(@message)
+ end
+
+ it 'lets a user destroy their participation' do
+ expect {
+ delete :destroy, :format => :json, id_field => @participation.target_id, :id => @participation.id
+ }.should change(Participation, :count).by(-1)
+ response.status.should == 202
+ end
+
+ it 'does not let a user destroy other participations' do
+ participation2 = eve.participate!(@message)
+
+ expect {
+ delete :destroy, :format => :json, id_field => participation2.target_id, :id => participation2.id
+ }.should_not change(Participation, :count)
+
+ response.status.should == 403
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/app/models/post_spec.js b/spec/javascripts/app/models/post_spec.js
index 4231096ae..dfe08bc66 100644
--- a/spec/javascripts/app/models/post_spec.js
+++ b/spec/javascripts/app/models/post_spec.js
@@ -7,7 +7,7 @@ describe("app.models.Post", function() {
it("should be /posts when it doesn't have an id", function(){
expect(new app.models.Post().url()).toBe("/posts")
})
-
+
it("should be /posts/id when it doesn't have an id", function(){
expect(new app.models.Post({id: 5}).url()).toBe("/posts/5")
})
@@ -60,4 +60,45 @@ describe("app.models.Post", function() {
expect(app.models.Like.prototype.destroy).toHaveBeenCalled();
})
})
+
+ describe("toggleFollow", function(){
+ it("calls unfollow when the user_participation exists", function(){
+ this.post.set({user_participation: "123"});
+ spyOn(this.post, "unfollow").andReturn(true);
+
+ this.post.toggleFollow();
+ expect(this.post.unfollow).toHaveBeenCalled();
+ })
+
+ it("calls follow when the user_participation does not exist", function(){
+ this.post.set({user_participation: null});
+ spyOn(this.post, "follow").andReturn(true);
+
+ this.post.toggleFollow();
+ expect(this.post.follow).toHaveBeenCalled();
+ })
+ })
+
+ describe("follow", function(){
+ it("calls create on the participations collection", function(){
+ spyOn(this.post.participations, "create");
+
+ this.post.follow();
+ expect(this.post.participations.create).toHaveBeenCalled();
+ })
+ })
+
+ describe("unfollow", function(){
+ it("calls destroy on the participations collection", function(){
+ var participation = new app.models.Participation();
+ this.post.set({user_participation : participation.toJSON()})
+
+ spyOn(app.models.Participation.prototype, "destroy");
+
+ this.post.unfollow();
+ expect(app.models.Participation.prototype.destroy).toHaveBeenCalled();
+ })
+ })
+
+
});
diff --git a/spec/javascripts/app/views/post_view_spec.js b/spec/javascripts/app/views/post_view_spec.js
index 27f0731fa..b615bba74 100644
--- a/spec/javascripts/app/views/post_view_spec.js
+++ b/spec/javascripts/app/views/post_view_spec.js
@@ -32,7 +32,6 @@ describe("app.views.Post", function(){
expect($(view.el).html()).not.toContain("0 Reshares")
})
-
context("embed_html", function(){
it("provides oembed html from the model response", function(){
this.statusMessage.set({"o_embed_cache" : {
diff --git a/spec/lib/stream/base_spec.rb b/spec/lib/stream/base_spec.rb
index 29c8e7b12..2fa37b9c5 100644
--- a/spec/lib/stream/base_spec.rb
+++ b/spec/lib/stream/base_spec.rb
@@ -16,6 +16,7 @@ describe Stream::Base do
posts = mock
@stream.stub(:posts).and_return(posts)
@stream.stub(:like_posts_for_stream!)
+ @stream.stub(:participation_posts_for_stream!)
posts.should_receive(:for_a_stream).with(anything, anything, alice).and_return(posts)
@stream.stream_posts