Amazon

Thursday, July 17, 2014

Why Controller#pre_render?

Recently I was pleasantly surprised that my pre_render methodology still works with Rails 4.1.4, which led me to re-visit and re-answer for myself the question of why it is helpful.

Consider the following Account model. To properly create an account, we need to associate it with a user record:

class Account < ActiveRecord::Base
  belongs_to :user
  validates :user, :presence => true
end

To do so, the following AccountsController#new is necessary:

class AccountsController < AuthenticatedController

  before_action :set_account, only: [:show, :edit, :update, :destroy]

  # GET /accounts/new
  def new
    @account = Account.new
    # Will be used by view to populate a
    # user drop down
    @users = User.all.order(:last_name)
  end

  # GET /accounts/1/edit
  def edit
    # Will be used by view to populate a
    # user drop down
    @users = User.all.order(:last_name)
  end

  # POST /accounts
  # POST /accounts.json
  def create
    @account = Account.new(account_params)
    respond_to do |format|
      if @account.save
        format.html { ... }
        format.json { ... }
      else
        format.html do
          # Needed here again to populate the user
          # drop down in error case
          @users = User.all.order(:last_name)
          render action: 'new'
        end
        format.json { ... }
      end
    end
  end

  # PATCH/PUT /accounts/1
  # PATCH/PUT /accounts/1.json
  def update
    respond_to do |format|
      if @account.update(account_params)
        format.html { ... }
        format.json { ... }
      else
        format.html do
          # Needed here again to populate the user
          # drop down in error case
          @users = User.all.order(:last_name)
          render action: 'edit'
        end
        format.json { ... }
      end
    end
  end

end

The same pattern needs to be repeated for the edit/update actions as well. Which means there are 4 different code paths in which @users needs to be initialized. This problem will grow for models with more complex relationships as well. So wouldn't it be nice if we can have a central place where @user is initialized? We can accomplish this using the #pre_render approach to be DRY-ier:

class AccountsController < AuthenticatedController

  before_action :set_account, only: [:show, :edit, :update, :destroy]

  # GET /accounts/new
  def new
    @account = Account.new
  end

  # GET /accounts/1/edit
  def edit
  end

  # POST /accounts
  # POST /accounts.json
  def create
    @account = Account.new(account_params)
    respond_to do |format|
      if @account.save
        format.html { ... }
        format.json { ... }
      else
        format.html { render action: 'new' }
        format.json { ... }
      end
    end
  end

  # PATCH/PUT /accounts/1
  # PATCH/PUT /accounts/1.json
  def update
    respond_to do |format|
      if @account.update(account_params)
        format.html { ... }
        format.json { ... }
      else
        format.html { render action: 'edit' }
        format.json { ... }
      end
    end
  end

protected

  def pre_render(action)
    case action
    when :edit, :new
      # Will be used by view to populate a
      # user drop down
      @users = User.all.order(:last_name)
    end
  end

end

Pretty nifty!

Thursday, August 15, 2013

Add new page to Rails app documentation

I learnt about rake doc:app in Rails 4.0.0 the other day. Pretty handy stuff. But, it turns out if I wanted to add, say, a CHANGES.rdoc at the same level as README.rdoc, and have it included as a page in the generated HTML output, there is no simple, built-in way to do it.

I know this because I figured out that the doc:app task is part of the railties gem. Opening up its documentation.rake file, I found this code block:

RDocTaskWithoutDescriptions.new("app") { |rdoc|
  rdoc.rdoc_dir = 'doc/app'
  rdoc.template = ENV['template'] if ENV['template']
  rdoc.title    = ENV['title'] || "Rails Application Documentation"
  rdoc.options << '--line-numbers'
  rdoc.options << '--charset' << 'utf-8'
  rdoc.rdoc_files.include('README.rdoc')
  rdoc.rdoc_files.include('app/**/*.rb')
  rdoc.rdoc_files.include('lib/**/*.rb')
}

As you can see, the task only recognizes README.rdoc in an app's top-level directory. But, if I change the code a tiny bit, then it will happily include as many .rdoc files as I create.

RDocTaskWithoutDescriptions.new("app") { |rdoc|
  rdoc.rdoc_dir = 'doc/app'
  rdoc.template = ENV['template'] if ENV['template']
  rdoc.title    = ENV['title'] || "Rails Application Documentation"
  rdoc.options << '--line-numbers'
  rdoc.options << '--charset' << 'utf-8'
  rdoc.rdoc_files.include('*.rdoc') # include all .rdoc files
  rdoc.rdoc_files.include('app/**/*.rb')
  rdoc.rdoc_files.include('lib/**/*.rb')
}

I opened up issue #11903 about this. Let's see how it goes.

Sunday, February 17, 2013

Controller#pre_render

As a follow-up to a previous post about Initialize Instance Variables Needed By A View In One Place, here is a more evolved/elegant approach.

First, I override the render method in app/controllers/application_controller.rb:

# override render so we can call pre_render on child classes

def render(*args)
  if self.class.method_defined? :pre_render
    action = args[0].is_a?(Hash) ? (args[0][:action] || action_name) : action_name
    self.pre_render(action.to_sym)
  end
  super
end

Then, I optionally define the pre_render method in my other controllers:

# define this method so that we can initialize the needed instance
# variables the views need all in one place

def pre_render(action)
  case action
  when :edit, :new
    @users = User.all
  when :show
    @subaccounts = @account.subaccounts.order(:name)
  end
end
Conceptually, this is similar to ASP.NET's PreRender() event handler.

Thursday, February 14, 2013

Linux Myth

I remember one of the knocks against Windows was how often it needs to be patched and rebooted, and that Linux/Unix does not. Well, I am here to say that is a myth. I manage a couple of Ubuntu boxes, and the rate at which patches and reboots come in are just as frequent, if not more so, than the Windows ones I manage.

Wednesday, February 6, 2013

Initialize Instance Variables Needed By A View In One Place

In Rails, should Controller#create fail, the pre-generated code will call render :action => "new". But all too often rendering the new view requires other instance variables to be assigned, which don't get assigned in this execution path if I place the initialization code in the Controller#new method.

I have been frustrated by the lack of a good way to handle it. At first I looked into the presenter pattern, but did not come up with anything that I liked.

Then, I came across this idea, which at first glance, seems to work well. My main goal is to have a good place to initialize the needed instance variables needed by the view only once, and overriding #render seems like a good way to go.

Your thoughts?
# Override this method so that we can initialize the needed
# instance variables the view needs all in one place

def render(*args)
  action = args[0][:action] || action_name
  case action
  when "new"
    @accounts = Account.order(:name)
    ...
  end
end

Monday, January 28, 2013

Stream Videos From Mac To AppleTV Without iTunes

I have been looking for a solution to stream videos from my iMac to Apple TV without needing to use iTunes.

I don't like to keep vidoes in iTunes because my iTunes library is already rather unwieldy. Adding gigabytes of videos to it, and then subjecting the bloated thing to Time Machine backup? Does not sound like fun. I could opt to not copy the videos into my iTunes library when I add them, but that same setting will apply to my music as well. No joy there. So how can I stream these videos?

First there was AirFlick, but that stopped working with Lion. Then I thought perhaps Mountain Lion's AirPlay Mirroring would do the trick, except my iMac isn't recent enough to use it. Bummer. I even purchased a copy of AirParrot to wirelessly use an AppleTV connected HDTV as a second monitor to a MacBook when it's in the same room as the TV. But no joy to streaming videos stored on an iMac, which is in an adjacent room.

Then a solution hit me. Here is what I did to set things up:

  1. Open Termninal, and create a symbolic link under /home/me/Sites/Videos, pointing to where I have my videos stored.
  2. Edit /etc/apache2/users/me.conf thusly (sudo required):
  3. # Adding the "*" at the end enables the server to serve up sub-directories.
    <Directory "/Users/me/Sites/*">
        Options Indexes MultiViews
        AllowOverride None
        Order allow,deny
        Allow from all
    </Directory>
    
  4. Go to System Preferences > Sharing, and turn on Web Sharing.
  5. Turn on and configure AirPlay on my AppleTV.

Now, whenever I want to watch a video stored in my iMac on my HDTV, I just:

  1. Open up Safari on an iOS device, and browse to http://myimac.local/~me/videos. Better yet, bookmark it.
  2. From the simplistic index page generated by Apache, choose a video to play.
  3. Once the video starts to play on my mobile device, tap on the AirPlay icon and choose AppleTV.
  4. In a few seconds, the video will start playing on the AppleTV. At this point, I can use the Remote app or the Apple Remote to control the playback on AppleTV.

This solution works perfectly, except for when my iMac is asleep. To overcome this, I installed the free mWOL app on my iOS devices and configured them to be able to remotely wake up the iMac as needed to stream videos.

By the way, the last two steps about choosing AirPlay to AppleTV are optional, especially when using an iPad.


Sunday, January 27, 2013

Stitching Together .m4v Files on OS X Lion

So I have a bunch of segmented .m4v files that I wanted to join together into one single file. A bunch of googling & experimentation later, I came across this solution:

  1. Download the most recent/stable version of GPAC, mount the disk image, and drag the application into the /Applications folder. Then, unmount the image.
  2. Open Terminal, and cd into the directory where my video files are.
  3. Suppose my files are named 1.m4v, 2.m4v, and 3.m4v, etc. Execute the following commands:
    • /Applications/Osmo4.app/Contents/MacOS/MP4Box -force-cat -cat 1.m4v movie.m4v
    • /Applications/Osmo4.app/Contents/MacOS/MP4Box -force-cat -cat 2.m4v movie.m4v
    • /Applications/Osmo4.app/Contents/MacOS/MP4Box -force-cat -cat 3.m4v movie.m4v
    • ... and so on ...

Voila! Now I can just play the one file instead of having to either playing each one individually to creating some sort of play list to do so.