Friday, March 21, 2008

Tag clouds in Ruby and Rails


I love tags, it's a better way to classify content, and it's trendy, at least it was back in 2005. Tagcloud also gives you good visual indication of hot topics of the site content. I will share the method I use for creating a tagcloud in my sites. The trick of tagcloud is collecting all the tags for certain model and give each tag a weight in a scale from 1 to 5 for example. and then render a tagcloud which is basically tags rendered according to their weight, the most frequent tag is much bolder than the less frequent ones.



Tags models
Before using tags we need to create them first. I use a polymorphic relationship to tag several content types using two additional tables. The first table is the "tags" table which basically list all the tags used in the site, it is composed of a simple tag_id and name fields. The other table is the "taggings" tables. It holds all the relation ships between the content and the tags. It is actually a regular many to many association table but serve more than one content type. It may be easier to explain it in code.


class Book < ActiveRecord::Base
has_many :taggings, :as => 'taggable'
end

class Link < ActiveRecord::Base
has_many :taggings, :as => 'taggable'
end

class Tag < ActiveRecord::Base
has_many :taggings
end

class Tagging < ActiveRecord::Base
belongs_to :tag
belongs_to :taggable, :polymorphic => true
end


In these ActiveRecord models, Link and Book are content models which have many taggings, and each Tagging by its turn belongs to one tag. But notice in the has_many association in the Book and Link model that it we passed an "as => 'taggable'" that tell the ActiveRecord that Tagging belongs to a polymorphic model that is called 'taggable'. Taggable could be any model in your application and this pattern make your tagging mechanism support new models as you make them transparently, all you need to do is adding 'has_many as' association.

class Photo < ActiveRecord::Base
has_many :taggings, :as => 'taggable'
end


Create the database tables
The following is a basic Mysql schema for the tags, taggings tables:


CREATE
TABLE `tags` (
`id` int(10) unsigned NOT NULL auto_increment,
`name` varchar(255),
`created_at` datetime,
`updated_at` datetime,
PRIMARY KEY (`id`)
);

CREATE TABLE `taggings` (
`id` int(10) unsigned NOT NULL auto_increment,
`tag_id` int(11),
`taggable_type` varchar(20),
`taggable_id` int(11),
`created_at` datetime,
PRIMARY KEY (`id`)
);



Collecting the tags
The following is a method that could be located in a model probably taggings or in a helper.


def tags_cloud(model, limit=30)
options = {
:select => "count(taggings.id) as count_all, tags.name as tag_name, tag_id",
:conditions => {:taggable_type => model.to_s },
:joins => " left outer join tags on tags.id=taggings.tag_id" ,
:group => 'tag_id',
:order => 'count_all desc',
:limit => limit
}

sql = Tagging.send(:construct_finder_sql, options)

taggings = ActiveRecord::Base.connection.select_all(sql)

return [] if taggings.blank?

maxlog = Math.log(taggings.first['count_all'])
minlog = Math.log(taggings.last['count_all'])
rangelog = maxlog - minlog;
rangelog = 1 if maxlog==minlog
min_font = 1
max_font = 5
font_range = max_font - min_font
cloud = []
taggings = taggings.sort{|a,b| a['tag_name'] <=> b['tag_name']}

taggings.each do |tagging|
font_size = min_font + font_range * (Math.log(tagging['count_all']) - minlog)/rangelog
cloud << [tagging['tag_name'], tagging['tag_id'], font_size.to_i, tagging['count_all']] end
return cloud
end


If I use active record to find the required taggings it will then return a bunch of instantiated Tagging object and that would be a waste of memory and resources. It's better to make Active Record to only construct the SQL for us and then use the SQL to query the database directly.

I used an array with all regular select, conditions, joins and other arguments we usually use with the find method, and then use construct_finder_sql which is a private method of ActiveRecord, that's why i use send method to call it. And it returns the sql statement that i will use directly in an sql connection derived from the ActiveRecord::Base class.

I learned a trick from one of web2.0 books from Oreilly, I don't remember it right now, the trick is to make a contrast between the most frequent tags and less frequent ones using logarithm. Anyway you can figure it our yourself, or just use it as it is :)

The View
This is a sample of a view template that render the tagcloud. IT could be used as a base for a more sophisticated tagcloud.


<% the_cloud = tags_cloud(model) %>
<% unless the_cloud.blank? %>
<div id="big_tags_cloud">
<% the_cloud.each do |tag,id,fsize,count| %>
<span class="t<%= fsize -%>">
<
a title="<%= count %>"
alt="<%= count -%>"
class="tag_<%= id %>"
href="/<%= model.to_s.downcase %>s/tag/<%= id -%>">
<%=
tag %>
<
a>
<
span>
<% end %>
<
div>
<% end %>



Styling the cloud


#big_tags_cloud {padding:5px; text-align:justify; }
#big_tags_cloud a {font-family:'Times New Roman'; line-height:1.7;border-bottom:1px solid #779944;}
#big_tags_cloud span.t1 a{ color:#779944;font-size:11pt;}
#big_tags_cloud span.t2 a{ color:#779944;font-size:12pt;}
#big_tags_cloud span.t3 a{ color:#779944;font-size:13pt;}
#big_tags_cloud span.t4 a{ color:#558800;font-size:14pt;font-weight:bold;}
#big_tags_cloud span.t5 a{ color:#669900;font-size:15pt; font-weight:bold;}


Thursday, March 13, 2008

Switch proxy settings quickly in Firefox

I keep on switching back and forth proxy setting in Firefox. I am behind a proxy at work and direct connection at home. For all poor souls who suffer the same hassle go install this little firefox addon QuickProxy.

Wednesday, March 12, 2008

Making GPRS Vodafone card work on Ubuntu

I spent a couple of hours trying to make a GPRS Vodafone PCMCIA card to work in my Ubuntu laptop. And It's working, and here are my tips. But I believe the following instructions should be useful in other Linux distributions.

Make sure the following packages are installed:
sudo apt-get install pcmcia-cs
sudo apt-get install gnome-ppp
Configuring Gnome-ppp
There some Vodafone Egypt specific data needed for connection
APN: internet.vodafone.net
user: internet
password: internet
Type on a shell the following command and try to know the device assigned to the pcmcia GPRS card. In my laptop it was /dev/ttyS3. I noticed other blogs mentioning cards assigned to USB devices! So look carefully. You may need to eject and reinsert the card again and observe the messages on the syslog.
tail -f /var/log/syslog
After knowing the device assign to GPRS card, click "Setup" and choose the device from the list.

Select speed like 115200, there are bloggers mentioning 460800 so you may try it later. It doesn't work for me.

Select "Tone" as the phone line type.

Click on "Init Strings" and enter the following lines in Init2 and Init3 respectively.
Init2: ATQ0 V1 E1 S0=0 &C1 &D2 +FCLASS=0
Init3: AT+CGDCONT=1,"IP","internet.vodafone.net"
.. and uncheck "wait for dialtone"

After I configured gnome-ppp I tried to connect, I used *99***1# as the dial number, clicked connect, and see the log. And I got the pppd died unexpectedly error message, that very message traveled me back 12 years ago, trying and failing to make ppp work. My frustration didn't last more than 10 minutes, I figured out that gnome-ppp was trying and failed to open wvdial files in /etc/ppp/peers, so i searched the net and get me a bunch of wvdial conf files, put them in place and here are them.

# File: /etc/ppp/peers/wvdial
# WvDial options
#
plugin passwordfd.so
#
noauth
name wvdial
defaultroute
replacedefaultroute
noipdefault
usepeerdns
novj
#File: /etc/wvdial.conf
[Dialer Defaults]
Modem = /dev/ttyS3
Baud = 115200
Init1 = ATZ
Init2 = ATQ0 V1 E1 S0=0 &C1 &D2
Init3 = AT+CGDCONT=1,"IP","internet.vodafone.net"
Area Code =
Phone = *99***1#
Username = internet
Password = internet
Ask Password = 0
Dial Command = ATDT
Stupid Mode = 1
Compuserve = 0
Force Address =
Idle Seconds = 3000
DialMessage1 =
DialMessage2 =
ISDN = 0
Auto DNS = 1

[Dialer Another]
Init3 = AT+CGDCONT=1,"IP","internet.vodafone.net"
And then i tried again, and it worked this time, it connects, and fetch the DNS and I browsed to google.com successfully. But for only 120 seconds. Excactly 120 secs every time i connect and then ppd dies unexpectedly, as the error keeps saying. I noticed after some more google results that two options called lcp-echo-interval and lcp-echo-failure and that one of them was set to 30 that's the interval and the other was set to 4 and that was the number of failure and then pppd should die if it didn't receive response. But when i set those options to 0 the connection get stable and didn't disconnect. Those lcp values should be (or i add them) to '/etc/ppp/options'.

###################
# /etc/ppp/options
###################
asyncmap 0
noauth
crtscts
lock
hide-password
modem
proxyarp
lcp-echo-interval 0
lcp-echo-failure 0
noipx
novj
And that's it, now I can launch gnome-ppp and connect, but as a root. I created a gnome-ppp "gksu gnome-ppp" launcher on gnome panel.

And I can launch it from command line by `wvdial`.

The connection speed may not be the useful for real work, but it's faster


Related links:

Tuesday, March 4, 2008

Web2.0 applications presentation

I made a simple presentation to enumerate few of the web2.0 application that encourage community content contributions. There are hundreds of community driven web2.0 applications, I just named a few. I selected one applciation per content type.

Blog services: www.blogger.com
Photos: www.flickr.com
Bookmarks: Del.icio.us
Videos: YouTube.com
Podcasts: Odeo.com
Presentations: www.slideshare.net
PDF documents: www.scribd.com

And of course the single most important web application is "Google Reader", which allows you to get the latest updates in one place.
RSS Reader: www.google.com/reader

Two most important features in all the mentioned applications; first they all support RSS feeds so you can subscribe and get the latest updates; Second, you can embed the content you uploaded whither it's photos, links, video, presentations, or even pdf, you can embed all these type of content right into your blog. You only have to copy and paste the code from the content page.