rails_readonly_injector 0.2.0 → 1.2.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.
- checksums.yaml +5 -5
- data/.github/ISSUE_TEMPLATE.md +18 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +24 -0
- data/.gitignore +1 -1
- data/.rubocop.yml +7 -0
- data/.travis.yml +3 -1
- data/Appraisals +10 -1
- data/CHANGELOG.md +6 -0
- data/CODE_OF_CONDUCT.md +0 -0
- data/Gemfile +4 -2
- data/LICENSE.txt +0 -0
- data/README.md +25 -6
- data/Rakefile +4 -2
- data/bin/console +4 -3
- data/cucumber_features/configuration.feature +0 -0
- data/cucumber_features/step_definitions/.gitkeep +0 -0
- data/cucumber_features/step_definitions/generic_steps.rb +5 -3
- data/cucumber_features/step_definitions/user_steps.rb +18 -14
- data/cucumber_features/support/.gitkeep +0 -0
- data/cucumber_features/user_controller.feature +0 -0
- data/cucumber_features/user_controller_readonly.feature +0 -0
- data/cucumber_features/user_model.feature +0 -0
- data/cucumber_features/user_model_readonly.feature +0 -0
- data/development_tasks/support/file_helpers.rb +57 -0
- data/development_tasks/support/gem_helpers.rb +63 -0
- data/development_tasks/tests.rake +40 -93
- data/gemfiles/.bundle/config +0 -0
- data/gemfiles/rails_3.gemfile +0 -0
- data/gemfiles/rails_4_0.gemfile +0 -0
- data/gemfiles/rails_4_1.gemfile +0 -0
- data/gemfiles/rails_4_2.gemfile +0 -0
- data/gemfiles/rails_5_0.gemfile +0 -0
- data/gemfiles/rails_5_1.gemfile +0 -0
- data/gemfiles/rails_5_2.gemfile +10 -0
- data/lib/rails_readonly_injector.rb +37 -20
- data/lib/rails_readonly_injector/configuration.rb +86 -20
- data/lib/rails_readonly_injector/version.rb +3 -1
- data/rails_readonly_injector.gemspec +16 -14
- data/rspec_specs/.gitkeep +0 -0
- data/rspec_specs/rails_readonly_injector/configuration_spec.rb +167 -0
- data/rspec_specs/{readonly_site_toggle_spec.rb → rails_readonly_injector_spec.rb} +42 -1
- data/rspec_specs/support/.gitkeep +0 -0
- metadata +36 -18
- data/rspec_specs/readonly_site_toggle/configuration_spec.rb +0 -31
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b312c92be7dd7bf454f93b2359d6615eac31e5db380c10823da084b1a012c43f
|
4
|
+
data.tar.gz: 82fd6c6929b64104bf7d68044e5c13557bf798cedaad996a5495234c99d08366
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5ebe26220df8db19da6ddf8623b5fb7f3d09a3c192eac5ffec3794106b9008126cbb4276f222027e6e4bfbfa1871f3abbd7da2f7356267a77a9e57760a677a5c
|
7
|
+
data.tar.gz: 476aa9f4c712902fac248b7b185a931b13d056f4414b5b483842087c19ddc7116118ddece04f3a86f41f579f2af1efe5826bce525f3846c60e2a851cf9bc680d
|
@@ -0,0 +1,24 @@
|
|
1
|
+
## Problem
|
2
|
+
|
3
|
+
<!-- Describe the problem that this PR resolves -->
|
4
|
+
|
5
|
+
#### Related issues:
|
6
|
+
<!-- Link to issues that are related to this problem. If there aren't any, you can remove the heading. -->
|
7
|
+
|
8
|
+
## How this PR resolves the problem
|
9
|
+
|
10
|
+
<!--- Describe how your PR resolves this problem -->
|
11
|
+
|
12
|
+
## Types of changes
|
13
|
+
|
14
|
+
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->
|
15
|
+
- [ ] Bug fix (non-breaking change which fixes an issue).
|
16
|
+
- [ ] New feature (non-breaking change which adds functionality).
|
17
|
+
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected).
|
18
|
+
|
19
|
+
## Checklist:
|
20
|
+
|
21
|
+
<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
|
22
|
+
- [ ] I've added tests for my code.
|
23
|
+
- [ ] My change requires a change to the documentation.
|
24
|
+
- [ ] I have updated the documentation accordingly.
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
data/.travis.yml
CHANGED
@@ -13,7 +13,6 @@ branches:
|
|
13
13
|
only:
|
14
14
|
- master
|
15
15
|
|
16
|
-
|
17
16
|
before_script:
|
18
17
|
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
19
18
|
- chmod +x ./cc-test-reporter
|
@@ -29,6 +28,7 @@ gemfile:
|
|
29
28
|
- gemfiles/rails_4_2.gemfile
|
30
29
|
- gemfiles/rails_5_0.gemfile
|
31
30
|
- gemfiles/rails_5_1.gemfile
|
31
|
+
- gemfiles/rails_5_2.gemfile
|
32
32
|
|
33
33
|
matrix:
|
34
34
|
fast_finish: true
|
@@ -41,6 +41,8 @@ matrix:
|
|
41
41
|
gemfile: gemfiles/rails_5_0.gemfile
|
42
42
|
- rvm: 2.1.3
|
43
43
|
gemfile: gemfiles/rails_5_1.gemfile
|
44
|
+
- rvm: 2.1.3
|
45
|
+
gemfile: gemfiles/rails_5_2.gemfile
|
44
46
|
- rvm: 2.2.7
|
45
47
|
gemfile: gemfiles/rails_3.gemfile
|
46
48
|
- rvm: 2.3.6
|
data/Appraisals
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
appraise 'rails_3' do
|
2
4
|
gem 'rails', '~> 3'
|
3
5
|
gem 'cucumber-rails', '~> 1.4.5'
|
@@ -43,4 +45,11 @@ appraise 'rails_5_1' do
|
|
43
45
|
gem 'cucumber-rails', '~> 1.5.0', group: :test, require: false
|
44
46
|
gem 'rspec-rails', '~> 3.7.2', group: :test
|
45
47
|
gem 'database_cleaner', '~> 1.0.1'
|
46
|
-
end
|
48
|
+
end
|
49
|
+
|
50
|
+
appraise 'rails_5_2' do
|
51
|
+
gem 'rails', '~> 5.2.0'
|
52
|
+
gem 'cucumber-rails', '~> 1.6.0', group: :test, require: false
|
53
|
+
gem 'rspec-rails', '~> 3.7.2', group: :test
|
54
|
+
gem 'database_cleaner', '~> 1.0.1'
|
55
|
+
end
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
## 1.0.1
|
2
|
+
- Added support for Rails 5.2
|
3
|
+
## 1.0.0
|
4
|
+
- Removed the ability to read the current read-only status using `RailsReadonlyInjector.config.read_only`, and replaced it with `RailsReadonlyInjector.in_read_only_mode?`.
|
5
|
+
## 0.3.0
|
6
|
+
- Added ability to explicitly include specific classes.
|
1
7
|
## 0.2.0
|
2
8
|
- Added ability to exclude specific classes from read-only mode.
|
3
9
|
- Improved support for Rails 5 (Automatic inclusion of descendants of `ApplicationRecord` rather than `ActiveRecord::Base`).
|
data/CODE_OF_CONDUCT.md
CHANGED
File without changes
|
data/Gemfile
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
source 'https://rubygems.org'
|
4
|
+
|
5
|
+
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
4
6
|
|
5
7
|
# Specify your gem's dependencies in rails_readonly_injector.gemspec
|
6
8
|
gemspec
|
data/LICENSE.txt
CHANGED
File without changes
|
data/README.md
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# Rails ReadOnly Injector
|
2
2
|
[](https://travis-ci.org/xtrasimplicity/rails_readonly_injector)
|
3
|
+
[](http://www.rubydoc.info/github/xtrasimplicity/rails_readonly_injector/master)
|
3
4
|
[](https://codeclimate.com/github/xtrasimplicity/rails_readonly_injector/maintainability)
|
4
5
|
[](https://codeclimate.com/github/xtrasimplicity/rails_readonly_injector/test_coverage)
|
5
6
|
[](https://badge.fury.io/rb/rails_readonly_injector)
|
@@ -24,14 +25,16 @@ Or install it yourself as:
|
|
24
25
|
|
25
26
|
## Usage
|
26
27
|
|
27
|
-
In order to switch a complete site into read-only mode, you will first need to set the action to occur when an attempt to commit changes to the database is made, from within a controller.
|
28
|
+
In order to switch a complete site into read-only mode, you will first need to set the action to occur when an attempt to commit changes to the database is made, from within a controller. This action will be executed from within the context of the referring controller.
|
29
|
+
|
30
|
+
Usually, this won't need to change once the application has booted, so I recommend defining it within an initializer, as follows:
|
28
31
|
|
29
32
|
```
|
30
33
|
# config/initializers/rails_readonly_injector.rb
|
31
34
|
RailsReadonlyInjector.config do |config|
|
32
35
|
config.controller_rescue_action = lambda do |context|
|
33
36
|
# Define actions to be performed if changes to a read-only model are attempted to be committed to the database.
|
34
|
-
# This lambda expression is evaluated from within the
|
37
|
+
# This lambda expression is evaluated from within the context of the referring controller.
|
35
38
|
|
36
39
|
# You may want to set a redirect, or flash an error message, or something, here.
|
37
40
|
# e.g.
|
@@ -41,21 +44,37 @@ RailsReadonlyInjector.config do |config|
|
|
41
44
|
end
|
42
45
|
```
|
43
46
|
|
44
|
-
When you want to switch a site into read-only mode, you can then simply
|
47
|
+
When you want to switch a site into read-only mode, you can then simply
|
48
|
+
* set `RailsReadOnlyInjector.config.read_only` to true, and then
|
49
|
+
* call `RailsReadonlyInjector.reload!` to (re-)load the configuration.
|
50
|
+
|
51
|
+
Alternatively, you can also set `read_only` from within the configuration block inside the initializer.
|
45
52
|
|
46
53
|
If you want to reset the configuration to the defaults, you can simply call `RailsReadonlyInjector.reset_configuration!` from anywhere in your application.
|
47
54
|
|
55
|
+
If you want to check whether read-only mode is currently enabled, you can use `RailsReadonlyInjector.in_read_only_mode?`.
|
56
|
+
|
48
57
|
## Configuration Options
|
58
|
+
The following configuration options can be set through `RailsReadonlyInjector.config`:
|
59
|
+
|
49
60
|
- `read_only` => Whether the site should be in read-only mode. (Default: false)
|
50
|
-
- `
|
61
|
+
- `classes_to_exclude` => An array of classes that should be _exempt_ from read-only mode. (Default: `[]`)
|
62
|
+
- `classes_to_include` => An array of classes that should be set to read-only mode. (Defaults to `ActiveRecord::Base.descendants` on Rails 3-4, and `ApplicationRecord.descendants` on Rails 5.0+)
|
51
63
|
- `controller_rescue_action` => A lambda expression/Proc to execute when an `ActiveRecord::ReadOnlyRecord` error is raised, from within a controller.
|
64
|
+
|
52
65
|
## Development
|
53
66
|
|
54
67
|
After checking out the repo, run `bundle install` to install the dependencies.
|
55
68
|
|
56
|
-
RSpec specs and Cucumber features are stored in `rspec_specs` and `cucumber_features`, respectively. When `bundle exec rake` is run against an appraisal, the contents of these folders are copied to the necessary folders under the temporary rails application that
|
69
|
+
RSpec specs and Cucumber features are stored in `rspec_specs` and `cucumber_features`, respectively. When `bundle exec rake` is run against an appraisal, the contents of these folders are copied to the necessary folders under the temporary rails application that `bundle exec rake` generates into `tmp/rails_app`.
|
70
|
+
|
71
|
+
To run tests for a specific version of Rails, simply run `bundle exec appraisal {APPRAISAL} bundle exec rake`, where `{APPRAISAL}` is one of the appraisals found under `Appraisals`. e.g. `bundle exec appraisal rails_4_1 bundle exec rake`
|
72
|
+
|
73
|
+
If you don't want to re-build the temporary application each time you run tests, you can execute the following rake tasks against an appraisal:
|
57
74
|
|
58
|
-
|
75
|
+
* `dev:run_features` => Synchronises features from `cucumber_features` into the temporary rails application, and runs cucumber against the temporary application.
|
76
|
+
* `dev:run_specs` => Synchronises specs from `rspec_specs` into the temporary rails application, and runs rspec against the temporary application.
|
77
|
+
* `dev:run_tests` => Runs both `dev:run_features` and `dev:run_specs`, and ret on failure.
|
59
78
|
|
60
79
|
## Contributing
|
61
80
|
|
data/Rakefile
CHANGED
@@ -1,7 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
2
4
|
|
3
5
|
development_tasks_path = File.expand_path('../development_tasks', __FILE__)
|
4
6
|
|
5
7
|
Dir[File.join(development_tasks_path, '*.rake')].each { |rb| load rb }
|
6
8
|
|
7
|
-
task :
|
9
|
+
task default: ['dev:deploy_test_app', 'dev:run_tests']
|
data/bin/console
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
|
-
require
|
4
|
-
require
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'rails_readonly_injector'
|
5
6
|
|
6
7
|
# You can add fixtures and/or initialization code here to make experimenting
|
7
8
|
# with your gem easier. You can also use a different console, if you like.
|
@@ -10,5 +11,5 @@ require "rails_readonly_injector"
|
|
10
11
|
# require "pry"
|
11
12
|
# Pry.start
|
12
13
|
|
13
|
-
require
|
14
|
+
require 'irb'
|
14
15
|
IRB.start(__FILE__)
|
File without changes
|
File without changes
|
@@ -1,7 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Given('I execute:') do |code|
|
2
4
|
eval code
|
3
5
|
end
|
4
6
|
|
5
|
-
Then(
|
7
|
+
Then('The page should contain {string}') do |string|
|
6
8
|
expect(page).to have_content(string)
|
7
|
-
end
|
9
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Given('There is a user that has been persisted to the database') do
|
2
4
|
RailsReadonlyInjector.config.read_only = false
|
3
5
|
RailsReadonlyInjector.reload!
|
4
6
|
|
@@ -7,43 +9,45 @@ Given("There is a user that has been persisted to the database") do
|
|
7
9
|
expect(@user.persisted?).to eq(true)
|
8
10
|
end
|
9
11
|
|
10
|
-
Given(
|
12
|
+
Given('There is a user that has not been persisted to the database') do
|
11
13
|
User.destroy_all
|
12
|
-
|
14
|
+
|
13
15
|
@user = User.new(name: 'Bob')
|
14
16
|
end
|
15
17
|
|
16
|
-
When(
|
18
|
+
When('I update the user') do
|
17
19
|
begin
|
18
20
|
@user.update_attributes(name: 'Fred')
|
19
21
|
rescue ActiveRecord::ReadOnlyRecord
|
20
22
|
end
|
21
23
|
end
|
22
24
|
|
23
|
-
When(
|
25
|
+
When('I save the user') do
|
24
26
|
begin
|
25
27
|
@user.save
|
26
28
|
rescue ActiveRecord::ReadOnlyRecord
|
27
29
|
end
|
28
30
|
end
|
29
31
|
|
30
|
-
Then(
|
32
|
+
Then('the user should not be updated') do
|
31
33
|
@user.reload
|
32
|
-
|
34
|
+
|
33
35
|
expect(@user.name).to eq('Bob')
|
34
36
|
end
|
35
37
|
|
36
|
-
Then(
|
38
|
+
Then('the user should be updated') do
|
37
39
|
@user.reload
|
38
40
|
|
39
41
|
expect(@user.name).to eq('Fred')
|
40
42
|
end
|
41
43
|
|
42
|
-
Then(
|
44
|
+
Then('the user should be saved') do
|
45
|
+
@user.reload
|
46
|
+
|
43
47
|
expect(@user.persisted?).to eq(true)
|
44
48
|
end
|
45
49
|
|
46
|
-
Then(
|
50
|
+
Then('the user should not be saved') do
|
47
51
|
expect(User.all.length).to eq(0)
|
48
52
|
end
|
49
53
|
|
@@ -54,15 +58,15 @@ When("I'm using a web browser and attempt to update the user") do
|
|
54
58
|
end
|
55
59
|
|
56
60
|
When("I'm using a web browser and attempt to create a user") do
|
57
|
-
visit
|
61
|
+
visit '/users/new'
|
58
62
|
fill_in 'user[name]', with: 'Fred'
|
59
63
|
click_on 'Create User'
|
60
64
|
end
|
61
65
|
|
62
|
-
Then(
|
66
|
+
Then('the user should be created') do
|
63
67
|
expect(User.all.length).to eq(1)
|
64
68
|
end
|
65
69
|
|
66
|
-
Then(
|
70
|
+
Then('I should be able to create a user without any errors') do
|
67
71
|
expect { User.create(name: 'Bob') }.not_to raise_error
|
68
|
-
end
|
72
|
+
end
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
# :nodoc:
|
6
|
+
module FileHelpers
|
7
|
+
GEM_ROOT_PATH = File.expand_path('../../..', __FILE__).freeze
|
8
|
+
RAILS_APP_PATH = File.expand_path(File.join(GEM_ROOT_PATH, 'tmp', 'rails_app')).freeze
|
9
|
+
|
10
|
+
def append_to_file(path_to_file, content)
|
11
|
+
raise 'The specified path is not a file!' unless File.file? path_to_file
|
12
|
+
|
13
|
+
File.open(path_to_file, 'a') do |f|
|
14
|
+
f.write content
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def append_to_beginning_of_file(path_to_file, content)
|
19
|
+
raise 'The specified path is not a file!' unless File.file? path_to_file
|
20
|
+
|
21
|
+
existing_content_as_array = File.open(path_to_file, 'r').readlines
|
22
|
+
|
23
|
+
new_content_arr = [content] + existing_content_as_array
|
24
|
+
|
25
|
+
write_file_with_content path_to_file, new_content_arr.join("\n")
|
26
|
+
end
|
27
|
+
|
28
|
+
def write_file_with_content(path_to_file, content)
|
29
|
+
File.open(path_to_file, 'w') do |f|
|
30
|
+
f.write content
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def switch_to_gems_root_path
|
35
|
+
FileUtils.cd GEM_ROOT_PATH
|
36
|
+
end
|
37
|
+
|
38
|
+
def switch_to_rails_app_path
|
39
|
+
FileUtils.cd RAILS_APP_PATH
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate_rails_application
|
43
|
+
# Remove the app, if it exists already
|
44
|
+
system("rm -rf #{RAILS_APP_PATH}")
|
45
|
+
|
46
|
+
FileUtils.mkdir_p RAILS_APP_PATH
|
47
|
+
system("bundle exec rails new #{RAILS_APP_PATH}")
|
48
|
+
end
|
49
|
+
|
50
|
+
def gems_root_path
|
51
|
+
GEM_ROOT_PATH
|
52
|
+
end
|
53
|
+
|
54
|
+
def rails_app_path
|
55
|
+
RAILS_APP_PATH
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GemHelpers
|
4
|
+
def add_gem(name, attributes = {})
|
5
|
+
line = if attributes.empty?
|
6
|
+
"gem '#{name}'"
|
7
|
+
else
|
8
|
+
"gem '#{name}', #{attributes}"
|
9
|
+
end
|
10
|
+
|
11
|
+
append_to_file('Gemfile', "#{line}\n")
|
12
|
+
end
|
13
|
+
|
14
|
+
# 'Override'/force a specific gem version, as defined in the Appraisal.
|
15
|
+
def ensure_gem_versions_defined_in_appraisal_are_used
|
16
|
+
gems_defined_in_appraisal = retrieve_gems_from_gemfile(ENV['BUNDLE_GEMFILE'])
|
17
|
+
gems_defined_in_gemfile = retrieve_gems_from_gemfile('Gemfile').collect(&:gem_name)
|
18
|
+
|
19
|
+
gems_defined_in_appraisal.reject { |l| gems_defined_in_gemfile.include? l.gem_name }.each do |gem|
|
20
|
+
add_gem gem.gem_name, gem.attributes.join(',')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def install_simplecov(coverage_dir)
|
25
|
+
append_to_beginning_of_file 'spec/spec_helper.rb', %(
|
26
|
+
require 'simplecov'
|
27
|
+
require 'rails_readonly_injector'
|
28
|
+
)
|
29
|
+
append_to_beginning_of_file 'features/support/env.rb', "require 'simplecov'"
|
30
|
+
|
31
|
+
write_file_with_content '.simplecov', %(
|
32
|
+
SimpleCov.start do
|
33
|
+
coverage_dir '#{coverage_dir}'
|
34
|
+
end
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def unset_appraisal_environment_variables
|
39
|
+
ENV.delete('BUNDLE_GEMFILE')
|
40
|
+
ENV.delete('BUNDLE_BIN_PATH')
|
41
|
+
ENV.delete('RUBYOPT')
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def retrieve_gems_from_gemfile(file_path)
|
47
|
+
gems = []
|
48
|
+
|
49
|
+
File.open(file_path).readlines.each do |line|
|
50
|
+
matches = line.match(/^\s*gem\s+['|"]/)
|
51
|
+
|
52
|
+
next if matches.nil?
|
53
|
+
|
54
|
+
parts = line.split(',')
|
55
|
+
|
56
|
+
gem_name = parts.first.gsub(/\s*gem\s*|["|']|\n/, '')
|
57
|
+
|
58
|
+
gems << OpenStruct.new(gem_name: gem_name, attributes: parts.drop(1))
|
59
|
+
end
|
60
|
+
|
61
|
+
gems
|
62
|
+
end
|
63
|
+
end
|