metka 0.1.2 → 1.0.0
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/.gitignore +4 -0
- data/.travis.yml +19 -7
- data/Gemfile.lock +50 -43
- data/README.md +124 -15
- data/bin/setup +1 -2
- data/gemfiles/rails5.gemfile +6 -0
- data/gemfiles/rails52.gemfile +6 -0
- data/gemfiles/rails6.gemfile +6 -0
- data/lib/generators/metka/strategies/materialized_view/materialized_view_generator.rb +1 -1
- data/lib/generators/metka/strategies/materialized_view/templates/migration.rb.erb +25 -18
- data/lib/metka/generic_parser.rb +27 -1
- data/lib/metka/model.rb +37 -23
- data/lib/metka/query_builder.rb +7 -3
- data/lib/metka/query_builder/all_tags_query.rb +4 -19
- data/lib/metka/query_builder/any_tags_query.rb +4 -21
- data/lib/metka/query_builder/base_query.rb +27 -0
- data/lib/metka/query_builder/exclude_all_tags_query.rb +11 -0
- data/lib/metka/query_builder/exclude_any_tags_query.rb +11 -0
- data/lib/metka/version.rb +1 -1
- data/metka.gemspec +7 -5
- metadata +49 -13
- data/lib/metka/query_builder/exclude_tags_query.rb +0 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 960a0b262fa5e1dc7a4a08baf844f4f12063696133527f0ef201a4414ee498c0
|
|
4
|
+
data.tar.gz: 2792d8b84d0ebf6c9aa197c3bbbe8d6b257f522493a2b770887105cd0d6b539d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4b47eb9c9b7e64609b0933b02e5704aa8e5dd5cca5ca1a9c796625942fed6b67551574cb6fe3c45190954ce8bfb56ae40bb125659751d55b5ef8f780a35beb0d
|
|
7
|
+
data.tar.gz: fe1b4c69679173d7f8c639f3c060722c617f39f537dba1253ff1f9cd70e26a44028e8ae6676d440c2b9165691162f907a0cf4e546510c8e37194db13c59adb44
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
|
@@ -8,17 +8,29 @@ sudo: false
|
|
|
8
8
|
notifications:
|
|
9
9
|
email: false
|
|
10
10
|
|
|
11
|
+
services:
|
|
12
|
+
- postgresql
|
|
11
13
|
addons:
|
|
12
|
-
postgresql:
|
|
13
|
-
|
|
14
|
-
env:
|
|
15
|
-
global:
|
|
16
|
-
- METKA_DB_USER=postgres
|
|
17
|
-
- METKA_DB_NAME=metka
|
|
14
|
+
postgresql: "11.2"
|
|
18
15
|
|
|
19
16
|
before_install:
|
|
20
17
|
- gem install -v 2.0.2 bundler
|
|
18
|
+
- sudo apt-get update
|
|
19
|
+
- sudo apt-get --yes remove postgresql\*
|
|
20
|
+
- sudo apt-get install -y postgresql-11 postgresql-client-11
|
|
21
|
+
- sudo cp /etc/postgresql/{9.6,11}/main/pg_hba.conf
|
|
22
|
+
- sudo service postgresql restart 11
|
|
21
23
|
|
|
22
24
|
before_script:
|
|
23
25
|
- gem update --system
|
|
24
|
-
-
|
|
26
|
+
- psql -c 'CREATE ROLE travis SUPERUSER LOGIN CREATEDB;' -U postgres
|
|
27
|
+
- ./bin/setup
|
|
28
|
+
|
|
29
|
+
matrix:
|
|
30
|
+
include:
|
|
31
|
+
- rvm: 2.5.1
|
|
32
|
+
gemfile: gemfiles/rails5.gemfile
|
|
33
|
+
- rvm: 2.5.5
|
|
34
|
+
gemfile: gemfiles/rails52.gemfile
|
|
35
|
+
- rvm: 2.6.2
|
|
36
|
+
gemfile: gemfiles/rails6.gemfile
|
data/Gemfile.lock
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
metka (0.
|
|
4
|
+
metka (1.0.0)
|
|
5
5
|
dry-configurable
|
|
6
|
-
rails (
|
|
6
|
+
rails (~> 5.1)
|
|
7
7
|
|
|
8
8
|
GEM
|
|
9
9
|
remote: https://rubygems.org/
|
|
@@ -52,27 +52,29 @@ GEM
|
|
|
52
52
|
arel (8.0.0)
|
|
53
53
|
ast (2.4.0)
|
|
54
54
|
builder (3.2.3)
|
|
55
|
+
coderay (1.1.2)
|
|
55
56
|
concurrent-ruby (1.1.5)
|
|
56
|
-
crass (1.0.
|
|
57
|
+
crass (1.0.5)
|
|
58
|
+
database_cleaner (1.7.0)
|
|
57
59
|
diff-lcs (1.3)
|
|
58
|
-
dry-configurable (0.
|
|
60
|
+
dry-configurable (0.9.0)
|
|
59
61
|
concurrent-ruby (~> 1.0)
|
|
60
62
|
dry-core (~> 0.4, >= 0.4.7)
|
|
61
63
|
dry-core (0.4.9)
|
|
62
64
|
concurrent-ruby (~> 1.0)
|
|
63
|
-
erubi (1.
|
|
64
|
-
faker (2.
|
|
65
|
-
i18n (
|
|
65
|
+
erubi (1.9.0)
|
|
66
|
+
faker (2.8.0)
|
|
67
|
+
i18n (>= 1.6, < 1.8)
|
|
66
68
|
globalid (0.4.2)
|
|
67
69
|
activesupport (>= 4.2.0)
|
|
68
|
-
i18n (1.
|
|
70
|
+
i18n (1.7.0)
|
|
69
71
|
concurrent-ruby (~> 1.0)
|
|
70
|
-
jaro_winkler (1.5.
|
|
71
|
-
jetrockets-standard (1.0.
|
|
72
|
+
jaro_winkler (1.5.4)
|
|
73
|
+
jetrockets-standard (1.0.4)
|
|
72
74
|
rubocop-rails (~> 2.3.2)
|
|
73
75
|
rubocop-rspec (~> 1.35.0)
|
|
74
76
|
standard (~> 0.1.4)
|
|
75
|
-
loofah (2.
|
|
77
|
+
loofah (2.4.0)
|
|
76
78
|
crass (~> 1.0.2)
|
|
77
79
|
nokogiri (>= 1.5.9)
|
|
78
80
|
mail (2.7.1)
|
|
@@ -80,14 +82,17 @@ GEM
|
|
|
80
82
|
method_source (0.9.2)
|
|
81
83
|
mini_mime (1.0.2)
|
|
82
84
|
mini_portile2 (2.4.0)
|
|
83
|
-
minitest (5.
|
|
84
|
-
nio4r (2.5.
|
|
85
|
-
nokogiri (1.10.
|
|
85
|
+
minitest (5.13.0)
|
|
86
|
+
nio4r (2.5.2)
|
|
87
|
+
nokogiri (1.10.7)
|
|
86
88
|
mini_portile2 (~> 2.4.0)
|
|
87
|
-
parallel (1.
|
|
88
|
-
parser (2.6.
|
|
89
|
+
parallel (1.19.1)
|
|
90
|
+
parser (2.6.5.0)
|
|
89
91
|
ast (~> 2.4.0)
|
|
90
92
|
pg (1.1.4)
|
|
93
|
+
pry (0.12.2)
|
|
94
|
+
coderay (~> 1.1.0)
|
|
95
|
+
method_source (~> 0.9.0)
|
|
91
96
|
rack (2.0.7)
|
|
92
97
|
rack-test (1.1.0)
|
|
93
98
|
rack (>= 1.0, < 3)
|
|
@@ -106,8 +111,8 @@ GEM
|
|
|
106
111
|
rails-dom-testing (2.0.3)
|
|
107
112
|
activesupport (>= 4.2.0)
|
|
108
113
|
nokogiri (>= 1.6)
|
|
109
|
-
rails-html-sanitizer (1.
|
|
110
|
-
loofah (~> 2.
|
|
114
|
+
rails-html-sanitizer (1.3.0)
|
|
115
|
+
loofah (~> 2.3)
|
|
111
116
|
railties (5.1.7)
|
|
112
117
|
actionpack (= 5.1.7)
|
|
113
118
|
activesupport (= 5.1.7)
|
|
@@ -115,36 +120,36 @@ GEM
|
|
|
115
120
|
rake (>= 0.8.7)
|
|
116
121
|
thor (>= 0.18.1, < 2.0)
|
|
117
122
|
rainbow (3.0.0)
|
|
118
|
-
rake (
|
|
119
|
-
rspec (3.
|
|
120
|
-
rspec-core (~> 3.
|
|
121
|
-
rspec-expectations (~> 3.
|
|
122
|
-
rspec-mocks (~> 3.
|
|
123
|
-
rspec-core (3.
|
|
124
|
-
rspec-support (~> 3.
|
|
125
|
-
rspec-expectations (3.
|
|
123
|
+
rake (13.0.1)
|
|
124
|
+
rspec (3.9.0)
|
|
125
|
+
rspec-core (~> 3.9.0)
|
|
126
|
+
rspec-expectations (~> 3.9.0)
|
|
127
|
+
rspec-mocks (~> 3.9.0)
|
|
128
|
+
rspec-core (3.9.0)
|
|
129
|
+
rspec-support (~> 3.9.0)
|
|
130
|
+
rspec-expectations (3.9.0)
|
|
126
131
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
127
|
-
rspec-support (~> 3.
|
|
128
|
-
rspec-mocks (3.
|
|
132
|
+
rspec-support (~> 3.9.0)
|
|
133
|
+
rspec-mocks (3.9.0)
|
|
129
134
|
diff-lcs (>= 1.2.0, < 2.0)
|
|
130
|
-
rspec-support (~> 3.
|
|
131
|
-
rspec-rails (3.
|
|
135
|
+
rspec-support (~> 3.9.0)
|
|
136
|
+
rspec-rails (3.9.0)
|
|
132
137
|
actionpack (>= 3.0)
|
|
133
138
|
activesupport (>= 3.0)
|
|
134
139
|
railties (>= 3.0)
|
|
135
|
-
rspec-core (~> 3.
|
|
136
|
-
rspec-expectations (~> 3.
|
|
137
|
-
rspec-mocks (~> 3.
|
|
138
|
-
rspec-support (~> 3.
|
|
139
|
-
rspec-support (3.
|
|
140
|
-
rubocop (0.
|
|
140
|
+
rspec-core (~> 3.9.0)
|
|
141
|
+
rspec-expectations (~> 3.9.0)
|
|
142
|
+
rspec-mocks (~> 3.9.0)
|
|
143
|
+
rspec-support (~> 3.9.0)
|
|
144
|
+
rspec-support (3.9.0)
|
|
145
|
+
rubocop (0.75.1)
|
|
141
146
|
jaro_winkler (~> 1.5.1)
|
|
142
147
|
parallel (~> 1.10)
|
|
143
148
|
parser (>= 2.6)
|
|
144
149
|
rainbow (>= 2.2.2, < 4.0)
|
|
145
150
|
ruby-progressbar (~> 1.7)
|
|
146
151
|
unicode-display_width (>= 1.4.0, < 1.7)
|
|
147
|
-
rubocop-performance (1.
|
|
152
|
+
rubocop-performance (1.5.1)
|
|
148
153
|
rubocop (>= 0.71.0)
|
|
149
154
|
rubocop-rails (2.3.2)
|
|
150
155
|
rack (>= 1.1)
|
|
@@ -152,16 +157,16 @@ GEM
|
|
|
152
157
|
rubocop-rspec (1.35.0)
|
|
153
158
|
rubocop (>= 0.60.0)
|
|
154
159
|
ruby-progressbar (1.10.1)
|
|
155
|
-
sprockets (
|
|
160
|
+
sprockets (4.0.0)
|
|
156
161
|
concurrent-ruby (~> 1.0)
|
|
157
162
|
rack (> 1, < 3)
|
|
158
163
|
sprockets-rails (3.2.1)
|
|
159
164
|
actionpack (>= 4.0)
|
|
160
165
|
activesupport (>= 4.0)
|
|
161
166
|
sprockets (>= 3.0.0)
|
|
162
|
-
standard (0.1.
|
|
163
|
-
rubocop (~> 0.
|
|
164
|
-
rubocop-performance (~> 1.
|
|
167
|
+
standard (0.1.6)
|
|
168
|
+
rubocop (~> 0.75.0)
|
|
169
|
+
rubocop-performance (~> 1.5.0)
|
|
165
170
|
thor (0.20.3)
|
|
166
171
|
thread_safe (0.3.6)
|
|
167
172
|
timecop (0.9.1)
|
|
@@ -179,13 +184,15 @@ DEPENDENCIES
|
|
|
179
184
|
activerecord (~> 5.1.1)
|
|
180
185
|
ammeter
|
|
181
186
|
bundler
|
|
187
|
+
database_cleaner
|
|
182
188
|
faker
|
|
183
189
|
jetrockets-standard (~> 1.0.1)
|
|
184
190
|
metka!
|
|
185
191
|
pg
|
|
192
|
+
pry (~> 0.12.2)
|
|
186
193
|
rake
|
|
187
|
-
rspec
|
|
188
|
-
rspec-rails
|
|
194
|
+
rspec (~> 3.9)
|
|
195
|
+
rspec-rails (~> 3.9)
|
|
189
196
|
timecop
|
|
190
197
|
|
|
191
198
|
BUNDLED WITH
|
data/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# Metka
|
|
5
5
|
|
|
6
|
-
Rails gem to manage tags with
|
|
6
|
+
Rails gem to manage tags with SonggreSQL array columns.
|
|
7
7
|
|
|
8
8
|
## Installation
|
|
9
9
|
|
|
@@ -24,33 +24,109 @@ Or install it yourself as:
|
|
|
24
24
|
## Tag objects
|
|
25
25
|
|
|
26
26
|
```ruby
|
|
27
|
-
class
|
|
28
|
-
include Metka::Model
|
|
29
|
-
|
|
27
|
+
class Song < ActiveRecord::Base
|
|
28
|
+
include Metka::Model(column: 'tags')
|
|
29
|
+
include Metka::Model(column: 'genres')
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
-
@
|
|
33
|
-
@
|
|
34
|
-
@
|
|
32
|
+
@Song = Song.new(title: 'Migrate tags in Rails to SonggreSQL')
|
|
33
|
+
@Song.tag_list = 'top, chill'
|
|
34
|
+
@Song.genre_list = 'rock, jazz, pop'
|
|
35
|
+
@Song.save
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
## Find tagged objects
|
|
38
39
|
|
|
40
|
+
### .with_all_#{column_name}
|
|
41
|
+
```ruby
|
|
42
|
+
Song.with_all_tags('top')
|
|
43
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
44
|
+
|
|
45
|
+
Song.with_all_tags('top, 1990')
|
|
46
|
+
=> []
|
|
47
|
+
|
|
48
|
+
Song.with_all_tags('')
|
|
49
|
+
=> []
|
|
50
|
+
|
|
51
|
+
Song.with_all_genres('rock')
|
|
52
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### .with_any_#{column_name}
|
|
56
|
+
```ruby
|
|
57
|
+
Song.with_any_tags('chill')
|
|
58
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
59
|
+
|
|
60
|
+
Song.with_any_tags('chill, 1980')
|
|
61
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
62
|
+
|
|
63
|
+
Song.with_any_tags('')
|
|
64
|
+
=> []
|
|
65
|
+
|
|
66
|
+
Song.with_any_genres('rock, rap')
|
|
67
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
68
|
+
```
|
|
69
|
+
### .without_all_#{column_name}
|
|
70
|
+
```ruby
|
|
71
|
+
Song.without_all_tags('top')
|
|
72
|
+
=> []
|
|
73
|
+
|
|
74
|
+
Song.without_all_tags('top, 1990')
|
|
75
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
76
|
+
|
|
77
|
+
Song.without_all_tags('')
|
|
78
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
79
|
+
|
|
80
|
+
Song.without_all_genres('rock, pop')
|
|
81
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
82
|
+
|
|
83
|
+
Song.without_all_genres('rock')
|
|
84
|
+
=> []
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### .without_any_#{column_name}
|
|
39
88
|
```ruby
|
|
40
|
-
|
|
41
|
-
=> [
|
|
89
|
+
Song.without_any_tags('top, 1990')
|
|
90
|
+
=> []
|
|
91
|
+
|
|
92
|
+
Song.without_any_tags('1990, 1980')
|
|
93
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
42
94
|
|
|
43
|
-
|
|
44
|
-
=>
|
|
95
|
+
Song.without_any_genres('rock, pop')
|
|
96
|
+
=> []
|
|
97
|
+
|
|
98
|
+
Song.without_any_genres('')
|
|
99
|
+
=> [#<Song id: 1, title: 'Migrate tags in Rails to SonggreSQL', tags: ['top', 'chill'], genres: ['rock', 'jazz', 'pop']]
|
|
45
100
|
```
|
|
46
101
|
|
|
47
|
-
|
|
102
|
+
## Custom delimiter
|
|
103
|
+
By default, a comma is used as a delimiter to create tags from a string.
|
|
104
|
+
You can make your own custom separator:
|
|
105
|
+
```ruby
|
|
106
|
+
Metka.config.delimiter = [',', ' ', '\|']
|
|
107
|
+
parsed_data = Metka::GenericParser.instance.call('cool, data|I have')
|
|
108
|
+
parsed_data.to_a
|
|
109
|
+
=>['cool', 'data', 'I', 'have']
|
|
110
|
+
```
|
|
48
111
|
|
|
112
|
+
## Tags with quote
|
|
49
113
|
```ruby
|
|
50
|
-
|
|
51
|
-
|
|
114
|
+
parsed_data = Metka::GenericParser.instance.call("'cool, data', code")
|
|
115
|
+
parsed_data.to_a
|
|
116
|
+
=> ['cool, data', 'code']
|
|
52
117
|
```
|
|
53
118
|
|
|
119
|
+
## Custom parser
|
|
120
|
+
By default we use [generic_parser](lib/metka/generic_parser.rb "generic_parser")
|
|
121
|
+
If you want use your custom parser you can do:
|
|
122
|
+
```ruby
|
|
123
|
+
class Song < ActiveRecord::Base
|
|
124
|
+
include Metka::Model(column: 'tags', parser: Your::Custom::Parser.instance)
|
|
125
|
+
include Metka::Model(column: 'genres')
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
Custom parser must be a singleton class that has a `.call` method that accepts the tag string
|
|
129
|
+
|
|
54
130
|
## Tag Cloud Strategies
|
|
55
131
|
|
|
56
132
|
There are several strategies to get tag statistics
|
|
@@ -123,7 +199,31 @@ Now you can create `TaggedNote` model and work with the view like you usually do
|
|
|
123
199
|
|
|
124
200
|
Similar to the strategy above, but the view will be Materialized and refreshed with the trigger
|
|
125
201
|
|
|
126
|
-
|
|
202
|
+
```bash
|
|
203
|
+
rails g metka:strategies:materialized_view --source-table-name=NAME_OF_TABLE_WITH_TAGS
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
The code above will generate a migration that creates view to store aggregated data about tag in `NAME_OF_TABLE_WITH_TAGS` table.
|
|
207
|
+
|
|
208
|
+
Lets take a look at real example. We have a `notes` table with `tags` column.
|
|
209
|
+
|
|
210
|
+
| Column | Type | Default |
|
|
211
|
+
|--------|---------------------|-----------------------------------|
|
|
212
|
+
| id | integer | nextval('notes_id_seq'::regclass) |
|
|
213
|
+
| body | text | |
|
|
214
|
+
| tags | character varying[] | '{}'::character varying[] |
|
|
215
|
+
|
|
216
|
+
Now lets generate a migration.
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
rails g metka:strategies:materialized_view --source-table-name=notes
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
The migration code you can see [here](spec/dummy/db/migrate/05_create_tagged_materialized_view_Songs_materialized_view.rb "here")
|
|
223
|
+
|
|
224
|
+
Now lets take a look at `tagged_notes` materialized view.
|
|
225
|
+
|
|
226
|
+
Now you can create `TaggedNote` model and work with the view like you usually do with Rails models.
|
|
127
227
|
|
|
128
228
|
### Table Strategy with Triggers
|
|
129
229
|
|
|
@@ -131,6 +231,11 @@ TBD
|
|
|
131
231
|
|
|
132
232
|
TBD
|
|
133
233
|
|
|
234
|
+
## Inspired by
|
|
235
|
+
1. [ActsAsTaggableOn](https://github.com/mbleigh/acts-as-taggable-on)
|
|
236
|
+
2. [ActsAsTaggableArrayOn](https://github.com/tmiyamon/acts-as-taggable-array-on)
|
|
237
|
+
3. [TagColumns](https://github.com/hopsoft/tag_columns)
|
|
238
|
+
|
|
134
239
|
## Development
|
|
135
240
|
|
|
136
241
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
@@ -141,6 +246,10 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
|
141
246
|
|
|
142
247
|
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/metka. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
143
248
|
|
|
249
|
+
## Credits
|
|
250
|
+

|
|
251
|
+
Metka is maintained by [JetRockets](http://www.jetrockets.ru).
|
|
252
|
+
|
|
144
253
|
## License
|
|
145
254
|
|
|
146
255
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/bin/setup
CHANGED
|
@@ -3,38 +3,45 @@
|
|
|
3
3
|
class <%= @migration_class_name %> < ActiveRecord::Migration<%= ActiveRecord::VERSION::MAJOR < 5 ? '' : '[5.0]' %>
|
|
4
4
|
def up
|
|
5
5
|
execute <<-SQL
|
|
6
|
-
CREATE
|
|
7
|
-
OR REPLACE FUNCTION metka_refresh_<%= view_name %>_materialized_view RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
|
6
|
+
CREATE OR REPLACE FUNCTION metka_refresh_<%= view_name %>_materialized_view() RETURNS trigger LANGUAGE plpgsql AS $$
|
|
8
7
|
BEGIN
|
|
9
8
|
IF TG_OP = 'INSERT' AND NEW.<%= source_column_name %> IS NOT NULL THEN
|
|
10
9
|
REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
END IF;
|
|
11
|
+
|
|
12
|
+
IF TG_OP = 'UPDATE' AND OLD.<%= source_column_name %> != NEW.<%= source_column_name %> THEN
|
|
14
13
|
REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
|
|
15
14
|
END IF;
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
IF TG_OP = 'DELETE' AND OLD.<%= source_column_name %> IS NOT NULL THEN
|
|
17
|
+
REFRESH MATERIALIZED VIEW CONCURRENTLY <%= view_name %>;
|
|
18
|
+
END IF;
|
|
19
|
+
RETURN NEW;
|
|
20
|
+
END $$;
|
|
19
21
|
|
|
20
|
-
|
|
22
|
+
DROP MATERIALIZED VIEW IF EXISTS <%= view_name %>;
|
|
23
|
+
CREATE MATERIALIZED VIEW <%= view_name %> AS
|
|
24
|
+
SELECT UNNEST
|
|
25
|
+
( <%= source_column_name %> ) AS <%= source_column_name.singularize %>_name,
|
|
26
|
+
COUNT ( * ) AS taggings_count
|
|
27
|
+
FROM
|
|
28
|
+
<%= source_table_name %>
|
|
29
|
+
GROUP BY
|
|
30
|
+
<%= source_column_name.singularize %>_name;
|
|
21
31
|
|
|
22
|
-
|
|
23
|
-
( <%= source_column_name %> ) AS <%= source_column_name.singularize %>_name,
|
|
24
|
-
COUNT ( * ) AS taggings_count
|
|
25
|
-
FROM
|
|
26
|
-
<%= source_table_name %>
|
|
27
|
-
GROUP BY
|
|
28
|
-
<%= source_column_name.singularize %>_name;
|
|
32
|
+
CREATE UNIQUE INDEX idx_<%= source_table_name %>_<%= source_column_name %> ON <%= view_name %>(<%= source_column_name.singularize %>_name);
|
|
29
33
|
|
|
30
34
|
CREATE TRIGGER metka_on_<%= source_table_name %>
|
|
31
|
-
|
|
32
|
-
EXECUTE PROCEDURE metka_refresh_<%= view_name %>_materialized_view();
|
|
35
|
+
AFTER UPDATE OR INSERT OR DELETE ON <%= source_table_name %> FOR EACH ROW
|
|
36
|
+
EXECUTE PROCEDURE metka_refresh_<%= view_name %>_materialized_view();
|
|
37
|
+
SQL
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
def down
|
|
36
41
|
execute <<-SQL
|
|
37
|
-
DROP
|
|
42
|
+
DROP TRIGGER IF EXISTS metka_on_<%= source_table_name %> ON <%= source_table_name %>;
|
|
43
|
+
DROP FUNCTION IF EXISTS metka_refresh_<%= view_name %>_materialized_view;
|
|
44
|
+
DROP MATERIALIZED VIEW IF EXISTS <%= view_name %>;
|
|
38
45
|
SQL
|
|
39
46
|
end
|
|
40
47
|
end
|
data/lib/metka/generic_parser.rb
CHANGED
|
@@ -16,11 +16,37 @@ module Metka
|
|
|
16
16
|
TagList.new.tap do |tag_list|
|
|
17
17
|
case value
|
|
18
18
|
when String
|
|
19
|
-
|
|
19
|
+
value = value.to_s.dup
|
|
20
|
+
gsub_quote_pattern!(tag_list, value, double_quote_pattern)
|
|
21
|
+
gsub_quote_pattern!(tag_list, value, single_quote_pattern)
|
|
22
|
+
|
|
23
|
+
tag_list.merge value.split(Regexp.new joined_delimiter).map(&:strip).reject(&:empty?)
|
|
20
24
|
when Enumerable
|
|
21
25
|
tag_list.merge value.reject(&:empty?)
|
|
22
26
|
end
|
|
23
27
|
end
|
|
24
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def gsub_quote_pattern!(tag_list, value, pattern)
|
|
33
|
+
value.gsub!(pattern) {
|
|
34
|
+
tag_list.add(Regexp.last_match[2])
|
|
35
|
+
''
|
|
36
|
+
}
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def joined_delimiter
|
|
40
|
+
delimeter = Metka.config.delimiter
|
|
41
|
+
delimeter.is_a?(Array) ? delimeter.join('|') : delimeter
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def single_quote_pattern
|
|
45
|
+
/(\A|#{joined_delimiter})\s*'(.*?)'\s*(?=#{joined_delimiter}\s*|\z)/
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def double_quote_pattern
|
|
49
|
+
/(\A|#{joined_delimiter})\s*"(.*?)"\s*(?=#{joined_delimiter}\s*|\z)/
|
|
50
|
+
end
|
|
25
51
|
end
|
|
26
52
|
end
|
data/lib/metka/model.rb
CHANGED
|
@@ -1,33 +1,47 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'active_support/concern'
|
|
4
|
-
|
|
5
3
|
module Metka
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
included do
|
|
11
|
-
scope :tagged_with, ->(tags, options = {}) do
|
|
12
|
-
tag_list = Metka.config.parser.instance.call(tags)
|
|
13
|
-
options = options.dup
|
|
14
|
-
|
|
15
|
-
return none if tag_list.empty?
|
|
4
|
+
def self.Model(column:, **options)
|
|
5
|
+
Metka::Model.new(column: column, **options)
|
|
6
|
+
end
|
|
16
7
|
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
class Model < Module
|
|
9
|
+
def initialize(column: , **options)
|
|
10
|
+
@column = column
|
|
11
|
+
@options = options
|
|
19
12
|
end
|
|
20
13
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
def included(base)
|
|
15
|
+
column = @column
|
|
16
|
+
parser = ->(tags) {
|
|
17
|
+
@options[:parser] ? @options[:parser].call(tags) : Metka.config.parser.instance.call(tags)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
search_by_tags = ->(model, tags, column, **options) {
|
|
21
|
+
parsed_tag_list = parser.call(tags)
|
|
22
|
+
if options[:without].present?
|
|
23
|
+
model.where.not(::Metka::QueryBuilder.new.call(model, column, parsed_tag_list, options))
|
|
24
|
+
else
|
|
25
|
+
return model.none if parsed_tag_list.empty?
|
|
26
|
+
model.where(::Metka::QueryBuilder.new.call(model, column, parsed_tag_list, options))
|
|
27
|
+
end
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
base.class_eval do
|
|
31
|
+
scope "with_all_#{column}", ->(tags) { search_by_tags.call(self, tags, column) }
|
|
32
|
+
scope "with_any_#{column}", ->(tags) { search_by_tags.call(self, tags, column, { any: true }) }
|
|
33
|
+
scope "without_all_#{column}", ->(tags) { search_by_tags.call(self, tags, column, { exclude_all: true, without: true }) }
|
|
34
|
+
scope "without_any_#{column}", ->(tags) { search_by_tags.call(self, tags, column, { exclude_any: true, without: true }) }
|
|
35
|
+
end
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
37
|
+
base.define_method(column.singularize + '_list=') do |v|
|
|
38
|
+
self.write_attribute(column, parser.call(v).to_a)
|
|
39
|
+
self.write_attribute(column, nil) if self.send(column).empty?
|
|
40
|
+
end
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
base.define_method(column.singularize + '_list') do
|
|
43
|
+
parser.call(self.send(column))
|
|
44
|
+
end
|
|
31
45
|
end
|
|
32
46
|
end
|
|
33
|
-
end
|
|
47
|
+
end
|
data/lib/metka/query_builder.rb
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'query_builder/
|
|
3
|
+
require_relative 'query_builder/base_query'
|
|
4
|
+
require_relative 'query_builder/exclude_all_tags_query'
|
|
5
|
+
require_relative 'query_builder/exclude_any_tags_query'
|
|
4
6
|
require_relative 'query_builder/any_tags_query'
|
|
5
7
|
require_relative 'query_builder/all_tags_query'
|
|
6
8
|
|
|
7
9
|
module Metka
|
|
8
10
|
class QueryBuilder
|
|
9
11
|
def call(taggable_model, column, tag_list, options)
|
|
10
|
-
if options[:
|
|
11
|
-
|
|
12
|
+
if options[:exclude_all].present?
|
|
13
|
+
ExcludeAllTagsQuery.instance.call(taggable_model, column, tag_list)
|
|
14
|
+
elsif options[:exclude_any].present?
|
|
15
|
+
ExcludeAnyTagsQuery.instance.call(taggable_model, column, tag_list)
|
|
12
16
|
elsif options[:any].present?
|
|
13
17
|
AnyTagsQuery.instance.call(taggable_model, column, tag_list)
|
|
14
18
|
else
|
|
@@ -1,26 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Metka
|
|
4
|
-
class AllTagsQuery
|
|
5
|
-
|
|
4
|
+
class AllTagsQuery < BaseQuery
|
|
5
|
+
private
|
|
6
6
|
|
|
7
|
-
def
|
|
8
|
-
|
|
9
|
-
'CAST',
|
|
10
|
-
[model.arel_table[column_name].as('text[]')]
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
value = Arel::Nodes::SqlLiteral.new(
|
|
14
|
-
# In Rails 5.2 and above Sanitanization moved to public level, but still we have to support 4.2 and 5.0 and 5.1
|
|
15
|
-
ActiveRecord::Base.send(:sanitize_sql_for_conditions, ['ARRAY[?]', tag_list.to_a])
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
value_cast = Arel::Nodes::NamedFunction.new(
|
|
19
|
-
'CAST',
|
|
20
|
-
[value.as('text[]')]
|
|
21
|
-
)
|
|
22
|
-
|
|
23
|
-
Arel::Nodes::InfixOperation.new('@>', column_cast, value_cast)
|
|
7
|
+
def infix_operator
|
|
8
|
+
'@>'
|
|
24
9
|
end
|
|
25
10
|
end
|
|
26
11
|
end
|
|
@@ -1,28 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'singleton'
|
|
4
|
-
|
|
5
3
|
module Metka
|
|
6
|
-
class AnyTagsQuery
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def call(model, column_name, tag_list)
|
|
10
|
-
column_cast = Arel::Nodes::NamedFunction.new(
|
|
11
|
-
'CAST',
|
|
12
|
-
[model.arel_table[column_name].as('text[]')]
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
value = Arel::Nodes::SqlLiteral.new(
|
|
16
|
-
# In Rails 5.2 and above Sanitanization moved to public level, but still we have to support 4.2 and 5.0 and 5.1
|
|
17
|
-
ActiveRecord::Base.send(:sanitize_sql_for_conditions, ['ARRAY[?]', tag_list.to_a])
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
value_cast = Arel::Nodes::NamedFunction.new(
|
|
21
|
-
'CAST',
|
|
22
|
-
[value.as('text[]')]
|
|
23
|
-
)
|
|
4
|
+
class AnyTagsQuery < BaseQuery
|
|
5
|
+
private
|
|
24
6
|
|
|
25
|
-
|
|
7
|
+
def infix_operator
|
|
8
|
+
'&&'
|
|
26
9
|
end
|
|
27
10
|
end
|
|
28
11
|
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require 'singleton'
|
|
3
|
+
|
|
4
|
+
module Metka
|
|
5
|
+
class BaseQuery
|
|
6
|
+
include Singleton
|
|
7
|
+
|
|
8
|
+
def call(model, column_name, tag_list)
|
|
9
|
+
column_cast = Arel::Nodes::NamedFunction.new(
|
|
10
|
+
'CAST',
|
|
11
|
+
[model.arel_table[column_name].as('text[]')]
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
value = Arel::Nodes::SqlLiteral.new(
|
|
15
|
+
# In Rails 5.2 and above Sanitanization moved to public level, but still we have to support 4.2 and 5.0 and 5.1
|
|
16
|
+
ActiveRecord::Base.send(:sanitize_sql_for_conditions, ['ARRAY[?]', tag_list.to_a])
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
value_cast = Arel::Nodes::NamedFunction.new(
|
|
20
|
+
'CAST',
|
|
21
|
+
[value.as('text[]')]
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
Arel::Nodes::InfixOperation.new(infix_operator, column_cast, value_cast)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
data/lib/metka/version.rb
CHANGED
data/metka.gemspec
CHANGED
|
@@ -7,8 +7,8 @@ require 'metka/version'
|
|
|
7
7
|
Gem::Specification.new do |spec|
|
|
8
8
|
spec.name = 'metka'
|
|
9
9
|
spec.version = Metka::VERSION
|
|
10
|
-
spec.authors = ['Igor Alexandrov']
|
|
11
|
-
spec.email = ['igor.alexandrov@gmail.com']
|
|
10
|
+
spec.authors = ['Igor Alexandrov', 'Andrey Morozov']
|
|
11
|
+
spec.email = ['igor.alexandrov@gmail.com', 'andrey.morozov@jetrockets.ru']
|
|
12
12
|
|
|
13
13
|
spec.summary = 'Rails tagging system based on PostgreSQL arrays'
|
|
14
14
|
spec.description = 'Rails tagging system based on PostgreSQL arrays'
|
|
@@ -25,15 +25,17 @@ Gem::Specification.new do |spec|
|
|
|
25
25
|
spec.require_paths = ['lib']
|
|
26
26
|
|
|
27
27
|
spec.add_dependency 'dry-configurable'
|
|
28
|
-
spec.add_dependency 'rails', '
|
|
28
|
+
spec.add_dependency 'rails', '~> 5.1'
|
|
29
29
|
|
|
30
30
|
spec.add_development_dependency 'ammeter'
|
|
31
|
+
spec.add_development_dependency 'pry', '~> 0.12.2'
|
|
31
32
|
spec.add_development_dependency 'bundler'
|
|
32
33
|
spec.add_development_dependency 'faker'
|
|
33
34
|
spec.add_development_dependency 'jetrockets-standard', '~> 1.0.1'
|
|
34
35
|
spec.add_development_dependency 'pg'
|
|
35
36
|
spec.add_development_dependency 'rake'
|
|
36
|
-
spec.add_development_dependency 'rspec'
|
|
37
|
-
spec.add_development_dependency 'rspec-rails'
|
|
37
|
+
spec.add_development_dependency 'rspec', '~> 3.9'
|
|
38
|
+
spec.add_development_dependency 'rspec-rails', '~> 3.9'
|
|
38
39
|
spec.add_development_dependency 'timecop'
|
|
40
|
+
spec.add_development_dependency 'database_cleaner'
|
|
39
41
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: metka
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Igor Alexandrov
|
|
8
|
+
- Andrey Morozov
|
|
8
9
|
autorequire:
|
|
9
10
|
bindir: exe
|
|
10
11
|
cert_chain: []
|
|
11
|
-
date: 2019-
|
|
12
|
+
date: 2019-12-13 00:00:00.000000000 Z
|
|
12
13
|
dependencies:
|
|
13
14
|
- !ruby/object:Gem::Dependency
|
|
14
15
|
name: dry-configurable
|
|
@@ -28,16 +29,16 @@ dependencies:
|
|
|
28
29
|
name: rails
|
|
29
30
|
requirement: !ruby/object:Gem::Requirement
|
|
30
31
|
requirements:
|
|
31
|
-
- - "
|
|
32
|
+
- - "~>"
|
|
32
33
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
34
|
+
version: '5.1'
|
|
34
35
|
type: :runtime
|
|
35
36
|
prerelease: false
|
|
36
37
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
38
|
requirements:
|
|
38
|
-
- - "
|
|
39
|
+
- - "~>"
|
|
39
40
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
41
|
+
version: '5.1'
|
|
41
42
|
- !ruby/object:Gem::Dependency
|
|
42
43
|
name: ammeter
|
|
43
44
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -52,6 +53,20 @@ dependencies:
|
|
|
52
53
|
- - ">="
|
|
53
54
|
- !ruby/object:Gem::Version
|
|
54
55
|
version: '0'
|
|
56
|
+
- !ruby/object:Gem::Dependency
|
|
57
|
+
name: pry
|
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
|
59
|
+
requirements:
|
|
60
|
+
- - "~>"
|
|
61
|
+
- !ruby/object:Gem::Version
|
|
62
|
+
version: 0.12.2
|
|
63
|
+
type: :development
|
|
64
|
+
prerelease: false
|
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - "~>"
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: 0.12.2
|
|
55
70
|
- !ruby/object:Gem::Dependency
|
|
56
71
|
name: bundler
|
|
57
72
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -126,18 +141,32 @@ dependencies:
|
|
|
126
141
|
name: rspec
|
|
127
142
|
requirement: !ruby/object:Gem::Requirement
|
|
128
143
|
requirements:
|
|
129
|
-
- - "
|
|
144
|
+
- - "~>"
|
|
130
145
|
- !ruby/object:Gem::Version
|
|
131
|
-
version: '
|
|
146
|
+
version: '3.9'
|
|
132
147
|
type: :development
|
|
133
148
|
prerelease: false
|
|
134
149
|
version_requirements: !ruby/object:Gem::Requirement
|
|
135
150
|
requirements:
|
|
136
|
-
- - "
|
|
151
|
+
- - "~>"
|
|
137
152
|
- !ruby/object:Gem::Version
|
|
138
|
-
version: '
|
|
153
|
+
version: '3.9'
|
|
139
154
|
- !ruby/object:Gem::Dependency
|
|
140
155
|
name: rspec-rails
|
|
156
|
+
requirement: !ruby/object:Gem::Requirement
|
|
157
|
+
requirements:
|
|
158
|
+
- - "~>"
|
|
159
|
+
- !ruby/object:Gem::Version
|
|
160
|
+
version: '3.9'
|
|
161
|
+
type: :development
|
|
162
|
+
prerelease: false
|
|
163
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
164
|
+
requirements:
|
|
165
|
+
- - "~>"
|
|
166
|
+
- !ruby/object:Gem::Version
|
|
167
|
+
version: '3.9'
|
|
168
|
+
- !ruby/object:Gem::Dependency
|
|
169
|
+
name: timecop
|
|
141
170
|
requirement: !ruby/object:Gem::Requirement
|
|
142
171
|
requirements:
|
|
143
172
|
- - ">="
|
|
@@ -151,7 +180,7 @@ dependencies:
|
|
|
151
180
|
- !ruby/object:Gem::Version
|
|
152
181
|
version: '0'
|
|
153
182
|
- !ruby/object:Gem::Dependency
|
|
154
|
-
name:
|
|
183
|
+
name: database_cleaner
|
|
155
184
|
requirement: !ruby/object:Gem::Requirement
|
|
156
185
|
requirements:
|
|
157
186
|
- - ">="
|
|
@@ -167,6 +196,7 @@ dependencies:
|
|
|
167
196
|
description: Rails tagging system based on PostgreSQL arrays
|
|
168
197
|
email:
|
|
169
198
|
- igor.alexandrov@gmail.com
|
|
199
|
+
- andrey.morozov@jetrockets.ru
|
|
170
200
|
executables: []
|
|
171
201
|
extensions: []
|
|
172
202
|
extra_rdoc_files: []
|
|
@@ -184,6 +214,9 @@ files:
|
|
|
184
214
|
- Rakefile
|
|
185
215
|
- bin/console
|
|
186
216
|
- bin/setup
|
|
217
|
+
- gemfiles/rails5.gemfile
|
|
218
|
+
- gemfiles/rails52.gemfile
|
|
219
|
+
- gemfiles/rails6.gemfile
|
|
187
220
|
- lib/generators/metka/strategies/materialized_view/materialized_view_generator.rb
|
|
188
221
|
- lib/generators/metka/strategies/materialized_view/templates/migration.rb.erb
|
|
189
222
|
- lib/generators/metka/strategies/view/templates/migration.rb.erb
|
|
@@ -194,7 +227,9 @@ files:
|
|
|
194
227
|
- lib/metka/query_builder.rb
|
|
195
228
|
- lib/metka/query_builder/all_tags_query.rb
|
|
196
229
|
- lib/metka/query_builder/any_tags_query.rb
|
|
197
|
-
- lib/metka/query_builder/
|
|
230
|
+
- lib/metka/query_builder/base_query.rb
|
|
231
|
+
- lib/metka/query_builder/exclude_all_tags_query.rb
|
|
232
|
+
- lib/metka/query_builder/exclude_any_tags_query.rb
|
|
198
233
|
- lib/metka/tag_list.rb
|
|
199
234
|
- lib/metka/version.rb
|
|
200
235
|
- metka.gemspec
|
|
@@ -217,7 +252,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
217
252
|
- !ruby/object:Gem::Version
|
|
218
253
|
version: '0'
|
|
219
254
|
requirements: []
|
|
220
|
-
|
|
255
|
+
rubyforge_project:
|
|
256
|
+
rubygems_version: 2.7.6
|
|
221
257
|
signing_key:
|
|
222
258
|
specification_version: 4
|
|
223
259
|
summary: Rails tagging system based on PostgreSQL arrays
|