set_vestal_versions 1.2.2
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/LICENSE +20 -0
- data/README.rdoc +196 -0
- data/lib/generators/vestal_versions.rb +11 -0
- data/lib/generators/vestal_versions/migration/migration_generator.rb +17 -0
- data/lib/generators/vestal_versions/migration/templates/initializer.rb +9 -0
- data/lib/generators/vestal_versions/migration/templates/migration.rb +28 -0
- data/lib/vestal_versions.rb +126 -0
- data/lib/vestal_versions/changes.rb +122 -0
- data/lib/vestal_versions/conditions.rb +57 -0
- data/lib/vestal_versions/control.rb +200 -0
- data/lib/vestal_versions/creation.rb +93 -0
- data/lib/vestal_versions/deletion.rb +39 -0
- data/lib/vestal_versions/options.rb +41 -0
- data/lib/vestal_versions/reload.rb +17 -0
- data/lib/vestal_versions/reset.rb +24 -0
- data/lib/vestal_versions/reversion.rb +82 -0
- data/lib/vestal_versions/users.rb +55 -0
- data/lib/vestal_versions/version.rb +80 -0
- data/lib/vestal_versions/version_num.rb +3 -0
- data/lib/vestal_versions/version_tagging.rb +50 -0
- data/lib/vestal_versions/versioned.rb +27 -0
- data/lib/vestal_versions/versions.rb +74 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/models.rb +19 -0
- data/spec/support/schema.rb +25 -0
- data/spec/vestal_versions/changes_spec.rb +134 -0
- data/spec/vestal_versions/conditions_spec.rb +103 -0
- data/spec/vestal_versions/control_spec.rb +120 -0
- data/spec/vestal_versions/creation_spec.rb +90 -0
- data/spec/vestal_versions/deletion_spec.rb +86 -0
- data/spec/vestal_versions/options_spec.rb +45 -0
- data/spec/vestal_versions/reload_spec.rb +18 -0
- data/spec/vestal_versions/reset_spec.rb +111 -0
- data/spec/vestal_versions/reversion_spec.rb +103 -0
- data/spec/vestal_versions/users_spec.rb +21 -0
- data/spec/vestal_versions/version_spec.rb +61 -0
- data/spec/vestal_versions/version_tagging_spec.rb +39 -0
- data/spec/vestal_versions/versioned_spec.rb +16 -0
- data/spec/vestal_versions/versions_spec.rb +176 -0
- metadata +165 -0
@@ -0,0 +1,57 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Allows version creation to occur conditionally based on given <tt>:if</tt> and/or
|
3
|
+
# <tt>:unless</tt> options.
|
4
|
+
module Conditions
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Class methods on ActiveRecord::Base to prepare the <tt>:if</tt> and <tt>:unless</tt> options.
|
8
|
+
module ClassMethods
|
9
|
+
# After the original +prepare_versioned_options+ method cleans the given options, this alias
|
10
|
+
# also extracts the <tt>:if</tt> and <tt>:unless</tt> options, chaning them into arrays
|
11
|
+
# and converting any symbols to procs. Procs are called with the ActiveRecord model instance
|
12
|
+
# as the sole argument.
|
13
|
+
#
|
14
|
+
# If all of the <tt>:if</tt> conditions are met and none of the <tt>:unless</tt> conditions
|
15
|
+
# are unmet, than version creation will proceed, assuming all other conditions are also met.
|
16
|
+
def prepare_versioned_options(options)
|
17
|
+
result = super(options)
|
18
|
+
|
19
|
+
vestal_versions_options[:if] = Array(options.delete(:if)).map(&:to_proc)
|
20
|
+
vestal_versions_options[:unless] = Array(options.delete(:unless)).map(&:to_proc)
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Instance methods that determine based on the <tt>:if</tt> and <tt>:unless</tt> conditions,
|
27
|
+
# whether a version is to be create or updated.
|
28
|
+
module InstanceMethods
|
29
|
+
private
|
30
|
+
# After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
|
31
|
+
# satisfied, the original, unaliased +create_version?+ method is called to determine
|
32
|
+
# whether a new version should be created upon update of the ActiveRecord::Base instance.
|
33
|
+
def create_version?
|
34
|
+
version_conditions_met? && super
|
35
|
+
end
|
36
|
+
|
37
|
+
# After first determining whether the <tt>:if</tt> and <tt>:unless</tt> conditions are
|
38
|
+
# satisfied, the original, unaliased +update_version?+ method is called to determine
|
39
|
+
# whther the last version should be updated to include changes merged from the current
|
40
|
+
# ActiveRecord::Base instance update.
|
41
|
+
#
|
42
|
+
# The overridden +update_version?+ method simply returns false, effectively delegating
|
43
|
+
# the decision to whether the <tt>:if</tt> and <tt>:unless</tt> conditions are met.
|
44
|
+
def update_version?
|
45
|
+
version_conditions_met? && super
|
46
|
+
end
|
47
|
+
|
48
|
+
# Simply checks whether the <tt>:if</tt> and <tt>:unless</tt> conditions given in the
|
49
|
+
# +versioned+ options are met: meaning that all procs in the <tt>:if</tt> array must
|
50
|
+
# evaluate to a non-false, non-nil value and that all procs in the <tt>:unless</tt> array
|
51
|
+
# must all evaluate to either false or nil.
|
52
|
+
def version_conditions_met?
|
53
|
+
vestal_versions_options[:if].all?{|p| p.call(self) } && !vestal_versions_options[:unless].any?{|p| p.call(self) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,200 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# The control feature allows use of several code blocks that provide finer control over whether
|
3
|
+
# a new version is created, or a previous version is updated.
|
4
|
+
module Control
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
class_attribute :_skip_version, :instance_writer => false
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
# Control blocks are called on ActiveRecord::Base instances as to not cause any conflict with
|
13
|
+
# other instances of the versioned class whose behavior could be inadvertently altered within
|
14
|
+
# a control block.
|
15
|
+
module InstanceMethods
|
16
|
+
# The +skip_version+ block simply allows for updates to be made to an instance of a versioned
|
17
|
+
# ActiveRecord model while ignoring all new version creation. The <tt>:if</tt> and
|
18
|
+
# <tt>:unless</tt> conditions (if given) will not be evaulated inside a +skip_version+ block.
|
19
|
+
#
|
20
|
+
# When the block closes, the instance is automatically saved, so explicitly saving the
|
21
|
+
# object within the block is unnecessary.
|
22
|
+
#
|
23
|
+
# == Example
|
24
|
+
#
|
25
|
+
# user = User.find_by_first_name("Steve")
|
26
|
+
# user.version # => 1
|
27
|
+
# user.skip_version do
|
28
|
+
# user.first_name = "Stephen"
|
29
|
+
# end
|
30
|
+
# user.version # => 1
|
31
|
+
def skip_version
|
32
|
+
_with_version_flag(:_skip_version) do
|
33
|
+
yield if block_given?
|
34
|
+
save
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Behaving almost identically to the +skip_version+ block, the only difference with the
|
39
|
+
# +skip_version!+ block is that the save automatically performed at the close of the block
|
40
|
+
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
|
41
|
+
def skip_version!
|
42
|
+
_with_version_flag(:_skip_version) do
|
43
|
+
yield if block_given?
|
44
|
+
save!
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Merging versions with the +merge_version+ block will take all of the versions that would
|
49
|
+
# be created within the block and merge them into one version and pushing that single version
|
50
|
+
# onto the ActiveRecord::Base instance's version history. A new version will be created and
|
51
|
+
# the instance's version number will be incremented.
|
52
|
+
#
|
53
|
+
# == Example
|
54
|
+
#
|
55
|
+
# user = User.find_by_first_name("Steve")
|
56
|
+
# user.version # => 1
|
57
|
+
# user.merge_version do
|
58
|
+
# user.update_attributes(:first_name => "Steven", :last_name => "Tyler")
|
59
|
+
# user.update_attribute(:first_name, "Stephen")
|
60
|
+
# user.update_attribute(:last_name, "Richert")
|
61
|
+
# end
|
62
|
+
# user.version # => 2
|
63
|
+
# user.versions.last.changes
|
64
|
+
# # => {"first_name" => ["Steve", "Stephen"], "last_name" => ["Jobs", "Richert"]}
|
65
|
+
#
|
66
|
+
# See VestalVersions::Changes for an explanation on how changes are appended.
|
67
|
+
def merge_version
|
68
|
+
_with_version_flag(:merge_version) do
|
69
|
+
yield if block_given?
|
70
|
+
end
|
71
|
+
save
|
72
|
+
end
|
73
|
+
|
74
|
+
# Behaving almost identically to the +merge_version+ block, the only difference with the
|
75
|
+
# +merge_version!+ block is that the save automatically performed at the close of the block
|
76
|
+
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
|
77
|
+
def merge_version!
|
78
|
+
_with_version_flag(:merge_version) do
|
79
|
+
yield if block_given?
|
80
|
+
end
|
81
|
+
save!
|
82
|
+
end
|
83
|
+
|
84
|
+
# A convenience method for determining whether a versioned instance is set to merge its next
|
85
|
+
# versions into one before version creation.
|
86
|
+
def merge_version?
|
87
|
+
!!@merge_version
|
88
|
+
end
|
89
|
+
|
90
|
+
# Appending versions with the +append_version+ block acts similarly to the +merge_version+
|
91
|
+
# block in that all would-be version creations within the block are defered until the block
|
92
|
+
# closes. The major difference is that with +append_version+, a new version is not created.
|
93
|
+
# Rather, the cumulative changes are appended to the serialized changes of the instance's
|
94
|
+
# last version. A new version is not created, so the version number is not incremented.
|
95
|
+
#
|
96
|
+
# == Example
|
97
|
+
#
|
98
|
+
# user = User.find_by_first_name("Steve")
|
99
|
+
# user.version # => 2
|
100
|
+
# user.versions.last.changes
|
101
|
+
# # => {"first_name" => ["Stephen", "Steve"]}
|
102
|
+
# user.append_version do
|
103
|
+
# user.last_name = "Jobs"
|
104
|
+
# end
|
105
|
+
# user.versions.last.changes
|
106
|
+
# # => {"first_name" => ["Stephen", "Steve"], "last_name" => ["Richert", "Jobs"]}
|
107
|
+
# user.version # => 2
|
108
|
+
#
|
109
|
+
# See VestalVersions::Changes for an explanation on how changes are appended.
|
110
|
+
def append_version
|
111
|
+
_with_version_flag(:merge_version) do
|
112
|
+
yield if block_given?
|
113
|
+
end
|
114
|
+
|
115
|
+
_with_version_flag(:append_version) do
|
116
|
+
save
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Behaving almost identically to the +append_version+ block, the only difference with the
|
121
|
+
# +append_version!+ block is that the save automatically performed at the close of the block
|
122
|
+
# is a +save!+, meaning that an exception will be raised if the object cannot be saved.
|
123
|
+
def append_version!
|
124
|
+
_with_version_flag(:merge_version) do
|
125
|
+
yield if block_given?
|
126
|
+
end
|
127
|
+
|
128
|
+
_with_version_flag(:append_version) do
|
129
|
+
save!
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# A convenience method for determining whether a versioned instance is set to append its next
|
134
|
+
# version's changes into the last version changes.
|
135
|
+
def append_version?
|
136
|
+
!!@append_version
|
137
|
+
end
|
138
|
+
|
139
|
+
# Used for each control block, the +_with_version_flag+ method sets a given variable to
|
140
|
+
# true and then executes the given block, ensuring that the variable is returned to a nil
|
141
|
+
# value before returning. This is useful to be certain that one of the control flag
|
142
|
+
# instance variables isn't inadvertently left in the "on" position by execution within the
|
143
|
+
# block raising an exception.
|
144
|
+
def _with_version_flag(flag)
|
145
|
+
instance_variable_set("@#{flag}", true)
|
146
|
+
yield
|
147
|
+
ensure
|
148
|
+
remove_instance_variable("@#{flag}")
|
149
|
+
end
|
150
|
+
|
151
|
+
# Overrides the basal +create_version?+ method to make sure that new versions are not
|
152
|
+
# created when inside any of the control blocks (until the block terminates).
|
153
|
+
def create_version?
|
154
|
+
!_skip_version? && !merge_version? && !append_version? && super
|
155
|
+
end
|
156
|
+
|
157
|
+
# Overrides the basal +update_version?+ method to allow the last version of an versioned
|
158
|
+
# ActiveRecord::Base instance to be updated at the end of an +append_version+ block.
|
159
|
+
def update_version?
|
160
|
+
append_version?
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
module ClassMethods
|
165
|
+
# The +skip_version+ block simply allows for updates to be made to an instance of a versioned
|
166
|
+
# ActiveRecord model while ignoring all new version creation. The <tt>:if</tt> and
|
167
|
+
# <tt>:unless</tt> conditions (if given) will not be evaulated inside a +skip_version+ block.
|
168
|
+
#
|
169
|
+
# When the block closes, the instance is automatically saved, so explicitly saving the
|
170
|
+
# object within the block is unnecessary.
|
171
|
+
#
|
172
|
+
# == Example
|
173
|
+
#
|
174
|
+
# user = User.find_by_first_name("Steve")
|
175
|
+
# user.version # => 1
|
176
|
+
# user.skip_version do
|
177
|
+
# user.first_name = "Stephen"
|
178
|
+
# end
|
179
|
+
# user.version # => 1
|
180
|
+
def skip_version
|
181
|
+
_with_version_flag(:_skip_version) do
|
182
|
+
yield if block_given?
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# Used for each control block, the +with_version_flag+ method sets a given variable to
|
187
|
+
# true and then executes the given block, ensuring that the variable is returned to a nil
|
188
|
+
# value before returning. This is useful to be certain that one of the control flag
|
189
|
+
# instance variables isn't inadvertently left in the "on" position by execution within the
|
190
|
+
# block raising an exception.
|
191
|
+
def _with_version_flag(flag)
|
192
|
+
self.send("#{flag}=", true)
|
193
|
+
yield
|
194
|
+
ensure
|
195
|
+
self.send("#{flag}=", nil)
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Adds the functionality necessary to control version creation on a versioned instance of
|
3
|
+
# ActiveRecord::Base.
|
4
|
+
module Creation
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
after_create :create_initial_version, :if => :create_initial_version?
|
9
|
+
after_update :create_version, :if => :create_version?
|
10
|
+
after_update :update_version, :if => :update_version?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Class methods added to ActiveRecord::Base to facilitate the creation of new versions.
|
14
|
+
module ClassMethods
|
15
|
+
# Overrides the basal +prepare_versioned_options+ method defined in VestalVersions::Options
|
16
|
+
# to extract the <tt>:only</tt>, <tt>:except</tt> and <tt>:initial_version</tt> options
|
17
|
+
# into +vestal_versions_options+.
|
18
|
+
def prepare_versioned_options(options)
|
19
|
+
result = super(options)
|
20
|
+
|
21
|
+
self.vestal_versions_options[:only] = Array(options.delete(:only)).map(&:to_s).uniq if options[:only]
|
22
|
+
self.vestal_versions_options[:except] = Array(options.delete(:except)).map(&:to_s).uniq if options[:except]
|
23
|
+
self.vestal_versions_options[:initial_version] = options.delete(:initial_version)
|
24
|
+
|
25
|
+
result
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Instance methods that determine whether to save a version and actually perform the save.
|
30
|
+
module InstanceMethods
|
31
|
+
private
|
32
|
+
# Returns whether an initial version should be created upon creation of the parent record.
|
33
|
+
def create_initial_version?
|
34
|
+
vestal_versions_options[:initial_version] == true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Creates an initial version upon creation of the parent record.
|
38
|
+
def create_initial_version
|
39
|
+
versions.create(version_attributes.merge(:number => 1))
|
40
|
+
reset_version_changes
|
41
|
+
reset_version
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns whether a new version should be created upon updating the parent record.
|
45
|
+
def create_version?
|
46
|
+
!version_changes.blank?
|
47
|
+
end
|
48
|
+
|
49
|
+
# Creates a new version upon updating the parent record.
|
50
|
+
def create_version(attributes = nil)
|
51
|
+
versions.create(attributes || version_attributes)
|
52
|
+
reset_version_changes
|
53
|
+
reset_version
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns whether the last version should be updated upon updating the parent record.
|
57
|
+
# This method is overridden in VestalVersions::Control to account for a control block that
|
58
|
+
# merges changes onto the previous version.
|
59
|
+
def update_version?
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
# Updates the last version's changes by appending the current version changes.
|
64
|
+
def update_version
|
65
|
+
return create_version unless v = versions.last
|
66
|
+
v.modifications_will_change!
|
67
|
+
v.update_attribute(:modifications, v.changes.append_changes(version_changes))
|
68
|
+
reset_version_changes
|
69
|
+
reset_version
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns an array of column names that should be included in the changes of created
|
73
|
+
# versions. If <tt>vestal_versions_options[:only]</tt> is specified, only those columns
|
74
|
+
# will be versioned. Otherwise, if <tt>vestal_versions_options[:except]</tt> is specified,
|
75
|
+
# all columns will be versioned other than those specified. Without either option, the
|
76
|
+
# default is to version all columns. At any rate, the four "automagic" timestamp columns
|
77
|
+
# maintained by Rails are never versioned.
|
78
|
+
def versioned_columns
|
79
|
+
case
|
80
|
+
when vestal_versions_options[:only] then self.class.column_names & vestal_versions_options[:only]
|
81
|
+
when vestal_versions_options[:except] then self.class.column_names - vestal_versions_options[:except]
|
82
|
+
else self.class.column_names
|
83
|
+
end - %w(created_at created_on updated_at updated_on)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Specifies the attributes used during version creation. This is separated into its own
|
87
|
+
# method so that it can be overridden by the VestalVersions::Users feature.
|
88
|
+
def version_attributes
|
89
|
+
{:modifications => version_changes, :number => last_version + 1}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Allows version creation to occur conditionally based on given <tt>:if</tt> and/or
|
3
|
+
# <tt>:unless</tt> options.
|
4
|
+
module Deletion
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
before_destroy :create_destroyed_version, :if => :delete_version?
|
9
|
+
end
|
10
|
+
|
11
|
+
# Class methods on ActiveRecord::Base
|
12
|
+
module ClassMethods
|
13
|
+
# After the original +prepare_versioned_options+ method cleans the given options, this alias
|
14
|
+
# also extracts the <tt>:depedent</tt> if it set to <tt>:tracking</tt>
|
15
|
+
def prepare_versioned_options(options)
|
16
|
+
result = super(options)
|
17
|
+
if result[:dependent] == :tracking
|
18
|
+
self.vestal_versions_options[:track_destroy] = true
|
19
|
+
options.delete(:dependent)
|
20
|
+
end
|
21
|
+
|
22
|
+
result
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module InstanceMethods
|
27
|
+
private
|
28
|
+
|
29
|
+
def delete_version?
|
30
|
+
vestal_versions_options[:track_destroy]
|
31
|
+
end
|
32
|
+
|
33
|
+
def create_destroyed_version
|
34
|
+
create_version({:modifications => attributes, :number => last_version + 1, :tag => 'deleted'})
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Provides +versioned+ options conversion and cleanup.
|
3
|
+
module Options
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
# Class methods that provide preparation of options passed to the +versioned+ method.
|
7
|
+
module ClassMethods
|
8
|
+
# The +prepare_versioned_options+ method has three purposes:
|
9
|
+
# 1. Populate the provided options with default values where needed
|
10
|
+
# 2. Prepare options for use with the +has_many+ association
|
11
|
+
# 3. Save user-configurable options in a class-level variable
|
12
|
+
#
|
13
|
+
# Options are given priority in the following order:
|
14
|
+
# 1. Those passed directly to the +versioned+ method
|
15
|
+
# 2. Those specified in an initializer +configure+ block
|
16
|
+
# 3. Default values specified in +prepare_versioned_options+
|
17
|
+
#
|
18
|
+
# The method is overridden in feature modules that require specific options outside the
|
19
|
+
# standard +has_many+ associations.
|
20
|
+
def prepare_versioned_options(options)
|
21
|
+
options.symbolize_keys!
|
22
|
+
options.reverse_merge!(VestalVersions.config)
|
23
|
+
options.reverse_merge!(
|
24
|
+
:class_name => 'VestalVersions::Version',
|
25
|
+
:dependent => :delete_all
|
26
|
+
)
|
27
|
+
# options.reverse_merge!(
|
28
|
+
# :order => "#{options[:class_name].constantize.table_name}.#{connection.quote_column_name('number')} ASC"
|
29
|
+
# )
|
30
|
+
|
31
|
+
class_inheritable_accessor :vestal_versions_options
|
32
|
+
self.vestal_versions_options = options.dup
|
33
|
+
|
34
|
+
options.merge!(
|
35
|
+
:as => :versioned,
|
36
|
+
:extend => Array(options[:extend]).unshift(Versions)
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module VestalVersions
|
2
|
+
# Ties into the existing ActiveRecord::Base#reload method to ensure that version information
|
3
|
+
# is properly reset.
|
4
|
+
module Reload
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
# Adds instance methods into ActiveRecord::Base to tap into the +reload+ method.
|
8
|
+
module InstanceMethods
|
9
|
+
# Overrides ActiveRecord::Base#reload, resetting the instance-variable-cached version number
|
10
|
+
# before performing the original +reload+ method.
|
11
|
+
def reload(*args)
|
12
|
+
reset_version
|
13
|
+
super
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|