Wolfmans Howlings

A programmers Blog about Ruby, Rails and a few other issues

Developing a social networking site part 3 - tag cloud

Posted by Jim Morris Sat, 23 Jun 2007 21:02:49 GMT

This is a simple one.

I use the excellent acts_as_taggable plugin, and I wanted to have a tag cloud like everyone does.

Of course I have different Models that can be tagged, and I want to keep everything DRY, so I created a helper and put the following in the application_helper.rb file

  # display a tag cloud for the given model
  def tag_cloud(model, title= nil)
    m= model.to_s.camelcase.constantize
    plural= model.to_s.capitalize.pluralize
    title ||= plural
    tags= m.tag_counts(:order => 'tags.name')
    gen= ""
    unless tags.empty?
      urlmeth= "tagged_#{model.to_s.pluralize}_path".to_sym
      gen= "<div class=\"tagcloud\">"
      gen += "<h3>#{title}</h3>"
      tags.each do |t|
        gen += "<span style=\"font-size:#{calc_size(t.count)}%\">"
        gen += link_to(h(t.name), self.send(urlmeth, :tag => t.name))
        gen += "</span> "
      end
      gen += "</div>"
      gen += "<hr/>"
    end
    gen
  end

This allows me to have multiple tag clouds for the different models, see snowdogsr.us on the left bar at the bottom.

The font is bigger for the more popular tags, and you can click on any tag to get a listing of all matching articles.

I use RESTful routing so I added this to routes.rb

  map.resources :places, :collection => { :tagged => :get }

which creates a named route tagged_places_path which returns an index with the places tagged with the tag that was clicked.

The places_controller method looks like this...

  def tagged
    @places= Place.find_tagged_with(params[:tag], :order => "updated_at DESC")
    @filter= "Tagged with #{params[:tag]}"
    respond_to do |format|
      format.html { render :action => 'index' }
      format.xml  { render :xml => @places.to_xml }
    end
  end

Posted in  | Tags , ,  | no comments | no trackbacks

Developing a social networking site part 2 - rating stars

Posted by Jim Morris Sat, 23 Jun 2007 20:49:00 GMT

In part 1 I outlined my project to implement snowdogsr.us a social networking site for snow dogs.

I am pleased to announce that version 1 of this site is up, however I had to make some trade-offs to get it up this far. I needed to prioritize my goals and just get the essentials implemented.

So the ability to create Posts, Events and Places, and add photos of your dogs was goal number 1. The linking to friends was postponed, although most of the logic was implemented, I couldn't figure out how to expose that in an intuitive UI.

The first thing I wanted was to be able to rate the various Posts and Places people entered, and how better to do that than to copy the Netflix and Google groups method of showing 5 stars and clicking on them to rate the item.

To that end some googling found an excellent CSS method to do this, all I had to do was integrate it into Rails. I had some extra business rules I needed to deal with

  • Only people logged in can rate an article
  • The creator of the article can not rate their own article
  • If the rating is readonly then it still needs to be displayed as stars, just not clickable ones.

Out of the box the Stars were always clickable, so I needed a way to show readonly stars when the user was not allowed to rate the article, luckily the above referenced blog explained how to do that in the blogs comments.

I'm using the acts_as_rated plugin, which works fine so long as you don't try using the optimized method of creating a stats table, that method didn't seem to work, and for now I'll just let the SQL do the math.

As I have many models that acts_as_rated I needed to write a helper to display the stars, and handle the rating clicks, and the business logic.

The helpers look a little ugly as you need to figure out what the parent model is.

So here is the code that goes in application_helper.rb

  def star_rating(rating, obj_type, id, allow_rate= true)
    per= rating > 0 ? (rating/5.0)*100 : 0;
    url_meth= "rate_#{obj_type}_path".to_sym
    if allow_rate
      links= [
        link_to('1', send(url_meth, {:id => id, :rate => 1}), :method => :put, :class => "one-star", :title =>"1 star out of 5"),
        link_to('2', send(url_meth, {:id => id, :rate => 2}), :method => :put, :class => "two-stars", :title =>"2 stars out of 5"),
        link_to('3', send(url_meth, {:id => id, :rate => 3}), :method => :put, :class => "three-stars", :title =>"3 stars out of 5"),
        link_to('4', send(url_meth, {:id => id, :rate => 4}), :method => :put, :class => "four-stars", :title =>"4 stars out of 5"),
        link_to('5', send(url_meth, {:id => id, :rate => 5}), :method => :put, :class => "five-stars", :title =>"5 stars out of 5")]
    end

    r= " "
    r += "<span class=\"inline-rating\">  <ul class=\"star-rating\"> <li class=\"current-rating\" style=\"width:#{per}%;\"></li>"

    # if we can't rate then just show stars
    if allow_rate
      (0..4).each do |i|
        r += "<li>#{links[i]}</li>"
      end
    end

    r += "</ul></span>"
    r += '<span class="prompt">Click star to rate</span>' if allow_rate
    r
  end

  def render_rating(o, allow_rating=true)  
    can_rate= allow_rating && logged_in? && ! o.created_by?(current_user)
    o_type= o.class.to_s.downcase

    if o.rated?
      r= "Rating #{o.rating_average}/5 by #{pluralize(o.rated_count, 'person')}"
      r += star_rating(o.rating_average, o_type, o.id, can_rate)
    elsif can_rate
      r= "Not yet Rated"
      r += star_rating(0, o_type, o.id, can_rate)
    else
      r= ""
    end
    r
  end

The render_rating method is what you call from your view, the star_rating method generates the HTML.

(I presume you included the star_rating.css which is available from the above mentioned blog, and the acts_as_rated plugin.)

The render_rating method takes the model object being rated, and an optional override to allow_rating or not (which determines if the stars are clickable or not).

I include some text saying what the rating is and how many people have rated the article, followed by the stars showing the rating.

I have all my ratings set to 1 to 5, YMMV.

In my view (HAML) code I have...

= render_rating(place)

Which will render the rating for the current place article.

the Place model has this code...

acts_as_rated(:rating_range => 1..5, :rater_class => 'Person')

because I have a Person class as the rater rather than the default User.

The rating percentage is calculated using..

  per= rating > 0 ? (rating/5.0)*100 : 0;

Which is used by the CSS to highlight the stars correctly.

I use RESTful routing so the links are generated using named routes, however as the named route will be different depending on the Model object being rated I need to use this code...

# added to routes.rb to provide a rate method
map.resources :places, :member => { :rate => :put }

# added to places_controller.rb to handle the new route
 def rate
    @place = Place.find(params[:id])
    rating= params[:rate].to_i
    @place.rate(rating, current_user.person)

    respond_to do |format|
      format.html { redirect_to :back }
      format.xml  { head :ok }
    end

  end

# code snippets from the application helper above
# get the named route to use in this case rate_place_path
url_meth= "rate_#{obj_type}_path".to_sym

...

# generate the named route, equivalent to 
# rate_place_path(:id => id, :rate => 1)

send(url_meth, {:id => id, :rate => 1})

One last thing slightly on topic, I added the following to the acts_as_rated.rb file so I could get a list of the top rated articles, seemed like a logical thing that anyone would want to do ;)

# Find the top rated items, ordered by average rating
def find_top_rated(limit=nil)
  rating_class = acts_as_rated_options[:rating_class].constantize
  base_sql = <<-EOS
    select #{table_name}.*,COALESCE(average,0) AS rating_average from #{table_name} left outer join
      (select avg(rating) as average, rated_id  
         from #{rating_class.table_name}
         where rated_type = '#{class_name}' 
         group by rated_id) as rated 
         on rated_id=id 
       order by rating_average DESC
  EOS
  base_sql += " LIMIT #{limit}" if limit
  find_by_sql base_sql
end

I hope this helps others trying the same thing, as always comments and code improvements always welcome.

Posted in  | Tags , , ,  | 11 comments | no trackbacks

More JEdit macros for rails

Posted by Jim Morris Thu, 31 May 2007 20:27:00 GMT

I have been using JEdit more and more for my rails development, I have gone back and forth between it and Epsilon, however JEdit is starting to win out. I have upgraded to the latest pre version (4.3pre9).

I have modified a number of macros to do my bidding, and I dumped the Ruby Plugin because I kept running into things it did that I disliked, and it still seems a little buggy.

The best thing I did was update the ruby.xml Language mode to fully indent properly, like unindent end else rescue etc, and do this when you type those words. This is now possible with some new features in the 4.3pre9 series.

I also wrote a HAML language mode.

The plugins I currently use are...

  • Buffer Selector
  • BufferTabs
  • Common Controls
  • Console
  • CssEditor
  • CtagsSideKick
  • ErrorList
  • Highlight
  • Info Viewer
  • Latest Version
  • Log Viewer
  • MacroManager
  • OpenIt
  • Project Viewer
  • QuickNotepad
  • RecentBufferSwitcher
  • SideKick
  • SuperAbbrevs
  • SwitchBuffer
  • Tags
  • TextTools
  • XercesPlugin

The macros I have downloaded, modified or written to help with rails development are...

  • Expand_Hash.bsh - My macro to expand # to #{} when in a string
  • Go_to_Ruby_method_v0.5.bsh - Downloaded from the macromanager
  • Open_Related_File.bsh - Downloaded from the macromanager
  • Search_Ruby_documentation - A modified version of the one I downloaded, modified to use qri, and select from a list if multiple hits
  • Run_Test_Case.bsh - My macro to run a specific test case or specification. Bind it to a key (I use Shift-F11), put the cursor in a test case or specification and type the shortcut, and that specific test case will run, the results going to the console plugin.
  • Select_Super_Abbrevs.bsh - My macro to select from a list of matching SupperAbbrevs
  • Find_Next_Selected.bsh - Downloaded from the macromanager

I have linked the ones I have written or modified so you can download them if you like.

Read more...

Posted in ,  | Tags , , ,  | 4 comments | no trackbacks

Snow Dogs R Us a rails based social networking site

Posted by Jim Morris Thu, 31 May 2007 19:59:01 GMT

I have started a new project for myself and a few friends, Snow Dogs R Us. This site just went live! (on 6/20/2007). It is a full blown Web2.0 (insert other buzz words here), social networking site for Snow Dogs and their (human) parents.

It is certainly a challenging project and taking much longer than I anticipated.

The basic features include sign-up, forgot password, login, optional profiles for the user and their snow dogs, and all the usual stuff there.

The advanced features include linking to friends for both the humans and the pets, so the humans can link to other human friends, and the pets can link to their friends too (Ala MySpace and Facebook etc). I want to eventually have a LinkedIn type of network as well, so you can see how many degrees you are away from other people, this is a real challenge to do in Rails, and I will probably actually write a Java engine to do that, as it is pretty compute and memory intensive. (Its not really an essential feature of the site, but I like technical challenges).

Of course there will be the ability to comment on everything, rate everything and tag everything, and there has to be the obligatory Mashup with Flickr and YouTube for all those cute photos and videos of our pets. (What other Web2.0 feature have I missed?).

Luckily the Rails community has already made available a lot of plugins we can use. I currently use these plugins...

  • active_scaffold - for the admin interface
  • acts_as_taggable_on_steroids - for the tagging
  • haml - For all my HTML structure needs
  • test_spec_on_rails - a better way to test
  • acts_as_commentable - pretty basic comments
  • cssformbuilder - highly customized, I need to make my mods available
  • restful_authentication - for authentication, I added the forgot password hack slightly modified
  • transactional_migrations - Ideal for Postgresql
  • acts_as_rated - I started using acts as rateable but switched
  • calendar_date_select - for inputting dates
  • sexy_migrations - Makes migrations so much easier
  • will_paginate - For paginating those pages
  • attachment_fu - For uploading the pictures (and FreeImage and ImageScience for scaling them)

I looked at has_many_friends but it was so close to what I had already done I just decided to keep my version, plus I want to add that degrees of freedom stuff later on.

There have been many posts about the friendships links in Rails, some decided to simply have a HABTM with two entries to make the link symmetric, I don't like wasting space so my solution uses a single entry for their friendship, and some SQL to find friends regardless of whether they invited you or you invited them. I'll blog about that later when I optimize it a bit more, as friendship links can grow exponentially I think it is prime candidate for optimization both in the amount of space is uses in the database and for database access, and simple has_many :through construct work but in this case will be very inefficient.

Another feature is the ability to create events and invite everyone or just your friends (think evite for dogs), this is relatively simple except keeping track of the invitations and who accepted etc. I also link to Places, because an event is usually at a place, and of course the place needs to have directions, so a link to Google maps is in order there. Of course a place needs to be taggable and commentable and rateable.

Lastly is the ability to make recommendations. In the dog community we share advice on Vets, food, dog friendly places to visit like restaurants, parks, beaches etc. These all need to be taggable, rateable and commentable and of course searchable. I've gone back and forth on how to implement that, right now it is a simple table called inputs (user input), which is taggable, commentable, and rateable. It relies on people tagging properly, IE tag with restaurant for dog friendly restaurants, and vet for vet recommendations etc. I originally had a belongs_to input_type with predefined categories, but decided to trust the user and just allow tags. We'll see how that works. Maybe the tag input field needs to auto_complete with a list of current tags so people are more inclined to tag with already used tags.

My biggest problem is the web design or look and feel, I am a programmer not a designer, so I can solve the complex programming issues and implement all the functionality, but I get really stuck when I try to figure out how to present that information. If this was a "for pay" project I would hire a web designer, but it is definitely a not-for-profit project so I can't afford that, so I'll struggle with the design.

I'll post more entries on some of my solutions to the technical problems as I go.

A few things I have learned, is to use piston for managing plugins, and avoid alpha plugins :)

Posted in  | Tags , , ,  | 9 comments | no trackbacks

Older posts: 1 2 3 4 5 6 ... 10