- Notifications
You must be signed in to change notification settings - Fork1.2k
A tagging plugin for Rails applications that allows for custom tagging along dynamic contexts.
License
mbleigh/acts-as-taggable-on
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
Table of Contentsgenerated withDocToc
This plugin was originally based on Acts as Taggable on Steroids by Jonathan Viney.It has evolved substantially since that point, but all credit goes to him for theinitial tagging functionality that so many people have used.
For instance, in a social network, a user might have tags that are called skills,interests, sports, and more. There is no real way to differentiate between tags andso an implementation of this type is not possible with acts as taggable on steroids.
Enter Acts as Taggable On. Rather than tying functionality to a specific keyword(namelytags
), acts as taggable on allows you to specify an arbitrary number oftag "contexts" that can be used locally or in combination in the same way steroidswas used.
To use it, add it to your Gemfile:
gem'acts-as-taggable-on'
and bundle:
bundle
Install migrations
# For the latest versions :rake acts_as_taggable_on_engine:install:migrations
Review the generated migrations then migrate :
rake db:migrate
If you do not wish or need to support multi-tenancy, the migration foradd_tenant_to_taggings
is optional and can be discarded safely.
You can circumvent at any time the problem of special charactersissue 623 by setting in an initializer file:
ActsAsTaggableOn.force_binary_collation=true
Or by running this rake task:
rake acts_as_taggable_on_engine:tag_names:collate_bin
See the Configuration section for more details, and a general note valid for olderversion of the gem.
Setup
classUser <ActiveRecord::Baseacts_as_taggable_on:tagsacts_as_taggable_on:skills,:interests#You can also configure multiple tag types per modelendclassUsersController <ApplicationControllerdefuser_paramsparams.require(:user).permit(:name,:tag_list)## Rails 4 strong params usageendend@user=User.new(:name=>"Bobby")
Add and remove a single tag
@user.tag_list.add("awesome")# add a single tag. alias for <<@user.tag_list.remove("awesome")# remove a single tag@user.save# save to persist tag_list
Add and remove multiple tags in an array
@user.tag_list.add("awesome","slick")@user.tag_list.remove("awesome","slick")@user.save
You can also add and remove tags in format of String. This wouldbe convenient in some cases such as handling tag input param in a String.
Pay attention you need to addparse: true
as option in this case.
You may also want to take a look at delimiter in the string. The defaultis comma,
so you don't need to do anything here. However, if you madea change on delimiter setting, make sure the string will match. Seeconfiguration for more about delimiter.
@user.tag_list.add("awesome, slick",parse:true)@user.tag_list.remove("awesome, slick",parse:true)
You can also add and remove tags by direct assignment. Note this willremove existing tags so use it with attention.
@user.tag_list="awesome, slick, hefty"@user.save@user.reload@user.tags=>[#<ActsAsTaggableOn::Tag id: 1, name: "awesome", taggings_count: 1>,#<ActsAsTaggableOn::Tag id: 2, name: "slick", taggings_count: 1>,#<ActsAsTaggableOn::Tag id: 3, name: "hefty", taggings_count: 1>]
With the defined context in model, you have multiple new methods at disposalto manage and view the tags in the context. For example, with:skill
contextthese methods are added to the model:skill_list
(andskill_list.add
,skill_list.remove
skill_list=
),skills
(plural),skill_counts
.
@user.skill_list="joking, clowning, boxing"@user.save@user.reload@user.skills=>[#<ActsAsTaggableOn::Tag id: 1, name: "joking", taggings_count: 1>,#<ActsAsTaggableOn::Tag id: 2, name: "clowning", taggings_count: 1>,#<ActsAsTaggableOn::Tag id: 3, name: "boxing", taggings_count: 1>]@user.skill_list.add("coding")@user.skill_list# => ["joking", "clowning", "boxing", "coding"]@another_user=User.new(:name=>"Alice")@another_user.skill_list.add("clowning")@another_user.saveUser.skill_counts=>[#<ActsAsTaggableOn::Tag id: 1, name: "joking", taggings_count: 1>,#<ActsAsTaggableOn::Tag id: 2, name: "clowning", taggings_count: 2>,#<ActsAsTaggableOn::Tag id: 3, name: "boxing", taggings_count: 1>]
To preserve the order in which tags are created useacts_as_ordered_taggable
:
classUser <ActiveRecord::Base# Alias for acts_as_ordered_taggable_on :tagsacts_as_ordered_taggableacts_as_ordered_taggable_on:skills,:interestsend@user=User.new(:name=>"Bobby")@user.tag_list="east, south"@user.save@user.tag_list="north, east, south, west"@user.save@user.reload@user.tag_list# => ["north", "east", "south", "west"]
You can find the most or least used tags by using:
ActsAsTaggableOn::Tag.most_usedActsAsTaggableOn::Tag.least_used
You can also filter the results by passing the method a limit, however the default limit is 20.
ActsAsTaggableOn::Tag.most_used(10)ActsAsTaggableOn::Tag.least_used(10)
Acts As Taggable On uses scopes to create an association for tags.This way you can mix and match to filter down your results.
classUser <ActiveRecord::Baseacts_as_taggable_on:tags,:skillsscope:by_join_date,order("created_at DESC")endUser.tagged_with("awesome").by_join_dateUser.tagged_with("awesome").by_join_date.paginate(:page=>params[:page],:per_page=>20)# Find users that matches all given tags:# NOTE: This only matches users that have the exact set of specified tags. If a user has additional tags, they are not returned.User.tagged_with(["awesome","cool"],:match_all=>true)# Find users with any of the specified tags:User.tagged_with(["awesome","cool"],:any=>true)# Find users that have not been tagged with awesome or cool:User.tagged_with(["awesome","cool"],:exclude=>true)# Find users with any of the tags based on context:User.tagged_with(['awesome','cool'],:on=>:tags,:any=>true).tagged_with(['smart','shy'],:on=>:skills,:any=>true)
You now have the following options for prefix, suffix and containment search, along with:any
or:exclude
option.Usewild: :suffix
to place a wildcard at the end of the tag. It will be looking forawesome%
andcool%
in SQL.Usewild: :prefix
to place a wildcard at the beginning of the tag. It will be looking for%awesome
and%cool
in SQL.Usewild: true
to place a wildcard both at the beginning and the end of the tag. It will be looking for%awesome%
and%cool%
in SQL.
Tip:User.tagged_with([])
orUser.tagged_with('')
will return[]
, an empty set of records.
You can find objects of the same type based on similar tags on certain contexts.Also, objects will be returned in descending order based on the total number ofmatched tags.
@bobby=User.find_by_name("Bobby")@bobby.skill_list# => ["jogging", "diving"]@frankie=User.find_by_name("Frankie")@frankie.skill_list# => ["hacking"]@tom=User.find_by_name("Tom")@tom.skill_list# => ["hacking", "jogging", "diving"]@tom.find_related_skills# => [<User name="Bobby">, <User name="Frankie">]@bobby.find_related_skills# => [<User name="Tom">]@frankie.find_related_skills# => [<User name="Tom">]
In addition to the generated tag contexts in the definition, it is also possibleto allow for dynamic tag contexts (this could be user generated tag contexts!)
@user=User.new(:name=>"Bobby")@user.set_tag_list_on(:customs,"same, as, tag, list")@user.tag_list_on(:customs)# => ["same", "as", "tag", "list"]@user.save@user.tags_on(:customs)# => [<Tag name='same'>,...]@user.tag_counts_on(:customs)User.tagged_with("same",:on=>:customs)# => [@user]
You can find tags for a specific context by using thefor_context
scope:
ActsAsTaggableOn::Tag.for_context(:tags)ActsAsTaggableOn::Tag.for_context(:skills)
If you want to change how tags are parsed, you can define your own implementation:
classMyParser <ActsAsTaggableOn::GenericParserdefparseActsAsTaggableOn::TagList.new.tapdo |tag_list|tag_list.add@tag_list.split('|')endendend
Now you can use this parser, passing it as parameter:
@user=User.new(:name=>"Bobby")@user.tag_list="east, south"@user.tag_list.add("north|west",parser:MyParser)@user.tag_list# => ["north", "east", "south", "west"]# Or also:@user.tag_list.parser=MyParser@user.tag_list.add("north|west")@user.tag_list# => ["north", "east", "south", "west"]
Or change it globally:
ActsAsTaggableOn.default_parser=MyParser@user=User.new(:name=>"Bobby")@user.tag_list="east|south"@user.tag_list# => ["east", "south"]
Tags can have owners:
classUser <ActiveRecord::Baseacts_as_taggerendclassPhoto <ActiveRecord::Baseacts_as_taggable_on:locationsend@some_user.tag(@some_photo,:with=>"paris, normandy",:on=>:locations)@some_user.owned_taggings@some_user.owned_tagsPhoto.tagged_with("paris",:on=>:locations,:owned_by=>@some_user)@some_photo.locations_from(@some_user)# => ["paris", "normandy"]@some_photo.owner_tags_on(@some_user,:locations)# => [#<ActsAsTaggableOn::Tag id: 1, name: "paris">...]@some_photo.owner_tags_on(nil,:locations)# => Ownerships equivalent to saying @some_photo.locations@some_user.tag(@some_photo,:with=>"paris, normandy",:on=>:locations,:skip_save=>true)#won't save @some_photo object
Note thattag_list
only returns tags whose taggings do not have an owner. Continuing from the above example:
@some_photo.tag_list# => []
To retrieve all tags of an object (regardless of ownership) or if only one owner can tag the object, useall_tags_list
.
Note thatowned tags are added all at once, in the form ofcomma separated tags in string.Also, when you try to addowned tags again, it simply overwrites the previous set ofowned tags.So to append tags in previously existingowned tags list, go as follows:
defadd_owned_tag@some_item=Item.find(params[:id])owned_tag_list=@some_item.all_tags_list -@some_item.tag_listowned_tag_list +=[(params[:tag])]@tag_owner.tag(@some_item,:with=>stringify(owned_tag_list),:on=>:tags)@some_item.saveenddefstringify(tag_list)tag_list.inject(''){ |memo,tag|memo +=(tag +',')}[0..-1]end
Similarly as above, removing will be as follows:
defremove_owned_tag@some_item=Item.find(params[:id])owned_tag_list=@some_item.all_tags_list -@some_item.tag_listowned_tag_list -=[(params[:tag])]@tag_owner.tag(@some_item,:with=>stringify(owned_tag_list),:on=>:tags)@some_item.saveend
Tags support multi-tenancy. This is useful for applications where a Tag belongs to a scoped set of models:
classAccount <ActiveRecord::Basehas_many:photosendclassUser <ActiveRecord::Basebelongs_to:accountacts_as_taggable_on:tagsacts_as_taggable_tenant:account_idend@user1.tag_list=["foo","bar"]# these taggings will automatically have the tenant saved@user2.tag_list=["bar","baz"]ActsAsTaggableOn::Tag.for_tenant(@user1.account.id)# returns Tag models for "foo" and "bar", but not "baz"
@bobby=User.find_by_name("Bobby")@bobby.skill_list# => ["jogging", "diving"]@bobby.skill_list_changed?#=> false@bobby.changes#=> {}@bobby.skill_list="swimming"@bobby.changes.should =={"skill_list"=>["jogging, diving",["swimming"]]}@bobby.skill_list_changed?#=> true@bobby.skill_list_change.should ==["jogging, diving",["swimming"]]
To construct tag clouds, the frequency of each tag needs to be calculated.Because we specifiedacts_as_taggable_on
on theUser
class, we canget a calculation of all the tag counts by usingUser.tag_counts_on(:customs)
. But what if we wanted a tag count fora single user's posts? To achieve this we call tag_counts on the association:
User.find(:first).posts.tag_counts_on(:tags)
A helper is included to assist with generating tag clouds.
Here is an example that generates a tag cloud.
Helper:
modulePostsHelperincludeActsAsTaggableOn::TagsHelperend
Controller:
classPostController <ApplicationControllerdeftag_cloud@tags=Post.tag_counts_on(:tags)endend
View:
<% tag_cloud(@tags, %w(css1 css2 css3 css4)) do |tag, css_class|%><%= link_to tag.name, { :action => :tag, :id => tag.name }, :class => css_class%><% end%>
CSS:
.css1 {font-size:1.0em; }.css2 {font-size:1.2em; }.css3 {font-size:1.4em; }.css4 {font-size:1.6em; }
If you would like to remove unused tag objects after removing taggings, add:
ActsAsTaggableOn.remove_unused_tags=true
If you want force tags to be saved downcased:
ActsAsTaggableOn.force_lowercase=true
If you want tags to be saved parametrized (you can redefine to_param as well):
ActsAsTaggableOn.force_parameterize=true
If you would like tags to be case-sensitive and not use LIKE queries for creation:
ActsAsTaggableOn.strict_case_match=true
If you would like to have an exact match covering special characters with MySql:
ActsAsTaggableOn.force_binary_collation=true
If you would like to specify table names:
ActsAsTaggableOn.tags_table='aato_tags'ActsAsTaggableOn.taggings_table='aato_taggings'
If you want to change the default delimiter (it defaults to ','). You can also pass in an array of delimiters such as ([',', '|']):
ActsAsTaggableOn.delimiter=','
NOTE 1: SQLite by default can't upcase or downcase multibyte characters, resulting in unwanted behavior. Load the SQLite ICU extension for proper handle of such characters.See docs
NOTE 2: the optionforce_binary_collation
is strongest thanstrict_case_match
and whenset to true, thestrict_case_match
is ignored.To roughly apply theforce_binary_collation
behaviour with a version of the gem <= 3.4.4, execute the following commands in the MySql console:
USE my_wonderful_app_db;ALTER TABLE tags MODIFY name VARCHAR(255) CHARACTER SET utf8 COLLATE utf8_bin;
seeUPGRADING
We have a long list of valued contributors.Check them all
Versions 2.x are compatible with Ruby 1.8.7+ and Rails 3.
Versions 2.4.1 and up are compatible with Rails 4 too (thanks to arabonradar and cwoodcox).
Versions >= 3.x are compatible with Ruby 1.9.3+ and Rails 3 and 4.
Versions >= 4.x are compatible with Ruby 2.0.0+ and Rails 4 and 5.
Versions >= 7.x are compatible with Ruby 2.3.7+ and Rails 5 and 6.
Versions >= 8.x are compatible with Ruby 2.3.7+ and Rails 5 and 6.
Versions >= 9.x are compatible with Ruby 2.5.0 and Rails 6 and 7.0.
Versions >= 11.x are compatible with Ruby 3.1.0 and Rails 7.0 and 7.1.
Versions >= 12.x are compatible with Ruby 3.2.0 and Rails 7.1, 7.2 and 8.0.
For an up-to-date roadmap, seehttps://github.com/mbleigh/acts-as-taggable-on/milestones
Acts As Taggable On uses RSpec for its test coverage. Inside the gemdirectory, you can run the specs with:
bundlerake spec
You can run all the tests across all the Rails versions by runningrake appraise
.If you'd also like torun the tests across all rubies and databases as configured for Github Actions, install and runwwtd
.
SeeLICENSE
About
A tagging plugin for Rails applications that allows for custom tagging along dynamic contexts.