opinions 0.0.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.
- data/.autotest +18 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +183 -0
- data/Rakefile +30 -0
- data/lib/opinions.rb +270 -0
- data/lib/opinions/version.rb +3 -0
- data/opinions.gemspec +27 -0
- data/test/acceptance_test_key_loader.rb +20 -0
- data/test/acceptance_test_opinions_opinion.rb +110 -0
- data/test/acceptance_test_opinions_opinionated_mixin.rb +140 -0
- data/test/acceptance_test_opinions_pollable_mixin.rb +111 -0
- data/test/integration_test_opinions_opinion.rb +60 -0
- data/test/integration_test_opinions_opinionated_mixin.rb +70 -0
- data/test/integration_test_opinions_pollable_mixin.rb +71 -0
- data/test/opinions_integration_test_redis.conf +6 -0
- data/test/test_helper.rb +112 -0
- data/test/unit_test_opinions_key_builder.rb +56 -0
- data/test/unit_test_opinions_opinion.rb +72 -0
- metadata +137 -0
data/.autotest
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'autotest/restart'
|
2
|
+
|
3
|
+
class Autotest
|
4
|
+
|
5
|
+
# def get_to_green
|
6
|
+
# begin
|
7
|
+
# rerun_all_tests
|
8
|
+
# wait_for_changes unless all_good
|
9
|
+
# end until all_good
|
10
|
+
# end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
Autotest.add_hook :initialize do |at|
|
15
|
+
at.add_mapping(/^test.*\/.*_test_.*rb$/) do |filename, _|
|
16
|
+
filename
|
17
|
+
end
|
18
|
+
end
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Lee Hambley
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
# Opinions
|
2
|
+
|
3
|
+
**Store opinions in Redis.** *Opinions* allows the storage of opinions in
|
4
|
+
Redis, a fast and atomic structured data store. If one's users *hate*, *love*,
|
5
|
+
*appreciate*, *despise* or *just-don-t-care*, one can store that easily via a
|
6
|
+
simple API.
|
7
|
+
|
8
|
+
It is not bound to *ActiveRecord*, or any other big libraries, any class exposing a
|
9
|
+
public `id` method can be used. The `id` method is not required to be
|
10
|
+
numerical.
|
11
|
+
|
12
|
+
``` ruby
|
13
|
+
class CatPicture < ActiveRecord::Base
|
14
|
+
|
15
|
+
include Opinions::Pollable
|
16
|
+
opinions :like, :dislike
|
17
|
+
|
18
|
+
end
|
19
|
+
```
|
20
|
+
|
21
|
+
This simple example shows that in our logical model cat pictures can either be
|
22
|
+
liked, or disliked. The following methods are available to all instances of `CatPicture`:
|
23
|
+
|
24
|
+
* `like_by(...)`
|
25
|
+
* `cancel_like_by(...)`
|
26
|
+
* `like_votes()`
|
27
|
+
* `dialike_by(...)`
|
28
|
+
* `cancel_dislike_by(...)`
|
29
|
+
* `dislike_votes()`
|
30
|
+
|
31
|
+
On the flip-side, one needs a way to share one's feelings, from the model representing
|
32
|
+
a user, or rater, or similar, one can easily use the opposite:
|
33
|
+
|
34
|
+
``` ruby
|
35
|
+
class User < ActiveRecord::Base
|
36
|
+
|
37
|
+
include Opinions::Opinionated
|
38
|
+
opinions :like, :dislike
|
39
|
+
|
40
|
+
end
|
41
|
+
```
|
42
|
+
|
43
|
+
This module will mix-into the `User` the following methods:
|
44
|
+
|
45
|
+
* `like(...)`
|
46
|
+
* `dislike(...)`
|
47
|
+
* `cancel_like(...)`
|
48
|
+
* `cancel_dislike(...)`
|
49
|
+
* `like_opinions()`
|
50
|
+
* `dislike_opinions()`
|
51
|
+
* `have_like_on(...)`
|
52
|
+
* `have_dislike_on(...)`
|
53
|
+
|
54
|
+
These methods can be passed instances of any class which has those opinions defined.
|
55
|
+
|
56
|
+
It should be absolutely trivial to extend these to any behaviour you
|
57
|
+
need.
|
58
|
+
|
59
|
+
**Note:** It is by design that these methods do not read particularly
|
60
|
+
naturally, you are invited to read the source, and tests of the
|
61
|
+
Pollable, and Opinionated modules and implement them in a way which
|
62
|
+
better reflects the grammar of your application, and desired *Opinions*.
|
63
|
+
Think of these modules as examples, something to expand upon.
|
64
|
+
|
65
|
+
## Inspiration
|
66
|
+
|
67
|
+
*Opinions* is inspired by [`schneems/likeable`](https://github.com/schneems/Likeable). A few
|
68
|
+
things concerned me about that project, so I wrote *opinions* after contributing
|
69
|
+
significant fixes to *likeable*.
|
70
|
+
|
71
|
+
### What's different from *Likeable*?
|
72
|
+
|
73
|
+
* There are no hard-coded assumptions about which opinions you'll be using, that's
|
74
|
+
up to your project needs.
|
75
|
+
|
76
|
+
* There are no callbacks, these are better handled with observers, either in the
|
77
|
+
classical OOP meaning of the word, or your framework's pattern. (In Rails they're
|
78
|
+
the same thing)
|
79
|
+
|
80
|
+
* A *very* comprehensive test suite, written with *MiniTest*. *Likeable* is quite
|
81
|
+
simple, and has about ~35 tests, that might be OK for you, and Gowalla, but I'd
|
82
|
+
feel better with real unit, functional and integration tests.
|
83
|
+
|
84
|
+
* It's not *totally* bound to Redis. Internally there's a Key/Value store proxy, this
|
85
|
+
uses Redis out of the box, but it should be easy for someone to replace this with
|
86
|
+
MongoDB, Riak, DynamoDB, SQLite, etc.
|
87
|
+
|
88
|
+
* It does not depend on *ActiveSupport*, *likeable* depends on *keytar*, which depends
|
89
|
+
on `ActiveSupport` for inflection and *ActiveSupport::Concern.*
|
90
|
+
|
91
|
+
* It does not depend on `Keytar`, Keytar is a handy tool for building NoSQL keys for
|
92
|
+
objects, however it's a little bit over-featured for this use-case.
|
93
|
+
|
94
|
+
* *Likeable* stores timestamps as floating point numbers, I'm confused
|
95
|
+
by this. Sub-second resoltion seems unusual here, and isn't easy for a
|
96
|
+
human being to read. *Opinions* uses the time format: `%Y-%m-%d %H:%M:%S %z`.
|
97
|
+
|
98
|
+
* *Likeable* doesn't store *symetrical* relationships, using *Likeable*
|
99
|
+
it's only possible to have one type of object sharing opinions on any
|
100
|
+
other (*Users*). *Opinions* stores the relationship symetrically, so
|
101
|
+
many kinds of objects can store many kinds of opinions.
|
102
|
+
|
103
|
+
* *Likeable* stores the class name unaltered, this can cause problems
|
104
|
+
with namespaced classes as the class namespace separator in Ruby is
|
105
|
+
`::`, this conflicts with the sepatator traditionally used in Redis.
|
106
|
+
*Opinions* stores the class names processed with an *ActiveSupport*
|
107
|
+
inspired `underscore` method which uses the forward slash character to
|
108
|
+
represent a namespace delimiter.
|
109
|
+
|
110
|
+
## Migrating from *Likeable*
|
111
|
+
|
112
|
+
Unfortunately the key structure is sufficiently different that you'll
|
113
|
+
need to explicitly migrate, there's no shortcut. The key to migrating
|
114
|
+
sucessfully is that the `vote(target)` and `vote_by(object)` methods
|
115
|
+
take an optional time parameter in the second position. If this is
|
116
|
+
passed then it will
|
117
|
+
|
118
|
+
## Installation
|
119
|
+
|
120
|
+
Add this line to your application's Gemfile:
|
121
|
+
|
122
|
+
gem 'opinions'
|
123
|
+
|
124
|
+
And then execute:
|
125
|
+
|
126
|
+
$ bundle
|
127
|
+
|
128
|
+
Or install it yourself as:
|
129
|
+
|
130
|
+
$ gem install opinions
|
131
|
+
|
132
|
+
## Usage
|
133
|
+
|
134
|
+
TODO: Write usage instructions here
|
135
|
+
|
136
|
+
## Contributing
|
137
|
+
|
138
|
+
1. Fork it
|
139
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
140
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
141
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
142
|
+
5. Create new Pull Request
|
143
|
+
|
144
|
+
|
145
|
+
## Sample Key Structure
|
146
|
+
|
147
|
+
Given the following example, the key
|
148
|
+
structure would be:
|
149
|
+
|
150
|
+
``` ruby
|
151
|
+
class Recommendation
|
152
|
+
iclude Opinions::Pollable
|
153
|
+
opinions :like
|
154
|
+
end
|
155
|
+
|
156
|
+
class User
|
157
|
+
include Opinions::Opinionated
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
User.find(123).like(Recommendation.find(789)
|
162
|
+
User.find(123).like(Recommendation.find(987)
|
163
|
+
|
164
|
+
User.find(321).like(Recommendation.find(789)
|
165
|
+
```
|
166
|
+
|
167
|
+
The resulting Redis structure would be something like this:
|
168
|
+
|
169
|
+
``` text
|
170
|
+
user:like:123:recommendation
|
171
|
+
"789" "2012-11-13 00:01:02 +01:00"
|
172
|
+
"987" "2011-02-01 00:03:01 +01:00"
|
173
|
+
|
174
|
+
user:like:321:recommendation
|
175
|
+
"789" "2014-02-01 17:15:01 +01:00"
|
176
|
+
|
177
|
+
recommendation:like:789:user
|
178
|
+
"123" "2012-11-13 00:01:02 +01:00"
|
179
|
+
"321" "2014-02-01 17:15:01 +01:00"
|
180
|
+
|
181
|
+
recommendation:like:987:user
|
182
|
+
"123" "2011-02-01 00:03:01 +01:00"
|
183
|
+
```
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
namespace :test do
|
6
|
+
|
7
|
+
Rake::TestTask.new(:units) do |t|
|
8
|
+
t.libs << "test"
|
9
|
+
t.test_files = FileList['test/unit_test*.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
Rake::TestTask.new(:acceptance) do |t|
|
13
|
+
t.libs << "test"
|
14
|
+
t.test_files = FileList['test/acceptance_test*.rb']
|
15
|
+
end
|
16
|
+
|
17
|
+
Rake::TestTask.new(:integration) do |t|
|
18
|
+
t.libs << "test"
|
19
|
+
t.test_files = FileList['test/integration_test*.rb']
|
20
|
+
end
|
21
|
+
|
22
|
+
task :default do
|
23
|
+
Rake::Task['test:units'].execute
|
24
|
+
Rake::Task['test:acceptance'].execute
|
25
|
+
Rake::Task['test:integration'].execute
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
task :default => 'test:default'
|
data/lib/opinions.rb
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
require 'opinions/version'
|
2
|
+
require 'singleton'
|
3
|
+
require 'redis'
|
4
|
+
|
5
|
+
module Opinions
|
6
|
+
|
7
|
+
class << self
|
8
|
+
attr_accessor :backend
|
9
|
+
end
|
10
|
+
|
11
|
+
module KeyBuilderExtensions
|
12
|
+
|
13
|
+
def generate_key(scope, id = nil)
|
14
|
+
[self.class.name, scope, id].compact.join(':')
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
class KeyBuilder
|
20
|
+
|
21
|
+
def initialize(args)
|
22
|
+
@object = args.fetch(:object)
|
23
|
+
@target = args.fetch(:target, nil)
|
24
|
+
@opinion = args.fetch(:opinion)
|
25
|
+
end
|
26
|
+
|
27
|
+
def key
|
28
|
+
object = @object.dup
|
29
|
+
object.class.send(:include, KeyBuilderExtensions)
|
30
|
+
key = object.generate_key(@opinion, object.id)
|
31
|
+
if @target
|
32
|
+
tcn = @target.class == Class ? @target.name : @target.class.name
|
33
|
+
key += ":#{tcn}"
|
34
|
+
end
|
35
|
+
key
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
class OpinionFactory
|
41
|
+
attr_reader :key_name
|
42
|
+
def initialize(args)
|
43
|
+
@direction = args.has_key?(:from_target) ? :ot : :to
|
44
|
+
@key_name = args.first[1]
|
45
|
+
end
|
46
|
+
def opinion
|
47
|
+
Opinions.backend.read_key(key_name).collect do |object_id, time|
|
48
|
+
target_class_name, opinion, target_id, object_class_name = key_name.split ':'
|
49
|
+
target_class, object_class = Kernel.const_get(target_class_name), Kernel.const_get(object_class_name)
|
50
|
+
Opinion.new(target: (@direction == :to ? object_class.find(object_id) : target_class.find(target_id)),
|
51
|
+
object: (@direction == :to ? target_class.find(target_id) : object_class.find(object_id)),
|
52
|
+
opinion: opinion.to_sym,
|
53
|
+
created_at: time)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class RedisBackend
|
59
|
+
|
60
|
+
attr_accessor :redis
|
61
|
+
|
62
|
+
def write_keys(key_hashes)
|
63
|
+
redis.multi do
|
64
|
+
key_hashes.each do |key_name, hash|
|
65
|
+
write_key(key_name, hash)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def write_key(key_name, hash)
|
71
|
+
hash.each do |hash_key, hash_value|
|
72
|
+
redis.hset key_name, hash_key, hash_value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
private :write_key
|
76
|
+
|
77
|
+
def read_key(key_name)
|
78
|
+
redis.hgetall(key_name)
|
79
|
+
end
|
80
|
+
|
81
|
+
def read_sub_key(key_name, key)
|
82
|
+
redis.hget(key_name, key)
|
83
|
+
end
|
84
|
+
|
85
|
+
def remove_sub_keys(key_pairs)
|
86
|
+
redis.multi do
|
87
|
+
key_pairs.each do |key_name, key|
|
88
|
+
redis.hdel(key_name, key.to_s)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def keys_matching(argument)
|
94
|
+
redis.keys(argument)
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
class KeyLoader
|
100
|
+
|
101
|
+
def initialize(key)
|
102
|
+
@object_class, @opinion, @object_id, @target_class = key.split(':')
|
103
|
+
end
|
104
|
+
|
105
|
+
def object
|
106
|
+
Object.const_get(@object_class).find(_object_id)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def _object_id
|
112
|
+
@object_id.to_i == @object_id ? @object_id : @object_id.to_i
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
class Opinion
|
118
|
+
|
119
|
+
attr_accessor :target, :object, :opinion, :created_at
|
120
|
+
|
121
|
+
def initialize(args = {})
|
122
|
+
@target, @object, @opinion, @created_at =
|
123
|
+
args.fetch(:target), args.fetch(:object), args.fetch(:opinion), args.fetch(:created_at, nil)
|
124
|
+
self
|
125
|
+
end
|
126
|
+
|
127
|
+
def persist(args = {time: Time.now.utc})
|
128
|
+
backend.write_keys({
|
129
|
+
target_key => {object.id.to_s => args.fetch(:time)},
|
130
|
+
object_key => {target.id.to_s => args.fetch(:time)},
|
131
|
+
})
|
132
|
+
end
|
133
|
+
|
134
|
+
def object_key
|
135
|
+
KeyBuilder.new(object: object, opinion: opinion, target: target).key
|
136
|
+
end
|
137
|
+
|
138
|
+
def target_key
|
139
|
+
KeyBuilder.new(object: target, opinion: opinion, target: object).key
|
140
|
+
end
|
141
|
+
|
142
|
+
def exists?
|
143
|
+
tk = backend.read_sub_key(target_key, object.id.to_s)
|
144
|
+
ok = backend.read_sub_key(object_key, target.id.to_s)
|
145
|
+
tk && ok
|
146
|
+
end
|
147
|
+
|
148
|
+
def remove
|
149
|
+
backend.remove_sub_keys([[target_key, object.id.to_s],
|
150
|
+
[object_key, target.id.to_s]])
|
151
|
+
end
|
152
|
+
|
153
|
+
def ==(other_opinion)
|
154
|
+
raise ArgumentError, "Can't compare a #{other_opinion} with #{self}" unless other_opinion.is_a?(Opinion)
|
155
|
+
opinion_equal = self.opinion == other_opinion.opinion
|
156
|
+
opinion_target = self.target == other_opinion.target
|
157
|
+
opinion_object = self.object == other_opinion.object
|
158
|
+
opinion_equal && opinion_target && opinion_object
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def backend
|
164
|
+
Opinions.backend
|
165
|
+
end
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
module Pollable
|
170
|
+
|
171
|
+
class << self
|
172
|
+
|
173
|
+
def included(klass)
|
174
|
+
klass.send(:include, InstanceMethods)
|
175
|
+
klass.send(:extend, ClassMethods)
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
module ClassMethods
|
181
|
+
def opinions(*opinions)
|
182
|
+
opinions.each { |opinion| register_opinion(opinion.to_sym) }
|
183
|
+
end
|
184
|
+
def register_opinion(name)
|
185
|
+
@registered_opinions ||= Array.new
|
186
|
+
@registered_opinions << name
|
187
|
+
end
|
188
|
+
def registered_opinions
|
189
|
+
@registered_opinions
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
module InstanceMethods
|
194
|
+
|
195
|
+
def initialize(*args)
|
196
|
+
super
|
197
|
+
self.class.registered_opinions.each do |opinion|
|
198
|
+
self.class.send :define_method, :"#{opinion}_by" do |*args|
|
199
|
+
opinionated, time = *args
|
200
|
+
time = time || Time.now.utc
|
201
|
+
e = Opinion.new(object: opinionated, target: self, opinion: opinion)
|
202
|
+
true & e.persist(time: time)
|
203
|
+
end
|
204
|
+
self.class.send :define_method, :"cancel_#{opinion}_by" do |opinionated|
|
205
|
+
true & Opinion.new(object: opinionated, target: self, opinion: opinion).remove
|
206
|
+
end
|
207
|
+
self.class.send :define_method, :"#{opinion}_votes" do
|
208
|
+
lookup_key_builder = KeyBuilder.new(object: self, opinion: opinion)
|
209
|
+
keys = Opinions.backend.keys_matching(lookup_key_builder.key + "*")
|
210
|
+
keys.collect do |key_name|
|
211
|
+
OpinionFactory.new(from_target: key_name).opinion
|
212
|
+
end.flatten
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
end
|
220
|
+
|
221
|
+
module Opinionated
|
222
|
+
|
223
|
+
def self.included(klass)
|
224
|
+
klass.send(:include, InstanceMethods)
|
225
|
+
klass.send(:extend, ClassMethods)
|
226
|
+
end
|
227
|
+
|
228
|
+
module ClassMethods
|
229
|
+
def opinions(*opinions)
|
230
|
+
opinions.each { |opinion| register_opinion(opinion.to_sym) }
|
231
|
+
end
|
232
|
+
def register_opinion(name)
|
233
|
+
@registered_opinions ||= Array.new
|
234
|
+
@registered_opinions << name
|
235
|
+
end
|
236
|
+
def registered_opinions
|
237
|
+
@registered_opinions
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
module InstanceMethods
|
242
|
+
def initialize(*args)
|
243
|
+
super
|
244
|
+
self.class.registered_opinions.each do |opinion|
|
245
|
+
self.class.send :define_method, :"#{opinion}" do |*args|
|
246
|
+
target, time = *args
|
247
|
+
time = time || Time.now.utc
|
248
|
+
e = Opinion.new(object: self, target: target, opinion: opinion)
|
249
|
+
true & e.persist(time: time)
|
250
|
+
end
|
251
|
+
self.class.send :define_method, :"cancel_#{opinion}" do |pollable|
|
252
|
+
true & Opinion.new(object: self, target: pollable, opinion: opinion).remove
|
253
|
+
end
|
254
|
+
self.class.send :define_method, :"have_#{opinion}_on" do |pollable|
|
255
|
+
send("#{opinion}_opinions").collect { |o| o.target == pollable }.any?
|
256
|
+
end
|
257
|
+
self.class.send :define_method, :"#{opinion}_opinions" do
|
258
|
+
lookup_key_builder = KeyBuilder.new(object: self, opinion: opinion)
|
259
|
+
keys = Opinions.backend.keys_matching(lookup_key_builder.key + "*")
|
260
|
+
keys.collect do |key_name|
|
261
|
+
OpinionFactory.new(from_object: key_name).opinion
|
262
|
+
end.flatten
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
269
|
+
|
270
|
+
end
|