rails_db_admin 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/GPL-3-LICENSE +674 -0
- data/README.md +5 -0
- data/Rakefile +30 -0
- data/app/assets/javascripts/rails_db_admin/application.js +9 -0
- data/app/assets/stylesheets/rails_db_admin/application.css +7 -0
- data/app/controllers/rails_db_admin/erp_app/desktop/base_controller.rb +182 -0
- data/app/controllers/rails_db_admin/erp_app/desktop/queries_controller.rb +173 -0
- data/app/helpers/rails_db_admin/application_helper.rb +4 -0
- data/app/views/layouts/application.html.erb +14 -0
- data/app/views/layouts/rails_db_admin/application.html.erb +14 -0
- data/config/initializers/rails_db_admin.rb +4 -0
- data/config/routes.rb +4 -0
- data/db/data_migrations/20110816005525_rails_db_admin_application.rb +31 -0
- data/lib/rails_db_admin.rb +15 -0
- data/lib/rails_db_admin/config.rb +27 -0
- data/lib/rails_db_admin/connection_handler.rb +17 -0
- data/lib/rails_db_admin/engine.rb +11 -0
- data/lib/rails_db_admin/extjs.rb +2 -0
- data/lib/rails_db_admin/extjs/json_column_builder.rb +139 -0
- data/lib/rails_db_admin/extjs/json_data_builder.rb +82 -0
- data/lib/rails_db_admin/query_support.rb +72 -0
- data/lib/rails_db_admin/table_support.rb +147 -0
- data/lib/rails_db_admin/version.rb +3 -0
- data/lib/tasks/rails_db_admin_tasks.rake +4 -0
- data/public/images/icons/rails_db_admin/rails_db_admin_16x16.png +0 -0
- data/public/images/icons/rails_db_admin/rails_db_admin_24x24.png +0 -0
- data/public/images/icons/rails_db_admin/rails_db_admin_32x32.png +0 -0
- data/public/images/icons/rails_db_admin/rails_db_admin_48x48.png +0 -0
- data/public/javascripts/erp_app/desktop/applications/rails_db_admin/database_combo.js +52 -0
- data/public/javascripts/erp_app/desktop/applications/rails_db_admin/module.js +429 -0
- data/public/javascripts/erp_app/desktop/applications/rails_db_admin/queries_tree_menu.js +86 -0
- data/public/javascripts/erp_app/desktop/applications/rails_db_admin/query_panel.js +206 -0
- data/public/javascripts/erp_app/desktop/applications/rails_db_admin/readonly_table_data_grid.js +27 -0
- data/public/javascripts/erp_app/desktop/applications/rails_db_admin/tables_tree_menu.js +87 -0
- data/public/stylesheets/erp_app/desktop/applications/rails_db_admin/rails_db_admin.css +10 -0
- data/spec/controllers/rails_db_admin/base_controller_spec.rb +481 -0
- data/spec/controllers/rails_db_admin/queries_controller_spec.rb +134 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +9 -0
- data/spec/dummy/app/assets/stylesheets/application.css +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/views/layouts/application.html.erb +14 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +45 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +14 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/cucumber.rb +3 -0
- data/spec/dummy/config/environments/spec.rb +27 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +10 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +12 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +5 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +26 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/lib/rails_db_admin/extjs/json_column_builder_spec.rb +206 -0
- data/spec/lib/rails_db_admin/extjs/json_data_builder_spec.rb +201 -0
- data/spec/lib/rails_db_admin/query_support_spec.rb +40 -0
- data/spec/lib/rails_db_admin/table_support_spec.rb +349 -0
- data/spec/spec_helper.rb +60 -0
- metadata +183 -0
data/README.md
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
![Logo](http://github.com/portablemind/compass_agile_enterprise/raw/master/erp_app/public/images/art/compass-logo-1-medium.png)
|
2
|
+
|
3
|
+
#Rails DB Admin
|
4
|
+
|
5
|
+
Rails DB Admin is an application that sits on top of the Compass AE framework that adds database management capabilities.
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'RailsDbAdmin'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
24
|
+
load 'rails/tasks/engine.rake'
|
25
|
+
|
26
|
+
Bundler::GemHelper.install_tasks
|
27
|
+
|
28
|
+
require "rspec/core/rake_task"
|
29
|
+
RSpec::Core::RakeTask.new(:spec)
|
30
|
+
task :default => :spec
|
@@ -0,0 +1,9 @@
|
|
1
|
+
// This is a manifest file that'll be compiled into including all the files listed below.
|
2
|
+
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
|
3
|
+
// be included in the compiled file accessible from http://example.com/assets/application.js
|
4
|
+
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
|
5
|
+
// the compiled file.
|
6
|
+
//
|
7
|
+
//= require jquery
|
8
|
+
//= require jquery_ujs
|
9
|
+
//= require_tree .
|
@@ -0,0 +1,7 @@
|
|
1
|
+
/*
|
2
|
+
* This is a manifest file that'll automatically include all the stylesheets available in this directory
|
3
|
+
* and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
|
4
|
+
* the top of the compiled file, but it's generally better to create a new file per style scope.
|
5
|
+
*= require_self
|
6
|
+
*= require_tree .
|
7
|
+
*/
|
@@ -0,0 +1,182 @@
|
|
1
|
+
module RailsDbAdmin
|
2
|
+
module ErpApp
|
3
|
+
module Desktop
|
4
|
+
class BaseController < ::ErpApp::Desktop::BaseController
|
5
|
+
before_filter :setup_database_connection
|
6
|
+
|
7
|
+
def databases
|
8
|
+
result = {:databases => []}
|
9
|
+
Rails.configuration.database_configuration.each do |k, v|
|
10
|
+
result[:databases] << {:display => k, :value => k}
|
11
|
+
end
|
12
|
+
|
13
|
+
render :json => result
|
14
|
+
end
|
15
|
+
|
16
|
+
def tables
|
17
|
+
result_hash = []
|
18
|
+
|
19
|
+
if params[:node] == "root"
|
20
|
+
tables = []
|
21
|
+
table_names = @database_connection_class.connection.tables
|
22
|
+
table_names.each do |table|
|
23
|
+
tables << {:name => table, :display => table} unless table.blank?
|
24
|
+
end
|
25
|
+
|
26
|
+
tables.sort! { |a,b| a[:name].downcase <=> b[:name].downcase }
|
27
|
+
|
28
|
+
tables.each do |table|
|
29
|
+
result_hash << table_hash = {:isTable => true,
|
30
|
+
:text => table[:display],
|
31
|
+
:id => table[:display],
|
32
|
+
:iconCls => 'icon-data',
|
33
|
+
:leaf => false}
|
34
|
+
end
|
35
|
+
else
|
36
|
+
columns = @database_connection_class.connection.columns(params[:node])
|
37
|
+
columns.each do |column|
|
38
|
+
result_hash << {:text => "#{column.name} : #{column.type}", :iconCls => 'icon-gear', :leaf => true}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
render :json => result_hash
|
43
|
+
end
|
44
|
+
|
45
|
+
def setup_table_grid
|
46
|
+
result = {:success => true}
|
47
|
+
table = params[:table]
|
48
|
+
columns = @database_connection_class.connection.columns(table)
|
49
|
+
|
50
|
+
|
51
|
+
if @table_support.table_contains_column(table, :id)
|
52
|
+
result[:columns] =
|
53
|
+
RailsDbAdmin::Extjs::JsonColumnBuilder.build_grid_columns(columns)
|
54
|
+
result[:model] = table
|
55
|
+
result[:fields] =
|
56
|
+
RailsDbAdmin::Extjs::JsonColumnBuilder.build_store_fields(columns)
|
57
|
+
result[:validations] = []
|
58
|
+
result[:id_property] = "id"
|
59
|
+
else
|
60
|
+
result[:columns] =
|
61
|
+
RailsDbAdmin::Extjs::JsonColumnBuilder.build_grid_columns(columns, true)
|
62
|
+
result[:model] = table
|
63
|
+
result[:fields] =
|
64
|
+
RailsDbAdmin::Extjs::JsonColumnBuilder.build_store_fields(columns, true)
|
65
|
+
result[:validations] = []
|
66
|
+
result[:id_property] = "fake_id"
|
67
|
+
end
|
68
|
+
|
69
|
+
render :json => result
|
70
|
+
end
|
71
|
+
|
72
|
+
def table_data
|
73
|
+
render :json => if request.get?
|
74
|
+
get_table_data
|
75
|
+
elsif request.post?
|
76
|
+
create_table_row
|
77
|
+
elsif request.put?
|
78
|
+
update_table_data
|
79
|
+
elsif request.delete?
|
80
|
+
delete_table_row
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def get_table_data
|
87
|
+
start = params[:start] || 0
|
88
|
+
limit = params[:limit] || 30
|
89
|
+
table = params[:table]
|
90
|
+
|
91
|
+
order = nil
|
92
|
+
|
93
|
+
if @table_support.table_contains_column(table, :id)
|
94
|
+
order = 'id desc'
|
95
|
+
elsif @table_support.table_contains_column(table, :created_at)
|
96
|
+
order = 'created_at desc'
|
97
|
+
end
|
98
|
+
|
99
|
+
@json_data_builder.build_json_data(:table => table, :limit => limit, :offset => start, :order => order)
|
100
|
+
end
|
101
|
+
|
102
|
+
def create_table_row
|
103
|
+
table = params[:table]
|
104
|
+
params[:data].delete('id')
|
105
|
+
record = nil
|
106
|
+
|
107
|
+
if @table_support.primary_key?(table)
|
108
|
+
id = @table_support.primary_key(table)
|
109
|
+
id[1] = @table_support.insert_row(table, params[:data])
|
110
|
+
record = @json_data_builder.get_row_data(table, id)
|
111
|
+
else
|
112
|
+
params[:data].delete('fake_id')
|
113
|
+
fake_id = @table_support.insert_row(table, params[:data],true)
|
114
|
+
record = @json_data_builder.get_row_data_no_id(table, params[:data])
|
115
|
+
record[:fake_id] = fake_id
|
116
|
+
end
|
117
|
+
|
118
|
+
{:success => true, :data => record}
|
119
|
+
end
|
120
|
+
|
121
|
+
def update_table_data
|
122
|
+
table = params[:table]
|
123
|
+
|
124
|
+
if @table_support.primary_key?(table)
|
125
|
+
id = @table_support.primary_key(table)
|
126
|
+
id[1] = params[:data][0][id[0]]
|
127
|
+
params[:data][0].delete(id[0])
|
128
|
+
|
129
|
+
@table_support.update_table(table, id, params[:data][0])
|
130
|
+
record = @json_data_builder.get_row_data(table, id)
|
131
|
+
else
|
132
|
+
fake_id = params[:data][0]['fake_id']
|
133
|
+
@table_support.update_table_without_id(table, params[:data])
|
134
|
+
record = @json_data_builder.get_row_data_no_id(table, params[:data][0])
|
135
|
+
record['fake_id'] = fake_id
|
136
|
+
end
|
137
|
+
|
138
|
+
{:success => true, :data => record}
|
139
|
+
end
|
140
|
+
|
141
|
+
def delete_table_row
|
142
|
+
table = params[:table]
|
143
|
+
id = params[:id]
|
144
|
+
exception = nil
|
145
|
+
|
146
|
+
if @table_support.primary_key?(table)
|
147
|
+
pk = @table_support.primary_key(table)
|
148
|
+
pk[1] = id
|
149
|
+
@table_support.delete_row(table.pluralize.underscore,pk)
|
150
|
+
else
|
151
|
+
exception = "Unable to determine primary key on this table. "\
|
152
|
+
"Delete not performed"
|
153
|
+
end
|
154
|
+
|
155
|
+
if (exception == nil)
|
156
|
+
{:success => true, :data => []}
|
157
|
+
else
|
158
|
+
{:success => false, :data => [], :exception => exception}
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def setup_database_connection
|
163
|
+
@database_connection_class = RailsDbAdmin::ConnectionHandler.create_connection_class(database_connection_name)
|
164
|
+
|
165
|
+
@query_support = RailsDbAdmin::QuerySupport.new(@database_connection_class, database_connection_name)
|
166
|
+
@table_support = RailsDbAdmin::TableSupport.new(@database_connection_class)
|
167
|
+
@json_data_builder = RailsDbAdmin::Extjs::JsonDataBuilder.new(@database_connection_class)
|
168
|
+
end
|
169
|
+
|
170
|
+
def database_connection_name
|
171
|
+
database_name = Rails.env
|
172
|
+
|
173
|
+
unless params[:database].blank?
|
174
|
+
database_name = params[:database]
|
175
|
+
end
|
176
|
+
|
177
|
+
database_name
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
module RailsDbAdmin
|
2
|
+
module ErpApp
|
3
|
+
module Desktop
|
4
|
+
class QueriesController < RailsDbAdmin::ErpApp::Desktop::BaseController
|
5
|
+
def save_query
|
6
|
+
query = params[:query]
|
7
|
+
query_name = params[:query_name]
|
8
|
+
|
9
|
+
@query_support.save_query(query, query_name)
|
10
|
+
|
11
|
+
render :json => {:success => true}
|
12
|
+
end
|
13
|
+
|
14
|
+
def saved_queries
|
15
|
+
names = @query_support.get_saved_query_names
|
16
|
+
|
17
|
+
names_hash_array = []
|
18
|
+
|
19
|
+
names_hash_array = names.collect do |name|
|
20
|
+
{:display => name, :value => name}
|
21
|
+
end unless names.empty?
|
22
|
+
|
23
|
+
render :json => {:data => names_hash_array}
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete_query
|
27
|
+
query_name = params[:query_name]
|
28
|
+
@query_support.delete_query(query_name)
|
29
|
+
|
30
|
+
render :json => {:success => true}
|
31
|
+
end
|
32
|
+
|
33
|
+
def saved_queries_tree
|
34
|
+
names = @query_support.get_saved_query_names
|
35
|
+
|
36
|
+
queries = []
|
37
|
+
|
38
|
+
queries = names.collect do |name|
|
39
|
+
{:text => name, :id => name, :iconCls => 'icon-document', :leaf => true}
|
40
|
+
end unless names.empty?
|
41
|
+
|
42
|
+
render :json => queries
|
43
|
+
end
|
44
|
+
|
45
|
+
def open_query
|
46
|
+
query_name = params[:query_name]
|
47
|
+
query = @query_support.get_query(query_name)
|
48
|
+
|
49
|
+
render :json => {:success => true, :query => query}
|
50
|
+
end
|
51
|
+
|
52
|
+
def open_and_execute_query
|
53
|
+
result = {}
|
54
|
+
query_name = params[:query_name]
|
55
|
+
|
56
|
+
query = @query_support.get_query(query_name)
|
57
|
+
columns, values, exception = @query_support.execute_sql(query)
|
58
|
+
|
59
|
+
if columns.blank? || values.blank?
|
60
|
+
result = {:success => false, :query => query,
|
61
|
+
:exception => "Empty result set"}
|
62
|
+
elsif exception.nil?
|
63
|
+
|
64
|
+
columns_array = columns.collect do |column|
|
65
|
+
RailsDbAdmin::Extjs::JsonColumnBuilder.build_readonly_column(column)
|
66
|
+
end
|
67
|
+
|
68
|
+
fields_array = columns.collect do |column|
|
69
|
+
{:name => column}
|
70
|
+
end
|
71
|
+
|
72
|
+
result = {:success => true, :query => query,
|
73
|
+
:columns => columns_array,
|
74
|
+
:fields => fields_array, :data => values}
|
75
|
+
else
|
76
|
+
result = {:success => false, :query => query,
|
77
|
+
:exception => exception.gsub("\n", " ")}
|
78
|
+
end
|
79
|
+
|
80
|
+
render :json => result
|
81
|
+
end
|
82
|
+
|
83
|
+
def select_top_fifty
|
84
|
+
table = params[:table]
|
85
|
+
sql, results = @query_support.select_top_fifty(table)
|
86
|
+
|
87
|
+
columns = @database_connection_class.connection.columns(table)
|
88
|
+
|
89
|
+
render :json => {:success => true,
|
90
|
+
:sql => sql,
|
91
|
+
:columns => RailsDbAdmin::Extjs::JsonColumnBuilder.build_grid_columns(columns),
|
92
|
+
:fields => RailsDbAdmin::Extjs::JsonColumnBuilder.build_store_fields(columns),
|
93
|
+
:data => results}
|
94
|
+
end
|
95
|
+
|
96
|
+
def execute_query
|
97
|
+
sql = params[:sql]
|
98
|
+
selection = params[:selected_sql]
|
99
|
+
sql = sql.rstrip
|
100
|
+
cursor_pos = params[:cursor_pos].to_i
|
101
|
+
|
102
|
+
#append a semicolon as the last character if the
|
103
|
+
#user forgot
|
104
|
+
if !sql.end_with?(";")
|
105
|
+
sql << ";"
|
106
|
+
end
|
107
|
+
|
108
|
+
sql_arr = sql.split("\n")
|
109
|
+
sql_stmt_arry = []
|
110
|
+
sql_str = ""
|
111
|
+
|
112
|
+
#search for the query to run based on cursor position if there
|
113
|
+
#was nothing selected by the user
|
114
|
+
if (selection == nil || selection == "")
|
115
|
+
last_stmt_end = 0
|
116
|
+
sql_arr.each_with_index do |val,idx|
|
117
|
+
if val.match(';')
|
118
|
+
sql_stmt_arry << {:begin => (sql_stmt_arry.length > 0) ? last_stmt_end +1 : 0, :end => idx}
|
119
|
+
last_stmt_end = idx
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
last_sql_stmt = sql_stmt_arry.length-1
|
124
|
+
#run the first complete query if we're in whitespace
|
125
|
+
#at the beginning of the text area
|
126
|
+
if cursor_pos <= sql_stmt_arry[0].fetch(:begin)
|
127
|
+
sql_str = sql_arr.values_at(sql_stmt_arry[0].fetch(:begin)..
|
128
|
+
sql_stmt_arry[0].fetch(:end)).join(" ")
|
129
|
+
#run the last query if we're in whitespace at the end of the
|
130
|
+
#textarea
|
131
|
+
elsif cursor_pos > sql_stmt_arry[last_sql_stmt].fetch(:begin)
|
132
|
+
sql_str = sql_arr.values_at(
|
133
|
+
sql_stmt_arry[last_sql_stmt].fetch(:begin)..
|
134
|
+
sql_stmt_arry[last_sql_stmt].fetch(:end)).join(" ")
|
135
|
+
#run query based on cursor position
|
136
|
+
else
|
137
|
+
sql_stmt_arry.each do |sql_stmt|
|
138
|
+
if cursor_pos >= sql_stmt.fetch(:begin) &&
|
139
|
+
cursor_pos <= sql_stmt.fetch(:end)
|
140
|
+
sql_str = sql_arr.values_at(sql_stmt.fetch(:begin)..
|
141
|
+
sql_stmt.fetch(:end)).join(" ")
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
else
|
146
|
+
sql_str = selection
|
147
|
+
end
|
148
|
+
|
149
|
+
columns, values, exception = @query_support.execute_sql(sql_str)
|
150
|
+
|
151
|
+
if !exception.nil?
|
152
|
+
result = {:success => false, :exception => exception.gsub("\n"," ")}
|
153
|
+
elsif columns.empty? || values.empty?
|
154
|
+
result = {:success => false, :exception => "Empty result set"}
|
155
|
+
else exception.nil?
|
156
|
+
columns_array = columns.collect do |column|
|
157
|
+
RailsDbAdmin::Extjs::JsonColumnBuilder.build_readonly_column(column)
|
158
|
+
end
|
159
|
+
|
160
|
+
fields_array = columns.collect do |column|
|
161
|
+
{:name => column}
|
162
|
+
end
|
163
|
+
|
164
|
+
result = {:success => true, :sql => sql,
|
165
|
+
:columns => columns_array,
|
166
|
+
:fields => fields_array, :data => values}
|
167
|
+
end
|
168
|
+
render :json => result
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>RailsDbAdmin</title>
|
5
|
+
<%= stylesheet_link_tag "rails_db_admin/application" %>
|
6
|
+
<%= javascript_include_tag "rails_db_admin/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|
@@ -0,0 +1,14 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>RailsDbAdmin</title>
|
5
|
+
<%= stylesheet_link_tag "rails_db_admin/application" %>
|
6
|
+
<%= javascript_include_tag "rails_db_admin/application" %>
|
7
|
+
<%= csrf_meta_tags %>
|
8
|
+
</head>
|
9
|
+
<body>
|
10
|
+
|
11
|
+
<%= yield %>
|
12
|
+
|
13
|
+
</body>
|
14
|
+
</html>
|