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
Thanks Solomon for your solution ! It works great!
I'd like to use your solution as a scope (because I'd like to make a search not based only on tags).
The problem is that the "all" method breaks chaining scope.
I tried to make a scope like this, but doesn't work (not the expected result) :(
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
Maybe not relevant now, but I had the same problem and found the solution in my PostsController under update. I just needed to add :tag_list to the permitted parameters like so:
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.
You could just patch the gem up to your own liking.
F.e.:
Add a column to the tag table (rails g migration AddDescriptionToTag description:text)
Open up the class (ActsAsTaggableOn::Tag) and go from there - make the description accessible or whatever you want.
Is there any way to make this work for multi-word tags?
My index page works fine with one-word tags (e.g. 'superman', 'comic', etc.), but when I have a multi-word tag (e.g. 'the riddler', 'the penguin', etc.) I get the following error:
undefined method 'joins' for []:Array.
When I try to filter my articles by the tag 'Tag 3', the URL that I am directed to is:
How can I list specific tags in route file? I want to create a link and filter specific tags. I want to be able to link the path to specific filtered tags for my posts using link_to. e.g. <%= link_to "Blog", posts_path %>
ruby
puts "
get 'tags/:tag', to: 'posts#index', as: :tag"
I'm on rails4 ruby2.1 and I can't seem to get the tagging table to record the association. Any help will be much appreciated. (doing this from scratch)
When I got to the 2 minute mark, at the page refresh, I got the following error: 'undefined method write_inheritable_attribute' for #<Class:0x007fccb5cd1c50>. Additionally it points to line 1:
I succesfully implemented a tag cloud in Ruby, from scratch. Excellent tutorial!
Now I would like to make my tag cloud more dynamic, so that it allows you to narrow down listings.
On my photoblog, each photo has several tags. Once you click on a tag, say "Tokyo", all photos tagged with "Tokyo" are listed. I now want to add two things:
* I want the tag cloud to be updated and display only tags associated to the remaining photos, the "Tokyo" subset.
* When you click on a tag in this subset-tag cloud, say "2008", I want all photos to be listed that are tagged with "Tokyo" and "2008" (instead of all photos tagged with "2008").
I have been trying to implement this for days, no luck so far. I'm new to Ruby so any help will be much appreciated!
Why exactly is the tagging model necessary? It's being used kind of as a join table right, but for the simpler functionality it seems unnecessary. Is there something I'm missing?
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!
Very helpful.
Shouldn't the assignment of
self.tags
in 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 :tags
in 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).last
with
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:
Thanks Solomon for your solution ! It works great!
I'd like to use your solution as a scope (because I'd like to make a search not based only on tags).
The problem is that the "all" method breaks chaining scope.
I tried to make a scope like this, but doesn't work (not the expected result) :(
scope :tagged_with, lambda { |names|
joins(:taggings,:tags).where(tags: {name: names}).group('articles.id').having(['COUNT(*) >= ?', names.length])
}
Any idea ?
@Solomon Thank you for posting this here. I've been looking for it for a while!
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
+1
Any Idea on how I can customize the tag_cloud to only show the top ten tags. I am also doing it from scratch.
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.
Maybe not relevant now, but I had the same problem and found the solution in my
PostsController
underupdate
. I just needed to add:tag_list
to the permitted parameters like so:Hope it may help someone else in the future.
I am from the future
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?
You could just patch the gem up to your own liking.
F.e.:
Add a column to the tag table (rails g migration AddDescriptionToTag description:text)
Open up the class (ActsAsTaggableOn::Tag) and go from there - make the description accessible or whatever you want.
What would it look like to add two tags to an existing or new article at the rails command line?
Thanks!
Hi there,
I'm having an issue. So far I installed it and run the migrations :
But then as soon as I add this line :
into my model, I've got the following error :
wrong number of arguments (3 for 1..2)
I'm using Ruby 2.0.0-p247 and Rails4.0.0.beta1 (The project is already pretty big and we can't dare to update the Rails version).
Anyone already faced the same issue?
Thanks a lot
Trying to follow this tutorial with Rails 4 but getting a 'RuntimeError' when trying to insert the tags_list into the view.
Do I need to change anything for Rails 4? Do I need to do anything with the 'attr_accessible' method, as this is moved for Rails 4?
Great episode, as usual.
Is there any way to make this work for multi-word tags?
My index page works fine with one-word tags (e.g. 'superman', 'comic', etc.), but when I have a multi-word tag (e.g. 'the riddler', 'the penguin', etc.) I get the following error:
undefined method 'joins' for []:Array.
When I try to filter my articles by the tag 'Tag 3', the URL that I am directed to is:
http://localhost:3000/tags/Tag%203
I imagine it has something to do with this line:
Anyone know how to make this work with multi-word tags?
How can I list specific tags in route file? I want to create a link and filter specific tags. I want to be able to link the path to specific filtered tags for my posts using link_to. e.g. <%= link_to "Blog", posts_path %>
puts " get 'tags/:tag', to: 'posts#index', as: :tag"
I am using Rails 4, and instead of: rails generate acts_as_taggable_on:migration,
I have to use: rake acts_as_taggable_on_engine:install:migrations.
But I want to remove this rake acts_as_taggable_on_engine:install:migrations that I did, but I can't find anywhere how to do this
nevermind
I'm on rails4 ruby2.1 and I can't seem to get the tagging table to record the association. Any help will be much appreciated. (doing this from scratch)
The tags do get created in the tag table; but no association defined in the tagging table between the record and the tag.
When I got to the 2 minute mark, at the page refresh, I got the following error: 'undefined method
write_inheritable_attribute' for #<Class:0x007fccb5cd1c50>
. Additionally it points to line 1:The Model:
I succesfully implemented a tag cloud in Ruby, from scratch. Excellent tutorial!
Now I would like to make my tag cloud more dynamic, so that it allows you to narrow down listings.
On my photoblog, each photo has several tags. Once you click on a tag, say "Tokyo", all photos tagged with "Tokyo" are listed. I now want to add two things:
* I want the tag cloud to be updated and display only tags associated to the remaining photos, the "Tokyo" subset.
* When you click on a tag in this subset-tag cloud, say "2008", I want all photos to be listed that are tagged with "Tokyo" and "2008" (instead of all photos tagged with "2008").
I have been trying to implement this for days, no luck so far. I'm new to Ruby so any help will be much appreciated!
Why exactly is the tagging model necessary? It's being used kind of as a join table right, but for the simpler functionality it seems unnecessary. Is there something I'm missing?
Mind you: In rails 4 dont use attr_accessible in the model, instead in the controller add:
ehh this took me an hour to figure out :)
ps: nice to add this in the modell though:
You have a wrong gem, the correct gem is:
gem 'foundation-rails'
And not:
gem 'zurb-foundation'
Here is a reddit post that really helped me get this working:
http://www.reddit.com/r/rails/comments/2chtgw/tagging_in_rails_4/
Ai perfecta dreptate in ce priveste http://calculatoaresecondhand.xyz/ pentru aplicabilitatea pe sistemele refurbished
This episode covering tagging from scratch has been upgraded to Rails 5. Checkout Tagging from Scratch in Rails 5