active_record_migration_ui 0.1.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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +86 -0
- data/Rakefile +48 -0
- data/app/channels/active_record_migration_ui/active_record_migration_ui_channel.rb +11 -0
- data/app/channels/active_record_migration_ui/application_cable/channel.rb +6 -0
- data/app/channels/active_record_migration_ui/application_cable/connection.rb +6 -0
- data/app/controllers/active_record_migration_ui/application_controller.rb +5 -0
- data/app/controllers/active_record_migration_ui/migrations_controller.rb +24 -0
- data/app/interactors/active_record_migration_ui/ensure_migration_version_is_included_in_pending_scripts.rb +49 -0
- data/app/interactors/active_record_migration_ui/find_all_pending_migration_scripts.rb +29 -0
- data/app/interactors/active_record_migration_ui/migrate_migration_script.rb +40 -0
- data/app/interactors/active_record_migration_ui/notify_migration_script_as_done_or_fail.rb +34 -0
- data/app/interactors/active_record_migration_ui/notify_migration_script_as_running.rb +31 -0
- data/app/interactors/active_record_migration_ui/organise_migrating_script.rb +11 -0
- data/app/views/active_record_migration_ui/migrations/index.html.erb +0 -0
- data/app/views/layouts/active_record_migration_ui/application.html.erb +36 -0
- data/config/routes.rb +7 -0
- data/lib/active_record_migration_ui.rb +65 -0
- data/lib/active_record_migration_ui/engine.rb +58 -0
- data/lib/active_record_migration_ui/logger.rb +53 -0
- data/lib/active_record_migration_ui/middleware.rb +55 -0
- data/lib/active_record_migration_ui/version.rb +3 -0
- data/public/ar-migration-ui-packs/js/application-5918ab1fd5fb12221e22.js +2 -0
- data/public/ar-migration-ui-packs/js/application-5918ab1fd5fb12221e22.js.gz +0 -0
- data/public/ar-migration-ui-packs/js/application-5918ab1fd5fb12221e22.js.map +1 -0
- data/public/ar-migration-ui-packs/js/application-5918ab1fd5fb12221e22.js.map.gz +0 -0
- data/public/ar-migration-ui-packs/manifest.json +14 -0
- data/public/ar-migration-ui-packs/manifest.json.gz +0 -0
- metadata +160 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 498bb67c6e2397910c46799ef954c689e9f6eb77a1c9e41416c2c043dddc51ea
|
|
4
|
+
data.tar.gz: 7c4e53766990419586895d6f534f1439ea6f66dae57135bb4b3c892f2547ec1f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 5cc8fbfb8636e13fd8817e29171ff6d5d5e8eb27dca5e34d2561ef15adf24191fedc1cc4b55d2e7b2a5bc82a66993d6bef64e32602700e95135f184674529ac8
|
|
7
|
+
data.tar.gz: d474a2ab12b5d4cf23daf691443932fd6fe5ff55a79026fb2ba27c11925e884698290d57dbaefc174e9b2b84c93e353f44c7b28dec74ac5b914184e90e63bb6d
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright 2019 TODO: Write your name
|
|
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,86 @@
|
|
|
1
|
+
# ActiveRecordMigrationUi
|
|
2
|
+
|
|
3
|
+
This gem replaces the Rails default pending migration page with a small React
|
|
4
|
+
app which allows you to run your migration scripts from your web browser.
|
|
5
|
+
|
|
6
|
+
So before this gem you had this :
|
|
7
|
+
|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
After having installed this gem you will have this :
|
|
11
|
+
|
|
12
|
+

|
|
13
|
+
|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
The ActiveRecord Migration UI page will show up as soon as you installed the gem,
|
|
19
|
+
restarted your server and have at least one pending migration script.
|
|
20
|
+
|
|
21
|
+
Click the "Migrate now!" button to run the pending migration scripts.
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Add this gem where you want to it to be available.
|
|
26
|
+
|
|
27
|
+
It is recommended to use it only on your environment machine, but you could also
|
|
28
|
+
deploy it to your staging environment for instance.
|
|
29
|
+
|
|
30
|
+
Add this line to your application's Gemfile:
|
|
31
|
+
|
|
32
|
+
```ruby
|
|
33
|
+
group :development do
|
|
34
|
+
gem 'active_record_migration_ui'
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
And then execute:
|
|
39
|
+
```bash
|
|
40
|
+
$ bundle
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or install it yourself as:
|
|
44
|
+
```bash
|
|
45
|
+
$ gem install active_record_migration_ui
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
1. Install [docker-sync](http://docker-sync.io): `gem install docker-sync`
|
|
51
|
+
2. Build the Docker image: `docker-compose build webpack`
|
|
52
|
+
3. Boot webpack: `docker-sync-stack start`
|
|
53
|
+
This step will fail with the error:
|
|
54
|
+
```
|
|
55
|
+
error Command "webpack-dev-server" not found.
|
|
56
|
+
```
|
|
57
|
+
_TODO : find a way to not have this error_
|
|
58
|
+
4. Install NPM packages
|
|
59
|
+
```
|
|
60
|
+
$ docker-compose run --rm -v armui-sync:/gem:nocopy webpack yarn
|
|
61
|
+
```
|
|
62
|
+
5. Restart again docker-sync: `docker-sync-stack start`
|
|
63
|
+
|
|
64
|
+
Now you should see webpack running on port 3036.
|
|
65
|
+
|
|
66
|
+
## Architecture
|
|
67
|
+
|
|
68
|
+
We made an `ARCHITECURE.md` file in order to guide you through this gem's code.
|
|
69
|
+
|
|
70
|
+
## Contributing
|
|
71
|
+
|
|
72
|
+
1. Fork this gem
|
|
73
|
+
2. Implement your new features
|
|
74
|
+
3. [Clean your branch history](https://thoughtbot.com/blog/git-interactive-rebase-squash-amend-rewriting-history)
|
|
75
|
+
4. Open a Merge Request
|
|
76
|
+
|
|
77
|
+
## License
|
|
78
|
+
|
|
79
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
80
|
+
|
|
81
|
+
## Releasing a new version
|
|
82
|
+
|
|
83
|
+
1. Run the `docker-sync-stack start` command (See [Development](#development)).
|
|
84
|
+
2. Run `docker-compose run --rm -v armui-sync:/gem:nocopy webpack bash -c 'RAILS_ENV=production RAILS_MASTER_KEY=4d046dc285e33d0750e78d7effe25f3b rails build'`
|
|
85
|
+
|
|
86
|
+
This will produce the gem in the `pkg/` folder.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require 'bundler/setup'
|
|
3
|
+
rescue LoadError
|
|
4
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
require 'rdoc/task'
|
|
8
|
+
|
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
11
|
+
rdoc.title = 'ActiveRecordMigrationUi'
|
|
12
|
+
rdoc.options << '--line-numbers'
|
|
13
|
+
rdoc.rdoc_files.include('README.md')
|
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
APP_RAKEFILE = File.expand_path('test/dummy/Rakefile', __dir__)
|
|
18
|
+
load 'rails/tasks/engine.rake'
|
|
19
|
+
|
|
20
|
+
load 'rails/tasks/statistics.rake'
|
|
21
|
+
|
|
22
|
+
require 'bundler/gem_tasks'
|
|
23
|
+
#
|
|
24
|
+
# require 'rake/testtask'
|
|
25
|
+
#
|
|
26
|
+
# Rake::TestTask.new(:test) do |t|
|
|
27
|
+
# t.libs << 'test'
|
|
28
|
+
# t.pattern = 'test/**/*_test.rb'
|
|
29
|
+
# t.verbose = false
|
|
30
|
+
# end
|
|
31
|
+
#
|
|
32
|
+
# task default: :test
|
|
33
|
+
|
|
34
|
+
#
|
|
35
|
+
# Adds Webpacker tasks before rails build
|
|
36
|
+
#
|
|
37
|
+
def yarn_install_available?
|
|
38
|
+
puts "Rails::VERSION::MAJOR: #{Rails::VERSION::MAJOR.inspect}"
|
|
39
|
+
puts "Rails::VERSION::MINOR: #{Rails::VERSION::MINOR.inspect}"
|
|
40
|
+
rails_major = Rails::VERSION::MAJOR
|
|
41
|
+
rails_minor = Rails::VERSION::MINOR
|
|
42
|
+
|
|
43
|
+
rails_major > 5 || (rails_major == 5 && rails_minor >= 1)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Rake::Task['build'].enhance([
|
|
47
|
+
'app:active_record_migration_ui:webpacker:compile'
|
|
48
|
+
])
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module ActiveRecordMigrationUi
|
|
2
|
+
class ActiveRecordMigrationUiChannel < ApplicationCable::Channel
|
|
3
|
+
def subscribed
|
|
4
|
+
stream_from ActiveRecordMigrationUi.ac_channel_name
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def migrate(data)
|
|
8
|
+
OrganiseMigratingScript.call(version: data['version'])
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_dependency 'active_record_migration_ui/application_controller'
|
|
2
|
+
|
|
3
|
+
module ActiveRecordMigrationUi
|
|
4
|
+
class MigrationsController < ApplicationController
|
|
5
|
+
# Rack Middleware uses this method in order to run the index action.
|
|
6
|
+
def self.call(env)
|
|
7
|
+
action(:index).call(env)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def index
|
|
11
|
+
respond_to do |format|
|
|
12
|
+
format.html
|
|
13
|
+
format.json do
|
|
14
|
+
interactor = FindAllPendingMigrationScripts.call
|
|
15
|
+
if interactor.failure?
|
|
16
|
+
render json: interactor.errors, status: :unprocessable_entity
|
|
17
|
+
else
|
|
18
|
+
render json: interactor.scripts, status: :ok
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module ActiveRecordMigrationUi
|
|
2
|
+
class EnsureMigrationVersionIsIncludedInPendingScripts
|
|
3
|
+
include Interactor
|
|
4
|
+
|
|
5
|
+
def call
|
|
6
|
+
sanity_checks!
|
|
7
|
+
|
|
8
|
+
return if version_is_included_in_pending_scripts?
|
|
9
|
+
|
|
10
|
+
context.fail!(errors: {
|
|
11
|
+
version: 'was not found in pending migration script list'
|
|
12
|
+
})
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def sanity_checks!
|
|
18
|
+
unless context.version
|
|
19
|
+
context.fail!(errors: { version: 'is missing' })
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless context.version =~ /\d+/
|
|
23
|
+
context.fail!(errors: {
|
|
24
|
+
version: 'is invalid (Expected numbers like 20180323142544).'
|
|
25
|
+
})
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
unless context.scripts
|
|
29
|
+
context.fail!(errors: { scripts: 'is missing' })
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
unless context.scripts.is_a?(Array)
|
|
33
|
+
context.fail!(errors: {
|
|
34
|
+
scripts: "must be an Array but it #{context.scripts.class.name}"
|
|
35
|
+
})
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
return unless context.scripts.size.zero?
|
|
39
|
+
|
|
40
|
+
context.fail!(errors: {
|
|
41
|
+
scripts: 'must contain migration scripts, but is empty'
|
|
42
|
+
})
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def version_is_included_in_pending_scripts?
|
|
46
|
+
context.scripts.detect { |script| script[:version] == context.version }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module ActiveRecordMigrationUi
|
|
2
|
+
class FindAllPendingMigrationScripts
|
|
3
|
+
include Interactor
|
|
4
|
+
|
|
5
|
+
def call
|
|
6
|
+
context.scripts = build_pending_scripts_list
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def migrations_status
|
|
12
|
+
# This will ensure the `schema_migrations` table exists
|
|
13
|
+
ActiveRecord::SchemaMigration.create_table
|
|
14
|
+
|
|
15
|
+
ActiveRecord::Base.connection.migration_context.migrations_status
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def build_pending_scripts_list
|
|
19
|
+
migrations_status.map do |status, version, name|
|
|
20
|
+
next unless status == 'down'
|
|
21
|
+
|
|
22
|
+
{
|
|
23
|
+
name: name,
|
|
24
|
+
version: version
|
|
25
|
+
}
|
|
26
|
+
end.compact
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module ActiveRecordMigrationUi
|
|
2
|
+
class MigrateMigrationScript
|
|
3
|
+
include Interactor
|
|
4
|
+
|
|
5
|
+
def call
|
|
6
|
+
sanity_checks!
|
|
7
|
+
|
|
8
|
+
migration_paths = ActiveRecord::Migrator.migrations_paths
|
|
9
|
+
migration_context = ActiveRecord::MigrationContext.new(migration_paths)
|
|
10
|
+
|
|
11
|
+
# Lets the logger broadcasting the logs to the UI
|
|
12
|
+
ActiveRecordMigrationUi.running_migration = true
|
|
13
|
+
|
|
14
|
+
# In case something went wrong in the migration script, an exception is
|
|
15
|
+
# thrown
|
|
16
|
+
migration_context.migrate(context.version.to_i)
|
|
17
|
+
|
|
18
|
+
# Preents the logger to broadcast the logs to the UI
|
|
19
|
+
ActiveRecordMigrationUi.running_migration = false
|
|
20
|
+
|
|
21
|
+
context.final_state = 'up'
|
|
22
|
+
rescue StandardError
|
|
23
|
+
context.final_state = 'failed'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def sanity_checks!
|
|
29
|
+
unless context.version
|
|
30
|
+
context.fail!(errors: { version: 'is missing' })
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return if context.version =~ /\d+/
|
|
34
|
+
|
|
35
|
+
context.fail!(errors: {
|
|
36
|
+
version: 'is invalid (Expected numbers like 20180323142544).'
|
|
37
|
+
})
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module ActiveRecordMigrationUi
|
|
2
|
+
class NotifyMigrationScriptAsDoneOrFail
|
|
3
|
+
include Interactor
|
|
4
|
+
|
|
5
|
+
def call
|
|
6
|
+
sanity_checks!
|
|
7
|
+
|
|
8
|
+
# Emit a message through the WebSocket to notify the front that the given
|
|
9
|
+
# migration script version changes the state to the given final_state.
|
|
10
|
+
ActionCable.server.broadcast ActiveRecordMigrationUi.ac_channel_name,
|
|
11
|
+
command: 'migrate',
|
|
12
|
+
version: context.version,
|
|
13
|
+
new_status: context.final_state
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def sanity_checks!
|
|
19
|
+
unless context.version
|
|
20
|
+
context.fail!(errors: { version: 'is missing' })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
unless context.version =~ /\d+/
|
|
24
|
+
context.fail!(errors: {
|
|
25
|
+
version: 'is invalid (Expected numbers like 20180323142544).'
|
|
26
|
+
})
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return if context.final_state
|
|
30
|
+
|
|
31
|
+
context.fail!(errors: { final_state: 'is missing' })
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module ActiveRecordMigrationUi
|
|
2
|
+
class NotifyMigrationScriptAsRunning
|
|
3
|
+
include Interactor
|
|
4
|
+
|
|
5
|
+
def call
|
|
6
|
+
sanity_checks!
|
|
7
|
+
|
|
8
|
+
# Emit a message through the WebSocket to notify the front that the given
|
|
9
|
+
# migration script version changes the state to running, which will show
|
|
10
|
+
# a loader next to the migration script.
|
|
11
|
+
ActionCable.server.broadcast ActiveRecordMigrationUi.ac_channel_name,
|
|
12
|
+
command: 'migrate',
|
|
13
|
+
version: context.version,
|
|
14
|
+
new_status: 'running'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def sanity_checks!
|
|
20
|
+
unless context.version
|
|
21
|
+
context.fail!(errors: { version: 'is missing' })
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
return if context.version =~ /\d+/
|
|
25
|
+
|
|
26
|
+
context.fail!(errors: {
|
|
27
|
+
version: 'is invalid (Expected numbers like 20180323142544).'
|
|
28
|
+
})
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module ActiveRecordMigrationUi
|
|
2
|
+
class OrganiseMigratingScript
|
|
3
|
+
include Interactor::Organizer
|
|
4
|
+
|
|
5
|
+
organize FindAllPendingMigrationScripts,
|
|
6
|
+
EnsureMigrationVersionIsIncludedInPendingScripts,
|
|
7
|
+
NotifyMigrationScriptAsRunning,
|
|
8
|
+
MigrateMigrationScript,
|
|
9
|
+
NotifyMigrationScriptAsDoneOrFail
|
|
10
|
+
end
|
|
11
|
+
end
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html class="h-100">
|
|
3
|
+
<head>
|
|
4
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
5
|
+
<title>ActiveRecord Migration Ui</title>
|
|
6
|
+
<% if ActiveRecordMigrationUi.configuration.webpacker_host %>
|
|
7
|
+
<%= javascript_pack_tag 'application', host: ActiveRecordMigrationUi.configuration.webpacker_host %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<%= action_cable_meta_tag %>
|
|
11
|
+
<%= csrf_meta_tags %>
|
|
12
|
+
</head>
|
|
13
|
+
<body class="h-100">
|
|
14
|
+
<div id="app" class="h-100" />
|
|
15
|
+
|
|
16
|
+
<!--- Avoids using webpacker helpers so that you can use this gem without
|
|
17
|
+
webpack allowing to use it in previous Rails versions --->
|
|
18
|
+
<script type="text/javascript">
|
|
19
|
+
fetch('/ar-migration-ui-packs/manifest.json')
|
|
20
|
+
.then(response => response.json())
|
|
21
|
+
.then(json => [json['application.js'], json['application.css']])
|
|
22
|
+
.then(([jsPath, cssPath]) => {
|
|
23
|
+
// Equivalent to stylesheet_pack_tag 'application'
|
|
24
|
+
let link = document.createElement('link')
|
|
25
|
+
link.rel = 'stylesheet'
|
|
26
|
+
link.href = cssPath
|
|
27
|
+
document.head.appendChild(link)
|
|
28
|
+
|
|
29
|
+
// Equivalent to javascript_pack_tag 'application'
|
|
30
|
+
let script = document.createElement('script')
|
|
31
|
+
script.src = jsPath
|
|
32
|
+
document.head.appendChild(script)
|
|
33
|
+
})
|
|
34
|
+
</script>
|
|
35
|
+
</body>
|
|
36
|
+
</html>
|