rails-alerter 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +36 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Appraisals +11 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/LICENSE.txt +22 -0
- data/README.md +111 -0
- data/Rakefile +10 -0
- data/alerter.gemspec +60 -0
- data/app/builders/alerter/base_builder.rb +26 -0
- data/app/builders/alerter/message_builder.rb +16 -0
- data/app/builders/alerter/receipt_builder.rb +13 -0
- data/app/mailers/alerter/base_mailer.rb +15 -0
- data/app/mailers/alerter/message_mailer.rb +12 -0
- data/app/models/alerter/mailbox.rb +74 -0
- data/app/models/alerter/message.rb +171 -0
- data/app/models/alerter/notification_type.rb +20 -0
- data/app/models/alerter/preference.rb +19 -0
- data/app/models/alerter/receipt.rb +89 -0
- data/app/views/alerter/message_mailer/new_message_email.html.erb +20 -0
- data/app/views/alerter/message_mailer/new_message_email.text.erb +10 -0
- data/db/migrate/20150821000000_create_alerter.rb +65 -0
- data/lib/alerter/cleaner.rb +9 -0
- data/lib/alerter/engine.rb +31 -0
- data/lib/alerter/message_dispatcher.rb +62 -0
- data/lib/alerter/models/notifiable.rb +174 -0
- data/lib/alerter/version.rb +3 -0
- data/lib/generators/alerter/install_generator.rb +37 -0
- data/lib/generators/alerter/namespacing_compatibility_generator.rb +24 -0
- data/lib/generators/alerter/templates/initializer.rb +29 -0
- data/lib/generators/alerter/views_generator.rb +11 -0
- data/lib/rails-alerter.rb +71 -0
- data/spec/alerter/message_dispatcher_spec.rb +101 -0
- data/spec/alerter_spec.rb +8 -0
- data/spec/dummy/.gitignore +5 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/controllers/home_controller.rb +4 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/models/cylon.rb +4 -0
- data/spec/dummy/app/models/duck.rb +3 -0
- data/spec/dummy/app/models/user.rb +3 -0
- data/spec/dummy/app/views/home/index.html.haml +7 -0
- data/spec/dummy/app/views/layouts/application.html.haml +11 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +47 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +24 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +29 -0
- data/spec/dummy/config/environments/production.rb +51 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/alerter.rb +26 -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/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +58 -0
- data/spec/dummy/config/sunspot.yml +17 -0
- data/spec/dummy/db/migrate/20110228120600_create_users.rb +14 -0
- data/spec/dummy/db/migrate/20110306002940_create_ducks.rb +14 -0
- data/spec/dummy/db/migrate/20110306015107_create_cylons.rb +14 -0
- data/spec/dummy/db/schema.rb +77 -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/public/index.html +239 -0
- data/spec/dummy/public/robots.txt +5 -0
- data/spec/dummy/public/uploads/testfile.txt +1 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/factories/general.rb +37 -0
- data/spec/integration/message_and_receipt_spec.rb +42 -0
- data/spec/integration/navigation_spec.rb +8 -0
- data/spec/mailers/message_mailer_spec.rb +44 -0
- data/spec/models/alerter_models_messageable_spec.rb +311 -0
- data/spec/models/mailbox_spec.rb +55 -0
- data/spec/models/message_spec.rb +51 -0
- data/spec/models/preference_spec.rb +35 -0
- data/spec/models/receipt_spec.rb +61 -0
- data/spec/spec_helper.rb +54 -0
- metadata +330 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: db24886c3f4f6847206d8e304b4491437247e1dc
|
4
|
+
data.tar.gz: 3ec40c899dbb26fdbe9862807ab96942a0733b42
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e0ee4dc0c52768b6cd8d5a4984befde66bd1e8ddd163e8b0bc379f5129c0d4cf01d5191906f0c2e8ebfb221141821814cff8024a31e2f6d9884cffa4bf31e7e7
|
7
|
+
data.tar.gz: 6030051ea01e9c5369d501c8d0e8d15c5be5909f083450b588216b75c49f95faee8f99fd0a160c448730e57352501bcfcc351113e3a450224fa75658dcf1335d
|
data/.gitignore
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/dummy/db/*.sqlite3
|
15
|
+
spec/dummy/log/*.log
|
16
|
+
spec/dummy/log/*.lck
|
17
|
+
spec/dummy/tmp/
|
18
|
+
spec/dummy/.project
|
19
|
+
spec/reports
|
20
|
+
test/tmp
|
21
|
+
test/version_tmp
|
22
|
+
tmp
|
23
|
+
*.bundle
|
24
|
+
*.so
|
25
|
+
*.o
|
26
|
+
*.a
|
27
|
+
mkmf.log
|
28
|
+
log/*.log
|
29
|
+
pkg/
|
30
|
+
**.tmp_*
|
31
|
+
.idea
|
32
|
+
.project
|
33
|
+
.document
|
34
|
+
.settings/
|
35
|
+
.yardoc/*
|
36
|
+
.rvmrc
|
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
alerter
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.2
|
data/Appraisals
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 DaKaZ
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
22
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Michael Kazmier
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# Alerter
|
2
|
+
|
3
|
+
### This is ALPHA software
|
4
|
+
|
5
|
+
We are still building out the mailers for everything except email...
|
6
|
+
|
7
|
+
## Overview
|
8
|
+
|
9
|
+
Alerter is a Rails gem for managing notifications within a standard Rails app. Notifications are getting more complex
|
10
|
+
as applications now typically have multiple faces (web, mobile, social, etc) and users are requesting varying
|
11
|
+
delivery methods for notifications (in-app, mobile push, email, text, social, etc).
|
12
|
+
|
13
|
+
Alter is largely derived from the outstanding Mailboxer app (https://github.com/mailboxer/mailboxer) but solving a
|
14
|
+
very different problem: Notifications
|
15
|
+
|
16
|
+
Alerter provides 4 basic functions:
|
17
|
+
* Extend your user object with acts-as-notifiable
|
18
|
+
* Manage multiple Notification Types (these are application specific, ex: Billing, Support, Sales, Achievement, etc)
|
19
|
+
* Send and track delivery state for the notification providing a full audit history
|
20
|
+
* Provide notification preferences for users
|
21
|
+
* Allow users to specify which methods (email, text, none, etc) they would like be notified on for different Notification Types
|
22
|
+
|
23
|
+
|
24
|
+
We need help with testing and documention, please jump in!
|
25
|
+
|
26
|
+
## Installation
|
27
|
+
|
28
|
+
Add this line to your application's Gemfile:
|
29
|
+
|
30
|
+
gem 'rails-alerter'
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
$ bundle install
|
35
|
+
|
36
|
+
Run the install script
|
37
|
+
|
38
|
+
$ rails g alerter:install
|
39
|
+
|
40
|
+
Migrate the DB
|
41
|
+
|
42
|
+
$ rake db:migrate
|
43
|
+
|
44
|
+
Generate your own email templates
|
45
|
+
|
46
|
+
$ rails g alerter:views
|
47
|
+
|
48
|
+
In your model:
|
49
|
+
|
50
|
+
class User < ActiveRecord::Base
|
51
|
+
acts_as_notifiable
|
52
|
+
end
|
53
|
+
|
54
|
+
## Usage
|
55
|
+
|
56
|
+
Alerter uses a `Alerter::Message` object that requires a short_msg and long_msg to accomidate different delivery
|
57
|
+
platforms. You can configure the maximum length of these in the initializer:
|
58
|
+
|
59
|
+
Alerter.setup do |config|
|
60
|
+
config.short_msg_length = 144 # Up to String length for your DB
|
61
|
+
config.long_msg_length = 512 # Any length you want
|
62
|
+
...
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
How can I send a message to a user?
|
67
|
+
|
68
|
+
#Send a message to beta
|
69
|
+
Alerter::Message.notify_all(beta, "Short Message", "Long Message")
|
70
|
+
|
71
|
+
Or you can include an array of recipients
|
72
|
+
|
73
|
+
recipients = [ alice, john, steve ]
|
74
|
+
Alerter::Message.notify_all(recipients, "Short Message", "Long Message")
|
75
|
+
|
76
|
+
How can I retrieve my conversations?
|
77
|
+
|
78
|
+
#alfa wants to retrieve all his messages (read and unread)
|
79
|
+
alfa.mailbox.inbox
|
80
|
+
|
81
|
+
#alfa gets the last message (chronologically, the first in the inbox)
|
82
|
+
message = alfa.mailbox.inbox.first
|
83
|
+
|
84
|
+
# get only unread messages
|
85
|
+
alfa.mailbox.inbox.unread
|
86
|
+
|
87
|
+
You can mark a message as read, unread or deleted or one or more users
|
88
|
+
|
89
|
+
# for user alpha
|
90
|
+
message.mark_as_read(alfa)
|
91
|
+
message.mark_as_unread(alfa)
|
92
|
+
message.mark_as_deleted(alfa)
|
93
|
+
|
94
|
+
|
95
|
+
You can change the way in which emails, SMS or mobile Push notifications are delivered by specifying a custom implementation of the mailer
|
96
|
+
|
97
|
+
Alerter.setup do |config|
|
98
|
+
config.email_message_mailer = CustomEmailMessageMailer
|
99
|
+
config.sms_message_mailer = CustomSmsMessageMailer
|
100
|
+
config.push_message_mailer = CustomPushMessageMailer
|
101
|
+
...
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
## Contributing
|
106
|
+
|
107
|
+
1. Fork it ( https://github.com/[my-github-username]/alerter/fork )
|
108
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
109
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
110
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
111
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/alerter.gemspec
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'alerter/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "rails-alerter"
|
8
|
+
s.version = Alerter::VERSION
|
9
|
+
|
10
|
+
s.authors = ["Michael Kazmier"]
|
11
|
+
s.summary = "Messaging system for rails apps."
|
12
|
+
s.description = "Many apps need to send basic notifications to users, often using multiple " +
|
13
|
+
"delivery methods (like email, push notifications, SMS, twitter, etc). This gem is designed to " +
|
14
|
+
"make that process easy and track the state of the notification in a centralized fashion."
|
15
|
+
s.email = [ "dakazmier@gmail.com" ]
|
16
|
+
s.homepage = "https://github.com/DaKaZ/alerter"
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.license = 'MIT'
|
19
|
+
|
20
|
+
# Gem dependencies
|
21
|
+
#
|
22
|
+
# SQL foreign keys
|
23
|
+
s.add_runtime_dependency('foreigner', '>= 0.9.1')
|
24
|
+
|
25
|
+
# Development Gem dependencies
|
26
|
+
s.add_runtime_dependency('rails', '>= 3.2.0')
|
27
|
+
|
28
|
+
if RUBY_ENGINE == "rbx" && RUBY_VERSION >= "2.1.0"
|
29
|
+
# Rubinius has it's own dependencies
|
30
|
+
s.add_runtime_dependency 'rubysl'
|
31
|
+
s.add_development_dependency 'racc'
|
32
|
+
end
|
33
|
+
# Specs
|
34
|
+
s.add_development_dependency 'pry'
|
35
|
+
s.add_development_dependency 'pry-rails'
|
36
|
+
s.add_development_dependency 'rspec-rails', '~> 3.0'
|
37
|
+
s.add_development_dependency 'rspec-its', '~> 1.1'
|
38
|
+
s.add_development_dependency 'rspec-collection_matchers', '~> 1.1'
|
39
|
+
s.add_development_dependency('appraisal', '~> 1.0.0')
|
40
|
+
s.add_development_dependency('shoulda-matchers')
|
41
|
+
s.add_development_dependency('simplecov')
|
42
|
+
# Fixtures
|
43
|
+
#if RUBY_VERSION >= '1.9.2'
|
44
|
+
# s.add_development_dependency('factory_girl', '>= 3.0.0')
|
45
|
+
#else
|
46
|
+
#s.add_development_dependency('factory_girl', '~> 2.6.0')
|
47
|
+
#end
|
48
|
+
s.add_development_dependency('factory_girl', '~> 3.3.0')
|
49
|
+
# Population
|
50
|
+
s.add_development_dependency('forgery', '>= 0.3.6')
|
51
|
+
# Integration testing
|
52
|
+
s.add_development_dependency('capybara', '>= 0.3.9')
|
53
|
+
# Testing database
|
54
|
+
if RUBY_PLATFORM == 'java'
|
55
|
+
s.add_development_dependency('jdbc-sqlite3')
|
56
|
+
s.add_development_dependency('activerecord-jdbcsqlite3-adapter', '1.3.0.rc1')
|
57
|
+
else
|
58
|
+
s.add_development_dependency('sqlite3')
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
class Alerter::BaseBuilder
|
2
|
+
|
3
|
+
attr_reader :params
|
4
|
+
|
5
|
+
def initialize(params)
|
6
|
+
@params = params.with_indifferent_access
|
7
|
+
end
|
8
|
+
|
9
|
+
def build
|
10
|
+
klass.new.tap do |object|
|
11
|
+
params.keys.each do |field|
|
12
|
+
object.send("#{field}=", get(field)) unless get(field).nil?
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def get(key)
|
20
|
+
respond_to?(key, true) ? send(key) : params[key]
|
21
|
+
end
|
22
|
+
|
23
|
+
def recipients
|
24
|
+
Array(params[:recipients]).uniq
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Alerter::BaseMailer < ActionMailer::Base
|
2
|
+
default :from => Alerter.default_from
|
3
|
+
|
4
|
+
private
|
5
|
+
|
6
|
+
def set_subject()
|
7
|
+
@subject ||= Alerter.default_subject
|
8
|
+
strip_tags(@subject)
|
9
|
+
end
|
10
|
+
|
11
|
+
def strip_tags(text)
|
12
|
+
::Alerter::Cleaner.instance.strip_tags(text)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
class Alerter::MessageMailer < Alerter::BaseMailer
|
2
|
+
#Sends and email with the message
|
3
|
+
def send_email(message, receiver)
|
4
|
+
@message = message
|
5
|
+
@receiver = receiver
|
6
|
+
set_subject
|
7
|
+
mail :to => receiver.send(Alerter.email_method),
|
8
|
+
:subject => @subject,
|
9
|
+
:template_name => 'new_message_email'
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
class Alerter::Mailbox
|
2
|
+
attr_reader :notifiable
|
3
|
+
|
4
|
+
#Initializer method
|
5
|
+
def initialize(notifiable)
|
6
|
+
@notifiable = notifiable
|
7
|
+
end
|
8
|
+
|
9
|
+
#Returns the messages for the messageable
|
10
|
+
def all_messages(options = {})
|
11
|
+
#:type => nil is a hack not to give Messages as Notifications
|
12
|
+
messages = Alerter::Message.receipts(@notifiable).where(:type => nil).order("alerter_messages.created_at DESC")
|
13
|
+
if options[:read] == false || options[:unread]
|
14
|
+
messages = messages.unread
|
15
|
+
end
|
16
|
+
|
17
|
+
messages
|
18
|
+
end
|
19
|
+
|
20
|
+
#Returns the conversations for the messageable
|
21
|
+
#
|
22
|
+
#Options
|
23
|
+
#
|
24
|
+
#* :mailbox_type
|
25
|
+
# * "inbox"
|
26
|
+
# * "trash"
|
27
|
+
#
|
28
|
+
#* :read=false
|
29
|
+
#* :unread=true
|
30
|
+
#
|
31
|
+
def messages(options = {})
|
32
|
+
messages = get_messages(options[:mailbox_type])
|
33
|
+
|
34
|
+
if options[:read] == false || options[:unread]
|
35
|
+
messages = messages.unread #(notifiable)
|
36
|
+
end
|
37
|
+
|
38
|
+
messages
|
39
|
+
end
|
40
|
+
|
41
|
+
#Returns the messages in the inbox of notifiable
|
42
|
+
#
|
43
|
+
#Same as conversations({:mailbox_type => 'inbox'})
|
44
|
+
def inbox(options={})
|
45
|
+
options = options.merge(:mailbox_type => 'inbox')
|
46
|
+
messages(options)
|
47
|
+
end
|
48
|
+
|
49
|
+
def trash(options={})
|
50
|
+
options = options.merge(:mailbox_type => 'trash')
|
51
|
+
messages(options)
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
#Returns all the receipts of notifiable from Messages
|
56
|
+
def receipts(options = {})
|
57
|
+
Alerter::Receipt.where(options).recipient(notifiable)
|
58
|
+
end
|
59
|
+
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def get_messages(mailbox)
|
64
|
+
case mailbox
|
65
|
+
when 'inbox'
|
66
|
+
Alerter::Message.inbox(notifiable)
|
67
|
+
when 'trash'
|
68
|
+
Alerter::Message.trash(notifiable)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
end
|