Rails Plugin: dynamically_tags

August 17, 2008 – 3:06 pm

Background

I wanted a way to allow a user to enter text into a field, and have those fields automatically link to existing objects based on keywords. For example, a user types a comment into a blog: “I think Chicago is a great city.” Somewhere in the database, there’s a City object with a :name field that contains “Chicago”. I want my application to realize this, and then DO something with that information. Most importantly, I want that linkage to persist, even if changes are made to the Chicago object. And, finally, I don’t want my application to have to scrape through ever bit of rendered text and compare it to every existing object each time that someone requests that information.

Solution: dynamically_tags

I wrote a Rails plugin called dynamically_tags that, as it says, dynamically tags bits of text that match designated fields in external models. It does this by substituting out the actual text and replacing it with a tagged reference to the other model.

For the example above, the BlogComment object might contain:

dynamically_tags [:content], :includes => {:cities => :name}

Then, given the input “I think Chicago is a great city.”, the actual content of the BlogComment would be replaced with a dynamic tag.

>> comment = BlogComment.new(:content => "I think Chicago is a great city.")
=> #<BlogComment id: nil, content: "I think {{City1}} is a great city.">

And, just like that, we’ve tagged out the actual text of the city and replaced it with a reference to the City object. If you try to access that :content field again, the plugin substitutes the :name attribute from the City. If you pass true to the recall method, it will give you the raw, tagged data.

>> comment.content
=> "I think Chicago is a great city."
>> comment.content(true)
=> "I think {{City1}} is a great city."

This allows us to access referenced objects based on these tags. The get_tagged_objects method gives us an array of all objects referenced in this way. You can pass a class-type as an argument to specify only objects of that type to be returned.

>> comment.get_tagged_objects
=> [#<City id: 1, name: "Chicago">]
comment.get_tagged_objects(City)
=> [#<City id: 1, name: "Chicago">]
comment.get_tagged_objects(Country)
=> []

Onward!

The plugin can be installed using:

./script/plugin install git://github.com/jcrystal/dynamically_tags.git

Below is the content of the README for the plugin which contains some more advanced examples of using this, such as basic scoping and multi-word searches. Take a look. As with most things open source, this is a work in progress. I’d appreciate any comments folks have on this, and will be improving it over time.

The README

(please see the git repository for the most current version of this)

This plugin allows your models to dynamically insert references to other objects on the fly based on the content of a text field.

Let’s say you have a web site that allows people to keep track of their favorite pizza toppings (a pretty lame site, but whatever) and people can post their thoughts and comments. (e.g. “Dude, olives are totally gross.”)

class User < ActiveRecord::Base
	# User fields: firstname, lastname
	has_many :posts
end
 
class Topping < ActiveRecord::Base
	# Topping fields: name, description
end
 
class Post < ActiveRecord::Base
	# Post fields: content, user_id
	belongs_to :user
end

OK, now our example’s all set up. If the above doesn’t make sense, now is your time to flee.

A Basic Example

This is similar to the Pizza example above. Every time someone mentions the name of a pizza topping in a post, we want to have that topping referenced (maybe for an image-rollover or something).

class Post < ActiveRecord::Base
	belongs_to :user
	dynamically_tags [:content], :includes => {:toppings => [:name]}, :scope => :all
end
>> Topping.create(:name => 'olives')
=> #<Topping id: 1, name: "olives">
 
>> p = Post.new(:content => 'I think olives are really gross!')
=> #<Post id: nil, user_id: nil, content: "I think {{Topping1}} are really gross!">
 
>> p.content
=> "I think olives are really gross!"
 
>> p.content(true) # pass true to the accessor method to get the raw, tagged string
=> "I think {{Topping3}} are really gross!"

More Advanced Example

Tag places where the content includes a firstname followed by a lastname, as well as toppings.

class Post < ActiveRecord::Base
	belongs_to :user
	dynamically_tags [:content], :includes => {:users => ['firstname lastname'], :toppings => [:name]}, :scope => :all
	# Note that this currently only works with space-separated strings, as above
end
 
class User < ActiveRecord::Base
	# User fields: firstname, lastname
	has_many :posts
 
	def name # We need this so dynamically_tags knows what to sub-in for the tag
		self.firstname + " " + self.lastname
	end
  end
>> User.create(:firstname => 'Jason', :lastname => 'Crystal')
=> #<User id: 1, firstname: "Jason", lastname: "Crystal">
 
>> Topping.create(:name => 'olives')
=> #<Topping id: 1, name: "olives">
 
>> p = Post.new(:content => "I strongly disagree with Jason Crystal.  I think olives are delicious!.")
=> #<Post id: nil, user_id: nil, content: "I strongly disagree with {{User1}}.  I think {{Topp...">
 
>> p.content
=> "I strongly disagree with Jason Crystal.  I think olives are delicious!."
 
>> p.content(true)
=> "I strongly disagree with {{User1}}.  I think {{Topping1}} are delicious!."
 
>> p.get_tagged_objects
=> [#<User id: 1, firstname: "Jason", lastname: "Crystal">, #<Topping id: 3, name: "olives">]
 
>> p.get_tagged_objects(Topping) # get all tagged toppings
=> [#<Topping id: 3, name: "olives"]

Note that if Jason Crystal suddenly decides he wants to change his name to Jeezy Chreezy, the {{User1}} tag will STILL properly point to the correct object!

One More Example

Look for firstname lastname combinations, but only within Post’s social_circle.

class Post < ActiveRecord::Base
	# Post fields: content, social_circle_id, user_id
	belongs_to :user
	belongs_to :social_circle
 
	dynamically_tags [:content], :includes => {:users => ['firstname lastname']}, :scope => :social_circle
end
 
class User < ActiveRecord::Base
	# User fields: firstname, lastname
 
	has_many :posts
	belongs_to :social_circle
 
	def name
		self.firstname + " " + self.lastname
	end
end
 
class SocialCircle < ActiveRecord::Base
	has_many :posts
	has_many :users
end
>> good_circle = SocialCircle.create(:name => "Jason's Friends")
=> #<SocialCircle id: 1, name: "Jason's Friends">
>> bad_circle = SocialCircle.create(:name => "Jason's Enemies")
=> #<SocialCircle id: 2, name: "Jason's Enemies">
 
>> User.create(:social_circle => good_circle, :firstname => 'Jason', :lastname => 'Crystal')
=> #<User id: 2, firstname: "Jason", lastname: "Crystal", social_circle_id: 1>
>> User.create(:social_circle => bad_circle, :firstname => "Evil", :lastname => "Villain")
=> #<User id: 3, firstname: "Evil", lastname: "Villain", social_circle_id: 2>
 
>> p = Post.new(:social_circle => good_circle, :content => 'I agree with Jason Crystal!')
=> #<Post id: nil, user_id: nil, content: "I agree with {{User2}}!", social_circle_id: 1>
>> p = Post.new(:social_circle => bad_circle, :content => 'I agree with Jason Crystal!')
=> #<Post id: nil, user_id: nil, content: "I agree with Jason Crystal!", social_circle_id: 2>

Since Jason Crystal is not in bad_circle, it was not tagged out because of the :scope parameter.

The end!

This is a work in progress. As always, any feedback is welcome!

  1. 4 Responses to “Rails Plugin: dynamically_tags”

  2. This is an interesting looking plugin. I could see this being very useful in a site where certain keywords need to link to a glossary of terms.

    By Tom Beddard on Aug 20, 2008

  3. ruby script/plugin install git://github.com/jcrystal/dynamically_tags.git

    not able to install plugin form this site !

    By dharmdeep on Aug 25, 2008

  4. Interesting. It seems to work for me. Do you receive an errors of some sort?

    By Jason on Aug 25, 2008

  1. 1 Trackback(s)

  2. Aug 18, 2008: Post » Blog Archive » Open-Sourcing our Portal: dynamically_tags

Post a Comment