informant-rails 0.9.2 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/LICENSE +8 -1
- data/README.markdown +7 -4
- data/Rakefile +5 -5
- data/lib/informant-rails.rb +3 -6
- data/lib/informant-rails/config.rb +11 -18
- data/lib/informant-rails/{connection_tester.rb → diagnostic.rb} +30 -16
- data/lib/informant-rails/middleware.rb +8 -4
- data/lib/informant-rails/railtie.rb +21 -13
- data/lib/informant-rails/request_tracking.rb +8 -2
- data/lib/informant-rails/validation_tracking.rb +1 -3
- data/lib/informant-rails/version.rb +1 -1
- data/lib/tasks/diagnostic.rake +7 -0
- metadata +42 -14
- data/lib/informant-rails/client.rb +0 -64
- data/lib/informant-rails/field_error.rb +0 -19
- data/lib/informant-rails/model.rb +0 -17
- data/lib/informant-rails/parameter_filter.rb +0 -13
- data/lib/informant-rails/request.rb +0 -31
- data/lib/tasks/test_connection.rake +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 69200fabf6b1767d2cb6a26d3a9c46e7c3c8fdb1d3d08a3ad9b63a919a0bc8c8
|
4
|
+
data.tar.gz: dbd81e0ed5e7c86ea0afa48a7bd0587d55b2f647ec896a09534e6571a25eee70
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c9220c0cfc3ddb9d4a5845851b31b50d2d6d6b53963ae9cb2b657c15fd179cb67a506c0e9a4b8ac77fb209e5c7945ecac06e930df40af7aeb714a0a018226bf9
|
7
|
+
data.tar.gz: 2746d2d97cae5c6fc47b67fe223e9bf097d08f72de1c1ac06cfa3bd14bcb0773be649b3d63b2eb061619d8a19ade22c1348c32be7c3f0626cbebb0320eab75b9
|
data/LICENSE
CHANGED
@@ -1 +1,8 @@
|
|
1
|
-
|
1
|
+
MIT License
|
2
|
+
Copyright (c) 2016 Informant, LLC
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
5
|
+
|
6
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
7
|
+
|
8
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
CHANGED
@@ -8,12 +8,11 @@ The informant-rails gem provides Rails and ActiveRecord hooks for The Informant.
|
|
8
8
|
|
9
9
|
## Compatibility
|
10
10
|
|
11
|
-
The informant-rails gem officially supports Ruby 2.
|
11
|
+
The informant-rails gem officially supports Ruby 2.5+.
|
12
12
|
|
13
|
-
It will work automatically with Rails
|
13
|
+
It will work automatically with Rails 5.2+ as well as Mongoid 6.2 and up.
|
14
14
|
|
15
|
-
[![
|
16
|
-
[![Code Climate](https://codeclimate.com/github/informantapp/informant-rails.png)](https://codeclimate.com/github/informantapp/informant-rails)
|
15
|
+
[![pipeline status](https://gitlab.com/informantapp/informant-rails/badges/master/pipeline.svg)](https://gitlab.com/informantapp/informant-rails/-/commits/master)
|
17
16
|
|
18
17
|
## Installation
|
19
18
|
|
@@ -28,6 +27,10 @@ It will work automatically with Rails 3 and up as well as Mongoid 3 and up.
|
|
28
27
|
4. Deploy with the gem installed
|
29
28
|
5. Submit a form and you'll see it appear in our web interface
|
30
29
|
|
30
|
+
We've set up
|
31
|
+
[informant-rails-example](https://gitlab.com/informantapp/informant-rails-example)
|
32
|
+
as a demonstrate of installation and operation.
|
33
|
+
|
31
34
|
## Usage
|
32
35
|
|
33
36
|
By default, any request that causes an ActiveRecord or Mongoid model to be validated will be tracked by the Informant once the gem is added to your project.
|
data/Rakefile
CHANGED
@@ -1,12 +1,12 @@
|
|
1
|
-
require
|
2
|
-
require
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'bundler/gem_tasks'
|
3
3
|
|
4
|
-
if !ENV[
|
5
|
-
require
|
4
|
+
if !ENV['APPRAISAL_INITIALIZED']
|
5
|
+
require 'appraisal/task'
|
6
6
|
Appraisal::Task.new
|
7
7
|
task default: :appraisal
|
8
8
|
else
|
9
|
-
require
|
9
|
+
require 'rspec/core/rake_task'
|
10
10
|
RSpec::Core::RakeTask.new
|
11
11
|
task default: :spec
|
12
12
|
end
|
data/lib/informant-rails.rb
CHANGED
@@ -3,15 +3,12 @@ if defined?(Rake)
|
|
3
3
|
Dir[File.join(File.dirname(__FILE__), 'tasks', '**/*.rake')].each { |rake| load rake }
|
4
4
|
end
|
5
5
|
|
6
|
+
require 'informant-common'
|
7
|
+
|
6
8
|
module InformantRails
|
7
9
|
autoload :Config, 'informant-rails/config'
|
8
|
-
autoload :
|
9
|
-
autoload :Client, 'informant-rails/client'
|
10
|
-
autoload :FieldError, 'informant-rails/field_error'
|
10
|
+
autoload :Diagnostic, 'informant-rails/diagnostic'
|
11
11
|
autoload :Middleware, 'informant-rails/middleware'
|
12
|
-
autoload :Model, 'informant-rails/model'
|
13
|
-
autoload :Request, 'informant-rails/request'
|
14
|
-
autoload :ParameterFilter, 'informant-rails/parameter_filter'
|
15
12
|
autoload :RequestTracking, 'informant-rails/request_tracking'
|
16
13
|
autoload :ValidationTracking, 'informant-rails/validation_tracking'
|
17
14
|
autoload :VERSION, 'informant-rails/version'
|
@@ -1,20 +1,13 @@
|
|
1
|
-
module InformantRails
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
def self.client_identifier
|
14
|
-
@client_identifier ||= "informant-rails-#{InformantRails::VERSION}"
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.enabled?
|
18
|
-
api_token.present?
|
1
|
+
module InformantRails
|
2
|
+
module Config
|
3
|
+
extend self
|
4
|
+
|
5
|
+
delegate :api_token, :api_token=,
|
6
|
+
:collector_host,
|
7
|
+
:enabled?,
|
8
|
+
:exclude_models, :exclude_models=,
|
9
|
+
:filter_parameters, :filter_parameters=,
|
10
|
+
:value_tracking?, :value_tracking=,
|
11
|
+
to: InformantCommon::Config
|
19
12
|
end
|
20
13
|
end
|
@@ -1,24 +1,23 @@
|
|
1
|
+
class TestClass
|
2
|
+
include ActiveModel::Model
|
3
|
+
|
4
|
+
attr_accessor :field_name
|
5
|
+
|
6
|
+
validates_presence_of :field_name
|
7
|
+
end
|
8
|
+
|
1
9
|
module InformantRails
|
2
|
-
class
|
3
|
-
def self.run
|
10
|
+
class Diagnostic
|
11
|
+
def self.run
|
12
|
+
new.run
|
13
|
+
end
|
4
14
|
|
5
15
|
def run
|
6
16
|
if Config.api_token.blank?
|
7
17
|
Rails.logger.info missing_api_token_message
|
8
18
|
else
|
9
|
-
Client.
|
10
|
-
|
11
|
-
Client.request.instance_variable_set('@models', [{
|
12
|
-
name: 'TestClass',
|
13
|
-
errors: [name: 'field_name', value: 'field_value', message: 'must be unique']
|
14
|
-
}])
|
15
|
-
response = Client.transmit(Client.request)
|
16
|
-
|
17
|
-
if response.success?
|
18
|
-
Rails.logger.info success_message
|
19
|
-
else
|
20
|
-
Rails.logger.info bad_response_message(response.body)
|
21
|
-
end
|
19
|
+
response = InformantCommon::Client.transmit(test_form_submission).join.value
|
20
|
+
display_response_message(response)
|
22
21
|
end
|
23
22
|
|
24
23
|
Rails.logger.info assistance_message
|
@@ -44,11 +43,26 @@ module InformantRails
|
|
44
43
|
end
|
45
44
|
|
46
45
|
def success_message
|
47
|
-
|
46
|
+
'Everything looks good. You should see a test request on your dashboard.'
|
48
47
|
end
|
49
48
|
|
50
49
|
def assistance_message
|
51
50
|
"If you need assistance or have any questions, send an email to support@informantapp.com or tweet @informantapp and we'll help you out!"
|
52
51
|
end
|
52
|
+
|
53
|
+
def test_form_submission
|
54
|
+
InformantCommon::Event::FormSubmission.new.tap do |form_submission|
|
55
|
+
form_submission.handler = 'Connectivity#test'
|
56
|
+
form_submission.process_model(InformantCommon::Model::ActiveModel.new(TestClass.new.tap(&:valid?)))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def display_response_message(response)
|
61
|
+
if response.code == '204'
|
62
|
+
Rails.logger.info success_message
|
63
|
+
else
|
64
|
+
Rails.logger.info bad_response_message(response.body)
|
65
|
+
end
|
66
|
+
end
|
53
67
|
end
|
54
68
|
end
|
@@ -1,9 +1,13 @@
|
|
1
1
|
module InformantRails
|
2
|
-
class Middleware
|
2
|
+
class Middleware
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
3
7
|
def call(env)
|
4
|
-
Client.
|
5
|
-
response = app.call(env)
|
6
|
-
Client.process
|
8
|
+
InformantCommon::Client.start_transaction!(env['REQUEST_METHOD'])
|
9
|
+
response = @app.call(env)
|
10
|
+
InformantCommon::Client.process
|
7
11
|
response
|
8
12
|
end
|
9
13
|
end
|
@@ -1,24 +1,32 @@
|
|
1
1
|
module InformantRails
|
2
2
|
class Railtie < ::Rails::Railtie
|
3
3
|
initializer 'informant' do |config|
|
4
|
-
|
4
|
+
begin
|
5
|
+
if Config.enabled?
|
6
|
+
InformantCommon::Client.transmit(
|
7
|
+
InformantCommon::Event::AgentInfo.new(
|
8
|
+
agent_identifier: "informant-rails-#{InformantRails::VERSION}",
|
9
|
+
framework_version: "rails-#{Rails.version}"
|
10
|
+
)
|
11
|
+
)
|
12
|
+
InformantRails::Config.filter_parameters = Rails.configuration.filter_parameters
|
5
13
|
|
6
|
-
|
7
|
-
config.middleware.use InformantRails::Middleware
|
14
|
+
config.middleware.use InformantRails::Middleware
|
8
15
|
|
9
|
-
|
16
|
+
ActiveSupport.on_load(:action_controller) do
|
17
|
+
include InformantRails::RequestTracking
|
18
|
+
end
|
10
19
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
ActiveSupport.on_load(:active_record) do
|
16
|
-
include InformantRails::ValidationTracking
|
17
|
-
end
|
20
|
+
ActiveSupport.on_load(:active_record) do
|
21
|
+
include InformantRails::ValidationTracking
|
22
|
+
end
|
18
23
|
|
19
|
-
|
20
|
-
|
24
|
+
ActiveSupport.on_load(:mongoid) do
|
25
|
+
include InformantRails::ValidationTracking
|
26
|
+
end
|
21
27
|
end
|
28
|
+
rescue StandardError => e
|
29
|
+
puts "Unable to bootstrap informant: #{e.message}"
|
22
30
|
end
|
23
31
|
end
|
24
32
|
end
|
@@ -3,8 +3,14 @@ module InformantRails
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
before_action
|
7
|
-
|
6
|
+
if defined? before_action
|
7
|
+
before_action do
|
8
|
+
InformantCommon::Client.current_transaction&.handler = [controller_name, action_name].join('#')
|
9
|
+
end
|
10
|
+
else
|
11
|
+
before_filter do
|
12
|
+
InformantCommon::Client.current_transaction&.handler = [controller_name, action_name].join('#')
|
13
|
+
end
|
8
14
|
end
|
9
15
|
end
|
10
16
|
end
|
metadata
CHANGED
@@ -1,29 +1,63 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: informant-rails
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Informant, LLC
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-05-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: informant-common
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.1.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.1.1
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: rails
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
31
|
- - ">="
|
18
32
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
33
|
+
version: 5.2.0
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: 7.0.0
|
20
37
|
type: :runtime
|
21
38
|
prerelease: false
|
22
39
|
version_requirements: !ruby/object:Gem::Requirement
|
23
40
|
requirements:
|
24
41
|
- - ">="
|
25
42
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
43
|
+
version: 5.2.0
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: 7.0.0
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: appraisal
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
27
61
|
description: The Informant tracks what users do wrong in your forms so you can make
|
28
62
|
them better.
|
29
63
|
email:
|
@@ -36,22 +70,17 @@ files:
|
|
36
70
|
- README.markdown
|
37
71
|
- Rakefile
|
38
72
|
- lib/informant-rails.rb
|
39
|
-
- lib/informant-rails/client.rb
|
40
73
|
- lib/informant-rails/config.rb
|
41
|
-
- lib/informant-rails/
|
42
|
-
- lib/informant-rails/field_error.rb
|
74
|
+
- lib/informant-rails/diagnostic.rb
|
43
75
|
- lib/informant-rails/middleware.rb
|
44
|
-
- lib/informant-rails/model.rb
|
45
|
-
- lib/informant-rails/parameter_filter.rb
|
46
76
|
- lib/informant-rails/railtie.rb
|
47
|
-
- lib/informant-rails/request.rb
|
48
77
|
- lib/informant-rails/request_tracking.rb
|
49
78
|
- lib/informant-rails/validation_tracking.rb
|
50
79
|
- lib/informant-rails/version.rb
|
51
|
-
- lib/tasks/
|
80
|
+
- lib/tasks/diagnostic.rake
|
52
81
|
homepage: https://www.informantapp.com
|
53
82
|
licenses:
|
54
|
-
-
|
83
|
+
- MIT
|
55
84
|
metadata: {}
|
56
85
|
post_install_message:
|
57
86
|
rdoc_options: []
|
@@ -68,8 +97,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
97
|
- !ruby/object:Gem::Version
|
69
98
|
version: '0'
|
70
99
|
requirements: []
|
71
|
-
|
72
|
-
rubygems_version: 2.5.1
|
100
|
+
rubygems_version: 3.0.3
|
73
101
|
signing_key:
|
74
102
|
specification_version: 4
|
75
103
|
summary: The Informant tracks server-side validation errors and gives you metrics
|
@@ -1,64 +0,0 @@
|
|
1
|
-
module InformantRails
|
2
|
-
class Client
|
3
|
-
|
4
|
-
def self.record(env)
|
5
|
-
new_request.request_url = env['HTTP_REFERER'] unless env['REQUEST_METHOD'] == 'GET'
|
6
|
-
end
|
7
|
-
|
8
|
-
def self.record_action(controller_name, action)
|
9
|
-
if request
|
10
|
-
request.filename = controller_name
|
11
|
-
request.action = action
|
12
|
-
end
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.record_validated_model(model)
|
16
|
-
request.process_model(model) if request && include_model?(model)
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.process
|
20
|
-
if request && request.models.any?
|
21
|
-
this_request = request
|
22
|
-
Thread.new { transmit(this_request) }
|
23
|
-
end
|
24
|
-
ensure
|
25
|
-
cleanup
|
26
|
-
end
|
27
|
-
|
28
|
-
def self.request
|
29
|
-
Thread.current[:informant_request]
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.cleanup
|
33
|
-
Thread.current[:informant_request] = nil
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.transmit(completed_request)
|
37
|
-
Net::HTTP.start(api_endpoint.host, api_endpoint.port, use_ssl: api_endpoint.scheme == 'https') do |http|
|
38
|
-
http.request(net_http_post_request(completed_request))
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
private
|
43
|
-
|
44
|
-
def self.net_http_post_request(completed_request)
|
45
|
-
Net::HTTP::Post.new(api_endpoint, {
|
46
|
-
"Authorization" => ActionController::HttpAuthentication::Token.encode_credentials(Config.api_token),
|
47
|
-
"Content-Type" => "application/json"
|
48
|
-
}).tap { |r| r.body = { payload: completed_request }.to_json }
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.include_model?(model)
|
52
|
-
!Config.exclude_models.include?(model.class.to_s)
|
53
|
-
end
|
54
|
-
|
55
|
-
def self.new_request
|
56
|
-
Thread.current[:informant_request] = Request.new
|
57
|
-
end
|
58
|
-
|
59
|
-
def self.api_endpoint
|
60
|
-
@api_endpoint ||= URI('https://api.informantapp.com/api/v1/form_submissions')
|
61
|
-
end
|
62
|
-
|
63
|
-
end
|
64
|
-
end
|
@@ -1,19 +0,0 @@
|
|
1
|
-
module InformantRails
|
2
|
-
class FieldError
|
3
|
-
attr_accessor :name, :value, :message
|
4
|
-
|
5
|
-
def initialize(name, value, message=nil)
|
6
|
-
self.name = name
|
7
|
-
self.value = value
|
8
|
-
self.message = message
|
9
|
-
end
|
10
|
-
|
11
|
-
def value=(value)
|
12
|
-
@value = InformantRails::ParameterFilter.filter(name, value)
|
13
|
-
end
|
14
|
-
|
15
|
-
def as_json(*args)
|
16
|
-
{ name: name, value: value, message: message }
|
17
|
-
end
|
18
|
-
end
|
19
|
-
end
|
@@ -1,17 +0,0 @@
|
|
1
|
-
module InformantRails
|
2
|
-
class Model < Struct.new(:model)
|
3
|
-
def name
|
4
|
-
model.class.name
|
5
|
-
end
|
6
|
-
|
7
|
-
def errors
|
8
|
-
model.errors.map do |field, error|
|
9
|
-
InformantRails::FieldError.new(field.to_s, model[field], error)
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
def as_json(*args)
|
14
|
-
{ name: name, errors: errors.map(&:as_json) }
|
15
|
-
end
|
16
|
-
end
|
17
|
-
end
|
@@ -1,13 +0,0 @@
|
|
1
|
-
module InformantRails
|
2
|
-
class ParameterFilter
|
3
|
-
def self.filter(name, value)
|
4
|
-
Config.value_tracking && !matcher.match(name) ? value : '[FILTERED]'
|
5
|
-
end
|
6
|
-
|
7
|
-
protected
|
8
|
-
|
9
|
-
def self.matcher
|
10
|
-
@matcher ||= Regexp.new(/#{Config.filter_parameters.join('|').presence || '$^'}/)
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
@@ -1,31 +0,0 @@
|
|
1
|
-
module InformantRails
|
2
|
-
class Request
|
3
|
-
attr_accessor :request_url, :filename, :action
|
4
|
-
|
5
|
-
def process_model(model)
|
6
|
-
if model && untracked?(model)
|
7
|
-
models << InformantRails::Model.new(model)
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
def models
|
12
|
-
@models ||= []
|
13
|
-
end
|
14
|
-
|
15
|
-
def as_json(*args)
|
16
|
-
{
|
17
|
-
models: models.map(&:as_json),
|
18
|
-
request_url: request_url,
|
19
|
-
filename: filename,
|
20
|
-
action: action,
|
21
|
-
client: InformantRails::Config.client_identifier
|
22
|
-
}
|
23
|
-
end
|
24
|
-
|
25
|
-
protected
|
26
|
-
|
27
|
-
def untracked?(model)
|
28
|
-
!models.detect { |container| container.model == model }
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|