[SUMMARY] Ruby Jobs Site (#47)

R

Ruby Quiz

Naturally I always hope that the Ruby Quizzes are timely, but this one was maybe
too much so. One day before I released the quiz, the Ruby jobs site went live.
That probably knocked a lot of excitement out of the problem. Oh well. We can
at least look over my solution.

I built a minimal site using Rails. It didn't take too long really, though I
fiddled with a few things for a while, being picky. As a testament to the power
or Rails, I wrote very little code. I used the code generators to create the
three pieces I needed: logins, jobs, and a mailer. Then I just tweaked the
code to tie it all together.

The Rails code is spread out over the whole system, so I'm not going to recreate
it all here. You can download it, if you want to see it all or play with the
site.

Rails is an MVC framework, so the code has three layers. The model layer is
mainly defined in SQL with Rails, so here's that file:

DROP TABLE IF EXISTS people;
CREATE TABLE people (
id INT NOT NULL auto_increment,
full_name VARCHAR(100) NOT NULL,
email VARCHAR(100) NOT NULL,
password CHAR(40) NOT NULL,
confirmation CHAR(6) DEFAULT NULL,
created_on DATE NOT NULL,
updated_on DATE NOT NULL,
PRIMARY KEY(id)
);

DROP TABLE IF EXISTS jobs;
CREATE TABLE jobs (
id INT NOT NULL auto_increment,
person_id INT NOT NULL,
company VARCHAR(100) NOT NULL,
country VARCHAR(100) NOT NULL,
state VARCHAR(100) NOT NULL,
city VARCHAR(100) NOT NULL,
pay VARCHAR(50) NOT NULL,
terms ENUM( 'contract',
'hourly',
'salaried' ) NOT NULL,
on_site ENUM( 'none',
'some',
'all' ) NOT NULL,
hours VARCHAR(50) NOT NULL,
travel VARCHAR(50) NOT NULL,
description TEXT NOT NULL,
required_skills TEXT NOT NULL,
desired_skills TEXT,
how_to_apply TEXT NOT NULL,
created_on DATE NOT NULL,
updated_on DATE NOT NULL,
PRIMARY KEY(id)
);

I wrote that for MySQL, but it's pretty simple SQL and I assume it would work
with few changes in most databases. The id fields are the unique identifiers
Rails likes, created_on and updated_on are date fields Rails can maintain for
you, and the rest is the actual data of my application.

Wrapping ActiveRecord around the jobs table was trivial:

class Job < ActiveRecord::Base
belongs_to :person

ON_SITE_CHOICES = %w{none some all}
TERMS_CHOICES = %w{contract hourly salaried}
STATE_CHOICES = %w{ Alabama Alaska Arizona Arkansas California
Colorado Connecticut Delaware Florida Georgia
Hawaii Idaho Illinois Indiana Iowa Kansas Kentucky
Louisiana Maine Maryland Massachusetts Michigan
Minnesota Mississippi Missouri Montana Nebraska
Nevada New\ Hampshire New\ Jersey New\ Mexico
New\ York North\ Carolina North\ Dakota Ohio
Oklahoma Oregon Pennsylvania Rhode\ Island
South\ Carolina South\ Dakota Tennessee Texas Utah
Vermont Virginia Washington West\ Virginia
Wisconsin Wyoming Other }

validates_inclusion_of :eek:n_site, :in => ON_SITE_CHOICES

validates_inclusion_of :terms, :in => TERMS_CHOICES

validates_presence_of :company, :eek:n_site, :terms,
:country, :state, :city,
:pay, :hours, :description, :required_skills,
:how_to_apply, :person_id

def location
"#{city}, #{state} (#{country})"
end
end

Most of that is just some constants I use to build menus later in the view. You
can see my basic validations in there as well. I also defined my own attribute
of location() which is just a combination of city, state, and country.

Wrapping people wasn't much different. I used the login generator to create
them, but renamed User to Person. That seemed to fit better with my idea of
building a site to collection information on Ruby people, jobs, groups, and
events. I did away with the concept of a login name in favor of email addresses
as a unique identifier. I also added an email confirmation to the login system,
so I'll show that here:

class Person < ActiveRecord::Base
# ...

def self.authenticate( email, password, confirmation )
person = find_first( [ "email = ? AND password = ?",
email, sha1(password) ] )
return nil if person.nil?
unless person.confirmation.blank?
if confirmation == person.confirmation
person.confirmation = nil
person.save or raise "Unable to remove confirmation."
person
else
false
end
end
end

protected

# ...

before_create :generate_confirmation

def generate_confirmation
code_chars = ("A".."Z").to_a + ("a".."z").to_a + (0..9).to_a
code = Array.new(6) { code_chars[rand(code_chars.size)] }.join
write_attribute "confirmation", code
end
end

You can see at the bottom that I added a filter to add random confirmation codes
to new people. I enhanced authenticate() to later verify the code and remove
it, showing a trusted email address. An ActionMailer instance (not shown) sent
the code to the person and the login form (not shown) was changed to read it on
the first login.

I made other changes to the login system. I had it store just the Person.id()
in the session, instead of the whole Person. I also added a login_optional()
filter, that uses information when available, but doesn't require it. All of
these were trivial to implement and are not shown here.

The controller layer is hardly worth talking about. The scaffold generator
truly gave me most of what I needed in this simple case. I added the login
filters and modified create() to handle my unusual form that allows you to menu
select a state in the U.S., or enter your own. Here's a peak at those changes:

class JobController < ApplicationController
before_filter :login_required, :except => [:index, :list, :show]
before_filter :login_optional, :eek:nly => [:show]

# ...

def create
@job = Job.new(params[:job])
@job.person_id = @person.id
@job.state = params[:eek:ther_state] if @job.state == "Other"
if @job.save
flash[:notice] = "Job was successfully created."
redirect_to :action => "list"
else
render :action => "new"
end
end

# ...
end

All very basic, as you can see. If the state() attribute of the job was set to
"Other", I just swap it out for the text field.

My views were also mostly just cleaned up versions of the stuff Rails generated
for me. Here's a peak at the job list view:

<h2>Listing jobs</h2>

<% if @jobs.nil? or @jobs.empty? -%>
<p>No jobs listed, currently. Check back soon.</p>
<% else -%>
<% @jobs.each do |job| -%>
<dl>
<dt>Posted:</dt>
<dd><%= job.created_on.strftime "%B %d, %Y" %></dd>

<dt>Company:</dt>
<dd><%= link_to h(job.company), :action => :show, :id => job %> in
<%= h job.location %></dd>

<dt>Description:</dt>
<dd><%= excerpt(job.description, job) %></dd>
</dl>
<% end -%>
<% end -%>

<%= pagination_links @job_pages -%>

<br />

<%= link_to "List your job", :action => "new" %>

This is a basic job listing, with pagination. What this page really needs that
I didn't add is some tools to control the sorting and filtering of jobs. This
would be great for looking at jobs just in your area. The above code relies on
a helper method called excerpt():

module JobHelper
def excerpt( textile, id )
html = sanitize(textilize(textile))
html.sub!(/<p>(.*?)<\/p>(.*)\Z/m) { $1.strip }
if $2 =~ /\S/
"#{html} #{link_to '...', :action => :show, :id => id}"
else
html
end
end
end

I used Redcloth to markup all the job description and skill fields. This method
allows me to grab just the first paragraph of the description, to use in the job
list view. It adds a "..." link, if content was trimmed.

Finally, I'll share one last trick. Using Rails generators and then adding the
files to Subversion can be tedious. Because of that, I added an action to the
Rakefile to do it for me:

### James's added tasks ###

desc "Add generated files to Subversion"
task :add_to_svn do
sh %Q{svn status | ruby -nae 'puts $F[1] if $F[0] == "?"' | } +
%Q{xargs svn add}
end

That's just a simple nicety, but I sure like it. Saves me a lot of hassle.
Just make sure you set Subversion properties to ignore files you don't want
automatically added to the repository.

Tomorrow's Ruby Quiz is Gavin Kistner's third topic, this time on captchas...
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,581
Members
45,055
Latest member
SlimSparkKetoACVReview

Latest Threads

Top