Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.
Raimonds Simanovskis

Rails-like JavaScript
using CoffeeScript,
Backbone.js and
Jasmine
Raimonds Simanovskis

       github.com/rsim




         @rsim

             .com
The Problem
Ruby code in Rails
JavaScript code in
   Rails 3.0.x
application.js
// Place your application-specific JavaScript functions and classes here
// This file is automatically incl...
Which leads to...
(example from Redmine)
application.js
                  (example from Redmine)
/* redMine - project management software
   Copyright (C) 2006-200...
/*
 * 1 - registers a callback which copies the csrf token into the
 * X-CSRF-Token header with each ajax request. Necessa...
The Problem #2
Do we really know
    (and love?)
   JavaScript?
Sample JavaScript
               (from RailsCasts #267)
var CreditCard = {
  cleanNumber: function(number) {
    return nu...
We see as this
                  “ugly” Ruby
CreditCard = {
  :cleanNumber => lambda { |number|
    return number.gsub(/[-...
Or as this “normal” Ruby
module CreditCard
  def self.clean_number(number)
    number.gsub(/[- ]/, "")
  end

  def self.v...
“Best practices” Ruby
class CreditCard
  def initialize(number)
    @number = clean_number(number)
  end

  def valid?
   ...
JavaScript has objects too!
var CreditCard = function(number) {
   function cleanNumber(number) {
     return number.repla...
But this would be much
        more Ruby-like!
class CreditCard
  cleanNumber = (number) -> number.replace /[- ]/g, ""

 c...
Sample CoffeeScript
# Assignment:                   # Splats:
number   = 42                   race = (winner, runners...) ...
Functions
square = (x) -> x * x
cube   = (x) -> square(x) * x

fill = (container, liquid = "coffee") ->
  "Filling the #{c...
Objects and Arrays
 song = ["do", "re", "mi", "fa", "so"]

 singers = {Jagger: "Rock", Elvis: "Roll"}

 bitlist   = [
   1...
Variable Scope

                          var changeNumbers, inner, outer;
outer = 1                 outer = 1;
changeNumb...
Existential Operator
 solipsism = true if mind? and not world?

 speed ?= 75

 footprints = yeti ? "bear"

 zip = lottery....
Conditionals
mood = greatlyImproved if singing

if happy and knowsIt
  clapsHands()
  chaChaCha()
else
  showIt()

date = ...
Loops

eat food for food in ['toast', 'cheese', 'wine']
countdown = (num for num in [10..1])

earsOld = max: 10, ida: 9, t...
Classes, Inheritance
     and super
  class Animal
    constructor: (@name) ->

    move: (meters) ->
      alert @name + ...
Function Binding

Account = (customer, cart) ->
  @customer = customer
  @cart = cart

  $('.shopping_cart').bind 'click',...
And many other
nice features...
How to install?


brew install node # or install node.js otherwise
curl http://npmjs.org/install.sh | sh
npm install -g co...
Back to the
Problem #1
Dynamic single page
    application
Identifying
components
    AppView
Identifying
components
    AppView



              TodoView
              TodoView
              TodoView
Identifying
components
          AppView

keypress event
click event          TodoView
      dblclick event TodoView
     ...
Browser-side
       Views and Models
          AppView
                                   TodoList
keypress event
click ev...
Browser-side
       Views and Models
          AppView
                                   new, fetch
                     ...
Browser-side
      Views and Models
         AppView
                               refresh, add    TodoList
keypress even...
Browser-side Models
and RESTful resources
Browser                  Rails
             GET
 TodoList            TodosContro...
Organize CoffeeScript
and JavaScript Code




 using http://github.com/Sutto/barista
application.coffee

    # main namespace
    window.TodoApp = {}
Todo model
class TodoApp.Todo extends Backbone.Model

 # If you don't provide a todo, one will be provided for you.
 EMPTY...
TodoList collection
class TodoApp.TodoList extends Backbone.Collection

 # Reference to this collection's model.
 model: T...
Todo item view
class TodoApp.TodoView extends Backbone.View
  # ... is a list tag.
  tagName: "li"

 # Cache the template ...
# Re-render the contents of the todo item.
render: ->
  $(@el).html @template @model.toJSON()
  @setContent()


          ...
Application view
class TodoApp.AppView extends Backbone.View

 # Instead of generating a new element, bind to the existing...
@collection.bind 'add',     @addOne
 @collection.bind 'refresh', @addAll
 @collection.bind 'all',     @renderStats



    ...
view = new TodoApp.TodoView model: todo
 @$("#todo-list").append view.render().el

# Add all items in the collection at on...
#todoapp
                  index.html.haml
  .title
    %h1 Todos
  .content
    #create-todo
      %input#new-todo{:place...
%input#new-todo{:placeholder => "What needs to be done?", :type => "text"}/
      %span.ui-tooltip-top{:style => "display:...
One more thing:
Backbone Controllers
          Routers
class Workspace extends Backbone.Controller Router

  routes:
    "...
How do you test it?
RSpec-like testing for
    JavaScript
Together with all
  other tests
Testing Todo model
describe "Todo", ->
  todo = null
  ajaxCall = (param) -> jQuery.ajax.mostRecentCall.args[0][param]

  ...
and TodoList
                  collection
describe "TodoList", ->
  attributes = [
    content: "First"
    done: true
  ,...
Rails 3.1
Asset Pipeline
application.js.coffee
    using Sprockets

     #=   require jquery
     #=   require underscore
     #=   require backbon...
Watch RailsConf
           DHH keynote
http://en.oreilly.com/rails2011/public/schedule/detail/19068
References
     http://jashkenas.github.com/coffee-script/

    http://documentcloud.github.com/backbone/

         http:/...
Used in eazybi.com
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine
You’ve finished this document.
Download and read it offline.
Upcoming SlideShare
Why Every Tester Should Learn Ruby
Next
Upcoming SlideShare
Why Every Tester Should Learn Ruby
Next
Download to read offline and view in fullscreen.

43

Share

Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine

Download to read offline

Presented at RailsWayCon 2011

Related Books

Free with a 30 day trial from Scribd

See all

Related Audiobooks

Free with a 30 day trial from Scribd

See all

Rails-like JavaScript Using CoffeeScript, Backbone.js and Jasmine

  1. 1. Raimonds Simanovskis Rails-like JavaScript using CoffeeScript, Backbone.js and Jasmine
  2. 2. Raimonds Simanovskis github.com/rsim @rsim .com
  3. 3. The Problem
  4. 4. Ruby code in Rails
  5. 5. JavaScript code in Rails 3.0.x
  6. 6. application.js // Place your application-specific JavaScript functions and classes here // This file is automatically included by javascript_include_tag :defaults
  7. 7. Which leads to... (example from Redmine)
  8. 8. application.js (example from Redmine) /* redMine - project management software Copyright (C) 2006-2008 Jean-Philippe Lang */ function checkAll (id, checked) { var els = Element.descendants(id); for (var i = 0; i < els.length; i++) { if (els[i].disabled==false) { els[i].checked = checked; } } } function toggleCheckboxesBySelector(selector) { boxes = $$(selector); var all_checked = true; for (i = 0; i < boxes.length; i++) { if (boxes[i].checked == false) { all_checked = false; } } for (i = 0; i < boxes.length; i++) { boxes[i].checked = !all_checked; } } function setCheckboxesBySelector(checked, selector) { var boxes = $$(selector); boxes.each(function(ele) { ele.checked = checked; }); } function showAndScrollTo(id, focus) { Element.show(id); if (focus!=null) { Form.Element.focus(focus); }
  9. 9. /* * 1 - registers a callback which copies the csrf token into the * X-CSRF-Token header with each ajax request. Necessary to application.js * work with rails applications which have fixed * CVE-2011-0447 * 2 - shows and hides ajax indicator */ Ajax.Responders.register({ (example from Redmine) onCreate: function(request){ var csrf_meta_tag = $$('meta[name=csrf-token]')[0]; if (csrf_meta_tag) { var header = 'X-CSRF-Token', token = csrf_meta_tag.readAttribute('content'); if (!request.options.requestHeaders) { request.options.requestHeaders = {}; } request.options.requestHeaders[header] = token; } if ($('ajax-indicator') && Ajax.activeRequestCount > 0) { Element.show('ajax-indicator'); } }, onComplete: function(){ if ($('ajax-indicator') && Ajax.activeRequestCount == 0) { Element.hide('ajax-indicator'); } } }); function hideOnLoad() { $$('.hol').each(function(el) { el.hide(); }); } Event.observe(window, 'load', hideOnLoad);
  10. 10. The Problem #2
  11. 11. Do we really know (and love?) JavaScript?
  12. 12. Sample JavaScript (from RailsCasts #267) var CreditCard = { cleanNumber: function(number) { return number.replace(/[- ]/g, ""); }, validNumber: function(number) { var total = 0; number = this.cleanNumber(number); for (var i=number.length-1; i >= 0; i--) { var n = parseInt(number[i]); if ((i+number.length) % 2 == 0) { n = n*2 > 9 ? n*2 - 9 : n*2; } total += n; }; return total % 10 == 0; } }; console.log(CreditCard.validNumber('4111 1111-11111111')); // true console.log(CreditCard.validNumber('4111111111111121')); // false
  13. 13. We see as this “ugly” Ruby CreditCard = { :cleanNumber => lambda { |number| return number.gsub(/[- ]/, ""); }, :validNumber => lambda { |number| total = 0; number = CreditCard[:cleanNumber].call(number); for i in 0..(number.length-1) n = number[i].to_i; if ((i+number.length) % 2 == 0) n = n*2 > 9 ? n*2 - 9 : n*2; end total += n; end; return total % 10 == 0; } }; puts(CreditCard[:validNumber].call('4111 1111-11111111')); # true puts(CreditCard[:validNumber].call('4111111111111121')); # false
  14. 14. Or as this “normal” Ruby module CreditCard def self.clean_number(number) number.gsub(/[- ]/, "") end def self.valid_number?(number) total = 0 number = clean_number(number) for i in 0...number.length n = number[i].to_i if i+number.length % 2 == 0 n = n*2 > 9 ? n*2 - 9 : n*2 end total += n end total % 10 == 0 end end puts CreditCard.valid_number?('4111 1111-11111111') # true puts CreditCard.valid_number?('4111111111111121') # false
  15. 15. “Best practices” Ruby class CreditCard def initialize(number) @number = clean_number(number) end def valid? total = 0 for i in 0...@number.length n = @number[i].to_i if i+@number.length % 2 == 0 n = n*2 > 9 ? n*2 - 9 : n*2 end total += n end total % 10 == 0 end private def clean_number(number) number.gsub(/[- ]/, "") end end puts CreditCard.new('4111 1111-11111111').valid? # true puts CreditCard.new('4111111111111121').valid? # false
  16. 16. JavaScript has objects too! var CreditCard = function(number) { function cleanNumber(number) { return number.replace(/[- ]/g, ""); } this.number = cleanNumber(number); }; CreditCard.prototype = { isValid: function() { var total = 0; for (var i=this.number.length-1; i >= 0; i--) { var n = parseInt(this.number[i]); if ((i+this.number.length) % 2 == 0) { n = n*2 > 9 ? n*2 - 9 : n*2; } total += n; }; return total % 10 == 0; } }; console.log( (new CreditCard('4111 1111-11111111')).isValid() ); // true console.log( (new CreditCard('4111111111111121')).isValid() ); // false
  17. 17. But this would be much more Ruby-like! class CreditCard cleanNumber = (number) -> number.replace /[- ]/g, "" constructor: (number) -> @number = cleanNumber number isValid: (number) -> total = 0 for i in [0...@number.length] n = +@number[i] if (i+@number.length) % 2 == 0 n = if n*2 > 9 then n*2 - 9 else n*2 total += n total % 10 == 0 console.log (new CreditCard '4111 1111-11111111').isValid() # true console.log (new CreditCard '4111111111111121').isValid() # false
  18. 18. Sample CoffeeScript # Assignment: # Splats: number = 42 race = (winner, runners...) -> opposite = true print winner, runners # Conditions: # Existence: number = -42 if opposite alert "I knew it!" if elvis? # Functions: # Array comprehensions: square = (x) -> x * x cubes = (math.cube num for num in list) # Arrays: list = [1, 2, 3, 4, 5] # Objects: math = root: Math.sqrt square: square cube: (x) -> x * square x
  19. 19. Functions square = (x) -> x * x cube = (x) -> square(x) * x fill = (container, liquid = "coffee") -> "Filling the #{container} with #{liquid}..." awardMedals = (first, second, others...) -> gold = first silver = second rest = others contenders = [ "Michael Phelps" "Liu Xiang" "Yao Ming" "Allyson Felix" "Shawn Johnson" ] awardMedals contenders...
  20. 20. Objects and Arrays song = ["do", "re", "mi", "fa", "so"] singers = {Jagger: "Rock", Elvis: "Roll"} bitlist = [ 1, 0, 1 0, 0, 1 1, 1, 0 ] kids = brother: name: "Max" age: 11 sister: name: "Ida" age: 9
  21. 21. Variable Scope var changeNumbers, inner, outer; outer = 1 outer = 1; changeNumbers = -> changeNumbers = function() { var inner; inner = -1 inner = -1; outer = 10 return outer = 10; inner = changeNumbers() }; inner = changeNumbers();
  22. 22. Existential Operator solipsism = true if mind? and not world? speed ?= 75 footprints = yeti ? "bear" zip = lottery.drawWinner?().address?.zipcode
  23. 23. Conditionals mood = greatlyImproved if singing if happy and knowsIt clapsHands() chaChaCha() else showIt() date = if friday then sue else jill options or= defaults
  24. 24. Loops eat food for food in ['toast', 'cheese', 'wine'] countdown = (num for num in [10..1]) earsOld = max: 10, ida: 9, tim: 11 ages = for child, age of yearsOld child + " is " + age
  25. 25. Classes, Inheritance and super class Animal constructor: (@name) -> move: (meters) -> alert @name + " moved " + meters + "m." class Snake extends Animal move: -> alert "Slithering..." super 5 class Horse extends Animal move: -> alert "Galloping..." super 45 sam = new Snake "Sammy the Python" tom = new Horse "Tommy the Palomino" sam.move() tom.move()
  26. 26. Function Binding Account = (customer, cart) -> @customer = customer @cart = cart $('.shopping_cart').bind 'click', (event) => @customer.purchase @cart
  27. 27. And many other nice features...
  28. 28. How to install? brew install node # or install node.js otherwise curl http://npmjs.org/install.sh | sh npm install -g coffee-script
  29. 29. Back to the Problem #1
  30. 30. Dynamic single page application
  31. 31. Identifying components AppView
  32. 32. Identifying components AppView TodoView TodoView TodoView
  33. 33. Identifying components AppView keypress event click event TodoView dblclick event TodoView TodoView click event
  34. 34. Browser-side Views and Models AppView TodoList keypress event click event TodoView Todo dblclick event TodoView Todo TodoView Todo click event
  35. 35. Browser-side Views and Models AppView new, fetch TodoList keypress event click event TodoView create, save Todo dblclick event TodoView Todo TodoView Todo click event
  36. 36. Browser-side Views and Models AppView refresh, add TodoList keypress event click event TodoView Todo dblclick event TodoViewchange, destroy Todo TodoView Todo click event
  37. 37. Browser-side Models and RESTful resources Browser Rails GET TodoList TodosController POST index Todo PUT show Todo create DELETE update Todo destroy JSON
  38. 38. Organize CoffeeScript and JavaScript Code using http://github.com/Sutto/barista
  39. 39. application.coffee # main namespace window.TodoApp = {}
  40. 40. Todo model class TodoApp.Todo extends Backbone.Model # If you don't provide a todo, one will be provided for you. EMPTY: "empty todo..." # Ensure that each todo created has `content`. initialize: -> unless @get "content" @set content: @EMPTY # Toggle the `done` state of this todo item. toggle: -> @save done: not @get "done"
  41. 41. TodoList collection class TodoApp.TodoList extends Backbone.Collection # Reference to this collection's model. model: TodoApp.Todo # Save all of the todo items under the `"todos"` namespace. url: '/todos' # Filter down the list of all todo items that are finished. done: -> @filter (todo) -> todo.get 'done' # Filter down the list to only todo items that are still not finished. remaining: -> @without this.done()... # We keep the Todos in sequential order, despite being saved by unordered # GUID in the database. This generates the next order number for new items. nextOrder: -> if @length then @last().get('order') + 1 else 1 # Todos are sorted by their original insertion order. comparator: (todo) -> todo.get 'order'
  42. 42. Todo item view class TodoApp.TodoView extends Backbone.View # ... is a list tag. tagName: "li" # Cache the template function for a single item. template: TodoApp.template '#item-template' # The DOM events specific to an item. events: "click .check" : "toggleDone" "dblclick div.todo-content" : "edit" "click span.todo-destroy" : "destroy" "keypress .todo-input" : "updateOnEnter" # The TodoView listens for changes to its model, re-rendering. Since there's # a one-to-one correspondence between a **Todo** and a **TodoView** in this # app, we set a direct reference on the model for convenience. initialize: -> _.bindAll this, 'render', 'close' @model.bind 'change', @render @model.bind 'destroy', => @remove() # Re-render the contents of the todo item. render: -> $(@el).html @template @model.toJSON() @setContent() this
  43. 43. # Re-render the contents of the todo item. render: -> $(@el).html @template @model.toJSON() @setContent() Todo item view this # To avoid XSS (not that it would be harmful in this particular app), # we use `jQuery.text` to set the contents of the todo item. setContent: -> content = @model.get 'content' @$('.todo-content').text content @input = @$('.todo-input') @input.blur @close @input.val content # Toggle the `"done"` state of the model. toggleDone: -> @model.toggle() # Switch this view into `"editing"` mode, displaying the input field. edit: -> $(@el).addClass "editing" @input.focus() # Close the `"editing"` mode, saving changes to the todo. close: -> @model.save content: @input.val() $(@el).removeClass "editing" # If you hit `enter`, we're through editing the item. updateOnEnter: (e) -> @close() if e.keyCode == 13 # Destroy the model. destroy: -> @model.destroy()
  44. 44. Application view class TodoApp.AppView extends Backbone.View # Instead of generating a new element, bind to the existing skeleton of # the App already present in the HTML. el: "#todoapp" # Our template for the line of statistics at the bottom of the app. statsTemplate: TodoApp.template '#stats-template' # Delegated events for creating new items, and clearing completed ones. events: "keypress #new-todo" : "createOnEnter" "keyup #new-todo" : "showTooltip" "click .todo-clear a" : "clearCompleted" # At initialization we bind to the relevant events on the `Todos` # collection, when items are added or changed. Kick things off by # loading any preexisting todos that might be saved. initialize: -> _.bindAll this, 'addOne', 'addAll', 'renderStats' @input = @$("#new-todo") @collection.bind 'add', @addOne @collection.bind 'refresh', @addAll @collection.bind 'all', @renderStats @collection.fetch()
  45. 45. @collection.bind 'add', @addOne @collection.bind 'refresh', @addAll @collection.bind 'all', @renderStats Application view @collection.fetch() # Re-rendering the App just means refreshing the statistics -- the rest # of the app doesn't change. renderStats: -> @$('#todo-stats').html @statsTemplate total: @collection.length done: @collection.done().length remaining: @collection.remaining().length # Add a single todo item to the list by creating a view for it, and # appending its element to the `<ul>`. addOne: (todo) -> view = new TodoApp.TodoView model: todo @$("#todo-list").append view.render().el # Add all items in the collection at once. addAll: -> @collection.each @addOne # Generate the attributes for a new Todo item. newAttributes: -> content: @input.val() order: @collection.nextOrder() done: false # If you hit return in the main input field, create new **Todo** model, # persisting it to server. createOnEnter: (e) -> if e.keyCode == 13 @collection.create @newAttributes() @input.val ''
  46. 46. view = new TodoApp.TodoView model: todo @$("#todo-list").append view.render().el # Add all items in the collection at once. Application view addAll: -> @collection.each @addOne # Generate the attributes for a new Todo item. newAttributes: -> content: @input.val() order: @collection.nextOrder() done: false # If you hit return in the main input field, create new **Todo** model, # persisting it to server. createOnEnter: (e) -> if e.keyCode == 13 @collection.create @newAttributes() @input.val '' # Clear all done todo items, destroying their views and models. clearCompleted: -> todo.destroy() for todo in @collection.done() false # Lazily show the tooltip that tells you to press `enter` to save # a new todo item, after one second. showTooltip: (e) -> tooltip = @$(".ui-tooltip-top") val = @input.val() tooltip.fadeOut() clearTimeout @tooltipTimeout if @tooltipTimeout unless val == '' or val == @input.attr 'placeholder' @tooltipTimeout = _.delay -> tooltip.show().fadeIn() , 1000
  47. 47. #todoapp index.html.haml .title %h1 Todos .content #create-todo %input#new-todo{:placeholder => "What needs to be done?", :type => "text"}/ %span.ui-tooltip-top{:style => "display:none;"} Press Enter to save this task #todos %ul#todo-list #todo-stats %ul#instructions %li Double-click to edit a todo. :coffeescript $ -> TodoApp.appView = new TodoApp.AppView collection: new TodoApp.TodoList %script#item-template{:type => "text/html"} .todo{:class => "{{#done}}done{{/done}}"} .display %input{:class => "check", :type => "checkbox", :"{{#done}}checked{{/done}}" => true} .todo-content %span.todo-destroy .edit %input.todo-input{:type => "text", :value => ""} %script#stats-template{:type => "text/html"} {{#if total}} %span.todo-count
  48. 48. %input#new-todo{:placeholder => "What needs to be done?", :type => "text"}/ %span.ui-tooltip-top{:style => "display:none;"} Press Enter to save this task #todos %ul#todo-list index.html.haml #todo-stats %ul#instructions %li Double-click to edit a todo. :coffeescript $ -> TodoApp.appView = new TodoApp.AppView collection: new TodoApp.TodoList %script#item-template{:type => "text/html"} .todo{:class => "{{#done}}done{{/done}}"} .display %input{:class => "check", :type => "checkbox", :"{{#done}}checked{{/done}}" => true} .todo-content %span.todo-destroy .edit %input.todo-input{:type => "text", :value => ""} %script#stats-template{:type => "text/html"} {{#if total}} %span.todo-count %span.number {{remaining}} %span.word {{pluralize remaining "item"}} left. {{/if}} {{#if done}} %span.todo-clear %a{:href => "#"} Clear %span.number-done {{done}} completed %span.word-done {{pluralize done "item"}} {{/if}}
  49. 49. One more thing: Backbone Controllers Routers class Workspace extends Backbone.Controller Router routes: "help" : "help" #help "search/:query" : "search" #search/kiwis "search/:query/p:page": "search" #search/kiwis/p7 help: -> ... search: (query, page) -> ...
  50. 50. How do you test it?
  51. 51. RSpec-like testing for JavaScript
  52. 52. Together with all other tests
  53. 53. Testing Todo model describe "Todo", -> todo = null ajaxCall = (param) -> jQuery.ajax.mostRecentCall.args[0][param] beforeEach -> todo = new TodoApp.Todo todos = new TodoApp.TodoList [todo] it "should initialize with empty content", -> expect(todo.get "content").toEqual "empty todo..." it "should initialize as not done", -> expect(todo.get "done").toBeFalsy() it "should save after toggle", -> spyOn jQuery, "ajax" todo.toggle() expect(ajaxCall "url").toEqual "/todos" expect(todo.get "done").toBeTruthy()
  54. 54. and TodoList collection describe "TodoList", -> attributes = [ content: "First" done: true , content: "Second" ] todos = null beforeEach -> todos = new TodoApp.TodoList attributes it "should return done todos", -> expect(_.invoke todos.done(), "toJSON").toEqual [attributes[0]] it "should return remaining todos", -> expect(_.invoke todos.remaining(), "toJSON").toEqual [attributes[1]]
  55. 55. Rails 3.1 Asset Pipeline
  56. 56. application.js.coffee using Sprockets #= require jquery #= require underscore #= require backbone #= require handlebars #= require ./todo_app #= require_tree ./models #= require ./views/helpers #= require_tree ./views
  57. 57. Watch RailsConf DHH keynote http://en.oreilly.com/rails2011/public/schedule/detail/19068
  58. 58. References http://jashkenas.github.com/coffee-script/ http://documentcloud.github.com/backbone/ http://pivotal.github.com/jasmine/ https://github.com/rsim/backbone_coffeescript_demo
  59. 59. Used in eazybi.com
  • TiffanyLitherland

    Apr. 29, 2019
  • bluemner

    Mar. 25, 2015
  • bunkertor

    Dec. 27, 2014
  • MilkAraya

    Dec. 19, 2013
  • ssuser86eb48

    Nov. 6, 2013
  • ericwangqing

    Jul. 16, 2013
  • ThnhHunh

    Apr. 22, 2013
  • makotoisawa

    Mar. 21, 2013
  • vadivelannatarajan94

    Feb. 14, 2013
  • sentdot

    Nov. 20, 2012
  • kbudde

    Oct. 7, 2012
  • little_yaw

    Aug. 10, 2012
  • rociiu

    Jun. 14, 2012
  • vasanth88

    Jun. 13, 2012
  • guido4000

    May. 5, 2012
  • BryantChan1

    Apr. 1, 2012
  • brucewinger

    Mar. 18, 2012
  • elfpavlik

    Mar. 9, 2012
  • mgrassotti

    Mar. 3, 2012
  • przyborowski

    Feb. 18, 2012

Presented at RailsWayCon 2011

Views

Total views

37,060

On Slideshare

0

From embeds

0

Number of embeds

18,486

Actions

Downloads

273

Shares

0

Comments

0

Likes

43

×