rails_db_admin 2.0.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.
- 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
|
+

|
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>
|