tmiyamon / acts-as-taggable-array-on Goto Github PK
View Code? Open in Web Editor NEWA simple and high performance tagging gem for Rails using PostgreSQL array.
License: MIT License
A simple and high performance tagging gem for Rails using PostgreSQL array.
License: MIT License
acts_as_taggable_on lets you read and write a tag_list to/from the underlying tags for the object, giving a quick-and-dirty way to set tags: in a form text field.
Was this left out intentionally, or would it be worth adding?
Something like the following virtual attributes:
acts_as_taggable_array_on :tags
def tags_string
tags.join(",")
end
def tags_string=(string)
self.tags = string.split(",")
end
turning
method=POST path=/admins/9 location=http://localhost:3000/admins/9 params={"admin"=>{"tags_string"=>"foo, bar"}}
into
UPDATE "admins" SET "tags" = $1, "updated_at" = $2 WHERE "admins"."id" = 9 [["tags_acl", "{\"foo\",\" bar\"}"], ["updated_at", "2015-08-27 23:01:33.009967"]]
This is the first time we've started using this gem. One of the very first things we tried to do was define tags in a Rails controller (a view has the same issue) where we scoped the tags based on a scoped association using an instance variable.
For example, lets say there is a tagged Widget
class which belongs to a Company
. In this case we want all of the tags for a widget for a specific company. On the controller we have this code:
@company = Company.find(params[:id])
@tags = Widget.all_tags { where(company: @company) }
The current all_tags
implementation is using instance_eval
:
Since there is no @company
on the class the scoped call "fails" returning an empty array []
.
SELECT tag FROM (
SELECT DISTINCT unnest(widgets.tags) as tag
FROM "widgets"
WHERE "widgets"."company_id" IS NULL
) subquery
To work around this we need to create artificial local variables just for the block scope:
@company = Company.find(params[:id])
company = @company
@tags = Widget.all_tags { where(company: company) }
Which produces the correct result:
SELECT tag FROM (
SELECT DISTINCT unnest(widgets.tags) as tag
FROM "widgets"
WHERE "widgets"."company_id" = 1
) subquery
It would be great if we could avoid having to do this two-step by either having the scope yielded to the block or if the code could support block local variables:
# yield scope
Widget.all_tags { |scope| scope.where(company: company) }
# block local variables
Widget.all_tags(@company) { |company| scope.where(company: company) }
If the yield scope version would be a breaking change, maybe the unused options
hash could be leveraged for a block local variable alternative; if there are no other plans for options
.
Running: PostgreSQL 11
Gemfile.lock
acts-as-taggable-array-on (0.6.0)
activerecord (>= 4)
activesupport (>= 4)
class Photo < ApplicationRecord
taggable_array :tags
end
> Photo.last.tags => ["coffee", "sun"]
> Photo.with_any_tags("coffee") => [#<Photo: …>, #<Photo: …>]
> Photo.tags_cloud => ActiveRecord::StatementInvalid: PG::UndefinedTable
Running Photo.tags_cloud
or seems to produce invalid query:
DEPRECATION WARNING: Dangerous query method (method whose arguments are used as raw SQL) called with non-attribute argument(s): "tag, count(*) as count". Non-attribute arguments will be disallowed in Rails 6.0. This method should not be called with user-provided values, such as request parameters or model attributes. Known-safe values can be passed by wrapping them in Arel.sql(). (called from __pry__ at (pry):4)
(1.4ms) SELECT tag, count(*) as count FROM (SELECT unnest(photos.tags) as tag FROM "photos") subquery GROUP BY tag ORDER BY "photos"."created_at" DESC, tag
ActiveRecord::StatementInvalid: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "photos"
LINE 1: ...tag FROM "photos") subquery GROUP BY tag ORDER BY "photos"."...
^
: SELECT tag, count(*) as count FROM (SELECT unnest(photos.tags) as tag FROM "photos") subquery GROUP BY tag ORDER BY "photos"."created_at" DESC, tag
from /Users/antonysastre/.rvm/gems/ruby-2.5.0/gems/activerecord
-5.2.0.rc2/lib/active_record/connection_adapters/postgresql_adapter.rb:603:in `async_exec'
—
Update: Getting the same error on Photo.all_tags
.
Update: Updated to 0.6.0, same issue.
transfer to https://github.com/acts-as-taggable-array-on
rewrite 'tmiyamon' to organization in the files of this repository
add deleted_at column, and add acts_as_paranoid
to modal
class User < ApplicationRecord
acts_as_taggable_array_on :tags
acts_as_paranoid
end
When run User.tags_cloud
, It will raise sql error
when use acts_as_paranoid
irb(main):001:0> Topic.all_tags
(2.8ms) SELECT tag FROM (SELECT DISTINCT unnest(topics.tags) as tag FROM "topics") subquery WHERE "topics"."deleted_at" IS NULL
Traceback (most recent call last):
1: from (irb):1
ActiveRecord::StatementInvalid (PG::UndefinedTable: ERROR: missing FROM-clause entry for table "topics")
LINE 1: ...topics.tags) as tag FROM "topics") subquery WHERE "topics"."...
^
: SELECT tag FROM (SELECT DISTINCT unnest(topics.tags) as tag FROM "topics") subquery WHERE "topics"."deleted_at" IS NULL
irb(main):002:0> Topic.unscoped.all_tags
(3.8ms) SELECT tag FROM (SELECT DISTINCT unnest(topics.tags) as tag FROM "topics") subquery
=> ["recommend", "adapter", "emojionearea", "hello", "emojione"]
irb(main):003:0>
User.where(user_id: @user.id).limit(10).order('count desc').tags_cloud.map{|a| {text: a[0]}}
This gives me
PG::UndefinedTable: ERROR: missing FROM-clause entry for table "users"
LINE 1: ...outs.tags) as tag FROM "users") subquery WHERE "users"...
^
: SELECT tag, count(*) as count FROM (SELECT unnest(users.tags) as tag FROM "users") subquery WHERE "users"."user_id" = $1 GROUP BY tag ORDER BY count desc, tag LIMIT 10
Completed 500 Internal Server Error in 9ms
[Rollbar] Reporting exception: PG::UndefinedTable: ERROR: missing FROM-clause entry for table "users"
LINE 1: ...outs.tags) as tag FROM "users") subquery WHERE "users"...
Not possible to scope tag clouds by an id like a traditional where clause?
As a quick exercise, I have added the ability to search the array values with a regular expression.
The following scope was added to taggable.rb
scope :"with_#{tag_name}_like", ->(pattern) { subquery = unscoped.select("*, unnest(#{table_name}.#{tag_name}) AS subquery_tag"); select('*').from(subquery).where('subquery_tag ~* ?', pattern) }
This allows you to get tags as follows:
Object.with_tags_like('^Material')
Object.with_tags_like('^(Wood|Iron)')
Commit 90e4278 moved database type reflection from runtime (scope) to load time (class level). Now, if you reference model with acts_as_taggable_array_on
in environment and you don't have database - it fails.
In my case i had devise in routes, which referenced User model, which had tags on it. And routes are loaded in environment for db tasks.
I don't know what is going on here, almost certainly user error on my part, but acts_as_taggable needs to be passed an array from the form to work, and that's lead to some really shonky code on my part. What am I doing wrong here?
def note_params
params.require(:note)
.permit(:tag_list,
:priority,
:date_show,
:body)
end
This leads to all sorts of problems when I try and save the record, saying that "method uniq is undefined for String" or whatever, basically because tag list is just a string.
So I've had to put this crazy thing in the controller:
@note.tag_list = @note.tag_list.split(',')
before I @note.save to make it work.
Which can't be right, can it?
What am I missing? :(
@tmiyamon firstly thanks for the excellent gem, I'm seriously considering pulling acts_as_taggable_on as this is more suited and performs faster for our needs.
One question is in regards to the scopes that get added by acts_as_taggable_array_on. You have designed it to expect multiple parameters when searching for records that match with_any or without_any etc. I'm not sure of a time when you would search for records this way unless you are on the console as most input data would be in a string of tags to search. For example from a search form the user would comma separate the tags to search for.
As you have specified *tags on the scope parameters, so with multiple tags you need to call the method with multiple parameters, which is fine in the console, i.e.:
User.with_all_tags "awesome", "slick"
But most of the time I would have though you will have your list of tags to search on in an array or string. So for example if you want to search with an array of tags you need to call this as follows:
User.with_all_tags *["awesome", "slick"]
Note: the * before the array
If you want to search with a string of comma separated tags, you would need to call
User.with_all_tags *("awesome, slick").join(",")collect(&:strip))
Am I missing something, and/or it may be worth documenting this? Also it might be better to move this / change the way these are called in the scope, though I guess any change like this would break any existing usage?
Just some thoughts anyway, I thought I would start the discussion and for now I'll use the workarounds above.
Jon
Getting this response when trying to do a simple query.. Just upgraded to PG 9.4 and rails 4.2. Any ideas?
2.1.3 :081 > Movement.with_any_tags("cool")
Movement Load (0.9ms) SELECT "movements".* FROM "movements" WHERE (tags && ARRAY['cool']::varchar[])
PG::UndefinedFunction: ERROR: operator does not exist: text[] && character varying[]
LINE 1: SELECT "movements".* FROM "movements" WHERE (tags && ARRAY['...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "movements".* FROM "movements" WHERE (tags && ARRAY['cool']::varchar[])
ActiveRecord::StatementInvalid: PG::UndefinedFunction: ERROR: operator does not exist: text[] && character varying[]
LINE 1: SELECT "movements".* FROM "movements" WHERE (tags && ARRAY['...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "movements".* FROM "movements" WHERE (tags && ARRAY['cool']::varchar[])
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/postgresql_adapter.rb:592:in `async_exec'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/postgresql_adapter.rb:592:in `block in exec_no_cache'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/abstract_adapter.rb:466:in `block in log'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activesupport-4.2.0.rc3/lib/active_support/notifications/instrumenter.rb:20:in `instrument'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/abstract_adapter.rb:460:in `log'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/postgresql_adapter.rb:592:in `exec_no_cache'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/postgresql_adapter.rb:584:in `execute_and_clear'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/postgresql/database_statements.rb:160:in `exec_query'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/abstract/database_statements.rb:336:in `select'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/abstract/database_statements.rb:32:in `select_all'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/connection_adapters/abstract/query_cache.rb:70:in `select_all'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/querying.rb:39:in `find_by_sql'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/relation.rb:638:in `exec_queries'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/goldiloader-0.0.8/lib/goldiloader/active_record_patches.rb:34:in `exec_queries_with_auto_include'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/relation.rb:514:in `load'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/relation.rb:243:in `to_a'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/activerecord-4.2.0.rc3/lib/active_record/relation.rb:629:in `inspect'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/railties-4.2.0.rc3/lib/rails/commands/console.rb:110:in `start'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/railties-4.2.0.rc3/lib/rails/commands/console.rb:9:in `start'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/railties-4.2.0.rc3/lib/rails/commands/commands_tasks.rb:68:in `console'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/railties-4.2.0.rc3/lib/rails/commands/commands_tasks.rb:39:in `run_command!'
from /home/adam/.rvm/gems/ruby-2.1.3@xlete/gems/railties-4.2.0.rc3/lib/rails/commands.rb:17:in `<top (required)>'
from bin/rails:4:in `require'
from bin/rails:4:in `<main>'2.1.3 :082 >
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.