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:
atabase.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'],
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,
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>