BlogFeed-icon-28x28

This blog post will cover how to write JavaScript that is easier to maintain inside of a Ruby on Rails application. The following technique is what I use whenever writing JavaScript for a Rails application now. It uses JavaScript Objects to provide application specific objects and methods.

First assume there exists an application called MyApp and it has requirements defined for managing users and a blog. Also assume the requirement calls for a spinner upon submitting object data to the controllers. To satisfy this requirement assume spin.js has been chosen as the spinner library.

In the past, when I was more of a newbie, I would have gone to each view page that called for a spinner and would have written something as follows...

The wrong way

app/views/users/create.html.erb

<script type='text/javascript'>
  $(document).ready(function() {
    var opts = {
      lines: 13, // The number of lines to draw
      length: 7, // The length of each line
      width: 4, // The line thickness
      radius: 10, // The radius of the inner circle
      corners: 1, // Corner roundness (0..1)
      rotate: 0, // The rotation offset
      color: '#000', // #rgb or #rrggbb
      speed: 1, // Rounds per second
      trail: 60, // Afterglow percentage
      shadow: false, // Whether to render a shadow
      hwaccel: false, // Whether to use hardware acceleration
      className: 'spinner', // The CSS class to assign to the spinner
      zIndex: 2e9, // The z-index (defaults to 2000000000)
      top: 'auto', // Top position relative to parent in px
      left: 'auto' // Left position relative to parent in px
    };
    $('input#submit-button').click(function() {
      var target = document.getElementById('my-spinner');
      var spinner = new Spinner(opts).spin(target);
    });
  });
</script>

app/views/posts/create.html.erb

<script type='text/javascript'>
  $(document).ready(function() {
    var opts = {
      lines: 13, // The number of lines to draw
      length: 7, // The length of each line
      width: 4, // The line thickness
      radius: 10, // The radius of the inner circle
      corners: 1, // Corner roundness (0..1)
      rotate: 0, // The rotation offset
      color: '#000', // #rgb or #rrggbb
      speed: 1, // Rounds per second
      trail: 60, // Afterglow percentage
      shadow: false, // Whether to render a shadow
      hwaccel: false, // Whether to use hardware acceleration
      className: 'spinner', // The CSS class to assign to the spinner
      zIndex: 2e9, // The z-index (defaults to 2000000000)
      top: 'auto', // Top position relative to parent in px
      left: 'auto' // Left position relative to parent in px
    };
    $('input#submit-button').click(function() {
      var target = document.getElementById('my-other-spinner');
      var spinner = new Spinner(opts).spin(target);
    });
  });
</script>

To make this better I use JavaScript objects and dependency injection to reuse this spinner whenever I need it (DRY right?).

The right way

First we'll make a base object in JavaScript that will provide a nice application specific object for our application:

app/assets/javascripts/my_app_base.js

// This is just an object so we have a place to put things
// and only add a single global.
window.MyApp = {};

Now we can make our reuseable spinner object:

app/assets/javascripts/my_app_spinner.js

MyApp.spinner = function(target, event_target, evt){
  var spin_it = function(target) {
    var opts = {
      lines: 13, // The number of lines to draw
      length: 7, // The length of each line
      width: 4, // The line thickness
      radius: 10, // The radius of the inner circle
      corners: 1, // Corner roundness (0..1)
      rotate: 0, // The rotation offset
      color: '#000', // #rgb or #rrggbb
      speed: 1, // Rounds per second
      trail: 60, // Afterglow percentage
      shadow: false, // Whether to render a shadow
      hwaccel: false, // Whether to use hardware acceleration
      className: 'spinner', // The CSS class to assign to the spinner
      zIndex: 2e9, // The z-index (defaults to 2000000000)
      top: 'auto', // Top position relative to parent in px
      left: 'auto' // Left position relative to parent in px
    };

    var spinner = new Spinner(opts);
    spinner.spin(target[0]);
  }

  target = $(target);
  event_target = $(event_target);

  event_target.on(evt, function(e) {
    spin_it(target);
  });
}

And now the view pages are a lot less cluttered with repeated JavaScripts:

app/views/users/create.html.erb

$(document).ready( new MyApp.spinner($('#my-spinner'), $('#submit-button'), 'click') );

app/views/posts/create.html.erb

$(document).ready( new MyApp.spinner($('#my-other-spinner'), $('#submit-button'), 'click') );

Things to note

For this particular instance, notice that we can pass any DOM/jQuery element to the spinner object with an event to bind to that object and the spinner object. These dependencies are chosen (or injected) at runtime. This is one element aiding cleaning up the code by removing the hard-coded dependencies.

About_josh_adams

January 18th, 2013 09:40

Linkdump! To the knowledge:

About_josh_adams

December 21st, 2012 10:21

It's that time again! LINKS!

About_josh_adams

December 14th, 2012 08:09

So I haven't done a linkdump in a really long time. I got sick and then I got lazy. I've overcome the laziness for today. Hurray.

Finally, David Chapman wished us all a Merry Circuitsmas as he was wrapping up his circuits class this semester :) Merry Circuitsmas

If you're getting something like undefined local variable or method 'new_user_session_path' after adding mount Refinery::Core::Engine, :at => '/' to config/routes.rb you can use the following fix:

config/initializers/devise.rb

Devise.setup do |config|
    config.router_name = :main_app
end

Devise will now know to use the main_app routes. End of line.

How to remove the purple from your grub menu, and how to get rid of that purple boot screen on Ubuntu:

Step 1:

sudo vim /lib/plymouth/themes/default.grub

# Change your background to black and white will this
if background_color 0,0,0; then
  clear
fi

Step 2:

sudo vim /etc/default/grub

:%s/quiet splash//g

Note: This changes GRUB_CMDLINE_LINUX_DEFAULT="quiet splash" to GRUB_CMDLINE_LINUX_DEFAULT=""

That is all :-)

About_adam_gamble

October 18th, 2012 15:33

> Team.where(:name => "Testing").where(:conference_id => 1).first
=> nil
> Team.where(:name => "Testing").where(:conference_id => 1).first_or_initialize
=> #<Team id: nil, name: "Testing", conference_id: 1, created_at: nil, updated_at:     nil, logo_image_uid: nil, short_name: nil>
> _.persisted?
=> false
> Team.where(:name => "Testing").where(:conference_id => 1).first_or_create
=> #<Team id: 128, name: "Testing", conference_id: 1, created_at: "2012-10-18 20:26:36", updated_at: "2012-10-18 20:26:36", logo_image_uid: nil, short_name: nil>
> _.persisted?
=> true

I didn't realize this was this elegant in rails, and just stumbled upon it. Maybe you didn't know either?

Enjoy.

@ryanbigg asked me how I set up deployment, figured I'd post this internal guide we've been using.

Derived from this link

The main difference is that this configuration supports rails 3.2.

To set up a server, do the following as root:

echo '--- Install system packages ---'
apt-get update

Let that finish. Then:

apt-get upgrade -y

Let that finish. Then:

apt-get install build-essential ruby-full libmagickcore-dev imagemagick libxml2-dev libxslt1-dev git-core postgresql postgresql-client postgresql-server-dev-8.4 nginx curl node

Let that finish. Then:

apt-get build-dep ruby1.9.1

Install node so you can do asset precompilation later:

sudo apt-get install python-software-properties && sudo add-apt-repository ppa:chris-lea/node.js && sudo apt-get update && sudo apt-get install nodejs nodejs-dev

Then make a deployer user:

useradd -m -g staff -s /bin/bash deployer
passwd deployer

Then fill in the deployer password.

Add this to /etc/sudoers:

%staff ALL=(ALL) ALL

Install system-wide rvm (as root)

bash -s stable < <(curl -s https://raw.github.com/wayneeseguin/rvm/master/binscripts/rvm-installer)

Install ruby

source /etc/profile.d/rvm.sh
rvm install 1.9.2
rvm use 1.9.2@projectname --create
gem install bundler

As your deployer user, add this to your ~/.rvmrc

rvm_trust_rvmrcs_flag=1

Add the following to /etc/environment to always run commands in production on this server.

RAILS_ENV=production

Setup postgres database:

sudo -u postgres createdb projectname_production
sudo -u postgres psql

Then execute the following SQL (use your own password):

CREATE USER projectname_production WITH PASSWORD 'isotope_bang';
GRANT ALL PRIVILEGES ON DATABASE projectname_production TO projectname_production;

Make sure this user can actually log in (by default, only local-system users can). Modify /etc/postgresql/9.1/main/pg_hba.conf and change out the line requiring local users to use ident to md5.

local   all         all                               md5

Restart postgres

/etc/init.d/postgresql restart

Then set up nginx:

# /etc/nginx/sites-available/default
upstream projectname.com {
  # fail_timeout=0 means we always retry an upstream even if it failed
  # to return a good HTTP response (in case the Unicorn master nukes a
  # single worker for timing out).

  # for UNIX domain socket setups:
  server unix:/tmp/projectname.com.socket fail_timeout=0;
}

server {
    # if you're running multiple servers, instead of "default" you should
    # put your main domain name here
    listen 80 default;

    # you could put a list of other domain names this application answers
    server_name projectname.com;

    root /home/deployer/apps/projectname.com/current/public;
    access_log /var/log/nginx/projectname.com_access.log;
    rewrite_log on;

    location / {
        #all requests are sent to the UNIX socket
        proxy_pass  http://projectname.com;
        proxy_redirect     off;

        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;

        client_max_body_size       10m;
        client_body_buffer_size    128k;

        proxy_connect_timeout      90;
        proxy_send_timeout         90;
        proxy_read_timeout         90;

        proxy_buffer_size          4k;
        proxy_buffers              4 32k;
        proxy_busy_buffers_size    64k;
        proxy_temp_file_write_size 64k;
    }

    # if the request is for a static resource, nginx should serve it directly
    # and add a far future expires header to it, making the browser
    # cache the resource and navigate faster over the website
    # this probably needs some work with Rails 3.1's asset pipe_line
    location ~ ^/(images|javascripts|stylesheets|system|assets)/  {
      root /home/deployer/apps/projectname.com/current/public;
      expires max;
      break;
    }
}

And then:

# /etc/nginx/nginx.conf 
user deployer staff;

# Change this depending on your hardware
worker_processes 4;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

http {
    types_hash_bucket_size 512;
    types_hash_max_size 2048;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay off;
    # server_tokens off;

    # server_names_hash_bucket_size 64;
    # server_name_in_redirect off;

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/access.log;
    error_log /var/log/nginx/error.log;

    gzip on;
    gzip_disable "msie6";

    # gzip_vary on;
    gzip_proxied any;
    gzip_min_length 500;
    # gzip_comp_level 6;
    # gzip_buffers 16 8k;
    # gzip_http_version 1.1;
    gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Now start nginx (won't work till unicorn's up but go ahead)

/etc/init.d/nginx start

Add unicorn to your project's Gemfile:

# Gemfile
gem "unicorn"

group :development do
  gem "capistrano"
end

Add unicorn config to your project:

# config/unicorn.rb
# Set environment to development unless something else is specified
env = ENV["RAILS_ENV"] || "development"

# See http://unicorn.bogomips.org/Unicorn/Configurator.html for complete
# documentation.
worker_processes 4

# listen on both a Unix domain socket and a TCP port,
# we use a shorter backlog for quicker failover when busy
listen "/tmp/my_site.socket", backlog: 64

# Preload our app for more speed
preload_app true

# nuke workers after 30 seconds instead of 60 seconds (the default)
timeout 30

pid "/tmp/unicorn.my_site.pid"

# Production specific settings
if env == "production"
  # Help ensure your application will always spawn in the symlinked
  # "current" directory that Capistrano sets up.
  working_directory "/home/deployer/apps/my_site/current"

  # feel free to point this anywhere accessible on the filesystem
  user 'deployer', 'staff'
  shared_path = "/home/deployer/apps/my_site/shared"

  stderr_path "#{shared_path}/log/unicorn.stderr.log"
  stdout_path "#{shared_path}/log/unicorn.stdout.log"
end

before_fork do |server, worker|
  # the following is highly recomended for Rails + "preload_app true"
  # as there's no need for the master process to hold a connection
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.connection.disconnect!
  end

  # Before forking, kill the master process that belongs to the .oldbin PID.
  # This enables 0 downtime deploys.
  old_pid = "/tmp/unicorn.my_site.pid.oldbin"
  if File.exists?(old_pid) && server.pid != old_pid
    begin
      Process.kill("QUIT", File.read(old_pid).to_i)
    rescue Errno::ENOENT, Errno::ESRCH
      # someone else did our job for us
    end
  end
end

after_fork do |server, worker|
  # the following is *required* for Rails + "preload_app true",
  if defined?(ActiveRecord::Base)
    ActiveRecord::Base.establish_connection
  end

  # if preload_app is true, then you may also want to check and
  # restart any other shared sockets/descriptors such as Memcached,
  # and Redis.  TokyoCabinet file handles are safe to reuse
  # between any number of forked children (assuming your kernel
  # correctly implements pread()/pwrite() system calls)
end

Capify the project:

capify .

Replace config/deploy.rb with the following:

# config/deploy.rb 
require "bundler/capistrano"

set :application,     "projectname.com"
set :scm,             :git
set :repository,      "git@github.com:isotope11/projectname.com.git"
set :branch,          "origin/master"
set :migrate_target,  :current
set :ssh_options,     { forward_agent: true }
set :rails_env,       "production"
set :deploy_to,       "/home/deployer/apps/projectname.com"
set :normalize_asset_timestamps, false

set :user,            "deployer"
set :group,           "staff"
set :use_sudo,        false

role :web,    "192.168.1.154"
role :app,    "192.168.1.154"
role :db,     "192.168.1.154", primary: true

set(:latest_release)  { fetch(:current_path) }
set(:release_path)    { fetch(:current_path) }
set(:current_release) { fetch(:current_path) }

set(:current_revision)  { capture("cd #{current_path}; git rev-parse --short HEAD").strip }
set(:latest_revision)   { capture("cd #{current_path}; git rev-parse --short HEAD").strip }
set(:previous_revision) { capture("cd #{current_path}; git rev-parse --short HEAD@{1}").strip }

default_environment["RAILS_ENV"] = 'production'

# Use our ruby-1.9.2-p318@my_site gemset
default_environment["PATH"]         = "--"
default_environment["GEM_HOME"]     = "--"
default_environment["GEM_PATH"]     = "--"
default_environment["RUBY_VERSION"] = "ruby-1.9.2-p318"

default_run_options[:shell] = 'bash'

namespace :deploy do
  desc "Deploy your application"
  task :default do
    update
    restart
  end

  desc "Setup your git-based deployment app"
  task :setup, except: { no_release: true } do
    dirs = [deploy_to, shared_path]
    dirs += shared_children.map { |d| File.join(shared_path, d) }
    run "#{try_sudo} mkdir -p #{dirs.join(' ')} && #{try_sudo} chmod g+w #{dirs.join(' ')}"
    run "git clone #{repository} #{current_path}"
  end

  task :cold do
    update
    migrate
  end

  task :update do
    transaction do
      update_code
    end
  end

  desc "Update the deployed code."
  task :update_code, except: { no_release: true } do
    run "cd #{current_path}; git fetch origin; git reset --hard #{branch}"
    finalize_update
  end

  desc "Update the database (overwritten to avoid symlink)"
  task :migrations do
    transaction do
      update_code
    end
    migrate
    restart
  end

  task :finalize_update, except: { no_release: true } do
    run "chmod -R g+w #{latest_release}" if fetch(:group_writable, true)

    # mkdir -p is making sure that the directories are there for some SCM's that don't
    # save empty folders
    run <<-CMD
      rm -rf #{latest_release}/log #{latest_release}/public/system #{latest_release}/tmp/pids &&
      mkdir -p #{latest_release}/public &&
      mkdir -p #{latest_release}/tmp &&
      ln -s #{shared_path}/log #{latest_release}/log &&
      ln -s #{shared_path}/system #{latest_release}/public/system &&
      ln -s #{shared_path}/pids #{latest_release}/tmp/pids &&
      ln -sf #{shared_path}/config/database.yml #{latest_release}/config/database.yml
    CMD

    #precompile the assets
    run "cd #{latest_release}; bundle exec rake assets:precompile"

    if fetch(:normalize_asset_timestamps, true)
      stamp = Time.now.utc.strftime("%Y%m%d%H%M.%S")
      asset_paths = fetch(:public_children, %w(images stylesheets javascripts)).map { |p| "#{latest_release}/public/#{p}" }.join(" ")
      run "find #{asset_paths} -exec touch -t #{stamp} {} ';'; true", env: { "TZ" => "UTC" }
    end
  end

  desc "Zero-downtime restart of Unicorn"
  task :restart, except: { no_release: true } do
    run "kill -s USR2 `cat /tmp/unicorn.my_site.pid`"
  end

  desc "Start unicorn"
  task :start, except: { no_release: true } do
    run "cd #{current_path} ; bundle exec unicorn_rails -c config/unicorn.rb -D"
  end

  desc "Stop unicorn"
  task :stop, except: { no_release: true } do
    run "kill -s QUIT `cat /tmp/unicorn.my_site.pid`"
  end  

  namespace :rollback do
    desc "Moves the repo back to the previous version of HEAD"
    task :repo, except: { no_release: true } do
      set :branch, "HEAD@{1}"
      deploy.default
    end

    desc "Rewrite reflog so HEAD@{1} will continue to point to at the next previous release."
    task :cleanup, except: { no_release: true } do
      run "cd #{current_path}; git reflog delete --rewrite HEAD@{1}; git reflog delete --rewrite HEAD@{1}"
    end

    desc "Rolls back to the previously deployed version."
    task :default do
      rollback.repo
      rollback.cleanup
    end
  end
end

def run_rake(cmd)
  run "cd #{current_path}; #{rake} #{cmd}"
end

Now there is one little thing you'll need to do. I like to run my apps, even on the server, to use their own gemset. This keeps everything clean and isolated. Login to the deployer account and create your gemset. Next run rvm info and fill the PATH, GEM_HOME and GEM_PATH variables accordingly.

Place the database configuration on the server in the shared directory.

# /home/deployer/apps/projectname.com/shared/config/database.yml 
production:
  adapter: postgresql
  encoding: unicode
  database: projectname_production
  pool: 5
  username: projectname_production
  password: password

Modify the server to support password-based SSH logins by editing /etc/ssh/sshd_config and setting:

PasswordAuthentication yes

Then restart ssh:

/etc/init.d/ssh restart

Set up a deploy key for the deployer user on your git repository. As deployer run the following:

ssh-keygen

Then:

cat ~/.ssh/id_rsa.pub

Push that as a deploy key to the project on github.

SSH into github as deployer from the server, to let this box validate that the host is valid to connect to:

ssh git@github.com

Then run a capistrano setup for the project:

cap deploy:setup

You also might have to go onto the server and, as deployer, run:

mkdir ~/apps/projectname.com/shared/pids

Deployments

Whenever you have a new feature developed in a feature branch, this is the process of deploying it:

Merge feature_branch into master
Run your tests to make sure everything is dandy.
Push master
Run `cap deploy`

This post isn't really programming related, but it will save a lot of people some grief, and it is computer related. If you've had any problems with YouTube videos looking like the image below, then there are a couple of things you can do to fix this.

Before Fix

First you can simply sign up for the YouTube HTML5 Video Player, but this doesn't solve the problem for the videos that fall back to the Adobe Flash player when they lack compatibility for the new HTML5 player.

To fix the videos that play in flash, right click on a YouTube video you're currently watching and go to "Settings...". Next you'll want to un-check "Enable hardware acceleration"

Settings

Reload the page, and vuala!

After Fix

So on a project we were working on, we had something that looked / acted an awful lot like a normal ActiveRecord association. However, it was a custom method we wrote that returned an ActiveRecord::Relation. This worked fantastically, for everything except for a moderately deep query involving this 'relation.'

The code for the relation looked like this:

# app/models/person.rb
class Person < ActiveRecord::Base
  has_many :person_school_links

  def person_school_links(status = :status_active)
    PersonSchoolLink.where(person_id: self.id).send(status)
  end
end

This let us do @person.person_school_links.more_chained_methods just fine, without causing any grief. We got a few months into this project without trouble.

Then, we were building a report that went across this 'relation.' The report had a query that looked like:

RewardDelivery.includes(to: [ :person_school_links ]).where(to: { person_school_links: { school_id: 1 } })

This query worked fine if there were no matching RewardDeliveries. However, any time it had actual data to return, Rails would throw

undefined method `target' for #<ActiveRecord::Relation:0x000000079e0fa0>

It took quite a bit of digging into the Rails stack to figure out what was happening, but essentially the methods generated by a has_many, etc. relationship in ActiveRecord::Base are not returning ActiveRecord::Relations, but rather something derived from ActiveRecord::Reflection::MacroReflection. Ultimately, when the ActiveRecord::Relation from the above query was coerced into something Array-like, Rails looks for a #target method on the reflection. Since we were returning an ActiveRecord::Relation, and since those don't actually have a #target method defined, it was blowing up.

To get around this and still get our preferred behaviour, I ended up writing a class I called MacroReflectionRelationFacade. I essentially use SimpleDelegator to wrap the ActiveRecord::Relation and add an acceptable #target method to it. Code follows, thought the story was interesting:

# lib/macro_reflection_relation_facade.rb
class MacroReflectionRelationFacade < SimpleDelegator
  def target
    self
  end
end

# app/models/person.rb
require 'macro_reflection_relation_facade'

class Person < ActiveRecord::Base
  has_many :person_school_links

  # Relationships
  def person_school_links(status = :status_active)
    MacroReflectionRelationFacade.new(PersonSchoolLink.where(person_id: self.id).send(status))
  end
end

I'm unsure if this is a perfect solution, but it works perfectly for all my purposes. Enjoy :)