harbinger 0.0.1.pre → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +3 -0
- data/.hound.yml +818 -0
- data/.travis.yml +20 -0
- data/Gemfile +23 -3
- data/README.md +8 -27
- data/Rakefile +47 -1
- data/app/controllers/harbinger/messages_controller.rb +24 -0
- data/app/models/harbinger/database_channel_message.rb +51 -0
- data/app/models/harbinger/database_channel_message_element.rb +19 -0
- data/app/views/harbinger/messages/index.html.erb +43 -0
- data/app/views/harbinger/messages/show.html.erb +24 -0
- data/config/routes.rb +3 -0
- data/db/migrate/20140310185338_create_harbinger_database_channel_message.rb +11 -0
- data/db/migrate/20140310185339_create_harbinger_database_channel_message_elements.rb +14 -0
- data/gemfiles/rails4.1.gemfile +12 -0
- data/gemfiles/rails4.gemfile +13 -0
- data/harbinger.gemspec +22 -7
- data/lib/generators/harbinger/install/install_generator.rb +23 -0
- data/lib/generators/harbinger/install/templates/harbinger_initializer.rb.erb +6 -0
- data/lib/harbinger.rb +100 -1
- data/lib/harbinger/channels.rb +25 -0
- data/lib/harbinger/channels/database_channel.rb +15 -0
- data/lib/harbinger/channels/logger_channel.rb +31 -0
- data/lib/harbinger/channels/null_channel.rb +7 -0
- data/lib/harbinger/configuration.rb +58 -0
- data/lib/harbinger/engine.rb +8 -1
- data/lib/harbinger/exceptions.rb +4 -0
- data/lib/harbinger/message.rb +20 -0
- data/lib/harbinger/reporters.rb +34 -0
- data/lib/harbinger/reporters/exception_reporter.rb +26 -0
- data/lib/harbinger/reporters/null_reporter.rb +14 -0
- data/lib/harbinger/reporters/request_reporter.rb +20 -0
- data/lib/harbinger/reporters/user_reporter.rb +20 -0
- data/lib/harbinger/version.rb +1 -1
- data/run_each_spec_in_isolation +9 -0
- data/script/fast_specs +20 -0
- data/spec/controllers/harbinger/messages_controller_spec.rb +26 -0
- data/spec/features/end_to_end_exception_handling_spec.rb +39 -0
- data/spec/lib/harbinger/channels/database_channel_spec.rb +18 -0
- data/spec/lib/harbinger/channels/logger_channel_spec.rb +21 -0
- data/spec/lib/harbinger/channels/null_channel_spec.rb +8 -0
- data/spec/lib/harbinger/channels_spec.rb +40 -0
- data/spec/lib/harbinger/configuration_spec.rb +53 -0
- data/spec/lib/harbinger/message_spec.rb +15 -0
- data/spec/lib/harbinger/reporters/exception_reporter_spec.rb +24 -0
- data/spec/lib/harbinger/reporters/null_reporter_spec.rb +21 -0
- data/spec/lib/harbinger/reporters/request_reporter_spec.rb +23 -0
- data/spec/lib/harbinger/reporters/user_reporter_spec.rb +17 -0
- data/spec/lib/harbinger/reporters_spec.rb +46 -0
- data/spec/lib/harbinger_spec.rb +60 -0
- data/spec/models/harbinger/database_channel_message_element_spec.rb +16 -0
- data/spec/models/harbinger/database_channel_message_spec.rb +68 -0
- data/spec/routing/harbinger/messages_routing_spec.rb +16 -0
- data/spec/spec_active_record_helper.rb +41 -0
- data/spec/spec_fast_helper.rb +70 -0
- data/spec/spec_slow_helper.rb +57 -0
- data/spec/spec_view_helper.rb +38 -0
- data/spec/test_app_templates/lib/generators/test_app_generator.rb +13 -0
- data/spec/views/harbinger/messages/index.html.erb_spec.rb +31 -0
- data/spec/views/harbinger/messages/show.html.erb_spec.rb +36 -0
- metadata +205 -20
- data/MIT-LICENSE +0 -20
data/.travis.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- "2.0.0"
|
4
|
+
- "2.1.1"
|
5
|
+
- "2.1.2"
|
6
|
+
|
7
|
+
gemfile:
|
8
|
+
- gemfiles/rails4.gemfile
|
9
|
+
- gemfiles/rails4.1.gemfile
|
10
|
+
|
11
|
+
env:
|
12
|
+
global:
|
13
|
+
- NOKOGIRI_USE_SYSTEM_LIBRARIES=true
|
14
|
+
|
15
|
+
script: 'COVERAGE=true rake spec:travis'
|
16
|
+
|
17
|
+
bundler_args: --without headless debug
|
18
|
+
|
19
|
+
before_install:
|
20
|
+
- gem install bundler
|
data/Gemfile
CHANGED
@@ -3,12 +3,32 @@ source "https://rubygems.org"
|
|
3
3
|
# Declare your gem's dependencies in harbinger.gemspec.
|
4
4
|
# Bundler will treat runtime dependencies like base dependencies, and
|
5
5
|
# development dependencies will be added by default to the :development group.
|
6
|
-
gemspec
|
6
|
+
gemspec path: File.expand_path('..', __FILE__)
|
7
7
|
|
8
8
|
# Declare any dependencies that are still in development here instead of in
|
9
9
|
# your gemspec. These might include edge Rails or gems from your path or
|
10
10
|
# Git. Remember to move these dependencies to your gemspec before releasing
|
11
11
|
# your gem to rubygems.org.
|
12
12
|
|
13
|
-
|
14
|
-
|
13
|
+
gem 'capybara', require: false
|
14
|
+
gem 'coveralls', require: false
|
15
|
+
if ! ENV['TRAVIS']
|
16
|
+
gem 'simplecov', require: false
|
17
|
+
gem 'guard-rspec'
|
18
|
+
gem 'guard-bundler'
|
19
|
+
gem 'guard-rails'
|
20
|
+
gem 'rb-fsevent'
|
21
|
+
gem 'terminal-notifier-guard'
|
22
|
+
gem 'sexp_processor'
|
23
|
+
gem 'ruby_parser'
|
24
|
+
gem 'pry', '~> 0.9.7'
|
25
|
+
gem 'pry-nav'
|
26
|
+
gem 'byebug'
|
27
|
+
end
|
28
|
+
|
29
|
+
file = File.expand_path("Gemfile", ENV['ENGINE_CART_DESTINATION'] || ENV['RAILS_ROOT'] || File.expand_path("../spec/internal", __FILE__))
|
30
|
+
|
31
|
+
if File.exists?(file)
|
32
|
+
puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
|
33
|
+
instance_eval File.read(file)
|
34
|
+
end
|
data/README.md
CHANGED
@@ -1,29 +1,10 @@
|
|
1
1
|
# Harbinger
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
And then execute:
|
12
|
-
|
13
|
-
$ bundle
|
14
|
-
|
15
|
-
Or install it yourself as:
|
16
|
-
|
17
|
-
$ gem install harbinger
|
18
|
-
|
19
|
-
## Usage
|
20
|
-
|
21
|
-
TODO: Write usage instructions here
|
22
|
-
|
23
|
-
## Contributing
|
24
|
-
|
25
|
-
1. Fork it ( http://github.com/<my-github-username>/harbinger/fork )
|
26
|
-
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
-
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
|
-
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
-
5. Create new Pull Request
|
3
|
+
[![Version](https://badge.fury.io/rb/harbinger.png)](http://badge.fury.io/rb/harbinger)
|
4
|
+
[![Build Status](https://travis-ci.org/ndlib/harbinger.png?branch=master)](https://travis-ci.org/ndlib/harbinger)
|
5
|
+
[![Code Climate](https://codeclimate.com/github/ndlib/harbinger.png)](https://codeclimate.com/github/ndlib/harbinger)
|
6
|
+
[![Coverage Status](https://img.shields.io/coveralls/ndlib/harbinger.svg)](https://coveralls.io/r/ndlib/harbinger)
|
7
|
+
[![API Docs](http://img.shields.io/badge/API-docs-blue.svg)](http://rubydoc.info/github/ndlib/harbinger/master/frames/)
|
8
|
+
[![APACHE 2 License](http://img.shields.io/badge/APACHE2-license-blue.svg)](./LICENSE)
|
9
|
+
|
10
|
+
A Rails engine for arbitrary message creation and delivery.
|
data/Rakefile
CHANGED
@@ -1 +1,47 @@
|
|
1
|
-
|
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
|
+
Bundler::GemHelper.install_tasks
|
8
|
+
|
9
|
+
begin
|
10
|
+
APP_RAKEFILE = File.expand_path('../spec/internal/Rakefile', __FILE__)
|
11
|
+
load 'rails/tasks/engine.rake'
|
12
|
+
rescue LoadError
|
13
|
+
puts "Unable to load all app tasks for #{APP_RAKEFILE}"
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'engine_cart/rake_task'
|
17
|
+
# http://stackoverflow.com/questions/23165506/rails-spring-breaking-generators
|
18
|
+
# https://github.com/cbeer/engine_cart/issues/15
|
19
|
+
EngineCart.rails_options = '--skip-spring'
|
20
|
+
require 'rspec/core/rake_task'
|
21
|
+
|
22
|
+
namespace :spec do
|
23
|
+
RSpec::Core::RakeTask.new(:all) do
|
24
|
+
ENV['COVERAGE'] = 'true'
|
25
|
+
end
|
26
|
+
|
27
|
+
desc 'Run the Travis CI specs'
|
28
|
+
task :travis do
|
29
|
+
ENV['RAILS_ENV'] = 'test'
|
30
|
+
spec_helper = File.expand_path('../spec/spec_slow_helper.rb', __FILE__)
|
31
|
+
ENV['SPEC_OPTS'] = "--profile 20 --require #{spec_helper}"
|
32
|
+
Rake::Task['engine_cart:clean'].invoke
|
33
|
+
Rake::Task['engine_cart:generate'].invoke
|
34
|
+
Rake::Task['spec:all'].invoke
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
begin
|
39
|
+
Rake::Task['default'].clear
|
40
|
+
rescue RuntimeError
|
41
|
+
puts 'Unable to find :default rake task; No worries.'
|
42
|
+
end
|
43
|
+
|
44
|
+
Rake::Task['spec'].clear
|
45
|
+
|
46
|
+
task spec: 'spec:all'
|
47
|
+
task default: 'spec:travis'
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Harbinger
|
2
|
+
class MessagesController < ActionController::Base
|
3
|
+
|
4
|
+
def index
|
5
|
+
messages
|
6
|
+
end
|
7
|
+
|
8
|
+
def show
|
9
|
+
message
|
10
|
+
end
|
11
|
+
|
12
|
+
protected
|
13
|
+
|
14
|
+
def messages
|
15
|
+
@messages ||= DatabaseChannelMessage.search(q: params[:q])
|
16
|
+
end
|
17
|
+
|
18
|
+
def message
|
19
|
+
@message ||= DatabaseChannelMessage.find(params[:id])
|
20
|
+
end
|
21
|
+
|
22
|
+
helper_method :message, :messages
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'harbinger/database_channel_message_element'
|
3
|
+
|
4
|
+
module Harbinger
|
5
|
+
class DatabaseChannelMessage < ActiveRecord::Base
|
6
|
+
self.table_name = 'harbinger_messages'
|
7
|
+
has_many :elements, class_name: 'Harbinger::DatabaseChannelMessageElement', foreign_key: :message_id
|
8
|
+
|
9
|
+
def contexts=(values)
|
10
|
+
super(Array.wrap(values).join(','))
|
11
|
+
end
|
12
|
+
|
13
|
+
def contexts
|
14
|
+
super.split(',')
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.store_message(message, storage = new)
|
18
|
+
storage.contexts = message.contexts
|
19
|
+
storage.state = 'new'
|
20
|
+
message.attributes.each do |key, value|
|
21
|
+
storage.elements.build(key: key, value: value)
|
22
|
+
end
|
23
|
+
storage.save!
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.search(params = {})
|
27
|
+
search_text(params[:q]).
|
28
|
+
search_state(params[:state]).
|
29
|
+
ordered
|
30
|
+
end
|
31
|
+
|
32
|
+
# Search the message and its elements for the matching text.
|
33
|
+
scope :search_text, lambda { |text|
|
34
|
+
if text
|
35
|
+
where(
|
36
|
+
arel_table[:contexts].matches("#{text}%").
|
37
|
+
or(
|
38
|
+
arel_table[:id].
|
39
|
+
in(Arel::SqlLiteral.new(DatabaseChannelMessageElement.search_text(text).select(:message_id).to_sql))
|
40
|
+
)
|
41
|
+
)
|
42
|
+
else
|
43
|
+
all
|
44
|
+
end
|
45
|
+
}
|
46
|
+
|
47
|
+
scope :search_state, ->(state) { state ? where(arel_table[:state].eq(state)) : all }
|
48
|
+
|
49
|
+
scope :ordered, -> { order(arel_table[:created_at].desc) }
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'harbinger/database_channel_message'
|
3
|
+
|
4
|
+
module Harbinger
|
5
|
+
class DatabaseChannelMessageElement < ActiveRecord::Base
|
6
|
+
self.table_name = 'harbinger_message_elements'
|
7
|
+
belongs_to :message, class_name: 'Harbinger::DatabaseChannelMessage', foreign_key: :message_id
|
8
|
+
serialize :value
|
9
|
+
|
10
|
+
scope :search_text, lambda { |text|
|
11
|
+
if text
|
12
|
+
# Using %text% because I am serializing the data.
|
13
|
+
where(arel_table[:value].matches("%#{text}%"))
|
14
|
+
else
|
15
|
+
self
|
16
|
+
end
|
17
|
+
}
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
<%= form_tag harbinger.messages_path, :method => :get, :class => "search-form" do %>
|
2
|
+
<fieldset>
|
3
|
+
<legend class="accessible-hidden">Search Messages</legend>
|
4
|
+
<%= label_tag :message_search, "Search Text", :class => "accessible-hidden" %>
|
5
|
+
<%= text_field_tag(
|
6
|
+
:q,
|
7
|
+
params[:q],
|
8
|
+
:class => "q search-query",
|
9
|
+
:id => "message_search",
|
10
|
+
:placeholder => "Search text of messages",
|
11
|
+
:size => "30",
|
12
|
+
:tabindex => "1",
|
13
|
+
:type => "search",
|
14
|
+
)%><button type="submit" class="search-submit btn btn-primary" id="keyword-search-submit" tabindex="2">
|
15
|
+
<i class="icon-search icon-white"></i><span class="accessible-hidden">Search</span>
|
16
|
+
</button>
|
17
|
+
</fieldset>
|
18
|
+
<% end %>
|
19
|
+
|
20
|
+
<table>
|
21
|
+
<caption>Messages</caption>
|
22
|
+
<thead>
|
23
|
+
<tr>
|
24
|
+
<th>Created At</th>
|
25
|
+
<th>Contexts</th>
|
26
|
+
<th>State</th>
|
27
|
+
<th>Actions</th>
|
28
|
+
</tr>
|
29
|
+
</thead>
|
30
|
+
<tbody>
|
31
|
+
<% messages.each do |message| %>
|
32
|
+
<tr class="message">
|
33
|
+
<td class="detail message-created-at-detail"><a href="<%= harbinger.message_path(message.to_param)%>"><time><%= message.created_at %></time></a></td>
|
34
|
+
<td>
|
35
|
+
<ul><% message.contexts.each do |context| %>
|
36
|
+
<li class="detail message-contexts-detail"><%= context %></li>
|
37
|
+
<% end %></ul>
|
38
|
+
</td>
|
39
|
+
<td class="detail message-state-detail"><%= message.state %></td>
|
40
|
+
<td class="actions"> </td>
|
41
|
+
<% end %>
|
42
|
+
</tbody>
|
43
|
+
</table>
|
@@ -0,0 +1,24 @@
|
|
1
|
+
<article class="message">
|
2
|
+
<header class="message-header">
|
3
|
+
<h2>Message</h2>
|
4
|
+
<dl>
|
5
|
+
<dt class="term message-contexts-term">Contexts</dt>
|
6
|
+
<% message.contexts.each do |context| %>
|
7
|
+
<dd class="detail message-contexts-detail"><%= context %></dd>
|
8
|
+
<% end %>
|
9
|
+
<dt class="term message-state-term">State</dt>
|
10
|
+
<dd class="detail message-state-detail"><%= message.state %></dd>
|
11
|
+
<dt class="term message-created-at-term">Created at</dt>
|
12
|
+
<dd class="detail message-created-at-detail"><time><%= message.created_at %></time></dd>
|
13
|
+
</dl>
|
14
|
+
</header>
|
15
|
+
<section class="message-body">
|
16
|
+
<dl class="message-elements">
|
17
|
+
<% message.elements.each do |element| %>
|
18
|
+
<% dom_class_prefix = element.key.gsub(/_+/, '-') %>
|
19
|
+
<dt class="term <%= dom_class_prefix %>-term"><%= element.key %></dt>
|
20
|
+
<dd class="detail <%= dom_class_prefix %>-detail"><%= element.value %></dd>
|
21
|
+
<% end %>
|
22
|
+
</dl>
|
23
|
+
</section>
|
24
|
+
</article>
|
data/config/routes.rb
CHANGED
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateHarbingerDatabaseChannelMessage < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :harbinger_messages do |t|
|
4
|
+
t.string :contexts
|
5
|
+
t.string :state, limit: 32
|
6
|
+
t.timestamps
|
7
|
+
end
|
8
|
+
add_index :harbinger_messages, :state
|
9
|
+
add_index :harbinger_messages, :contexts
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class CreateHarbingerDatabaseChannelMessageElements < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :harbinger_message_elements do |t|
|
4
|
+
t.integer :message_id
|
5
|
+
t.string :key
|
6
|
+
t.text :value, limit: 2147483647
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
add_index :harbinger_message_elements, :message_id
|
10
|
+
add_index :harbinger_message_elements, :key
|
11
|
+
add_index :harbinger_message_elements, [:message_id, :key]
|
12
|
+
add_index :harbinger_message_elements, [:message_id, :value]
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
file = File.expand_path("../../Gemfile", __FILE__)
|
4
|
+
|
5
|
+
if File.exists?(file)
|
6
|
+
puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
|
7
|
+
instance_eval File.read(file)
|
8
|
+
end
|
9
|
+
gem 'sass', '~> 3.2.15'
|
10
|
+
gem 'sprockets', '~> 2.11.0'
|
11
|
+
|
12
|
+
gem 'rails', '4.1.0'
|
@@ -0,0 +1,13 @@
|
|
1
|
+
source 'http://rubygems.org'
|
2
|
+
|
3
|
+
file = File.expand_path("../../Gemfile", __FILE__)
|
4
|
+
|
5
|
+
if File.exists?(file)
|
6
|
+
puts "Loading #{file} ..." if $DEBUG # `ruby -d` or `bundle -v`
|
7
|
+
instance_eval File.read(file)
|
8
|
+
end
|
9
|
+
|
10
|
+
gem 'sass', '~> 3.2.15'
|
11
|
+
gem 'sprockets', '~> 2.11.0'
|
12
|
+
|
13
|
+
gem 'rails', '4.0.3'
|
data/harbinger.gemspec
CHANGED
@@ -6,18 +6,33 @@ require 'harbinger/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = "harbinger"
|
8
8
|
spec.version = Harbinger::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
|
11
|
-
|
12
|
-
spec.
|
13
|
-
|
9
|
+
spec.authors = [
|
10
|
+
"Jeremy Friesen"
|
11
|
+
]
|
12
|
+
spec.email = [
|
13
|
+
"jeremy.n.friesen@gmail.com"
|
14
|
+
]
|
15
|
+
spec.summary = %q{A Rails engine for arbitrary message creation and delivery.}
|
16
|
+
spec.description = %q{A Rails engine for arbitrary message creation and delivery.}
|
17
|
+
spec.homepage = "https://github.com/ndlib/harbinger"
|
14
18
|
spec.license = "Apache2"
|
15
19
|
|
16
20
|
spec.files = `git ls-files -z`.split("\x0")
|
17
|
-
spec.executables = spec.files.grep(%r{^bin/})
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) do |f|
|
22
|
+
f == 'bin/rails' ? nil : File.basename(f)
|
23
|
+
end.compact
|
18
24
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
25
|
spec.require_paths = ["lib"]
|
20
26
|
|
21
27
|
spec.add_development_dependency "bundler", "~> 1.5"
|
22
|
-
spec.add_development_dependency "rake"
|
28
|
+
spec.add_development_dependency "rake", '~> 10.3'
|
29
|
+
spec.add_development_dependency "rspec-given", '~> 3.5'
|
30
|
+
spec.add_development_dependency 'rspec-rails', '~> 3.0'
|
31
|
+
spec.add_development_dependency 'rspec-html-matchers', '~>0.6'
|
32
|
+
spec.add_development_dependency "engine_cart", '~> 0.3'
|
33
|
+
spec.add_development_dependency "rails", "~> 4.0"
|
34
|
+
spec.add_development_dependency 'sqlite3', '~> 1.3'
|
35
|
+
spec.add_development_dependency 'database_cleaner', '~> 1.3'
|
36
|
+
spec.add_development_dependency 'capybara', '~> 2.4'
|
37
|
+
|
23
38
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
module Harbinger
|
3
|
+
class InstallGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('../templates', __FILE__)
|
5
|
+
|
6
|
+
class_option :skip_database_channel, default: false, type: :boolean
|
7
|
+
|
8
|
+
def install_database_channel
|
9
|
+
if ! options[:skip_database_channel]
|
10
|
+
rake 'harbinger:install:migrations'
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def install_configuration_file
|
15
|
+
template "harbinger_initializer.rb.erb", "config/initializers/harbinger_initializer.rb"
|
16
|
+
end
|
17
|
+
|
18
|
+
def install_routes
|
19
|
+
route 'mount Harbinger::Engine => "/harbinger"'
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|