userstamper 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +2 -0
- data/.travis.yml +35 -0
- data/CHANGELOG.md +116 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +222 -0
- data/LICENSE +22 -0
- data/README.md +142 -0
- data/lib/userstamper.rb +23 -0
- data/lib/userstamper/configuration.rb +40 -0
- data/lib/userstamper/controller_concern.rb +44 -0
- data/lib/userstamper/migration_concern.rb +9 -0
- data/lib/userstamper/model_concern.rb +6 -0
- data/lib/userstamper/railtie.rb +15 -0
- data/lib/userstamper/stampable.rb +106 -0
- data/lib/userstamper/stamper.rb +54 -0
- data/lib/userstamper/utilities.rb +57 -0
- data/spec/controllers/posts_controller_spec.rb +44 -0
- data/spec/controllers/users_controller_spec.rb +50 -0
- data/spec/coverage_helper.rb +58 -0
- data/spec/dummy/README.rdoc +28 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +0 -0
- data/spec/dummy/app/assets/images/.keep +0 -0
- data/spec/dummy/app/assets/javascripts/application.js +13 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/controllers/application_controller.rb +13 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/controllers/posts_controller.rb +36 -0
- data/spec/dummy/app/controllers/users_controller.rb +22 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.keep +0 -0
- data/spec/dummy/app/models/comment.rb +5 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/person.rb +3 -0
- data/spec/dummy/app/models/post.rb +6 -0
- data/spec/dummy/app/models/tag.rb +3 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/bin/bundle +3 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +29 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +12 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/database.yml +23 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +41 -0
- data/spec/dummy/config/environments/production.rb +79 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/assets.rb +11 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/cookies_serializer.rb +3 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +4 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/session_store.rb +3 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +23 -0
- data/spec/dummy/config/routes.rb +56 -0
- data/spec/dummy/config/secrets.yml +22 -0
- data/spec/dummy/lib/assets/.keep +0 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/public/404.html +67 -0
- data/spec/dummy/public/422.html +67 -0
- data/spec/dummy/public/500.html +66 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/lib/configuration_spec.rb +20 -0
- data/spec/lib/migration_spec.rb +60 -0
- data/spec/lib/stamper_spec.rb +66 -0
- data/spec/lib/stamping_spec.rb +235 -0
- data/spec/lib/userstamp_spec.rb +4 -0
- data/spec/rails_helper.rb +7 -0
- data/spec/spec_helper.rb +98 -0
- data/spec/support/database_helpers.rb +73 -0
- data/spec/support/with_temporary_table.rb +51 -0
- data/userstamper.gemspec +46 -0
- metadata +279 -0
data/lib/userstamper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require "active_support"
|
2
|
+
require "active_support/rails"
|
3
|
+
require "active_support/concern"
|
4
|
+
|
5
|
+
require "zeitwerk"
|
6
|
+
loader = Zeitwerk::Loader.for_gem
|
7
|
+
loader.setup
|
8
|
+
|
9
|
+
module Userstamper
|
10
|
+
# Retrieves the configuration for the Userstamper gem.
|
11
|
+
# @return [Userstamper::Configuration]
|
12
|
+
def self.config
|
13
|
+
Configuration
|
14
|
+
end
|
15
|
+
|
16
|
+
# Configures the gem.
|
17
|
+
# @yield [Userstamper::Configuration] The configuration for the gem.
|
18
|
+
def self.configure
|
19
|
+
yield config
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
require "userstamper/railtie" if defined?(Rails::Railtie)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Userstamper::Configuration
|
2
|
+
# !@attribute [r] default_stamper
|
3
|
+
# Determines the default model used to stamp other models.
|
4
|
+
#
|
5
|
+
# By default, this is set to +'User'+.
|
6
|
+
def self.default_stamper
|
7
|
+
ActiveRecord::Base.stamper_class_name
|
8
|
+
end
|
9
|
+
|
10
|
+
# !@attribute [rw] default_stamper
|
11
|
+
# @see {.default_stamper}
|
12
|
+
def self.default_stamper=(stamper)
|
13
|
+
ActiveRecord::Base.stamper_class_name = stamper
|
14
|
+
end
|
15
|
+
self.default_stamper = 'User'.freeze
|
16
|
+
|
17
|
+
# @!attribute [r] default_stamper_class
|
18
|
+
# Determines the default model used to stamp other models.
|
19
|
+
def self.default_stamper_class
|
20
|
+
ActiveRecord::Base.stamper_class
|
21
|
+
end
|
22
|
+
|
23
|
+
# !@attribute [rw] creator_attribute
|
24
|
+
# Determines the name of the column in the database which stores the name of the creator.
|
25
|
+
#
|
26
|
+
# Override the attribute by using the stampable class method within a model.
|
27
|
+
#
|
28
|
+
# By default, this is set to +:creator_id+.
|
29
|
+
mattr_accessor :creator_attribute
|
30
|
+
self.creator_attribute = :creator_id
|
31
|
+
|
32
|
+
# !@attribute [rw] updater_attribute
|
33
|
+
# Determines the name of the column in the database which stores the name of the updater.
|
34
|
+
#
|
35
|
+
# Override the attribute by using the stampable class method within a model.
|
36
|
+
#
|
37
|
+
# By default, this is set to +:updater_id+.
|
38
|
+
mattr_accessor :updater_attribute
|
39
|
+
self.updater_attribute = :updater_id
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# The +Userstamper::Controller+ module, when included into a controller, adds an +before_action+
|
2
|
+
# callback (named +set_stamper+) and an +after_action+ callback (named +reset_stamper+). These
|
3
|
+
# methods assume a couple of things, but can be re-implemented in your controller to better suit
|
4
|
+
# your application.
|
5
|
+
#
|
6
|
+
# See the documentation for `set_stamper` and `reset_stamper` for specific implementation details.
|
7
|
+
module Userstamper::ControllerConcern
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
around_action :with_stamper
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
# This {#with_stamper} method sets the stamper for the duration of the action. This ensures
|
17
|
+
# that exceptions raised within the controller action would properly restore the previous stamper.
|
18
|
+
#
|
19
|
+
# TODO: Remove set_stamper/reset_stamper
|
20
|
+
def with_stamper
|
21
|
+
set_stamper
|
22
|
+
yield
|
23
|
+
ensure
|
24
|
+
reset_stamper
|
25
|
+
end
|
26
|
+
|
27
|
+
# The {#set_stamper} method as implemented here assumes a couple of things. First, that you are
|
28
|
+
# using a +User+ model as the stamper and second that your controller has a +current_user+
|
29
|
+
# method that contains the currently logged in stamper. If either of these are not the case in
|
30
|
+
# your application you will want to manually add your own implementation of this method to the
|
31
|
+
# private section of your +ApplicationController+
|
32
|
+
def set_stamper
|
33
|
+
@_userstamp_stamper = Userstamper.config.default_stamper_class.stamper
|
34
|
+
Userstamper.config.default_stamper_class.stamper = current_user
|
35
|
+
end
|
36
|
+
|
37
|
+
# The {#reset_stamper} method as implemented here assumes that a +User+ model is being used as
|
38
|
+
# the stamper. If this is not the case then you will need to manually add your own
|
39
|
+
# implementation of this method to the private section of your +ApplicationController+
|
40
|
+
def reset_stamper
|
41
|
+
Userstamper.config.default_stamper_class.stamper = @_userstamp_stamper
|
42
|
+
@_userstamp_stamper = nil
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Userstamper
|
2
|
+
class Railtie < Rails::Railtie
|
3
|
+
initializer "userstamper.action_controller" do
|
4
|
+
ActiveSupport.on_load(:action_controller_base) do
|
5
|
+
include Userstamper::ControllerConcern
|
6
|
+
end
|
7
|
+
|
8
|
+
ActiveSupport.on_load(:active_record) do
|
9
|
+
include Userstamper::ModelConcern
|
10
|
+
|
11
|
+
ActiveRecord::ConnectionAdapters::TableDefinition.send(:include, Userstamper::MigrationConcern)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
# Extends the stamping functionality of ActiveRecord by automatically recording the model
|
2
|
+
# responsible for creating, updating the current object. See the +Stamper+ and
|
3
|
+
# +ControllerAdditions+ modules for further documentation on how the entire process works.
|
4
|
+
module Userstamper::Stampable
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
# Should ActiveRecord record userstamps? Defaults to true.
|
9
|
+
class_attribute :record_userstamp
|
10
|
+
self.record_userstamp = true
|
11
|
+
|
12
|
+
class_attribute :stamper_class_name
|
13
|
+
|
14
|
+
before_validation :set_updater_attribute, if: :record_userstamp
|
15
|
+
before_validation :set_creator_attribute, on: :create, if: :record_userstamp
|
16
|
+
before_save :set_updater_attribute, if: :record_userstamp
|
17
|
+
before_create :set_creator_attribute, if: :record_userstamp
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def columns(*)
|
22
|
+
columns = super
|
23
|
+
return columns if defined?(@stamper_initialized) && @stamper_initialized
|
24
|
+
|
25
|
+
add_userstamp_associations({})
|
26
|
+
columns
|
27
|
+
end
|
28
|
+
|
29
|
+
# This method customizes how the gem functions. For example:
|
30
|
+
#
|
31
|
+
# class Post < ActiveRecord::Base
|
32
|
+
# stampable stamper_class_name: Person.name
|
33
|
+
# end
|
34
|
+
#
|
35
|
+
# the gem configuration.
|
36
|
+
def stampable(options = {})
|
37
|
+
self.stamper_class_name = options.delete(:stamper_class_name) if options.key?(:stamper_class_name)
|
38
|
+
|
39
|
+
add_userstamp_associations(options)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Temporarily allows you to turn stamping off. For example:
|
43
|
+
#
|
44
|
+
# Post.without_stamps do
|
45
|
+
# post = Post.find(params[:id])
|
46
|
+
# post.update_attributes(params[:post])
|
47
|
+
# post.save
|
48
|
+
# end
|
49
|
+
def without_stamps
|
50
|
+
original_value = self.record_userstamp
|
51
|
+
self.record_userstamp = false
|
52
|
+
yield
|
53
|
+
ensure
|
54
|
+
self.record_userstamp = original_value
|
55
|
+
end
|
56
|
+
|
57
|
+
def stamper_class #:nodoc:
|
58
|
+
stamper_class_name.to_s.camelize.constantize rescue nil
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Defines the associations for Userstamper.
|
64
|
+
def add_userstamp_associations(options)
|
65
|
+
@stamper_initialized = true
|
66
|
+
Userstamper::Utilities.remove_association(self, :creator)
|
67
|
+
Userstamper::Utilities.remove_association(self, :updater)
|
68
|
+
|
69
|
+
associations = Userstamper::Utilities.available_association_columns(self)
|
70
|
+
return if associations.nil?
|
71
|
+
|
72
|
+
config = Userstamper.config
|
73
|
+
klass = stamper_class.try(:name)
|
74
|
+
relation_options = options.reverse_merge(class_name: klass)
|
75
|
+
|
76
|
+
belongs_to :creator, relation_options.reverse_merge(foreign_key: config.creator_attribute, required: false) if associations.first
|
77
|
+
belongs_to :updater, relation_options.reverse_merge(foreign_key: config.updater_attribute, required: false) if associations.second
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def has_stamper?
|
84
|
+
!self.class.stamper_class.nil? && !self.class.stamper_class.stamper.nil?
|
85
|
+
end
|
86
|
+
|
87
|
+
def set_creator_attribute
|
88
|
+
return unless has_stamper?
|
89
|
+
|
90
|
+
creator_association = self.class.reflect_on_association(:creator)
|
91
|
+
return unless creator_association
|
92
|
+
return if creator.present?
|
93
|
+
|
94
|
+
Userstamper::Utilities.assign_stamper(self, creator_association)
|
95
|
+
end
|
96
|
+
|
97
|
+
def set_updater_attribute
|
98
|
+
return unless has_stamper?
|
99
|
+
|
100
|
+
updater_association = self.class.reflect_on_association(:updater)
|
101
|
+
return unless updater_association
|
102
|
+
return if !new_record? && !changed?
|
103
|
+
|
104
|
+
Userstamper::Utilities.assign_stamper(self, updater_association)
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module Userstamper::Stamper
|
2
|
+
extend ActiveSupport::Concern
|
3
|
+
|
4
|
+
module ClassMethods
|
5
|
+
def model_stamper
|
6
|
+
# don't allow multiple calls
|
7
|
+
return if singleton_class.included_modules.include?(InstanceMethods)
|
8
|
+
extend Userstamper::Stamper::InstanceMethods
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module InstanceMethods
|
13
|
+
# Used to set the current stamper for this model.
|
14
|
+
#
|
15
|
+
# @overload stamper=(object_id)
|
16
|
+
# @param [Fixnum] object_id The ID of the stamper.
|
17
|
+
# @return [Fixnum]
|
18
|
+
# @overload stamper=(object)
|
19
|
+
# @param [ActiveRecord::Base] object The stamper object.
|
20
|
+
# @return [ActiveRecord::Base]
|
21
|
+
def stamper=(object)
|
22
|
+
Thread.current[stamper_identifier] = object
|
23
|
+
end
|
24
|
+
|
25
|
+
# Retrieves the existing stamper.
|
26
|
+
def stamper
|
27
|
+
Thread.current[stamper_identifier]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Sets the stamper back to +nil+ to prepare for the next request.
|
31
|
+
def reset_stamper
|
32
|
+
self.stamper = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
# For the duration that execution is within the provided block, the stamper for this class
|
36
|
+
# would be the specified value.
|
37
|
+
#
|
38
|
+
# This replaces the {#stamper=} and {#reset_stamper} pair because this guarantees exception
|
39
|
+
# safety.
|
40
|
+
def with_stamper(stamper)
|
41
|
+
old_stamper = self.stamper
|
42
|
+
self.stamper = stamper
|
43
|
+
yield
|
44
|
+
ensure
|
45
|
+
self.stamper = old_stamper
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def stamper_identifier
|
51
|
+
"#{self.to_s.downcase}_#{self.object_id}_stamper"
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Userstamper::Utilities
|
2
|
+
# Removes the association methods from the model.
|
3
|
+
#
|
4
|
+
# @param [Class] model The model to remove methods from.
|
5
|
+
# @param [Symbol] association The name of the association to remove.
|
6
|
+
# @return [void]
|
7
|
+
def self.remove_association(model, association)
|
8
|
+
methods = [
|
9
|
+
association,
|
10
|
+
"#{association}=",
|
11
|
+
"build_#{association}",
|
12
|
+
"create_#{association}",
|
13
|
+
"create_#{association}!"
|
14
|
+
]
|
15
|
+
|
16
|
+
model.generated_association_methods.module_eval do
|
17
|
+
methods.each do |method|
|
18
|
+
remove_possible_method(method)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Obtains the creator/updater columns which are present in the model.
|
24
|
+
#
|
25
|
+
# @param [Class] model The model to query.
|
26
|
+
# @return [nil|Array<(bool, bool, bool)>] Nil if the model does not have a table defined.
|
27
|
+
# Otherwise, a tuple of booleans indicating the presence of the created, updated columns.
|
28
|
+
def self.available_association_columns(model)
|
29
|
+
return nil if model.name.nil? || model.table_name.empty?
|
30
|
+
columns = Set[*model.column_names]
|
31
|
+
config = Userstamper.config
|
32
|
+
|
33
|
+
[config.creator_attribute.present? && columns.include?(config.creator_attribute.to_s),
|
34
|
+
config.updater_attribute.present? && columns.include?(config.updater_attribute.to_s)]
|
35
|
+
rescue ActiveRecord::StatementInvalid => _
|
36
|
+
nil
|
37
|
+
end
|
38
|
+
|
39
|
+
# Assigns the stamper to the given association reflection in the record.
|
40
|
+
#
|
41
|
+
# If the stamper is a record, then it is assigned to the association; if it is a number, then it
|
42
|
+
# is assigned to the foreign key.
|
43
|
+
#
|
44
|
+
# @param [ActiveRecord::Base] record The record to assign.
|
45
|
+
# @param [ActiveRecord::Reflection] association The association to assign
|
46
|
+
def self.assign_stamper(record, association)
|
47
|
+
stamp_value = record.class.stamper_class.stamper
|
48
|
+
attribute =
|
49
|
+
if stamp_value.is_a?(ActiveRecord::Base)
|
50
|
+
association.name
|
51
|
+
else
|
52
|
+
association.foreign_key
|
53
|
+
end
|
54
|
+
|
55
|
+
record.send("#{attribute}=", stamp_value)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.describe PostsController, type: :controller do
|
4
|
+
controller do
|
5
|
+
end
|
6
|
+
|
7
|
+
before(:each) { define_first_post }
|
8
|
+
|
9
|
+
context 'when updating a Post' do
|
10
|
+
it 'sets the correct updater' do
|
11
|
+
request.session = { person_id: @delynn.id }
|
12
|
+
post :update, params: { id: @first_post.id, post: { title: 'Different' } }
|
13
|
+
|
14
|
+
expect(response.status).to eq(200)
|
15
|
+
expect(controller.instance_variable_get(:@post).title).to eq('Different')
|
16
|
+
expect(controller.instance_variable_get(:@post).updater).to eq(@delynn)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'when handling multiple requests' do
|
21
|
+
def simulate_second_request
|
22
|
+
old_request_session = request.session
|
23
|
+
request.session = { person_id: @nicole.id }
|
24
|
+
|
25
|
+
post :update, params: { id: @first_post.id, post: { title: 'Different Second'} }
|
26
|
+
expect(controller.instance_variable_get(:@post).updater).to eq(@nicole)
|
27
|
+
ensure
|
28
|
+
request.session = old_request_session
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'sets the correct updater' do
|
32
|
+
request.session = { person_id: @delynn.id }
|
33
|
+
get :edit, params: { id: @first_post.id }
|
34
|
+
expect(response.status).to eq(200)
|
35
|
+
|
36
|
+
simulate_second_request
|
37
|
+
|
38
|
+
post :update, params: { id: @first_post.id, post: { title: 'Different' } }
|
39
|
+
expect(response.status).to eq(200)
|
40
|
+
expect(controller.instance_variable_get(:@post).title).to eq('Different')
|
41
|
+
expect(controller.instance_variable_get(:@post).updater).to eq(@delynn)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.describe UsersController, type: :controller do
|
4
|
+
controller do
|
5
|
+
end
|
6
|
+
|
7
|
+
context 'when updating a User' do
|
8
|
+
it 'sets the correct updater' do
|
9
|
+
request.session = { user_id: @hera.id }
|
10
|
+
patch :update, params: { id: @hera.id, user: { name: 'Different'} }
|
11
|
+
|
12
|
+
expect(response.status).to eq(200)
|
13
|
+
expect(controller.instance_variable_get(:@user).name).to eq('Different')
|
14
|
+
expect(controller.instance_variable_get(:@user).updater).to eq(@hera)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'when handling multiple requests' do
|
19
|
+
def simulate_second_request
|
20
|
+
old_request_session = request.session
|
21
|
+
request.session = { user_id: @zeus.id }
|
22
|
+
|
23
|
+
post :update, params: { id: @hera.id, user: { name: 'Different Second' } }
|
24
|
+
expect(controller.instance_variable_get(:@user).updater).to eq(@zeus)
|
25
|
+
ensure
|
26
|
+
request.session = old_request_session
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'sets the correct updater' do
|
30
|
+
request.session = { user_id: @hera.id }
|
31
|
+
get :edit, params: { id: @hera.id }
|
32
|
+
expect(response.status).to eq(200)
|
33
|
+
|
34
|
+
simulate_second_request
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
context 'when the handler raises an exception' do
|
39
|
+
before { @stamper = User.stamper }
|
40
|
+
it 'restores the correct stamper' do
|
41
|
+
begin
|
42
|
+
request.session = { user_id: @zeus.id }
|
43
|
+
post :create
|
44
|
+
rescue
|
45
|
+
end
|
46
|
+
|
47
|
+
expect(User.stamper).to be(@stamper)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|