seperate interactions from posts

Lazily load post interactions on show page
hella refactorz
This commit is contained in:
Dennis Collinson
2012-05-12 17:37:56 -07:00
parent 871d3cc672
commit f6e3c1b88b
41 changed files with 388 additions and 535 deletions

View File

@@ -7,5 +7,18 @@ app.collections.Comments = Backbone.Collection.extend({
initialize : function(models, options) {
this.post = options.post
},
make : function(text){
var self = this
var comment = new app.models.Comment({text: text })
, deferred = comment.save({}, {url : self.url()})
comment.set({author: app.currentUser.toJSON(), parent: self.post })
this.add(comment)
return deferred
}
});

View File

@@ -0,0 +1,4 @@
app.collections.Reshares = Backbone.Collection.extend({
model: app.models.Reshare,
url : "/reshares"
});

View File

@@ -1 +0,0 @@
app.models.Participation = Backbone.Model.extend({ });

View File

@@ -2,27 +2,27 @@ app.models.Post = Backbone.Model.extend(_.extend({}, app.models.formatDateMixin,
urlRoot : "/posts",
initialize : function() {
this.setupCollections();
this.bind("change", this.setupCollections, this)
this.interactions = new app.models.Post.Interactions(_.extend({post : this}, this.get("interactions")))
this.delegateToInteractions()
},
setupCollections: function() {
this.comments = new app.collections.Comments(this.get("comments") || this.get("last_three_comments"), {post : this});
this.likes = this.likes || new app.collections.Likes([], {post : this}); // load in the user like initially
this.participations = this.participations || new app.collections.Participations([], {post : this}); // load in the user like initially
delegateToInteractions : function(){
this.comments = this.interactions.comments
this.likes = this.interactions.likes
this.comment = function(){
this.interactions.comment.apply(this.interactions, arguments)
}
},
setFrameName : function(){
var templatePicker = new app.models.Post.TemplatePicker(this)
this.set({frame_name : templatePicker.getFrameName()})
this.set({frame_name : new app.models.Post.TemplatePicker(this).getFrameName()})
},
interactedAt : function() {
return this.timeOf("interacted_at");
},
createReshareUrl : "/reshares",
reshare : function(){
return this._reshare = this._reshare || new app.models.Reshare({root_guid : this.get("guid")});
},
@@ -31,15 +31,6 @@ app.models.Post = Backbone.Model.extend(_.extend({}, app.models.formatDateMixin,
return this.get("author")
},
toggleLike : function() {
var userLike = this.get("user_like")
if(userLike) {
this.unlike()
} else {
this.like()
}
},
toggleFavorite : function(options){
this.set({favorite : !this.get("favorite")})
@@ -47,40 +38,6 @@ app.models.Post = Backbone.Model.extend(_.extend({}, app.models.formatDateMixin,
if(options.save){ this.save() }
},
like : function() {
var self = this;
this.likes.create({}, {success : function(resp){
self.set(resp)
self.trigger('interacted', self)
}});
},
unlike : function() {
var self = this;
var likeModel = new app.models.Like(this.get("user_like"));
likeModel.url = this.likes.url + "/" + likeModel.id;
likeModel.destroy({success : function(model, resp) {
self.set(resp);
self.trigger('interacted', this)
}});
},
comment : function (text) {
var self = this
, postComments = this.comments;
postComments.create({"text": text}, {
url : postComments.url(),
wait:true, // added a wait for the time being. 0.5.3 was not optimistic, but 0.9.2 is.
error:function () {
alert(Diaspora.I18n.t("failed_to_post_message"));
}
});
},
headline : function() {
var headline = this.get("text").trim()
, newlineIdx = headline.indexOf("\n")

View File

@@ -0,0 +1,116 @@
//require ../post
app.models.Post.Interactions = Backbone.Model.extend({
url : function(){
return this.post.url() + "/interactions"
},
initialize : function(options){
this.post = options.post
this.comments = new app.collections.Comments(this.get("comments"), {post : this.post})
this.likes = new app.collections.Likes(this.get("likes"), {post : this.post});
this.reshares = new app.collections.Reshares(this.get("reshares"), {post : this.post});
},
parse : function(resp){
this.comments.reset(resp.comments)
this.likes.reset(resp.likes)
this.reshares.reset(resp.reshares)
var comments = this.comments
, likes = this.likes
, reshares = this.reshares
return {
comments : comments,
likes : likes,
reshares : reshares,
fetched : true
}
},
likesCount : function(){
return (this.get("fetched") ? this.likes.models.length : this.get("likes_count") )
},
resharesCount : function(){
return this.get("fetched") ? this.reshares.models.length : this.get("reshares_count")
},
commentsCount : function(){
return this.get("fetched") ? this.comments.models.length : this.get("comments_count")
},
userLike : function(){
return this.likes.select(function(like){ return like.get("author").guid == app.currentUser.get("guid")})[0]
},
userReshare : function(){
return this.reshares.select(function(reshare){ return reshare.get("author").guid == app.currentUser.get("guid")})[0]
},
toggleLike : function() {
if(this.userLike()) {
this.unlike()
} else {
this.like()
}
},
like : function() {
var self = this;
this.likes.create({}, {success : function(){
self.trigger("change")
self.set({"likes_count" : self.get("likes_count") + 1})
}})
},
unlike : function() {
var self = this;
this.userLike().destroy({success : function(model, resp) {
self.trigger('change')
self.set({"likes_count" : self.get("likes_count") - 1})
}});
},
comment : function (text) {
var self = this;
this.comments.make(text).fail(function () {
alert(Diaspora.I18n.t("failed_to_post_message"));
}).done(function() {
self.trigger('change') //updates after sync
});
this.trigger("change") //updates count in an eager manner
},
reshare : function(){
var interactions = this
, reshare = this.post.reshare()
reshare.save({}, {
success : function(resp){
var flash = new Diaspora.Widgets.FlashMessages;
flash.render({
success: true,
notice: Diaspora.I18n.t("reshares.successful")
});
}
}).done(function(){
interactions.reshares.add(reshare)
}).done(function(){
interactions.trigger("change")
});
},
userCanReshare : function(){
var isReshare = this.post.get("post_type") == "Reshare"
, rootExists = (isReshare ? this.post.get("root") : true)
, publicPost = this.post.get("public")
, userIsNotAuthor = this.post.get("author").diaspora_id != app.currentUser.get("diaspora_id")
, userIsNotRootAuthor = rootExists && (isReshare ? this.post.get("root").author.diaspora_id != app.currentUser.get("diaspora_id") : true)
return publicPost && app.currentUser.authenticated() && userIsNotAuthor && userIsNotRootAuthor;
}
});

View File

@@ -1,4 +1,6 @@
app.models.Reshare = app.models.Post.extend({
urlRoot : "/reshares",
rootPost : function(){
this._rootPost = this._rootPost || new app.models.Post(this.get("root"));
return this._rootPost

View File

@@ -9,8 +9,10 @@ app.pages.PostViewer = app.views.Base.extend({
},
initialize : function(options) {
this.model = new app.models.Post({ id : options.id });
var post = this.model = new app.models.Post({ id : options.id });
this.model.preloadOrFetch().done(_.bind(this.initViews, this));
this.model.interactions.fetch() //async, yo, might want to throttle this later.
this.bindEvents()
},

View File

@@ -31,8 +31,9 @@ app.views.CommentStream = app.views.Base.extend({
presenter: function(){
return _.extend(this.defaultPresenter(), {
moreCommentsCount : (this.model.get("comments_count") - 3),
showExpandCommentsLink : (this.model.get("comments_count") > 3)
moreCommentsCount : (this.model.interactions.commentsCount() - 3),
showExpandCommentsLink : (this.model.interactions.commentsCount() > 3),
commentsCount : this.model.interactions.commentsCount()
})
},

View File

@@ -4,11 +4,15 @@ app.views.Comment = app.views.Content.extend({
className : "comment media",
events : function() {
return _.extend(app.views.Content.prototype.events, {
return _.extend({}, app.views.Content.prototype.events, {
"click .comment_delete": "destroyModel"
});
},
initialize : function(){
this.model.on("change", this.render, this)
},
presenter : function() {
return _.extend(this.defaultPresenter(), {
canRemove: this.canRemove(),

View File

@@ -1,5 +1,4 @@
app.views.Feedback = app.views.Base.extend({
templateName: "feedback",
className : "info",
@@ -10,47 +9,30 @@ app.views.Feedback = app.views.Base.extend({
},
initialize : function() {
this.model.bind('interacted', this.render, this);
this.model.interactions.on('change', this.render, this);
},
presenter : function() {
return _.extend(this.defaultPresenter(), {
userCanReshare : this.userCanReshare()
var interactions = this.model.interactions
return _.extend(this.defaultPresenter(),{
commentsCount : interactions.commentsCount(),
likesCount : interactions.likesCount(),
resharesCount : interactions.resharesCount(),
userCanReshare : interactions.userCanReshare(),
userLike : interactions.userLike(),
userReshare : interactions.userReshare(),
})
},
toggleLike: function(evt) {
if(evt) { evt.preventDefault(); }
this.model.toggleLike();
this.model.interactions.toggleLike();
},
resharePost : function(evt) {
if(evt) { evt.preventDefault(); }
if(!window.confirm(Diaspora.I18n.t("reshares.post", {name: this.model.reshareAuthor().name}))) { return }
var reshare = this.model.reshare()
var model = this.model
reshare.save({}, {
url: this.model.createReshareUrl,
success : function(resp){
var flash = new Diaspora.Widgets.FlashMessages;
flash.render({
success: true,
notice: Diaspora.I18n.t("reshares.successful")
});
model.trigger("interacted")
}
});
},
userCanReshare : function() {
var isReshare = this.model.get("post_type") == "Reshare"
var rootExists = (isReshare ? this.model.get("root") : true)
var publicPost = this.model.get("public");
var userIsNotAuthor = this.model.get("author").diaspora_id != app.currentUser.get("diaspora_id");
var userIsNotRootAuthor = rootExists && (isReshare ? this.model.get("root").author.diaspora_id != app.currentUser.get("diaspora_id") : true)
return publicPost && app.currentUser.authenticated() && userIsNotAuthor && userIsNotRootAuthor;
this.model.interactions.reshare();
}
});

View File

@@ -10,23 +10,19 @@ app.views.LikesInfo = app.views.StreamObject.extend({
tooltipSelector : ".avatar",
initialize : function() {
this.model.bind('expandedLikes', this.render, this)
this.model.interactions.bind('change', this.render, this)
},
presenter : function() {
return _.extend(this.defaultPresenter(), {
likes : this.model.likes.models
likes : this.model.interactions.likes.toJSON(),
likesCount : this.model.interactions.likesCount(),
likes_fetched : this.model.interactions.get("fetched"),
})
},
showAvatars : function(evt){
if(evt) { evt.preventDefault() }
var self = this;
this.model.likes.fetch()
.done(function(resp){
// set like attribute and like collection
self.model.set({likes : self.model.likes.reset(resp)})
self.model.trigger("expandedLikes")
})
this.model.interactions.fetch()
}
});

View File

@@ -18,6 +18,11 @@ app.views.PostViewerFeedback = app.views.Feedback.extend({
tooltipSelector : ".label, .home-button",
initialize : function(){
this.model.interactions.on("change", this.render, this)
},
postRenderTemplate : function() {
this.sneakyVisiblity()
},
@@ -36,5 +41,4 @@ app.views.PostViewerFeedback = app.views.Feedback.extend({
alert("you must be logged in to do that!")
return false;
}
});

View File

@@ -5,7 +5,8 @@ app.views.PostViewerInteractions = app.views.Base.extend({
subviews : {
"#post-feedback" : "feedbackView",
"#post-reactions" : "reactionsView",
"#new-post-comment" : "newCommentView"
"#new-post-comment" : "newCommentView",
".interaction_counts" : "interactionCountsView"
},
templateName: "post-viewer/interactions",
@@ -18,7 +19,7 @@ app.views.PostViewerInteractions = app.views.Base.extend({
},
initViews : function() {
this.reactionsView = new app.views.PostViewerReactions({ model : this.model })
this.reactionsView = new app.views.PostViewerReactions({ model : this.model.interactions })
/* subviews that require user */
this.feedbackView = new app.views.PostViewerFeedback({ model : this.model })

View File

@@ -10,7 +10,7 @@ app.views.PostViewerNewComment = app.views.Base.extend({
scrollableArea : "#post-reactions",
initialize : function(){
this.model.comments.bind("sync", this.clearAndReactivateForm, this)
this.model.interactions.comments.bind("sync", this.clearAndReactivateForm, this)
},
postRenderTemplate : function() {
@@ -25,7 +25,6 @@ app.views.PostViewerNewComment = app.views.Base.extend({
},
clearAndReactivateForm : function() {
this.model.trigger("interacted")
this.toggleFormState()
this.$("textarea").val("")
.css('height', '18px')

View File

@@ -7,7 +7,16 @@ app.views.PostViewerReactions = app.views.Base.extend({
tooltipSelector : ".avatar",
initialize : function() {
this.model.bind('interacted', this.render, this);
this.model.on('change', this.render, this);
this.model.comments.bind("add", this.appendComment, this)
},
presenter : function(){
return {
likes : this.model.likes.toJSON(),
comments : this.model.comments.toJSON(),
reshares : this.model.reshares.toJSON()
}
},
postRenderTemplate : function() {
@@ -21,14 +30,15 @@ app.views.PostViewerReactions = app.views.Base.extend({
/* copy pasta from commentStream */
appendComment: function(comment) {
// Set the post as the comment's parent, so we can check
// on post ownership in the Comment view.
comment.set({parent : this.model.toJSON()})
// Set the post as the comment's parent, so we can check on post ownership in the Comment view.
// model was post on old view, is interactions on new view
var parent = this.model.get("post_type") ? this.model.toJSON : this.model.post.toJSON()
comment.set({parent : parent})
this.$("#post-comments").append(new app.views.Comment({
model: comment,
className : "post-comment media"
}).render().el);
}
});

View File

@@ -11,7 +11,7 @@
<div class="comments"> </div>
{{#if loggedIn}}
<div class="comment no-border media new_comment_form_wrapper {{#unless comments_count}} hidden {{/unless}}">
<div class="comment no-border media new_comment_form_wrapper {{#unless commentsCount}} hidden {{/unless}}">
{{#with current_user}}
<a href="/people/{{guid}}" class="img">
{{{personImage this}}}

View File

@@ -15,7 +15,7 @@
<a href="#" class="like_action" rel='nofollow'>
{{#if user_like}}
{{#if userLike}}
{{t "stream.unlike"}}
{{else}}
{{t "stream.like"}}

View File

@@ -1,4 +1,4 @@
{{#if likes_count}}
{{#if likesCount}}
<div class="comment">
<div class="media">
<span class="img">
@@ -6,21 +6,20 @@
</span>
<div class="bd">
{{#unless likes.length}}
{{#unless likes_fetched}}
<a href="#" class="expand_likes grey">
{{t "stream.likes" count=likes_count}}
{{t "stream.likes" count=likesCount}}
</a>
{{else}}
{{#each likes}}
{{#with attributes.author}}
{{#with author}}
<a href="/people/{{guid}}">
<img src="{{avatar.small}}" class="avatar micro" title="{{name}}"/>
</a>
{{/with}}
{{/each}}
{{/unless}}
</div>
</div>

View File

@@ -1,35 +1,35 @@
<a href="#" rel="auth-required" class="label like" title="{{#if user_like}} {{t "viewer.unlike"}} {{else}} {{t "viewer.like"}} {{/if}}">
{{#if user_like}}
<a href="#" rel="auth-required" class="label like" title="{{#if userLike}} {{t "viewer.unlike"}} {{else}} {{t "viewer.like"}} {{/if}}">
{{#if userLike}}
<i class="icon-heart icon-red"></i>
{{else}}
<i class="icon-heart icon-white"></i>
{{/if}}
{{likes_count}}
{{likesCount}}
</a>
{{#if userCanReshare}}
<a href="#" rel="auth-required" class="label reshare" title="{{#if user_reshare}} {{t "viewer.reshared"}} {{else}} {{t "viewer.reshare"}} {{/if}}">
{{#if user_reshare}}
<a href="#" rel="auth-required" class="label reshare" title="{{#if userReshare}} {{t "viewer.reshared"}} {{else}} {{t "viewer.reshare"}} {{/if}}">
{{#if userReshare}}
<i class="icon-retweet icon-blue"></i>
{{else}}
<i class="icon-retweet icon-white"></i>
{{/if}}
{{reshares_count}}
{{resharesCount}}
</a>
{{else}}
<a class="label reshare-viewonly" title="{{#if user_reshare}} {{t "viewer.reshared"}} {{else}} {{t "viewer.reshare"}} {{/if}}">
{{#if user_reshare}}
<a class="label reshare-viewonly" title="{{#if userReshare}} {{t "viewer.reshared"}} {{else}} {{t "viewer.reshare"}} {{/if}}">
{{#if userReshare}}
<i class="icon-retweet icon-blue"></i>
{{else}}
<i class="icon-retweet icon-white"></i>
{{/if}}
{{reshares_count}}
{{resharesCount}}
</a>
{{/if}}
<a href="#" class="label comment" rel="invoke-interaction-pane" title="{{t "viewer.comment"}}">
<i class="icon-comment icon-white"></i>
{{comments_count}}
{{commentsCount}}
</a>
<!-- this acts as a dock underlay -->

View File

@@ -38,9 +38,9 @@
<i class="icon-time timestamp" title="{{created_at}}" rel="tooltip"></i>
<i class="icon-chevron-right permalink" title="View Post" rel="tooltip"></i>
<i class="icon-heart"></i> {{likes_count}}
<i class="icon-retweet"></i> {{reshares_count}}
<i class="icon-comment"></i> {{comments_count}}
<i class="icon-heart"></i> {{likesCount}}
<i class="icon-retweet"></i> {{resharesCount}}
<i class="icon-comment"></i> {{commentsCount}}
</div>
</div>

View File

@@ -56,7 +56,7 @@ class CommentsController < ApplicationController
@comments = @post.comments.for_a_stream
respond_with do |format|
format.json { render :json => CommentPresenter.new(@comments), :status => 200 }
format.json { render :json => CommentPresenter.as_collection(@comments), :status => 200 }
format.mobile{render :layout => false}
end
end

View File

@@ -13,13 +13,13 @@ class LikesController < ApplicationController
:json
def create
@like = current_user.like!(target) if target
@like = current_user.like!(target) if target rescue ActiveRecord::RecordInvalid
if @like
respond_to do |format|
format.html { render :nothing => true, :status => 201 }
format.mobile { redirect_to post_path(@like.post_id) }
format.json { render :json => find_json_for_like, :status => 201 }
format.json { render :json => @like.as_api_response(:backbone), :status => 201 }
end
else
render :nothing => true, :status => 422
@@ -27,32 +27,22 @@ class LikesController < ApplicationController
end
def destroy
@like = Like.where(:id => params[:id], :author_id => current_user.person.id).first
@like = Like.find_by_id_and_author_id!(params[:id], current_user.person.id)
if @like
current_user.retract(@like)
respond_to do |format|
format.json { render :json => find_json_for_like, :status => 202 }
end
else
respond_to do |format|
format.mobile { redirect_to :back }
format.json { render :nothing => true, :status => 403}
end
current_user.retract(@like)
respond_to do |format|
format.json { render :nothing => true, :status => 204 }
end
end
#I can go when the old stream goes.
def index
if target
@likes = target.likes.includes(:author => :profile)
@people = @likes.map(&:author)
@likes = target.likes.includes(:author => :profile)
@people = @likes.map(&:author)
respond_to do |format|
format.all{ render :layout => false }
format.json{ render :json => @likes.as_api_response(:backbone) }
end
else
render :nothing => true, :status => 404
respond_to do |format|
format.all { render :layout => false }
format.json { render :json => @likes.as_api_response(:backbone) }
end
end
@@ -60,21 +50,11 @@ class LikesController < ApplicationController
def target
@target ||= if params[:post_id]
current_user.find_visible_shareable_by_id(Post, params[:post_id])
current_user.find_visible_shareable_by_id(Post, params[:post_id]) || raise(ActiveRecord::RecordNotFound.new)
else
comment = Comment.find(params[:comment_id])
comment = nil unless current_user.find_visible_shareable_by_id(Post, comment.commentable_id)
comment
end
end
def find_json_for_like
if @like.parent.is_a? Post
ExtremePostPresenter.new(@like.parent, current_user).as_json
elsif @like.parent.is_a? Comment
CommentPresenter.new(@like.parent)
else
@like.parent.respond_to?(:as_api_response) ? @like.parent.as_api_response(:backbone) : @like.parent.as_json
Comment.find(params[:comment_id]).tap do |comment|
raise(ActiveRecord::RecordNotFound.new) unless current_user.find_visible_shareable_by_id(Post, comment.commentable_id)
end
end
end
end

View File

@@ -1,68 +0,0 @@
# 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 Rails.root.join("app", "presenters", "post_presenter")
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 => ExtremePostPresenter.new(@participation.parent, current_user), :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.json { render :json => ExtremePostPresenter.new(@participation.parent, current_user), :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

View File

@@ -9,7 +9,7 @@ class PostsController < ApplicationController
before_filter :authenticate_user!, :except => [:show, :iframe, :oembed]
before_filter :set_format_if_malformed_from_status_net, :only => :show
before_filter :find_post, :only => [:show, :next, :previous]
before_filter :find_post, :only => [:show, :next, :previous, :interactions]
layout 'post'
@@ -25,15 +25,13 @@ class PostsController < ApplicationController
end
def show
return log_and_redirect_back unless @post
mark_corresponding_notification_read if user_signed_in?
respond_to do |format|
format.html{ gon.post = ExtremePostPresenter.new(@post, current_user); render 'posts/show.html.haml' }
format.html{ gon.post = PostPresenter.new(@post, current_user); render 'posts/show.html.haml' }
format.xml{ render :xml => @post.to_diaspora_xml }
format.mobile{render 'posts/show.mobile.haml', :layout => "application"}
format.json{ render :json => ExtremePostPresenter.new(@post, current_user) }
format.json{ render :json => PostPresenter.new(@post, current_user) }
end
end
@@ -43,7 +41,7 @@ class PostsController < ApplicationController
def oembed
post_id = OEmbedPresenter.id_from_url(params.delete(:url))
post = find_by_guid_or_id_with_current_user(post_id)
post = Post.find_by_guid_or_id_with_user(post_id, current_user)
if post.present?
oembed = OEmbedPresenter.new(post, params.slice(:format, :maxheight, :minheight))
render :json => oembed
@@ -52,72 +50,54 @@ class PostsController < ApplicationController
end
end
def destroy
@post = current_user.posts.where(:id => params[:id]).first
if @post
current_user.retract(@post)
respond_to do |format|
format.js {render 'destroy'}
format.json { render :nothing => true, :status => 204 }
format.all {redirect_to stream_path}
end
else
Rails.logger.info "event=post_destroy status=failure user=#{current_user.diaspora_handle} reason='User does not own post'"
render :nothing => true, :status => 404
end
end
def update
@post = current_user.posts.find(params[:id])
if @post
@post.favorite = !@post.favorite
@post.save
render :nothing => true, :status => 202
end
end
def next
next_post = visible_posts_from_author.newer(@post)
next_post = Post.visible_from_author(@post.author, current_user).newer(@post)
respond_to do |format|
format.html{ redirect_to post_path(next_post) }
format.json{ render :json => ExtremePostPresenter.new(next_post, current_user)}
format.json{ render :json => PostPresenter.new(next_post, current_user)}
end
end
def previous
previous_post = visible_posts_from_author.older(@post)
previous_post = Post.visible_from_author(@post.author, current_user).older(@post)
respond_to do |format|
format.html{ redirect_to post_path(previous_post) }
format.json{ render :json => ExtremePostPresenter.new(previous_post, current_user)}
format.json{ render :json => PostPresenter.new(previous_post, current_user)}
end
end
def interactions
respond_with(PostInteractionPresenter.new(@post, current_user))
end
def destroy
find_current_user_post(params[:id])
current_user.retract(@post)
respond_to do |format|
format.js { render 'destroy' }
format.json { render :nothing => true, :status => 204 }
format.all { redirect_to stream_path }
end
end
def update
find_current_user_post(params[:id])
@post.favorite = !@post.favorite
@post.save
render :nothing => true, :status => 202
end
protected
def log_and_redirect_back #preserving old functionality, but this should probably be removed
user_id = (user_signed_in? ? current_user : nil)
Rails.logger.info(":event => :link_to_nonexistent_post, :ref => #{request.env['HTTP_REFERER']}, :user_id => #{user_id}, :post_id => #{params[:id]}")
flash[:error] = I18n.t('posts.show.not_found')
redirect_to :back
def find_post #checks whether current user can see it
@post = Post.find_by_guid_or_id_with_user(params[:id], current_user)
end
def find_post
@post = find_by_guid_or_id_with_current_user(params[:id])
end
def visible_posts_from_author
Post.visible_from_author(@post.author, current_user)
end
def find_by_guid_or_id_with_current_user(id)
key = id.to_s.length <= 8 ? :id : :guid
if user_signed_in?
current_user.find_visible_shareable_by_id(Post, id, :key => key)
else
Post.where(key => id, :public => true).includes(:author, :comments => :author).first
end
def find_current_user_post(id) #makes sure current_user can modify
@post = current_user.posts.find(id)
end
def set_format_if_malformed_from_status_net

View File

@@ -143,4 +143,15 @@ class Post < ActiveRecord::Base
def nsfw
self.author.profile.nsfw?
end
def self.find_by_guid_or_id_with_user(id, user=nil)
key = id.to_s.length <= 8 ? :id : :guid
post = if user
user.find_visible_shareable_by_id(Post, id, :key => key)
else
Post.where(key => id, :public => true).includes(:author, :comments => :author).first
end
post || raise(ActiveRecord::RecordNotFound.new("could not find a post with id #{id}"))
end
end

View File

@@ -9,6 +9,6 @@ class ExtremePostPresenter
def as_json(options={})
post = PostPresenter.new(@post, @current_user)
interactions = PostInteractionPresenter.new(@post, @current_user)
post.as_json.merge!(interactions.as_json)
post.as_json.merge!(:interactions => interactions.as_json)
end
end

View File

@@ -4,6 +4,8 @@ class LastThreeCommentsDecorator
end
def as_json(options={})
@presenter.as_json.merge({:last_three_comments => CommentPresenter.as_collection(@presenter.post.last_three_comments)})
@presenter.as_json.tap do |post|
post[:interactions].merge!(:comments => CommentPresenter.as_collection(@presenter.post.last_three_comments))
end
end
end

View File

@@ -20,9 +20,6 @@ class PostPresenter
:public => @post.public,
:created_at => @post.created_at,
:interacted_at => @post.interacted_at,
:comments_count => @post.comments_count,
:likes_count => @post.likes_count,
:reshares_count => @post.reshares_count,
:provider_display_name => @post.provider_display_name,
:post_type => @post.post_type,
:image_url => @post.image_url,
@@ -38,8 +35,14 @@ class PostPresenter
:title => title,
:next_post => next_post_path,
:previous_post => previous_post_path,
:user_like => user_like,
:user_reshare => user_reshare
:interactions => {
:likes => [user_like].compact,
:reshares => [user_reshare].compact,
:comments_count => @post.comments_count,
:likes_count => @post.likes_count,
:reshares_count => @post.reshares_count,
}
}
end

View File

@@ -17,8 +17,10 @@ Diaspora::Application.routes.draw do
member do
get :next
get :previous
get :interactions
end
resources :likes, :only => [:create, :destroy, :index]
resources :likes, :only => [:create, :destroy, :index ]
resources :participations, :only => [:create, :destroy, :index]
resources :comments, :only => [:new, :create, :destroy, :index]
end

View File

@@ -7,9 +7,10 @@ Feature: Post Viewer
Background:
Given a user with email "alice@alice.com"
And I sign in as "alice@alice.com"
@wip
Scenario: Paging through posts
Given I have posts for each type of template
Then I visit all of my posts
And I should have seen all of my posts displayed with the correct template
# Wip tag sad on new cucumber, commenting for now.
# @wip
# Scenario: Paging through posts
# Given I have posts for each type of template
# Then I visit all of my posts
# And I should have seen all of my posts displayed with the correct template

View File

@@ -7,7 +7,7 @@ module Federated
def create!(options={})
relayable = build(options)
if relayable.save
if relayable.save!
FEDERATION_LOGGER.info("user:#{@user.id} dispatching #{relayable.class}:#{relayable.guid}")
Postzord::Dispatcher.defer_build_and_post(@user, relayable)
relayable

View File

@@ -1,4 +1,4 @@
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# Copyright (c) 2010-2011, Diaspora Inc. This file is
# licensed under the Affero General Public License version 3 or later. See
# the COPYRIGHT file.
@@ -88,7 +88,7 @@ describe LikesController do
it 'returns a 404 for a post not visible to the user' do
sign_in eve
get :index, id_field => @message.id
expect{get :index, id_field => @message.id}.to raise_error(ActiveRecord::RecordNotFound)
end
it 'returns an array of likes for a post' do
@@ -114,22 +114,19 @@ describe LikesController do
expect {
delete :destroy, :format => :json, id_field => @like.target_id, :id => @like.id
}.should change(Like, :count).by(-1)
response.status.should == 202
response.status.should == 204
end
it 'does not let a user destroy other likes' do
like2 = eve.like!(@message)
like_count = Like.count
expect {
delete :destroy, :format => :json, id_field => like2.target_id, :id => like2.id
}.should_not change(Like, :count)
}.should raise_error(ActiveRecord::RecordNotFound)
response.status.should == 403
end
Like.count.should == like_count
it 'returns the parent post presenter' do
delete :destroy, :format => :json, id_field => @like.target_id, :id => @like.id
response.body.should include 'post' if class_const != Comment
end
end
end

View File

@@ -1,128 +0,0 @@
# 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
it 'returns the parent post presenter' do
delete :destroy, :format => :json, id_field => @participation.target_id, :id => @participation.id
response.body.should include 'post'
end
end
end
end

View File

@@ -55,9 +55,8 @@ describe PostsController do
response.should be_success
end
it 'redirects if the post is missing' do
get :show, :id => 1234567
response.should be_redirect
it '404 if the post is missing' do
expect { get :show, :id => 1234567 }.to raise_error(ActiveRecord::RecordNotFound)
end
end
@@ -86,8 +85,7 @@ describe PostsController do
it 'does not show a private post' do
status = alice.post(:status_message, :text => "hello", :public => false, :to => 'all')
get :show, :id => status.id
response.status = 302
expect { get :show, :id => status.id }.to raise_error(ActiveRecord::RecordNotFound)
end
# We want to be using guids from now on for this post route, but do not want to break
@@ -128,8 +126,7 @@ describe PostsController do
end
it 'returns a 404 response when the post is not found' do
get :oembed, :url => "/posts/#{@message.id}"
response.should_not be_success
expect { get :oembed, :url => "/posts/#{@message.id}" }.to raise_error(ActiveRecord::RecordNotFound)
end
end
@@ -155,15 +152,13 @@ describe PostsController do
it 'will not let you destroy posts visible to you' do
message = bob.post(:status_message, :text => "hey", :to => bob.aspects.first.id)
delete :destroy, :format => :js, :id => message.id
response.should_not be_success
expect { delete :destroy, :format => :js, :id => message.id }.to raise_error(ActiveRecord::RecordNotFound)
StatusMessage.exists?(message.id).should be_true
end
it 'will not let you destory posts you do not own' do
message = eve.post(:status_message, :text => "hey", :to => eve.aspects.first.id)
delete :destroy, :format => :js, :id => message.id
response.should_not be_success
expect { delete :destroy, :format => :js, :id => message.id }.to raise_error(ActiveRecord::RecordNotFound)
StatusMessage.exists?(message.id).should be_true
end
end
@@ -171,8 +166,8 @@ describe PostsController do
describe "#next" do
before do
sign_in alice
#lets make a class and unit test it, because this is still not working
@controller.stub_chain(:visible_posts_from_author, :newer).and_return(next_post)
Post.stub(:find_by_guid_or_id_with_user).and_return(mock_model(Post, :author => 4))
Post.stub_chain(:visible_from_author, :newer).and_return(next_post)
end
let(:next_post){ mock_model(StatusMessage, :id => 34)}
@@ -181,7 +176,7 @@ describe PostsController do
let(:mock_presenter) { mock(:as_json => {:title => "the unbearable lightness of being"}) }
it "should return a show presenter the next post" do
ExtremePostPresenter.should_receive(:new).with(next_post, alice).and_return(mock_presenter)
PostPresenter.should_receive(:new).with(next_post, alice).and_return(mock_presenter)
get :next, :id => 14, :format => :json
response.body.should == {:title => "the unbearable lightness of being"}.to_json
end
@@ -198,8 +193,8 @@ describe PostsController do
describe "previous" do
before do
sign_in alice
#lets make a class and unit test it, because this is still not working
@controller.stub_chain(:visible_posts_from_author, :older).and_return(previous_post)
Post.stub(:find_by_guid_or_id_with_user).and_return(mock_model(Post, :author => 4))
Post.stub_chain(:visible_from_author, :older).and_return(previous_post)
end
let(:previous_post){ mock_model(StatusMessage, :id => 11)}
@@ -208,7 +203,7 @@ describe PostsController do
let(:mock_presenter) { mock(:as_json => {:title => "existential crises"})}
it "should return a show presenter the next post" do
ExtremePostPresenter.should_receive(:new).with(previous_post, alice).and_return(mock_presenter)
PostPresenter.should_receive(:new).with(previous_post, alice).and_return(mock_presenter)
get :previous, :id => 14, :format => :json
response.body.should == {:title => "existential crises"}.to_json
end

View File

@@ -0,0 +1,45 @@
describe("app.models.Post.Interactions", function(){
beforeEach(function(){
this.interactions = factory.post()
this.interactions = this.interactions.interactions
this.author = factory.author({guid: "loggedInAsARockstar"})
loginAs({guid: "loggedInAsARockstar"})
this.userLike = new app.models.Like({author : this.author})
})
describe("toggleLike", function(){
it("calls unliked when the user_like exists", function(){
this.interactions.likes.add(this.userLike)
spyOn(this.interactions, "unlike").andReturn(true);
this.interactions.toggleLike();
expect(this.interactions.unlike).toHaveBeenCalled();
})
it("calls liked when the user_like does not exist", function(){
this.interactions.likes.reset([]);
spyOn(this.interactions, "like").andReturn(true);
this.interactions.toggleLike();
expect(this.interactions.like).toHaveBeenCalled();
})
})
describe("like", function(){
it("calls create on the likes collection", function(){
spyOn(this.interactions.likes, "create");
this.interactions.like();
expect(this.interactions.likes.create).toHaveBeenCalled();
})
})
describe("unlike", function(){
it("calls destroy on the likes collection", function(){
this.interactions.likes.add(this.userLike)
spyOn(this.userLike, "destroy");
this.interactions.unlike();
expect(this.userLike.destroy).toHaveBeenCalled();
})
})
})

View File

@@ -36,43 +36,4 @@ describe("app.models.Post", function() {
expect(this.post.createdAt()).toEqual(+date);
});
});
describe("toggleLike", function(){
it("calls unliked when the user_like exists", function(){
this.post.set({user_like : "123"});
spyOn(this.post, "unlike").andReturn(true);
this.post.toggleLike();
expect(this.post.unlike).toHaveBeenCalled();
})
it("calls liked when the user_like does not exist", function(){
this.post.set({user_like : null});
spyOn(this.post, "like").andReturn(true);
this.post.toggleLike();
expect(this.post.like).toHaveBeenCalled();
})
})
describe("like", function(){
it("calls create on the likes collection", function(){
spyOn(this.post.likes, "create");
this.post.like();
expect(this.post.likes.create).toHaveBeenCalled();
})
})
describe("unlike", function(){
it("calls destroy on the likes collection", function(){
var like = new app.models.Like();
this.post.set({user_like : like.toJSON()})
spyOn(app.models.Like.prototype, "destroy");
this.post.unlike();
expect(app.models.Like.prototype.destroy).toHaveBeenCalled();
})
})
});

View File

@@ -29,28 +29,6 @@ describe("app.views.CommentStream", function(){
})
})
describe("createComment", function(){
beforeEach(function(){
spyOn(this.view.model.comments, "create")
})
it("clears the new comment textarea", function(){
var comment = {
"id": 1234,
"text": "hey",
"author": "not_null"
};
spyOn($, "ajax").andCallFake(function(params) {
params.success(comment);
});
$(this.view.el).html($("<textarea/>", {"class" : 'comment_box'}).val(comment.text))
this.view.createComment()
expect(this.view.$(".comment_box").val()).toBe("")
expect(this.view.model.comments.create).toHaveBeenCalled()
})
})
describe("appendComment", function(){
it("appends this.model as 'parent' to the comment", function(){
var comment = new app.models.Comment(factory.comment())

View File

@@ -19,7 +19,7 @@ describe("app.views.Feedback", function(){
describe("triggers", function() {
it('re-renders when the model triggers feedback', function(){
spyOn(this.view, "postRenderTemplate")
this.view.model.trigger("interacted")
this.view.model.interactions.trigger("change")
expect(this.view.postRenderTemplate).toHaveBeenCalled()
})
})
@@ -32,15 +32,17 @@ describe("app.views.Feedback", function(){
context("likes", function(){
it("calls 'toggleLike' on the target post", function(){
loginAs(this.post.interactions.likes.models[0].get("author"))
this.view.render();
spyOn(this.post, "toggleLike");
spyOn(this.post.interactions, "toggleLike");
this.link().click();
expect(this.post.toggleLike).toHaveBeenCalled();
expect(this.post.interactions.toggleLike).toHaveBeenCalled();
})
context("when the user likes the post", function(){
it("the like action should be 'Unlike'", function(){
spyOn(this.post.interactions, "userLike").andReturn(factory.like());
this.view.render()
expect(this.link().text()).toContain(Diaspora.I18n.t('stream.unlike'))
})
})
@@ -137,7 +139,7 @@ describe("app.views.Feedback", function(){
it("reshares the model", function(){
spyOn(window, "confirm").andReturn(true);
spyOn(this.view.model.reshare(), "save")
spyOn(this.view.model.reshare(), "save").andReturn(new $.Deferred)
this.view.$(".reshare_action").first().click();
expect(this.view.model.reshare().save).toHaveBeenCalled();
})

View File

@@ -16,34 +16,32 @@ describe("app.views.LikesInfo", function(){
describe(".render", function(){
it("displays a the like count if it is above zero", function() {
spyOn(this.view.model.interactions, "likesCount").andReturn(3);
this.view.render();
this.view.model.set({"likes_count" : 1})
expect($(this.view.el).find(".expand_likes").length).toBe(1)
})
it("does not display the like count if it is zero", function() {
this.post.save({likes_count : 0});
spyOn(this.view.model.interactions, "likesCount").andReturn(0);
this.view.render();
expect($(this.view.el).html().trim()).toBe("");
})
it("fires on a model change", function(){
spyOn(this.view, "postRenderTemplate")
this.view.model.trigger('expandedLikes')
this.view.model.interactions.trigger('change')
expect(this.view.postRenderTemplate).toHaveBeenCalled()
})
})
describe("showAvatars", function(){
beforeEach(function(){
spyOn(this.post.likes, "fetch").andCallThrough()
spyOn(this.post.interactions, "fetch").andCallThrough()
})
it("calls fetch on the model's like collection", function(){
this.view.showAvatars();
expect(this.post.likes.fetch).toHaveBeenCalled();
expect(this.post.interactions.fetch).toHaveBeenCalled();
})
it("sets the fetched response to the model's likes", function(){

View File

@@ -57,20 +57,24 @@ factory = {
"provider_display_name" : null,
"created_at" : "2012-01-03T19:53:13Z",
"interacted_at" : '2012-01-03T19:53:13Z',
"last_three_comments" : null,
"public" : false,
"guid" : this.guid(),
"image_url" : null,
"o_embed_cache" : null,
"photos" : [],
"text" : "jasmine is bomb",
"reshares_count" : 0,
"id" : this.id.next(),
"object_url" : null,
"root" : null,
"post_type" : "StatusMessage",
"likes_count" : 0,
"comments_count" : 0
"interactions" : {
"reshares_count" : 0,
"likes_count" : 0,
"comments_count" : 0,
"comments" : [],
"likes" : [],
"reshares" : []
}
}
},

View File

@@ -77,9 +77,10 @@ describe User::SocialActions do
it "does not allow multiple likes" do
alice.like!(@status)
lambda {
alice.like!(@status)
}.should_not change(@status, :likes)
likes = @status.likes
expect { alice.like!(@status) }.to raise_error
@status.reload.likes.should == likes
end
end
end