port tagFollowings to BackBone

This commit is contained in:
David McMullin 2012-11-04 16:35:24 +00:00
parent 7517c29ed0
commit 97664cb880
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",
"liked": "stream",
"mentions": "stream",
"followed_tags": "stream",
"tags/:name": "stream",
"followed_tags": "followed_tags",
"tags/:name": "followed_tags",
"people/:id/photos": "photos",
"people/:id": "stream",
@ -63,6 +63,24 @@ app.Router = Backbone.Router.extend({
app.photos = new app.models.Stream([], {collection: app.collections.Photos});
app.page = new app.views.Photos({model : app.photos});
$("#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){
evt.preventDefault();
var confirmMessage = Diaspora.I18n.t("getting_started.no_tags");
if(($("#as-selections-tags").find(".as-selection-item").length > 0) || confirm(confirmMessage)) {
$('.tag_input').submit();
/* flash message prompt */
var message = Diaspora.I18n.t("getting_started.preparing_your_stream");
@ -55,18 +53,25 @@ Diaspora.Pages.UsersGettingStarted = function() {
/* ------ */
var autocompleteInput = $("#follow_tags");
var tagFollowings = new app.collections.TagFollowings();
autocompleteInput.autoSuggest("/tags", {
selectedItemProp: "name",
selectedValuesProp: "name",
searchObjProps: "name",
asHtmlID: "tags",
neverSubmit: true,
retriveLimit: 10,
retrieveLimit: 10,
selectionLimit: false,
minChars: 2,
keyDelay: 200,
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){

View file

@ -2778,6 +2778,7 @@ a.toggle_selector
:left 24px
input[type='text']
:width 100%
:font
: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

@ -165,7 +165,8 @@ class PeopleController < ApplicationController
def redirect_if_tag_search
if search_query.starts_with?('#')
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
flash[:error] = I18n.t('tags.show.none', :name => search_query)
redirect_to :back

View file

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

View file

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

View file

@ -32,8 +32,10 @@ class TagsController < ApplicationController
end
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])
respond_with do |format|
format.json { render :json => @stream.stream_posts.map { |p| LastThreeCommentsDecorator.new(PostPresenter.new(p, current_user)) }}
end
@ -47,18 +49,10 @@ class TagsController < ApplicationController
def prep_tags_for_javascript
@tags.map! do |tag|
{
:name => ("#" + tag.name),
:value => ("#" + tag.name),
:url => tag_path(tag.name)
}
{ :name => ("#" + tag.name) }
end
@tags << {
:name => ('#' + params[:q]),
:value => ("#" + params[:q]),
:url => tag_path(params[:q].downcase)
}
@tags << { :name => ('#' + params[:q]) }
@tags.uniq!
end
end

View file

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

View file

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

View file

@ -7,15 +7,4 @@
%li
%b=link_to t('streams.followed_tag.title'), followed_tags_stream_path, :class => 'home_selector'
- if @stream.is_a?(Stream::FollowedTag)
%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"
%ul.sub_nav#tags_list

View file

@ -9,27 +9,6 @@
- else
= 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
= "tags_show"
@ -49,12 +28,6 @@
.span-15.last
.stream_container
#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
= @stream.display_tag_name
%small

View file

@ -65,7 +65,7 @@
.row
.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
.clearfix
= label_tag 'follow_tags', t('.hashtag_suggestions'), :class => "bootstrapped"
@ -77,5 +77,5 @@
%br
%br
.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"
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:
home: "Home"
profile: "Profile"

View file

@ -74,13 +74,8 @@ Diaspora::Application.routes.draw do
end
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]
resources "tag_followings", :only => [:create, :destroy, :index]
get 'tags/:name' => 'tags#show', :as => 'tag'

View file

@ -67,12 +67,12 @@ describe PeopleController do
context 'query is a tag' do
it 'goes to a tag page' do
get :index, :q => '#babies'
response.should redirect_to(tag_path('babies', :q => '#babies'))
response.should redirect_to(tag_path('babies'))
end
it 'removes dots from the query' do
get :index, :q => '#babi.es'
response.should redirect_to(tag_path('babies', :q => '#babi.es'))
response.should redirect_to(tag_path('babies'))
end
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);
})
})
})