Giter VIP home page Giter VIP logo

gutentag's Introduction

Gutentag

Gem Version Build Status Code Climate

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).

Contents

Usage

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))
# => []

Installation

Dependencies

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.

Installing

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.)

Without Rails

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"

Upgrading

Please refer to the CHANGELOG, which covers significant and breaking changes between versions.

Configuration

Gutentag tries to take a convention-over-configuration approach, while also striving to be modular enough to allow changes to behaviour in certain cases.

Tag validations

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

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 }

Case-sensitive tags

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.

Extending

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.

Contribution

Please note that this project now has a Contributor Code of Conduct. By participating in this project you agree to abide by its terms.

Licence

Copyright (c) 2013-2022, Gutentag is developed and maintained by Pat Allan, and is released under the open MIT Licence.

gutentag's People

Contributors

0tsuki avatar grosser avatar jduff avatar kenips avatar laleshii avatar mtrolle avatar notapatch avatar olleolleolle avatar om-nishu-trantor avatar pat avatar rajror avatar rasmachineman avatar rmehner avatar rromanchuk avatar seelensonne avatar smo921 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

gutentag's Issues

Should `tag_names` be assignable on new?

The current validation requires tagging to have a valid belongs_to taggable. This seems to prevent assigning tag_names on new object, which is allowed by other libraries such as acts-as-taggable-on. Just wondering if this will be supported soon?

Can't get list of tags for a given class

I'm unable to get an array of tags scoped by model as specified by the README:

In my model:

  Gutentag::ActiveRecord.call self
  def tags_as_string
    tag_names.join(", ")
  end
  def tags_as_string=(string)
    self.tag_names = string.split(/,\s*/)
  end

In my view:
Gutentag::Tag.names_for_scope(MyClassName)

Resulting Error:

undefined method names_for_scope' for #Class:0x00007fbf91d1df28`

Tag.names_for_scope when scope is empty

I'm using Tag.names_for_scope on a multi-tenant project, and getting results that I didn't expect when a given tenant has no tags. Here's a spec showing the issue:

it "returns an empty array for an empty scope" do
  Article.create :title => "mammals", :tag_names => %w[ koala wombat ]
  Article.create :title => "birds",   :tag_names => %w[ cassowary ]

  expect(Gutentag::Tag.names_for_scope(Article.where(:title => "reptiles"))).
    to match_array([])
end

When run, this produces:

  1) Tag names for scopes returns an empty array for an empty scope
     Failure/Error:
       expect(Gutentag::Tag.names_for_scope(Article.where(:title => "reptiles"))).
         to match_array([])

       expected collection contained:  []
       actual collection contained:    ["cassowary", "koala", "wombat"]
       the extra elements were:        ["cassowary", "koala", "wombat"]
     # ./spec/lib/tag_names_for_scope_spec.rb:27:in `block (2 levels) in <top (required)>'

Before I work up a patch, I wanted to check whether this makes sense to you as a matter of design.

tag_names plucks multiple times

Have an existing Rails 7.0.3 app and installed that gem.

When calling a model with tag_names on an XYZ.last

Console puts out

Gutentag::Tag Pluck (1.2ms)  SELECT "gutentag_tags"."name" FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = $1 AND "gutentag_taggings"."taggable_type" = $2  [["taggable_id", 38], ["taggable_type", "XYZ"]]
Gutentag::Tag Pluck (0.3ms)  SELECT "gutentag_tags"."name" FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = $1 AND "gutentag_taggings"."taggable_type" = $2  [["taggable_id", 38], ["taggable_type", "XYZ"]]

Oddly when i do Gutentag::ActiveRecord.call self on the User-class (used with devise) and i do an User.last

User.last
User Load (1.1ms)  SELECT "users".* FROM "users" ORDER BY "users"."id" DESC LIMIT $1  [["LIMIT", 1]]
Gutentag::Tag Pluck (0.3ms)  SELECT "gutentag_tags"."name" FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = $1 AND "gutentag_taggings"."taggable_type" = $2  [["taggable_id", 4], ["taggable_type", "User"]]
Gutentag::Tag Pluck (0.2ms)  SELECT "gutentag_tags"."name" FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = $1 AND "gutentag_taggings"."taggable_type" = $2  [["taggable_id", 4], ["taggable_type", "User"]]

it calls tag_names directly. Seems the tag_names attribute is calculated on initialize. Did somebody had similar issues?
The second issue might be a different issue on my side, have to check wich addition causes that. Maybe paper_trail or something.

Gutentag creates 'empty string' tags

Hello, thanks for a nice gem!

Just noticed that Gutentag happily creates a tag with '' value.

I suggest adding a default validation for minimum length of 1. I cannot imagine using a tag with blank value.

These values are sometimes submitted from forms generated by Formtastic and similar gems - they add hidden field to make sure data is sent when all tags are cleared.

What do you think?

Update Record get rollback transaction

system:
ruby: 2.5.0
rails: 5.2.0

If I am update a record add tag_name 'never' and Gutentag::Tag never have 'never' then system be return rollback transaction.

2018-04-11 4 01 58

Can't handle UUIDs

I tried to use UUIDs for my taggable object resulting in

ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR:  invalid input syntax for integer: "b8cc337e-8535-4115-b782-e261c4cfa32a"
: SELECT "gutentag_tags".* FROM "gutentag_tags" INNER JOIN "gutentag_taggings" ON "gutentag_tags"."id" = "gutentag_taggings"."tag_id" WHERE "gutentag_taggings"."taggable_id" = $1 AND "gutentag_taggings"."taggable_type" = $2

trying to debug it now. found the problem.

Had to modify the generated migration from

create_table :gutentag_taggings do |t|
      t.integer :tag_id,        null: false
      t.integer :taggable_id,   null: false
      t.string  :taggable_type, null: false
      t.timestamps null: false
    end

to

create_table :gutentag_taggings do |t|
      t.integer :tag_id,        null: false
      t.uuid :taggable_id,   null: false
      t.string  :taggable_type, null: false
      t.timestamps null: false
    end

Adding an existing tag leaves the tag_names array out of sync with the DB

Environment

  • Rails 5.2.2
  • JRuby

Steps

If you use += on tag_names to add a tag that is already in tag_names then the tag_names array is out of sync with the DB:

irb(main):018:0> model.tag_names = ["tag1", "tag2"]
=> ["tag1", "tag2"]
irb(main):019:0> model.save!
=> true
irb(main):020:0> model.tag_names += ["tag2", "tag3"]
=> ["tag1", "tag2", "tag2", "tag3"]
irb(main):021:0> model.save!
=> true
irb(main):022:0> model.tag_names
=> ["tag1", "tag2", "tag2", "tag3"]
irb(main):023:0> model.reload
irb(main):024:0> model.tag_names
=> ["tag1", "tag2", "tag3"]

Possibly as designed. I worked around it by calling reload.

Tag names case sensitive

Hey there,

I have kind of a strange requirement, but it makes sense for the client: tag names have to be case sensitive and a tag named Ruby is not the same as ruby.

So here's what I did:

# config/initializers/gutentag.rb

# don't normalise tag names
Gutentag.normaliser = lambda { |name| name }

# overwrite validations, so uniqueness is not case insensitive as it is the default
class TagValidations
  def self.call(klass)
    new(klass).call
  end

  def initialize(klass)
    @klass = klass
  end

  def call
    klass.validates :name, presence: true, uniqueness: true, length: {maximum: 255}
  end

  private

  attr_reader :klass
end

Gutentag.tag_validations = TagValidations

Which works fine to a certain point, but there's a nuance in MySQL. Depending on the collation of the field (which often is utf8_general_ci, which is a default in many MySQL setups I have seen), the index might be case sensitive or not. If you set a unique index, like we do at

add_index :gutentag_tags, :name, :unique => true
MySQL will treat this index as case insensitive, meaning ruby and Ruby will be the same and therefore will throw an error.

To get it working, you have to change the collation to something that is case sensitive in MySQL (there's a whole bunch of docs about that here https://dev.mysql.com/doc/refman/5.7/en/case-sensitivity.html).

In my case, I wrote this migration:

class ChangeCollationOfGutentagTags < ActiveRecord::Migration[5.2]
  def up
    execute "ALTER TABLE `gutentag_tags` CONVERT TO CHARACTER SET UTF8 COLLATE utf8_bin"
  end

  def down
    execute "ALTER TABLE `gutentag_tags` CONVERT TO CHARACTER SET UTF8 COLLATE utf8_general_ci"
  end
end

and it now works like a charm :)

I don't know if Gutentag can do anything about this (unlikely), but at least this is documented somewhere now :)

Switching between act-as-taggable and gutentag.

Hi,

I'm using act-as-taggable gem but it has many bugs and no one seems to fix it. And I'm thinking about switching the gems. But I have questions.

  1. Can I use this with searchkick

To use this gem with searchkick I need to create a model which inherits from tag. See: https://github.com/ankane/searchkick#getting-started

Act-as-taggable has something like below. How can I do that with gutentag?

class Tag < ActsAsTaggableOn::Tag
  after_save do
    # magic
  end
end
  1. Can I use active record model callbacks?

When new tags added to table I'm broadcasting to clients so their apps keeping up to date. Actually this is same with question 1.

  1. Can I use this with paranoia

Users can delete their account and it means their posts and post tags also be deleted. But I want to keep tags. How can I do that?

Any way to scope tags more granularly than at the Model level?

I'm using Gutentag for a tagging feature in an application. The feature allows tagging of responses to a suggestion box.

Ideally, I'd like for the tag dropdown to only show tag names that have been used in a given box, as opposed to all tags used globally in the system. Looking at the docs, I'm not seeing a way to do that. Is there a way to use Gutentag to save/read tags scoped by a model ID?

Short tutorial

It would be nice to add a short rails tutorial in the readme, for example add tags red, green, blue to a products form :)

Searching tags with Ransack

I think this would be a common desire, so I'm posting here with my trouble. I'm loving how simple Gutentag is, adding tags is no problem. But I would like to search through them with Ransack.

Whether by adding tag_names or tags_as_string as a ransackable_attribute, I get a undefined method type' for nil:NilClass` error.

How can I return a proper tag attribute for Ransack to search for? Could you provide instructions for getting Gutentag to work with Ransack?

search for taggables that match all tags

tagged_with is a OR query, since it uses IN ... how about adding tagged_with_all ... or tagged_with x, all: true ?

code I'm using atm:

        scope = scope.joins(:taggings).where("gutentag_taggings.tag_id" => v)
        if v.size == 1
          scope.distinct
        else
          scope.having("count(repositories.id) = #{v.size}").group('repositories.id')
        end

obj.tag_names not supporting for String

In version, 0.5.0, we had support for setting tag_names with String, i.e. ar_obj.tag_names = 'test1'.

However, in later version, tag_names= is expecting array and throwing following exception for strings:

TypeError Exception: no implicit conversion of String into Array

Rails 5 not deleting tag with 0 tagging_count

Perhaps I'm missing something but when I have a model with 4 tags associated with it and then save the model with three tags. The fourth tag tagging_count goes to 0 but does not get deleted.

user.tag_names = [ 'one', 'two', 'three', 'four' ]
user.save!

user.tag_names = [ 'one', 'two', 'three' ]
user.save!

tag 'four' is still in the Tag table but has a count of 0. My expecation is that when a Tag goes to zero it is automatically deleted. Wrong assumption???

Very nice implementation, I'm transitioning from acts_as_taggable_on as it's 210k and too complex for such a simple task as tagging.

Thanks for the effort! - Art

eager_load tries to load tag_names on Rails 4.2

Under Rails 4.2, queries that eager-load a tagged model try to load a non-existent tag_names column from the underlying table.

e.g for the usual trivial blog models:

class Post < ActiveRecord::Base
  has_many :comments
  Gutentag::ActiveRecord.call(self)
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

calling Comment.eager_load(:post) or Comment.references(:post).includes(:post)generates the following SQL:

SELECT "comments"."id" AS t0_r0,
       "comments"."post_id" AS t0_r1,
       "comments"."body" AS t0_r2,
       "comments"."created_at" AS t0_r3,
       "comments"."updated_at" AS t0_r4,
       "posts"."id" AS t1_r0,
       "posts"."title" AS t1_r1,
       "posts"."body" AS t1_r2,
       "posts"."created_at" AS t1_r3,
       "posts"."updated_at" AS t1_r4,
       "posts"."tag_names" AS t1_r5
FROM "comments" LEFT OUTER JOIN "posts" ON "posts"."id" = "comments"."post_id"

The same queries work under Rails 5.2, so it seems to be a difference in the behaviour of the Attributes API in 4.2

Changing lib/gutentag/active_record.rb to treat 4.2 the same way as <=4.1 seemed to resolve it in casual testing via the Rails console.

Does it make sense to define a :with_tag scope by default?

I'm finding that I need to add this to almost every Taggable object:

  scope :with_tag, ->(tag_name){ joins(:tags).where('gutentag_tags.name' => tag_name)

Maybe it would be a nice convenience scope that the library could include by default. It'd also isolate the user from having to know the internal table name.

My 2c, feel free to dismiss.

Feature request: Support tag_names in ActiveRecord update and new

Right now, I'm doing:

@mymodel = MyModel.new(params.require(:mymodel).permit(:mymodelattrib))
tag_names = params.require(:mymodel)[:tag_names]
@mymodel.tag_names = tag_names if tag_names.present?

it would be great if that could be reduced to

@mymodel = MyModel.new(params.require(:mymodel).permit(:mymodelattrib, :tag_names))

And the same for ActiveRecord's update method.

v2.2.0 does not read or write tags anymore

At least for us in AlchemyCMS the 2.2.0 release and especially 38b31f3 broke the tagging feature.

See the passing build on Travis for v2.1.0
See the broken build on Travis for v2.2.0

How to replicate locally

  1. Clone the Alchemy branch that shows the bug
git clone https://github.com/tvdeyen/alchemy_cms
git checkout -b gutentag-bug origin/gutentag-bug
bundle install
cd spec/dummy
bin/rake db:setup
bin/rails server
  1. Navigate to https://localhost:3000/admin/locations
  2. Create an event location and set a tag
    alchemy cms - locations 2018-03-05 10-47-11
  3. See the tag appearing in the tag list on the right and under https://localhost:3000/admin/tags
    alchemy cms - locations 2018-03-05 10-56-58
  4. Edit the event location and see that the tag does not appear in the tag list
    alchemy cms - locations 2018-03-05 10-57-07
  5. Set another tag
    alchemy cms - locations 2018-03-05 11-01-30
  6. See only the last tag appearing in the tag list on the right
    alchemy cms - locations 2018-03-05 11-01-43
  7. The previous tag is still existing as you can see at https://localhost:3000/admin/tags
    alchemy cms - tags 2018-03-05 11-01-53

Investigation

We have a taggable module that is included in all models that need taggings.

# lib/alchemy/taggable.rb
module Alchemy
  # Gutentag interface compatibility module
  # Include this module to add tagging support to your model.
  module Taggable
    SPLITTER_REGEX = /,\s*/

    def self.included(base)
      Gutentag::ActiveRecord.call base
      base.extend ClassMethods
    end

    def tag_names=(names)
      if names.is_a? String
        names = names.split(SPLITTER_REGEX)
      end
      super(names)
    end

    module ClassMethods
      # Find all records matching all of the given tags.
      # Separate multiple tags by comma.
      def tagged_with(names)
        if names.is_a? String
          names = names.split(SPLITTER_REGEX)
        end
        super(names: names, match: :all)
      end

      # Returns all unique tags
      def tag_counts
        Gutentag::Tag.distinct.joins(:taggings)
          .where(gutentag_taggings: {taggable_type: name})
      end
    end
  end
end

It is very basic and only adds some convenient methods to the classes.

First and foremost we need to be able to set tags from a string separated by comma, so we made this change to the setter:

# lib/alchemy/taggable.rb:12
def tag_names=(names)
  if names.is_a? String
    names = names.split(SPLITTER_REGEX)
  end
  super(names)
end

This worked until we updated to 2.2.0. If we remove the calls to super in the tag_names attribute reader:

# lib/gutentag/active_record/modern_instance_methods.rb:9
def tag_names
  self.tag_names = tags.pluck(:name)
end

it works again. I actually don't see a need to call super here. What was the reason for this?

Unfortunately I can't get the writing of tags working with removing calls to super. The tags stay the same as before. You can't change them anymore.

It even is broken if we introduce our own writer:

# lib/alchemy/taggable.rb:12
def tag_list=(names)
  if names.is_a? String
    names = names.split(SPLITTER_REGEX)
  end
  self.tag_names = names
end

I am not sure what I am doing wrong here.

Thanks for the great library and you help is very much appreciated.

Shouldn't this be v1.0.0?

Used in production, has a fair number of people depending on it's public API, and does exactly what it set out to do?

Independent tag set for subdomain

I want to use this gem for an application with several subdomains, each of them will have an independent tag set.

Is there an easy way to add this functionality to the gem, or should I fork the gem?

table name collision

A prefix in table names would help to avoid collisions, such as gutentag_tags ...
I was trying to do it myself but I'm having problems in running rspec from the clean project.

Provide succinct example of extending models in Readme or Wiki

Monkey patching / extending models seems to be a bit tricky in Rails, though I'm sure there's a conventional approach you might recommend for the Gutentag models.

I'm trying to override #to_params on Gutentag::Tag in an initializer, but I believe it's being canceled by Rails's loading process, and I'm going to hack around it for now as I can't work it out.

I understand solving that problem isn't within the scope of this project, but I thought that a simple example of the conventional pattern for extending the models might be a good addition to the readme or wiki here, as it seems like a potentially common use case.

Multitenancy

Hi Pat,

I have patched Gutentag to support multitenancy, i.e. a self-contained set of tags for each account in my Rails app, and I thought you might like to see how. It's been running happily in production for a few weeks.

First I added a scoping column to Gutentag::Tag which I called :tenant_id. This involved modifying the migration:

create_table :gutentag_tags do |t|
  t.integer :tenant_id, :null => false  # added this line

  t.string :name, :null => false
  t.timestamps :null => false
end

add_index :gutentag_tags, [:name, :tenant_id], :unique => true  # changed this line

Then I patched the class to take :tenant_id into account:

# config/initializers/gutentag.rb
Gutentag::Tag.class_eval do
  attr_accessible :tenant_id if ActiveRecord::VERSION::MAJOR == 3

  scope :by_tenant_id, ->(tenant_id) { where tenant_id: tenant_id }

  # Change uniqueness validation to act within a scope.
  #
  # We cannot modify the existing one in place so we:
  #
  #1) remove it (works on Rails 3.2; not tested on Rails 4)
  _validators[:name].reject! { |v| v.is_a?(ActiveRecord::Validations::UniquenessValidator) }

  _validate_callbacks.reject! do |callback|
    callback.raw_filter.is_a?(ActiveRecord::Validations::UniquenessValidator) &&
      callback.raw_filter.attributes == [:name]
  end

  #2) add a new one
  validates :name, uniqueness: {case_sensitive: false, scope: :tenant_id}

  def self.find_by_name_and_tenant_id(name, tenant_id)
    where(tenant_id: tenant_id).find_by_name(name)
  end

  def self.find_or_create_by_name_and_tenant_id(name, tenant_id)
    find_by_name_and_tenant_id(name, tenant_id) || create(name: name, tenant_id: tenant_id)
  end
end

The only tricky part was updating the uniqueness validator to scope by :tenant_id.

Then I needed to tell Gutentag::Persistence to use a custom tagger which is aware of a tenant's scope. Unfortunately this was also a little tricky because it's only ever instantiated inside the after_save callback set up by the has_many_tags method – which is difficult to reach inside.

I created a module which a taggable model can include instead of calling has_many_tags:

# The taggable must respond to `#tenant_id`.
#
# Alternatively we could change the signature of `#has_many_tags` to take a block, which would
# be a lambda version of a taggable's current `#tenant_id` method.  Then the `after_save` callback
# could get the `tenant_id` by calling the block we passed in.
module Taggable
  def self.included(base)
    base.extend ClassMethods
    base.has_many_tags  # bootstrap
  end

  module ClassMethods
    # I need to inject my own tagger into Gutentag::Persistence so I can use the tenant_id when
    # finding or creating tags.  Although Gutentag::Persistence supports an injectable tagger,
    # the way it is instantiated in the after_save callback doesn't give any opportunity to inject
    # a tagger.
    #
    # I can't find a way to skip the after_save callback on Gutentag::ActiveRecord because it is
    # defined inline as a lambda.  Were it a symbol/instance method, it ought to be skippable so:
    #
    #   skip_callback :save, :after, :persist
    #
    # Instead I copy-paste the non-callback parts of Gutentag::ActiveRecord::ClassMethods#has_many_tags
    # and add my own after_save callback.
    def has_many_tags
      has_many :taggings, :class_name => 'Gutentag::Tagging', :as => :taggable,
        :dependent => :destroy
      has_many :tags,     :class_name => 'Gutentag::Tag',
        :through => :taggings

      after_save do |instance|
        persister = Gutentag::Persistence.new(instance)
        persister.tagger = TenantTagger.new(instance.tenant_id)
        persister.persist
      end
    end
  end

  def tenant_id
    raise NotImplementedError, 'taggable must implement'
  end
end

Finally, here is the custom tagger:

class TenantTagger
  def initialize(tenant_id)
    @tenant_id = tenant_id
  end

  def find_by_name(name)
    Gutentag::Tag.find_by_name_and_tenant_id(name, tenant_id)
  end

  def find_or_create(name)
    Gutentag::Tag.find_or_create_by_name_and_tenant_id(name, tenant_id)
  end

  private

  attr_reader :tenant_id
end

Client code can use it like this:

class Foo < ActiveRecord::Base
  belongs_to :account  # for example

  include Taggable

  def tenant_id
    account.id
  end
end

Overall I was pleased by how little I had to change conceptually. The implementation was a little messy because it isn't straightforward to modify validators or after-save callbacks.

I know you don't necessarily want to support multitenancy, but I wonder whether we could rearrange Gutentag's code at all, wihtout changing its function, to make extending for multitenancy easier?

Thanks for such a cleanly and clearly written gem!

Cheers,
Andy

Cross-ref: #9.

Tag name too long

Hey there,

using this lib in a Rails 5.2.0 project right now and in fact I'm quite happy with it. However, there's one thing I stumbled over (or rather the client I'm working for stumbled over):

The default migration sets up the gutentag_tags-table with name defined as string, which will translate to VARCHAR(255) on MySQL.

However, there is no validation in place that checks that the tag for being not longer than 255 chars.

I wonder what the best way to fix this is (happy to provide a PR once we've discussed the solutions :))

  1. Just have the validation made on app side in the model?
  2. Add the validation to the existing ones?
  3. Make the name column a text column? (I know, having tags longer than 255 chars sounds wild :))

Anything you'd prefer? For now I'll go with option 1 in our app.

a Rails 3.2.3 compatibilty issue?

in the Rails console, executing

MyModel.first.tag_names << 'hq'

results in

Gutentag::Tag Load (0.2ms)  SELECT "tags".* FROM "tags" WHERE "tags"."name" = 'hq' LIMIT 1
ActiveModel::MassAssignmentSecurity::Error: Can't mass-assign protected attributes: name
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/activemodel-3.2.13/lib/active_model/mass_assignment_security/sanitizer.rb:48:in `process_removed_attributes'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/activemodel-3.2.13/lib/active_model/mass_assignment_security/sanitizer.rb:20:in `debug_protected_attribute_removal'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/activemodel-3.2.13/lib/active_model/mass_assignment_security/sanitizer.rb:12:in `sanitize'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/activemodel-3.2.13/lib/active_model/mass_assignment_security.rb:230:in `sanitize_for_mass_assignment'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/activerecord-3.2.13/lib/active_record/attribute_assignment.rb:75:in `assign_attributes'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/activerecord-3.2.13/lib/active_record/base.rb:498:in `initialize'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/activerecord-3.2.13/lib/active_record/persistence.rb:44:in `new'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/activerecord-3.2.13/lib/active_record/persistence.rb:44:in `create'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/gutentag-0.2.0/lib/gutentag/tag_names.rb:34:in `<<'
    from (irb):17
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:47:in `start'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:8:in `start'
    from /opt/local/lib/ruby1.9/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands.rb:41:in `<top (required)>'
    from script/rails:6:in `require'
    from script/rails:6:in `<main>'

Seems like perhaps Gutentag isn't compatible with the security change made in Rails 3.2.3?

More details here:

http://www.h-online.com/security/news/item/Rails-3-2-3-makes-mass-assignment-change-1498547.html

do not inject into AR

makes the setup a tiny bit more verbose but removed monkey-patching ...

class Foo < ActiveRecord::Base
  include Gutentag:: ActiveRecord
  has_many_tags
end

ActiveRecord::Deadlocked and ActiveRecord::RecordNotUnique errors

Use Case: Straight forward parsing of content, adding tags to the Article model

Error: Getting these errors:

ActiveRecord::Deadlocked: Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction: UPDATE gutentag_tags SET taggings_count = COALESCE(taggings_count, 0) + 1 WHERE gutentag_tags.id = 6

ActiveRecord::RecordNotUnique: Mysql2::Error: Duplicate entry 'women' for key 'index_gutentag_tags_on_name': INSERT INTO gutentag_tags (name, created_at, updated_at) VALUES ('women', '2020-08-19 03:02:13', '2020-08-19 03:02:13')

I'm adding the tags via tag_names << 'tag' and then calling update or save on the article.

This only happens occasionally, and everything is running in Resque jobs.

ActiveRecord Query returning tag_names nil when there's tags

Gem Version: 2.5.4
Rails: 6.1.3
Ruby: 2.7.1

When I try AR query.

Video.where.not(link: nil)
  => [#<Video:0x00007fe735627680
  id: "15d9dfa3-37d0-4d99-a3d8-35431a4d0da0",
...
  tag_names: nil>,

When I call the object directly

[41] pry(#<VideoUploadsController>)> @video
=> #<Video:0x00007fe737b54b88
id: "15d9dfa3-37d0-4d99-a3d8-35431a4d0da0",
...
tag_names: ["hips", "glutes"]>

Using gutentag provided query

 Video.tagged_with(names: ['hips'])
 <ActiveRecord::Relation [#<Video id: "15d9dfa3-37d0-4d99-a3d8-35431a4d0da0", 
 ...
 tag_names: nil>]>

This came about when I trying Video.where.not(tag_names: nil)

It returns values but tag_names is shown as nil. Not sure why

Can't use the gem outide rails?

Thanks for this awesome gem. Anyway, I'm trying to gemify all the models and use it outside Rails (Grape) and now when I include the gem it doesn't seems to be working?

I added the gem to my models.gemspec like spec.add_dependency "gutentag","~> 0.6.0".

Now when I try to use has_many_tags in my model I get:

undefined local variable or method `has_many_tags' for #<Class:0x007fde9d9cac38> (NameError)

When I try to require the gem in the model I get this error:

/Users/info/.rvm/gems/ruby-2.2.2/gems/activesupport-4.2.3/lib/active_support/dependencies.rb:274:in `require':LoadError: cannot load such file -- rails/engine

Any help would be appreciated.

undefined method `downcase' for nil:NilClass

undefined method `downcase' for nil:NilClass

Application Trace | Framework Trace | Full Trace
gutentag (0.5.0) lib/gutentag/tag_name.rb:11:in `to_s'
gutentag (0.5.0) lib/gutentag/tag_name.rb:3:in `normalise'
gutentag (0.5.0) app/models/gutentag/tag.rb:26:in `normalise_name'
{"utf8"=>"✓",
 "authenticity_token"=>"RZeSIgEnepeh7FbA9zTWhxPmTLRaVPt/SoWzgwTANgY=",
 "tag"=>{"name"=>"Sample College"},
 "commit"=>"Create Tag"}

Migration causes `id: serial` in schema db, causing inserts to fail under sqlite3

First of all, I'm rather new to Rails so I offer my apologies in advance because all I'm going to say might be the result of my own cluelessness.

I'm running Rails 6.1.3.1 on Ruby 2.6.3 using pg 1.2.3 in dev and sqlite3 1.4.2 on test.

Following the instructions in the README I added tags to my Model. It worked just fine in Development (I was able to add tags through a form and was able to verify they were stored correctly in the Gutentag tables).

The problems appeared when I tried setting up a simple test case that loaded a model object, set a tag, and saved it. This failed on saving with an error from Sqlite claiming that NOT NULL constraint failed: gutentag_tags.id. I simplified the test to directly replicate your own test cases, and it failed in the same way both using tag_names and adding a tag object through the tags collection. Finally I debugged the test case to get the SQL INSERT command that was failing and tried to execute it on my own in the sqlite database with the same result.

Then I noticed that the id fields for both gutentag_tags and gutentag_taggings were of type "serial"... which is a bit confusing to me, since the sqlite documentation claims that it doesn't support a "serial" type. In any case, where my db browser showed the id fields of other models to be of type integer, it claimed that gutentag's were serial.

Looking at schema.rb the create table commands for the gutentag tables had the following:

create_table "gutentag_taggings", id: :serial, force: :cascade do |t|
# ...
end 

create_table "gutentag_tags", id: :serial, force: :cascade do |t|
# ...
end

So I fixed my problems doing the following:

  • Run the three gutentag migrations down
  • Changed their superclass to ActiveRecord::Migration[6.1]
  • Did a db:migrate again

After doing so the id: :serial disappeared from schema.rb, the id fields appear as "integer (auto increment)" in sqlite and my tests run ok.

So my possibly uninformed conclusion is that the default migrations created by the install procedure create a schema under rails 6.1 that does not work with sqlite. I was able to fix my own site by changing the migration superclass but I'm letting you know in case this serves to avoid other people the frustration.

Database does not exist

When I run rails db:create || bundle exec rails db:create I get the stack trace given below.

~/apps/project/myapp$ bin/rails db:create
rails aborted!
ActiveRecord::NoDatabaseError: FATAL:  database "myapp_development" does not exist
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb:684:in `rescue in connect'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb:680:in `connect'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb:215:in `initialize'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb:40:in `new'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb:40:in `postgresql_connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb:809:in `new_connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb:853:in `checkout_new_connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb:832:in `try_to_checkout_new_connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb:793:in `acquire_connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb:521:in `checkout'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb:380:in `connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb:1008:in `retrieve_connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_handling.rb:118:in `retrieve_connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/connection_handling.rb:90:in `connection'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/bundler/gems/rails-6d1dd1643dc8/activerecord/lib/active_record/model_schema.rb:324:in `table_exists?'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/gems/gutentag-2.0.0/lib/gutentag/tag_validations.rb:26:in `add_length_validation?'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/gems/gutentag-2.0.0/lib/gutentag/tag_validations.rb:34:in `validation_options'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/gems/gutentag-2.0.0/lib/gutentag/tag_validations.rb:18:in `call'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/gems/gutentag-2.0.0/lib/gutentag/tag_validations.rb:10:in `call'
/home/developer/.rvm/gems/ruby-2.4.1@myapp/gems/gutentag-2.0.0/lib/gutentag.rb:46:in `block in <top (required)>'

It is not allowing me to create a database.

ActiveModel::MissingAttributeError: missing attribute: tag_names

After running through installation instructions, and including Gutentag on my model like so, I get an error whenever I call tag_names, which also results in me not being able to save any MyModel record due to the callbacks defined in the extension.

class MyModel < ActiveRecord::Base
  Gutentag::ActiveRecord.call self
end
c = MyModel.find id
c.tag_names
=> ActiveModel::MissingAttributeError: missing attribute: tag_names
from <...>/vendor/cache/ruby/2.3.0/gems/activerecord-5.0.0/lib/active_record/attribute_methods/read.rb:66:in `block in _read_attribute'

Model.tag_names or similar functionality?

I just recently found your gem and I can't seem to find a cleaner way to get a list of all tags for a particular model. I'm hoping for something along the lines of the following:

Article.tag_names #=> ['business', 'leisure', 'nsfw']

At the moment, I'm using the following. It feels a bit hacky with the multi-tenancy. Is this the best option?

module Taggable
  extend ActiveSupport::Concern

  included do
    Gutentag::ActiveRecord.call self
  end

  class_methods do
    def tag_names
      Gutentag::Tag.joins(:taggings)
        .where(gutentag_taggings: { taggable_type: self, taggable_id: pluck(:id) })
        .distinct.pluck(:name)
    end
  end
end

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.