[QUIZ] Ruby Jobs Site (#47)

R

Ruby Quiz

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/
 
J

James Edward Gray II

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
 
P

Paul

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 ([email protected])
## 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'], :eek:rder
=> '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, :eek:rder =>
'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>
 
J

James Edward Gray II

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
 
P

Paul

/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.
 
E

Ezra Zygmuntowicz

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
 

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

No members online now.

Forum statistics

Threads
473,776
Messages
2,569,603
Members
45,197
Latest member
ScottChare

Latest Threads

Top