Commit 1924c40d authored by Florian Staudacher's avatar Florian Staudacher
Browse files

replace vendored backbone.js/underscore.js with 'backbone-on-rails' gem

- updates underscore to 1.5.2 and backbone to 1.1.0

backbone had some breaking changes:
- fix url/urlRoot handling in models & collections
- options are no longer attached to the view by default
- collections reset when 'fetch' is called, tell it to keep the existing
  models

other changes:
- fix some events triggering multiple times in connection with deleting
  a model
- use document fragments instead of an element array for stream entries
- adapt jasmine and cucumber specs to the changed code
  * no longer test the backbone router as part of our code
  * jasmine factory already returns model instances, no need to wrap
    that again
parent 6fc5ccb9
......@@ -79,6 +79,7 @@ For more details see https://wiki.diasporafoundation.org/Updating
* Added a statistics route with general pod information, and if enabled in pod settings, total user, half year/monthly active users and local post counts [#4602](https://github.com/diaspora/diaspora/pull/4602)
* Add indication about markdown formatting in the publisher [#4589](https://github.com/diaspora/diaspora/pull/4589)
* Add captcha to signup form [#4659](https://github.com/diaspora/diaspora/pull/4659)
* Update Underscore.js 1.3.1 to 1.5.2, update Backbone.js 0.9.2 to 1.1.0 [#4662](https://github.com/diaspora/diaspora/pull/4662)
## Gem updates
* selenium-webdriver 2.34.0 -> 2.39.0
......
......@@ -125,6 +125,7 @@ group :assets do
# JavaScript
gem 'backbone-on-rails', '1.1.0'
gem 'handlebars_assets', '0.12.0'
gem 'jquery-rails', '2.1.4'
......
......@@ -43,6 +43,11 @@ GEM
fog (>= 1.8.0)
atomic (1.1.14)
bcrypt-ruby (3.1.2)
backbone-on-rails (1.1.0.0)
eco
ejs
jquery-rails
rails (>= 3.1)
bootstrap-sass (2.2.2.0)
sass (~> 3.2)
builder (3.0.4)
......@@ -99,6 +104,12 @@ GEM
thread_safe (~> 0.1)
warden (~> 1.2.3)
diff-lcs (1.2.5)
eco (1.0.0)
coffee-script
eco-source
execjs
eco-source (1.1.0.rc.1)
ejs (1.1.1)
entypo-rails (2.0.2)
railties (>= 3.1, <= 5)
erubis (2.7.0)
......@@ -436,6 +447,7 @@ DEPENDENCIES
acts_as_api (= 0.4.2)
addressable (= 2.3.5)
asset_sync (= 1.0.0)
backbone-on-rails (= 1.1.0)
bootstrap-sass (= 2.2.2.0)
capybara (= 2.2.1)
carrierwave (= 0.9.0)
......
app.collections.Comments = Backbone.Collection.extend({
model: app.models.Comment,
url : function(){
return this.post.url() + "/comments"
},
url: function() { return _.result(this.post, 'url') + '/comments'; },
initialize : function(models, options) {
this.post = options.post
this.post = options.post;
},
make : function(text){
var self = this
var self = this;
var comment = new app.models.Comment({text: text })
var comment = new app.models.Comment({text: text });
var deferred = comment.save({}, {
url : self.url(),
url: '/posts/'+this.post.id+'/comments',
success: function() {
comment.set({author: app.currentUser.toJSON(), parent: self.post })
self.add(comment)
......
......@@ -16,16 +16,16 @@ app.models.Stream = Backbone.Collection.extend({
},
fetch: function() {
if(this.isFetching()){ return false }
var url = this.url()
if( this.isFetching() ) return false;
var url = this.url();
this.deferred = this.items.fetch({
add : true,
remove : false,
url : url
}).done(_.bind(this.triggerFetchedEvents, this))
},
isFetching : function(){
return this.deferred && this.deferred.state() == "pending"
isFetching : function() {
return (this.deferred && this.deferred.state() == "pending");
},
triggerFetchedEvents : function(resp){
......@@ -56,7 +56,7 @@ app.models.Stream = Backbone.Collection.extend({
/* This function is for adding a large number of posts one by one.
* Mainly used by backbone when loading posts from the server
*
*
* After adding the posts, you have to trigger "fetched" on the
* stream for the changes to take effect in the infinite stream view
*/
......
......@@ -9,11 +9,6 @@ app.views.Base = Backbone.View.extend({
},
setupRenderEvents : function(){
if(this.model) {
//this should be in streamobjects view
this.model.bind('remove', this.remove, this);
}
// this line is too generic. we usually only want to re-render on
// feedback changes as the post content, author, and time do not change.
//
......@@ -88,9 +83,21 @@ app.views.Base = Backbone.View.extend({
destroyModel: function(evt) {
evt && evt.preventDefault();
var self = this;
var url = this.model.urlRoot + '/' + this.model.id;
if (confirm(Diaspora.I18n.t("confirm_dialog"))) {
this.model.destroy();
this.remove();
this.model.destroy({ url: url })
.done(function() {
self.remove();
})
.fail(function() {
var flash = new Diaspora.Widgets.FlashMessages;
flash.render({
success: false,
notice: Diaspora.I18n.t('failed_to_remove')
});
});
}
}
});
......
......@@ -9,8 +9,7 @@
app.views.InfScroll = app.views.Base.extend({
setupInfiniteScroll : function() {
this.postViews = this.postViews || [];
this.appendedPosts = [];
this.prependedPosts = [];
this._resetPostFragments();
this.bind("loadMore", this.fetchAndshowLoader, this);
this.stream.bind("fetched", this.finishedLoading, this);
......@@ -22,6 +21,11 @@ app.views.InfScroll = app.views.Base.extend({
$(window).scroll(throttledScroll);
},
_resetPostFragments: function() {
this.appendedPosts = document.createDocumentFragment();
this.prependedPosts = document.createDocumentFragment();
},
postRenderTemplate : function() {
if(this.stream.isFetching()) { this.showLoader() }
},
......@@ -29,6 +33,7 @@ app.views.InfScroll = app.views.Base.extend({
createPostView : function(post){
var postView = new this.postClass({ model: post, stream: this.stream });
if (this.collection.at(0).id == post.id) {
// post is first in collection - insert view at top of the list
this.postViews.unshift(postView);
} else {
this.postViews.push(postView);
......@@ -36,12 +41,13 @@ app.views.InfScroll = app.views.Base.extend({
return postView;
},
// called for every item inserted in this.collection
addPostView : function(post) {
var el = this.createPostView(post).render().el;
if (this.collection.at(0).id == post.id) {
this.prependedPosts.unshift(el);
this.prependedPosts.insertBefore(el, this.prependedPosts.firstChild);
} else {
this.appendedPosts.push(el);
this.appendedPosts.appendChild(el);
}
},
......@@ -55,15 +61,16 @@ app.views.InfScroll = app.views.Base.extend({
renderInitialPosts : function(){
this.$el.empty();
var els = [];
var els = document.createDocumentFragment();
this.stream.items.each(_.bind(function(post){
els.push(this.createPostView(post).render().el);
els.appendChild(this.createPostView(post).render().el);
}, this))
this.$el.append(els);
this.$el.html(els);
},
fetchAndshowLoader : function(){
if(this.stream.isFetching()) { return false }
if( this.stream.isFetching() ) return false;
this.stream.fetch();
this.showLoader();
},
......@@ -73,11 +80,9 @@ app.views.InfScroll = app.views.Base.extend({
},
finishedAdding: function() {
var el = $('<span></span>');
this.$el.prepend(this.prependedPosts);
this.$el.append(this.appendedPosts);
this.appendedPosts = [];
this.prependedPosts = [];
this._resetPostFragments();
},
finishedLoading: function() {
......@@ -96,7 +101,7 @@ app.views.InfScroll = app.views.Base.extend({
, bufferPx = 500;
if(distFromBottom < bufferPx) {
this.trigger("loadMore")
this.trigger("loadMore");
}
}
});
......@@ -12,6 +12,10 @@ app.views.PublisherAspectSelector = Backbone.View.extend({
"click .dropdown_list > li": "toggleAspect"
},
initialize: function(opts) {
this.form = opts.form;
},
// event handler for aspect selection
toggleAspect: function(evt) {
var el = $(evt.target);
......@@ -53,7 +57,7 @@ app.views.PublisherAspectSelector = Backbone.View.extend({
var self = this;
// remove previous selection
this.options.form.find('input[name="aspect_ids[]"]').remove();
this.form.find('input[name="aspect_ids[]"]').remove();
// create fields for current selection
this.$('.dropdown_list li.selected').each(function() {
......@@ -80,7 +84,7 @@ app.views.PublisherAspectSelector = Backbone.View.extend({
_addHiddenAspectInput: function(id) {
var uid = _.uniqueId('aspect_ids_');
this.options.form.append(
this.form.append(
'<input id="'+uid+'" name="aspect_ids[]" type="hidden" value="'+id+'">'
);
}
......
......@@ -8,23 +8,29 @@
// for describing their use to new users.
app.views.PublisherGettingStarted = Backbone.View.extend({
initialize: function(opts) {
this.el_first_msg = opts.el_first_msg;
this.el_visibility = opts.el_visibility;
this.el_stream = opts.el_stream;
},
// initiate all the popover message boxes
show: function() {
this._addPopover(this.options.el_first_msg, {
this._addPopover(this.el_first_msg, {
trigger: 'manual',
offset: 30,
id: 'first_message_explain',
placement: 'right',
html: true
}, 600);
this._addPopover(this.options.el_visibility, {
this._addPopover(this.el_visibility, {
trigger: 'manual',
offset: 10,
id: 'message_visibility_explain',
placement: 'bottom',
html: true
}, 1000);
this._addPopover(this.options.el_stream, {
this._addPopover(this.el_stream, {
trigger: 'manual',
offset: -5,
id: 'stream_explain',
......@@ -34,8 +40,8 @@ app.views.PublisherGettingStarted = Backbone.View.extend({
// hide some popovers when a post is created
this.$('.button.creation').click(function() {
this.options.el_visibility.popover('hide');
this.options.el_first_msg.popover('hide');
this.el_visibility.popover('hide');
this.el_first_msg.popover('hide');
});
},
......
......@@ -13,7 +13,10 @@ app.views.PublisherServices = Backbone.View.extend({
tooltipSelector: '.service_icon',
initialize: function() {
initialize: function(opts) {
this.input = opts.input;
this.form = opts.form;
// init tooltip plugin
this.$(this.tooltipSelector).tooltip();
},
......@@ -32,7 +35,7 @@ app.views.PublisherServices = Backbone.View.extend({
// keep track of character count
_createCounter: function() {
// remove any obsolete counters
this.options.input.siblings('.counter').remove();
this.input.siblings('.counter').remove();
// create new counter
var min = 40000;
......@@ -42,18 +45,18 @@ app.views.PublisherServices = Backbone.View.extend({
var num = parseInt($(value).attr('maxchar'));
if (min > num) { min = num; }
});
this.options.input.charCount({allowed: min, warning: min/10 });
this.input.charCount({allowed: min, warning: min/10 });
}
},
// add or remove the input containing the selected service
_toggleServiceField: function(provider) {
var hidden_field = this.options.form.find('input[name="services[]"][value="'+provider+'"]');
var hidden_field = this.form.find('input[name="services[]"][value="'+provider+'"]');
if(hidden_field.length > 0){
hidden_field.remove();
} else {
var uid = _.uniqueId('services_');
this.options.form.append(
this.form.append(
'<input id="'+uid+'" name="services[]" type="hidden" value="'+provider+'">');
}
}
......
......@@ -8,7 +8,9 @@ app.views.PublisherUploader = Backbone.View.extend({
allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'tif', 'tiff'],
sizeLimit: 4194304, // bytes
initialize: function() {
initialize: function(opts) {
this.publisher = opts.publisher;
this.uploader = new qq.FileUploaderBasic({
element: this.el,
button: this.el,
......@@ -31,9 +33,9 @@ app.views.PublisherUploader = Backbone.View.extend({
});
this.el_info = $('<div id="fileInfo" />');
this.options.publisher.el_wrapper.before(this.el_info);
this.publisher.el_wrapper.before(this.el_info);
this.options.publisher.el_photozone.on('click', '.x', _.bind(this._removePhoto, this));
this.publisher.el_photozone.on('click', '.x', _.bind(this._removePhoto, this));
},
progressHandler: function(id, fileName, loaded, total) {
......@@ -48,7 +50,7 @@ app.views.PublisherUploader = Backbone.View.extend({
// add photo placeholders to the publisher to indicate an upload in progress
_addPhotoPlaceholder: function() {
var publisher = this.options.publisher;
var publisher = this.publisher;
publisher.setButtonsEnabled(false);
publisher.el_wrapper.addClass('with_attachments');
......@@ -78,7 +80,7 @@ app.views.PublisherUploader = Backbone.View.extend({
// replace the first photo placeholder with the finished uploaded image and
// add the id to the publishers form
_addFinishedPhoto: function(id, url) {
var publisher = this.options.publisher;
var publisher = this.publisher;
// add form input element
publisher.$('.content_creation form').append(
......@@ -103,7 +105,7 @@ app.views.PublisherUploader = Backbone.View.extend({
},
_cancelPhotoUpload: function() {
var publisher = this.options.publisher;
var publisher = this.publisher;
var placeholder = publisher.el_photozone.find('li.loading').first();
placeholder
.removeClass('loading')
......@@ -125,9 +127,9 @@ app.views.PublisherUploader = Backbone.View.extend({
$.when(photo.fadeOut(400)).then(function(){
photo.remove();
if( self.options.publisher.$('.publisher_photo').length == 0 ) {
if( self.publisher.$('.publisher_photo').length == 0 ) {
// no more photos left...
self.options.publisher.el_wrapper.removeClass('with_attachments');
self.publisher.el_wrapper.removeClass('with_attachments');
}
self.trigger('change');
......
......@@ -25,7 +25,9 @@ app.views.Publisher = Backbone.View.extend({
"keypress #location_address" : "avoidEnter"
},
initialize : function(){
initialize : function(opts){
this.standalone = opts ? opts.standalone : false;
// init shortcut references to the various elements
this.el_input = this.$('#status_message_fake_text');
this.el_hiddenInput = this.$('#status_message_text');
......@@ -47,7 +49,7 @@ app.views.Publisher = Backbone.View.extend({
// hide close and preview buttons, in case publisher is standalone
// (e.g. bookmarklet, mentions popup)
if( this.options.standalone ) {
if( this.standalone ) {
this.$('#hide_publisher').hide();
this.el_preview.hide();
}
......
......@@ -25,8 +25,7 @@ app.views.StreamPost = app.views.Post.extend({
tooltipSelector : ".timeago, .post_scope, .block_user, .delete",
initialize : function(){
this.model.bind('remove', this.remove, this);
this.model.on('remove', this.remove, this);
//subviews
this.commentStreamView = new app.views.CommentStream({model : this.model});
this.oEmbedView = new app.views.OEmbed({model : this.model});
......
......@@ -60,6 +60,7 @@ en:
show_more: "show more"
failed_to_like: "Failed to like!"
failed_to_post_message: "Failed to post message!"
failed_to_remove: "Failed to remove the entry!"
comments:
show: "show all comments"
hide: "hide comments"
......
......@@ -105,9 +105,10 @@ And /^I hover over the "([^"]+)"$/ do |element|
end
When /^I prepare the deletion of the first post$/ do
within('.stream_element', match: :first) do
find('.controls').hover
find('.remove_post').click
within(find('.stream .stream_element')) do
ctrl = find('.controls')
ctrl.hover
ctrl.find('.remove_post').click
end
end
......
......@@ -20,11 +20,11 @@ end
World(WithinHelpers)
Given /^(?:|I )am on (.+)$/ do |page_name|
visit path_to(page_name)
navigate_to(page_name)
end
When /^(?:|I )go to (.+)$/ do |page_name|
visit path_to(page_name)
navigate_to(page_name)
end
When /^(?:|I )press "([^"]*)"(?: within "([^"]*)")?$/ do |button, selector|
......
......@@ -26,7 +26,11 @@ module NavigationHelpers
when /^the requestors profile$/
person_path(Request.where(:recipient_id => @me.person.id).first.sender)
when /^"([^\"]*)"'s page$/
person_path(User.find_by_email($1).person)
p = User.find_by_email($1).person
{ path: person_path(p),
# '.diaspora_handle' on desktop, '.description' on mobile
special_elem: { selector: '.diaspora_handle, .description', text: p.diaspora_handle }
}
when /^my account settings page$/
edit_user_path
when /^my new profile page$/
......@@ -46,10 +50,21 @@ module NavigationHelpers
path_to "the new user session page"
end
def post_path_by_content text
def post_path_by_content(text)
p = Post.find_by_text(text)
post_path(p)
end
def navigate_to(page_name)
path = path_to(page_name)
unless path.is_a?(Hash)
visit(path)
else
visit(path[:path])
await_elem = path[:special_elem]
find(await_elem.delete(:selector), await_elem)
end
end
end
World(NavigationHelpers)
......@@ -16,7 +16,7 @@ describe("app.models.Stream", function() {
it("it fetches posts from the window's url, and ads them to the collection", function() {
this.stream.fetch()
expect(this.stream.items.fetch).toHaveBeenCalledWith({ add : true, url : this.expectedPath});
expect(this.stream.items.fetch).toHaveBeenCalledWith({ remove: false, url: this.expectedPath});
});
it("returns the json path with max_time if the collection has models", function() {
......@@ -25,7 +25,7 @@ describe("app.models.Stream", function() {
this.stream.add(post);
this.stream.fetch()
expect(this.stream.items.fetch).toHaveBeenCalledWith({ add : true, url : this.expectedPath + "?max_time=1234"});
expect(this.stream.items.fetch).toHaveBeenCalledWith({ remove: false, url: this.expectedPath + "?max_time=1234"});
});
it("triggers fetched on the stream when it is fetched", function(){
......
......@@ -34,42 +34,38 @@ describe('app.Router', function () {
});
describe("when routing to /stream and hiding inactive stream lists", function() {
var router;
var aspects;
var tagFollowings;
beforeEach(function() {
router = new app.Router();
});
it('calls hideInactiveStreamLists', function () {
var hideInactiveStreamLists = spyOn(app.router, 'hideInactiveStreamLists').andCallThrough();
spyOn(window.history, 'pushState').andCallFake(function (data, title, url) {
var route = app.router._routeToRegExp("stream");
var args = app.router._extractParameters(route, url.replace(/^\//, ""));
app.router.stream(args[0]);
});
app.router.navigate('/stream');
var hideInactiveStreamLists = spyOn(router, 'hideInactiveStreamLists').andCallThrough();
router.stream();
expect(hideInactiveStreamLists).toHaveBeenCalled();
});
it('hides the aspects list', function(){
var aspects = new app.collections.Aspects([{ name: 'Work', selected: true }]);
aspects = new app.collections.Aspects([{ name: 'Work', selected: true }]);
var aspectsListView = new app.views.AspectsList({collection: aspects});
var hideAspectsList = spyOn(aspectsListView, 'hideAspectsList').andCallThrough();
app.router.aspects_list = aspectsListView;
spyOn(window.history, 'pushState').andCallFake(function (data, title, url) {
var route = app.router._routeToRegExp("stream");
var args = app.router._extractParameters(route, url.replace(/^\//, ""));
app.router.stream(args[0]);
});
app.router.navigate('/stream');
router.aspects_list = aspectsListView;
router.stream();
expect(hideAspectsList).toHaveBeenCalled();
});
it('hides the followed tags view', function(){
var tagFollowings = new app.collections.TagFollowings();
tagFollowings = new app.collections.TagFollowings();
var followedTagsView = new app.views.TagFollowingList({collection: tagFollowings});
var hideFollowedTags = spyOn(followedTagsView, 'hideFollowedTags').andCallThrough();
app.router.followedTagsView = followedTagsView;
spyOn(window.history, 'pushState').andCallFake(function (data, title, url) {
var route = app.router._routeToRegExp("stream");