agave-client 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.travis.yml +14 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/CONTRIBUTORS.md +34 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +29 -0
- data/README.md +41 -0
- data/Rakefile +7 -0
- data/agave-client.gemspec +63 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/exe/agave +10 -0
- data/lib/agave.rb +11 -0
- data/lib/agave/api_client.rb +97 -0
- data/lib/agave/api_error.rb +23 -0
- data/lib/agave/cli.rb +93 -0
- data/lib/agave/dump/dsl/add_to_data_file.rb +14 -0
- data/lib/agave/dump/dsl/create_data_file.rb +14 -0
- data/lib/agave/dump/dsl/create_post.rb +33 -0
- data/lib/agave/dump/dsl/directory.rb +30 -0
- data/lib/agave/dump/dsl/root.rb +37 -0
- data/lib/agave/dump/format.rb +28 -0
- data/lib/agave/dump/format/json.rb +18 -0
- data/lib/agave/dump/format/toml.rb +31 -0
- data/lib/agave/dump/format/yaml.rb +55 -0
- data/lib/agave/dump/operation/add_to_data_file.rb +41 -0
- data/lib/agave/dump/operation/create_data_file.rb +28 -0
- data/lib/agave/dump/operation/create_post.rb +34 -0
- data/lib/agave/dump/operation/directory.rb +34 -0
- data/lib/agave/dump/operation/root.rb +27 -0
- data/lib/agave/dump/runner.rb +47 -0
- data/lib/agave/dump/ssg_detector.rb +36 -0
- data/lib/agave/json_api_deserializer.rb +38 -0
- data/lib/agave/json_api_serializer.rb +145 -0
- data/lib/agave/local/entities_repo.rb +46 -0
- data/lib/agave/local/field_type/boolean.rb +12 -0
- data/lib/agave/local/field_type/color.rb +63 -0
- data/lib/agave/local/field_type/date.rb +12 -0
- data/lib/agave/local/field_type/date_time.rb +12 -0
- data/lib/agave/local/field_type/file.rb +65 -0
- data/lib/agave/local/field_type/float.rb +12 -0
- data/lib/agave/local/field_type/gallery.rb +23 -0
- data/lib/agave/local/field_type/global_seo.rb +56 -0
- data/lib/agave/local/field_type/image.rb +82 -0
- data/lib/agave/local/field_type/integer.rb +12 -0
- data/lib/agave/local/field_type/json.rb +13 -0
- data/lib/agave/local/field_type/lat_lon.rb +30 -0
- data/lib/agave/local/field_type/link.rb +12 -0
- data/lib/agave/local/field_type/links.rb +21 -0
- data/lib/agave/local/field_type/rich_text.rb +21 -0
- data/lib/agave/local/field_type/seo.rb +33 -0
- data/lib/agave/local/field_type/slug.rb +12 -0
- data/lib/agave/local/field_type/string.rb +12 -0
- data/lib/agave/local/field_type/text.rb +12 -0
- data/lib/agave/local/field_type/theme.rb +43 -0
- data/lib/agave/local/field_type/video.rb +77 -0
- data/lib/agave/local/item.rb +179 -0
- data/lib/agave/local/items_repo.rb +210 -0
- data/lib/agave/local/json_api_entity.rb +78 -0
- data/lib/agave/local/loader.rb +60 -0
- data/lib/agave/local/site.rb +73 -0
- data/lib/agave/paginator.rb +33 -0
- data/lib/agave/repo.rb +93 -0
- data/lib/agave/site/client.rb +24 -0
- data/lib/agave/upload/file.rb +92 -0
- data/lib/agave/upload/image.rb +8 -0
- data/lib/agave/utils/favicon_tags_builder.rb +85 -0
- data/lib/agave/utils/locale_value.rb +15 -0
- data/lib/agave/utils/meta_tags/article_modified_time.rb +15 -0
- data/lib/agave/utils/meta_tags/article_publisher.rb +18 -0
- data/lib/agave/utils/meta_tags/base.rb +55 -0
- data/lib/agave/utils/meta_tags/description.rb +24 -0
- data/lib/agave/utils/meta_tags/image.rb +35 -0
- data/lib/agave/utils/meta_tags/og_locale.rb +15 -0
- data/lib/agave/utils/meta_tags/og_site_name.rb +18 -0
- data/lib/agave/utils/meta_tags/og_type.rb +18 -0
- data/lib/agave/utils/meta_tags/robots.rb +14 -0
- data/lib/agave/utils/meta_tags/title.rb +46 -0
- data/lib/agave/utils/meta_tags/twitter_card.rb +14 -0
- data/lib/agave/utils/meta_tags/twitter_site.rb +18 -0
- data/lib/agave/utils/seo_tags_builder.rb +45 -0
- data/lib/agave/version.rb +4 -0
- data/lib/agave/watch/site_change_watcher.rb +37 -0
- metadata +504 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Agave
|
2
|
+
module Local
|
3
|
+
module FieldType
|
4
|
+
class Theme
|
5
|
+
attr_reader :primary_color, :dark_color, :light_color, :accent_color
|
6
|
+
|
7
|
+
def self.parse(value, repo)
|
8
|
+
value && new(
|
9
|
+
value[:logo],
|
10
|
+
value[:primary_color],
|
11
|
+
value[:dark_color],
|
12
|
+
value[:light_color],
|
13
|
+
value[:accent_color],
|
14
|
+
repo
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(logo, primary_color, dark_color, light_color, accent_color, repo)
|
19
|
+
@logo = logo
|
20
|
+
@primary_color = primary_color
|
21
|
+
@dark_color = dark_color
|
22
|
+
@light_color = light_color
|
23
|
+
@accent_color = accent_color
|
24
|
+
@repo = repo
|
25
|
+
end
|
26
|
+
|
27
|
+
def logo
|
28
|
+
@logo && File.parse(@logo, @repo)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_hash(*args)
|
32
|
+
{
|
33
|
+
primary_color: primary_color,
|
34
|
+
dark_color: dark_color,
|
35
|
+
light_color: light_color,
|
36
|
+
accent_color: accent_color,
|
37
|
+
logo: logo && logo.to_hash(*args)
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'active_support/core_ext/hash/compact'
|
3
|
+
|
4
|
+
module Agave
|
5
|
+
module Local
|
6
|
+
module FieldType
|
7
|
+
class Video
|
8
|
+
attr_reader :url
|
9
|
+
attr_reader :thumbnail_url
|
10
|
+
attr_reader :title
|
11
|
+
attr_reader :width
|
12
|
+
attr_reader :height
|
13
|
+
attr_reader :provider
|
14
|
+
attr_reader :provider_url
|
15
|
+
attr_reader :provider_uid
|
16
|
+
|
17
|
+
def self.parse(value, _repo)
|
18
|
+
value && new(
|
19
|
+
value[:url],
|
20
|
+
value[:thumbnail_url],
|
21
|
+
value[:title],
|
22
|
+
value[:width],
|
23
|
+
value[:height],
|
24
|
+
value[:provider],
|
25
|
+
value[:provider_url],
|
26
|
+
value[:provider_uid]
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def initialize(
|
31
|
+
url,
|
32
|
+
thumbnail_url,
|
33
|
+
title,
|
34
|
+
width,
|
35
|
+
height,
|
36
|
+
provider,
|
37
|
+
provider_url,
|
38
|
+
provider_uid
|
39
|
+
)
|
40
|
+
@url = url
|
41
|
+
@thumbnail_url = thumbnail_url
|
42
|
+
@title = title
|
43
|
+
@width = width
|
44
|
+
@height = height
|
45
|
+
@provider = provider
|
46
|
+
@provider_url = provider_url
|
47
|
+
@provider_uid = provider_uid
|
48
|
+
end
|
49
|
+
|
50
|
+
def iframe_embed(width = self.width, height = self.height)
|
51
|
+
# rubocop:disable Metrics/LineLength
|
52
|
+
if provider == 'youtube'
|
53
|
+
%(<iframe width="#{width}" height="#{height}" src="//www.youtube.com/embed/#{provider_uid}?rel=0" frameborder="0" allowfullscreen></iframe>)
|
54
|
+
elsif provider == 'vimeo'
|
55
|
+
%(<iframe src="//player.vimeo.com/video/#{provider_uid}?title=0&byline=0&portrait=0" width="#{width}" height="#{height}" frameborder="0" webkitAllowFullScreen mozallowfullscreen allowFullScreen></iframe>)
|
56
|
+
elsif provider == 'facebook'
|
57
|
+
%(<iframe src="//www.facebook.com/plugins/video.php?href=#{url}&width=#{width}&show_text=false&height=#{height}" width="#{width}" height="#{height}" style="border:none;overflow:hidden;width:100%;" scrolling="no" frameborder="0" allowTransparency="true" allow="encrypted-media" allowFullScreen="true"></iframe>)
|
58
|
+
end
|
59
|
+
# rubocop:enable Metrics/LineLength
|
60
|
+
end
|
61
|
+
|
62
|
+
def to_hash(*_args)
|
63
|
+
{
|
64
|
+
url: url,
|
65
|
+
thumbnail_url: thumbnail_url,
|
66
|
+
title: title,
|
67
|
+
width: width,
|
68
|
+
height: height,
|
69
|
+
provider: provider,
|
70
|
+
provider_url: provider_url,
|
71
|
+
provider_uid: provider_uid
|
72
|
+
}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'forwardable'
|
3
|
+
require 'active_support/inflector/transliterate'
|
4
|
+
require 'active_support/hash_with_indifferent_access'
|
5
|
+
require 'agave/utils/locale_value'
|
6
|
+
|
7
|
+
Dir[File.dirname(__FILE__) + '/field_type/*.rb'].each do |file|
|
8
|
+
require file
|
9
|
+
end
|
10
|
+
|
11
|
+
module Agave
|
12
|
+
module Local
|
13
|
+
class Item
|
14
|
+
extend Forwardable
|
15
|
+
|
16
|
+
attr_reader :entity
|
17
|
+
def_delegators :entity, :id
|
18
|
+
|
19
|
+
def initialize(entity, items_repo)
|
20
|
+
@entity = entity
|
21
|
+
@items_repo = items_repo
|
22
|
+
end
|
23
|
+
|
24
|
+
def ==(other)
|
25
|
+
other.is_a?(Item) && other.id == id
|
26
|
+
end
|
27
|
+
|
28
|
+
def seo_meta_tags
|
29
|
+
Utils::SeoTagsBuilder.new(self, @items_repo.site).meta_tags
|
30
|
+
end
|
31
|
+
|
32
|
+
def singleton?
|
33
|
+
item_type.singleton
|
34
|
+
end
|
35
|
+
alias single_instance? singleton?
|
36
|
+
|
37
|
+
def item_type
|
38
|
+
@item_type ||= entity.item_type
|
39
|
+
end
|
40
|
+
|
41
|
+
def fields
|
42
|
+
@fields ||= item_type.fields.sort_by(&:position)
|
43
|
+
end
|
44
|
+
|
45
|
+
def attributes
|
46
|
+
fields.each_with_object(
|
47
|
+
ActiveSupport::HashWithIndifferentAccess.new
|
48
|
+
) do |field, acc|
|
49
|
+
acc[field.api_key.to_sym] = send(field.api_key)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def position
|
54
|
+
entity.position
|
55
|
+
end
|
56
|
+
|
57
|
+
def parent
|
58
|
+
@items_repo.find(entity.parent_id) if item_type.tree && entity.parent_id
|
59
|
+
end
|
60
|
+
|
61
|
+
def children
|
62
|
+
@items_repo.children_of(id).sort_by(&:position) if item_type.tree
|
63
|
+
end
|
64
|
+
|
65
|
+
def updated_at
|
66
|
+
Time.parse(entity.updated_at).utc
|
67
|
+
end
|
68
|
+
|
69
|
+
def created_at
|
70
|
+
# @TODO ?
|
71
|
+
# Time.parse(entity.created_at).utc
|
72
|
+
end
|
73
|
+
|
74
|
+
def to_s
|
75
|
+
api_key = item_type.api_key
|
76
|
+
"#<Item id=#{id} item_type=#{api_key} attributes=#{attributes}>"
|
77
|
+
end
|
78
|
+
alias inspect to_s
|
79
|
+
|
80
|
+
def to_hash(max_depth = 3, current_depth = 0)
|
81
|
+
return id if current_depth >= max_depth
|
82
|
+
|
83
|
+
base = {
|
84
|
+
id: id,
|
85
|
+
item_type: item_type.api_key,
|
86
|
+
updated_at: updated_at,
|
87
|
+
created_at: created_at
|
88
|
+
}
|
89
|
+
|
90
|
+
base[:position] = position if item_type.sortable
|
91
|
+
|
92
|
+
if item_type.tree
|
93
|
+
base[:position] = position
|
94
|
+
base[:children] = children.map do |child|
|
95
|
+
child.to_hash(
|
96
|
+
max_depth,
|
97
|
+
current_depth + 1
|
98
|
+
)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
fields.each_with_object(base) do |field, result|
|
103
|
+
value = send(field.api_key)
|
104
|
+
|
105
|
+
result[field.api_key.to_sym] =
|
106
|
+
if value.respond_to?(:to_hash)
|
107
|
+
m = value.method(:to_hash)
|
108
|
+
if m.arity == 2
|
109
|
+
value.to_hash(max_depth, current_depth + 1)
|
110
|
+
else
|
111
|
+
value.to_hash
|
112
|
+
end
|
113
|
+
else
|
114
|
+
value
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
private
|
120
|
+
|
121
|
+
def read_attribute(method, field)
|
122
|
+
field_type = field.field_type
|
123
|
+
type_klass_name = "::Agave::Local::FieldType::#{field_type.camelize}"
|
124
|
+
type_klass = type_klass_name.safe_constantize
|
125
|
+
|
126
|
+
value = if field.localized
|
127
|
+
obj = entity.send(method) || {}
|
128
|
+
Utils::LocaleValue.find(obj)
|
129
|
+
else
|
130
|
+
entity.send(method)
|
131
|
+
end
|
132
|
+
|
133
|
+
if type_klass
|
134
|
+
type_klass.parse(value, @items_repo)
|
135
|
+
else
|
136
|
+
warning = [
|
137
|
+
"Warning: unrecognized field of type `#{field_type}`",
|
138
|
+
"for item `#{item_type.api_key}` and",
|
139
|
+
"field `#{method}`: returning a simple Hash instead.",
|
140
|
+
'Please upgrade to the latest version of the `agave` gem!'
|
141
|
+
]
|
142
|
+
puts warning.join(' ')
|
143
|
+
|
144
|
+
value
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def method_missing(method, *arguments, &block)
|
149
|
+
field = fields.find { |f| f.api_key.to_sym == method }
|
150
|
+
if field && arguments.empty?
|
151
|
+
read_attribute(method, field)
|
152
|
+
else
|
153
|
+
super
|
154
|
+
end
|
155
|
+
rescue NoMethodError => e
|
156
|
+
if e.name === method
|
157
|
+
message = []
|
158
|
+
message << "Undefined method `#{method}`"
|
159
|
+
message << "Available fields for a `#{item_type.api_key}` item:"
|
160
|
+
message += fields.map do |f|
|
161
|
+
"* .#{f.api_key}"
|
162
|
+
end
|
163
|
+
raise NoMethodError, message.join("\n")
|
164
|
+
else
|
165
|
+
raise e
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def respond_to_missing?(method, include_private = false)
|
170
|
+
field = fields.find { |f| f.api_key.to_sym == method }
|
171
|
+
if field
|
172
|
+
true
|
173
|
+
else
|
174
|
+
super
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'active_support/core_ext/string'
|
3
|
+
require 'agave/local/item'
|
4
|
+
require 'agave/local/site'
|
5
|
+
|
6
|
+
module Agave
|
7
|
+
module Local
|
8
|
+
class ItemsRepo
|
9
|
+
attr_reader :entities_repo, :collections_by_type, :item_type_methods
|
10
|
+
|
11
|
+
def initialize(entities_repo)
|
12
|
+
@entities_repo = entities_repo
|
13
|
+
@collections_by_type = {}
|
14
|
+
@items_by_id = {}
|
15
|
+
@items_by_parent_id = {}
|
16
|
+
@item_type_methods = {}
|
17
|
+
|
18
|
+
build_cache!
|
19
|
+
end
|
20
|
+
|
21
|
+
def find(id)
|
22
|
+
@items_by_id[id.to_s]
|
23
|
+
end
|
24
|
+
|
25
|
+
def children_of(id)
|
26
|
+
@items_by_parent_id.fetch(id.to_s, [])
|
27
|
+
end
|
28
|
+
|
29
|
+
def respond_to_missing?(method, include_private = false)
|
30
|
+
if collections_by_type.key?(method)
|
31
|
+
true
|
32
|
+
else
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def site
|
38
|
+
Site.new(
|
39
|
+
entities_repo.find_entities_of_type('site').first,
|
40
|
+
self
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def available_locales
|
45
|
+
site.locales.map(&:to_sym)
|
46
|
+
end
|
47
|
+
|
48
|
+
def item_types
|
49
|
+
entities_repo.find_entities_of_type('item_type')
|
50
|
+
end
|
51
|
+
|
52
|
+
def single_instance_item_types
|
53
|
+
item_types.select(&:singleton)
|
54
|
+
end
|
55
|
+
|
56
|
+
def collection_item_types
|
57
|
+
item_types.select do |item_type|
|
58
|
+
!item_type.singleton
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def items_of_type(item_type)
|
63
|
+
method = item_type_methods[item_type]
|
64
|
+
|
65
|
+
if item_type.singleton
|
66
|
+
Array(@collections_by_type[method])
|
67
|
+
else
|
68
|
+
@collections_by_type[method]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def build_cache!
|
75
|
+
build_item_type_methods!
|
76
|
+
build_collections_by_type!
|
77
|
+
build_singletons_by_type!
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_item_type_methods!
|
81
|
+
@item_type_methods = {}
|
82
|
+
|
83
|
+
singleton_keys = single_instance_item_types.map(&:api_key)
|
84
|
+
collection_keys = collection_item_types.map(&:api_key)
|
85
|
+
.map(&:pluralize)
|
86
|
+
|
87
|
+
clashing_keys = singleton_keys & collection_keys
|
88
|
+
|
89
|
+
item_types.each do |item_type|
|
90
|
+
pluralized_api_key = item_type.api_key.pluralize
|
91
|
+
|
92
|
+
method = if item_type.singleton
|
93
|
+
item_type.api_key
|
94
|
+
else
|
95
|
+
pluralized_api_key
|
96
|
+
end
|
97
|
+
|
98
|
+
if clashing_keys.include?(pluralized_api_key)
|
99
|
+
suffix = item_type.singleton ? 'instance' : 'collection'
|
100
|
+
method = "#{method}_#{suffix}"
|
101
|
+
end
|
102
|
+
|
103
|
+
@item_type_methods[item_type] = method.to_sym
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_collections_by_type!
|
108
|
+
item_types.each do |item_type|
|
109
|
+
method = item_type_methods[item_type]
|
110
|
+
@collections_by_type[method] = if item_type.singleton
|
111
|
+
nil
|
112
|
+
else
|
113
|
+
ItemCollection.new
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
item_entities.each do |item_entity|
|
118
|
+
item = Item.new(item_entity, self)
|
119
|
+
method = item_type_methods[item_entity.item_type]
|
120
|
+
|
121
|
+
unless item_entity.item_type.singleton
|
122
|
+
@collections_by_type[method].push item
|
123
|
+
end
|
124
|
+
|
125
|
+
@items_by_id[item.id] = item
|
126
|
+
|
127
|
+
if item_entity.respond_to?(:parent_id) && item_entity.parent_id
|
128
|
+
@items_by_parent_id[item_entity.parent_id] ||= []
|
129
|
+
@items_by_parent_id[item_entity.parent_id] << item
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
item_types.each do |item_type|
|
134
|
+
method = item_type_methods[item_type]
|
135
|
+
if !item_type.singleton && item_type.sortable
|
136
|
+
@collections_by_type[method].sort_by!(&:position)
|
137
|
+
elsif item_type.ordering_field
|
138
|
+
@collections_by_type[method].sort_by! do |item|
|
139
|
+
item.send(item_type.ordering_field.api_key)
|
140
|
+
end
|
141
|
+
if item_type.ordering_direction == 'desc'
|
142
|
+
@collections_by_type[method].reverse!
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def build_singletons_by_type!
|
149
|
+
item_types.each do |item_type|
|
150
|
+
method = item_type_methods[item_type]
|
151
|
+
next unless item_type.singleton
|
152
|
+
|
153
|
+
item = if item_type.singleton_item
|
154
|
+
@items_by_id[item_type.singleton_item.id]
|
155
|
+
end
|
156
|
+
|
157
|
+
@collections_by_type[method] = item
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def item_entities
|
162
|
+
entities_repo.find_entities_of_type('item')
|
163
|
+
end
|
164
|
+
|
165
|
+
def method_missing(method, *arguments, &block)
|
166
|
+
if collections_by_type.key?(method) && arguments.empty?
|
167
|
+
collections_by_type[method]
|
168
|
+
else
|
169
|
+
super
|
170
|
+
end
|
171
|
+
rescue NoMethodError
|
172
|
+
message = []
|
173
|
+
message << "Undefined method `#{method}`"
|
174
|
+
message << 'Available AgaveCMS collections/items:'
|
175
|
+
message += collections_by_type.map do |key, _value|
|
176
|
+
"* .#{key}"
|
177
|
+
end
|
178
|
+
raise NoMethodError, message.join("\n")
|
179
|
+
end
|
180
|
+
|
181
|
+
class ItemCollection < Array
|
182
|
+
def each(&block)
|
183
|
+
if block && block.arity == 2
|
184
|
+
each_with_object({}) do |item, acc|
|
185
|
+
acc[item.id] = item
|
186
|
+
end.each(&block)
|
187
|
+
else
|
188
|
+
super(&block)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def [](id)
|
193
|
+
if id.is_a? String
|
194
|
+
find { |item| item.id == id }
|
195
|
+
else
|
196
|
+
super(id)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def keys
|
201
|
+
map(&:id)
|
202
|
+
end
|
203
|
+
|
204
|
+
def values
|
205
|
+
to_a
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|