millstone 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .rvmrc
2
+ .bundle
3
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=d
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in millstone.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Yoshikazu Ozawa
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # Millstone
2
+
3
+ ActiveRecord plugin which hides records instead of deleting them for Rails3
4
+
5
+ Millstone is extending ActiveRecord::Relation
6
+
7
+ ## Credits
8
+
9
+ This plugin was inspired by [rails3_acts_as_paranoid](https://github.com/goncalossilva/rails3_acts_as_paranoid) and [acts_as_paranoid](http://github.com/technoweenie/acts_as_paranoid).
10
+
11
+ ## Usage
12
+
13
+ class User < ActiveRecord::Base
14
+ millstone
15
+ end
16
+
17
+ ### Options
18
+
19
+ - :column => 'deleted_at'
20
+ - :type => 'time'
21
+
22
+ ### Filtering
23
+
24
+ User.all # retrieves the non-deleted records
25
+ User.only_deleted # retrieves the deleted records
26
+ User.with_deleted # retrieves all records, deleted or not
27
+
28
+ ### Real deletion
29
+
30
+ user.destroy!
31
+ user.delete_all!(conditions)
32
+
33
+ ### Validation
34
+
35
+ class User < ActiveRecord::Base
36
+ acts_as_paranoid
37
+ validates_uniqueness_of :name
38
+ end
39
+
40
+ User.create(:name => 'foo').destroy
41
+ User.new(:name => 'foo').valid? #=> true
42
+
43
+
44
+ class User < ActiveRecord::Base
45
+ acts_as_paranoid
46
+ validates_uniqueness_of_with_deleted :name
47
+ end
48
+
49
+ User.create(:name => 'foo').destroy
50
+ User.new(:name => 'foo').valid? #=> false
51
+
52
+ ### Status
53
+
54
+ user = User.create(:name => 'foo')
55
+ user.deleted? #=> false
56
+ user.destroy
57
+ User.with_deleted.first.deleted? #=> true
58
+
59
+ ### Association options
60
+
61
+ class Parent < ActiveRecord::Base
62
+ acts_as_pranoid
63
+ has_many :children
64
+ end
65
+
66
+ class Child < ActiveRecord::Base
67
+ acts_as_paranoid
68
+ belongs_to :parent
69
+ end
70
+
71
+ parent = Parent.create(:name => "foo")
72
+ child = parent.children.create!(:name => "bar")
73
+ parent.destroy
74
+ child.parent # => nil
75
+
76
+
77
+ class Child < ActiveRecord::Base
78
+ acts_as_paranoid
79
+ belongs_to :parent, :with_deleted => true
80
+ end
81
+
82
+ parent = Parent.create(:name => "foo")
83
+ child = parent.children.create!(:name => "bar")
84
+ parent.destroy
85
+ child.parent #=> #<Parent ... deleted_at: deleted_time>
86
+ child.parent.deleted? #=> true
87
+
88
+ Copyright © 2011 Yoshikazu Ozawa, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require 'bundler'
3
+ Bundler::GemHelper.install_tasks
4
+
5
+ require 'rspec/core'
6
+ require 'rspec/core/rake_task'
7
+ RSpec::Core::RakeTask.new(:spec) do |spec|
8
+ spec.pattern = FileList['spec/**/*_spec.rb']
9
+ end
10
+
11
+ task :default => :spec
12
+
@@ -0,0 +1,14 @@
1
+ module Millstone
2
+ module ActiveRecord
3
+ module Associations
4
+ class BelongsToAssociation < ::ActiveRecord::Associations::BelongsToAssociation
5
+ private
6
+ def find_target
7
+ @reflection.klass.send(:with_scope, :find => {:with_deleted => true}) do
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module Millstone
2
+ module ActiveRecord
3
+ module Associations
4
+ class BelongsToPolymorphicAssociation < ::ActiveRecord::Associations::BelongsToPolymorphicAssociation
5
+ private
6
+ def find_target
7
+ association_class.try(:send, :with_scope, :find => {:with_deleted => true}) do
8
+ super
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,19 @@
1
+ module Millstone
2
+ module ActiveRecord
3
+ module Associations
4
+ class HasManyThroughWithoutDeletedAssociation < ::ActiveRecord::Associations::HasManyThroughAssociation
5
+ def construct_conditions
6
+ return super unless @reflection.through_reflection.klass.as_millstone?
7
+
8
+ table_name = @reflection.through_reflection.quoted_table_name
9
+ conditions = construct_quoted_owner_attributes(@reflection.through_reflection).map do |attr, value|
10
+ "#{table_name}.#{attr} = #{value}"
11
+ end
12
+ conditions << @reflection.through_reflection.klass.millstone_without_deleted_conditions
13
+ conditions << sql_conditions if sql_conditions
14
+ "(" + conditions.join(') AND (') + ")"
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,50 @@
1
+ require 'millstone/active_record/associations/belongs_to_association'
2
+ require 'millstone/active_record/associations/belongs_to_polymorphic_association'
3
+ require 'millstone/active_record/associations/has_many_through_without_deleted_association'
4
+
5
+ module Millstone
6
+ module ActiveRecord
7
+ module Associations
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ class << self
12
+ alias_method_chain :has_many, :millstone
13
+ alias_method_chain :belongs_to, :millstone
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ def has_many_with_millstone(association_id, options = {}, &extension)
19
+ with_deleted = options.delete :with_deleted
20
+ has_many_without_millstone(association_id, options, &extension).tap do
21
+ reflection = reflect_on_association(association_id)
22
+
23
+ if options[:through] and !with_deleted
24
+ collection_accessor_methods(reflection, Millstone::ActiveRecord::Associations::HasManyThroughWithoutDeletedAssociation)
25
+ end
26
+ end
27
+ end
28
+
29
+ def belongs_to_with_millstone(association_id, options = {})
30
+ with_deleted = options.delete :with_deleted
31
+ belongs_to_without_millstone(association_id, options).tap do
32
+ if with_deleted
33
+ reflection = reflect_on_association(association_id)
34
+
35
+ if reflection.options[:polymorphic]
36
+ association_accessor_methods(reflection, Millstone::ActiveRecord::Associations::BelongsToPolymorphicAssociation)
37
+ else
38
+ association_accessor_methods(reflection, Millstone::ActiveRecord::Associations::BelongsToAssociation)
39
+ association_constructor_method(:build, reflection, Millstone::ActiveRecord::Associations::BelongsToAssociation)
40
+ association_constructor_method(:create, reflection, Millstone::ActiveRecord::Associations::BelongsToAssociation)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ ActiveRecord::Base.send :include, Millstone::ActiveRecord::Associations
@@ -0,0 +1,137 @@
1
+ require 'millstone/active_record/relation_methods'
2
+ require 'millstone/active_record/validations/uniqueness_with_deleted'
3
+
4
+ module Millstone
5
+ module ActiveRecord
6
+ class AlreadyMarkedDeletion < StandardError
7
+ end
8
+
9
+ module Extension
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ class << self
14
+ # for acts_as_paranoid
15
+ alias_method :acts_as_paranoid, :millstone
16
+ alias_method :paranoid?, :as_millstone?
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ def as_millstone?
22
+ self.included_modules.include?(InstanceMethods)
23
+ end
24
+
25
+ def millstone(options = {})
26
+ options = options.reverse_merge(:column => :deleted_at, :type => :time)
27
+
28
+ unless [:time, :boolean].include? options[:type]
29
+ raise ArgumentError, "'time' or 'boolean' expected for :type option, got #{options[:type]}"
30
+ end
31
+
32
+ class_attribute :millstone_configuration, :millstone_column_reference
33
+ self.millstone_configuration = options
34
+ self.millstone_column_reference = "#{self.table_name}.#{millstone_configuration[:column]}"
35
+
36
+ return if as_millstone?
37
+
38
+ extend ClassMethods
39
+ include InstanceMethods
40
+ include Validations
41
+
42
+ self.class_eval do
43
+ alias_method :paranoid_value, :millstone_column_value
44
+ end
45
+
46
+ class << self
47
+ delegate :destroy!, :destroy_all!, :delete!, :delete_all!, :to => :scoped
48
+ delegate :with_deleted, :only_deleted, :to => :scoped
49
+ alias_method_chain :relation, :millstone
50
+
51
+ # for acts_as_paranoid
52
+ alias_method :paranoid_column, :millstone_column
53
+ alias_method :paranoid_column_type, :millstone_type
54
+ alias_method :paranoid_column_reference, :millstone_column_reference
55
+ alias_method :paranoid_configuration, :millstone_configuration
56
+ end
57
+ end
58
+
59
+ module ClassMethods
60
+ def millstone_column
61
+ millstone_configuration[:column].to_sym
62
+ end
63
+
64
+ def millstone_type
65
+ millstone_configuration[:type].to_sym
66
+ end
67
+
68
+ def millstone_generate_column_value
69
+ case millstone_type
70
+ when :time then Time.now
71
+ when :boolean then true
72
+ end
73
+ end
74
+
75
+ def millstone_without_deleted_conditions
76
+ sanitize_sql(["#{millstone_column_reference} IS ?", nil])
77
+ end
78
+
79
+ def millstone_only_deleted_conditions
80
+ sanitize_sql(["#{millstone_column_reference} IS NOT ?", nil])
81
+ end
82
+
83
+ private
84
+ def relation_with_millstone
85
+ relation = relation_without_millstone
86
+ relation.extending Millstone::ActiveRecord::RelationMethods
87
+ end
88
+ end
89
+
90
+ module InstanceMethods
91
+ def destroy
92
+ with_transaction_returning_status do
93
+ _run_destroy_callbacks do
94
+ raise AlreadyMarkedDeletion, "#{self.class.name} ID=#{self.id} already marked deletion." unless self.millstone_column_value.nil?
95
+ self.class.delete(self.id)
96
+ self.millstone_column_value = self.class.millstone_generate_column_value
97
+ freeze
98
+ end
99
+ end
100
+ end
101
+
102
+ def destroy!
103
+ with_transaction_returning_status do
104
+ _run_destroy_callbacks do
105
+ self.class.delete!(self.id)
106
+ freeze
107
+ end
108
+ end
109
+ end
110
+
111
+ def recover(options = {})
112
+ raise "Millstone is not support"
113
+ end
114
+
115
+ def recover_dependent_associations(window, options = {})
116
+ raise "Millstone is not support"
117
+ end
118
+
119
+ def deleted?
120
+ !millstone_column_value.nil?
121
+ end
122
+
123
+ def millstone_column_value
124
+ self.send(self.class.millstone_column)
125
+ end
126
+
127
+ private
128
+ def millstone_column_value=(value)
129
+ self.send("#{self.class.millstone_column}=", value)
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+
137
+ ActiveRecord::Base.send :include, Millstone::ActiveRecord::Extension
@@ -0,0 +1,120 @@
1
+ module Millstone
2
+ module ActiveRecord
3
+ module RelationMethods
4
+ def self.extended(base)
5
+ base.class_eval do
6
+ attr_accessor :with_deleted_value, :only_deleted_value
7
+ end
8
+ end
9
+
10
+ def destroy_all!(conditions = nil)
11
+ if with_deleted_value.nil? and only_deleted_value.nil?
12
+ with_deleted.destroy_all!(conditions)
13
+ elsif conditions
14
+ where(conditions).destroy_all!
15
+ else
16
+ to_a.each {|object| object.destroy! }.tap { reset }
17
+ end
18
+ end
19
+
20
+ def destroy!(id)
21
+ if with_deleted_value.nil? and only_deleted_value.nil?
22
+ with_deleted.destroy!(id)
23
+ elsif id.is_a?(Array)
24
+ id.map {|one_id| destroy!(one_id) }
25
+ else
26
+ find(id).destroy!
27
+ end
28
+ end
29
+
30
+ def delete_all(conditions = nil)
31
+ conditions ? where(conditions).delete_all : update_all(@klass.millstone_column => @klass.millstone_generate_column_value)
32
+ end
33
+
34
+ def delete_all!(conditions = nil)
35
+ if with_deleted_value.nil? and only_deleted_value.nil?
36
+ with_deleted.delete_all!(conditions)
37
+ elsif conditions
38
+ where(conditions).delete_all!
39
+ else
40
+ arel.delete.tap { reset }
41
+ end
42
+ end
43
+
44
+ def delete(id_or_array)
45
+ where(@klass.primary_key => id_or_array).update_all(@klass.millstone_column => @klass.millstone_generate_column_value)
46
+ end
47
+
48
+ def delete!(id_or_array)
49
+ where(@klass.primary_key => id_or_array).delete_all!
50
+ end
51
+
52
+ def with_deleted(with = true)
53
+ relation = clone
54
+ relation.with_deleted_value = with
55
+ relation
56
+ end
57
+
58
+ def only_deleted(only = true)
59
+ relation = clone
60
+ relation.only_deleted_value = only
61
+ relation
62
+ end
63
+
64
+ def merge(r)
65
+ merged_relation = super
66
+ [:with_deleted, :only_deleted].each do |method|
67
+ value = r.send(:"#{method}_value")
68
+ merged_relation.send(:"#{method}_value=", value) unless value.nil?
69
+ end
70
+ merged_relation
71
+ end
72
+
73
+ def except(*skips)
74
+ result = super
75
+ ([:with_deleted, :only_deleted] - skips).each do |method|
76
+ result.send(:"#{method}_value=", send(:"#{method}_value"))
77
+ end
78
+ result
79
+ end
80
+
81
+ def only(*onlies)
82
+ result = super
83
+ ([:with_deleted, :only_deleted] & onlies).each do |method|
84
+ result.send(:"#{method}_value=", send(:"#{method}_value"))
85
+ end
86
+ result
87
+ end
88
+
89
+ def apply_finder_options(options)
90
+ return clone unless options
91
+
92
+ finders = options.dup
93
+ with_deleted = finders.delete(:with_deleted)
94
+ only_deleted = finders.delete(:only_deleted)
95
+
96
+ relation = super(finders)
97
+ relation = relation.with_deleted(with_deleted) unless with_deleted.nil?
98
+ relation = relation.only_deleted(only_deleted) unless only_deleted.nil?
99
+
100
+ relation
101
+ end
102
+
103
+ private
104
+ def build_arel
105
+ arel = super
106
+ return arel unless @klass.as_millstone?
107
+
108
+ if @with_deleted_value
109
+ # nothing
110
+ elsif @only_deleted_value
111
+ arel = collapse_wheres(arel, [@klass.millstone_only_deleted_conditions])
112
+ else
113
+ arel = collapse_wheres(arel, [@klass.millstone_without_deleted_conditions])
114
+ end
115
+
116
+ arel
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,49 @@
1
+ require 'active_support/core_ext/array/wrap'
2
+
3
+ module Millstone
4
+ module ActiveRecord
5
+ module Validations
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ end
10
+
11
+ class UniquenessWithDeletedValidator < ::ActiveRecord::Validations::UniquenessValidator
12
+ def validate_each(record, attribute, value)
13
+ finder_class = find_finder_class_for(record)
14
+ table = finder_class.unscoped
15
+
16
+ table_name = record.class.quoted_table_name
17
+
18
+ if value && record.class.serialized_attributes.key?(attribute.to_s)
19
+ value = YAML.dump value
20
+ end
21
+
22
+ sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
23
+
24
+ relation = table.with_deleted.where(sql, *params) # find with deleted
25
+
26
+ Array.wrap(options[:scope]).each do |scope_item|
27
+ scope_value = record.send(scope_item)
28
+ relation = relation.where(scope_item => scope_value)
29
+ end
30
+
31
+ unless record.new_record?
32
+ # TODO : This should be in Arel
33
+ relation = relation.where("#{record.class.quoted_table_name}.#{record.class.primary_key} <> ?", record.send(:id))
34
+ end
35
+
36
+ if relation.exists?
37
+ record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope).merge(:value => value))
38
+ end
39
+ end
40
+ end
41
+
42
+ module ClassMethods
43
+ def validates_uniqueness_of_with_deleted(*attr_names)
44
+ validates_with UniquenessWithDeletedValidator, _merge_attributes(attr_names)
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
data/lib/millstone.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'millstone/active_record/extension'
2
+ require 'millstone/active_record/associations'
data/millstone.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'millstone'
6
+ s.version = '0.0.2'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Yoshikazu Ozawa']
9
+ s.email = ['yoshikazu.ozawa@gmail.com']
10
+ s.homepage = 'https://github.com/relax4u/millstone'
11
+ s.summary = 'ActiveRecord plugin which allows you to hide without actually deleting them for Rails3.'
12
+ s.description = 'ActiveRecord plugin which allows you to hide without actually deleting them for Rails3. Millstone is extending ActiveRecord::Relation'
13
+
14
+ s.files = `git ls-files`.split("\n")
15
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
16
+
17
+ s.require_paths = ['lib']
18
+
19
+ s.licenses = ['MIT']
20
+
21
+ s.add_dependency 'rails', ['>= 3.0.6']
22
+ s.add_development_dependency 'bundler', ['>= 1.0.0']
23
+ s.add_development_dependency 'sqlite3', ['>= 0']
24
+ s.add_development_dependency 'rspec', ['>= 2.0.0']
25
+ s.add_development_dependency 'rspec-rails', ['>= 2.0.0']
26
+ s.add_development_dependency 'delorean', ['>= 0']
27
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ class ParentHasChild < ActiveRecord::Base
4
+ set_table_name 'time_columns'
5
+ millstone
6
+ has_one :child, :class_name => 'Child', :foreign_key => 'parent_id'
7
+ has_one :child_with_millstone, :class_name => 'ChildWithDeleted', :foreign_key => 'parent_id'
8
+ end
9
+
10
+ class Child < ActiveRecord::Base
11
+ set_table_name 'children'
12
+ belongs_to :parent, :class_name => 'ParentHasChild', :foreign_key => 'parent_id'
13
+ end
14
+
15
+ class ChildWithDeleted < ActiveRecord::Base
16
+ set_table_name 'children'
17
+ belongs_to :parent, :class_name => 'ParentHasChild', :foreign_key => 'parent_id', :with_deleted => true
18
+ end
19
+
20
+ describe Millstone::ActiveRecord::Associations::BelongsToAssociation do
21
+ describe ActiveRecord::Base do
22
+ let(:parent) { ParentHasChild.create!(:context => "parent") }
23
+
24
+ describe ".belongs_to", "with_deleted => false (default)" do
25
+ let(:record) { parent.create_child(:context => "child") }
26
+ let(:record_which_destroyed_parent) do
27
+ record.parent.destroy
28
+ record.reload
29
+ end
30
+
31
+ specify { record.parent.should eq parent }
32
+ specify { record_which_destroyed_parent.parent.should be_nil }
33
+ end
34
+
35
+ describe ".belongs_to", "with_deleted => true" do
36
+ let(:record) { parent.create_child_with_millstone(:context => "child") }
37
+ let(:record_which_destroyed_parent) do
38
+ record.parent.destroy
39
+ record.reload
40
+ end
41
+
42
+ specify { record.parent.should eq parent }
43
+ specify { record_which_destroyed_parent.parent.should eq parent }
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ class ParentHasPolymorphicChild < ActiveRecord::Base
4
+ set_table_name 'time_columns'
5
+ millstone
6
+ has_one :child, :class_name => 'PolymorphicChild', :as => :polymorphous
7
+ has_one :child_with_millstone, :class_name => 'PolymorphicChildWithDeleted', :as => :polymorphous
8
+ end
9
+
10
+ class PolymorphicChild < ActiveRecord::Base
11
+ set_table_name 'polymorphic_children'
12
+ belongs_to :polymorphous, :polymorphic => true
13
+ end
14
+
15
+ class PolymorphicChildWithDeleted < ActiveRecord::Base
16
+ set_table_name 'polymorphic_children'
17
+ belongs_to :polymorphous, :polymorphic => true, :with_deleted => true
18
+ end
19
+
20
+ describe Millstone::ActiveRecord::Associations::BelongsToPolymorphicAssociation do
21
+ describe ActiveRecord::Base do
22
+ let(:parent) { ParentHasPolymorphicChild.create!(:context => "parent") }
23
+
24
+ describe ".belongs_to", "with_deleted => false (default)" do
25
+ let(:record) { parent.create_child(:context => "child") }
26
+ let(:record_which_destroyed_parent) do
27
+ record.polymorphous.destroy
28
+ record.reload
29
+ end
30
+
31
+ specify { record.polymorphous.should eq parent }
32
+ specify { record_which_destroyed_parent.polymorphous.should be_nil }
33
+ end
34
+
35
+ describe ".belongs_to", "with_deleted => true" do
36
+ let(:record) { parent.create_child_with_millstone(:context => "child") }
37
+ let(:record_which_destroyed_parent) do
38
+ record.polymorphous.destroy
39
+ record.reload
40
+ end
41
+
42
+ specify { record.polymorphous.should eq parent }
43
+ specify { record_which_destroyed_parent.polymorphous.should eq parent }
44
+ end
45
+ end
46
+ end