acts-as-taggable-on 2.2.2 → 2.3.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.
data/.gitignore CHANGED
@@ -5,4 +5,6 @@
5
5
  .rvmrc
6
6
  Gemfile.lock
7
7
  spec/database.yml
8
- tmp
8
+ tmp*.sw?
9
+ *.sw?
10
+ tmp
data/README.rdoc CHANGED
@@ -16,17 +16,15 @@ was used.
16
16
 
17
17
  == Installation
18
18
 
19
- === Rails 2.3.x
19
+ === Rails 2.x
20
20
 
21
- To use it, add it to your Gemfile:
22
-
23
- gem 'acts-as-taggable-on', '~>2.1.0'
21
+ Not supported any more! It is time for update guys.
24
22
 
25
23
  === Rails 3.x
26
24
 
27
25
  To use it, add it to your Gemfile:
28
26
 
29
- gem 'acts-as-taggable-on', '~>2.2.0'
27
+ gem 'acts-as-taggable-on', '~> 2.2.2'
30
28
 
31
29
  ==== Post Installation
32
30
 
@@ -61,6 +59,25 @@ directory, you can run the specs for RoR 3.x with:
61
59
  User.skill_counts # => [<Tag name="joking" count=2>,<Tag name="clowning" count=1>...]
62
60
  @frankie.skill_counts
63
61
 
62
+ To preserve the order in which tags are created use acts_as_ordered_taggable:
63
+
64
+ class User < ActiveRecord::Base
65
+ # Alias for <tt>acts_as_ordered_taggable_on :tags</tt>:
66
+ acts_as_ordered_taggable
67
+ acts_as_ordered_taggable_on :skills, :interests
68
+ end
69
+
70
+ @user = User.new(:name => "Bobby")
71
+ @user.tag_list = "east, south"
72
+ @user.save
73
+
74
+ @user.tag_list = "north, east, south, west"
75
+ @user.save
76
+
77
+ @user.reload
78
+ @user.tag_list # => ["north", "east", "south", "west"]
79
+
80
+
64
81
  === Finding Tagged Objects
65
82
 
66
83
  Acts As Taggable On utilizes named_scopes to create an association for tags.
@@ -76,19 +93,19 @@ compatibility with the will_paginate gem:
76
93
  User.tagged_with("awesome").by_date.paginate(:page => params[:page], :per_page => 20)
77
94
 
78
95
  # Find a user with matching all tags, not just one
79
- User.tagged_with(["awesome", "cool"], :match_all => :true)
96
+ User.tagged_with(["awesome", "cool"], :match_all => true)
80
97
 
81
98
  # Find a user with any of the tags:
82
99
  User.tagged_with(["awesome", "cool"], :any => true)
83
100
 
84
101
  # Find a user that not tags with awesome or cool:
85
102
  User.tagged_with(["awesome", "cool"], :exclude => true)
86
-
103
+
87
104
  # Find a user with any of tags based on context:
88
105
  User.tagged_with(['awesome, cool'], :on => :tags, :any => true).tagged_with(['smart', 'shy'], :on => :skills, :any => true)
89
106
 
90
107
  You can also use :wild => true option along with :any or :exclude option. It will looking for %awesome% and %cool% in sql.
91
-
108
+
92
109
  Tip: User.tagged_with([]) or '' will return [], but not all records.
93
110
 
94
111
  === Relationships
@@ -141,6 +158,21 @@ Tags can have owners:
141
158
  @some_photo.locations_from(@some_user) # => ["paris", "normandy"]
142
159
  @some_photo.owner_tags_on(@some_user, :locations) # => [#<ActsAsTaggableOn::Tag id: 1, name: "paris">...]
143
160
  @some_photo.owner_tags_on(nil, :locations) # => Ownerships equivalent to saying @some_photo.locations
161
+ @some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations, :skip_save => true) #won't save @some_photo object
162
+
163
+ === Dirty objects
164
+
165
+ @bobby = User.find_by_name("Bobby")
166
+ @bobby.skill_list # => ["jogging", "diving"]
167
+
168
+ @boddy.skill_list_changed? #=> false
169
+ @boddy.changes #=> {}
170
+
171
+ @bobby.skill_list = "swimming"
172
+ @bobby.changes.should == {"skill_list"=>["jogging, diving", ["swimming"]]}
173
+ @boddy.skill_list_changed? #=> true
174
+
175
+ @bobby.skill_list_change.should == ["jogging, diving", ["swimming"]]
144
176
 
145
177
  === Tag cloud calculations
146
178
 
@@ -182,14 +214,21 @@ CSS:
182
214
  .css3 { font-size: 1.4em; }
183
215
  .css4 { font-size: 1.6em; }
184
216
 
185
- == Remove unused tags
217
+ == Configuration
218
+
219
+ If you would like to remove unused tag objects after removing taggings, add
220
+
221
+ ActsAsTaggableOn.remove_unused_tags = true
222
+
223
+ If you want force tags to be saved downcased:
224
+
225
+ ActsAsTaggableOn.force_lowercase = true
226
+
227
+ If you want tags to be saved parametrized (you can redefine to_param as well):
228
+
229
+ ActsAsTaggableOn.force_parameterize = true
230
+
186
231
 
187
- If you would like to remove unused tag objects after removing taggings, add
188
-
189
- ActsAsTaggableOn::Tag.remove_unused = true
190
-
191
- to initializer file.
192
-
193
232
  == Contributors
194
233
 
195
234
  We have a long list of valued contributors. {Check them all}[https://github.com/mbleigh/acts-as-taggable-on/contributors]
@@ -11,7 +11,7 @@ Gem::Specification.new do |gem|
11
11
  gem.homepage = ''
12
12
 
13
13
  gem.add_runtime_dependency 'rails', '~> 3.0'
14
- gem.add_development_dependency 'rspec', '~> 2.5'
14
+ gem.add_development_dependency 'rspec', '~> 2.6'
15
15
  gem.add_development_dependency 'ammeter', '~> 0.1.3'
16
16
  gem.add_development_dependency 'sqlite3'
17
17
  gem.add_development_dependency 'mysql2', '~> 0.3.7'
@@ -6,6 +6,29 @@ require "digest/sha1"
6
6
 
7
7
  $LOAD_PATH.unshift(File.dirname(__FILE__))
8
8
 
9
+ module ActsAsTaggableOn
10
+ mattr_accessor :delimiter
11
+ @@delimiter = ','
12
+
13
+ mattr_accessor :force_lowercase
14
+ @@force_lowercase = false
15
+
16
+ mattr_accessor :force_parameterize
17
+ @@force_parameterize = false
18
+
19
+ mattr_accessor :remove_unused_tags
20
+ self.remove_unused_tags = false
21
+
22
+ def self.glue
23
+ @@delimiter.ends_with?(" ") ? @@delimiter : "#{@@delimiter} "
24
+ end
25
+
26
+ def self.setup
27
+ yield self
28
+ end
29
+ end
30
+
31
+
9
32
  require "acts_as_taggable_on/utils"
10
33
 
11
34
  require "acts_as_taggable_on/taggable"
@@ -14,6 +37,7 @@ require "acts_as_taggable_on/acts_as_taggable_on/collection"
14
37
  require "acts_as_taggable_on/acts_as_taggable_on/cache"
15
38
  require "acts_as_taggable_on/acts_as_taggable_on/ownership"
16
39
  require "acts_as_taggable_on/acts_as_taggable_on/related"
40
+ require "acts_as_taggable_on/acts_as_taggable_on/dirty"
17
41
 
18
42
  require "acts_as_taggable_on/tagger"
19
43
  require "acts_as_taggable_on/tag"
@@ -31,4 +55,5 @@ end
31
55
 
32
56
  if defined?(ActionView::Base)
33
57
  ActionView::Base.send :include, ActsAsTaggableOn::TagsHelper
34
- end
58
+ end
59
+
@@ -1,4 +1,4 @@
1
1
  module ActsAsTaggableOn
2
- VERSION = '2.2.2'
2
+ VERSION = '2.3.0'
3
3
  end
4
4
 
@@ -18,11 +18,22 @@ module ActsAsTaggableOn::Taggable
18
18
  tag_type = tags_type.to_s.singularize
19
19
  context_taggings = "#{tag_type}_taggings".to_sym
20
20
  context_tags = tags_type.to_sym
21
-
21
+ taggings_order = (preserve_tag_order? ? "#{ActsAsTaggableOn::Tagging.table_name}.id" : nil)
22
+
22
23
  class_eval do
23
- has_many context_taggings, :as => :taggable, :dependent => :destroy, :include => :tag, :class_name => "ActsAsTaggableOn::Tagging",
24
- :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type]
25
- has_many context_tags, :through => context_taggings, :source => :tag, :class_name => "ActsAsTaggableOn::Tag"
24
+ # when preserving tag order, include order option so that for a 'tags' context
25
+ # the associations tag_taggings & tags are always returned in created order
26
+ has_many context_taggings, :as => :taggable,
27
+ :dependent => :destroy,
28
+ :include => :tag,
29
+ :class_name => "ActsAsTaggableOn::Tagging",
30
+ :conditions => ["#{ActsAsTaggableOn::Tagging.table_name}.context = ?", tags_type],
31
+ :order => taggings_order
32
+
33
+ has_many context_tags, :through => context_taggings,
34
+ :source => :tag,
35
+ :class_name => "ActsAsTaggableOn::Tag",
36
+ :order => taggings_order
26
37
  end
27
38
 
28
39
  class_eval %(
@@ -41,11 +52,11 @@ module ActsAsTaggableOn::Taggable
41
52
  end
42
53
  end
43
54
 
44
- def acts_as_taggable_on(*args)
45
- super(*args)
55
+ def taggable_on(preserve_tag_order, *tag_types)
56
+ super(preserve_tag_order, *tag_types)
46
57
  initialize_acts_as_taggable_on_core
47
58
  end
48
-
59
+
49
60
  # all column names are necessary for PostgreSQL group clause
50
61
  def grouped_column_names_for(object)
51
62
  object.column_names.map { |column| "#{object.table_name}.#{column}" }.join(", ")
@@ -85,8 +96,8 @@ module ActsAsTaggableOn::Taggable
85
96
  tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ? ESCAPE '!'", "%#{escape_like(t)}%"]) }.join(" OR ")
86
97
  else
87
98
  tags_conditions = tag_list.map { |t| sanitize_sql(["#{ActsAsTaggableOn::Tag.table_name}.name #{like_operator} ?", t]) }.join(" OR ")
88
- end
89
-
99
+ end
100
+
90
101
  conditions << "#{table_name}.#{primary_key} NOT IN (SELECT #{ActsAsTaggableOn::Tagging.table_name}.taggable_id FROM #{ActsAsTaggableOn::Tagging.table_name} JOIN #{ActsAsTaggableOn::Tag.table_name} ON #{ActsAsTaggableOn::Tagging.table_name}.tag_id = #{ActsAsTaggableOn::Tag.table_name}.#{ActsAsTaggableOn::Tag.primary_key} AND (#{tags_conditions}) WHERE #{ActsAsTaggableOn::Tagging.table_name}.taggable_type = #{quote_value(base_class.name)})"
91
102
 
92
103
  elsif options.delete(:any)
@@ -94,16 +105,18 @@ module ActsAsTaggableOn::Taggable
94
105
  if options.delete(:wild)
95
106
  tags = ActsAsTaggableOn::Tag.named_like_any(tag_list)
96
107
  else
97
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
108
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
98
109
  end
99
-
110
+
100
111
  return scoped(:conditions => "1 = 0") unless tags.length > 0
101
112
 
102
113
  # setup taggings alias so we can chain, ex: items_locations_taggings_awesome_cool_123
103
114
  # avoid ambiguous column name
104
115
  taggings_context = context ? "_#{context}" : ''
105
116
 
106
- taggings_alias = "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:safe_name).join('_'))}"
117
+ taggings_alias = adjust_taggings_alias(
118
+ "#{alias_base_name[0..4]}#{taggings_context[0..6]}_taggings_#{sha_prefix(tags.map(&:name).join('_'))}"
119
+ )
107
120
 
108
121
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
109
122
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
@@ -117,12 +130,12 @@ module ActsAsTaggableOn::Taggable
117
130
  joins << tagging_join
118
131
 
119
132
  else
120
- tags = ActsAsTaggableOn::Tag.named_any(tag_list)
121
- return empty_result unless tags.length == tag_list.length
133
+ tags = ActsAsTaggableOn::Tag.named_any(tag_list)
134
+ return empty_result unless tags.length == tag_list.length
122
135
 
123
136
  tags.each do |tag|
124
137
 
125
- taggings_alias = "#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.safe_name)}"
138
+ taggings_alias = adjust_taggings_alias("#{alias_base_name[0..11]}_taggings_#{sha_prefix(tag.name)}")
126
139
 
127
140
  tagging_join = "JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
128
141
  " ON #{taggings_alias}.taggable_id = #{table_name}.#{primary_key}" +
@@ -143,7 +156,7 @@ module ActsAsTaggableOn::Taggable
143
156
  end
144
157
  end
145
158
 
146
- taggings_alias, tags_alias = "#{alias_base_name}_taggings_group", "#{alias_base_name}_tags_group"
159
+ taggings_alias, tags_alias = adjust_taggings_alias("#{alias_base_name}_taggings_group"), "#{alias_base_name}_tags_group"
147
160
 
148
161
  if options.delete(:match_all)
149
162
  joins << "LEFT OUTER JOIN #{ActsAsTaggableOn::Tagging.table_name} #{taggings_alias}" +
@@ -166,6 +179,13 @@ module ActsAsTaggableOn::Taggable
166
179
  def is_taggable?
167
180
  true
168
181
  end
182
+
183
+ def adjust_taggings_alias(taggings_alias)
184
+ if taggings_alias.size > 75
185
+ taggings_alias = 'taggings_alias_' + Digest::SHA1.hexdigest(taggings_alias)
186
+ end
187
+ taggings_alias
188
+ end
169
189
  end
170
190
 
171
191
  module InstanceMethods
@@ -234,13 +254,19 @@ module ActsAsTaggableOn::Taggable
234
254
  ##
235
255
  # Returns all tags that are not owned of a given context
236
256
  def tags_on(context)
237
- base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s]).all
257
+ scope = base_tags.where(["#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND #{ActsAsTaggableOn::Tagging.table_name}.tagger_id IS NULL", context.to_s])
258
+ # when preserving tag order, return tags in created order
259
+ # if we added the order to the association this would always apply
260
+ scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
261
+ scope.all
238
262
  end
239
263
 
240
264
  def set_tag_list_on(context, new_list)
241
265
  add_custom_context(context)
242
266
 
243
267
  variable_name = "@#{context.to_s.singularize}_list"
268
+ process_dirty_object(context, new_list) unless custom_contexts.include?(context.to_s)
269
+
244
270
  instance_variable_set(variable_name, ActsAsTaggableOn::TagList.from(new_list))
245
271
  end
246
272
 
@@ -248,6 +274,20 @@ module ActsAsTaggableOn::Taggable
248
274
  custom_contexts + self.class.tag_types.map(&:to_s)
249
275
  end
250
276
 
277
+ def process_dirty_object(context,new_list)
278
+ value = new_list.is_a?(Array) ? new_list.join(', ') : new_list
279
+ attrib = "#{context.to_s.singularize}_list"
280
+
281
+ if changed_attributes.include?(attrib)
282
+ # The attribute already has an unsaved change.
283
+ old = changed_attributes[attrib]
284
+ changed_attributes.delete(attrib) if (old.to_s == value.to_s)
285
+ else
286
+ old = tag_list_on(context).to_s
287
+ changed_attributes[attrib] = old if (old.to_s != value.to_s)
288
+ end
289
+ end
290
+
251
291
  def reload(*args)
252
292
  self.class.tag_types.each do |context|
253
293
  instance_variable_set("@#{context.to_s.singularize}_list", nil)
@@ -261,21 +301,38 @@ module ActsAsTaggableOn::Taggable
261
301
  tagging_contexts.each do |context|
262
302
  next unless tag_list_cache_set_on(context)
263
303
 
304
+ # List of currently assigned tag names
264
305
  tag_list = tag_list_cache_on(context).uniq
265
306
 
266
307
  # Find existing tags or create non-existing tags:
267
- tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
308
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list)
268
309
 
310
+ # Tag objects for currently assigned tags
269
311
  current_tags = tags_on(context)
270
- old_tags = current_tags - tag_list
271
- new_tags = tag_list - current_tags
312
+
313
+ # Tag maintenance based on whether preserving the created order of tags
314
+ if self.class.preserve_tag_order?
315
+ # First off order the array of tag objects to match the tag list
316
+ # rather than existing tags followed by new tags
317
+ tags = tag_list.map{|l| tags.detect{|t| t.name.downcase == l.downcase}}
318
+ # To preserve tags in the order in which they were added
319
+ # delete all current tags and create new tags if the content or order has changed
320
+ old_tags = (tags == current_tags ? [] : current_tags)
321
+ new_tags = (tags == current_tags ? [] : tags)
322
+ else
323
+ # Delete discarded tags and create new tags
324
+ old_tags = current_tags - tags
325
+ new_tags = tags - current_tags
326
+ end
272
327
 
273
328
  # Find taggings to remove:
274
- old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
275
- :context => context.to_s, :tag_id => old_tags).all
329
+ if old_tags.present?
330
+ old_taggings = taggings.where(:tagger_type => nil, :tagger_id => nil,
331
+ :context => context.to_s, :tag_id => old_tags).all
332
+ end
276
333
 
334
+ # Destroy old taggings:
277
335
  if old_taggings.present?
278
- # Destroy old taggings:
279
336
  ActsAsTaggableOn::Tagging.destroy_all "#{ActsAsTaggableOn::Tagging.primary_key}".to_sym => old_taggings.map(&:id)
280
337
  end
281
338
 
@@ -0,0 +1,37 @@
1
+ module ActsAsTaggableOn::Taggable
2
+ module Dirty
3
+ def self.included(base)
4
+ base.extend ActsAsTaggableOn::Taggable::Dirty::ClassMethods
5
+
6
+ base.initialize_acts_as_taggable_on_dirty
7
+ end
8
+
9
+ module ClassMethods
10
+ def initialize_acts_as_taggable_on_dirty
11
+ tag_types.map(&:to_s).each do |tags_type|
12
+ tag_type = tags_type.to_s.singularize
13
+ context_tags = tags_type.to_sym
14
+
15
+ class_eval %(
16
+ def #{tag_type}_list_changed?
17
+ changed_attributes.include?("#{tag_type}_list")
18
+ end
19
+
20
+ def #{tag_type}_list_was
21
+ changed_attributes.include?("#{tag_type}_list") ? changed_attributes["#{tag_type}_list"] : __send__("#{tag_type}_list")
22
+ end
23
+
24
+ def #{tag_type}_list_change
25
+ [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
26
+ end
27
+
28
+ def #{tag_type}_list_changes
29
+ [changed_attributes['#{tag_type}_list'], __send__('#{tag_type}_list')] if changed_attributes.include?("#{tag_type}_list")
30
+ end
31
+ )
32
+
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -31,12 +31,16 @@ module ActsAsTaggableOn::Taggable
31
31
  module InstanceMethods
32
32
  def owner_tags_on(owner, context)
33
33
  if owner.nil?
34
- base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s]).all
34
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ?), context.to_s])
35
35
  else
36
- base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
- #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s]).all
36
+ scope = base_tags.where([%(#{ActsAsTaggableOn::Tagging.table_name}.context = ? AND
37
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_id = ? AND
38
+ #{ActsAsTaggableOn::Tagging.table_name}.tagger_type = ?), context.to_s, owner.id, owner.class.to_s])
39
39
  end
40
+ # when preserving tag order, return tags in created order
41
+ # if we added the order to the association this would always apply
42
+ scope = scope.order("#{ActsAsTaggableOn::Tagging.table_name}.id") if self.class.preserve_tag_order?
43
+ scope.all
40
44
  end
41
45
 
42
46
  def cached_owned_tag_list_on(context)
@@ -73,21 +77,38 @@ module ActsAsTaggableOn::Taggable
73
77
  def save_owned_tags
74
78
  tagging_contexts.each do |context|
75
79
  cached_owned_tag_list_on(context).each do |owner, tag_list|
80
+
76
81
  # Find existing tags or create non-existing tags:
77
- tag_list = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
82
+ tags = ActsAsTaggableOn::Tag.find_or_create_all_with_like_by_name(tag_list.uniq)
78
83
 
79
- owned_tags = owner_tags_on(owner, context)
80
- old_tags = owned_tags - tag_list
81
- new_tags = tag_list - owned_tags
84
+ # Tag objects for owned tags
85
+ owned_tags = owner_tags_on(owner, context)
86
+
87
+ # Tag maintenance based on whether preserving the created order of tags
88
+ if self.class.preserve_tag_order?
89
+ # First off order the array of tag objects to match the tag list
90
+ # rather than existing tags followed by new tags
91
+ tags = tag_list.uniq.map{|s| tags.detect{|t| t.name.downcase == s.downcase}}
92
+ # To preserve tags in the order in which they were added
93
+ # delete all owned tags and create new tags if the content or order has changed
94
+ old_tags = (tags == owned_tags ? [] : owned_tags)
95
+ new_tags = (tags == owned_tags ? [] : tags)
96
+ else
97
+ # Delete discarded tags and create new tags
98
+ old_tags = owned_tags - tags
99
+ new_tags = tags - owned_tags
100
+ end
82
101
 
83
102
  # Find all taggings that belong to the taggable (self), are owned by the owner,
84
103
  # have the correct context, and are removed from the list.
85
- old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
86
- :tagger_type => owner.class.to_s, :tagger_id => owner.id,
87
- :tag_id => old_tags, :context => context).all
104
+ if old_tags.present?
105
+ old_taggings = ActsAsTaggableOn::Tagging.where(:taggable_id => id, :taggable_type => self.class.base_class.to_s,
106
+ :tagger_type => owner.class.to_s, :tagger_id => owner.id,
107
+ :tag_id => old_tags, :context => context).all
108
+ end
88
109
 
110
+ # Destroy old taggings:
89
111
  if old_taggings.present?
90
- # Destroy old taggings:
91
112
  ActsAsTaggableOn::Tagging.destroy_all(:id => old_taggings.map(&:id))
92
113
  end
93
114