Merge pull request #3850 from marpo60/port-my-aspects-to-backbone

Port my aspects to backbone
This commit is contained in:
Jonne Haß 2013-01-16 15:13:56 -08:00
commit ab86423fe9
24 changed files with 443 additions and 139 deletions

View file

@ -13,6 +13,7 @@
* Remove the hack for loading the entire lib folder with a proper solution. [#3809](https://github.com/diaspora/diaspora/issues/3750)
* Update and refactor the default public view `public/default.html` [#3811](https://github.com/diaspora/diaspora/issues/3811)
* Write unicorn stderr and stdout [#3785](https://github.com/diaspora/diaspora/pull/3785)
* Ported aspects to backbone [#3850](https://github.com/diaspora/diaspora/pull/3850)
## Features

View file

@ -0,0 +1,26 @@
app.collections.Aspects = Backbone.Collection.extend({
model: app.models.Aspect,
selectedAspects: function(attribute){
return _.pluck(_.filter(this.toJSON(), function(a){
return a.selected;
}), attribute);
},
allSelected: function(){
return this.length === _.filter(this.toJSON(), function(a){ return a.selected; }).length;
},
selectAll: function(){
this.map(function(a){ a.set({ 'selected' : true })} );
},
deselectAll: function(){
this.map(function(a){ a.set({ 'selected' : false })} );
},
toSentence: function(){
var separator = Diaspora.I18n.t("comma") + ' ';
return this.selectedAspects('name').join(separator).replace(/,\s([^,]+)$/, ' ' + Diaspora.I18n.t("and") + ' $1') || Diaspora.I18n.t("my_aspects");
}
})

View file

@ -0,0 +1,5 @@
app.models.Aspect = Backbone.Model.extend({
toggleSelected: function(){
this.set({'selected' : !this.get('selected')});
}
});

View file

@ -0,0 +1,27 @@
app.models.StreamAspects = app.models.Stream.extend({
url : function(){
return _.any(this.items.models) ? this.timeFilteredPath() : this.basePath()
},
initialize : function(models, options){
var collectionClass = options && options.collection || app.collections.Posts;
this.items = new collectionClass([], this.collectionOptions());
this.aspects_ids = options.aspects_ids;
},
basePath : function(){
return '/aspects';
},
fetch: function() {
if(this.isFetching()){ return false }
var url = this.url();
var ids = this.aspects_ids;
this.deferred = this.items.fetch({
add : true,
url : url,
data : { 'a_ids': ids }
}).done(_.bind(this.triggerFetchedEvents, this))
}
});

View file

@ -11,8 +11,8 @@ app.Router = Backbone.Router.extend({
"stream": "stream",
"participate": "stream",
"explore": "stream",
"aspects": "stream",
"aspects:query": "stream",
"aspects": "aspects",
"aspects/stream": "aspects_stream",
"commented": "stream",
"liked": "stream",
"mentions": "stream",
@ -74,13 +74,36 @@ app.Router = Backbone.Router.extend({
followedTagsView.setupAutoSuggest();
app.tagFollowings.add(preloads.tagFollowings);
if(name) {
var followedTagsAction = new app.views.TagFollowingAction(
{tagText: name}
);
$("#author_info").prepend(followedTagsAction.render().el)
}
},
aspects : function(){
app.aspects = new app.collections.Aspects(app.currentUser.get('aspects'));
var aspects_list = new app.views.AspectsList({ collection: app.aspects });
aspects_list.render();
this.aspects_stream();
},
aspects_stream : function(){
var ids = app.aspects.selectedAspects('id');
app.stream = new app.models.StreamAspects([], { aspects_ids: ids });
app.stream.fetch();
app.page = new app.views.Stream({model : app.stream});
app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.items});
app.publisher.updateAspectsSelector(ids);
var streamFacesView = new app.views.StreamFaces({collection : app.stream.items});
$("#main_stream").html(app.page.render().el);
$('#selected_aspect_contacts .content').html(streamFacesView.render().el);
}
});

View file

@ -0,0 +1,28 @@
app.views.Aspect = app.views.Base.extend({
templateName: "aspect",
tagName: "li",
initialize: function(){
if (this.model.get('selected')){
this.$el.addClass('active');
};
},
events: {
'click a.aspect_selector': 'toggleAspect'
},
toggleAspect: function(evt){
if (evt) { evt.preventDefault(); };
this.$el.toggleClass('active');
this.model.toggleSelected();
app.router.aspects_stream();
},
presenter : function() {
return _.extend(this.defaultPresenter(), {
aspect : this.model
})
}
});

View file

@ -0,0 +1,55 @@
app.views.AspectsList = app.views.Base.extend({
templateName: 'aspects-list',
el: '#aspects_list',
events: {
'click .toggle_selector' : 'toggleAll'
},
initialize: function(){
this.collection.on('change', this.toggleSelector, this);
this.collection.on('change', this.updateStreamTitle, this);
},
postRenderTemplate: function() {
this.collection.each(this.appendAspect, this);
this.$('a[rel*=facebox]').facebox();
this.updateStreamTitle();
this.toggleSelector();
},
appendAspect: function(aspect) {
$("#aspects_list > *:last").before(new app.views.Aspect({
model: aspect, attributes: {'data-aspect_id': aspect.get('id')}
}).render().el);
},
toggleAll: function(evt){
if (evt) { evt.preventDefault(); };
if (this.collection.allSelected()) {
this.collection.deselectAll();
this.$('li:not(:last)').removeClass("active");
} else {
this.collection.selectAll();
this.$('li:not(:last)').addClass("active");
}
this.toggleSelector();
app.router.aspects_stream();
},
toggleSelector: function(){
var selector = this.$('a.toggle_selector');
if (this.collection.allSelected()) {
selector.text(Diaspora.I18n.t('aspect_navigation.deselect_all'));
} else {
selector.text(Diaspora.I18n.t('aspect_navigation.select_all'));
}
},
updateStreamTitle: function(){
$('.stream_title').text(this.collection.toSentence());
}
})

View file

@ -21,16 +21,28 @@
}
// update the selection summary
AspectsDropdown.updateNumber(
el.closest(".dropdown_list"),
null,
el.parent().find('li.selected').length,
''
);
this._updateAspectsNumber(el);
this._updateSelectedAspectIds();
},
updateAspectsSelector: function(ids){
var el = this.$("ul.dropdown_list");
this.$('.dropdown_list > li').each(function(){
var el = $(this);
var aspectId = el.data('aspect_id');
if (_.contains(ids, aspectId)) {
el.addClass('selected');
}
else {
el.removeClass('selected');
}
});
this._updateAspectsNumber(el);
this._updateSelectedAspectIds();
},
// take care of the form fields that will indicate the selected aspects
_updateSelectedAspectIds: function() {
var self = this;
@ -52,6 +64,15 @@
});
},
_updateAspectsNumber: function(el){
AspectsDropdown.updateNumber(
el.closest(".dropdown_list"),
null,
el.parent().find('li.selected').length,
''
);
},
_addHiddenAspectInput: function(id) {
var uid = _.uniqueId('aspect_ids_');
this.$('.content_creation form').append(
@ -59,4 +80,4 @@
);
}
};
})();
})();

View file

@ -1,7 +0,0 @@
Diaspora.Pages.StreamsAspects = function() {
var self = this;
this.subscribe("page/ready", function(evt, document) {
self.aspectNavigation = self.instantiate("AspectNavigation", document.find("ul#aspect_nav"));
});
};

View file

@ -1,98 +0,0 @@
/* Copyright (c) 2010, Diaspora Inc. This file is
* licensed under the Affero General Public License version 3 or later. See
* the COPYRIGHT file.
*/
(function() {
Diaspora.Widgets.AspectNavigation = function() {
var self = this;
this.subscribe("widget/ready", function(evt, aspectNavigation) {
$.extend(self, {
aspectNavigation: aspectNavigation,
aspectSelectors: aspectNavigation.find("a.aspect_selector[data-guid]"),
aspectLis: aspectNavigation.find("li[data-aspect_id]"),
toggleSelector: aspectNavigation.find("a.toggle_selector")
});
self.aspectSelectors.click(self.toggleAspect);
self.toggleSelector.click(self.toggleAll);
});
this.selectedAspects = function() {
return self.aspectNavigation.find("li.active[data-aspect_id]").map(function() { return $(this).data('aspect_id') });
};
this.toggleAspect = function(evt) {
evt.preventDefault();
$(this).parent().toggleClass("active");
self.perform();
};
this.toggleAll = function(evt) {
evt.preventDefault();
if (self.allSelected()) {
self.aspectLis.removeClass("active");
} else {
self.aspectLis.addClass("active");
}
self.perform();
};
this.perform = function() {
if (self.noneSelected()) {
// clear the posts
app.page.collection.reset();
app.page.render();
// toggle the button
this.calculateToggleText();
return;
} else {
window.location = self.generateURL(); // hella hax
}
};
this.calculateToggleText = function() {
if (self.allSelected()) {
self.toggleSelector.text(Diaspora.I18n.t('aspect_navigation.deselect_all'));
} else {
self.toggleSelector.text(Diaspora.I18n.t('aspect_navigation.select_all'));
}
};
this.generateURL = function() {
var baseURL = 'aspects';
// generate new url
baseURL = baseURL.replace('#','');
baseURL += '?';
self.aspectLis.each(function() {
var aspectLi = $(this);
if (aspectLi.hasClass("active")) {
baseURL += "a_ids[]=" + aspectLi.data("aspect_id") + "&";
}
});
if(!$("#publisher").hasClass("closed")) {
// open publisher
baseURL += "op=true";
} else {
// slice last '&'
baseURL = baseURL.slice(0,baseURL.length-1);
}
return baseURL;
};
this.noneSelected = function() {
return self.aspectLis.filter(".active").length === 0;
}
this.allSelected = function() {
return self.aspectLis.not(".active").length === 0;
}
};
})();

View file

@ -0,0 +1,6 @@
<div class="edit">
<a href="/aspects/{{id}}/edit" rel="facebox">
<img alt="Pencil" src="{{imageUrl "icons/pencil.png"}}" title="{{t 'edit'}} {{name}}">
</a>
</div>
<a class="aspect_selector name" data-guid="{{id}}" href="/aspects/query"> {{name}} </a>

View file

@ -0,0 +1,6 @@
<a class="toggle_selector" href="#">
{{ t "aspect_navigation.select_all" }}
</a>
<li>
<a class="new_aspect" href="/aspects/new" rel="facebox">{{ t "aspect_navigation.add_an_aspect" }}</a>
</li>

View file

@ -21,7 +21,7 @@ class StreamsController < ApplicationController
:json
def aspects
aspect_ids = (session[:a_ids] ? session[:a_ids] : [])
aspect_ids = (session[:a_ids] || [])
@stream = Stream::Aspect.new(current_user, aspect_ids,
:max_time => max_time)
stream_responder

View file

@ -39,7 +39,8 @@ module LayoutHelper
def set_current_user_in_javascript
return unless user_signed_in?
user = UserPresenter.new(current_user).to_json
a_ids = session[:a_ids] || []
user = UserPresenter.new(current_user, a_ids).to_json
content_tag(:script) do
<<-JS.html_safe
window.current_user_attributes = #{user}

View file

@ -1,8 +1,9 @@
class UserPresenter
attr_accessor :user
attr_accessor :user, :aspects_ids
def initialize(user)
self.user = user
def initialize(user, aspects_ids)
self.user = user
self.aspects_ids = aspects_ids
end
def to_json(options = {})
@ -27,7 +28,11 @@ class UserPresenter
end
def aspects
AspectPresenter.as_collection(user.aspects)
@aspects ||= begin
aspects = AspectPresenter.as_collection(user.aspects)
no_aspects = self.aspects_ids.empty?
aspects.each{ |a| a[:selected] = no_aspects || self.aspects_ids.include?(a[:id].to_s) }
end
end
def notifications_count

View file

@ -4,19 +4,6 @@
%ul#aspect_nav.left_nav.sub
%li.all_aspects
= link_to t('streams.aspects.title'), aspects_path, :class => 'home_selector'
= link_to t('streams.aspects.title'), aspects_path, :class => 'home_selector', :rel => 'backbone'
- if @stream.is_a?(Stream::Aspect)
%ul.sub_nav
- if defined?(stream)
%a.toggle_selector{:href => '#'}
= stream.for_all_aspects? ? t('.deselect_all') : t('.select_all')
- for aspect in all_aspects
%li{:data => {:aspect_id => aspect.id}, :class => ("active" if defined?(stream) && stream.aspect_ids.include?(aspect.id))}
.edit
= link_to image_tag("icons/pencil.png", :title => t('.edit_aspect', :name => aspect.name)), edit_aspect_path(aspect), :rel => "facebox"
%a.aspect_selector{:href => aspects_path("a_ids[]" => aspect.id), :class => "name", 'data-guid' => aspect.id}
= aspect
%li
= link_to t('.add_an_aspect'), new_aspect_path, :class => "new_aspect", :rel => "facebox"
%ul#aspects_list.sub_nav

View file

@ -5,7 +5,7 @@
%h3#aspect_stream_header.stream_title
= stream.title
= render 'shared/publisher', :selected_aspects => stream.aspects, :aspect_ids => stream.aspect_ids, :for_all_aspects => stream.for_all_aspects?, :aspect => stream.aspect
= render 'shared/publisher', :selected_aspects => stream.aspects, :aspect_ids => stream.aspect_ids, :for_all_aspects => stream.for_all_aspects?, :aspect => stream.aspect
= render 'aspects/no_posts_message'
#gs-shim{:title => popover_with_close_html("3. #{t('.stay_updated')}"), 'data-content' => t('.stay_updated_explanation')}

View file

@ -9,6 +9,9 @@ en:
delete: "Delete"
ignore: "Ignore"
ignore_user: "Ignore this user?"
and: "and"
comma: ","
edit: "Edit"
timeago:
prefixAgo: ""
prefixFromNow: ""
@ -28,6 +31,7 @@ en:
my_activity: "My Activity"
my_stream: "Stream"
my_aspects: "My Aspects"
videos:
watch: "Watch this video on <%= provider %>"
@ -64,6 +68,7 @@ en:
select_all: "Select all"
deselect_all: "Deselect all"
no_aspects: "No aspects selected"
add_an_aspect: "+ Add an aspect"
getting_started:
hey: "Hey, <%= name %>!"
no_tags: "Hey, you haven't followed any tags! Continue anyway?"

View file

@ -17,6 +17,7 @@ Feature: oembed
When I fill in the following:
| status_message_fake_text | http://youtube.com/watch?v=M3r2XDceM6A&format=json |
And I press "Share"
And I wait for the ajax to finish
And I follow "My Aspects"
Then I should see a video player
And I should see a ".oembed" within ".post-content"

View file

@ -0,0 +1,94 @@
describe("app.collections.Aspects", function(){
beforeEach(function(){
Diaspora.I18n.loadLocale({
'and' : "and",
'comma' : ",",
'my_aspects' : "My Aspects"
});
var my_aspects = [{ name: 'Work', selected: true },
{ name: 'Friends', selected: false },
{ name: 'Acquaintances', selected: false }]
this.aspects = new app.collections.Aspects(my_aspects);
});
describe("#selectAll", function(){
it("selects every aspect in the collection", function(){
this.aspects.selectAll();
this.aspects.each(function(aspect){
expect(aspect.get('selected')).toBeTruthy();
});
});
});
describe("#deselectAll", function(){
it("deselects every aspect in the collection", function(){
this.aspects.deselectAll();
this.aspects.each(function(aspect){
expect(aspect.get('selected')).toBeFalsy();
});
});
});
describe("#allSelected", function(){
it("returns true if every aspect is selected", function(){
this.aspects.at(1).set('selected', true);
this.aspects.at(2).set('selected', true);
expect(this.aspects.allSelected()).toBeTruthy();
});
it("returns false if at least one aspect is not selected", function(){
expect(this.aspects.allSelected()).toBeFalsy();
});
});
describe("#toSentence", function(){
describe('without aspects', function(){
beforeEach(function(){
this.aspects = new app.collections.Aspects({ name: 'Work', selected: false })
spyOn(this.aspects, 'selectedAspects').andCallThrough();
});
it("returns the name of the aspect", function(){
expect(this.aspects.toSentence()).toEqual('My Aspects');
expect(this.aspects.selectedAspects).toHaveBeenCalled();
});
});
describe("with one aspect", function(){
beforeEach(function(){
this.aspects = new app.collections.Aspects({ name: 'Work', selected: true })
spyOn(this.aspects, 'selectedAspects').andCallThrough();
});
it("returns the name of the aspect", function(){
expect(this.aspects.toSentence()).toEqual('Work');
expect(this.aspects.selectedAspects).toHaveBeenCalled();
});
});
describe("with three aspect", function(){
it("returns the name of the selected aspect", function(){
expect(this.aspects.toSentence()).toEqual('Work');
});
it("returns the names of the two selected aspects", function(){
this.aspects.at(1).set('selected', true);
expect(this.aspects.toSentence()).toEqual('Work and Friends');
});
it("returns the names of the selected aspects in a comma-separated sentence", function(){
this.aspects.at(1).set('selected', true);
this.aspects.at(2).set('selected', true);
expect(this.aspects.toSentence()).toEqual('Work, Friends and Acquaintances');
});
});
});
describe("#selectedAspects", function(){
describe("by name", function(){
it("returns the names of the selected aspects", function(){
expect(this.aspects.selectedAspects('name')).toEqual(["Work"]);
});
});
});
});

View file

@ -0,0 +1,15 @@
describe("app.models.Aspect", function(){
describe("#toggleSelected", function(){
it("should select the aspect", function(){
this.aspect = new app.models.Aspect({ name: 'John Doe', selected: false });
this.aspect.toggleSelected();
expect(this.aspect.get("selected")).toBeTruthy();
});
it("should deselect the aspect", function(){
this.aspect = new app.models.Aspect({ name: 'John Doe', selected: true });
this.aspect.toggleSelected();
expect(this.aspect.get("selected")).toBeFalsy();
});
});
});

View file

@ -0,0 +1,42 @@
describe("app.views.Aspect", function(){
beforeEach(function(){
this.aspect = new app.models.Aspect({ name: 'Acquaintances', selected: true });
this.view = new app.views.Aspect({ model: this.aspect });
});
describe("render", function(){
beforeEach(function(){
this.view.render();
});
it('should show the aspect selected', function(){
expect(this.view.$el.hasClass('active')).toBeTruthy();
});
it('should show the name of the aspect', function(){
expect(this.view.$('a.aspect_selector').text()).toMatch('Acquaintances');
});
describe('selecting aspects', function(){
beforeEach(function(){
app.router = new app.Router();
spyOn(app.router, 'aspects_stream');
spyOn(this.view, 'toggleAspect').andCallThrough();
this.view.delegateEvents();
});
it('it should deselect the aspect', function(){
this.view.$('a.aspect_selector').trigger('click');
expect(this.view.toggleAspect).toHaveBeenCalled();
expect(this.view.$el.hasClass('active')).toBeFalsy();
expect(app.router.aspects_stream).toHaveBeenCalled();
});
it('should call #toggleSelected on the model', function(){
spyOn(this.aspect, 'toggleSelected');
this.view.$('a.aspect_selector').trigger('click');
expect(this.aspect.toggleSelected).toHaveBeenCalled();
});
});
});
});

View file

@ -0,0 +1,61 @@
describe("app.views.AspectsList", function(){
beforeEach(function(){
setFixtures('<ul id="aspects_list"></ul>');
Diaspora.I18n.loadLocale({ aspect_navigation : {
'select_all' : 'Select all',
'deselect_all' : 'Deselect all'
}});
var aspects = [{ name: 'Work', selected: true },
{ name: 'Friends', selected: false },
{ name: 'Acquaintances', selected: false }];
this.aspects = new app.collections.Aspects(aspects);
this.view = new app.views.AspectsList({ collection: this.aspects });
});
describe('rendering', function(){
beforeEach(function(){
this.view.render();
});
it('should show the corresponding aspects selected', function(){
expect(this.view.$('.active').length).toBe(1);
expect(this.view.$('.active > .aspect_selector').text()).toMatch('Work');
});
it('should show all the aspects', function(){
var aspect_selectors = this.view.$('.aspect_selector');
expect(aspect_selectors.length).toBe(3)
expect(aspect_selectors[0].text).toMatch('Work');
expect(aspect_selectors[1].text).toMatch('Friends');
expect(aspect_selectors[2].text).toMatch('Acquaintances');
});
it('should show \'Select all\' link', function(){
expect(this.view.$('.toggle_selector').text()).toMatch('Select all');
});
describe('selecting aspects', function(){
context('selecting all aspects', function(){
beforeEach(function(){
app.router = new app.Router();
spyOn(app.router, 'aspects_stream');
spyOn(this.view, 'toggleAll').andCallThrough();
spyOn(this.view, 'toggleSelector').andCallThrough();
this.view.delegateEvents();
this.view.$('.toggle_selector').click();
});
it('should show all the aspects selected', function(){
expect(this.view.toggleAll).toHaveBeenCalled();
expect(this.view.$('li.active').length).toBe(3);
});
it('should show \'Deselect all\' link', function(){
expect(this.view.toggleSelector).toHaveBeenCalled();
expect(this.view.$('.toggle_selector').text()).toMatch('Deselect all');
});
});
});
});
});

View file

@ -2,7 +2,7 @@ require 'spec_helper'
describe UserPresenter do
before do
@presenter = UserPresenter.new(bob)
@presenter = UserPresenter.new(bob, [])
end
describe '#to_json' do
@ -34,4 +34,4 @@ describe UserPresenter do
@presenter.configured_services.should include("fakebook")
end
end
end
end