Incorrect base URL for Backbone DELETE requests - uses relative instead of absolute URLs - django

TL;DR version:
Building a Phonegap app using Backbone, and have a model called Client and a collection called Clients. Using a Tastypie API to communicate with a separate server. When I run fetch(), the URL uses the correct absolute URL (something like http://127.0.0.1:8000/api/v1/client/1/, but when I run Client.destroy(), it uses a relative URL of file:///api/v1/client/1/. How can I make it use the absolute URL for deleting the object?
Long version:
I'm building a mobile app with Backbone.js that consumes a Django/Tastypie API, and I've run into some seemingly odd behaviour that I can't figure out.
I define a base URL for the server at the top of the file:
// Set the base URL for querying the API
baseUrl = 'http://127.0.0.1:8000/api/v1/';
I have the following model and collection:
// Client model
Client = Backbone.Model.extend({
urlRoot: baseUrl + 'client',
// Default values
defaults: {
id: '',
name: '',
mobile: '',
email: '',
notes: '',
operator: '',
date_client_joined: '',
address: '',
postcode: ''
}
});
// Client collection
Clients = Backbone.Collection.extend({
// Will hold Client objects
model: Client,
// Set URL
url: baseUrl + 'client/'
});
And the individual clients are rendered in a list using the following view:
// Client list item view
ClientListItemView = Backbone.View.extend({
tagName: 'li',
events: {
'click .delete': 'deleteclient'
},
render: function () {
// Render the client list item template
var template = _.template($('#client-list-item-template').html());
this.$el.html(template(this.model.toJSON()));
// Return the object
return this;
},
deleteclient: function () {
this.model.destroy();
return false;
}
});
Now, the app actually uses jQuery Mobile and each client has a Delete button next to it with a class of delete, so the deleteclient function is executed each time one of these buttons is clicked. I'm also using backbone-tastypie to iron out the inconsistencies between Backbone and Tastypie.
The deleteclient function is running, but it sends the HTTP DELETE request to a relative URL of file:///api/v1/client/1/ (as this is a Phonegap app, I'm just viewing the files locally). From the documentation setting urlRoot manually seems like the way to go, but doing so didn't seem to solve the issue. Running the fetch() function to populate the collection works absolutely fine, though - it uses the correct absolute URL.
So, my question is how can I override the default behaviour and ensure my HTTP DELETE request is sent to the correct URL?

By looking at your code it should work ok. The Model in backbone already has a url() function defined which should do this:
url: function() {
var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError();
if (this.isNew()) return base;
return base + (base.charAt(base.length - 1) === '/' ? '' : '/') + encodeURIComponent(this.id);
},
Can you use the debugger to see if it enters inside this code and what is the result of it? Mainly check the values from the _.result() calls...
Anyway, you can override the url property in your models rather than passing it in every call to destroy():
Client = Backbone.Model.extend({
url: function () { return baseUrl + 'client/' + this.id + '/'; }
// other code...
});

I found a solution, though I'm not entirely happy with it:
deleteclient: function () {
if (confirm('Are you sure you wish to delete this client?')) {
// Destroy the model
this.model.destroy({
url: baseUrl + 'client/' + this.model.get('id') + '/'
});
// Remove the view
this.remove();
}
}
Basically, if I explicitly pass through the URL to destroy(), that does the trick. It's a little annoying that I can't find a more DRY way to do this, so I'm open to any other method of doing the same thing.

Related

Ember Simple Auth immediately invalidates the session after authenticating with Torii

I am trying to set up Torii with my own OAuth flow and Ember-Simple-Auth. I can get a successful authentication event, but immediately after I authenticate, the invalidateSession trigger is fired causing my session to end. I can see this by intercepting sessionInvalidated() in /app/routes/application.js (which has the ApplicationRouteMixin).
Have any of you come across this? Is there something peculiar that would cause an immediate session validation? Any advice would be greatly appreciated.
EDIT: I think it has to do with the torii popup code because the first return works, but the second doesn't. Any thoughts?
import OAuth2 from 'torii/providers/oauth2-code';
import {configurable} from 'torii/configuration';
export default OAuth2.extend({
name: 'api',
init() { this.set('clientID', this.get('apiKey')); },
baseUrl: configurable('baseUrl'),
redirectUri: configurable('redirectUri'),
responseParams: ['access_token', 'user_id', 'first_name'],
requiredUrlParams: ['client_id', 'redirect_uri', 'response_type'],
open() {
let name = this.get('name');
let url = this.buildUrl();
let redirectUri = this.get('redirectUri');
let responseParams = this.get('responseParams');
// this return works
return { 'yes' : 'no' }
// this return causes the immediate invalidation
return this.get('popup').open(url, responseParams).then((authData) => {
var missingResponseParams = [];
responseParams.forEach(function(param){
if (authData[param] === undefined) {
missingResponseParams.push(param);
}
});
if (missingResponseParams.length){
throw new Error("The response from the provider is missing " +
"these required response params: " + missingResponseParams.join(', '));
}
return {
access_token: authData.access_token,
first_name: authData.first_name,
user_id: authData.user_id,
provider: name,
redirectUri: redirectUri
};
});
}
});
the real answer is using this fork: https://github.com/simplabs/ember-simple-auth/pull/931 (hopefully it'll be in master soon).
You might have this.get('session').invalidate(); somewhere. Probably in one of your controllers action properties. You would usually put that in your actions for your logout button. Maybe you copy and pasted it by accident. If you post some code I might be able to look at it some more

Ember Data use different URL for requests

I am beginner for ember.js. I am tried to use ember data for restful api. However there is some convention on the path. For example App.Post.find() corresponding to http://www.example.com/posts.
However due to my application current pattern, all my list of data is listed under /post/list, what shall i do? here i am using post not posts and not using /posts but /post/list.
Please help.
It's simple enough to change how the adapter builds the url. You just extend the current adapter and override the methods that build the url. In your case I am setting the application adapter (meaning it would apply to all types). I wasn't positive how the url should have been built when you supply a particular id, but I'm pretty sure this is enough to get you started, and you can play the string manipulation game from this point.
App.ApplicationAdapter= DS.RESTAdapter.extend({
pathForType: function(type) {
var camelized = Ember.String.camelize(type);
return camelized; //Ember.String.pluralize(camelized);
},
buildURL: function(type, id) {
var url = [],
host = Em.get(this, 'host'),
prefix = this.urlPrefix();
if (type) { url.push(this.pathForType(type)); }
url.push('list');
if (id) { url.push(id); }
if (prefix) { url.unshift(prefix); }
url = url.join('/');
if (!host && url) { url = '/' + url; }
return url;
}
});
Example: http://emberjs.jsbin.com/OxIDiVU/690/edit

Typeahead/Bloodhound - Using Jquery Ajax for remote causes only a single server side request

I need to use a jquery ajax setup in Bloodhound's remote property since I have a server side page that takes POST requests only. Everything works, but just once. Any subsequent change to the text in the typeahead input box calls the filter function, but does not fire a new server side request to fetch new data. It just filters through the data that it got in the first request. I need for it make a new request as the user removes the text and types in something else.
I am new to typeahead and I am spending way too much time trying to figure this out. Here is my code.
var users = new Bloodhound({
datumTokenizer: function (d) {
return Bloodhound.tokenizers.whitespace(d.value);
},
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: 'fake.jsp',
filter: function (users) {
return $.map(users, function (user) {
return {
value: user.USER_ID,
name: user.DISPLAYNAME
};
});
},
ajax: {
type: 'POST',
data: {
param: function(){
return $('#userid').val();
}
},
context: this
}
}
});
users.initialize(true);
$('#userid').typeahead({
minLength: 3,
highlight: true
}, {
name: 'userslist',
displayKey: 'name',
source: users.ttAdapter()
});
I had the same solution and discovered jQuery's cache: false; option does not work in this situation for whatever reason. Here is the solution I found:
remote: {
url: ...
replace: function(url, query) {
return url + "#" + query; // used to prevent the data from being cached. New requests aren't made without this (cache: false setting in ajax settings doesn't work)
}
}
try this:
remote: {
url: 'fake.jsp/?' + Math.random(),
.
.
.
it's not really the solution but at least the results will be fetched from server everytime the page is refreshed.

Ember 1.0 Final Internet Explorer 8 and 9 (History API - pushState and replaceState)

I was under the impression that Ember was well tested under some older versions of IE. However, upon finally firing up my near complete app (form wizard). I'm noticing IE is complaining about replaceState and pushState, two methods that are not supported according to http://caniuse.com/#search=pushState
Are there any workarounds for this?
SCRIPT438: Object doesn't support property or method 'replaceState'
get(this, 'history').replaceState(state, null, path);
UPDATE: As of Ember 1.5.0+ I can confirm that they added 'auto' which should eliminate the need for the example below.
App.Router.reopen({
location: 'auto'
});
Original Answer:
Apparently you need to feature detect the history API:
if (window.history && window.history.pushState) {
App.Router.reopen({
location: 'history'
});
}
There is no work around. If you need to use real URLs (/users) instead of hash URLs (/#/users) then you have to either exclude IE 8 & 9 from your list of supported browsers, or you need to use Ember in a "progressive enhancement" manner, still serve valid content from the server for each real URL that is visited, and use feature detection to selectively enable your Ember app.
To properly support both pushSate and non-pushState browsers, you need to have a translator between the 2 different state mechanisms.
For example, let's say your rootURL is '/admin/' and you start with this URL:
/admin/users/123
For IE8/9, you'll want to redirect the user to '/admin/#/users/123' before Ember's routing mechanism takes over. Likewise, if you start with this URL:
/admin/#/users/123
...then for browsers that support pushState, you'll want to replace the state to '/admin/users/123' before Ember's routing mechanism takes over.
This is the default behavior of Backbone's router and it works fairly well.
To achieve this result in Ember, you can do something like this, which was inspired by Backbone's source code:
App.Router.reopen({
rootURL: '/admin/',
init: function () {
this.translateRoute();
this._super();
},
translateRoute: function () {
var hasPushState = window.history && window.history.pushState;
var atRoot = window.location.pathname === this.rootURL;
var fragment = decodeURIComponent(window.location.pathname);
if (!fragment.indexOf(this.rootURL))
fragment = fragment.substr(this.rootURL.length);
if (hasPushState)
this.location = 'history';
// If we started with a route from a pushState-enabled browser,
// but we're currently in a browser that doesn't support it...
if (!hasPushState && !atRoot) {
window.location.replace(this.rootURL + '#/' + fragment);
return;
}
// If we started with a hash-based route,
// but we're currently in a browser that supports pushState...
if (hasPushState && atRoot && window.location.hash) {
fragment = window.location.hash.replace(/^(#\/|[#\/])/, '');
window.history.replaceState({}, document.title, window.location.protocol + '//' + window.location.host + this.rootURL + fragment);
}
}
});

Backbone.js Problems with fetch() in IE

I'm building a web app with Django on back-end and Backbone.js on front-end
I have problems with IE when I'm trying to fetch data from the server. When I run my HTML page in IE, the collection fetch always invoke the error func.
My code:
$(function(){
var Chapter = Backbone.Model.extend({});
var Chapters = Backbone.Collection.extend({
model: Chapter,
url: 'http://ip.olya.ivanovss.info/chapters'
});
var chapters = new Chapters();
var Router = new (Backbone.Router.extend({
routes: {
"": "choose_activity",
"/": "choose_activity"
},
choose_activity: function () {
chapters.fetch({
success: function () {
AppView.render();
},
error: function() {
alert('error');
}
});
}
}))();
var AppView = new (Backbone.View.extend({
el: '.popup',
templates: {
choose_activity: Handlebars.compile($('#tpl-activities').html())
},
render: function () {
this.$el.html(this.templates["choose_activity"]({ chapters: chapters.toJSON()}));
}
}))();
Backbone.history.start();
});
Django's View:
def chapters(request):
chapters = list(Chapter.objects.order_by('id'))
response = HttpResponse(json.dumps(chapters, default=encode_myway), mimetype='text/plain')
if request.META.get('HTTP_ORIGIN', None) in ('http://localhost', 'http://html.olya.ivanovss.info', 'http://10.0.2.2'):
response['Access-Control-Allow-Origin'] = request.META['HTTP_ORIGIN']
return response
Thank you in advance
IE7 doesn't support CORS.
There are 2 ways around this. The EASY way is Proxy over your API. My Python is rusty (I'm a Node/PHP dev), but I'm sure that there are a million and one resources on how do do this. The good thing about this is you don't have to touch the API. But it means your local server has to CURL and return every single request from your API server.
And second (and much less server intensive way) is JSONP! The idea of JSONP is that it appends a <script> to the document with the URL you specify. jQuery appends a ?callback=jQueryNNN where NNN is a random number. So effectively when the <script> loads, it calls jQueryNNN('The Response Text') and jQuery knows to parse the response from there. The bad thing about this is you have to wrap all of your responses on the API side (which is super easy if you're just starting, not so easy if you already have an infrastructure built out).
The annoying things about JSONP is that by it's nature you can't do a POST/PUT/DELETE. BUT you can emulate it if you have access to the API:
Backbone.emulateHTTP = true;
model.save(); // POST to "/collection/id", with "_method=PUT" + header.
To integrate JSONP with Backbone is pretty simple (little secret Backbone.sync uses jQuery's $.ajax() and the options parameters forwards over to jQuery ;)).
For each one of your models/collections which access a cross origin you can add a su
var jsonpSync = function (method, model, options) {
options.timeout = 10000; // for 404 responses
options.dataType = "jsonp";
return Backbone.sync(method, model, options);
};
In each collection and model what does cross-origin:
var MyCollection = Backbone.Collection.extend({
sync : jsonpSync
});
Or just overwrite the whole Backbone sync
Backbone.__sync = Backbone.sync;
var jsonpSync = function (method, model, options) {
options.timeout = 10000; // for 404 responses
options.dataType = "jsonp";
return Backbone.__sync(method, model, options);
};
Backbone.sync = jsonpSync;
On the server side you can do: this to return a JSONP response (copy pasted here):
def randomTest(request):
callback = request.GET.get('callback', '')
req = {}
req ['title'] = 'This is a constant result.'
response = json.dumps(req)
response = callback + '(' + response + ');'
return HttpResponse(response, mimetype="application/json")