Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

[QUIZ] Ruby Jobs Site (#47)

0 views
Skip to first unread message

Ruby Quiz

unread,
Sep 16, 2005, 8:39:39 AM9/16/05
to
The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

It's been proven now that you can develop functional web applications with very
little time, using the right tools:

http://www.rubyonrails.com/media/video/rails_take2_with_sound.mov

I guess that puts web applications in the valid Ruby Quiz category, so let's
tackle one using your framework of choice: CGI, WEBrick servlets, Rails, or
whatever.

When I first came to Ruby, even just a year ago, I really doubt the community
was ready to support a Ruby jobs focused web site. Now though, times have
changed. I'm seeing more and more posting about Ruby jobs scattered among the
various Ruby sites. Rails has obviously played no small part in this and the
biggest source of jobs postings is probably the Rails weblog, but there have
been other Ruby jobs offered recently as well.

Wouldn't it be nice if we had a centralized site we could go to and scan these
listings for our interests?

This week's Ruby Quiz is to create a web application that allows visitors to
post jobs for other visitors to find. Our focus will be on functionality at
this point, so don't waste too much energy making the site beautiful. (That can
be done after you decide this was a brilliant idea and you're really going to
launch your site!)

What should a jobs site record for each position? I'm not going to set any hard
and fast rules on this. The answer is simply: Whatever you think we should
enter. If you need more ideas though, browse job listings in your local paper
or check out a site like:

http://jobs.perl.org/


James Edward Gray II

unread,
Sep 18, 2005, 3:49:34 PM9/18/05
to
What follows is a basic solution, using Rails.

The Code
--------

You can download the code from:

http://rubyquiz.com/jobsite.zip

Requirements
------------

If you try to play around with the site, you will need to have
Redcloth installed. You will also need to edit the ActionMailer
settings in development.rb to play with the login system.

The SQL for the database is included in the db folder.

How was it made?
----------------

I used the login generator to build the user portion of the code, the
scaffold generator to get me started with the jobs portion of the
code, and the mailer generator to prepare the confirmation email. Of
course, I modified each of those. I added a confirmation email to
the login system, a login_optional filter to support editing job
posts, and modified the system to store only the id (not the whole
user) in the session object. My changes to the other two components
were more cosmetic in nature, mainly.

What could use improving?
-------------------------

Obviously, the stylesheet could use some serious attention. I did
try to get the basics in there, but the site has a long way to go to
become pretty.

The other big need, in my opinion, is some tools for controlling the
sorting and filtering of listed positions. In this regard, I've
actually considered making fields like Pay and Hours into menu
selections so I could count on their formats and allow users to sort/
filter based on them, in addition to the other fields.

Other Ideas
-----------

I named my site Ruby Dossiers, with a bit of a grand vision in mind.

Obviously, this quiz focuses on the job aspect of the site, but I
could see adding more to it in the future. I would like to reverse
the job finding aspect for starters, and allow users to post skill
profiles for employers to browse. Another idea I had was to roll in
local group meet-up management.

Of course, all this brainstorming was before there was a Ruby jobs
site. Perhaps the ideas will at least prove useful though.

James Edward Gray II

Paul

unread,
Sep 21, 2005, 2:32:38 PM9/21/05
to
What follows is another basic solution using ActiveRecord only and CGI.
There is minimal CSS styling, but you could add some very easily.

What is interesting about this solution is that it's very small (a
single file and a single table), does not need users and sets itself
up. The solution uses sqlite3 (via gems) and checks if the database
file exists as part of the startup. If it doesn't exist, it creates the
file and the table that it needs.

Listing, searching, viewing details, creating new posts and closing
posts are supported. Each new post generates a 'secret' that the person
posting can then use to close the post with latter, such that users are
not required. Posts can also be 'administratively' closed.

Additionally, although this solution is a single file, all the
interfaces are templated using ERB. Each template is a separate entry
after the __END__ marker, with the first non-whitespace line being the
name and all lines after until the separater line as the file contents.
DRY principles are also in place as the header/footer are seperate
templates and included into each page rather then being repeated.

----------------------
Copy the following and paste it into a .cgi file. It has been tested
with lighttpd and apache.
----------------------

#!/usr/bin/env ruby

## Proposed solution to http://www.rubyquiz.org/quiz047.html
## Written by Paul Vaillant (paul.v...@gmail.com)
## Permission granted to do whatever you'd like with this code

require 'digest/md5'
require 'cgi'
require 'erb'

## gems are required for sqlite3 and active_record
require 'rubygems'
require 'sqlite3'
require 'active_record'

## Check if the database exists; create it and the table we need if it
doesn't
DB_FILE = "/tmp/jobs.db"
unless File.readable?(DB_FILE)
table_def = <<-EOD
CREATE TABLE postings (
id INTEGER PRIMARY KEY AUTOINCREMENT,
posted INTEGER,
title VARCHAR(255),
company VARCHAR(255),
location VARCHAR(255),
length VARCHAR(255),
contact VARCHAR(255),
travel INTEGER(2), -- 0%, 0-25%, 25-50%, 50-75%, 75-100%
onsite INTEGER(1),
description TEXT,
requirements TEXT,
terms INTEGER(2), -- C(hourly), C(project), E(hourly), E(pt),
E(ft)
hours VARCHAR(255),
secret VARCHAR(255) UNIQUE,
closed INTEGER(1) DEFAULT 0
);
EOD
db = SQLite3::Database.new(DB_FILE)
db.execute(table_def)
db.close
end

## Setup ActiveRecord database connection and the one ORM class we need
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile
=> DB_FILE)
class Posting < ActiveRecord::Base
TRAVEL = ['0%','0-25%','25-50%','50-75%','75-100%']
TERMS = ['Contract(hourly)','Contract(project)','Employee(hourly)',
'Employee(part-time)','Employee(full-time)']
end

class Actions
ADMIN_SECRET = 's3cr3t'
@@templates = nil
def self.template(t)
unless @@templates
@@templates = Hash.new
name = nil
data = ''
DATA.each_line {|l|
if name.nil?
name = l.strip
elsif l.strip == '-=-=-=-=-'
@@templates[name] = data if name
name = nil
data = ''
else
data << l.strip << "\n"
end unless l =~ /^\s*$/
}
@@templates[name] = data if name
end
return @@templates[t]
end

def self.dispatch()
cgi = CGI.new
begin
## map path_info to the method that handles it (ie controller)
## ex. no path_info (/jobs.cgi) goes to 'index'
## /search (/jobs.cgi/search) goes to 'search'
## /create/save (/jobs.cgi/create/save) goes to 'create__save'
action = if cgi.path_info
a = cgi.path_info[1,cgi.path_info.length-1].gsub(/\//,'__')
(a && a != '' ? a : 'index')
else
"index"
end
a = Actions.new(cgi)
m = a.method(action.to_sym)
if m && m.arity == 0
resbody = m.call()
else
raise "Failed to locate valid handler for [#{action}]"
end
rescue Exception => e
puts cgi.header('text/plain')
puts "EXCEPTION: #{e.message}"
puts e.backtrace.join("\n")
else
puts cgi.header()
puts resbody
end
end

attr_reader :cgi
def initialize(cgi)
@cgi = cgi
end

def index
@postings = Posting.find(:all, :conditions => ['closed = 0'], :order
=> 'posted desc', :limit => 10)
render('index')
end

def search
q = '%' << (cgi['q'] || '') << '%'
conds = ['closed = 0 AND (description like ? OR requirements like ?
OR title like ?)', q, q, q]
@postings = Posting.find(:all, :conditions => conds, :order =>
'posted desc')
render('index')
end

def view
id = cgi['id'].to_i
@post = Posting.find(id)
render('view')
end

def create
if cgi['save'] && cgi['save'] != ''
post = Posting.new
post.posted = Time.now().to_i
['title','company','location','length','contact',
'description','requirements','hours'].each {|f|
post[f] = cgi[f]
}
['travel','onsite','terms'].each {|f|
post[f] = cgi[f].to_i
}
post.secret =
Digest::MD5.hexdigest([rand(),Time.now.to_i,$$].join("|"))
post.closed = 0
if post.save
@post = post
end
end
render('create')
end

def close
## match secret OR id+ADMIN_SECRET

secret = cgi['secret']
if secret =~ /^(\d+)\+(.+)$/
id,admin_secret = secret.split(/\+/)
post = Posting.find(id.to_i) if admin_secret == ADMIN_SECRET
else
post = Posting.find(:first, :conditions => ['secret = ?', secret])
end

if post
post.closed = 1
post.save
@post = post
else
@error = "Failed to match given secret to your post"
end

render('close')
end

## helper methods
def link_to(name, url_frag)
return "<a href=\"#{ENV['SCRIPT_NAME']}/#{url_frag}\">#{name}</a>"
end

def form_tag(url_frag, meth="POST")
return "<form method=\"#{meth}\"
action=\"#{ENV['SCRIPT_NAME']}/#{url_frag}\">"
end

def select(name, options, selected=nil)
sel = "<select name=\"#{name}\">"
options.each_with_index {|o,i|
sel << "<option value=\"#{i}\" #{(i == selected ? "selected=\"1\"" :
'')}>#{o}</option>"
}
sel << "</select>"
return sel
end

def radio_yn(name,val=1)
val ||= 1
radio = "Yes <input type=\"radio\" name=\"#{name}\" value=\"1\"
#{(val == 1 ? "checked=\"checked\"": '')}/> / "
radio << "No <input type=\"radio\" name=\"#{name}\" value=\"0\"
#{(val == 0 ? "checked=\"checked\"" : '')} />"
return radio
end

def textfield(name,val)
return "<input type=\"text\" name=\"#{name}\" value=\"#{val}\" />"
end

def textarea(name,val)
return "<textarea name=\"#{name}\" rows=\"7\" cols=\"60\">" <<
CGI.escapeHTML(val || '') << "</textarea>"
end

def render(name)
return ERB.new(Actions.template(name),nil,'%<>').result(binding)
end
end

Actions.dispatch

__END__
index
<%= render('header') %>

<h1>Postings</h1>
<% if @postings.empty? %>
<p>Sorry, no job postings at this time.</p>
<% else %>
<% for post in @postings %>
<p><%= link_to post.title, "view?id=#{post.id}" %>, <%= post.company
%><br />
<%= post.location %> (<%= Time.at(0).strftime('%Y-%m-%d') %>)</p>
<% end %>
</table>
<% end %>

<%= render('footer') %>

-=-=-=-=-
create
<%= render('header') %>

<h1>Create new Post</h1>
<% if @post %>
<p>Your post has been successfully added. Please note the following
information, as you will need it
to close you post once it has been filled; <br /><br />
Close code: <%= @post.secret %></p>
<p>Thank you</p>
<% else %>
<% if @error %><p class="error">ERROR: <%= @error %></p><% end %>
<%= form_tag "create" %>
<label for="title">Title</label> <%= textfield "title", cgi['title']
%><br />
<label for="company">Company</label> <%= textfield "company",
cgi['company'] %><br />
<label for="location">Location</label> <%= textfield "location",
cgi['location'] %><br />
<label for="length">Length</label> <%= textfield "length",
cgi['length'] %><br />
<label for="contact">Contact</label> <%= textfield "contact",
cgi['contact'] %><br />
<label for="travel">Travel</label> <%= select 'travel',
Posting::TRAVEL, cgi['travel'] %><br />
<label for="onsite">Onsite</label> <%= radio_yn "onsite", cgi['onsite']
%><br />
<label for="description">Description</label> <%= textarea
"description", cgi['description'] %><br />
<label for="requirements">Requirements</label> <%= textarea
"requirements", cgi['requirements'] %><br />
<label for="terms">Employment Terms</label> <%= select 'terms',
Posting::TERMS, cgi['terms'] %><br />
<label for="hours">Hours</label> <%= textfield "hours", cgi['hours']
%><br />
<input type="submit" name="save" value="create" />
</form>
<% end %>

<%= render('footer') %>

-=-=-=-=-
view
<%= render('header') %>

<% if @post %>
<h1><%= @post.title %></h1>
<table>
<tr><td>Posted</td><td><%=
Time.at(@post.posted.to_i).strftime('%Y-%m-%d') %></td></tr>
<tr><td>Company</td><td><%= @post.company %></td></tr>
<tr><td>Length of employment</td><td><%= @post.length %></td></tr>
<tr><td>Contact info</td><td><%= @post.contact %></td></tr>
<tr><td>Travel</td><td><%= Posting::TRAVEL[@post.travel] %></td></tr>
<tr><td>Onsite</td><td><%= ['No','Yes'][@post.onsite] %></td></tr>
<tr><td>Description</td><td><%=
CGI.escapeHTML(@post.description).gsub(/\n/,"<br />\n") %></td></tr>
<tr><td>Requirements</td><td><%=
CGI.escapeHTML(@post.requirements).gsub(/\n/,"<br />\n") %></td></tr>
<tr><td>Employment terms</td><td><%= Posting::TERMS[@post.terms]
%></td></tr>
<tr><td>Hours</td><td><%= @post.hours %></td></tr>
</table>
<% else %>
<p>ERROR: failed to load given post.</p>
<% end %>

<%= render('footer') %>

-=-=-=-=-
close
<%= render('header') %>

<h1>Close Post</h1>
<% if @post %>
<p>Successfully closed post '<%= @post.title %>' by <%= @post.company
%>.</p>
<% elsif @error %>
<p>ERROR: <%= @error %></p>
<% else %>
<p>ERROR: post not successfully closed, no further description of
error.</p>
<% end %>

<%= render('footer') %>

-=-=-=-=-
header
<html>
<head>
<title>Simple Job Site</title>
<style>
form { display: inline; }
</style>
</head>
<body>
<%= link_to "Home", "index" %> |
<%= link_to "Create new Post", "create" %> |
<%= form_tag "close" %>
<input name="secret" type="text" size="16" />
<input type="submit" value="close" />
</form> |
<%= form_tag "search" %>
<input name="q" type="text" size="15" /> <input type="submit"
value="search" />
</form><br />

-=-=-=-=-
footer
</body>
</html>

James Edward Gray II

unread,
Sep 21, 2005, 2:53:30 PM9/21/05
to
On Sep 21, 2005, at 1:36 PM, Paul wrote:

> What follows is another basic solution using ActiveRecord only and
> CGI.
> There is minimal CSS styling, but you could add some very easily.
>
> What is interesting about this solution is that it's very small (a
> single file and a single table), does not need users and sets itself
> up. The solution uses sqlite3 (via gems) and checks if the database
> file exists as part of the startup. If it doesn't exist, it creates
> the
> file and the table that it needs.
>
> Listing, searching, viewing details, creating new posts and closing
> posts are supported. Each new post generates a 'secret' that the
> person
> posting can then use to close the post with latter, such that users
> are
> not required. Posts can also be 'administratively' closed.
>
> Additionally, although this solution is a single file, all the
> interfaces are templated using ERB. Each template is a separate entry
> after the __END__ marker, with the first non-whitespace line being the
> name and all lines after until the separater line as the file
> contents.
> DRY principles are also in place as the header/footer are seperate
> templates and included into each page rather then being repeated.

Hey, this is great stuff! Glad to see that someone else had a chance
to play around with this one.

Don't feel bad when the summary doesn't mention it tomorrow. My
schedule forced me to write it earlier today. :(

Thanks for sharing the solution.

James Edward Gray II

Paul

unread,
Sep 21, 2005, 3:02:38 PM9/21/05
to
/me is sad now...

I understand about schedules though; same reason my solution comes so
late. Hopefully I'll have more time for next weeks.

Ezra Zygmuntowicz

unread,
Sep 21, 2005, 7:56:04 PM9/21/05
to

On Sep 21, 2005, at 11:36 AM, Paul wrote:

> What follows is another basic solution using ActiveRecord only and
> CGI.
> There is minimal CSS styling, but you could add some very easily.

Paul-
Wow cool! Great use of ActiveRecord and cgi all in one file. I
love it!

Thanks
-Ezra


Ruby Quiz

unread,
Sep 22, 2005, 9:01:16 AM9/22/05
to
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 :on_site, :in => ON_SITE_CHOICES

validates_inclusion_of :terms, :in => TERMS_CHOICES

validates_presence_of :company, :on_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, :only => [:show]

# ...

def create
@job = Job.new(params[:job])
@job.person_id = @person.id
@job.state = params[:other_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...


0 new messages