I recently presented biking.michael-simons.eu, a Sinatra based application written in Ruby.
Its main purpose for me is to keep track of my milage in 2009.
Although the application is completely self containing it has some nice features:
Simple setup
The application is simple and i didn’t want to use a “big” database like PostgreSQL or MySQL, therefore i choose SQLite. Together with DataMapper i can use the following and have a running SQLite database in no time:
configure :development do
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/biking.dev.sqlite3")
end
# A milage is stored for a bike at the beginning of the month.
# Its the milage of the bike at this point of time.
class Milage
include DataMapper::Resource
property :id, Integer, :serial => true
property :when, Date, :nullable => false
property :value, BigDecimal, :nullable => false, :precision => 8, :scale => 2
property :created_at, DateTime
belongs_to :bike
validates_is_unique :when, :scope => :bike
is :list, :scope => [:bike_id]
end
DataMapper.auto_upgrade! |
configure :development do
DataMapper.setup(:default, "sqlite3://#{Dir.pwd}/biking.dev.sqlite3")
end
# A milage is stored for a bike at the beginning of the month.
# Its the milage of the bike at this point of time.
class Milage
include DataMapper::Resource
property :id, Integer, :serial => true
property :when, Date, :nullable => false
property :value, BigDecimal, :nullable => false, :precision => 8, :scale => 2
property :created_at, DateTime
belongs_to :bike
validates_is_unique :when, :scope => :bike
is :list, :scope => [:bike_id]
end
DataMapper.auto_upgrade!
Creating charts with the google_chart gem
Google has a nice api for creating charts, the Google Charts API. The charts are created through URL parameters and as we know, most browsers limits the length of a query string. And for that, the parameters must be encoded.
Nobody wants to do it themselves so there is gchartrb that i use:
# Create the google chart
# All will include all float values
all = Array.new
@gc = GoogleChart::BarChart.new('800x375', nil, :vertical, false) do |bc|
@bikes.each do |b|
# Help is an array of floats for all periods
hlp = b.milage_report.collect{|mr| mr[:milage]}
# and gets added to the chart for that bike
bc.data b.name, hlp, b.color
all += hlp
end
hlp = Array.new
# Get all periods. It sure can be done via a group by but
# the app should be database agnostiv
periods = Milage.all.collect{|m| m.when.strftime '%m.%Y'}.uniq
# Get all other trips in this period and also add them to the chart
periods.each {|period| hlp <<= (AssortedTrip.sum(:distance, :conditions => ['strftime("%m.%Y", "when") = ?', period]) || 0.0)}
all += hlp
bc.data "Assorted Trips", hlp[0,hlp.length-1], "003366"
# Define the labels
bc.axis :x, :labels => periods, :font_size => 16, :alignment => :center
# and the range from 0 to the highest value
bc.axis :y, :range => [0,all.max], :font_size => 16, :alignment => :center
bc.show_legend = true
bc.grid :x_step => 0, :y_step => (100.0/all.max)*25.0, :length_segment => 1, :length_blank => 0 if all.size > 0
end if @bikes.size > 0 |
# Create the google chart
# All will include all float values
all = Array.new
@gc = GoogleChart::BarChart.new('800x375', nil, :vertical, false) do |bc|
@bikes.each do |b|
# Help is an array of floats for all periods
hlp = b.milage_report.collect{|mr| mr[:milage]}
# and gets added to the chart for that bike
bc.data b.name, hlp, b.color
all += hlp
end
hlp = Array.new
# Get all periods. It sure can be done via a group by but
# the app should be database agnostiv
periods = Milage.all.collect{|m| m.when.strftime '%m.%Y'}.uniq
# Get all other trips in this period and also add them to the chart
periods.each {|period| hlp <<= (AssortedTrip.sum(:distance, :conditions => ['strftime("%m.%Y", "when") = ?', period]) || 0.0)}
all += hlp
bc.data "Assorted Trips", hlp[0,hlp.length-1], "003366"
# Define the labels
bc.axis :x, :labels => periods, :font_size => 16, :alignment => :center
# and the range from 0 to the highest value
bc.axis :y, :range => [0,all.max], :font_size => 16, :alignment => :center
bc.show_legend = true
bc.grid :x_step => 0, :y_step => (100.0/all.max)*25.0, :length_segment => 1, :length_blank => 0 if all.size > 0
end if @bikes.size > 0
The gc object will be used as simple as
%img{:src => @gc.to_url, :alt => 'gc'} |
%img{:src => @gc.to_url, :alt => 'gc'}
Using libxml for parsing RSS feeds
It seems that i’m not going to write an application without including my daily faces project dailyfratze.de in some way.
So i decided not only to present numbers but also my biking pictures. The images are available through a Media RSS feed. The feed itself contains pointers to other pages of that very same feed.
One of the fastest ways to parse XML is LibXML and luckily, it’s available for ruby through LibXML Ruby.
Together with memcached it can be used to efficiently handle feeds like so:
def BikingPicture.random_url
# Check if pictures are cached...
biking_pictures = @@cache['biking_pictures']
unless biking_pictures then
# Start retrieving the feed
url = @@uri.parse("http://dailyfratze.de/michael/tags/Thema/Radtour?format=rss&dir=d")
next_page = false
Net::HTTP.new(url.host).start do |http|
# Its a media rss feed that defines previous and next feeds
while url
req = Net::HTTP::Get.new("#{url.path}?#{url.query}")
xml = http.request(req).body
# Parse the data
doc = LibXML::XML::Parser.io(StringIO.new(xml)).parse
# Unless this url hasn't been retrieved...
unless @@cache[url.to_s]
doc.find('/rss/channel/item').each do |item|
# Get all pictures and store them if not already grapped
biking_picture = BikingPicture.first :url => item['url']
unless biking_picture
biking_picture = BikingPicture.new({:url => item.find_first('media:thumbnail')['url'], :link => item.find_first('link').content })
biking_picture.save
end
end
# Mark this url as seen
@@cache[url.to_s] = next_page
next_page = true
end
# Check if there are more feeds...
next_url = doc.find_first("/rss/channel/atom:link[@rel = 'next']")
url = if next_url
@@uri.parse next_url['href']
else
nil
end
end
end
# Get the data...
biking_pictures = BikingPicture.all()
# ...and store it
@@cache.set 'biking_pictures', biking_pictures, 3600
end
biking_pictures.sort_by{rand}[0]
end |
def BikingPicture.random_url
# Check if pictures are cached...
biking_pictures = @@cache['biking_pictures']
unless biking_pictures then
# Start retrieving the feed
url = @@uri.parse("http://dailyfratze.de/michael/tags/Thema/Radtour?format=rss&dir=d")
next_page = false
Net::HTTP.new(url.host).start do |http|
# Its a media rss feed that defines previous and next feeds
while url
req = Net::HTTP::Get.new("#{url.path}?#{url.query}")
xml = http.request(req).body
# Parse the data
doc = LibXML::XML::Parser.io(StringIO.new(xml)).parse
# Unless this url hasn't been retrieved...
unless @@cache[url.to_s]
doc.find('/rss/channel/item').each do |item|
# Get all pictures and store them if not already grapped
biking_picture = BikingPicture.first :url => item['url']
unless biking_picture
biking_picture = BikingPicture.new({:url => item.find_first('media:thumbnail')['url'], :link => item.find_first('link').content })
biking_picture.save
end
end
# Mark this url as seen
@@cache[url.to_s] = next_page
next_page = true
end
# Check if there are more feeds...
next_url = doc.find_first("/rss/channel/atom:link[@rel = 'next']")
url = if next_url
@@uri.parse next_url['href']
else
nil
end
end
end
# Get the data...
biking_pictures = BikingPicture.all()
# ...and store it
@@cache.set 'biking_pictures', biking_pictures, 3600
end
biking_pictures.sort_by{rand}[0]
end
Sinatra, Passenger and Memcached
Speaking of being efficient, the application certainly runs through Phusion Passenger aka modrails.
Running a Sinatra app under passenger is as simple as this: Create a directory for a new vhost, setup a structure like
biking
\- public
- tmp
- config.ru
- biking.rb
and let config.ru contain the following:
root_dir = File.dirname(__FILE__)
require 'biking.rb'
set :environment, ENV['RACK_ENV'].to_sym
set :root, root_dir
set :app_file, File.join(root_dir, 'biking.rb')
disable :run
run Sinatra::Application |
root_dir = File.dirname(__FILE__)
require 'biking.rb'
set :environment, ENV['RACK_ENV'].to_sym
set :root, root_dir
set :app_file, File.join(root_dir, 'biking.rb')
disable :run
run Sinatra::Application
The apache vhost is configured like so:
<VirtualHost *>
DocumentRoot "/path/to/the/applications/public/folder"
RackBaseURI /
</VirtualHost> |
<VirtualHost *>
DocumentRoot "/path/to/the/applications/public/folder"
RackBaseURI /
</VirtualHost>
I have turned off the Rack and Rails Autodetect features (RailsAutoDetect off, RackAutoDetect off) so i need to explicitly turn them on for a vhost.
After that, the application is running and thats all there is.
But wait. I’ve written before about problems with memcache-client and Passenger and this problems needs to be addressed in a Rails as well as Rack application.
I handle them in Sinatra as follows:
configure do
# Create a global memcache client instance...
@@cache = MemCache.new({
:c_threshold => 10000,
:compression => true,
:debug => false,
:namespace => 'some_ns',
:readonly => false,
:urlencode => false
})
@@cache.servers = 'some_server:11211'
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
@@cache.reset if forked
end
end
end |
configure do
# Create a global memcache client instance...
@@cache = MemCache.new({
:c_threshold => 10000,
:compression => true,
:debug => false,
:namespace => 'some_ns',
:readonly => false,
:urlencode => false
})
@@cache.servers = 'some_server:11211'
if defined?(PhusionPassenger)
PhusionPassenger.on_event(:starting_worker_process) do |forked|
@@cache.reset if forked
end
end
end
If PhusionPassenger is available, install an event handler that resets the freshly forked memcached connection to avoid corruption.
Using Geonames.org
Google Maps is great but one thing that’s often forgotten is reverse geocoding. Google doesn’t offer such api (as far as i know) but GeoNames does.
I’ve written a mobile J2ME client around JSR 179 api that can push my current location to this server when i’m biking so my girlfriend can follow my rides on the map.
The client pushes latitude and longitude and the server replies with the coordinates in angles and the name of the place. The name is retrieved through the Ruby Geonames API like so:
#
# Adds a new location
#
post '/locations' do
require_administrative_privileges
location = Location.new params
if location.save then
begin
country_subdivision = Geonames::WebService.country_subdivision(location.latitude, location.longitude)
places_nearby = Geonames::WebService.find_nearby_place_name(location.latitude, location.longitude).first
(location.description = "#{places_nearby.name}, #{country_subdivision ? country_subdivision.admin_name_1 + ', ' : ''}#{places_nearby.country_name}"[0,2048].strip) and location.save if places_nearby
rescue Exception => exc
end
end
"#{location.to_s}\n"
end |
#
# Adds a new location
#
post '/locations' do
require_administrative_privileges
location = Location.new params
if location.save then
begin
country_subdivision = Geonames::WebService.country_subdivision(location.latitude, location.longitude)
places_nearby = Geonames::WebService.find_nearby_place_name(location.latitude, location.longitude).first
(location.description = "#{places_nearby.name}, #{country_subdivision ? country_subdivision.admin_name_1 + ', ' : ''}#{places_nearby.country_name}"[0,2048].strip) and location.save if places_nearby
rescue Exception => exc
end
end
"#{location.to_s}\n"
end
Inline templates, external resources
The thing started as a small application. PHP was out of the question but Rails was overkill, too. I just wanted one single file.
I can use the great Haml syntax as inline templates. This goes for creating HTML as well as for CSS through Sass.
To get some basics, i also include one of the W3 core styles.
The only resources that lives outside biking.rb is JQuery for easy creation of the autorefreshing code and some images, namely the cute green bikers from Greensmilies.
Summary
It’s absolutely possible to write an application with thee MVC Pattern in mind without one of the “big” frameworks in just one single script.
I would go much further with this app using just one script, but for it’s current purpose, it’s great, i think.
I hope you enjoyed reading my little annotations as i enjoyed writing them and the application. The full source code is available right through biking.michael-simons.eu and it’s published under the BSD license.
Filed in English posts
|