mongoid 2.3.5 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. data/CHANGELOG.md +34 -176
  2. data/LICENSE +1 -1
  3. data/lib/config/locales/bg.yml +6 -0
  4. data/lib/config/locales/de.yml +6 -0
  5. data/lib/config/locales/en-GB.yml +8 -0
  6. data/lib/config/locales/en.yml +8 -0
  7. data/lib/config/locales/es.yml +9 -3
  8. data/lib/config/locales/fr.yml +6 -0
  9. data/lib/config/locales/hi.yml +6 -0
  10. data/lib/config/locales/hu.yml +6 -0
  11. data/lib/config/locales/id.yml +6 -0
  12. data/lib/config/locales/it.yml +6 -0
  13. data/lib/config/locales/ja.yml +6 -0
  14. data/lib/config/locales/kr.yml +6 -0
  15. data/lib/config/locales/nl.yml +8 -0
  16. data/lib/config/locales/pl.yml +6 -0
  17. data/lib/config/locales/pt-BR.yml +6 -0
  18. data/lib/config/locales/pt.yml +8 -2
  19. data/lib/config/locales/ro.yml +6 -0
  20. data/lib/config/locales/ru.yml +6 -0
  21. data/lib/config/locales/sv.yml +6 -0
  22. data/lib/config/locales/vi.yml +14 -8
  23. data/lib/config/locales/zh-CN.yml +6 -0
  24. data/lib/mongoid/atomic.rb +62 -13
  25. data/lib/mongoid/atomic/modifiers.rb +33 -1
  26. data/lib/mongoid/attributes.rb +5 -19
  27. data/lib/mongoid/callbacks.rb +2 -1
  28. data/lib/mongoid/collection.rb +2 -2
  29. data/lib/mongoid/collections/retry.rb +18 -6
  30. data/lib/mongoid/components.rb +2 -0
  31. data/lib/mongoid/config.rb +8 -63
  32. data/lib/mongoid/config/environment.rb +41 -0
  33. data/lib/mongoid/config/options.rb +74 -0
  34. data/lib/mongoid/contexts/enumerable.rb +0 -24
  35. data/lib/mongoid/contexts/mongo.rb +33 -3
  36. data/lib/mongoid/copyable.rb +1 -1
  37. data/lib/mongoid/criteria.rb +4 -2
  38. data/lib/mongoid/criterion/inclusion.rb +1 -16
  39. data/lib/mongoid/criterion/optional.rb +37 -10
  40. data/lib/mongoid/criterion/scoping.rb +83 -0
  41. data/lib/mongoid/criterion/selector.rb +9 -6
  42. data/lib/mongoid/default_scope.rb +1 -1
  43. data/lib/mongoid/dirty.rb +163 -29
  44. data/lib/mongoid/document.rb +58 -7
  45. data/lib/mongoid/errors.rb +2 -0
  46. data/lib/mongoid/errors/no_environment.rb +19 -0
  47. data/lib/mongoid/errors/scope_overwrite.rb +21 -0
  48. data/lib/mongoid/extensions.rb +6 -0
  49. data/lib/mongoid/extensions/array/deep_copy.rb +25 -0
  50. data/lib/mongoid/extensions/hash/deep_copy.rb +25 -0
  51. data/lib/mongoid/extensions/hash/scoping.rb +1 -1
  52. data/lib/mongoid/extensions/object/deep_copy.rb +21 -0
  53. data/lib/mongoid/extensions/proc/scoping.rb +2 -2
  54. data/lib/mongoid/extensions/symbol/inflections.rb +1 -0
  55. data/lib/mongoid/fields.rb +171 -104
  56. data/lib/mongoid/fields/{serializable → internal}/array.rb +33 -1
  57. data/lib/mongoid/fields/{serializable → internal}/big_decimal.rb +16 -1
  58. data/lib/mongoid/fields/{serializable → internal}/bignum.rb +1 -1
  59. data/lib/mongoid/fields/{serializable → internal}/binary.rb +1 -1
  60. data/lib/mongoid/fields/{serializable → internal}/boolean.rb +16 -1
  61. data/lib/mongoid/fields/{serializable → internal}/date.rb +1 -1
  62. data/lib/mongoid/fields/{serializable → internal}/date_time.rb +1 -1
  63. data/lib/mongoid/fields/{serializable → internal}/fixnum.rb +1 -1
  64. data/lib/mongoid/fields/{serializable → internal}/float.rb +16 -1
  65. data/lib/mongoid/fields/internal/foreign_keys/array.rb +74 -0
  66. data/lib/mongoid/fields/{serializable → internal}/foreign_keys/object.rb +11 -2
  67. data/lib/mongoid/fields/{serializable → internal}/hash.rb +1 -1
  68. data/lib/mongoid/fields/{serializable → internal}/integer.rb +16 -1
  69. data/lib/mongoid/fields/{serializable → internal}/localized.rb +23 -2
  70. data/lib/mongoid/fields/{serializable → internal}/nil_class.rb +16 -1
  71. data/lib/mongoid/fields/{serializable → internal}/object.rb +1 -1
  72. data/lib/mongoid/fields/{serializable → internal}/object_id.rb +16 -1
  73. data/lib/mongoid/fields/{serializable → internal}/range.rb +21 -2
  74. data/lib/mongoid/fields/{serializable → internal}/set.rb +16 -1
  75. data/lib/mongoid/fields/{serializable → internal}/string.rb +16 -1
  76. data/lib/mongoid/fields/{serializable → internal}/symbol.rb +17 -1
  77. data/lib/mongoid/fields/{serializable → internal}/time.rb +1 -1
  78. data/lib/mongoid/fields/{serializable → internal}/time_with_zone.rb +1 -1
  79. data/lib/mongoid/fields/{serializable → internal}/timekeeping.rb +16 -1
  80. data/lib/mongoid/fields/mappings.rb +8 -3
  81. data/lib/mongoid/fields/serializable.rb +34 -3
  82. data/lib/mongoid/hierarchy.rb +14 -14
  83. data/lib/mongoid/identity_map.rb +3 -2
  84. data/lib/mongoid/logger.rb +1 -7
  85. data/lib/mongoid/named_scope.rb +16 -12
  86. data/lib/mongoid/observer.rb +5 -1
  87. data/lib/mongoid/paranoia.rb +1 -0
  88. data/lib/mongoid/persistence.rb +11 -4
  89. data/lib/mongoid/persistence/atomic.rb +4 -1
  90. data/lib/mongoid/persistence/atomic/add_to_set.rb +17 -1
  91. data/lib/mongoid/persistence/atomic/sets.rb +1 -1
  92. data/lib/mongoid/railties/database.rake +1 -1
  93. data/lib/mongoid/relations.rb +1 -3
  94. data/lib/mongoid/relations/auto_save.rb +1 -1
  95. data/lib/mongoid/relations/builders.rb +1 -1
  96. data/lib/mongoid/relations/builders/embedded/many.rb +2 -6
  97. data/lib/mongoid/relations/builders/nested_attributes/many.rb +1 -1
  98. data/lib/mongoid/relations/builders/nested_attributes/one.rb +1 -1
  99. data/lib/mongoid/relations/builders/referenced/many_to_many.rb +1 -1
  100. data/lib/mongoid/relations/cascading/delete.rb +1 -1
  101. data/lib/mongoid/relations/cyclic.rb +10 -6
  102. data/lib/mongoid/relations/embedded/atomic.rb +3 -3
  103. data/lib/mongoid/relations/embedded/many.rb +98 -20
  104. data/lib/mongoid/relations/macros.rb +2 -0
  105. data/lib/mongoid/relations/many.rb +13 -0
  106. data/lib/mongoid/relations/metadata.rb +3 -3
  107. data/lib/mongoid/relations/nested_builder.rb +4 -3
  108. data/lib/mongoid/relations/proxy.rb +0 -1
  109. data/lib/mongoid/relations/referenced/batch.rb +3 -2
  110. data/lib/mongoid/relations/referenced/in.rb +3 -3
  111. data/lib/mongoid/relations/referenced/many.rb +89 -10
  112. data/lib/mongoid/relations/referenced/many_to_many.rb +34 -43
  113. data/lib/mongoid/relations/referenced/one.rb +8 -4
  114. data/lib/mongoid/relations/synchronization.rb +22 -5
  115. data/lib/mongoid/threaded.rb +38 -276
  116. data/lib/mongoid/threaded/lifecycle.rb +18 -18
  117. data/lib/mongoid/timestamps/updated.rb +13 -3
  118. data/lib/mongoid/validations.rb +22 -1
  119. data/lib/mongoid/validations/presence.rb +40 -0
  120. data/lib/mongoid/validations/uniqueness.rb +14 -3
  121. data/lib/mongoid/version.rb +1 -1
  122. data/lib/mongoid/versioning.rb +6 -2
  123. data/lib/rails/mongoid.rb +7 -1
  124. metadata +64 -45
  125. data/lib/mongoid/fields/serializable/foreign_keys/array.rb +0 -42
  126. data/lib/mongoid/relations/embedded/sort.rb +0 -31
@@ -0,0 +1,41 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc
3
+ module Config
4
+
5
+ # Encapsulates logic for getting environment information.
6
+ module Environment
7
+ extend self
8
+
9
+ # Get the name of the environment that we are running under. This first
10
+ # looks for Rails, then Sinatra, then a RACK_ENV environment variable,
11
+ # and if none of those are found returns "development".
12
+ #
13
+ # @example Get the env name.
14
+ # Environment.env_name
15
+ #
16
+ # @return [ String ] The name of the current environment.
17
+ #
18
+ # @since 2.3.0
19
+ def env_name
20
+ return Rails.env if defined?(Rails)
21
+ return Sinatra::Base.environment.to_s if defined?(Sinatra)
22
+ ENV["RACK_ENV"] || raise(Errors::NoEnvironment.new)
23
+ end
24
+
25
+ # Load the yaml from the provided path and return the settings for the
26
+ # current environment.
27
+ #
28
+ # @example Load the yaml.
29
+ # Environment.load_yaml("/work/mongoid.yml")
30
+ #
31
+ # @param [ String ] path The location of the file.
32
+ #
33
+ # @return [ Hash ] The settings.
34
+ #
35
+ # @since 2.3.0
36
+ def load_yaml(path)
37
+ YAML.load(ERB.new(File.new(path).read).result)[env_name]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc
3
+ module Config
4
+
5
+ # Encapsulates logic for setting options.
6
+ module Options
7
+
8
+ # Get the defaults or initialize a new empty hash.
9
+ #
10
+ # @example Get the defaults.
11
+ # options.defaults
12
+ #
13
+ # @return [ Hash ] The default options.
14
+ #
15
+ # @since 2.3.0
16
+ def defaults
17
+ @defaults ||= {}
18
+ end
19
+
20
+ # Define a configuration option with a default.
21
+ #
22
+ # @example Define the option.
23
+ # Options.option(:persist_in_safe_mode, :default => false)
24
+ #
25
+ # @param [ Symbol ] name The name of the configuration option.
26
+ # @param [ Hash ] options Extras for the option.
27
+ #
28
+ # @option options [ Object ] :default The default value.
29
+ #
30
+ # @since 2.0.0.rc.1
31
+ def option(name, options = {})
32
+ defaults[name] = settings[name] = options[:default]
33
+
34
+ class_eval <<-RUBY
35
+ def #{name}
36
+ settings[#{name.inspect}]
37
+ end
38
+
39
+ def #{name}=(value)
40
+ settings[#{name.inspect}] = value
41
+ end
42
+
43
+ def #{name}?
44
+ #{name}
45
+ end
46
+ RUBY
47
+ end
48
+
49
+ # Reset the configuration options to the defaults.
50
+ #
51
+ # @example Reset the configuration options.
52
+ # config.reset
53
+ #
54
+ # @return [ Hash ] The defaults.
55
+ #
56
+ # @since 2.3.0
57
+ def reset
58
+ settings.replace(defaults)
59
+ end
60
+
61
+ # Get the settings or initialize a new empty hash.
62
+ #
63
+ # @example Get the settings.
64
+ # options.settings
65
+ #
66
+ # @return [ Hash ] The setting options.
67
+ #
68
+ # @since 2.3.0
69
+ def settings
70
+ @settings ||= {}
71
+ end
72
+ end
73
+ end
74
+ end
@@ -210,18 +210,6 @@ module Mongoid #:nodoc:
210
210
 
211
211
  protected
212
212
 
213
- # Get the root class collection name.
214
- #
215
- # @example Get the root class collection name.
216
- # context.collection_name
217
- #
218
- # @return [ String ] The name of the collection.
219
- #
220
- # @since 2.4.3
221
- def collection_name
222
- root ? root.collection_name : nil
223
- end
224
-
225
213
  # Filters the documents against the criteria's selector
226
214
  #
227
215
  # @example Filter the documents.
@@ -263,22 +251,10 @@ module Mongoid #:nodoc:
263
251
  documents
264
252
  end
265
253
 
266
- # Get the root document for the enumerable.
267
- #
268
- # @example Get the root document.
269
- # context.root
270
- #
271
- # @return [ Document ] The root.
272
254
  def root
273
255
  @root ||= documents.first.try(:_root)
274
256
  end
275
257
 
276
- # Get the root class for the enumerable.
277
- #
278
- # @example Get the root class.
279
- # context.root_class
280
- #
281
- # @return [ Class ] The root class.
282
258
  def root_class
283
259
  @root_class ||= root ? root.class : nil
284
260
  end
@@ -145,10 +145,40 @@ module Mongoid #:nodoc:
145
145
  #
146
146
  # @return [ Cursor ] An enumerable +Cursor+ of results.
147
147
  def execute
148
- criteria.inclusions.reject! do |metadata|
149
- metadata.eager_load(criteria)
148
+ collection, options = klass.collection, process_options
149
+ if criteria.inclusions.any?
150
+ collection.find(selector, options).entries.tap do |docs|
151
+ parent_ids = docs.map(&:id)
152
+ criteria.inclusions.reject! do |metadata|
153
+ if metadata.macro == :referenced_in
154
+ child_ids = load_ids(metadata.foreign_key)
155
+ metadata.eager_load(child_ids)
156
+ else
157
+ metadata.eager_load(parent_ids)
158
+ end
159
+ end
160
+ end
161
+ else
162
+ collection.find(selector, options)
150
163
  end
151
- klass.collection.find(selector, process_options) || []
164
+ end
165
+
166
+ # Loads an array of ids only for the current criteria. Used by eager
167
+ # loading to determine the documents to load.
168
+ #
169
+ # @example Load the related ids.
170
+ # criteria.load_ids("person_id")
171
+ #
172
+ # @param [ String ] key The id or foriegn key string.
173
+ #
174
+ # @return [ Array<String, BSON::ObjectId> ] The ids to load.
175
+ #
176
+ # @since 2.2.0
177
+ def load_ids(key)
178
+ klass.collection.driver.find(
179
+ selector,
180
+ process_options.merge({ :fields => { key => 1 }})
181
+ ).map { |doc| doc[key] }
152
182
  end
153
183
 
154
184
  # Return the first result for the +Context+.
@@ -31,7 +31,7 @@ module Mongoid #:nodoc:
31
31
  #
32
32
  # @return [ Document ] The new document.
33
33
  def initialize_copy(other)
34
- @attributes = other.as_document
34
+ other.as_document
35
35
  instance_variables.each { |name| remove_instance_variable(name) }
36
36
  COPYABLES.each do |name|
37
37
  value = other.instance_variable_get(name)
@@ -6,6 +6,7 @@ require "mongoid/criterion/exclusion"
6
6
  require "mongoid/criterion/inclusion"
7
7
  require "mongoid/criterion/inspection"
8
8
  require "mongoid/criterion/optional"
9
+ require "mongoid/criterion/scoping"
9
10
  require "mongoid/criterion/selector"
10
11
 
11
12
  module Mongoid #:nodoc:
@@ -29,6 +30,7 @@ module Mongoid #:nodoc:
29
30
  include Criterion::Inclusion
30
31
  include Criterion::Inspection
31
32
  include Criterion::Optional
33
+ include Criterion::Scoping
32
34
 
33
35
  attr_accessor \
34
36
  :documents,
@@ -253,10 +255,10 @@ module Mongoid #:nodoc:
253
255
  # scope for use with named scopes.
254
256
  #
255
257
  # @example Get the criteria as a scoped hash.
256
- # criteria.scoped
258
+ # criteria.as_conditions
257
259
  #
258
260
  # @return [ Hash ] The criteria as a scoped hash.
259
- def scoped
261
+ def as_conditions
260
262
  scope_options = @options.dup
261
263
  sorting = scope_options.delete(:sort)
262
264
  scope_options[:order_by] = sorting if sorting
@@ -165,7 +165,7 @@ module Mongoid #:nodoc:
165
165
  #
166
166
  # @since 2.2.1
167
167
  def from_map_or_db
168
- doc = IdentityMap.get(klass, extract_id)
168
+ doc = IdentityMap.get(klass, extract_id || selector)
169
169
  doc && doc.matches?(selector) ? doc : first
170
170
  end
171
171
 
@@ -228,21 +228,6 @@ module Mongoid #:nodoc:
228
228
  @inclusions ||= []
229
229
  end
230
230
 
231
- # Loads an array of ids only for the current criteria. Used by eager
232
- # loading to determine the documents to load.
233
- #
234
- # @example Load the related ids.
235
- # criteria.load_ids("person_id")
236
- #
237
- # @param [ String ] key The id or foriegn key string.
238
- #
239
- # @return [ Array<String, BSON::ObjectId> ] The ids to load.
240
- #
241
- # @since 2.2.0
242
- def load_ids(key)
243
- driver.find(selector, { :fields => { key => 1 }}).map { |doc| doc[key] }
244
- end
245
-
246
231
  # Adds a criterion to the +Criteria+ that specifies values to do
247
232
  # geospacial searches by. The field must be indexed with the "2d" option.
248
233
  #
@@ -15,8 +15,8 @@ module Mongoid #:nodoc:
15
15
  # @return [ Criteria ] The cloned criteria.
16
16
  def ascending(*fields)
17
17
  clone.tap do |crit|
18
- crit.options[:sort] = [] unless options[:sort] || fields.first.nil?
19
- fields.flatten.each { |field| merge_options(crit.options[:sort], [ field, :asc ]) }
18
+ setup_sort_options(crit.options) unless fields.first.nil?
19
+ fields.flatten.each { |field| merge_options(crit.options[:sort], [ localize(field), :asc ]) }
20
20
  end
21
21
  end
22
22
  alias :asc :ascending
@@ -56,8 +56,8 @@ module Mongoid #:nodoc:
56
56
  # @return [ Criteria ] The cloned criteria.
57
57
  def descending(*fields)
58
58
  clone.tap do |crit|
59
- crit.options[:sort] = [] unless options[:sort] || fields.first.nil?
60
- fields.flatten.each { |field| merge_options(crit.options[:sort], [ field, :desc ]) }
59
+ setup_sort_options(crit.options) unless fields.first.nil?
60
+ fields.flatten.each { |field| merge_options(crit.options[:sort], [ localize(field), :desc ]) }
61
61
  end
62
62
  end
63
63
  alias :desc :descending
@@ -89,11 +89,12 @@ module Mongoid #:nodoc:
89
89
  #
90
90
  # @return [ Criteria ] The cloned criteria.
91
91
  def for_ids(*ids)
92
+ field = klass.fields["_id"]
92
93
  ids.flatten!
93
94
  if ids.size > 1
94
- where(:_id.in => ::BSON::ObjectId.convert(klass, ids))
95
+ any_in(:_id => ids.map{ |id| field.serialize(id) })
95
96
  else
96
- where(:_id => ids.first)
97
+ where(:_id => field.serialize(ids.first))
97
98
  end
98
99
  end
99
100
 
@@ -135,7 +136,7 @@ module Mongoid #:nodoc:
135
136
  def order_by(*args)
136
137
  clone.tap do |crit|
137
138
  arguments = args.size == 1 ? args.first : args
138
- crit.options[:sort] = [] unless options[:sort] || args.first.nil?
139
+ setup_sort_options(crit.options) unless args.first.nil?
139
140
  if arguments.is_a?(Array)
140
141
  #[:name, :asc]
141
142
  if arguments.size == 2 && (arguments.first.is_a?(Symbol) || arguments.first.is_a?(String))
@@ -198,12 +199,12 @@ module Mongoid #:nodoc:
198
199
  "due to the fact that hash doesn't have order this may cause unpredictable results"
199
200
  end
200
201
  arguments.each_pair do |field, direction|
201
- merge_options(crit.options[:sort], [ field, direction ])
202
+ merge_options(crit.options[:sort], [ localize(field), direction ])
202
203
  end
203
204
  when Array
204
- merge_options(crit.options[:sort],arguments)
205
+ merge_options(crit.options[:sort], arguments.map{ |field| localize(field) })
205
206
  when Complex
206
- merge_options(crit.options[:sort], [ arguments.key, arguments.operator.to_sym ])
207
+ merge_options(crit.options[:sort], [ localize(arguments.key), arguments.operator.to_sym ])
207
208
  end
208
209
  end
209
210
 
@@ -227,6 +228,32 @@ module Mongoid #:nodoc:
227
228
  options << new_option.flatten
228
229
  end
229
230
  end
231
+
232
+ # Initialize the sort options
233
+ # Set options[:sort] to an empty array if it does not exist, or dup it if
234
+ # it already has been defined
235
+ #
236
+ # @example criteria.setup_sort_options(crit.options)
237
+ #
238
+ # @param [ Array<Array> ] Existing options
239
+ #
240
+ # @since 2.4.0
241
+ def setup_sort_options(options)
242
+ options[:sort] = options[:sort] ? options[:sort].dup : []
243
+ end
244
+
245
+ # Check if field is localized and return localized version if it is.
246
+ #
247
+ # @example localize
248
+ # criteria.localize(:description)
249
+ #
250
+ # @param [ <Symbol> ] field to localize
251
+ def localize(field)
252
+ if klass.fields[field.to_s].try(:localized?)
253
+ field = "#{field}.#{::I18n.locale}".to_sym
254
+ end
255
+ field
256
+ end
230
257
  end
231
258
  end
232
259
  end
@@ -0,0 +1,83 @@
1
+ # encoding: utf-8
2
+ module Mongoid #:nodoc:
3
+ module Criterion #:nodoc:
4
+ module Scoping
5
+
6
+ attr_accessor :default_scopable
7
+
8
+ # Apply the model's default scope to this criteria.
9
+ #
10
+ # @example Apply the default scope.
11
+ # criteria.apply_default_scope
12
+ #
13
+ # @return [ Criteria ] The criteria.
14
+ #
15
+ # @since 2.4.0
16
+ def apply_default_scope
17
+ if klass.default_scoping && default_scopable?
18
+ self.default_scopable = false
19
+ fuse(klass.default_scoping)
20
+ else
21
+ self
22
+ end
23
+ end
24
+
25
+ # Is the default scope of the class allowed to be applied?
26
+ #
27
+ # @example Can the default scope be applied?
28
+ # criteria.default_scopable?
29
+ #
30
+ # @return [ true, false ] The the default can be applied.
31
+ #
32
+ # @since 2.4.0
33
+ def default_scopable?
34
+ default_scopable != false
35
+ end
36
+
37
+ # Force the default scope to be applied to the criteria.
38
+ #
39
+ # @example Force default scoping.
40
+ # criteria.scoped
41
+ #
42
+ # @return [ Criteria ] The criteria.
43
+ #
44
+ # @since 2.4.0
45
+ def scoped
46
+ self.default_scopable = true
47
+ apply_default_scope
48
+ end
49
+
50
+ # Get the criteria with the default scoping removed.
51
+ #
52
+ # @note This has slightly different behaviour than AR - will remove the
53
+ # default scoping if no other criteria have been chained and tampered
54
+ # with the criterion instead of clearing everything.
55
+ #
56
+ # @example Get the criteria unscoped.
57
+ # criteria.unscoped
58
+ #
59
+ # @return [ Criteria ] The unscoped criteria.
60
+ #
61
+ # @since 2.4.0
62
+ def unscoped
63
+ clone.tap do |criteria|
64
+ criteria.clear_scoping
65
+ criteria.default_scopable = false
66
+ end
67
+ end
68
+
69
+ # Remove all scoping from the criteria.
70
+ #
71
+ # @example Remove the default scope.
72
+ # criteria.clear_scoping
73
+ #
74
+ # @return [ nil ] No guaranteed return value.
75
+ #
76
+ # @since 2.4.0
77
+ def clear_scoping
78
+ selector.clear
79
+ options.clear
80
+ end
81
+ end
82
+ end
83
+ end