coderberry

AngularJS on Rails 4 - Part 2

| Comments

Let’s pick up where we left off. If you haven’t already, make sure you go through Part 1 to create your base Rails app with the API setup.

You can either continue using the code you have created on part 1 or you can catch up by checking out the tagged code:

$ git clone https://github.com/cavneb/angular_casts
$ cd angular_casts
$ git checkout step-1
$ bundle install
$ rake db:migrate; rake db:migrate RAILS_ENV=test
$ rake test
$ rake screencast_sync:railscasts

Add Angular Libraries

There are a couple of different ways we can add Angular into our application. Ryan Bates suggests using the angular-rails gem. Even though this is an excellent gem which is well maintained, it’s good to know how to do this without a gem.

In our app we are going to link our scripts using a CDN. We can find the CDN for Angular at https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js. We will also be adding Angular Resource via the CDN as well.

Update your layout file:

app/views/layouts/application.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!DOCTYPE html>
<html>
<head>
  <title>Angular Casts</title>
  <%= stylesheet_link_tag "application", media: "all" %>
  <%= csrf_meta_tags %>
</head>
<body>

  <%= yield %>

  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular-resource.min.js"></script>
  <%= javascript_include_tag "application" %>
</body>
</html>

Setup the Javascript Folders

We want to keep our code organized by placing our Angular controllers, filters, services, directives, etc. in the app/assets/javascripts folder. Create the following directories:

  • app/assets/javascripts/angular/controllers
  • app/assets/javascripts/angular/directives
  • app/assets/javascripts/angular/services

Here’s a shortcut to do this:

$ mkdir -p app/assets/javascripts/angular/controllers \
           app/assets/javascripts/angular/directives \
           app/assets/javascripts/angular/services

Now let’s create the main javascript file which will drive our Angular application.

app/assets/javascripts/app.js.coffee
1
window.App = angular.module('AngularCasts', ['ngResource'])

In this file we create a new module called AngularCasts and assign it to window.App. We also add the dependency of ngResource which provides simple REST client functionality.

Next, we need to update our JavaScript manifest to include the our Angular scripts. The order of these is important due to the latter ones depending on the ones prior to them.

app/assets/javascripts/application.js
1
2
//= require app
//= require_tree ./angular

This is quite a change from what exists in the manifest already. We will add jQuery later, but via CDN. You’ll see why later in this post.

Add the View

Next, we need to create a controller. This will allow us to set up a route to a view.

$ rails g controller home index

This set up the HomeController and added the action index. Before we modify this view, let’s update our layout to acts as an Angular app. This is done by adding the directive ng-app to our <html> tag:

app/views/layouts/application.html.erb
1
2
3
<!DOCTYPE html>
<html ng-app>
...

Now let’s update our index view with some simple Angular code:

app/views/home/index.html.erb
1
2
3
4
5
6
<div>
  <label>Name:</label>
  <input type="text" ng-model="yourName" placeholder="Enter a name here">
  <hr>
  <h1>Hello {{yourName}}!</h1>
</div>

Update your routes file to use this view as the root.

config/routes.rb
1
2
3
4
5
6
7
AngularCasts::Application.routes.draw do
  scope :api do
    get "/screencasts(.:format)" => "screencasts#index"
    get "/screencasts/:id(.:format)" => "screencasts#show"
  end
  root to: "home#index"
end

Note that the line get 'home#index' was removed. This is not needed because the root path directs to it.

Start up your server and open up http://localhost:3000. Type in your name into the text field. If the content changes as you type, it worked! You now have a functional Angular application!

If you are using Rails 3, you will need to delete the file public/index.html

Now the fun begins!

In order for us to tell the page that it should use the App module, we need to add the module name to the ng-app directive. Set the value of the attribute to AngularCasts:

app/views/layouts/application.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!DOCTYPE html>
<html ng-app="AngularCasts">
<head>
  <title>Angular Casts</title>
  <%= stylesheet_link_tag "application", media: "all" %>
  <%= csrf_meta_tags %>
</head>
<body>
  <header>
    Angular Casts
  </header>

  <%= yield %>

  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular-resource.min.js"></script>
  <%= javascript_include_tag "application" %>
</body>
</html>

Now our view knows to use the AngularCasts module.

On lines 9-11 we have added the <header> content. Make sure you have this in your layout as well.

Create an Angular Controller

Let’s create a controller that will be used to list out the episodes. Create a new coffeescript file at app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee

app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee
1
2
3
App.controller 'ScreencastsCtrl', ['$scope', ($scope) ->
  $scope.message = "Angular Rocks!"
]

On line 1, we create a new Angular controller belonging to App named ScreencastsCtrl. The controller will be referenced in our view as ScreencastsCtrl. For more information on Angular controllers, read http://docs.angularjs.org/guide/dev_guide.mvc.understanding_controller.

Let’s update our view to display the message.

app/views/home/index.html.erb
1
2
3
<div ng-controller="ScreencastsCtrl">
  <h1>Message: {{message}}</h1>
</div>

Here we have bound the contents of the div to the controller ScreencastsCtrl. Refresh the browser and you should see ‘Message: Angular Rocks!’.

Make it Pretty!

Lets add the much needed CSS to our application. Copy the following into app/assets/stylesheets/home.css.scss.

app/assets/stylesheets/home.css.scss
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
body {
  font-size: 12px;
  font-family: Helvetica, sans-serif;
  background-color: #ddd;
  margin: 0px;
}

header {
  background-color: #4F4F4F;
  color: #fff;
  position: absolute;
  height: 36px;
  top: 0;
  left: 0;
  right: 0;
  font-size: 18px;
  line-height: 36px;
  font-weight: bold;
  padding-left: 15px;
}

#screencast-ctrl {
  background-color: #fff;
  position: absolute;
  top: 37px;
  width: 100%;
  bottom: 0;
  overflow: auto;
}

#screencast-list-container {
  background-color: #fff;
  position: absolute;
  min-height: 700px;
  width: 300px;
  top: 37px;
  left: 0;
  bottom: 0;
  overflow: auto;
  -webkit-overflow-scrolling: touch;
  ul {
    margin: 0px;
    list-style: none;
    padding: 0px;
    li {
      cursor: pointer;
      border-bottom: 1px solid #ddd;
      padding: 0 10px;
    }
  }
  h3 {
    font-size: 14px;
    small {
      font-size: 12px;
      color: #ccc;
      font-weight: normal;
    }
    &.active {
      color: red;
    }
  }
}

#screencast-view-container {
  position: absolute;
  border-left: 1px solid #d0d0d0;
  top: 37px;
  left: 300px;
  right: 0;
  bottom: 0;
  background-color: #fff;
  min-height: 700px;
  padding: 5px 25px;

  #player {
    border: 1px solid #000;
    max-width: 800px;
  }
}

Refresh the browser. Ooooh!

Start with the Service

Our Angular controller is going to access the data from our API using ngResource. ngResource enables interation with RESTful server-side data sources.

Angular services are singletons that carry out specific tasks common to web apps. Services are commonly used to perform the XHR interaction with the server. To learn about the differences between services and factories, read this. Let’s start off by creating a service at screencast.js.coffee:

app/assets/javascripts/angular/services/screencast.js.coffee
1
2
3
App.factory 'Screencast', ['$resource', ($resource) ->
  $resource '/api/screencasts/:id', id: '@id'
]

Now tell the controller to use this service:

app/assets/javascripts/controllers/screencasts_ctrl.js.coffee
1
2
3
App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) ->
  $scope.screencasts = Screencast.query()
]

Update the index view with the following:

app/views/home/index.html.erb
1
2
3
4
5
6
7
8
9
<div ng-controller="ScreencastsCtrl">
  <div id="screencast-list-container">
    <ul>
      <li ng-repeat="screencast in screencasts">
        <h3>{{screencast.title}} <small>({{screencast.duration}})</small></h3>
      </li>
    </ul>
  </div>
</div>

Now refresh the page. If all worked well, you should see a list of screencasts on the left side. When we reloaded the page, a GET request was sent to /api/screencasts, populating the screencasts attribute in our scope. This is the power of Angular ngResource.

Now What?

We are doing great! Now we have a list of screencasts on the side which are clickable. However, we don’t do anything when they are clicked. What we want to do is show the screencast in the main section along with some additional screencast information.

Start off by adding the HTML code which will be used to display the main content. This is done inside the index.html.erb file:

app/views/home/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div ng-controller="ScreencastsCtrl">
  <div id="screencast-list-container">
    <ul>
      <li ng-repeat="screencast in screencasts">
        <h3>{{screencast.title}} <small>({{screencast.duration}})</small></h3>
      </li>
    </ul>
  </div>

  <div id="screencast-view-container" ng-show="selectedScreencast">
    <h2>{{selectedScreencast.title}}</h2>
    <p>{{selectedScreencast.summary}}</p>
    <p>
      Published at {{selectedScreencast.published_at | date: 'mediumDate'}}
      - <a ng-href="{{selectedScreencast.link}}">{{selectedScreencast.link}}</a>
    </p>
  </div>
</div>

On lines 10-17, we have added a div which shows the screencast title and summary. On line 10, we use the ng-show directive which only displays the div if selectedScreencast exists.

Go ahead and refresh the page. You should not see any changes. Click on a screencast. Still no changes.

Click and Show

In order for us to show the main content with the screencast information, we need to do a few things. The first thing we need to do is add an ng-click directive to our screencast list:

app/views/home/index.html.erb
1
2
3
4
5
6
7
8
9
10
...
  <div id="screencast-list-container" >
    <ul>
      <li ng-repeat="screencast in screencasts"
          ng-click="showScreencast(screencast)">
        <h3>{{screencast.title}} <small>({{screencast.duration}})</small></h3>
      </li>
    </ul>
  </div>
...

Now when the list item is clicked the function showScreencast will be triggered with the screencast being passed in to it. Now let’s update our controller with this function:

app/assets/javascripts/angular/controllers/screencasts_ctrl.js.coffee
1
2
3
4
5
6
7
8
9
App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) ->
  # Attributes accessible on the view
  $scope.screencasts        = Screencast.query()
  $scope.selectedScreencast = null

  # Set the selected screencast to the one which was clicked
  $scope.showScreencast = (screencast) ->
    $scope.selectedScreencast = screencast
]

Refresh your browser and click on a screencast. As my wife would incorrectly say: “Waalah!”

Show the Screencast

After doing a bit of looking around, I found that Flow Player offered the easiest and cleanest way to show videos. Let’s add the dependent scripts and css links to our layout:

app/views/layouts/application.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html ng-app="AngularCasts">
<head>
  <title>Angular Casts</title>
  <link href="//releases.flowplayer.org/5.4.0/skin/minimalist.css" rel="stylesheet">
  <%= stylesheet_link_tag "application", media: "all" %>
  <%= csrf_meta_tags %>
</head>
<body>
  <header>
    Angular Casts
  </header>

  <%= yield %>

  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8/jquery.min.js"></script>
  <script src="//releases.flowplayer.org/5.4.0/flowplayer.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
  <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular-resource.min.js"></script>
  <%= javascript_include_tag "application" %>
</body>
</html>

Remember how we removed jQuery from our Gemfile and javascript manifest? The reason is because we didn’t want it concatenated with the other scripts. FlowPlayer depends on jQuery and so jQuery needs to be available prior to Flow Player being loaded. We also can load it via CDN. Good times for all.

Create the FlowPlayer Directive

FlowPlayer requires triggering a function flowplayer() to show the video. We could add this into our controller, but we love to learn. Let’s create a directive which listens to the controller and triggers the flowplayer function when showScreencast is called.

Create the directive at app/assets/javascripts/angular/directives/flow_player.js.coffee

app/assets/javascripts/angular/directives/flow_player.js.coffee
1
2
3
4
5
6
7
8
9
10
11
12
App.directive 'flowPlayer', ->
  (scope, element, attrs) ->

    # Trigger when the selectedScreencast function is called
    # with a screencast
    scope.$watch 'selectedScreencast', (screencast) ->
      if screencast

        # See http://flowplayer.org/docs/
        element.flowplayer
          playlist: [[mp4: screencast.video_url]]
          ratio: 9 / 14

Now add the directive into our view:

app/views/home/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
...
  <div id="screencast-view-container" ng-show="selectedScreencast">
    <h2>{{selectedScreencast.title}}</h2>
    <p>{{selectedScreencast.summary}}</p>
    <div flow-player="" id="player"></div>
    <p>
      Published at {{selectedScreencast.published_at | date: 'mediumDate'}}
      - <a ng-href="{{selectedScreencast.link}}">{{selectedScreencast.link}}</a>
    </p>
  </div>
...

Refresh your browser and go nuts!

Extra Goodies

One final thing that I would like to see is some sort of indicator which lets us know which video is playing on the screencast list. This can be done via CSS and some simple code.

In our CSS file, we have already added some style for an active screencast. Any H3 tag on the side with the class of active will show as red. Try it out by adding the class to our view:

app/views/home/index.html.erb
1
2
3
4
5
6
7
8
9
10
...
  <div id="screencast-list-container" >
    <ul>
      <li ng-repeat="screencast in screencasts"
          ng-click="showScreencast(screencast)">
        <h3 class="active">{{screencast.title}} <small>({{screencast.duration}})</small></h3>
      </li>
    </ul>
  </div>
...

Refresh the page. You should now see that every screencast link on the left is red.

For us to make it show for the active screencast only we have to make a few changes to our view and controller. Update the view to use the ng-class directive:

app/views/home/index.html.erb
1
2
3
4
5
6
7
8
9
10
11
...
  <div id="screencast-list-container" >
    <ul>
      <li ng-repeat="screencast in screencasts"
          ng-click="showScreencast(screencast, $index)">
        <h3 ng-class="{active: $index == selectedRow}">{{screencast.title}}
          <small>({{screencast.duration}})</small></h3>
      </li>
    </ul>
  </div>
...

Note that on line 5 we added the 2nd attribute &index. This is available via the ng-repeat directive and is the index value of the array (integer). We also modified line 6 to use the ng-class directive which only shows “active” if the $index is equal to $scope.selectedRow.

Now update the controller to work with these changes:

app/assets/javascripts/controllers/screencasts_ctrl.js.coffee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
App.controller 'ScreencastsCtrl', ['$scope', 'Screencast', ($scope, Screencast) ->
  # Attributes accessible on the view
  $scope.selectedScreencast = null
  $scope.selectedRow        = null

  # Gather the screencasts and set the selected one to the first on success
  $scope.screencasts = Screencast.query ->
    $scope.selectedScreencast = $scope.screencasts[0]
    $scope.selectedRow = 0

  # Set the selected screencast to the one which was clicked
  $scope.showScreencast = (screencast, row) ->
    $scope.selectedScreencast = screencast
    $scope.selectedRow = row
]

We have added the new param row to the showScreencast function and set this to $scope.selectedRow. Refresh your browser and see how things have changed.

Completed Project

View the working app at http://angular-casts.herokuapp.com/.

Final Thoughts

I know there are no front-end tests for this tutorial. This was intentional. They were too hard. I spent hours upon hours trying to get the tests working with $httpBackend and service testing, etc. I tried karma and testem. Too much time. Too little benefit. If you are better at this stuff than I am please send me an email or pull request or something with the changes you’ve made to include the tests. I can always add a Part 3 which is about nothing other than testing the frontend.

Thank you all who have patiently waited for me to finish this post. I learned never to release a post before it has been proofread and run through several times. Next time I’m going to do a screencast.. just like the old days.

AngularJS on Rails 4 - Part 1

| Comments

Angular seems to be the big craze as of late. Some may agree and some may not, but AngularJS is one of the next big contenders for being the number one choice of developers. At the time of writing this article, AngularJS is the 12th most watched project on GitHub.

Here I want to create a useful Rails application using Angular. The goal is to have a single-page application which allows us to select a screencast link on the left and view it on the right. An example of this would be found at http://ember101.com.

Originally I had presented this topic at our local ruby users group. My typical workflow is to write a blog post before presenting and have that post be a reference to my presentation. Since then, I have received a lot of feedback on how I could have enhanced the app. These posts (part 1 and 2) been re-written to reflect those changes. Special thanks goes to Tad Thorley for providing the excellent example application based off of the original. Also thanks goes out to those who have commented on these posts.

Creating the Rails Application

I had a hard time deciding when I began this project on whether to use a full Rails application or a very lightweight ruby web stack like Sinatra. I also experimented with a middle-ground solution called Rails::API (see Railscast). In the end, I used standard Rails (version 4.0.0.rc1). This gave me the flexibility I want; and for the scope of this tutorial I didn’t want to distract from learning how to use Angular in an Rails application.

Before anything, we need to create a new Rails application called Angular Casts

$ rails new angular_casts
...
$ cd angular_casts

Creating the Model and Controller

Our application will be pretty simple. We are only going to worry about storing screencast information in our database. Rails by default comes with a very lightweight database server called SQLite. If you aren’t familiar with this, you can visit http://guides.rubyonrails.org/getting_started.html#configuring-a-database to learn more.

Before we create our model, we need to determine what information we want to store. As a user of this application, I think the most useful would be:

  • title: What is the name of the screencast?
  • summary: What is the screencast about?
  • duration: How long is the screencast?
  • link: How do I get to the original screencast?
  • published: When was the screencast published?
  • source: Who is the provider of the screencast?

Let’s create a model and controller based on this information. We need to add the video_url field as well, which will be used to embed the video into our app.

$ rails g resource screencast title summary:text duration link published_at:datetime source video_url

By running the resource generator, we now have a model and a controller. The controller will provide our REST API. We also can see that the screencasts resources have been added to our routes:

config/routes.rb
1
2
3
4
AngularCasts::Application.routes.draw do
  resources :screencasts
  ...
end

Run the migration tasks for both development and test environments:

$ rake db:migrate; rake db:migrate RAILS_ENV=test

Testing the Model

Testing is not the primary topic of these blog posts, so less time and explanation will be given to them. If you want to learn more about testing, I recommend http://railscasts.com/episodes/275-how-i-test.

For us to have a long-lasting, maintainable application it is imperative that we keep our code tested. This will also help us stay focused on what our application and code is supposed to do.

Start by running the rake task for testing to see make sure the tests run:

$ rake test

If all worked well, you should see something like 0 tests, 0 assertions, 0 failures, 0 errors, 0 skips. This means that the test suite ran, but it didn’t find any tests to run.

There are a couple of things we will want to test:

  • Make sure that all the required data exists for each screencast.
  • Make sure that we do not have two of the same screencast (duplicates).

Before we write our tests, let’s update our fixtures file with some testable data:

test/fixtures/screencasts.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html

fast_rails_commands:
  title: "Fast Rails Commands"
  summary: "Rails commands, such as generators, migrations, and tests, have a tendency to be slow because they need to load the Rails app each time. Here I show three tools to make this faster: Zeus, Spring, and Commands."
  duration: "8:06"
  link: "http://railscasts.com/episodes/412-fast-rails-commands"
  published_at: "Thu, 04 Apr 2013 00:00:00 -0700"
  source: "railscasts"
  video_url: "http://media.railscasts.com/assets/episodes/videos/412-fast-rails-commands.mp4"

wizard_forms_with_wicked:
  title: "Wizard Forms with Wicked"
  summary: "Creating a wizard form can be tricky in Rails. Learn how Wicked can help by turning a controller into a series of multiple steps."
  duration: "11:57"
  link: "http://railscasts.com/episodes/346-wizard-forms-with-wicked"
  published_at: "Thu, 03 May 2012 00:00:00 -0700"
  source: "railscasts"
  video_url: "http://media.railscasts.com/assets/episodes/videos/346-wizard-forms-with-wicked.mp4"

sending_html_emails:
  title: "Sending HTML Email"
  summary: "HTML email can be difficult to code because any CSS should be made inline. Here I present a few tools for doing this including the premailer-rails3 and roadie gems."
  duration: "5:42"
  link: "http://railscasts.com/episodes/312-sending-html-email"
  published_at: "Mon, 02 Jan 2012 00:00:00 -0800"
  source: "railscasts"
  video_url: "http://media.railscasts.com/assets/episodes/videos/312-sending-html-email.mp4"

Open up the auto-generated file test/models/screencast_test.rb and add the following tests. If you are using Rails 3.x, the test file will appear under test/unit/screencast_test.rb.

test/models/screencast_test.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
require 'test_helper'

class ScreencastTest < ActiveSupport::TestCase
  setup do
    @screencast_defaults = {
      title:        'Facebook Authentication',
      summary:      'This will show how to create a new facebook application and configure it. Then add some authentication with the omniauth-facebook gem and top it off with a client-side authentication using the JavaScript SDK.',
      duration:     '12:09',
      link:         'http://railscasts.com/episodes/360-facebook-authentication',
      published_at: Date.parse('Mon, 25 Jun 2012 00:00:00 -0700'),
      source:       'railscasts',
      video_url:    'http://media.railscasts.com/assets/episodes/videos/360-facebook-authentication.mp4'
    }
  end

  test "should be invalid if missing required data" do
    screencast = Screencast.new
    assert !screencast.valid?
    [:title, :summary, :duration, :link, :published_at, :source, :video_url].each do |field_name|
      assert screencast.errors.keys.include? field_name
    end
  end

  test "should be valid if required data exists" do
    screencast = Screencast.new(@screencast_defaults)
    assert screencast.valid?
  end

  test "should only allow one screencast with the same video url" do
    screencast = Screencast.new(@screencast_defaults)
    screencast.video_url = screencasts(:fast_rails_commands).video_url
    assert !screencast.valid?
    assert screencast.errors[:video_url].include? "has already been taken"
  end
end

Once this is in place, run the tests again with the command rake test.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ rake test
Run options: --seed 29768

# Running tests:

F.F

Finished tests in 0.048601s, 61.7271 tests/s, 61.7271 assertions/s.

  1) Failure:
ScreencastTest#test_should_be_invalid_if_missing_required_data [../angular_casts/test/models/screencast_test.rb:18]:
Failed assertion, no message given.

  2) Failure:
ScreencastTest#test_should_only_allow_one_screencast_with_the_same_video_url [../angular_casts/test/models/screencast_test.rb:32]:
Failed assertion, no message given.

3 tests, 3 assertions, 2 failures, 0 errors, 0 skips

You can see we have have 3 tests with 3 assertions and 2 failures. The reason we didn’t get 3 failures is because the second test, “should be valid if required data exists”, will always pass until we set up some restrictions on the model.

Let’s update the model to make these tests pass.

app/models/screencast.rb
1
2
3
4
class Screencast < ActiveRecord::Base
  validates_presence_of :title, :summary, :duration, :link, :published_at, :source, :video_url
  validates_uniqueness_of :video_url
end

Run the tests again to see them pass successfully!

Importing the Video Data

Because we are going to import video feeds from external sites, we need to use a feed parsing library. The best one available is feedzirra. Let’s add it to our Gemfile and remove turbolinks, jbuilder and sdoc. We also remove jquery-rails because we will use it via CDN instead of inside the Asset Pipeline. This will be further explained in part 2.

Gemfile
1
2
3
4
5
6
7
8
source 'https://rubygems.org'

gem 'rails',        '4.0.0.rc1'
gem 'sqlite3'
gem 'sass-rails',   '~> 4.0.0.rc1'
gem 'uglifier',     '>= 1.3.0'
gem 'coffee-rails', '~> 4.0.0'
gem 'feedzirra'

Now install the gems:

$ bundle install

Create an Import Library

Now that we have a place to put all of the screencast information, we need to be able to import it from external feeds. Here we are going to create a simple Ruby class that uses the feedzirra gem to grab the feed data, parse it and then add it to our database.

Let’s start off by creating a new class called ScreencastImporter. Paste the following code into lib/screencast_importer.rb.

lib/screencast_importer.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
require 'feedzirra'

class ScreencastImporter
  def self.import_railscasts

    # because the Railscasts feed is targeted at itunes, there is additional metadata that
    # is not collected by Feedzirra by default. By using add_common_feed_entry_element,
    # we can let Feedzirra know how to map those values. See more information at
    # http://www.ruby-doc.org/gems/docs/f/feedzirra-0.1.2/Feedzirra/Feed.html
    Feedzirra::Feed.add_common_feed_entry_element(:enclosure, :value => :url, :as => :video_url)
    Feedzirra::Feed.add_common_feed_entry_element('itunes:duration', :as => :duration)

    # Capture the feed and iterate over each entry
    feed = Feedzirra::Feed.fetch_and_parse("http://feeds.feedburner.com/railscasts")
    feed.entries.each do |entry|

      # Strip out the episode number from the title
      title = entry.title.gsub(/^#\d{1,}\s/, '')

      # Find or create the screencast data into our database
      Screencast.where(video_url: entry.video_url).first_or_create(
        title:        title,
        summary:      entry.summary,
        duration:     entry.duration,
        link:         entry.url,
        published_at: entry.published,
        source:       'railscasts' # set this manually
      )
    end

    # Return the number of total screencasts for the source
    Screencast.where(source: 'railscasts').count
  end
end

Note that on lines 17-18, we strip out the episode number from the Railscast title. So “#412 Fast Rails Commands” would become “Fast Rails Commands”. See this Rubular to see how I determined the RegExp pattern.

Now if we were to go into our Rails console, we could trigger this import manually. Give it a shot!

$ rails c
Loading development environment (Rails 4.0.0.rc1)

>> require 'screencast_importer'
=> true

>> ScreencastImporter.import_railscasts
.... lots ... of ... feedback ....
=> 345

>> Screencast.count
=> 345
At the time of writing this article, there were 345 public Railscasts. This number will increase as time goes on.

Trigger Import via Rake

Instead of loading up the Rails console and performing the import manually, it would me much easier to have a simple command that we could run to perform the imports. This is where rake tasks come in. Let’s create a rake task that will perform the import.

lib/tasks/screencast_sync.rake
1
2
3
4
5
6
7
8
9
require 'screencast_importer'

namespace :screencast_sync do
  desc 'sync all missing screencasts from Railscasts.com'
  task :railscasts => :environment do
    total = ScreencastImporter.import_railscasts
    puts "There are now #{total} screencasts from Railscasts.com"
  end
end

Now that we have our rake task set up, go ahead and run the command:

$ rake screencast_sync:railscasts
There are now 345 screencasts from Railscasts.com

It worked! But no time to celebrate.. let’s move on.

Making Episodes Accessible via API

Because we are planning on using Angular for our front-end, we only need to expose our data as JSON. This will allow Angular to talk to the backend via ajax calls.

We are going to only use two calls to the API:

  • /screencasts.json - returns a full list of episodes
  • /screencasts/ID.json - returns data for a specified screencast (where ID is the unique ID of the screencast in our db)

Because we used the resource generator, we already have our controller and routes.

Let’s do some cleanup to the routes and make sure we only are allowing what we want to use. On top of that, let’s scope our calls to the API with api.

config/routes.rb
1
2
3
4
5
6
AngularCasts::Application.routes.draw do
  scope :api do
    get "/screencasts(.:format)" => "screencasts#index"
    get "/screencasts/:id(.:format)" => "screencasts#show"
  end
end

Run the command rake routes to see our changes.

$ rake routes
Prefix Verb URI Pattern                    Controller#Action
 GET /api/screencasts(.:format)     screencasts#index
 GET /api/screencasts/:id(.:format) screencasts#show

Now update the controller to render the correct JSON data for the two URL’s.

app/controllers/screencast_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
class ScreencastsController < ApplicationController
  # GET /screencasts
  # GET /screencasts.json
  def index
    render json: Screencast.all
  end

  # GET /screencasts/:id
  # GET /screencasts/:id.json
  def show
    render json: Screencast.find(params[:id])
  end
end

Now start up your Rails application and visit this link: http://localhost:3000/api/screencasts.json. If all went well, you should see JSON data. You should also be able to view http://localhost:3000/api/screencasts/1.json and see the data belonging to a single screencast.

http://localhost:3000/api/screencasts/1.json
1
2
3
4
5
6
7
8
9
10
11
12
{
  "id": 1,
  "title": "Upgrading to Rails 4",
  "summary": "With the release of Rails 4.0.0.rc1 it's time to try it out and report any bugs. Here I walk you through the steps to upgrade a Rails 3.2 application to Rails 4.",
  "duration": "12:44",
  "link": "http://railscasts.com/episodes/415-upgrading-to-rails-4",
  "published_at": "2013-05-06T07:00:00.000Z",
  "source": "railscasts",
  "video_url": "http://media.railscasts.com/assets/episodes/videos/415-upgrading-to-rails-4.mp4",
  "created_at": "2013-05-21T18:22:29.719Z",
  "updated_at": "2013-05-21T18:22:29.719Z"
}

Testing the API

Of course we are going to test the API! It actually isn’t as complicated as it may seem. I am not going to go over much explanation beyond the inline comments.

Create a new integration test at test/integration/api_screencasts_test.rb.

test/integration/api_screencasts_test.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
require 'test_helper'

class ApiScreencastsTest < ActionDispatch::IntegrationTest
  test "get /api/screencasts.json" do
    get "/api/screencasts.json"
    assert_response :success
    assert body == Screencast.all.to_json
    screencasts = JSON.parse(response.body)
    assert screencasts.size == 3 # because there are three fixtures (see screencasts.yml)
    assert screencasts.any? { |s| s["title"] == screencasts(:fast_rails_commands).title }
  end

  test "get /api/screencasts/:id" do
    screencast = screencasts(:fast_rails_commands)
    get "/api/screencasts/#{screencast.id}.json"
    assert_response :success
    assert body == screencast.to_json
    assert JSON.parse(response.body)["title"] == screencast.title
  end
end

Go ahead and run your tests:

$ rake test
...
5 tests, 18 assertions, 0 failures, 0 errors, 0 skips

Looks like our test are passing.


Let’s stop for now. Our next steps will be getting our hands dirty with AngularJS.

Go to Part 2

What Is the Best Memory Config for RubyMine?

| Comments

RubyMine has become awesomer and awesomer over the years and is now my primary development tool for Rails development. One thing I’ve found frustrating however is that there is very little documentation on the best memory settings for your computer.

I’d like to get a thread going here with people identifying their computer, CPU, memory and config settings for RubyMine.

The config file can be located in the bin folder of your application. On my mac, the file is /Applications/RubyMine/bin/idea.vmoptions. If it doesn’t exist you may need to create it.

So here are my computer specs and vmoptions:

MacBook Pro Retina 2.6 GHz Intel Core i7 with 16 GB memory

1
2
3
4
5
6
-Xms128m
-Xmx2048m
-XX:MaxPermSize=500m
-XX:ReservedCodeCacheSize=128m
-XX:+UseCodeCacheFlushing
-XX:+UseCompressedOops

Please feel free to post yours in the comments below. I believe this will help others optimize their RubyMine experience.