Posted by Jim Morris
Tue, 26 Jun 2007 22:14:04 GMT
This one is so blatantly obvious it bit me in the Butt at 4am this
morning when I had to get up and fix it! I am so embarrassed, luckily
no private data got out, as no-one has entered any private data yet.
I used the script/generate scaffold_resource to get started, and I
left in those nice format.xml things in, thinking I may use them in
the future. For the most part this is not a problem, but one of my
controllers is a profile table. Much of the data in there is public
anyway so no big deal, but a few columns are private data like email,
date of birth, phone numbers etc. These are specifically private and
not viewable publicly. This is enforced but not having a view that
shows any of that stuff to the general public.
However the tricky little scaffold-generated code...
def index
@profiles = Profile.find(:all, :order => "first_name, last_name, alias")
respond_to do |format|
format.html
format.xml { render :xml => @profiles.to_xml }
end
end
Has this cool .to_xml stanza, which happily takes every column and
converts it to XML and sends it back as a response to the query
/profiles.xml
Yikes, I woke up with a start when I realized that, and rushed to test
it and yep it works as it is supposed to.
Obviously this is easy to fix, Just exclude the attributes you don't
want shown:
@profiles.to_xml(:only => [:first_name, :last_name])
But it sure is a nasty back door if you forget!
Caveat Programmer!
Posted in Rails | Tags rails, scaffold_resource | 4 comments | no trackbacks
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
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 Rails | Tags acts_as_taggable, rails, tagcloud | no comments | no trackbacks
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 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...
map.resources :places, :member => { :rate => :put }
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
url_meth= "rate_#{obj_type}_path".to_sym
...
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 ;)
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 Rails | Tags acts_as_rated, rails, rating, stars | 11 comments | no trackbacks