Backbone.js automagic syncing of Collections and Models

The idea here is to periodically fetch() and keep the client and server Collections in sync in such a way that the consuming View(s) only get updated when a Model is added to or removed from a Collection, or the attributes of one of the Models in it change. What's done is:

  • Compare the existing Collection to the incoming set and remove every Model from the existing Collection that is not in the incoming set.
  • Compare the incoming set to the existing Collection and add every Model from the incoming set to the existing Collection that is not already there.
  • Compare each Model in the two sets and update the ones in the existing Collection to the ones in the incoming set that are different.

The first two steps compare the Model's resource_uris and the last part is done with SHA1 hashes.

window.Model = Backbone.Model.extend({
    urlRoot: BASE_API + 'model/',
    defaults: {
        foo: ''
    },
    initialize: function() {
        console.log('Model->initialize()', this);
        this.bind('change', function(model) {
            console.log('Model->change()', model);
        });
    }
});
window.ModelCollection = Backbone.Collection.extend({
    model: Model,
    url: BASE_API + 'model/',
    initialize: function() {
        console.log('ModelCollection->initialize()', this);
        this.bind('add', function(model) {
            console.log('ModelCollection->add()', model);
        });
        this.bind('remove', function(model) {
            console.log('ModelCollection->remove()', model);
        });
    }
});

window.Root = Backbone.Model.extend({
    urlRoot: BASE_API + 'root/',
    defaults: {
        models: new ModelCollection()
    },
    parse: function(data) {
        var attrs = data && data.objects && ( _.isArray( data.objects ) ? data.objects[ 0 ] : data.objects ) || data;
        var model = this;
        incoming_model_uris = _.map(attrs.models, function(model) {
            return model.resource_uri;
        });
        existing_model_uris = this.get('models').map(function(model) {
            return model.get('resource_uri');
        });
        _.each(existing_model_uris, function(uri) {
            if(incoming_model_uris.indexOf(uri) == -1) {
                model.get('models').remove(model.get('models').get(uri));
            }
        });
        _.each(incoming_model_uris, function(uri) {
            if(existing_model_uris.indexOf(uri) == -1) {
                model.get('models').add(_.detect( attrs.models, function(model) { return model.resource_uri == uri; }));
            }
        });
        _.each(attrs.models, function(model) {
            if(Sha1.hash(JSON.stringify(model)) != Sha1.hash(JSON.stringify(model.get('models').get(model.resource_uri)))) {
                model.get('models').get(model.resource_uri).set(model);
            }
        });         

        delete attrs.models;        

        return attrs;
    },
    initialize: function() {
        _.bindAll(this, 'parse');
        this.fetch();
    }
});

Update 3: looking at this afterwards, I'm not sure of the complete watertightness of the above. Perhaps the Backbone Poller project would be be a better approach.

Update 2: In order to avoid borking on an empty response, do:

var attrs = Backbone.Model.prototype.parse.apply(this, data);
if(!attrs) return;

Update: My Javascript-Fu is weak which made me not see the obvious. As suggested in Backbone.js documentation, you can call the parent's implementation like this:

Backbone.Model.prototype.method.apply(this, args);

So, instead of unnecessarily copypasting behavior from Backbone-tastypie.js, we can say:

var attrs = Backbone.Model.prototype.parse.apply(this, data);

…and still have Backbone-tastypie.js do it's parsing thing for us.

Ugh.

Tagged with:

Categorised as:


Note to self: Django/South basic usage

Not that I understand how this schema migration thing actually works. Anyway, initially, on an empty database:
./manage.py syncdb --noinput
./manage.py schemamigration YOUR_APP --initial
./manage.py migrate YOUR_APP 0001 --fake
Then, after fiddling with your models:
./manage.py schemamigration YOUR_APP --auto
./manage.py migrate YOUR_APP
That should do it. But like I said, my understanding of the system is very limited and I'm sure there are cases when this simplistic pattern just won't cut it. My needs, however, at the moment are not the most complicated.

Tagged with:

Categorised as:


Note to self: Django's Error: cannot import name Foo

If Foo is definitely defined, then this is most likely a circular import which can be avoided by dropping the "redundant" import statement and referring not directly to the Model itself, but its' name. This is known as a lazy relationship:
foo = models.ForeignKey('Foo')

Tagged with:

Categorised as: