#167
Jun 22, 2009

More on Virtual Attributes

Use a virtual attribute to implement a simple tagging feature. In this episode I show you how to assign virtual attributes through a callback instead of a setter method.
Tags: models forms
Download (12.4 MB, 7:44)
alternative download for iPod & Apple TV (8.4 MB, 7:44)

Resources

script/generate model tag name:string
script/generate model tagging article_id:integer tag_id:integer
rake db:migrate
# models/tagging.rb
class Tagging < ActiveRecord::Base
  belongs_to :article
  belongs_to :tag
end

# models/tag.rb
class Tag < ActiveRecord::Base
  has_many :taggings, :dependent => :destroy
  has_many :articles, :through => :taggings
end

# models/article.rb
class Article < ActiveRecord::Base
  has_many :comments, :dependent => :destroy
  has_many :taggings, :dependent => :destroy
  has_many :tags, :through => :taggings
  validates_presence_of :name, :content
  attr_writer :tag_names
  after_save :assign_tags
  
  def tag_names
    @tag_names || tags.map(&:name).join(' ')
  end
  
  private
  
  def assign_tags
    if @tag_names
      self.tags = @tag_names.split(/\s+/).map do |name|
        Tag.find_or_create_by_name(name)
      end
    end
  end
end
<!-- articles/_form.html.erb -->
<p>
  <%= f.label :tag_names %><br />
  <%= f.text_field :tag_names %>
</p>

RSS Feed for Episode Comments 27 comments

1. Marellana Jun 22, 2009 at 00:07

Nice screencast, very useful as always.


2. Nils R Jun 22, 2009 at 00:20

This will come in quite handy on the next project - as always with your screencasts.

<3


3. RailsCasts Fan Jun 22, 2009 at 00:24

Thank you for your great screencast, it will be useful to me shortly. :)


4. Kieran P Jun 22, 2009 at 00:31

Another great episode. Hope you feel better soon.


5. J Jun 22, 2009 at 00:58

Thanks for this great episode :) However, I did have one question. What happens if the virtual attribute model fails validation?


6. Madan Kumar Rajan Jun 22, 2009 at 00:59

Dear Ryan,

This episode is as great as it always use to be. I have one doubt though...
You have defined task_names as writer. It means it wont be defining

def task_names=(name)
  @task_names = name
end

Then how does its value get assigned?

Thanks,
Madan Kumar Rajan


7. QuBiT Jun 22, 2009 at 01:18

and don't forget that this code also allows you to remove tags from an article.

and be aware of unused tags if you heavily add and remove tags, since only taggings are destroyed.


8. xi Jun 22, 2009 at 02:13

very good,thx


9. Vincent Bray Jun 22, 2009 at 02:20

Boo to sniffles, get well soon.

That's the first time I've ever seen f.error_messages.. How long has that been available?


10. hey Jun 22, 2009 at 05:24

Madan Kumar Rajan,

attr_writer creates that code automatically. Search about meta-programming :)


11. Robin Kaarsgaard Jun 22, 2009 at 07:30

This code could potentially lead to orphaned tags and taggings, couldn't it? I don't know what meta-magic goes on behind the stage, but the code for cleaning up after yourself does seem to be missing?

Otherwise, great job :)


12. Matthijs Langenberg Jun 22, 2009 at 10:47

Two years ago this stuff could be called complex, but Ryan, as you just say, today we are using it in all our everyday projects.

These days it really shouldn't be called hard or complex, we probably need a different approach to the problem.


13. Ryan Bates Jun 22, 2009 at 13:26

@J, good question. Here Tag doesn't have any validations (it is too simple), but if there are validations on the model you'll definitely need to take that into consideration.

One solution is to do "create!" (with a bang) to ensure an exception is raised if the validation fails. However this is not pretty to the end user. Unfortunately I don't know of an easy way to handle validations in a pretty way here.

@Vincent, f.error_messages has been around for quite some time, I think since Rails 2.0 but I'm not positive.

@Robin, It should not orphan any Tagging records (the tags= method takes care of this), but Tag models will continue to exist even if they do not have articles assigned to them.

I don't think that is much of an issue here, but there are a couple ways around this. One is to add an after_destroy hook on tagging to check if it is the last one and remove the tag as well. Alternatively you could set up an external recurring task to loop through all tags and remove empty ones.


14. Chris Gunther Jun 22, 2009 at 15:51

Great screencast.

Short but informative and to the point.

Chris


15. Cathal Jun 25, 2009 at 04:27

Thanks for this Ryan!

I'm trying to use virtual attributes on a model that maintains a 'duration' field, which is an integer of seconds. In my model I'd like to be able to represent this duration value as 'hours' & 'minutes'. The model saves correctly, however I can't seem to find a way to initialise hours & minutes from the database. Could you help?

http://pastie.org/524093


16. Johan Jun 25, 2009 at 09:31

Are you using :value for your textfields? :value=>@course.duration/3600


17. Jess Jun 26, 2009 at 06:40

AWESOME :D

Just when I was trying to remember how to do this, railscasts to the rescue


18. Felix Jun 27, 2009 at 15:35

Hi!
I like that stylesheet. Is it made by you and can I use it for my testing?

Keep up with the good work!
Felix


19. Karim Helal Jun 28, 2009 at 01:24

Man I love this site. Every single screencast has somehow been included in a project i'm working on, and this one is no exception :)

Now taking this further, how would i go about doing a Article.find_by_tag_name method or named_scope?

Any help would be greatly appreciated.

Thx.


20. Karim Helal Jun 28, 2009 at 02:06

Well i think i might have found a way.. I've created a named_scope in the Articles model:

named_scope :find_by_tag, lambda { |*args| {:include => :tags, :conditions => ["tags.name ILIKE ?", '%'+args.first+'%']} }

Not sure if it's the best way (probably not) but seems to work... any better ideas?


21. einarmagnus Jun 30, 2009 at 22:35

Karim:
sorry if this is not what you are looking for but what about:

def find_by_tag name
Tag.find_by_name(name).articles
end


22. Kevin Kaske Jul 22, 2009 at 09:50

Thanks for making quality screen casts! They are always a big help!


23. Mike Larkin Aug 25, 2009 at 14:46

I'm attempting to clean up orphaned tags by using a after_destroy hook, but it doesn't seem to even be fired?

Does the virtual writer not fire the destroy events?

Thanks!


24. Jon Aug 30, 2009 at 03:09

Hi Ryan, would it not be better to use a before_save callback to call assign_tags, and then use find_or_initialize_by_name instead of find_or_create_by_name. That way, the tags all get saved in one atomic action.


25. Brad Sep 11, 2009 at 05:39

I have used this code in my own app but took it one step further by adding a counter cache to the tags table called taggings_count. This is where the problems start. It increments the count when taggings are added but does not decrement the count when taggings are deleted. Any suggestions would be appreciated.


26. Andreyka Oct 17, 2009 at 00:40

Thanks for topic it's help me very much


27. nickmenow Oct 30, 2009 at 21:32

Great cast. Great site. I wake up on Mondays ready to watch another cast.

I recently needed to start using tagging in my application, so I immediately came to railscasts.com

Instead of articles, my model is entry. Companies have many entries. Entries have many tags. A tag belongs to a company. A user belongs to a company. I would like to track the tags by company by adding a company_id to the tag table. What code would I need to modify to be able to add the company_id that is a hidden field in the form?


28. Jake Jan 11, 2010 at 21:33

@Brad. I have the same problem with decrementing the counters. I even tried adding before_destroy and after_destroy to the taggings, but they don't even get called. I guess that they are deleted and not destroyed. Thoughts Ryan?


29. Jake Jan 14, 2010 at 21:00

I managed to get the counter caches to decrement. It's ugly and probably excessive. But it does work.

def assign_tags
 if @tag_names
  new_tags = @tag_names.split(/\s+/).map do |name|
   Tag.find_or_create_by_name(name)
  end
  # destroy taggings to decrement counter cache
  self.taggings.each do |t|
    t.destroy if !new_tags.collect(&:id).include?(t.tag_id)
  end
  # assign any new tags
  self.tags = new_tags
 end
end


30. Igorek Apr 09, 2010 at 21:31

Yes, it's so.


31. Iraida Apr 11, 2010 at 00:02

Thanks for share great casts!


32. Tokito Apr 20, 2010 at 21:54

I'm download. Thanks


33. louis vuitton wallets Jul 14, 2010 at 01:36

great!!!


34. iPhone Ringtone Maker for mac Jul 20, 2010 at 19:10

this is a a


35. cij Jul 29, 2010 at 03:05

G5=L ?>=@028;AO A09B. 5?;>E>5 >D>@<;5=85. ABL GB> ?>G8B0BL. #A?5E>2 2 @0728B88


36. Leo Jul 30, 2010 at 01:52

I got a question here, how to do this tagging thing with a polymorphic association? for example:

Article has_many :taggings, :as => 'taggable', :dependent => :destroy

Article has_many :tags, :through => 'taggings'

-------------------------------

Taggings belongs_to :taggable, :polymorphic => true

Taggings belongs_to :tags

-------------------------------

Tag has_many :taggings

Im getting an error with the setter method "Could not find the association "taggings" in model Article". I think something wrong with "Article has_many :tags, :through => 'taggings'", but how to correct this, Please help thx


37. free directory list Aug 11, 2010 at 22:36

Very useful information!


38. Nike Sb Dunks Aug 19, 2010 at 00:14

Thanks for share great casts! I like that stylesheet. Is it made by you and can I use it for my testing?


39. wholesale new era hats Aug 20, 2010 at 20:32

These are wonderful! Thank you for finding and sharing


40. Wholesale Electronics Aug 25, 2010 at 01:53

Discount Wholesale Electronics, Wholesale Cell Phones, Electronic Gadgets and More from the Best Dropship Wholesaler


41. louis vuitton shoes Aug 26, 2010 at 21:17

Thanks for sharing your article. I really enjoyed it. I put a link to my site to here so other people can read it. My readers have about the same interets


42. Dual Card Cell Phones Aug 30, 2010 at 20:16

wow, nice post, i like it very much.


43. snow boots Aug 30, 2010 at 20:56

Im getting an error with the setter method "Could not find the association "taggings" in model Article". I think something wrong with "Article has_many :tags, :through => 'taggings'", but how to correct this, Please help thx


44. louis vuitton sunglasses Sep 01, 2010 at 22:38

Extremely great post, really beneficial stuff. Never thought I’d find the facts I would like in this article. I’ve been looking all over the net for some time now and was starting to get irritated.

Add your comment:

(SKIP THIS ONE)

(required)

(not shown)


(use pastie or gist for code)

sponsored by:
if you want to help:
required:
Get Quicktime Player
Give Back to Open Source