• If you are citizen of an European Union member nation, you may not use this service unless you are at least 16 years old.

View
 

Todos 06-Building with Rails

Page history last edited by Valeriano Manassero 14 years, 2 months ago

About Rails

 

From its site: "Ruby on Rails is an open-source web framework that's optimized for programmer happiness and sustainable productivity. It lets you write beautiful code by favoring convention over configuration."

 

Installation

 

Make sure you have installed Rails 2.3.4 or above and the sqlite gem

 

$ sudo gem install sqlite3-ruby
$ sudo gem install rails

 

Create the project

A small rails application will serve as a backend for the Todos application.

 

Create a new Todos app by entering the following command into the console:

 

$ rails todos
$ cd todos

 

Make sure that you do issue this command outside your SproutCore application folder!

 

Setup the data model

 

We can use Rails generators to greate a basic RESTful interface for the Task model by typing

 

$ script/generate scaffold Task description:string isDone:boolean order:integer
 

Now you need to update your database to conform the model. For that, type

 

$ rake db:migrate
 

Adjust JSON communication

 

Sproutcore uses the field guid for objects ids, but Rails calls this field id.

 

You have two options on how to convert between these naming conventions:

 

Option 1: Adjust Rails JSON output

 

To customize the JSON output of an object, write a json_to_task protected method just before the last "end" line in TasksController (app/controllers/tasks_controller.rb): 

 

  protected
  def json_for_task(task)
    { :guid => task_path(task),
      :description => task.description,
      :isDone => task.isDone
    }
  end

Option 2: Set primaryKey in your Sproutcore model class

 

You can subclass SC.Record and set the primaryKey of your sublcass to "id". Your Todo-Object then needs to extend from e.g. App.Rails.Record.

 

App.Rails.Record = SC.Record.extend({
  primaryKey : "id" // Extend your records from App.Rails.Record instead of SC.Record.
});

 

(Thanks to Giacomo Luca Maioli for this idea.)

 

This tutorial sticks to Option 1. The rails-code for option 2 is the same, just omit calling json_for_task().

 

Writing the controller

 

Open the file app/controllers/tasks_controllers.rb, and modify the index method like this:

 
def index
  @tasks = Task.all

  respond_to do |format|
    tasks = @tasks.map {|task| json_for_task(task) }
    format.json { render :json => { :content => tasks } }
    format.html
    format.xml  { render :xml => @tasks }
  end
end
 

Start the server from the Rails app directory:

 

$ script/server

 

To fill your database, create some tasks by visiting http://localhost:3000/tasks and entering some test data.

 

To test your server now, open the following address in your web browser: http://localhost:3000/tasks?format=json. You should receive a plain-text-file, with the datafields of your test data. 

 

Implementing CRUD operations

 

The following sections show how to implement the CRUD (create, read, update, delete) operations for our Task model.

 

We will start with read (or also GET), as it is the easiest. Extend the code like this:

 

Read or GET Task

 

  def show
    @task = Task.find(params[:id])

    respond_to do |format|
      format.html # show.html.erb
      format.xml  { render :xml => @task }
      format.json do
        render :json => {
          :content => json_for_task(@task),
          :location => task_path(@task)
        }
      end
    end
  end

 

Try the outcome by calling http://localhost:3000/tasks/show/1?format=json.

 

Create Task

 

 

  def create
    @task = Task.new(params[:task])

    respond_to do |format|
      if @task.save
        flash[:notice] = 'Task was successfully created.'
        format.json do
          render :json => { :content => json_for_task(@task) }, :status => :created,
                            :location => task_path(@task)
        end
        format.html { redirect_to(@task) }
        format.xml  { render :xml => @task, :status => :created, :location => @task }
      else
        format.html { render :action => "new" }
        format.xml  { render :xml => @task.errors, :status => :unprocessable_entity }
      end
    end
  end

Update Task

 

  def update
    @task = Task.find(params[:id])

    respond_to do |format|
      params[:task].delete(:guid)
      if @task.update_attributes(params[:task])
        flash[:notice] = 'Task was successfully updated.'
        format.json do
          render :json => { :content => json_for_task(@task) }, :location => task_path(@task)
        end
        format.html { redirect_to(@task) }
        format.xml  { head :ok }
      else
        format.html { render :action => "edit" }
        format.xml  { render :xml => @task.errors, :status => :unprocessable_entity }
      end
    end
  end

Delete Task

 
  def destroy
    @task = Task.find(params[:id])
    @task.destroy

    respond_to do |format|
      format.json { head :ok }
      format.html { redirect_to(tasks_url) }
      format.xml  { head :ok }
    end
  end

 

Customize the data source

 

To fullfill Ruby on Rails expectations, modify the send() call in the createRecord and updateRecord in the data_source/task.js file in your SproutCore project like this:

 

.send({ task: store.readDataHash(storeKey) });

Setup your proxy

 

Add the following line to the Buildfile file in your SproutCore project:

proxy "/tasks", :to => "localhost:3000"

You’re Done!

 

Continue to next step: Step 7: Hooking Up to the Backend »

Comments (18)

Brandon Tennant said

at 8:55 am on Oct 5, 2009

@ReinerPittinger Following this method, I always get a single task in my task list with no description or isDone property. I'm not entirely sure why. Are you getting the same results.

Brandon Tennant said

at 8:50 am on Oct 6, 2009

So by default, rails will produce a JSON output of the above in the following structure which would require a change in the merb_data_source.js file to pull each task object out into a temporary array in the form that is expected.

{
   "content":[
      {
         "task":{
            "isDone":true,
            "created_at":"2009-10-06T05:19:25Z",
            "updated_at":"2009-10-06T05:19:25Z",
            "id":1,
            "description":"This is a description"
         }
      },
      {
         "task":{
            "isDone":false,
            "created_at":"2009-10-06T05:20:10Z",
            "updated_at":"2009-10-06T05:20:10Z",
            "id":2,
            "description":"This is another description"
         }
      }
   ]
}

fetchDidComplete: in merb_data_source.js is updated to include the following (kinda hacky)

/* Here we're creating a new array in the form expected by loadRecords */
var newContentArray = new Array();

for(i = 0; i < response.content.length; i++)
{
newContentArray[i] = response.content[i].dont;
}

response.content = newContentArray;

storeKeys = params.store.loadRecords(fetchKey.get('recordType'), response.content);

// update the storeKeyArray to contain the store keys so that all of the observers
// will be updated with the keys of the newly loaded data. this is where the magic
// of SproutCore happens.
params.storeKeyArray.replace(0,0,storeKeys);


This gets us as far as to load the data in from the rails server. However for me, it's only ever loading 1 record. Further debugging shows each loaded record is being assigned a storeKey value of 1 and I'm not at all sure why.


Spencer Dillard said

at 1:59 am on Nov 17, 2009

I'm a newb for sure, but one thing I started doing to better handle the problem of keeping my model in synch is to create a partial for the model that is just the types. In my generated model, I do sc_require('models'/{model_name}_partial'); And in that file, I keep all the type definitions. Next step is to write a script that will inspect a list of models in rails and generate the partial so that this can be done via rake.

One note -- I am non rails 2.2.2 and your json changes don't work for me, since element was added later (2.3?) -- just an FYI in case someone else starts to take the time to hunt that down.

But thanks -- got me on the way so far.

And one question. I am a little confused how the properties get translated. In rails I have something like zip_code, and in sc, I have zipCode. Is there somewhere that this mystical translation happens?

Erich Ocean said

at 7:39 am on Nov 17, 2009

You have to translate property names in your SC.Record subclass definition, e.g.

zipCode: SC.Record.attr(String, { key: 'zip_code' })

Giacomo Luca Maioli said

at 8:42 am on Dec 2, 2009

I managed to make a rails json backend to work withoud redefine "to_json" in ActiveRecord::Base subclasses. I made it by setting the primaryKey of my extension of SC.Record to "id" instead of "guid" (why guid anyway?).

App.Rails.Record = SC.Record.extend(

primaryKey : "id" // I'll extend my app records from App.Rails.Record instead of SC.Record.

});
I don't know if this has any collateral effect, but seems to work fine.

Even though I've wrote my own version of "ResourceController" (that only serve json :-D) you guys can (skip?) accelerate development of json responders using http://github.com/giraffesoft/resource_controller

this one, along with resource mapping routes, should get you to work with rails as a backend in (no?) less time.

Cheers.

Charles Jolley said

at 2:23 pm on Dec 10, 2009

I removed the section at the top of this page that talks about Rails suitability for SproutCore. This tutorial is not the place to make these kinds of arguments. Feel free to argue about it on your blog or in the mailing list/IRC channel.

Peter Wagenet said

at 9:48 am on Dec 11, 2009

Good call Charles. I probably should have just done that rather than modifying it.

Steve B. said

at 11:09 pm on Mar 3, 2010

This is a bit awkward because the "hook up" part is on the next Step but I'm having some difficulties specific to Rails. I followed Option #1.

Listing tasks ("XHR finished loading: "http://localhost:4020/tasks") works fine as does deleting a task.

Creating a task, at least the first part, works fine as well (I end up with a new task called "New Task"). However, when I attempt to edit it, I see: "XHR finished loading: "http://localhost:4020/undefined"" in the debugger window. I do not see anything on the server (Rails side). So this is problem #1.

When I try to update an existing task, I see "XHR finished loading: "http://localhost:4020/tasks/7"" in the debugger window but on the server (Rails side), there is an internal server error because of the "guid" property not belonging to Task. This is problem #2.

So I attempted to follow Option #2...except I'm not sure where to put "App.Rails.Record" (or should it be "Todos.Rails.Record"?). I tried to put:

Todos.Rails.Record = SC.Record.extend( { primaryKey: "id"});

in main.js and made the necessary change in models/task.js but that didn't work. It pretty much well bombed everywhere saying Todos.Rails.Record wasn't defined which caused Todos.Task to fail and so on down the line.

At this point, I'm really engaging in cargo cult coding...any ideas?

Steve B. said

at 11:58 am on Mar 4, 2010

I've solved problem #2. I looked around the internets and found code by Mike Sublesky for an earlier version of SproutCore. The Rails side, however, did have some code that was useful. To the tasks_controller.rb, I added at the top:

before_filter :clean_params, :only => [:create,:update]

and at the bottom, I added:

private
def clean_params
params[ :task].delete( :guid) if params[ :task]
end

This fixes updating to Tasks that both sides know about.

Problem #1 remains...if you create a new task, the server side is informed of the new task (with description: "NewTask", isDone: false) which is created but the client is somehow not informed of any information about this task. Therefore any subsequent editing fails. The initial editing fails quietly (you change the text on the client but it's not updated on the server). Any subsequent editing fails hard with a SC.Error:sc33:Not found (-1) in the JS debugger.

Steve B. said

at 1:50 pm on Mar 4, 2010

And now I've solved problem #2.

In the actions above, in several places, you will note that the render => :json has the following form:

render :json => { :content => json_for_task(@task) }, :status => :created, :location => task_path(@task)

Note where the Hash ends ("}"). In the next step, for didCreateTask in todos/data_sources/task.js, you will write code that looks for the location in the following manner:

var url = response.header( 'Location');

I narrowed the problem down to this line of code. The client simply isn't getting that "url". I tried changing the string from "Location" to "location" but that didn't work either. What I finally did was change the hash that is returned by the create action in tasks_controller.rb to:

render :json => { :content => json_for_task(@task), :status => :created, :location => task_path(@task)}

Note where the Hash ends now. Then in todos/data_sources/task.js, I changed that one line in didCreateTask to:

var url = response.get( 'body').location;

Everything now works like a charm. Right now I feel like I'm hacking because I don't know why what was described didn't work.

Steve B. said

at 6:42 pm on Mar 5, 2010

Just for the fun (?) of it, I started looking into option #2. Thanks to #sproutcore for helping clarify some things for me. Note that there are lot of changes to be made between Option #1 and Option #2 because...

1. The code as written assumes "guid" is the RESTful path to the Task. When you change the code to use "id" instead of "guid", this has ramifications throughout the code as presented.

2. As was previously noticed, Rails' to_json method returns a different formatting than SproutCore expects. Specifically, the JSON for @tasks is:

{"content":[{"task":{"isDone":false,"id":5,"description":"learn more SproutCore."}}]}

instead of:

{"content":[{"isDone":false,"id":5,"description":"learn more SproutCore."}]}

so you either need to have code on the front-end that handles it or code on the back-end that handles it. I picked the back-end because my ruby-fu is better than my javascript-fu.

Step 1. Create the SC.Record subclass and use it.

Todos.Rails = {} ;
Todos.Rails.Record = SC.Record.extend( primaryKey : "id"}) ;

should go in core.js. I originally put it in main.js but it doesn't get executed soon enough. In core.js, it does. Change SC.Record to Todos.Rails.Record in models/task.js.

Step 2. Edit json_for_task:

def json_for_task( task)
task.attributes
end

this won't necessarily scale well if properties are themselves ActiveRecord objects. It seems like the right thing to do is override and rewrite to_json but I didn't (yet).

Steve B. said

at 6:42 pm on Mar 5, 2010

Step 3. Fix places where store.idFor(storeKey) is expected return a RESTful path to the Task (through 'guid').

At this point fetch & didFetchTasks will work but not much else. The problems are going to be lines like these:

in retrieveTask:
var url = store.idFor(storeKey);
SC.Request.getUrl(url).json().notify(this, 'didRetrieveTask', store, storeKey).send();

because url isn't the RESTful URL anymore. This needs to be changed to:

var url = "/tasks/" + store.idFor(storeKey) ;
SC.Request.getUrl(url).json().notify(this, 'didRetrieveTask', store, storeKey).send();

Similarly, in didCreateTask:

var url = response.get( 'body').location;
store.dataSourceDidComplete( storeKey, null, url); // update url

needs to be changed to:

var id = response.get( 'body').content.id;
store.dataSourceDidComplete( storeKey, null, id);

And similarly throughout the CRUD methods and callbacks.

Step 3. Fix places where store.idFor(storeKey) is expected return a RESTful path to the Task (through 'guid') by adding "/tasks/" in front of it.

At this point fetch & didFetchTasks will work but not much else. The problems are going to be lines like these:

in retrieveTask:
var url = store.idFor(storeKey);
SC.Request.getUrl(url).json().notify(this, 'didRetrieveTask', store, storeKey).send();

because url isn't the RESTful URL anymore. This needs to be changed to:

var url = "/tasks/" + store.idFor(storeKey) ;
SC.Request.getUrl(url).json().notify(this, 'didRetrieveTask', store, storeKey).send();

Similarly, in didCreateTask:

var url = response.get( 'body').location;
store.dataSourceDidComplete( storeKey, null, url); // update url

needs to be changed to:

var id = response.get( 'body').content.id;
store.dataSourceDidComplete( storeKey, null, id);

And similarly throughout the CRUD methods and callbacks.

Steve B. said

at 6:44 pm on Mar 5, 2010

ugh...step 3. is listed twice and no "edit."

Hugh Evans said

at 4:44 am on Apr 3, 2010

You can also simply add in guid as an alias for id in to_json:

alias_attribute :guid, :id

alias_method :ar_to_json, :to_json

def to_json(options = {}, &block)
default_options = { :methods => :guid }
self.ar_to_json(options.merge(default_options), &block)
end

Specs:

def build_task
task = Task.new
task.save(:validate => false)
task
end

describe Task do
describe 'Customising #to_json for Sprout Core' do
it 'should have an guid alias for id' do
@task = build_task
@task.guid.should == @task.id
end

it 'should include guid in #to_json' do
@task = build_task
json = ActiveSupport::JSON.decode(@task.to_json)
json.has_key?('guid').should be_true
json['guid'].should == @task.id
end
end
end

Oliver said

at 10:18 am on Apr 11, 2010

I feel like a complete idiot asking this but I cant find the directory data_source when I'm supposed to modify the send() call in the createRecord and updateRecord in the data_source/task.js file in your SproutCore project like this:



.send({ task: store.readDataHash(storeKey) });

Devin said

at 12:04 am on May 2, 2010

For everyone who has this problem, it is actually addressed first thing next chapter. that is when the data_source/task.js is created. Or, you can just run:
sc-gen data-source Todos.TaskDataSource
now, in the todos directory and skip the first part of the next chapter.

Rob Bartholomew said

at 8:30 pm on May 10, 2010

This may be completely obvious to most, but I'll state it for anyone having grief: Restart your sc-server after changing the proxy.

Arunjit Singh said

at 7:46 am on Jun 13, 2010

In reference to problem#1
I did some digging around in the console and can see that when I create, a new task appears on the server AND can be accessed in the SC application's store. So, it's just not appearing on the UI for me to edit!
All else works!

You don't have permission to comment on this page.