make_taggable 1.2.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/docs/contexts.md +17 -0
- data/docs/querying.md +9 -1
- data/lib/make_taggable/taggable/core.rb +5 -2
- data/lib/make_taggable/taggable/tagged_with_query/exclude_tags_query.rb +12 -6
- data/lib/make_taggable/taggable.rb +27 -0
- data/lib/make_taggable/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99cb64f83f0cef74cc21f3fc09a341e62db5a2248b8c88ef5d720de4b9446abb
|
|
4
|
+
data.tar.gz: c3c08be5a20df6100567523555be2394dc32665770f6c65226e0fe91b3be5652
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ad8701479ff9565786acebff64af05fe5aaa88eddbcfc660f88778e4ca885a0d26438677754cfa3072dd54e3eef6374ebed4f1e4de6a70fe911824050cb0b19e
|
|
7
|
+
data.tar.gz: 47804d66800ff4df491f3b7037ba5e82c131fbaf4c9eb8442a650ed6ecbf1772483d2ae2237c5437cb8ffefca831d107744a3ffacb2c517cb65a161063bc6d0e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,33 @@ All notable changes to this project are documented here.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
|
|
6
6
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.2.1] - 2026-08-23
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `tagged_with(..., exclude: true)` raised on any model whose primary key is not `id`. The exclude
|
|
13
|
+
strategy built its `NOT IN` predicate against a hardcoded `id` column, where the other two
|
|
14
|
+
strategies already used the model's primary key.
|
|
15
|
+
|
|
16
|
+
- `tagged_with(..., on: <context>, exclude: true)` ignored the context entirely, gathering taggings
|
|
17
|
+
from every context, so a record tagged in one context was excluded from a query about another.
|
|
18
|
+
|
|
19
|
+
- `tagged_with([], exclude: true)` returned nothing rather than everything. Excluding no tags rules
|
|
20
|
+
nothing out, so the whole scope now stands. This restores the property that `tagged_with(list)`
|
|
21
|
+
and `tagged_with(list, exclude: true)` partition the scope between them for any list.
|
|
22
|
+
|
|
23
|
+
- A tag context whose name cannot become a Ruby method name -- one starting with a digit, say --
|
|
24
|
+
raised `SyntaxError` while the model was loading, from inside Active Record's association
|
|
25
|
+
builder. Since `SyntaxError` is not a `StandardError` it slipped past application rescues.
|
|
26
|
+
Contexts are now checked as they are declared and rejected with an `ArgumentError` naming the
|
|
27
|
+
context. Non-ASCII context names keep working.
|
|
28
|
+
|
|
29
|
+
### Internal
|
|
30
|
+
|
|
31
|
+
- The suite's schema teardown no longer depends on the order `connection.tables` returns. MySQL
|
|
32
|
+
ignores `DROP TABLE ... CASCADE` for foreign keys, so it only worked because `taggings` happens
|
|
33
|
+
to sort before `tags`.
|
|
34
|
+
|
|
8
35
|
## [1.2.0] - 2026-08-23
|
|
9
36
|
|
|
10
37
|
### Fixed
|
data/docs/contexts.md
CHANGED
|
@@ -40,6 +40,23 @@ user.skill_list_change # => [["jogging"], ["diving"]]
|
|
|
40
40
|
user.will_save_change_to_skill_list?
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
### Naming a context
|
|
44
|
+
|
|
45
|
+
Because the context becomes part of every name in the table above, it has to be usable as a Ruby
|
|
46
|
+
method and instance variable name. A context starting with a digit, or containing a hyphen or a
|
|
47
|
+
space, is rejected when the model declares it:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
class Book < ActiveRecord::Base
|
|
51
|
+
make_taggable :"1categories"
|
|
52
|
+
end
|
|
53
|
+
# => ArgumentError: :"1categories" cannot be used as a tag context: make_taggable generates
|
|
54
|
+
# methods and instance variables from it, and "1categories_list" is not a valid Ruby name.
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Validity is decided by asking Ruby, not by a pattern, so anything that makes a legal method name is
|
|
58
|
+
allowed — non-ASCII context names included.
|
|
59
|
+
|
|
43
60
|
## Adding contexts later
|
|
44
61
|
|
|
45
62
|
Calling `make_taggable` again adds contexts rather than replacing them, which is what lets a
|
data/docs/querying.md
CHANGED
|
@@ -28,12 +28,20 @@ empty relation rather than every record — worth knowing when the tags come fro
|
|
|
28
28
|
| `:exclude` | Match records carrying none of the tags |
|
|
29
29
|
| `:match_all` | Match records carrying only these tags and no others |
|
|
30
30
|
| `:wild` | Match tags *containing* the given text, i.e. `%sci%` |
|
|
31
|
-
| `:on` | Restrict to one context |
|
|
31
|
+
| `:on` | Restrict to one context. Honoured by every option, `:exclude` included |
|
|
32
32
|
| `:owned_by` | Restrict to tags applied by one tagger |
|
|
33
33
|
| `:order_by_matching_tag_count` | With `:any`, order by how many tags matched, most first |
|
|
34
34
|
| `:start_at` | Only tags applied after this time |
|
|
35
35
|
| `:end_at` | Only tags applied before this time |
|
|
36
36
|
|
|
37
|
+
An empty tag list means "nothing matches" for the matching options and "nothing is ruled out" for
|
|
38
|
+
`:exclude`, so the two always partition the scope between them:
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Book.tagged_with([]) # => none
|
|
42
|
+
Book.tagged_with([], exclude: true) # => every book
|
|
43
|
+
```
|
|
44
|
+
|
|
37
45
|
`:wild` combines with `:any` or `:exclude`:
|
|
38
46
|
|
|
39
47
|
```ruby
|
|
@@ -129,7 +129,8 @@ module MakeTaggable::Taggable
|
|
|
129
129
|
# @option options [Symbol, String] :on only tags applied in this context
|
|
130
130
|
# @option options [Time, Date] :start_at only tags applied after this time
|
|
131
131
|
# @option options [Time, Date] :end_at only tags applied before this time
|
|
132
|
-
# @return [ActiveRecord::Relation] empty when no tags are given
|
|
132
|
+
# @return [ActiveRecord::Relation] empty when no tags are given, except under `:exclude`,
|
|
133
|
+
# where excluding no tags leaves the whole scope standing
|
|
133
134
|
#
|
|
134
135
|
# @example Every tag
|
|
135
136
|
# User.tagged_with(["awesome", "cool"])
|
|
@@ -144,7 +145,9 @@ module MakeTaggable::Taggable
|
|
|
144
145
|
tag_list = MakeTaggable.default_parser.new(tags).parse
|
|
145
146
|
options = options.dup
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
# Asking for no tags matches nothing, but *excluding* no tags rules
|
|
149
|
+
# nothing out, so the whole scope stands.
|
|
150
|
+
return options[:exclude].present? ? all : none if tag_list.empty?
|
|
148
151
|
|
|
149
152
|
::MakeTaggable::Taggable::TaggedWithQuery.build(self, MakeTaggable::Tag, MakeTaggable::Tagging, tag_list, options)
|
|
150
153
|
end
|
|
@@ -20,15 +20,21 @@ module MakeTaggable::Taggable::TaggedWithQuery
|
|
|
20
20
|
private
|
|
21
21
|
|
|
22
22
|
def tags_not_in_list
|
|
23
|
-
|
|
23
|
+
on_condition = tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
|
|
24
|
+
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
|
|
25
|
+
.and(tags_match_type)
|
|
26
|
+
|
|
27
|
+
# Without this the subquery gathers taggings from every context, so a
|
|
28
|
+
# record tagged in one context is excluded from a query about another.
|
|
29
|
+
if options[:on].present?
|
|
30
|
+
on_condition = on_condition.and(tagging_arel_table[:context].eq(options[:on]))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
taggable_arel_table[taggable_model.primary_key].not_in(
|
|
24
34
|
tagging_arel_table
|
|
25
35
|
.project(tagging_arel_table[:taggable_id])
|
|
26
36
|
.join(tag_arel_table)
|
|
27
|
-
.on(
|
|
28
|
-
tagging_arel_table[:tag_id].eq(tag_arel_table[:id])
|
|
29
|
-
.and(tagging_arel_table[:taggable_type].eq(taggable_model.base_class.name))
|
|
30
|
-
.and(tags_match_type)
|
|
31
|
-
)
|
|
37
|
+
.on(on_condition)
|
|
32
38
|
)
|
|
33
39
|
|
|
34
40
|
# FIXME: missing time scope, this is also missing in the original implementation
|
|
@@ -69,6 +69,7 @@ module MakeTaggable
|
|
|
69
69
|
#
|
|
70
70
|
def taggable_on(preserve_tag_order, *tag_types)
|
|
71
71
|
tag_types = tag_types.to_a.flatten.compact.map(&:to_sym)
|
|
72
|
+
tag_types.each { |tag_type| validate_tag_context!(tag_type) }
|
|
72
73
|
|
|
73
74
|
if taggable?
|
|
74
75
|
self.tag_types = (self.tag_types + tag_types).uniq
|
|
@@ -97,5 +98,31 @@ module MakeTaggable
|
|
|
97
98
|
include Ownership
|
|
98
99
|
include Related
|
|
99
100
|
end
|
|
101
|
+
|
|
102
|
+
# Rejects a context that cannot become the methods and instance variables the
|
|
103
|
+
# library generates from it.
|
|
104
|
+
#
|
|
105
|
+
# A context is interpolated straight into generated source -- `#{context}_list`,
|
|
106
|
+
# `#{context}_taggings`, `@#{context}_list`. A name that is not a valid identifier
|
|
107
|
+
# produces source Ruby cannot parse, and the resulting SyntaxError descends from
|
|
108
|
+
# ScriptError rather than StandardError, so it slips past an application's own
|
|
109
|
+
# rescue and takes the boot down pointing at Active Record's association builder
|
|
110
|
+
# rather than at the offending declaration.
|
|
111
|
+
#
|
|
112
|
+
# Validity is decided by asking Ruby rather than by pattern, so a context is
|
|
113
|
+
# accepted on exactly the terms the generated code needs -- non-ASCII names
|
|
114
|
+
# included, since those make perfectly good method names.
|
|
115
|
+
#
|
|
116
|
+
# @param tag_type [Symbol] the context being declared
|
|
117
|
+
# @return [void]
|
|
118
|
+
# @raise [ArgumentError] when the context cannot become an identifier
|
|
119
|
+
def validate_tag_context!(tag_type)
|
|
120
|
+
Object.new.instance_variable_defined?(:"@#{tag_type}_list")
|
|
121
|
+
rescue NameError
|
|
122
|
+
raise ArgumentError,
|
|
123
|
+
"#{tag_type.inspect} cannot be used as a tag context: " \
|
|
124
|
+
"make_taggable generates methods and instance variables from it, and " \
|
|
125
|
+
"\"#{tag_type}_list\" is not a valid Ruby name."
|
|
126
|
+
end
|
|
100
127
|
end
|
|
101
128
|
end
|