Merge pull request #3713 from davecocoa/feature/3630-backbone-ify-followed-tags

port tagFollowings to BackBone
This commit is contained in:
Jonne Haß 2012-11-25 09:30:47 -08:00
commit 7b548fd571
25 changed files with 353 additions and 98 deletions

View file

@ -0,0 +1,17 @@
app.collections.TagFollowings = Backbone.Collection.extend({
model: app.models.TagFollowing,
url : "/tag_followings",
create : function(model) {
var name = model.name || model.get("name");
if(!this.any(
function(tagFollowing){
return tagFollowing.get("name") === name;
})) {
Backbone.Collection.prototype.create.apply(this, arguments);
}
}
});

View file

@ -0,0 +1,3 @@
app.models.TagFollowing = Backbone.Model.extend({
urlRoot: "/tag_followings"
});

View file

@ -16,8 +16,8 @@ app.Router = Backbone.Router.extend({
"commented": "stream", "commented": "stream",
"liked": "stream", "liked": "stream",
"mentions": "stream", "mentions": "stream",
"followed_tags": "stream", "followed_tags": "followed_tags",
"tags/:name": "stream", "tags/:name": "followed_tags",
"people/:id/photos": "photos", "people/:id/photos": "photos",
"people/:id": "stream", "people/:id": "stream",
@ -63,6 +63,24 @@ app.Router = Backbone.Router.extend({
app.photos = new app.models.Stream([], {collection: app.collections.Photos}); app.photos = new app.models.Stream([], {collection: app.collections.Photos});
app.page = new app.views.Photos({model : app.photos}); app.page = new app.views.Photos({model : app.photos});
$("#main_stream").html(app.page.render().el); $("#main_stream").html(app.page.render().el);
},
followed_tags : function(name) {
this.stream();
app.tagFollowings = new app.collections.TagFollowings();
var followedTagsView = new app.views.TagFollowingList({collection: app.tagFollowings});
$("#tags_list").replaceWith(followedTagsView.render().el);
followedTagsView.setupAutoSuggest();
app.tagFollowings.add(preloads.tagFollowings);
if(name) {
var followedTagsAction = new app.views.TagFollowingAction(
{tagText: name}
);
$("#author_info").prepend(followedTagsAction.render().el)
}
} }
}); });

View file

@ -0,0 +1,62 @@
app.views.TagFollowingAction = app.views.Base.extend({
templateName: "tag_following_action",
events : {
"mouseenter .button.red_on_hover": "mouseIn",
"mouseleave .button.red_on_hover": "mouseOut",
"click .button": "tagAction"
},
initialize : function(options){
this.tagText = options.tagText;
this.getTagFollowing();
app.tagFollowings.bind("remove add", this.getTagFollowing, this);
},
presenter : function() {
return _.extend(this.defaultPresenter(), {
tag_is_followed : this.tag_is_followed(),
followString : this.followString()
})
},
followString : function() {
if(this.tag_is_followed()) {
return Diaspora.I18n.t("stream.tags.following", {"tag" : this.model.attributes.name});
} else {
return Diaspora.I18n.t("stream.tags.follow", {"tag" : this.model.attributes.name});
}
},
tag_is_followed : function() {
return !this.model.isNew();
},
getTagFollowing : function(tagFollowing) {
this.model = app.tagFollowings.where({"name":this.tagText})[0] ||
new app.models.TagFollowing({"name":this.tagText});
this.model.bind("change", this.render, this);
this.render();
},
mouseIn : function(){
this.$("input").removeClass("in_aspects");
this.$("input").val( Diaspora.I18n.t('stream.tags.stop_following', {tag: this.model.attributes.name} ) );
},
mouseOut : function() {
this.$("input").addClass("in_aspects");
this.$("input").val( Diaspora.I18n.t('stream.tags.following', {"tag" : this.model.attributes.name} ) );
},
tagAction : function(evt){
if(evt){ evt.preventDefault(); }
if(this.tag_is_followed()) {
this.model.destroy();
} else {
app.tagFollowings.create(this.model);
}
}
});

View file

@ -0,0 +1,75 @@
//= require jquery.autoSuggest.custom
app.views.TagFollowingList = app.views.Base.extend({
templateName: "tag_following_list",
className : "sub_nav",
id : "tags_list",
tagName : "ul",
events: {
"submit form": "createTagFollowing"
},
initialize : function(){
this.collection.bind("add", this.appendTagFollowing, this);
},
postRenderTemplate : function() {
this.collection.each(this.appendTagFollowing, this);
},
setupAutoSuggest : function() {
this.$("input").autoSuggest("/tags", {
selectedItemProp: "name",
selectedValuesProp: "name",
searchObjProps: "name",
asHtmlID: "tags",
neverSubmit: true,
retrieveLimit: 10,
selectionLimit: false,
minChars: 2,
keyDelay: 200,
startText: "",
emptyText: "no_results",
selectionAdded: _.bind(this.suggestSelection, this)
});
this.$("input").bind('keydown', function(evt){
if(evt.keyCode == 13 || evt.keyCode == 9 || evt.keyCode == 32){
evt.preventDefault();
if( $('li.as-result-item.active').length == 0 ){
$('li.as-result-item').first().click();
}
}
});
},
presenter : function() {
return this.defaultPresenter();
},
suggestSelection : function(elem) {
this.$(".tag_input").val($(elem[0]).text().substring(2));
elem.remove();
this.createTagFollowing();
},
createTagFollowing: function(evt) {
if(evt){ evt.preventDefault(); }
var name = this.$(".tag_input").val();
this.collection.create({"name":this.$(".tag_input").val()});
this.$(".tag_input").val("");
return this;
},
appendTagFollowing: function(tag) {
this.$el.prepend(new app.views.TagFollowing({
model: tag
}).render().el);
}
});

View file

@ -0,0 +1,33 @@
app.views.TagFollowing = app.views.Base.extend({
templateName: "tag_following",
className : "unfollow",
tagName: "li",
events : {
"click .tag_following_delete": "destroyModel"
},
initialize : function(){
this.el.id = "tag-following-" + this.model.get("name");
this.model.bind("destroy", this.hide, this);
},
hide : function() {
this.$el.slideUp();
},
postRenderTemplate : function() {
this.$el.hide();
this.$el.slideDown();
},
presenter : function() {
return _.extend(this.defaultPresenter(), {
tag : this.model
})
}
});

View file

@ -36,12 +36,10 @@ Diaspora.Pages.UsersGettingStarted = function() {
}); });
$("#awesome_button").bind("click", function(evt){ $("#awesome_button").bind("click", function(evt){
evt.preventDefault();
var confirmMessage = Diaspora.I18n.t("getting_started.no_tags"); var confirmMessage = Diaspora.I18n.t("getting_started.no_tags");
if(($("#as-selections-tags").find(".as-selection-item").length > 0) || confirm(confirmMessage)) { if(($("#as-selections-tags").find(".as-selection-item").length > 0) || confirm(confirmMessage)) {
$('.tag_input').submit();
/* flash message prompt */ /* flash message prompt */
var message = Diaspora.I18n.t("getting_started.preparing_your_stream"); var message = Diaspora.I18n.t("getting_started.preparing_your_stream");
@ -55,18 +53,25 @@ Diaspora.Pages.UsersGettingStarted = function() {
/* ------ */ /* ------ */
var autocompleteInput = $("#follow_tags"); var autocompleteInput = $("#follow_tags");
var tagFollowings = new app.collections.TagFollowings();
autocompleteInput.autoSuggest("/tags", { autocompleteInput.autoSuggest("/tags", {
selectedItemProp: "name", selectedItemProp: "name",
selectedValuesProp: "name",
searchObjProps: "name", searchObjProps: "name",
asHtmlID: "tags", asHtmlID: "tags",
neverSubmit: true, neverSubmit: true,
retriveLimit: 10, retrieveLimit: 10,
selectionLimit: false, selectionLimit: false,
minChars: 2, minChars: 2,
keyDelay: 200, keyDelay: 200,
startText: "", startText: "",
emptyText: "no_results" emptyText: "no_results",
selectionAdded: function(elem){tagFollowings.create({"name":$(elem[0]).text().substring(2)})},
selectionRemoved: function(elem){
tagFollowings.where({"name":$(elem[0]).text().substring(2)})[0].destroy();
elem.remove();
}
}); });
autocompleteInput.bind('keydown', function(evt){ autocompleteInput.bind('keydown', function(evt){

View file

@ -2778,6 +2778,7 @@ a.toggle_selector
:left 24px :left 24px
input[type='text'] input[type='text']
:width 100%
:font :font
:size 13px :size 13px

View file

@ -0,0 +1,10 @@
<div class="right">
<form accept-charset="UTF-8" action="/tag_followings" method="post">
<input type="submit" class="button take_action
{{#if tag_is_followed }}
red_on_hover in_aspects
{{/if}}
" type="submit" value="{{followString}}"
/>
</form>
</div>

View file

@ -0,0 +1,5 @@
<li>
<form accept-charset="UTF-8" action="/tag_followings" id="new_tag_following" method="post">
<input class="tag_input" type="text" name="name" placeholder="{{t "stream.followed_tag.add_a_tag"}}" />
</form>
</li>

View file

@ -0,0 +1,9 @@
<div class="unfollow_icon hidden">
<a href="#" id="unfollow_{{name}}" rel="nofollow" class="delete tag_following_delete" title="{{t "delete"}}">
<img alt="Deletelabel" src="{{imageUrl "deletelabel.png"}}" />
<a/>
</div>
<a class="tag_selector" href="/tags/{{name}}">
#{{ name }}
</a>

View file

@ -153,7 +153,8 @@ class PeopleController < ApplicationController
def redirect_if_tag_search def redirect_if_tag_search
if search_query.starts_with?('#') if search_query.starts_with?('#')
if search_query.length > 1 if search_query.length > 1
redirect_to tag_path(:name => search_query.delete('#.'), :q => search_query)
redirect_to tag_path(:name => search_query.delete('#.'))
else else
flash[:error] = I18n.t('tags.show.none', :name => search_query) flash[:error] = I18n.t('tags.show.none', :name => search_query)
redirect_to :back redirect_to :back

View file

@ -52,6 +52,7 @@ class StreamsController < ApplicationController
end end
def followed_tags def followed_tags
gon.tagFollowings = tags
stream_responder(Stream::FollowedTag) stream_responder(Stream::FollowedTag)
end end

View file

@ -6,7 +6,7 @@
class TagFollowingsController < ApplicationController class TagFollowingsController < ApplicationController
before_filter :authenticate_user! before_filter :authenticate_user!
respond_to :html, :json respond_to :json
# POST /tag_followings # POST /tag_followings
# POST /tag_followings.xml # POST /tag_followings.xml
@ -14,52 +14,38 @@ class TagFollowingsController < ApplicationController
name_normalized = ActsAsTaggableOn::Tag.normalize(params['name']) name_normalized = ActsAsTaggableOn::Tag.normalize(params['name'])
if name_normalized.nil? || name_normalized.empty? if name_normalized.nil? || name_normalized.empty?
flash[:error] = I18n.t('tag_followings.create.none') render :nothing => true, :status => 403
else else
@tag = ActsAsTaggableOn::Tag.find_or_create_by_name(name_normalized) @tag = ActsAsTaggableOn::Tag.find_or_create_by_name(name_normalized)
@tag_following = current_user.tag_followings.new(:tag_id => @tag.id) @tag_following = current_user.tag_followings.new(:tag_id => @tag.id)
if @tag_following.save if @tag_following.save
flash[:notice] = I18n.t('tag_followings.create.success', :name => name_normalized) render :json => @tag.to_json, :status => 201
else else
flash[:error] = I18n.t('tag_followings.create.failure', :name => name_normalized) render :nothing => true, :status => 403
end end
end end
redirect_to :back
end end
# DELETE /tag_followings/1 # DELETE /tag_followings/1
# DELETE /tag_followings/1.xml # DELETE /tag_followings/1.xml
def destroy def destroy
@tag = ActsAsTaggableOn::Tag.find_by_name(params[:name]) tag_following = current_user.tag_followings.find_by_tag_id( params['id'] )
tag_following = current_user.tag_followings.find_by_tag_id( @tag.id )
if tag_following && tag_following.destroy if tag_following && tag_following.destroy
tag_unfollowed = true respond_to do |format|
format.any(:js, :json) { render :nothing => true, :status => 204 }
end
else else
tag_unfollowed = false respond_to do |format|
end format.any(:js, :json) {render :nothing => true, :status => 403}
respond_to do |format|
format.js { render 'tags/update' }
format.any {
if tag_unfollowed
flash[:notice] = I18n.t('tag_followings.destroy.success', :name => params[:name])
else
flash[:error] = I18n.t('tag_followings.destroy.failure', :name => params[:name])
end
redirect_to tag_path(:name => params[:name])
}
end
end
def create_multiple
if params[:tags].present?
params[:tags].split(",").each do |name|
name_normalized = ActsAsTaggableOn::Tag.normalize(name)
@tag = ActsAsTaggableOn::Tag.find_or_create_by_name(name_normalized)
@tag_following = current_user.tag_followings.create(:tag_id => @tag.id)
end end
end end
redirect_to stream_path end
def index
respond_to do |format|
format.json{ render(:json => tags.to_json, :status => 200) }
end
end end
end end

View file

@ -32,8 +32,10 @@ class TagsController < ApplicationController
end end
def show def show
if user_signed_in?
gon.tagFollowings = tags
end
@stream = Stream::Tag.new(current_user, params[:name], :max_time => max_time, :page => params[:page]) @stream = Stream::Tag.new(current_user, params[:name], :max_time => max_time, :page => params[:page])
respond_with do |format| respond_with do |format|
format.json { render :json => @stream.stream_posts.map { |p| LastThreeCommentsDecorator.new(PostPresenter.new(p, current_user)) }} format.json { render :json => @stream.stream_posts.map { |p| LastThreeCommentsDecorator.new(PostPresenter.new(p, current_user)) }}
end end
@ -47,18 +49,10 @@ class TagsController < ApplicationController
def prep_tags_for_javascript def prep_tags_for_javascript
@tags.map! do |tag| @tags.map! do |tag|
{ { :name => ("#" + tag.name) }
:name => ("#" + tag.name),
:value => ("#" + tag.name),
:url => tag_path(tag.name)
}
end end
@tags << { @tags << { :name => ('#' + params[:q]) }
:name => ('#' + params[:q]),
:value => ("#" + params[:q]),
:url => tag_path(params[:q].downcase)
}
@tags.uniq! @tags.uniq!
end end
end end

View file

@ -1,5 +1,7 @@
class ActsAsTaggableOn::Tag class ActsAsTaggableOn::Tag
self.include_root_in_json = false
def followed_count def followed_count
@followed_count ||= TagFollowing.where(:tag_id => self.id).count @followed_count ||= TagFollowing.where(:tag_id => self.id).count
end end

View file

@ -46,6 +46,8 @@
= yield(:head) = yield(:head)
= csrf_meta_tag = csrf_meta_tag
= include_gon(:camel_case => true, :namespace => :preloads)
%body %body
= flash_messages = flash_messages

View file

@ -7,15 +7,4 @@
%li %li
%b=link_to t('streams.followed_tag.title'), followed_tags_stream_path, :class => 'home_selector' %b=link_to t('streams.followed_tag.title'), followed_tags_stream_path, :class => 'home_selector'
- if @stream.is_a?(Stream::FollowedTag) %ul.sub_nav#tags_list
%ul.sub_nav
- if tags.size > 0
- for tg in tags
%li.unfollow{:id => "tag-following-#{tg.name}"}
.unfollow_icon.hidden
= link_to image_tag("icons/monotone_close_exit_delete.png", :height => 16, :title => t('aspects.index.unfollow_tag', :tag => tg.name)), tag_tag_followings_path(:name => tg.name, :remote => true), :data => { :confirm => t('are_you_sure') }, :method => :delete, :remote => true, :id => "unfollow_" + tg.name
= link_to "##{tg.name}", tag_path(:name => tg.name), :class => "tag_selector"
%li
= form_for TagFollowing.new do |tg|
= text_field_tag :name, "", :class => "tag_input", :placeholder => t('streams.followed_tag.add_a_tag')
= tg.submit t('streams.followed_tag.follow'), :class => "button"

View file

@ -9,27 +9,6 @@
- else - else
= t('.whatup', :pod => @pod_url) = t('.whatup', :pod => @pod_url)
- content_for :head do
= javascript_include_tag :home
:javascript
$(document).ready(function(){
// Change the text and color of the "follow this tag" button on hover.
$(".button.tag_following").hover(function(){
$this = $(this);
$this.removeClass("in_aspects");
$this.val("#{t('.stop_following', :tag => @stream.tag_name)}");
},
function(){
$this = $(this);
$this.addClass("in_aspects");
$this.val("#{t('.following', :tag => @stream.tag_name)}");
});
});
$(".people_stream .pagination a").live("click", function() {
$.getScript(this.href);
return false;
});
- content_for :body_class do - content_for :body_class do
= "tags_show" = "tags_show"
@ -49,12 +28,6 @@
.span-15.last .span-15.last
.stream_container .stream_container
#author_info #author_info
- if user_signed_in?
.right
- unless tag_followed?
= button_to t('.follow', :tag => @stream.tag_name), tag_tag_followings_path(:name => @stream.tag_name), :method => :post, :class => 'button take_action'
- else
= button_to t('.following', :tag => @stream.tag_name), tag_tag_followings_path(:name => @stream.tag_name), :method => :delete, :class => 'button red_on_hover tag_following in_aspects take_action'
%h2 %h2
= @stream.display_tag_name = @stream.display_tag_name
%small %small

View file

@ -63,7 +63,7 @@
.row .row
.span13 .span13
= form_tag(multiple_tag_followings_path, :method => 'post', :class => "tag_input search_form") do = form_tag(tag_followings_path, :method => 'get', :class => "tag_input search_form") do
%fieldset %fieldset
.clearfix .clearfix
= label_tag 'follow_tags', t('.hashtag_suggestions'), :class => "bootstrapped" = label_tag 'follow_tags', t('.hashtag_suggestions'), :class => "bootstrapped"
@ -75,5 +75,5 @@
%br %br
%br %br
.input .input
= link_to "#{t('.awesome_take_me_to_diaspora')} »", "#", :id => "awesome_button", :class => "btn primary" = link_to "#{t('.awesome_take_me_to_diaspora')} »", stream_path, :id => "awesome_button", :class => "btn primary"

View file

@ -106,6 +106,17 @@ en:
one: "Show <%= count %> more comment" one: "Show <%= count %> more comment"
other: "Show <%= count %> more comments" other: "Show <%= count %> more comments"
followed_tag:
title: "#Followed Tags"
contacts_title: "People who dig these tags"
add_a_tag: "Add a tag"
follow: "Follow"
tags:
follow: "Follow #<%= tag %>"
following: "Following #<%= tag %>"
stop_following: "Stop Following #<%= tag %>"
header: header:
home: "Home" home: "Home"
profile: "Profile" profile: "Profile"

View file

@ -74,13 +74,8 @@ Diaspora::Application.routes.draw do
end end
resources :tags, :only => [:index] resources :tags, :only => [:index]
scope "tags/:name" do
post "tag_followings" => "tag_followings#create", :as => 'tag_tag_followings'
delete "tag_followings" => "tag_followings#destroy", :as => 'tag_tag_followings'
end
post "multiple_tag_followings" => "tag_followings#create_multiple", :as => 'multiple_tag_followings' resources "tag_followings", :only => [:create, :destroy, :index]
resources "tag_followings", :only => [:create]
get 'tags/:name' => 'tags#show', :as => 'tag' get 'tags/:name' => 'tags#show', :as => 'tag'

View file

@ -67,12 +67,12 @@ describe PeopleController do
context 'query is a tag' do context 'query is a tag' do
it 'goes to a tag page' do it 'goes to a tag page' do
get :index, :q => '#babies' get :index, :q => '#babies'
response.should redirect_to(tag_path('babies', :q => '#babies')) response.should redirect_to(tag_path('babies'))
end end
it 'removes dots from the query' do it 'removes dots from the query' do
get :index, :q => '#babi.es' get :index, :q => '#babi.es'
response.should redirect_to(tag_path('babies', :q => '#babi.es')) response.should redirect_to(tag_path('babies'))
end end
it 'stay on the page if you search for the empty hash' do it 'stay on the page if you search for the empty hash' do

View file

@ -0,0 +1,13 @@
describe("app.collections.TagFollowings", function(){
beforeEach(function(){
this.collection = new app.collections.TagFollowings();
})
describe("create", function(){
it("should not allow duplicates", function(){
this.collection.create({"name":"name"})
this.collection.create({"name":"name"})
expect(this.collection.length).toBe(1)
})
})
})

View file

@ -0,0 +1,50 @@
describe("app.views.TagFollowingAction", function(){
beforeEach(function(){
app.tagFollowings = new app.collections.TagFollowings();
this.tagName = "test_tag";
this.view = new app.views.TagFollowingAction({tagName : this.tagName})
})
describe("render", function(){
it("shows the output of followString", function(){
spyOn(this.view, "tag_is_followed").andReturn(false)
spyOn(this.view, "followString").andReturn("a_follow_string")
expect(this.view.render().$('input').val()).toMatch(/^a_follow_string$/)
})
it("should have the extra classes if the tag is followed", function(){
spyOn(this.view, "tag_is_followed").andReturn(true)
expect(this.view.render().$('input').hasClass("red_on_hover")).toBe(true)
expect(this.view.render().$('input').hasClass("in_aspects")).toBe(true)
})
})
describe("tagAction", function(){
it("toggles the tagFollowed from followed to unfollowed", function(){
// first set the tag to followed
var origModel = this.view.model;
this.view.model.set("id", 3);
expect(this.view.tag_is_followed()).toBe(true);
spyOn(this.view.model, "destroy").andCallFake(_.bind(function(){
// model.destroy leads to collection.remove, which is bound to getTagFollowing
this.view.getTagFollowing();
}, this) )
this.view.tagAction();
expect(origModel.destroy).toHaveBeenCalled()
expect(this.view.tag_is_followed()).toBe(false);
})
it("toggles the tagFollowed from unfollowed to followed", function(){
expect(this.view.tag_is_followed()).toBe(false);
spyOn(app.tagFollowings, "create").andCallFake(function(model){
// 'save' the model by giving it an id
model.set("id", 3)
})
this.view.tagAction();
expect(this.view.tag_is_followed()).toBe(true);
})
})
})