A good, simple, solid tagging extension for ActiveRecord.
This was initially built partly as a proof-of-concept, partly to see how a tagging gem could work when it's not all stuffed within models, and partly just because I wanted a simpler tagging library. It's now a solid little tagging Rails engine.
If you want to know more, read this blog post, or have a look at the Examples page in the wiki (which includes a starting point for accepting tag values in a form).
The first step is easy: add the tag associations to whichever models should have tags (in these examples, the Article model):
class Article < ActiveRecord::Base
# ...
Gutentag::ActiveRecord.call self
# ...
end
That's all it takes to get a tags association on each article. Of course, populating tags can be a little frustrating, unless you want to manage Gutentag::Tag instances yourself? As an alternative, just use the tag_names accessor to get/set tags via string representations.
article.tag_names #=> ['pancakes', 'melbourne', 'ruby']
article.tag_names << 'portland'
article.tag_names #=> ['pancakes', 'melbourne', 'ruby', 'portland']
article.tag_names -= ['ruby']
article.tag_names #=> ['pancakes', 'melbourne', 'portland']
Changes to tag_names are not persisted immediately - you must save your taggable object to have the tag changes reflected in your database:
article.tag_names << 'ruby'
article.save
You can also query for instances with specified tags. The default :match
mode is :any
, and so provides OR logic, not AND - it'll match any instances that have any of the tags or tag names:
Article.tagged_with(:names => ['tag1', 'tag2'], :match => :any)
Article.tagged_with(
:tags => Gutentag::Tag.where(name: ['tag1', 'tag2']),
:match => :any
)
Article.tagged_with(:ids => [tag_id], :match => :any)
To return records that have all specified tags, use :match => :all
:
# Returns all articles that have *both* tag_a and tag_b.
Article.tagged_with(:ids => [tag_a.id, tag_b.id], :match => :all)
To return records that have none of the specified tags, use :match => :none
:
# Returns all articles that have *neither* tag_a nor tag_b.
Article.tagged_with(:ids => [tag_a.id, tag_b.id], :match => :none)
To return all tag names used by an instance of a model or relation
# Returns array of tag names
Gutentag::Tag.names_for_scope(Article)
# => ['tag1', 'tag2', 'tag3']
Gutentag::Tag.names_for_scope(Article.where(:created_at => 1.week.ago..1.second.ago))
# => ['tag3']
# Return array of the tag names used from the two most recent articles
Gutentag::Tag.names_for_scope(Article.order(created_at: :desc).limit(2))
# => []
These are the versions the test suite runs against. It's possible it may work on older versions of Ruby, but it definitely won't work on older versions of Rails.
- Ruby: MRI v2.3-v3.1, JRuby v9.2.5
- Rails/ActiveRecord: v4.0-v7.0
If you're using MRI v2.2 and/or ActiveRecord v3.2, the last version of Gutentag that fully supported those versions is v2.4.1.
Get it into your Gemfile - and don't forget the version constraint!
gem 'gutentag', '~> 2.6'
Next: your tags get persisted to your database, so let's import the migrations, update them to your current version of Rails, and then migrate:
bundle exec rake gutentag:install:migrations
bundle exec rails generate gutentag:migration_versions
bundle exec rake db:migrate
If you're using UUID primary keys, make sure you alter the migration files before running db:migrate
to use UUIDs for the taggable_id
foreign key column (as noted in issue 57.)
If you want to use Gutentag outside of Rails, you can. However, there is one caveat: You'll want to set up your database with the same schema (as importing in the migrations isn't possible without Rails). The schema from 0.7.0 onwards is below:
create_table :gutentag_tags do |t|
t.string :name, null: false, index: {unique: true}
t.bigint :taggings_count, null: false, index: true, default: 0
t.timestamps null: false
end
create_table :gutentag_taggings do |t|
t.references :tag, null: false, index: true, foreign_key: {to_table: :gutentag_tags}
t.references :taggable, null: false, index: true, polymorphic: true
t.timestamps null: false
end
add_index :gutentag_taggings, [:taggable_type, :taggable_id, :tag_id], unique: true, name: "gutentag_taggings_uniqueness"
Please refer to the CHANGELOG, which covers significant and breaking changes between versions.
Gutentag tries to take a convention-over-configuration approach, while also striving to be modular enough to allow changes to behaviour in certain cases.
The default validations on Gutentag::Tag
are:
- presence of the tag name.
- case-insensitive uniqueness of the tag name.
- maximum length of the tag name (if the column has a limit).
You can view the logic for this in Gutentag::TagValidations
, and you can set an alternative if you wish:
Gutentag.tag_validations = CustomTagValidations
The supplied value must respond to call
, and the argument supplied is the model.
Tag normalisation is used to convert supplied tag values consistently into string tag names. The default is to convert the value into a string, and then to lower-case.
If you want to do something different, provide an object that responds to call and accepts a single value to Gutentag.normaliser
:
Gutentag.normaliser = lambda { |value| value.to_s.upcase }
Gutentag ignores case by default, but can be customised to be case-sensitive by supplying your own validations and normaliser, as outlined by Robin Mehner in issue 42. Further changes may be required for your schema though, depending on your database.
If you need to extend Gutentag's models, you will need to wrap the include
inside a to_prepare
hook to ensure it's loaded consistently in all Rails environments:
# config/initializers/gutentag.rb or equivalent
Rails.application.config.to_prepare do
Gutentag::Tag.include TagExtensions
end
Further discussion and examples of this can be found in issue 65.
Please note that this project now has a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.
Copyright (c) 2013-2022, Gutentag is developed and maintained by Pat Allan, and is released under the open MIT Licence.