bento_search 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.md +299 -0
  3. data/Rakefile +40 -0
  4. data/app/assets/images/bento_search/large_loader.gif +0 -0
  5. data/app/assets/javascripts/bento_search.js +3 -0
  6. data/app/assets/javascripts/bento_search/ajax_load.js +22 -0
  7. data/app/assets/stylesheets/bento_search/bento.css +4 -0
  8. data/app/controllers/bento_search/bento_search_controller.rb +7 -0
  9. data/app/controllers/bento_search/search_controller.rb +72 -0
  10. data/app/helpers/bento_search_helper.rb +138 -0
  11. data/app/item_decorators/bento_search/only_premade_openurl.rb +16 -0
  12. data/app/item_decorators/bento_search/openurl_add_other_link.rb +35 -0
  13. data/app/item_decorators/bento_search/openurl_main_link.rb +30 -0
  14. data/app/models/bento_search/author.rb +25 -0
  15. data/app/models/bento_search/link.rb +30 -0
  16. data/app/models/bento_search/multi_searcher.rb +109 -0
  17. data/app/models/bento_search/openurl_creator.rb +128 -0
  18. data/app/models/bento_search/registrar.rb +70 -0
  19. data/app/models/bento_search/result_item.rb +203 -0
  20. data/app/models/bento_search/results.rb +54 -0
  21. data/app/models/bento_search/results/pagination.rb +67 -0
  22. data/app/models/bento_search/search_engine.rb +219 -0
  23. data/app/models/bento_search/search_engine/capabilities.rb +65 -0
  24. data/app/search_engines/bento_search/#Untitled-1# +11 -0
  25. data/app/search_engines/bento_search/ebsco_host_engine.rb +356 -0
  26. data/app/search_engines/bento_search/eds_engine.rb +557 -0
  27. data/app/search_engines/bento_search/google_books_engine.rb +184 -0
  28. data/app/search_engines/bento_search/primo_engine.rb +231 -0
  29. data/app/search_engines/bento_search/scopus_engine.rb +295 -0
  30. data/app/search_engines/bento_search/summon_engine.rb +398 -0
  31. data/app/search_engines/bento_search/xerxes_engine.rb +168 -0
  32. data/app/views/bento_search/_link.html.erb +4 -0
  33. data/app/views/bento_search/_search_error.html.erb +22 -0
  34. data/app/views/bento_search/_std_item.html.erb +39 -0
  35. data/app/views/bento_search/search/search.html.erb +1 -0
  36. data/config/locales/en.yml +25 -0
  37. data/lib/bento_search.rb +29 -0
  38. data/lib/bento_search/engine.rb +5 -0
  39. data/lib/bento_search/routes.rb +45 -0
  40. data/lib/bento_search/version.rb +3 -0
  41. data/lib/generators/bento_search/pull_ebsco_dbs_generator.rb +24 -0
  42. data/lib/generators/bento_search/templates/ebsco_global_var.erb +6 -0
  43. data/lib/http_client_patch/include_client.rb +86 -0
  44. data/lib/tasks/bento_search_tasks.rake +4 -0
  45. data/test/dummy/README.rdoc +261 -0
  46. data/test/dummy/Rakefile +7 -0
  47. data/test/dummy/app/assets/javascripts/application.js +15 -0
  48. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  49. data/test/dummy/app/controllers/application_controller.rb +3 -0
  50. data/test/dummy/app/helpers/application_helper.rb +2 -0
  51. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  52. data/test/dummy/config.ru +4 -0
  53. data/test/dummy/config/application.rb +56 -0
  54. data/test/dummy/config/boot.rb +10 -0
  55. data/test/dummy/config/database.yml +25 -0
  56. data/test/dummy/config/environment.rb +5 -0
  57. data/test/dummy/config/environments/development.rb +37 -0
  58. data/test/dummy/config/environments/production.rb +67 -0
  59. data/test/dummy/config/environments/test.rb +37 -0
  60. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  61. data/test/dummy/config/initializers/inflections.rb +15 -0
  62. data/test/dummy/config/initializers/mime_types.rb +5 -0
  63. data/test/dummy/config/initializers/secret_token.rb +7 -0
  64. data/test/dummy/config/initializers/session_store.rb +8 -0
  65. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  66. data/test/dummy/config/locales/en.yml +5 -0
  67. data/test/dummy/config/routes.rb +6 -0
  68. data/test/dummy/db/test.sqlite3 +0 -0
  69. data/test/dummy/log/test.log +3100 -0
  70. data/test/dummy/public/404.html +26 -0
  71. data/test/dummy/public/422.html +26 -0
  72. data/test/dummy/public/500.html +25 -0
  73. data/test/dummy/public/favicon.ico +0 -0
  74. data/test/dummy/script/rails +6 -0
  75. data/test/functional/bento_search/search_controller_test.rb +81 -0
  76. data/test/helper/bento_search_helper_test.rb +125 -0
  77. data/test/integration/navigation_test.rb +10 -0
  78. data/test/support/mock_engine.rb +23 -0
  79. data/test/support/test_with_cassette.rb +38 -0
  80. data/test/test_helper.rb +52 -0
  81. data/test/unit/#vcr_test.rb# +68 -0
  82. data/test/unit/ebsco_host_engine_test.rb +134 -0
  83. data/test/unit/eds_engine_test.rb +105 -0
  84. data/test/unit/google_books_engine_test.rb +93 -0
  85. data/test/unit/item_decorators_test.rb +66 -0
  86. data/test/unit/multi_searcher_test.rb +49 -0
  87. data/test/unit/openurl_creator_test.rb +111 -0
  88. data/test/unit/pagination_test.rb +59 -0
  89. data/test/unit/primo_engine_test.rb +37 -0
  90. data/test/unit/register_engine_test.rb +50 -0
  91. data/test/unit/result_item_display_test.rb +39 -0
  92. data/test/unit/result_item_test.rb +36 -0
  93. data/test/unit/scopus_engine_test.rb +130 -0
  94. data/test/unit/search_engine_base_test.rb +178 -0
  95. data/test/unit/search_engine_test.rb +95 -0
  96. data/test/unit/summon_engine_test.rb +161 -0
  97. data/test/unit/xerxes_engine_test.rb +70 -0
  98. data/test/vcr_cassettes/ebscohost/error_bad_db.yml +45 -0
  99. data/test/vcr_cassettes/ebscohost/error_bad_password.yml +45 -0
  100. data/test/vcr_cassettes/ebscohost/get_info.yml +3626 -0
  101. data/test/vcr_cassettes/ebscohost/live_search.yml +45 -0
  102. data/test/vcr_cassettes/ebscohost/live_search_smoke_test.yml +1311 -0
  103. data/test/vcr_cassettes/eds/basic_search_smoke_test.yml +1811 -0
  104. data/test/vcr_cassettes/eds/get_auth_token.yml +75 -0
  105. data/test/vcr_cassettes/eds/get_auth_token_failure.yml +39 -0
  106. data/test/vcr_cassettes/eds/get_with_auth.yml +243 -0
  107. data/test/vcr_cassettes/eds/get_with_auth_recovers_from_bad_auth.yml +368 -0
  108. data/test/vcr_cassettes/gbs/error_condition.yml +40 -0
  109. data/test/vcr_cassettes/gbs/pagination.yml +702 -0
  110. data/test/vcr_cassettes/gbs/search.yml +340 -0
  111. data/test/vcr_cassettes/primo/search_smoke_test.yml +1112 -0
  112. data/test/vcr_cassettes/scopus/bad_api_key_should_return_error_response.yml +60 -0
  113. data/test/vcr_cassettes/scopus/escaped_chars.yml +187 -0
  114. data/test/vcr_cassettes/scopus/fielded_search.yml +176 -0
  115. data/test/vcr_cassettes/scopus/simple_search.yml +227 -0
  116. data/test/vcr_cassettes/scopus/zero_results_search.yml +67 -0
  117. data/test/vcr_cassettes/summon/bad_auth.yml +54 -0
  118. data/test/vcr_cassettes/summon/proper_tags_for_snippets.yml +216 -0
  119. data/test/vcr_cassettes/summon/search.yml +242 -0
  120. data/test/vcr_cassettes/xerxes/live_search.yml +2580 -0
  121. data/test/view/std_item_test.rb +98 -0
  122. metadata +421 -0
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The page you were looking for doesn't exist (404)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/404.html -->
21
+ <div class="dialog">
22
+ <h1>The page you were looking for doesn't exist.</h1>
23
+ <p>You may have mistyped the address or the page may have moved.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>The change you wanted was rejected (422)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/422.html -->
21
+ <div class="dialog">
22
+ <h1>The change you wanted was rejected.</h1>
23
+ <p>Maybe you tried to change something you didn't have access to.</p>
24
+ </div>
25
+ </body>
26
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>We're sorry, but something went wrong (500)</title>
5
+ <style type="text/css">
6
+ body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
7
+ div.dialog {
8
+ width: 25em;
9
+ padding: 0 4em;
10
+ margin: 4em auto 0 auto;
11
+ border: 1px solid #ccc;
12
+ border-right-color: #999;
13
+ border-bottom-color: #999;
14
+ }
15
+ h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
16
+ </style>
17
+ </head>
18
+
19
+ <body>
20
+ <!-- This file lives in public/500.html -->
21
+ <div class="dialog">
22
+ <h1>We're sorry, but something went wrong.</h1>
23
+ </div>
24
+ </body>
25
+ </html>
File without changes
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
3
+
4
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
5
+ require File.expand_path('../../config/boot', __FILE__)
6
+ require 'rails/commands'
@@ -0,0 +1,81 @@
1
+ require 'test_helper'
2
+
3
+ module BentoSearch
4
+ class SearchControllerTest < ActionController::TestCase
5
+ def setup
6
+ BentoSearch.register_engine("mock") do |config|
7
+ config.engine = "MockEngine"
8
+ config.allow_routable_results = true
9
+ end
10
+
11
+ BentoSearch.register_engine("not_routable") do |config|
12
+ config.engine = "MockEngine"
13
+ # no allow_routable_results
14
+ end
15
+
16
+ end
17
+
18
+ def teardown
19
+ BentoSearch.reset_engine_registrations!
20
+ end
21
+
22
+
23
+ test "search" do
24
+ get :search, {:engine_id => "mock", :query => "my search"}
25
+ assert_response :success
26
+ assert_not_nil assigns(:results)
27
+
28
+ assert_template "bento_search/search"
29
+ end
30
+
31
+ test "non-routable engine" do
32
+ get :search, {:engine_id => "not_routable", :query => "my search"}
33
+
34
+ assert_response 403
35
+ end
36
+
37
+ test "non-existent engine" do
38
+ get :search, {:engine_id => "not_existing", :query => "my search"}
39
+
40
+ assert_response 404
41
+ end
42
+
43
+ test "custom before filter" do
44
+ # Okay, we're going to do a weird thing with a custom controller subclass
45
+ # we can add a custom before filter like a local app might.
46
+ #
47
+ # SUPER HACKY, but I dunno what else to do.
48
+
49
+ class CustomSearchController < BentoSearch::SearchController
50
+ before_filter :deny_everyone
51
+
52
+ def deny_everyone
53
+ raise BentoSearch::SearchController::AccessDenied
54
+ end
55
+ end
56
+
57
+
58
+
59
+ orig_controller = @controller
60
+
61
+ begin
62
+ Rails.application.routes.draw do
63
+ match "/custom_search" => "bento_search/search_controller_test/custom_search#search"
64
+ end
65
+ @controller = CustomSearchController.new
66
+
67
+ get :search, {:engine_id => "mock", :query => "my search"}
68
+
69
+ assert_response 403
70
+ ensure
71
+ @controller = orig_controller
72
+ Rails.application.reload_routes!
73
+ end
74
+
75
+ end
76
+
77
+
78
+
79
+
80
+ end
81
+ end
@@ -0,0 +1,125 @@
1
+ require 'test_helper'
2
+
3
+ class BentoSearchHelperTest < ActionView::TestCase
4
+ include BentoSearchHelper
5
+
6
+
7
+ def teardown
8
+ BentoSearch.reset_engine_registrations!
9
+ end
10
+
11
+
12
+
13
+
14
+ def setup
15
+ # Make routing work
16
+ @routes = Rails.application.routes
17
+ end
18
+
19
+ def test_with_results_arg
20
+ results = MockEngine.new.search(:query => "foo")
21
+ bento_search(results)
22
+
23
+ assert_select("div.bento_item", 10)
24
+ end
25
+
26
+ def test_with_failed_search
27
+ results = BentoSearch::Results.new
28
+ results.error = {:error => true}
29
+
30
+ assert results.failed?
31
+
32
+ response = HTML::Document.new(bento_search(results))
33
+
34
+ assert (no_results_div = response.find(:attributes => {:class => "bento_search_error alert alert-error"})), "has search_error div"
35
+
36
+ assert no_results_div.match(Regexp.new I18n.translate("bento_search.search_error")), "has error message"
37
+
38
+ assert_nil response.find(:attributes => {:class => "bento_item"})
39
+ end
40
+
41
+ def test_with_empty_results
42
+ results = MockEngine.new(:num_results => 0).search(:query => "foo")
43
+
44
+ response = HTML::Document.new(bento_search(results))
45
+
46
+ assert (no_results_div = response.find(:attributes => {:class => "bento_search_no_results"})), "has no_results div"
47
+ assert no_results_div.match(Regexp.new(I18n.translate("bento_search.no_results")))
48
+
49
+
50
+ assert_nil response.find(:attributes => {:class => "bento_item"}), "has no results message"
51
+ end
52
+
53
+ def test_with_engine_arg
54
+ engine = MockEngine.new
55
+ bento_search(engine, :query => "QUERY")
56
+
57
+ assert_select("div.bento_item", 10).each_with_index do |node, i|
58
+ node.match /QUERY/
59
+ node.match /#{i +1 }/
60
+ end
61
+ end
62
+
63
+ def test_with_registered_id
64
+ BentoSearch.register_engine("test_engine") do |conf|
65
+ conf.engine = "MockEngine"
66
+ end
67
+
68
+ bento_search("test_engine", :query => "QUERY")
69
+
70
+ assert_select("div.bento_item", 10).each_with_index do |node, i|
71
+ node.match /QUERY/
72
+ node.match /#{i +1 }/
73
+ end
74
+ end
75
+
76
+ def test_ajax_load_without_registration
77
+ assert_raises(ArgumentError) { bento_search(MockEngine.new, :load => :ajax_auto) }
78
+ end
79
+
80
+ def test_ajax_load
81
+ BentoSearch.register_engine("test_engine") do |conf|
82
+ conf.engine = "MockEngine"
83
+ end
84
+
85
+ results = bento_search("test_engine", :query => "QUERY", :load => :ajax_auto)
86
+ results = HTML::Document.new(results)
87
+
88
+
89
+ div = results.find(:attributes => {:class => "bento_search_ajax_wait"})
90
+ assert div, "produces div.bento_search_ajax_wait"
91
+
92
+ assert_present div.attributes["data-bento-ajax-url"]
93
+ url = URI.parse(div.attributes["data-bento-ajax-url"])
94
+ assert_equal "/bento/test_engine", url.path
95
+
96
+ query = CGI.parse(url.query.gsub("&amp;", "&")) # gsub weirdness of HTML::Tag
97
+ assert_equal ["QUERY"], query["query"]
98
+ assert_empty query["load"]
99
+
100
+ assert div.find(:tag => "noscript"), "has <noscript> tag"
101
+
102
+ assert (img = div.find(:tag => "img")), "Has spinner gif"
103
+ assert_equal I18n.translate("bento_search.ajax_loading"), img.attributes["alt"]
104
+ end
105
+
106
+
107
+ def test_sort_hash_for
108
+ tested_keys = %w{title_asc date_desc relevance author_asc}
109
+
110
+ sort_definitions = {}
111
+ tested_keys.each {|k| sort_definitions[k] = {}}
112
+
113
+ engine = MockEngine.new(:sort_definitions => sort_definitions)
114
+
115
+ hash = bento_sort_hash_for(engine)
116
+
117
+ assert_present hash
118
+
119
+ tested_keys.each do |key|
120
+ assert_equal key, hash[ I18n.translate(key, :scope => "bento_search.sort_keys") ]
121
+ end
122
+ end
123
+
124
+
125
+ end
@@ -0,0 +1,10 @@
1
+ require 'test_helper'
2
+
3
+ class NavigationTest < ActionDispatch::IntegrationTest
4
+ fixtures :all
5
+
6
+ # test "the truth" do
7
+ # assert true
8
+ # end
9
+ end
10
+
@@ -0,0 +1,23 @@
1
+ class MockEngine
2
+ include BentoSearch::SearchEngine
3
+
4
+ def search_implementation(args)
5
+ results = BentoSearch::Results.new
6
+ 1.upto(configuration.num_results) do |i|
7
+ results << BentoSearch::ResultItem.new(:title => "Item #{i}: #{args[:query]}", :link => configuration.link)
8
+ end
9
+ results.total_items = configuration.total_items
10
+ return results
11
+ end
12
+
13
+ def self.default_configuration
14
+ { :num_results => 10,
15
+ :total_items => 1000,
16
+ :link => "http://example.org"}
17
+ end
18
+
19
+ def sort_definitions
20
+ configuration.sort_definitions || {}
21
+ end
22
+
23
+ end
@@ -0,0 +1,38 @@
1
+
2
+ module TestWithCassette
3
+ def test_with_cassette(name, group = nil, vcr_options ={}, &block)
4
+ # cribbed from Rails and modified for VCR
5
+ # https://github.com/rails/rails/blob/b451de0d6de4df6bc66b274cec73b919f823d5ae/activesupport/lib/active_support/testing/declarative.rb#L25
6
+
7
+ test_name_safe = name.gsub(/\s+/,'_')
8
+
9
+ test_method_name = "test_#{test_name_safe}".to_sym
10
+
11
+ raise "#{test_method_name} is already defined in #{self}" if methods.include?(test_method_name)
12
+
13
+ cassette_name = vcr_options.delete(:cassette)
14
+ unless cassette_name
15
+ # calculate default cassette name from test name
16
+ cassette_name = test_name_safe
17
+ # put in group subdir if group
18
+ cassette_name = "#{group}/#{cassette_name}" if group
19
+ end
20
+
21
+ # default tag with groupname, can be over-ridden.
22
+ vcr_options = {:tag => group}.merge(vcr_options) if group
23
+
24
+ if block_given?
25
+ define_method(test_method_name) do
26
+ VCR.use_cassette(cassette_name , vcr_options) do
27
+ instance_eval &block
28
+ end
29
+ end
30
+ else
31
+ define_method(test_method_name) do
32
+ flunk "No implementation provided for #{name}"
33
+ end
34
+ end
35
+ end
36
+ end
37
+
38
+
@@ -0,0 +1,52 @@
1
+ # Configure Rails Environment
2
+ ENV["RAILS_ENV"] = "test"
3
+
4
+ require File.expand_path("../dummy/config/environment.rb", __FILE__)
5
+
6
+ # we insist on minitest, when only the best will do.
7
+ # Rails will build on top of it if it's there.
8
+ require 'minitest/unit'
9
+
10
+ require "rails/test_help"
11
+
12
+ Rails.backtrace_cleaner.remove_silencers!
13
+
14
+ # Load support files
15
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
16
+
17
+ # Load fixtures from the engine
18
+ if ActiveSupport::TestCase.method_defined?(:fixture_path=)
19
+ ActiveSupport::TestCase.fixture_path = File.expand_path("../fixtures", __FILE__)
20
+ end
21
+
22
+ # VCR is used to 'record' HTTP interactions with
23
+ # third party services used in tests, and play em
24
+ # back. Useful for efficiency, also useful for
25
+ # testing code against API's that not everyone
26
+ # has access to -- the responses can be cached
27
+ # and re-used.
28
+ require 'vcr'
29
+ require 'webmock'
30
+
31
+ # To allow us to do real HTTP requests in a VCR.turned_off, we
32
+ # have to tell webmock to let us.
33
+ WebMock.allow_net_connect!
34
+
35
+ VCR.configure do |c|
36
+ c.cassette_library_dir = 'test/vcr_cassettes'
37
+ # webmock needed for HTTPClient testing
38
+ c.hook_into :webmock
39
+ end
40
+
41
+ # Silly way to not have to rewrite all our tests if we
42
+ # temporarily disable VCR, make VCR.use_cassette a no-op
43
+ # instead of no-such-method.
44
+ if ! defined? VCR
45
+ module VCR
46
+ def self.use_cassette(*args)
47
+ yield
48
+ end
49
+ end
50
+ end
51
+
52
+
@@ -0,0 +1,68 @@
1
+ require 'httpclient'
2
+ require 'vcr'
3
+ require 'webmock'
4
+ require 'test/unit'
5
+
6
+ # To allow us to do real HTTP requests in a VCR.turned_off, we
7
+ # have to tell webmock to let us.
8
+ WebMock.allow_net_connect!
9
+
10
+ VCR.configure do |c|
11
+ c.cassette_library_dir = 'test/vcr_cassettes'
12
+ # webmock needed for HTTPClient testing
13
+ c.hook_into :webmock
14
+ end
15
+
16
+ class VcrTest < Test::Unit::TestCase
17
+
18
+
19
+
20
+
21
+ @@http_client = HTTPClient.new
22
+
23
+
24
+ def setup
25
+ @body = "{ \"UserId\":\"user\", \"Password\":\"password\" } "
26
+ # this was originally a real vendor URL, but test is reproducible
27
+ # even with this fake URL that isn't connectable.
28
+ # But reproduces with a real URL that
29
+ # accepts POSTs too.
30
+ @url = "http://example.org"
31
+
32
+ end
33
+
34
+
35
+
36
+ def test_get_one
37
+ VCR.use_cassette("test_one") do
38
+ response = @@http_client.post(@url, @body)
39
+ end
40
+ end
41
+
42
+ def test_get_two
43
+ VCR.use_cassette("test_two") do
44
+ response = @@http_client.post(@url, @body)
45
+ end
46
+
47
+
48
+ end
49
+
50
+ # If this VCR.turned_off is present, for some reason we get
51
+ # the "appears to be a bug in Webmock's" warning even when
52
+ # re-playing pre-recorded cassettes, otherwise only when
53
+ # recording.
54
+ def test_aaa_turned_off
55
+ VCR.turned_off do
56
+ response = @@http_client.post(@url, @body)
57
+ end
58
+ end
59
+
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+ end