There are several gems to help implement tags in a Rails app. Here I show you how to integrate acts-as-taggable-on and then show how to do it from scratch.
Do we have XSS vulnerability because of displaying 'raw tags.map'? Hacker could inject some sort of malicious code in a tag name so it would be sent to user without escaping.
Thanks Ryan. I've been working on a some simple tagging with the native Postgres string-array type this week, it seems to work very well although I don't have any comparative data on performance yet.
Shouldn't the assignment of self.tags in the method tag_names= be moved to an after_save callback method? What if the validations on a newly created article fail? The tags would still be created without proper referencing.
After a lot of investigating I found a way to run validations on tags using something very close to Ryan's code.
I'll just put the code that I changed:
ruby
classPost < ActiveRecord::Base
attr_accessible :content, :summary, :title, :tag_list, :tags# association code here...
validates_associated :tag_list
before_save :save_tags# validation code here...# other methods shown in original code...deftag_list
caller[0][/`([^']*)'/, 1] == 'block in validate' ? @tag_list : tags.map(&:name).join(", ")
enddeftag_list=(names)
@tag_list = names.split(",").map do |n|
#self.tags.find_or_initialize_by_name(name: n.strip) #uncomment this if you want invalid tags to show in tag listTag.find_or_initialize_by_name(name: n.strip)
endend
private
defsave_tagsself.tags = Tag.transaction do@tag_list.each(&:save)
endendend
A little explanation: I found that adding Tag objects to @post.tags automatically validates, and I believe attempts to save, the Tag object. So if any Tag object is invalid, it's validation will run, and fail, when the array of Tag objects is set to the value of self.tags in the tag_list= method.
(The method used by the update method in the PostController to update the model, update_attributes, automatically saves the entry if it's valid by the way.)
So, we need a way then to validate the Tag objects when the post object is being validated. So what what we can do is store the array of Tag objects generated in the tag_list= method to an instance variable called, wait for it, '@tag_list'. By adding :tag_list to our list of attr_accessible items, we can then use validates_associated to run validations on @tag_list when the post object is validated.
Because we named the array @tag_list, errors will be returned as errors on the tag_list attribute that we access for our post form. Which is good.
If the post object successfully saves -- meaning everything, including the Tag objects, validated -- we use a before_save hook to save the array of Tag objects stored in the tag_list and update self.tags.
The only thing left to do was to update the tag_list method so that it returns the string of tags except when the method is being called by a validation method, in which case it returns the @tag_list array.
Assuming that a tag should only be created once (unique constraint on name) I'd rather use:
ruby
self.tags = names.split(",").map do |n|
Tag.find_or_create_by_name(n.strip)
end
instead of
ruby
self.tags = names.split(",").map do |n|
Tag.where(name: n.strip).first_or_create!
end
For better performance one should also consider a counter cache on the belongs_to :tags in the taggings model. This would avoid the join when calculating the tag counts in:
ruby
defself.tag_countsTag.select("tags.*, count(taggings.tag_id) as count").
joins(:taggings).group("taggings.tag_id")
end
The code from the article is the Rails 3.2+ compliant version of the replacement you supplied. They do the exact same thing but your version wont work in Rails 4.1
I implemented my own tagging system similar to the code you wrote a while back, but I noticed that it did not scale well. If you only have a few tags it works fine, but if you have hundreds of thousands or millions and similar numbers of articles then performance degrades substantially. I ended up having to do quite a bit of caching to get acceptable performance, especially for the tag cloud.
I did not use, or know about, the acts_as_taggable_on gem at the time. Does anybody know how well it scales? If not I will definitely be running some performance tests on it this week.
I didn't see your comment before I posted mine. I don't remember everything I did, but counter_cache was probably the single biggest improvement. I also recall using memcached for storing certain results.
That said, I was doing some pretty unnecessary tag stuff. It was possible to create queries of tags with logical and, or, and not operators. Features that ultimately went mostly unused.
The original code will produce unnecessary iterations that might start to affect performance once the number of tags increases. In general: Keep iterations to a minimum – in my experience, it's the number one performance problem of most Rails apps.
Well, both of those return an empty array. I guess this is because it is looking for tags that match both conditions (which is impossible since the name attribute is unique), instead of looking for posts that have both the tags.
Anyway thank you for pointing me to arel_table, I'll have a deeper look on that to see if it is what I need.
defself.tagged_with(name) #name is an array of 1 or more tag names
all(:conditions => {:tags => {:name => name}},
:joins => :taggings,
:joins => :tags,
:group => 'articles.id',
:having => ['COUNT(*) >= ?', name.length]
)
end
There is only one thing that the gem covers in the example and the solution made from scratch doesn't cover: the relation for the tagging to belong to a user (act_as_tagger in the user model). How do you think this might be done, as most user authentication solutions (like Devise, Authlogic or Sorcery) doesn't provide the current_user helper in the ActiveRecord layer.
I haven't touched RoR in a month and getting rusty, took a small break came back to computer and it hit me, the problem was with a haml conversion from html:
Any ideas how to overwrite the to_param method? Overwriting it in /app/models/tag.rb just doesn't work in urls, though it works in console when invokung Tag.first.to_param
What i mean is instead of urls like /tags/foo%20bar i would like to have urls like /tags/foo-bar
it is not working for me as well..
I get the error:
PGError: ERROR: column "tags.id" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: SELECT tags.*, count(taggings.tag_id) as count FROM "tags" I...
I used this code, and it works but only if there are few tags.
Problem is that count is not a number so
max = tags.sort_by(&:count).last
doesn't sort it properly when number of tags is more than 9 for specific post/article.
I will try to write function to find max properly unless anyone has an idea how to have it as a number in the first place..
in case someone had this issue - in the end I have Tomaž Zaman solution with the helper method modification as below:
ruby
deftag_cloud(tags, classes)
max = 0
tags.each do |t|
if t.count.to_i > max
max = t.count.to_i
endend
tags.each do |tag|
index = tag.count.to_f / max * (classes.size - 1)
yield(tag, classes[index.round])
endend
Was noticing that this was not doing anything to standardize the capitalizations so therefore tagname and Tagname are two different things in this implementation, in order to fix this I simply used a before_save to titleize the names with this:
ruby
before_save :titleizedeftitleizeself.name = self.name.titleize
end
Pretty simple but assigning of the name threw me for a loop for a minute. Great cast!
Here are the necessary model methods for any DataMapper users:
ruby
has n, :taggings
has n, :tags, :through => :taggingsdefself.tagged_with(name)
Tag.all(name: name).articles
enddeftag_list
tags.map(&:name).join(", ")
enddefself.tag_countsDataMapper.repository.adapter.select('select tags.*, t.count from tags inner join (select taggings.tag_id, count(taggings.tag_id) as count from taggings group by taggings.tag_id) t where tags.id = t.tag_id')
enddeftag_list=(names)
self.tags = names.split(',').map do |n|
Tag.first_or_create(name: n.strip)
endend
When trying to tag an article, lets say "chemistry, sept 20/2012" as in want to organize all the articles posted on a certain date.
How do you rewrite the routes.rb:
get 'tags/:tag', to: 'articles#index', as: :tag
in order to access tag names that embed "/" forward slash?!
I get this error when I try to access sept 20/2012:
No route matches [GET] "/tags/sept%2020/2012"
Hello. I have a problem. This app use sqlite3, but i need postgresql. I edit my Gemfile and database.yml. My app work in rails server, but on heroku doesn't. May be it's needs to edit something other. Write me please, what i needs to edit to use postgersql.
Do we have XSS vulnerability because of displaying 'raw tags.map'? Hacker could inject some sort of malicious code in a tag name so it would be sent to user without escaping.
I think link_to ensures that?
Thanks Ryan. I've been working on a some simple tagging with the native Postgres string-array type this week, it seems to work very well although I don't have any comparative data on performance yet.
https://github.com/dockyard/postgres_ext was a huge help for getting started with it.
Thanks very much!
Shouldn't the assignment of
self.tagsin the methodtag_names=be moved to an after_save callback method? What if the validations on a newly created article fail? The tags would still be created without proper referencing.After a lot of investigating I found a way to run validations on tags using something very close to Ryan's code.
I'll just put the code that I changed:
A little explanation: I found that adding Tag objects to @post.tags automatically validates, and I believe attempts to save, the Tag object. So if any Tag object is invalid, it's validation will run, and fail, when the array of Tag objects is set to the value of self.tags in the tag_list= method.
(The method used by the update method in the PostController to update the model, update_attributes, automatically saves the entry if it's valid by the way.)
So, we need a way then to validate the Tag objects when the post object is being validated. So what what we can do is store the array of Tag objects generated in the tag_list= method to an instance variable called, wait for it, '@tag_list'. By adding :tag_list to our list of attr_accessible items, we can then use validates_associated to run validations on @tag_list when the post object is validated.
Because we named the array @tag_list, errors will be returned as errors on the tag_list attribute that we access for our post form. Which is good.
If the post object successfully saves -- meaning everything, including the Tag objects, validated -- we use a before_save hook to save the array of Tag objects stored in the tag_list and update self.tags.
The only thing left to do was to update the tag_list method so that it returns the string of tags except when the method is being called by a validation method, in which case it returns the @tag_list array.
Assuming that a tag should only be created once (unique constraint on name) I'd rather use:
instead of
For better performance one should also consider a counter cache on the
belongs_to :tagsin the taggings model. This would avoid the join when calculating the tag counts in:The code from the article is the Rails 3.2+ compliant version of the replacement you supplied. They do the exact same thing but your version wont work in Rails 4.1
I implemented my own tagging system similar to the code you wrote a while back, but I noticed that it did not scale well. If you only have a few tags it works fine, but if you have hundreds of thousands or millions and similar numbers of articles then performance degrades substantially. I ended up having to do quite a bit of caching to get acceptable performance, especially for the tag cloud.
I did not use, or know about, the acts_as_taggable_on gem at the time. Does anybody know how well it scales? If not I will definitely be running some performance tests on it this week.
How did you implement the caching? In the comment above I suggested using a counter_cache.
I didn't see your comment before I posted mine. I don't remember everything I did, but counter_cache was probably the single biggest improvement. I also recall using memcached for storing certain results.
That said, I was doing some pretty unnecessary tag stuff. It was possible to create queries of tags with logical and, or, and not operators. Features that ultimately went mostly unused.
Such an awesome episode. Though to get this example to work, I had to adjust the syntax from line 2 of your routes.rb example from:
puts "get 'tags/:tag', to: 'articles#index', as: :tag"to
puts "get 'tags/:tag' => 'articles#index', :as => :tag"Ryan used Ruby 1.9 hash syntax, that's why. You seem to be on Ruby 1.8x?
Alas, yes.
You probably want to replace
max = tags.sort_by(&:count).lastwith
max = tags.max_by(&:count)There's no need to sort the array only to pick the largest element.
should really be
article.tags.map { |t| link_to t.name, tag_path(t.name) }.join(', ')The original code will produce unnecessary iterations that might start to affect performance once the number of tags increases. In general: Keep iterations to a minimum – in my experience, it's the number one performance problem of most Rails apps.
What if I want to query for articles that belong to multiple tags?
I tried this method:
but this returns me every post that has either tag foo, or bar or both.
How do I search only for posts that have both foo and bar at the same time?
You should use arel_table.
@Patrick, that actually gives me the same result.
Let me explain with better words what I'm trying to achieve:
post1 has tags "foo, bar, baz"
post2 has tags "foo, bar"
post3 has tags "foo, baz"
I need to a query that passing "foo, baz" returns only post1 and post3, since they are the only posts to have both foo and baz.
Thank you for your help.
https://github.com/rails/arel/blob/master/lib/arel/predications.rb
or maybe
Well, both of those return an empty array. I guess this is because it is looking for tags that match both conditions (which is impossible since the name attribute is unique), instead of looking for posts that have both the tags.
Anyway thank you for pointing me to arel_table, I'll have a deeper look on that to see if it is what I need.
Ever figure this one out TopperH? I've been struggling with the same problem.
Here is the solution for those who may want it:
To complete the full circle, it would be great to see how to use tags with http://aehlke.github.com/tag-it/
There is only one thing that the gem covers in the example and the solution made from scratch doesn't cover: the relation for the tagging to belong to a user (act_as_tagger in the user model). How do you think this might be done, as most user authentication solutions (like Devise, Authlogic or Sorcery) doesn't provide the current_user helper in the ActiveRecord layer.
Check out http://stackoverflow.com/questions/3619361/update-owner-tags-via-form, implementing this functionality in an observer worked for me.
I haven't touched RoR in a month and getting rusty, took a small break came back to computer and it hit me, the problem was with a haml conversion from html:
WRONG WAY
RIGHT WAY
Can delete my questions to clean up this railscast =)
I've deleted your questions and duplicate comments per your request :)
Any ideas how to overwrite the to_param method? Overwriting it in /app/models/tag.rb just doesn't work in urls, though it works in console when invokung Tag.first.to_param
What i mean is instead of urls like /tags/foo%20bar i would like to have urls like /tags/foo-bar
the tag_cloud from scratch isn't working here. Is it may be because I'm using postgresql as a database?
it is not working for me as well..
I get the error:
Can anyone help with that?
Same problem here
EDIT: solved, first you need to add id and name columns to group
then you need to coerce the count into an integer in the helper:
worked like a charm :) Thank you!!
I used this code, and it works but only if there are few tags.
Problem is that count is not a number so
max = tags.sort_by(&:count).last
doesn't sort it properly when number of tags is more than 9 for specific post/article.
I will try to write function to find max properly unless anyone has an idea how to have it as a number in the first place..
in case someone had this issue - in the end I have Tomaž Zaman solution with the helper method modification as below:
+1
Thanks for the reply! I didn't notice your solution until now.
Hello! Thanks for gem. But I have problem: on editing model with tags, I have empty tags field. I tried both ways of realising but also empty.
I tried to print simple @post.tag_list, an it is not empty but in form it is empty( Need help.
I too have an empty tag_list in my form when editing or updating. If I leave it empty it removes my tags.
Awesome! With the help of this and http://railscasts.com/episodes/17-habtm-checkboxes-revised I was able to integrate this to work the way I wanted. Thanks Ryan.
Was noticing that this was not doing anything to standardize the capitalizations so therefore tagname and Tagname are two different things in this implementation, in order to fix this I simply used a before_save to titleize the names with this:
Pretty simple but assigning of the name threw me for a loop for a minute. Great cast!
Here are the necessary model methods for any DataMapper users:
Hope that helps.
When trying to tag an article, lets say "chemistry, sept 20/2012" as in want to organize all the articles posted on a certain date.
How do you rewrite the routes.rb:
get 'tags/:tag', to: 'articles#index', as: :tag
in order to access tag names that embed "/" forward slash?!
I get this error when I try to access sept 20/2012:
No route matches [GET] "/tags/sept%2020/2012"
we need to add the following like of code:
ActsAsTaggableOn.force_parameterize = true
Hello. I have a problem. This app use sqlite3, but i need postgresql. I edit my Gemfile and database.yml. My app work in rails server, but on heroku doesn't. May be it's needs to edit something other. Write me please, what i needs to edit to use postgersql.
Hello there,
This episode is great and helpful,but what if I need additional fields for storing like tag description? Any idea?
What would it look like to add two tags to an existing or new article at the rails command line?
Thanks!
First sign in through GitHub to post a comment.