Ruby 2.0.0, Rails 4.0.3, Windows 8.1 Update, PostgreSQL 9.3.3
I have code that uses JavaScript to power dependent selects. To do so, it references a controller method that retrieves the data for the following select. I'm told that, because that method is non-standard, this is not RESTful.
I understand that REST is a set of specific constraints regarding client/server communications. I've read some information about it but certainly don't have in-depth knowledge. I am curious about the impact and resolution. So, regarding the question about my configuration and REST: First, would that be accurate that it is not RESTful? Second, how does that impact my application? Third, what should/could I do to resolve that? Providing one example:
The route is: (probably the concern?)
post 'cars/make_list', to: 'cars#make_list'
This is the first select: (OBTW, I use ERB but removed less than/percent)
= f.input(:ymm_year_id, {input_html: {form: 'edit_car', car: #car, value: #car.year}, collection: YmmYear.all.order("year desc").collect { |c| [c.year, c.id] }, prompt: "Year?"})
This is the dependent select:
= render partial: "makes", locals: {form: 'edit_car', car: #car}
This is the partial:
= simple_form_for car,
defaults: {label: false},
remote: true do |f|
makes ||= ""
make = ""
make = car.make_id if car.class == Car and Car.exists?(car.id)
if !makes.blank?
= f.input :ymm_make_id, {input_html: {form: form, car: car, value: make}, collection: makes.collect { |s| [s.make, s.id] }, prompt: "Make?"}
else
= f.input :ymm_make_id, {input_html: {form: form, car: car, value: make}, collection: [], prompt: "Make?"}
end
end
JS:
$(document).ready(function () {
...
// when the #year field changes
$("#car_ymm_year_id").change(function () {
var year = $('select#car_ymm_year_id :selected').val();
var form = $('select#car_ymm_year_id').attr("form");
var car = $('select#car_ymm_year_id').attr("car");
$.post('/cars/make_list/',
{
form: form,
year: year,
car: car
},
function (data) {
$("#car_ymm_make_id").html(data);
});
return false;
});
...
});
And the method:
def make_list
makes = params[:year].blank? ? "" : YmmMake.where(ymm_year_id: params[:year]).order(:make)
render partial: "makes", locals: {car: params[:car], form: params[:form], makes: makes}
end
If I had to describe if, being RESTful means that:
You provide meaningful resources names
You use the HTTP verbs to express your intents
You make proper use of HTTP codes to indicate status
Provide meaningful resource names
As you probably heard it before, everything in REST is about resources. But from the outside, it's just the paths you expose. Your resources are then just a bunch of paths such as:
GET /burgers # a collection of burgers
GET /burger/123 # a burger identified with id 123
GET /burger/123/nutrition_facts # the nutrition facts of burger 123
POST /burgers # with data: {name: "humble jack", ingredients: [...]} to create a new burger
PUT /burger/123 # with data: {name: "chicken king"} to change the name of burger 123
For instance, if you had a path with the url
GET /burger_list?id=123
That would not be considered good practice.
It means you need to think hard about the names you give your resources to make sure the intent is explicit.
Use HTTP verbs to express your intents
It basically means using:
GET to read a resource identified by an identifier (id) or a collection of resources
PUT to update a specific resource that you identify by an identifier (id)
DELETE to destroy a specific resource that you identify by an id
POST to create a new resource
Usually, in Rails, those verbs are, by convention, used to map specific actions in your controller.
GET goes to show or index
PUT goes to update
DELETE goes to destroy
POST goes to create
That's why people usually say that if you have actions in your controllers that don't follow that pattern, you're not "RESTful". But in the end, only the routes you expose count. Not really your controller actions. It is a convention of course, and conventions are useful for readability and maintainability.
You make proper use of HTTP codes to indicate status
You already know the usual suspects:
200 means OK, everything went fine.
404 means NOT FOUND, could not find resource
401 means UNAUTHORIZED, authentication failed, auth token invalid
500 means INTERNAL SERVER ERROR, in other words: kaput
But there are more that you could be using in your responses:
201 means CREATED, it means the resource was successfully created
403 means FORBIDDEN, you don't have the privileges to access that resource
...
You get the picture, it's really about replying with the right HTTP code that represents clearly what happens.
Answering your questions
would that be accurate that it is not RESTful?
From what I see, the first issue is your path.
post 'cars/make_list', to: 'cars#make_list'
What I understand is that you are retrieving a collection of car makes. Using a POST to retrieve a collection is against REST rules, you should be using a GET instead. That should answer your first question.
how does that impact my application?
Well, the impact of not being restful in your case is not very big. It's mainly about readability, clarity and maintainability. Separating concerns and putting them in the right place etc... It's not impacting performance, nor is it a dangerous issue. You're just not RESTful and that makes it more complicated to understand your app, in your case of course.
what should/could I do to resolve that?
Besides the route problem, the other issue is that your action is called make_list and that doesn't follow Rails REST conventions. Rails has a keyword to create RESTful routes:
resources :car_makes, only: [:index] # GET /car_makes , get the list of car makes
This route expresses your intent much better than the previous one and is now a GET request. You can then use query parameters to filter the results. But it means we need to create a new controller to deal with it.
class CarMakesController < ApplicationController
def index
makes = params[:year].blank? ? "" : YmmMake.where(ymm_year_id: params[:year]).order(:make)
render partial: "makes", locals: {car: params[:car], form: params[:form], makes: makes}
end
private
# Strong parameters stuff...
end
And of course we also need to change your jquery to make a GET request instead of a POST.
$(document).ready(function () {
...
// when the #year field changes
$("#car_ymm_year_id").change(function () {
// ...
$.get({
url: '/car_makes',
data: {
form: form,
year: year,
car: car
},
success: function (data) {
$("#car_ymm_make_id").html(data);
});
return false;
});
...
});
This is a much better solution, and it doesn't require too much work.
There is an excellent tutorial on REST on REST API tutorial, if you want to know more about the specifics. I don't know much about the small details, mostly what is useful on a day to day basis.
Hope this helps.
Related
The type ahead functionality works where it is supposed to. The issue though is that the type ahead functionality is making the JSON request on every request for the data when it should really only happen for one specific request.
I have the following controller:
#controllers/agencies_controller.rb
class AgenciesController < ApplicationController
def get_unique_agency_names
#unique_agency_names = Agency.uniq.pluck(:name)
respond_to do |format|
format.json { render json: #unique_agency_names }
end
end
...
end
I have the following in my javascript file:
#app/assets/javascripts.agencies/index.js
$(document).ready(function(){
/* For typeahead functionality on name input of search form for agencies */
var agency_names = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.whitespace,
queryTokenizer: Bloodhound.tokenizers.whitespace,
prefetch: '../agencies/get_unique_agency_names.json'
});
$('#prefetch .typeahead.name_input').typeahead(null, {
name: 'agency_names',
source: agency_names
});
});
And just for completion, here is the one place where I want this functionality to happen: In this form:
# just showing the relevant part of the form
<div class="form-group" id="prefetch">
<%= f.label :name_cont, "Agency Name" %>
<%= f.text_field :name_cont, class: "form-control typeahead name_input", placeholder: "Enter Agency Name" %>
</div>
Here is my relevant route in config/routes.rb
resources :agencies do
collection do
get 'get_unique_agency_names'
end
end
My specific question is: How can I ensure that the GET "/agencies/get_unique_agency_names" is only called when it is supposed to? Right now it is appending this JSON request for every request. I only want the JSON request to happen for one specific request.
Twitter's Type Ahead Examples
I think you need this
$('#prefetch .typeahead.name_input').typeahead(null, {
generateOnLoad:false,
name: 'agency_names',
source: agency_names
});
generateOnLoad {boolean}
null (default)
If enabled, the source data will be generated (doing Ajax request and preparing to data to be searched) on page load instead of waiting for the input to be focused.
PN:This option does not work well with dynamic: true unless some other configuration is adjusted.
Also, If you find the rails manifest file adds your call to every page just check element exists before type ahead is bound:
if($('#prefetch .typeahead.name_input').length){
$('#prefetch .typeahead.name_input').typeahead(null, {
generateOnLoad:false,
name: 'agency_names',
source: agency_names
});
}
I would suggest a strategy for “limiting” requests, rather than programmatically enforcing a single request, as it seems you are dealing with a large data set. You can set Bloodhound’s prefetch option to cache what you can, and the sufficient and remote options to ensure that requests for your JSON file are back-filled and rate-limited. Set a higher value for Typeahead’s minLength option as a last line of defense.
Prefetch will serve you to a degree, as it depends on the browser’s local storage for caching, which has limited capacity. If your data set is large, it will only cache a portion, and further requests will be required after initialization.
Remote handles requests after initialization, and can debounce or throttle these requests for you, thereby reducing the number of requests made. See the url, rateLimitBy, and rateLimitWait options, which can be assigned to remote as a hash. Further, Bloodhound will use remote to back-fill additional datums if your sufficient datum count is not reached.
Finally, don’t pass null and set the minLength option to something higher than the default of 1 on your Typeahead constructor. This will prevent overly abundant lookups on your dataset. Just be sure that the minLength value is shorter than the length of your smallest, possible result.
I'm using the semantic-ui framework to create a simple search form that uses auto-complete information generated by calling an api via an ajax request.
The server endpoint generates a simple JSON array
e.g.
http://data.nzor.org.nz/names/lookups?query=lu
gives
["Lubbockia","Lubbockia aculeata","Lubbockia squillimana","Lucanidae"]
I can see the search making the request but I'm not sure how to get the results to display.
I have created a jsfiddle at http://jsfiddle.net/6ojkdvnn/4/
$(document)
.ready(function () {
$('.ui.search')
.search({
apiSettings: {
url: 'http://data.nzor.org.nz/names/lookups?query={query}'
},
debug: true,
verbose: true
});
});
I have tried various options but have now stripped it back to the basic settings above so as not to confuse the matter. The documentation is pretty good (http://semantic-ui.com/modules/search.html) but I can't quite see how to make it work.
I'd prefer not change the api response if it can be helped.
I too had problem with the seach api of the Semantic-UI.
So after some researchs, i learned that it can be used this way:
I am using Ruby on Rails also.
jQuery File to autocomplete cities names:
# Semantic-Ui Search
# Sets for autocomplete name of cities while typing.
$('.ui.search.city').search
apiSettings:
action: 'search', url: '/cities/autocomplete.json?query={query}'
fields:
results : 'cities'
title : 'name'
description : 'state'
minCharacters : 3
Semantic-UI (search) expects a "results" as root and nodes with childs content with title and description, and others that api specify. So if you had json result with other names, then you have to change in the method call of searc function.
Because of this, i changed results for cities, title for name and description to state.
In my Controller i just made a query like this:
def autocomplete
#cities = City.includes(:state).order(:name).where('name like ?', "%#{params[:query]}%")
end
In my Routes file i specify the get method that return a collection.
# Resources for cities.
resources :cities, only: :index do
get :autocomplete, :on => :collection
end
And using the Rabl gem i format the output of json file:
collection #cities, root: :cities , object_root: false
attributes :id, :name
node(:state) { |city| city.state.name }
node(:query) { params[:query] }
Thats it's all, it works for me.
Now, for query http://data.nzor.org.nz/names/lookups?query=lu, server responds with XML data. It is not JSON.
<ArrayOfString xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<string>Lubbockia</string>
<string>Lubbockia aculeata</string>
<string>Lubbockia squillimana</string>
<string>Lucanidae</string>
<string>Lucaninae</string>
<string>Lucerapex</string>
<string>Lucerapex angustatus</string>
<string>Lucerne</string>
<string>Lucerne Australian latent virus</string>
<string>Lucerne dodder</string>
</ArrayOfString>
In search module code, semantic require both response.success and response.results object in response
For example, line 1050, if(response.results !== undefined) {
In API description, it is not clear if you can modifiy the response before uses by Semantic. May be use the callback onSuccess: function() {
described at http://semantic-ui.com/behaviors/api.html#/usage
But I am sceptical...
I'm building a site at the moment where there are many relational links between data. As an example, users can make bookings, which will have booker and bookee, along with an array of messages which can be attached to a booking.
An example json would be...
booking = {
id: 1,
location: 'POST CDE',
desc: "Awesome stackoverflow description."
booker: {
id: 1, fname: 'Lawrence', lname: 'Jones',
},
bookee: {
id: 2, fname: 'Stack', lname: 'Overflow',
},
messages: [
{ id: 1, mssg: 'For illustration only' }
]
}
Now my question is, how would you model this data in your angular app? And, while very much related, how would you pull it from the server?
As I can see it I have a few options.
Pull everything from the server at once
Here I would rely on the server to serialize the nested data and just use the given json object. Downsides are that I don't know what users will be involved when requesting a booking or similar object, so I can't cache them and I'll therefore be pulling a large chunk of data every time I request.
Pull the booking with booker/bookee as user ids
For this I would use promises for my data models, and have the server return an object such as...
booking = {
id: 1,
location: 'POST CDE',
desc: "Awesome stackoverflow description."
booker: 1, bookee: 2,
messages: [1]
}
Which I would then pass to a Booking constructor, which would resolve the relevant (booker,bookee and message) ids into data objects via their respective factories.
The disadvantages here are that many ajax requests are used for a single booking request, though it gives me the ability to cache user/message information.
In summary, is it better practise to rely on a single ajax request to collect all the nested information at once, or rely on various requests to 'flesh out' the initial response after the fact.
I'm using Rails 4 if that helps (maybe Rails would be more suited to a single request?)
I'm going to use a system where I can hopefully have the best of both worlds, by creating a base class for all my resources that will be given a custom resolve function, that will know what fields in that particular class may require resolving. A sample resource function would look like this...
class Booking
# other methods...
resolve: ->
booking = this
User
.query(booking.booker, booking.bookee)
.then (users) ->
[booking.booker, booking.bookee] = users
Where it will pass the value of the booker and bookee fields to the User factory, which will have a constructor like so...
class User
# other methods
constructor: (data) ->
user = this
if not isNaN(id = parseInt data, 10)
User.get(data).then (data) ->
angular.extend user, data
else angular.extend this, data
If I have passed the User constructor a value that cannot be parsed into a number (so this will happily take string ids as well as numerical) then it will use the User factorys get function to retrieve the data from the server (or through a caching system, implementation is obviously inside the get function itself). If however the value is detected to be non-NaN, then I'll assume that the User has already been serialized and just extend this with the value.
So it's invisible in how it caches and is independent of how the server returns the nested objects. Allows for modular ajax requests and avoids having to redownload unnecessary data via its caching system.
Once everything is up and running I'll write some tests to see whether the application would be better served with larger, chunked ajax requests or smaller modular ones like above. Either way this lets you pass all model data through your angular factories, so you can rely on every record having inherited any prototype methods you may want to use.
I'm trying to figure out a Collection/Model system that can handle retrieving
data given the context it's asked from, for example:
Available "root" resources:
/api/accounts
/api/datacenters
/api/networks
/api/servers
/api/volumes
Available "sub" resources:
/api/accounts/:id
/api/accounts/:id/datacenters
/api/accounts/:id/datacenters/:id/networks
/api/accounts/:id/datacenters/:id/networks/:id/servers
/api/accounts/:id/datacenters/:id/networks/:id/servers/:id/volumes
/api/accounts/:id/networks
/api/accounts/:id/networks/:id/servers
/api/accounts/:id/networks/:id/servers/:id/volumes
/api/accounts/:id/servers
/api/accounts/:id/servers/:id/volumes
/api/accounts/:id/volumes
Then, given the Collection/Model system, I would be able to do things like:
// get the first account
var account = AccountCollection.fetch().first()
// get only the datacenters associated to that account
account.get('datacenters')
// get only the servers associated to the first datacenter's first network
account.get('datacenters').first().get('networks').first().get('servers')
Not sure if that makes sense, so let me know if I need to clarify anything.
The biggest kicker as to why I want to be able to do this, is that if the
request being made (ie account.get('datacenters').first().get('networks'))
hasn't be made (the networks of that datacenter aren't loaded on the client)
that it is made then (or can be fetch()d perhaps?)
Any help you can give would be appreciated!
You can pass options to fetch that will be translated to querystring params.
For example:
// get the first account
var account = AccountCollection.fetch({data: {pagesize: 1, sort: "date_desc"}});
Would translate to:
/api/accounts?pagesize=1&sort=date_desc
It is not quite a fluent DSL but it is expressive and efficient since it only transmits the objects requested rather than filtering post fetch.
Edit:
You can lazy load your sub collections and use the same fetch params technique to filter down your list by query string criteria:
var Account = Backbone.Model.extend({
initialize: function() {
this.datacenters = new Datacenters;
this.datacenters.url = "/api/account/" + this.id + '/datacenters';
}
});
Then from an account instance:
account.datacenters.fetch({data: {...}});
Backbone docs on fetching nested models and collections
I have such router in my app:
TravelApi.Router.map ->
#resource 'stars', ->
#resource 'star', { path: '/:star_id' }
When I go to http://localhost/#/stars
I see in web console that there is one request
GET http://localhost:3000/stars
And when I go to http://localhost/#/stars/1
I see in web console that there are two requests
GET http://localhost:3000/stars
GET http://localhost:3000/stars/1
Question: why there are two requests in the second case?
Stars route:
TravelApi.StarRoute = Ember.Route.extend(
model: (params) ->
TravelApi.Star.find(params.star_id)
)
TravelApi.StarsRoute = Ember.Route.extend(
model: ->
TravelApi.Star.find()
)
And my templates:
application.js.hbs.hamlbars
= hb "linkTo 'stars'" do
stars
%div= hb 'outlet'
stars.js.hbs.hamlbars
%ul
=hb "each star in controller" do
%li<
=hb 'star.name'
star.js.hbs.hamlbars
Star:
= hb 'name'
store.js.coffee
TravelApi.store = DS.Store.create(
revision: 11
adapter: DS.RESTAdapter.create()
)
The way nested resources work, if the first request returns a promise (which ember data is supposed to do), the nested resource/route model function will not be called until after the ajax request will complete.
Following this logic, there should only be 1 ajax call.
TravelApi.Star.find() should create an ajax request to fetch all records
TravelApi.Star.find(1) should not require an ajax request because the record should be there.
In your case however, the star resource is not waiting for the stars resource to finish the ajax. The reason is TravelApi.Star.find() returns results immediately (which are all available records in the store which of course is empty), instead of a promise (I think).
To solve this, you can return a findQuery promise by writing the following in your model:
TravelApi.Star.find({})
This will cause the star resource to wait for the stars resource to finish its ajax request.
The route should look like this:
TravelApi.StarsRoute = Ember.Route.extend
model: ->
TravelApi.Star.find({})
TravelApi.StarRoute = Ember.Route.extend
model: (params) ->
TravelApi.Star.find(params.star_id)
It seems that you don't want to fetch all the stars when you load /star/:id
The reason why all the stars are fetched if you access /star/1 directly is because the star route is nested inside stars
The reason why you would nest your routes, is because your UI is nested. This means that when you are looking at one star, somewhere in your UI (in the side bar maybe), you are showing the list of stars. In that case, you need the request localhost:3000/stars because you need to display all the stars even if you are looking at one star. This means that the ajax requested is necessary and therefore not a problem.
If however, you are not displaying the list of all the the stars when looking at one star, then your routes shouldn't be nested in the first place. In that case, fix your routes to look like this:
TravelApi.Router.map ->
#resource 'stars'
#resource 'star', { path: '/:star_id' }