notifly 0.0.3
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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +197 -0
- data/Rakefile +23 -0
- data/app/assets/javascripts/notifly.js +5 -0
- data/app/assets/javascripts/notifly/counter.js.erb +23 -0
- data/app/assets/javascripts/notifly/get_notifications.js.erb +26 -0
- data/app/assets/stylesheets/notifly.css +5 -0
- data/app/assets/stylesheets/notifly/layout.css +50 -0
- data/app/controllers/notifly/application_controller.rb +4 -0
- data/app/controllers/notifly/notifications_controller.rb +36 -0
- data/app/helpers/notifly/view_helper.rb +17 -0
- data/app/models/notifly/notification.rb +27 -0
- data/app/views/notifly/layouts/_actions.html.erb +4 -0
- data/app/views/notifly/layouts/_counter.html.erb +4 -0
- data/app/views/notifly/layouts/_footer.html.erb +3 -0
- data/app/views/notifly/layouts/_index.html.erb +6 -0
- data/app/views/notifly/layouts/_notification.html.erb +12 -0
- data/app/views/notifly/layouts/_notifly.html.erb +25 -0
- data/app/views/notifly/notifications/counter.js.erb +2 -0
- data/app/views/notifly/notifications/index.js.erb +20 -0
- data/app/views/notifly/notifications/read_specific.js.erb +4 -0
- data/app/views/notifly/notifications/toggle_read.js.erb +5 -0
- data/app/views/notifly/templates/_default.html.erb +3 -0
- data/config/routes.rb +11 -0
- data/db/migrate/20141103170528_create_notifly_notifications.rb +13 -0
- data/db/migrate/20141104150224_add_data_to_notification.rb +5 -0
- data/db/migrate/20141117193436_add_seen_to_notifly_notification.rb +5 -0
- data/lib/generators/notifly/install/install_generator.rb +22 -0
- data/lib/generators/notifly/install/templates/config/initializers/notifly.rb +4 -0
- data/lib/generators/notifly/install/utils.rb +21 -0
- data/lib/generators/notifly/views/USAGE +8 -0
- data/lib/generators/notifly/views/views_generator.rb +22 -0
- data/lib/notifly.rb +24 -0
- data/lib/notifly/engine.rb +22 -0
- data/lib/notifly/models/base.rb +13 -0
- data/lib/notifly/models/flyable.rb +127 -0
- data/lib/notifly/models/notifiable.rb +11 -0
- data/lib/notifly/models/options/fly.rb +37 -0
- data/lib/notifly/railtie.rb +14 -0
- data/lib/notifly/version.rb +3 -0
- data/lib/tasks/notifly_tasks.rake +4 -0
- metadata +219 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 77982bba3899b260f37bc98c4a95a56658bef69c
|
4
|
+
data.tar.gz: 8645e6bae431b62fa69743e026687984da53528a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6b416b97f187382029ea72e5121a4d980872a50b6c3b1a1fb505904efdcb135e74e0231f2f9b99a49da15f4ff314cd779bdd3a766cf508b26a0f02ce5ecf8e56
|
7
|
+
data.tar.gz: eac2d4e0026728c73b91e74a05c7bb9bdd49f89d8c10466acf27b65194425d98283b92d50f7756d51a55450e3998d1a2e06e6e1b7d6c78b0a66b262efe0fe084
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,197 @@
|
|
1
|
+
# Notifly
|
2
|
+
|
3
|
+
This project intend to offer a full notification system, back and front-end.
|
4
|
+
In actual version, notifications are composed by:
|
5
|
+
|
6
|
+
- Receiver (**required**): the object that will receive the notification
|
7
|
+
- Sender: who sent the notification
|
8
|
+
- Target: object that you will be the subject between the sender and receiver
|
9
|
+
- Data: hash where you can store more info about the notification
|
10
|
+
- Template (**required**): html template used by notification
|
11
|
+
- Read: flag that records if the receiver read the notification
|
12
|
+
- Seen: flag that records if the receiver seen the notification
|
13
|
+
- If and Unless: used to create notifications conditionally
|
14
|
+
|
15
|
+
|
16
|
+
# Install
|
17
|
+
|
18
|
+
First we need to add the gem to `Gemfile`
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
gem 'notifly'
|
22
|
+
```
|
23
|
+
|
24
|
+
Run the bundle command to install it. After that, you need to run the initializer
|
25
|
+
|
26
|
+
```shell
|
27
|
+
$ rails generate notifly:install
|
28
|
+
```
|
29
|
+
|
30
|
+
You can choose to change the namespace for Notifly routes, but the default is `notifly`.
|
31
|
+
It will creates `config/initializers/notifly.rb` too. Also, in this file you can
|
32
|
+
see/change the default configs
|
33
|
+
|
34
|
+
Notifly **need** to storage the notifications and to do it you need to run the migrations
|
35
|
+
|
36
|
+
```shell
|
37
|
+
$ rake db:migrate
|
38
|
+
```
|
39
|
+
|
40
|
+
# Usage
|
41
|
+
|
42
|
+
## Back-end
|
43
|
+
|
44
|
+
We have two ways to create notifications:
|
45
|
+
|
46
|
+
#### 1. Using `#notifly` method in your classes (as callback)
|
47
|
+
|
48
|
+
If you want to create notifications after (or before) **any** method call.
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
class TicketOrder < ActiveRecord::Base
|
52
|
+
belongs_to :ticket
|
53
|
+
belongs_to :buyer
|
54
|
+
belongs_to :owner
|
55
|
+
|
56
|
+
notifly default_values: { receiver: :owner }
|
57
|
+
|
58
|
+
notifly before: :destroy, template: :destroy, sender: :buyer, data: :attributes
|
59
|
+
notifly after: :send_gift!, template: :ticket_gift, sender: :buyer,
|
60
|
+
target: :ticket, if: -> { completed? }
|
61
|
+
notifly after: :accept_gift, sender: :owner, receiver: :buyer, target: :ticket
|
62
|
+
|
63
|
+
def send_gift!
|
64
|
+
# code here
|
65
|
+
end
|
66
|
+
|
67
|
+
def accept_gift
|
68
|
+
# code here
|
69
|
+
end
|
70
|
+
end
|
71
|
+
```
|
72
|
+
Value explanation about each parameter:
|
73
|
+
|
74
|
+
| Parameter | Value |
|
75
|
+
| ------------------- | ------------- |
|
76
|
+
| `before` or `after` | The method which will create notification before or after its call |
|
77
|
+
| `receiver` | The method which returns the notification receiver object |
|
78
|
+
| `sender` | The method which returns the notification sender object |
|
79
|
+
| `template` | The symbol or string that indicates which partial will be rendered at views. The partial must be inside `app/views/notifly/templates/`. Default is `:default`. |
|
80
|
+
| `target` | The method which returns the notification target object. It's a third actor of the notification. Example: In "Max sent you a ticket" notification, Max is the sender, you are the receiver and the **ticket is the target**. |
|
81
|
+
| `data` | A method which returns a hash with usefull values to be persisted, like ticket price or whatever you want to persist. |
|
82
|
+
|
83
|
+
Note that you can use the `default_values` parameter, it is specific to DRY your
|
84
|
+
notiflies and set the values to all notiflies. If you need to overwrite some
|
85
|
+
default value, just declare it again like the `:accept_gift` notifly above.
|
86
|
+
|
87
|
+
|
88
|
+
#### Using `#notifly!` method on your receiver object
|
89
|
+
|
90
|
+
If you need to create notifications without callbacks, even in the
|
91
|
+
controller scope.
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
class TicketOrder < ActiveRecord::Base
|
95
|
+
belongs_to :ticket
|
96
|
+
belongs_to :buyer
|
97
|
+
belongs_to :owner
|
98
|
+
|
99
|
+
before_destroy do
|
100
|
+
owner.notifly! template: :destroy, sender: :buyer, data: :attributes
|
101
|
+
end
|
102
|
+
|
103
|
+
def send_gift!
|
104
|
+
# code here
|
105
|
+
|
106
|
+
if completed?
|
107
|
+
owner.notifly! template: :ticket_gift, sender: :buyer, target: :ticket
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def accept_gift
|
112
|
+
# code here
|
113
|
+
|
114
|
+
buyer.notifly! sender: :owner, target: :ticket
|
115
|
+
end
|
116
|
+
end
|
117
|
+
```
|
118
|
+
|
119
|
+
The receiver will be always the object which you call `#notifly!`
|
120
|
+
|
121
|
+
You can access the notifications using the following methods:
|
122
|
+
|
123
|
+
- `object.notiflies` (**TODO**)
|
124
|
+
- Querying `Notifly::Notifications`
|
125
|
+
- Using our front-end helpers
|
126
|
+
|
127
|
+
## Front-end
|
128
|
+
|
129
|
+
First, you need to have a `current_user` in `ApplicationController`, if you use
|
130
|
+
[Devise](https://github.com/plataformatec/devise) it is already there.
|
131
|
+
|
132
|
+
After that you need our assets, add them to your `application.js`
|
133
|
+
|
134
|
+
```javascript
|
135
|
+
//= require notifly
|
136
|
+
```
|
137
|
+
|
138
|
+
```css
|
139
|
+
/*
|
140
|
+
*= require notifly
|
141
|
+
*/
|
142
|
+
```
|
143
|
+
|
144
|
+
Now finally you can see the notifications view adding code bellow to your view
|
145
|
+
|
146
|
+
```html
|
147
|
+
<%= notiflies %>
|
148
|
+
```
|
149
|
+
|
150
|
+
This will inject our views and it will be like that
|
151
|
+
|
152
|
+

|
153
|
+
|
154
|
+
As you can see, notifications are rendered with their templates. It uses a simple
|
155
|
+
default template but if you want to change it or create new ones run the code below
|
156
|
+
or create them in `app/views/notifly/templates/_your_template.html.erb`
|
157
|
+
|
158
|
+
```shell
|
159
|
+
$ rails generate notifly:views
|
160
|
+
```
|
161
|
+
|
162
|
+
Now if you want to customize the layout, just generate it adding the option `--layout`
|
163
|
+
to the code above and change it as you want. But if you already have a layout and just
|
164
|
+
want add our features to it, take a look at [Adapting your layout](#adapting).
|
165
|
+
|
166
|
+
### <a name='adapting'></a> Adapting your layout
|
167
|
+
|
168
|
+
All partials that we insert in your layout are in the gem or if you generated them,
|
169
|
+
they will be in `app/views/notifly/layouts/`
|
170
|
+
|
171
|
+
Above are the elements that will loading the Notifly in your layout
|
172
|
+
|
173
|
+
- **Counter**: this element will show how many notifications are not seen. It
|
174
|
+
should have the id `#notifly-counter`, and the html rendered in will be
|
175
|
+
the `_counter.html.erb`
|
176
|
+
- **Notifications icon**: this element is the trigger to load the notifications
|
177
|
+
and you should have an icon to show when the user "have notifications" and
|
178
|
+
"do not have notifications" this element should have the id `#notifly-icon`. The
|
179
|
+
html icon is defined in our view helper `notifly_icon` you can overwrite it,
|
180
|
+
just remember that this method should have the argument `have_notifications=false`
|
181
|
+
and is this method that tell us which icon will be in the view.
|
182
|
+
- **Notifications**: they will be inserted in `#notifly-notifications-container`,
|
183
|
+
this element will contain all notifications (`_notification.html.erb`) rendered
|
184
|
+
by `_index.html.erb`
|
185
|
+
- **Next page link**: this link will append the next notifications page to the
|
186
|
+
`#notifly-notifications-container`, this is rendered by `_footer.html.erb` and
|
187
|
+
will be injected in `#notifly-notifications-footer`
|
188
|
+
- **Loading**: html element that will be showing while the notifications request
|
189
|
+
isn't completed. It should be in `#notifly-notifications-container` and should
|
190
|
+
have the class `loading`
|
191
|
+
- **Mark as read**: this link will mark all notifications in the page as read,
|
192
|
+
it will be rendered in `#notifly-header-actions`
|
193
|
+
- **Toggle read**: this link will be rendered by `_actions.html.erb' in
|
194
|
+
`_notification.html.erb`
|
195
|
+
|
196
|
+
|
197
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,23 @@
|
|
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
|
+
require 'rdoc/task'
|
8
|
+
|
9
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
10
|
+
rdoc.rdoc_dir = 'rdoc'
|
11
|
+
rdoc.title = 'Notifly'
|
12
|
+
rdoc.options << '--line-numbers'
|
13
|
+
rdoc.rdoc_files.include('README.rdoc')
|
14
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
15
|
+
end
|
16
|
+
|
17
|
+
APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
|
18
|
+
load 'rails/tasks/engine.rake'
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
Bundler::GemHelper.install_tasks
|
23
|
+
|
@@ -0,0 +1,23 @@
|
|
1
|
+
<% notifly = Notifly::Engine.routes.url_helpers %>
|
2
|
+
var notiflyCounterRequested;
|
3
|
+
|
4
|
+
var _notiflyGetCounter = function (notiflyPath) {
|
5
|
+
var $counter = $('#notifly-counter');
|
6
|
+
|
7
|
+
if (!notiflyCounterRequested) {
|
8
|
+
$.ajax({
|
9
|
+
url: '<%= notifly.counter_notifications_path %>',
|
10
|
+
type: 'GET',
|
11
|
+
error: _notiflyHandleCounterError
|
12
|
+
});
|
13
|
+
}
|
14
|
+
};
|
15
|
+
|
16
|
+
|
17
|
+
var _notiflyHandleCounterError = function () {
|
18
|
+
notiflyCounterRequested = false;
|
19
|
+
};
|
20
|
+
|
21
|
+
$(document).ready(function() {
|
22
|
+
_notiflyGetCounter();
|
23
|
+
});
|
@@ -0,0 +1,26 @@
|
|
1
|
+
<% notifly = Notifly::Engine.routes.url_helpers %>
|
2
|
+
var notiflyNotificationsRequested;
|
3
|
+
|
4
|
+
var _notiflyGetNotifications = function () {
|
5
|
+
var $notifly = $('#notifly-icon');
|
6
|
+
|
7
|
+
$notifly.click(function () {
|
8
|
+
if (!notiflyNotificationsRequested) {
|
9
|
+
notiflyNotificationsRequested = true;
|
10
|
+
|
11
|
+
$.ajax({
|
12
|
+
url: '<%= notifly.notifications_path %>',
|
13
|
+
type: 'GET',
|
14
|
+
error: _notiflyHandleNotificationsError
|
15
|
+
});
|
16
|
+
}
|
17
|
+
});
|
18
|
+
};
|
19
|
+
|
20
|
+
var _notiflyHandleNotificationsError = function () {
|
21
|
+
notiflyNotificationsRequested = false;
|
22
|
+
};
|
23
|
+
|
24
|
+
$(document).ready(function() {
|
25
|
+
_notiflyGetNotifications();
|
26
|
+
});
|
@@ -0,0 +1,50 @@
|
|
1
|
+
.notifly-counter-label {
|
2
|
+
padding: 3px 7px 3px 7px;
|
3
|
+
background: #cc0000;
|
4
|
+
color: #ffffff;
|
5
|
+
font-weight: bold;
|
6
|
+
border-radius: 9px;
|
7
|
+
-moz-border-radius: 9px;
|
8
|
+
-webkit-border-radius: 9px;
|
9
|
+
position: absolute;
|
10
|
+
margin-top: -8px;
|
11
|
+
margin-left: 12px;
|
12
|
+
font-size: 11px;
|
13
|
+
}
|
14
|
+
|
15
|
+
#notifly-notifications-panel {
|
16
|
+
padding: 0px !important;
|
17
|
+
border: 0px !important;
|
18
|
+
}
|
19
|
+
|
20
|
+
#notifly-notifications-header {
|
21
|
+
padding: 9px;
|
22
|
+
font-weight: bold;
|
23
|
+
font-size: 13px;
|
24
|
+
border-bottom: 1px solid #dddddd;
|
25
|
+
}
|
26
|
+
|
27
|
+
#notifly-notifications-footer {
|
28
|
+
padding: 9px;
|
29
|
+
text-align: center;
|
30
|
+
font-weight: bold;
|
31
|
+
font-size: 12px;
|
32
|
+
border-top: 1px solid #dddddd;
|
33
|
+
}
|
34
|
+
|
35
|
+
.notifly-notification-not-read {
|
36
|
+
background-color: #e9eaed;
|
37
|
+
}
|
38
|
+
|
39
|
+
.notifly-notification {
|
40
|
+
padding: 9px;
|
41
|
+
}
|
42
|
+
|
43
|
+
#notifly-notifications-panel {
|
44
|
+
width: 430px;
|
45
|
+
}
|
46
|
+
|
47
|
+
#notifly-notifications-content {
|
48
|
+
overflow-y: scroll;
|
49
|
+
max-height: 300px;
|
50
|
+
}
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require_dependency "notifly/application_controller"
|
2
|
+
|
3
|
+
module Notifly
|
4
|
+
class NotificationsController < ApplicationController
|
5
|
+
def counter
|
6
|
+
@counter = count_unseen
|
7
|
+
end
|
8
|
+
|
9
|
+
def index
|
10
|
+
@notifications = current_user_notifications.page(params[:page]).per(Notifly.per_page)
|
11
|
+
Notifly::Notification.where(id: @notifications.map(&:id)).update_all(seen: true)
|
12
|
+
@counter = count_unseen
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
def read_specific
|
17
|
+
size = params[:pages].to_i * Notifly.per_page
|
18
|
+
@notifications = current_user_notifications.limit(size)
|
19
|
+
@notifications.update_all read: true
|
20
|
+
end
|
21
|
+
|
22
|
+
def toggle_read
|
23
|
+
@notification = Notifly::Notification.find(params[:notification_id])
|
24
|
+
@notification.update(read: !@notification.read)
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def current_user_notifications
|
29
|
+
Notifly::Notification.all_from(current_user).order('created_at DESC')
|
30
|
+
end
|
31
|
+
|
32
|
+
def count_unseen
|
33
|
+
Notifly::Notification.unseen_from(current_user).count
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Notifly
|
2
|
+
module ViewHelper
|
3
|
+
def notiflies
|
4
|
+
notiflies_for current_user
|
5
|
+
end
|
6
|
+
|
7
|
+
def notiflies_for(receiver)
|
8
|
+
render partial: 'notifly/layouts/notifly', locals: { receiver: receiver }
|
9
|
+
end
|
10
|
+
|
11
|
+
def notifly_icon(have_notifications=false)
|
12
|
+
icon = have_notifications ? Notifly.icon : Notifly.icon_empty
|
13
|
+
size = Notifly.icon_size
|
14
|
+
fa_icon "#{icon} #{size}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Notifly
|
2
|
+
class Notification < ActiveRecord::Base
|
3
|
+
belongs_to :target, polymorphic: true
|
4
|
+
belongs_to :sender, polymorphic: true
|
5
|
+
belongs_to :receiver, polymorphic: true
|
6
|
+
|
7
|
+
before_validation :convert_data, :set_template
|
8
|
+
|
9
|
+
scope :all_from, ->(receiver) { where(receiver: receiver) }
|
10
|
+
scope :unseen_from, ->(receiver) { where(receiver: receiver, seen: false) }
|
11
|
+
|
12
|
+
validates :receiver, :template, presence: true
|
13
|
+
|
14
|
+
def data
|
15
|
+
YAML.load(read_attribute(:data))
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
def convert_data
|
20
|
+
self.data = read_attribute(:data).to_json
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_template
|
24
|
+
self.template ||= :default
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<!-- This partial requires the locals: notification -->
|
2
|
+
|
3
|
+
<% read = notification.read ? '' : 'notifly-notification-not-read' %>
|
4
|
+
|
5
|
+
<div id='<%= "notifly-notification-#{notification.id}" %>' class="notifly-notification <%= read %>">
|
6
|
+
<%= render partial: "notifly/templates/#{notification.template}",
|
7
|
+
locals: { notification: notification } %>
|
8
|
+
|
9
|
+
<span class="notifly-notification-actions">
|
10
|
+
<%= render partial: 'notifly/layouts/actions', locals: { notification: notification } %>
|
11
|
+
</span>
|
12
|
+
</div>
|
@@ -0,0 +1,25 @@
|
|
1
|
+
<!-- This partial requires the locals: receiver -->
|
2
|
+
|
3
|
+
<div id="notifly" class="dropdown">
|
4
|
+
|
5
|
+
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
|
6
|
+
<span id="notifly-counter"></span>
|
7
|
+
<span id="notifly-icon"><%= notifly_icon %></span>
|
8
|
+
</a>
|
9
|
+
|
10
|
+
|
11
|
+
<div id="notifly-notifications-panel" class="dropdown-menu">
|
12
|
+
<div id="notifly-notifications-header">
|
13
|
+
Notifications
|
14
|
+
|
15
|
+
<span id="notifly-header-actions" class="pull-right">
|
16
|
+
</span>
|
17
|
+
</div>
|
18
|
+
|
19
|
+
<div id="notifly-notifications-content">
|
20
|
+
<div class="loading">Loading notifications...</div>
|
21
|
+
</div>
|
22
|
+
|
23
|
+
<div id="notifly-notifications-footer"></div>
|
24
|
+
</div>
|
25
|
+
</div>
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<%
|
2
|
+
rendered_notifications = render partial: 'notifly/layouts/index',
|
3
|
+
locals: { notifications: @notifications }
|
4
|
+
|
5
|
+
notifications_pages = { pages: @notifications.current_page }
|
6
|
+
rendered_header_actions = link_to 'Mark as read', read_specific_notifications_path(notifications_pages),
|
7
|
+
remote: true, method: :put
|
8
|
+
|
9
|
+
rendered_footer = render partial: 'notifly/layouts/footer',
|
10
|
+
locals: { notifications: @notifications }
|
11
|
+
%>
|
12
|
+
|
13
|
+
$('#notifly-header-actions').html("<%= escape_javascript rendered_header_actions %>");
|
14
|
+
|
15
|
+
$('#notifly-notifications-content .loading').remove();
|
16
|
+
$('#notifly-notifications-content').append("<%= escape_javascript rendered_notifications %>");
|
17
|
+
$('#notifly-notifications-footer').html("<%= escape_javascript rendered_footer %>");
|
18
|
+
|
19
|
+
$('#notifly-counter').html("<%= escape_javascript(render 'notifly/layouts/counter', counter: @counter)%>");
|
20
|
+
$('#notifly-icon').html("<%= escape_javascript notifly_icon(@counter > 0) %>")
|
data/config/routes.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
class CreateNotiflyNotifications < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :notifly_notifications do |t|
|
4
|
+
t.string :template
|
5
|
+
t.boolean :read, default: false
|
6
|
+
t.references :target, index: true, polymorphic: true
|
7
|
+
t.references :sender, index: true, polymorphic: true
|
8
|
+
t.references :receiver, index: true, polymorphic: true
|
9
|
+
|
10
|
+
t.timestamps
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require File.expand_path('../utils', __FILE__)
|
2
|
+
|
3
|
+
module Notifly
|
4
|
+
module Generators
|
5
|
+
class InstallGenerator < Rails::Generators::Base
|
6
|
+
include Generators::Utils::InstanceMethods
|
7
|
+
source_root File.expand_path('../templates', __FILE__)
|
8
|
+
argument :namespace, type: :string, required: false, desc: 'Notifly url namespace'
|
9
|
+
|
10
|
+
desc 'Notifly installation generator'
|
11
|
+
|
12
|
+
def mount_engine
|
13
|
+
namespace = ask_for('Where do you want to mount Notifly?', 'notifly', namespace)
|
14
|
+
route("mount Notifly::Engine => '/#{namespace}', as: 'notifly'")
|
15
|
+
end
|
16
|
+
|
17
|
+
def copy_config
|
18
|
+
template "config/initializers/notifly.rb"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# github.com/sferik/rails_admin/blob/master/lib/generators/rails_admin/utils.rb
|
2
|
+
|
3
|
+
module Notifly
|
4
|
+
module Generators
|
5
|
+
module Utils
|
6
|
+
module InstanceMethods
|
7
|
+
def display(output, color = :green)
|
8
|
+
say(" - #{output}", color)
|
9
|
+
end
|
10
|
+
|
11
|
+
def ask_for(wording, default_value = nil, override_if_present_value = nil)
|
12
|
+
if override_if_present_value.present?
|
13
|
+
display("Using [#{override_if_present_value}] for question '#{wording}'") && override_if_present_value
|
14
|
+
else
|
15
|
+
ask(" ? #{wording} Press <enter> for [#{default_value}] >", :yellow).presence || default_value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Notifly
|
2
|
+
module Generators
|
3
|
+
class ViewsGenerator < Rails::Generators::Base
|
4
|
+
source_root File.expand_path('../../../../../app/views/notifly', __FILE__)
|
5
|
+
class_option :layout, type: :boolean, default: false, desc: 'Include layout files.'
|
6
|
+
|
7
|
+
def copy_views
|
8
|
+
copy_file 'templates/_default.html.erb', "#{main_app_path}/templates/_default.html.erb"
|
9
|
+
layout_files if options.layout?
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def main_app_path
|
14
|
+
'app/views/notifly'
|
15
|
+
end
|
16
|
+
|
17
|
+
def layout_files
|
18
|
+
directory 'layouts', "#{main_app_path}/layouts"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/notifly.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'notifly/engine'
|
2
|
+
require 'notifly/railtie'
|
3
|
+
require 'font-awesome-rails'
|
4
|
+
|
5
|
+
module Notifly
|
6
|
+
# How many notifications per page.
|
7
|
+
mattr_accessor :per_page
|
8
|
+
@@per_page = 10
|
9
|
+
|
10
|
+
mattr_accessor :icon_size
|
11
|
+
@@icon_size = '2x'
|
12
|
+
|
13
|
+
mattr_accessor :icon
|
14
|
+
@@icon = 'bell'
|
15
|
+
|
16
|
+
mattr_accessor :icon_empty
|
17
|
+
@@icon_empty = 'bell-o'
|
18
|
+
|
19
|
+
# Default way to setup Notifly. Run rails generate notifly:install to create
|
20
|
+
# a fresh initializer with all configuration values.
|
21
|
+
def self.setup
|
22
|
+
yield self
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'jquery-rails'
|
2
|
+
require 'kaminari'
|
3
|
+
|
4
|
+
module Notifly
|
5
|
+
class Engine < ::Rails::Engine
|
6
|
+
isolate_namespace Notifly
|
7
|
+
|
8
|
+
config.generators do |g|
|
9
|
+
g.test_framework :rspec
|
10
|
+
end
|
11
|
+
|
12
|
+
# http://pivotallabs.com/leave-your-migrations-in-your-rails-engines/
|
13
|
+
|
14
|
+
initializer :append_migrations do |app|
|
15
|
+
unless app.root.to_s.match root.to_s
|
16
|
+
config.paths["db/migrate"].expanded.each do |expanded_path|
|
17
|
+
app.config.paths["db/migrate"] << expanded_path
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
require_relative 'options/fly'
|
2
|
+
|
3
|
+
module Notifly
|
4
|
+
module Models
|
5
|
+
module Flyable
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
attr_reader :flies, :default_fly, :flyable_callbacks
|
10
|
+
|
11
|
+
def notifly(options = {})
|
12
|
+
@flies ||= []
|
13
|
+
@flyable_callbacks ||= []
|
14
|
+
|
15
|
+
fly = Notifly::Models::Options::Fly.new options
|
16
|
+
|
17
|
+
if options[:default_values]
|
18
|
+
@default_fly = fly
|
19
|
+
else
|
20
|
+
@flies << fly
|
21
|
+
|
22
|
+
if [:create, :save, :update, :destroy].include? fly.method_name
|
23
|
+
_create_callback_for_active_record_from fly
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_added(method_name)
|
29
|
+
_create_callbacks_for method_name
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
def _create_callbacks_for(method_name)
|
35
|
+
method_flies = _flies_for method_name
|
36
|
+
|
37
|
+
method_flies.each do |fly|
|
38
|
+
_create_callback_for_instance_method_from(fly)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def _flies_for(method_name)
|
43
|
+
if flies.present?
|
44
|
+
flies.select { |fly| fly.method_name == method_name }
|
45
|
+
else
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def _create_callback_for_active_record_from(fly)
|
51
|
+
callback_name = "#{fly.hook}_#{fly.method_name}"
|
52
|
+
flyable_callbacks << "#{callback_name}_#{fly.object_id}"
|
53
|
+
|
54
|
+
send(callback_name, if: fly.if, unless: fly.unless) do |record|
|
55
|
+
_create_notification_for(fly)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def _create_callback_for_instance_method_from(fly)
|
60
|
+
notifly_callback_name = _format_callback_name_for(fly)
|
61
|
+
|
62
|
+
if not flyable_callbacks.include? notifly_callback_name
|
63
|
+
flyable_callbacks << notifly_callback_name
|
64
|
+
|
65
|
+
define_callbacks notifly_callback_name
|
66
|
+
set_callback notifly_callback_name, fly.hook, if: fly.if, unless: fly.unless do |record|
|
67
|
+
_create_notification_for(fly)
|
68
|
+
end
|
69
|
+
|
70
|
+
old_method = instance_method(fly.method_name)
|
71
|
+
|
72
|
+
define_method(fly.method_name) do |*args|
|
73
|
+
run_callbacks(notifly_callback_name) do
|
74
|
+
old_method.bind(self).call(*args)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def _format_callback_name_for(fly)
|
81
|
+
ending_chars = {
|
82
|
+
'!' => :_dangerous,
|
83
|
+
'?' => :_question
|
84
|
+
}
|
85
|
+
|
86
|
+
method_name = fly.method_name.to_s.gsub(/(?<char>[\?|\!])/, ending_chars)
|
87
|
+
|
88
|
+
"notifly_#{fly.hook}_#{method_name}_#{fly.object_id}"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def _create_notification_for(fly)
|
93
|
+
new_fly = _default_fly.merge(fly)
|
94
|
+
Notifly::Notification.create! _get_attributes_from(new_fly)
|
95
|
+
end
|
96
|
+
|
97
|
+
def notifly_notifications(options={})
|
98
|
+
Notifly::Notification.where(receiver: self)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
def _default_fly
|
103
|
+
self.class.default_fly || Notifly::Models::Options::Fly.new
|
104
|
+
end
|
105
|
+
|
106
|
+
def _get_attributes_from(fly)
|
107
|
+
evaluated_attributes = {}
|
108
|
+
|
109
|
+
fly.attributes.each do |key, value|
|
110
|
+
evaluated_attributes[key] = _eval_for(key, value)
|
111
|
+
end
|
112
|
+
|
113
|
+
evaluated_attributes
|
114
|
+
end
|
115
|
+
|
116
|
+
def _eval_for(key, value)
|
117
|
+
if key.to_sym == :template
|
118
|
+
value
|
119
|
+
elsif value == :self
|
120
|
+
self
|
121
|
+
else
|
122
|
+
send(value)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Notifly
|
2
|
+
module Models
|
3
|
+
module Options
|
4
|
+
class Fly
|
5
|
+
attr_accessor :before, :after, :template, :sender, :receiver, :target,
|
6
|
+
:if, :unless, :data
|
7
|
+
|
8
|
+
def initialize(options={})
|
9
|
+
options = options.fetch(:default_values, options)
|
10
|
+
options.each { |key, value| try "#{key}=", value }
|
11
|
+
end
|
12
|
+
|
13
|
+
def hook
|
14
|
+
if @before.nil?
|
15
|
+
:after
|
16
|
+
else
|
17
|
+
:before
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_name
|
22
|
+
self.send(hook)
|
23
|
+
end
|
24
|
+
|
25
|
+
def attributes
|
26
|
+
instance_values.reject { |key| [hook, :if, :unless].include? key.to_sym }
|
27
|
+
end
|
28
|
+
|
29
|
+
def merge(fly)
|
30
|
+
raise TypeError, "#{fly} is not a Fly" unless fly.is_a? self.class
|
31
|
+
|
32
|
+
Notifly::Models::Options::Fly.new attributes.merge(fly.attributes)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require_relative '../../app/helpers/notifly/view_helper'
|
2
|
+
require_relative 'models/base'
|
3
|
+
|
4
|
+
module Notifly
|
5
|
+
class Railtie < Rails::Railtie
|
6
|
+
initializer 'Notifly.view_helpers' do
|
7
|
+
ActionView::Base.send :include, ViewHelper
|
8
|
+
end
|
9
|
+
|
10
|
+
initializer 'Notifly.active_model_helpers' do
|
11
|
+
ActiveRecord::Base.send :include, Notifly::Models::Base
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
metadata
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: notifly
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Pedro Passalini
|
8
|
+
- Rafael Carvalho
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-11-20 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rails
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '4'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '4'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: jquery-rails
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '3.0'
|
35
|
+
- - "<"
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '5'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '3.0'
|
45
|
+
- - "<"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: kaminari
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.16.1
|
55
|
+
type: :runtime
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.16.1
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: font-awesome-rails
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 4.2.0
|
69
|
+
type: :runtime
|
70
|
+
prerelease: false
|
71
|
+
version_requirements: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.2.0
|
76
|
+
- !ruby/object:Gem::Dependency
|
77
|
+
name: pg
|
78
|
+
requirement: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
type: :development
|
84
|
+
prerelease: false
|
85
|
+
version_requirements: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
- !ruby/object:Gem::Dependency
|
91
|
+
name: rspec-rails
|
92
|
+
requirement: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 3.1.0
|
97
|
+
type: :development
|
98
|
+
prerelease: false
|
99
|
+
version_requirements: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 3.1.0
|
104
|
+
- !ruby/object:Gem::Dependency
|
105
|
+
name: capybara
|
106
|
+
requirement: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.4.4
|
111
|
+
type: :development
|
112
|
+
prerelease: false
|
113
|
+
version_requirements: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: 2.4.4
|
118
|
+
- !ruby/object:Gem::Dependency
|
119
|
+
name: poltergeist
|
120
|
+
requirement: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: 1.5.1
|
125
|
+
type: :development
|
126
|
+
prerelease: false
|
127
|
+
version_requirements: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: 1.5.1
|
132
|
+
- !ruby/object:Gem::Dependency
|
133
|
+
name: shoulda-matchers
|
134
|
+
requirement: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: 2.7.0
|
139
|
+
type: :development
|
140
|
+
prerelease: false
|
141
|
+
version_requirements: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: 2.7.0
|
146
|
+
description: This project intend to offer a full notification system, back and front-end.
|
147
|
+
email:
|
148
|
+
- henrique.passalini@gmail.com
|
149
|
+
- rafael@algorich.com.br
|
150
|
+
executables: []
|
151
|
+
extensions: []
|
152
|
+
extra_rdoc_files: []
|
153
|
+
files:
|
154
|
+
- MIT-LICENSE
|
155
|
+
- README.md
|
156
|
+
- Rakefile
|
157
|
+
- app/assets/javascripts/notifly.js
|
158
|
+
- app/assets/javascripts/notifly/counter.js.erb
|
159
|
+
- app/assets/javascripts/notifly/get_notifications.js.erb
|
160
|
+
- app/assets/stylesheets/notifly.css
|
161
|
+
- app/assets/stylesheets/notifly/layout.css
|
162
|
+
- app/controllers/notifly/application_controller.rb
|
163
|
+
- app/controllers/notifly/notifications_controller.rb
|
164
|
+
- app/helpers/notifly/view_helper.rb
|
165
|
+
- app/models/notifly/notification.rb
|
166
|
+
- app/views/notifly/layouts/_actions.html.erb
|
167
|
+
- app/views/notifly/layouts/_counter.html.erb
|
168
|
+
- app/views/notifly/layouts/_footer.html.erb
|
169
|
+
- app/views/notifly/layouts/_index.html.erb
|
170
|
+
- app/views/notifly/layouts/_notification.html.erb
|
171
|
+
- app/views/notifly/layouts/_notifly.html.erb
|
172
|
+
- app/views/notifly/notifications/counter.js.erb
|
173
|
+
- app/views/notifly/notifications/index.js.erb
|
174
|
+
- app/views/notifly/notifications/read_specific.js.erb
|
175
|
+
- app/views/notifly/notifications/toggle_read.js.erb
|
176
|
+
- app/views/notifly/templates/_default.html.erb
|
177
|
+
- config/routes.rb
|
178
|
+
- db/migrate/20141103170528_create_notifly_notifications.rb
|
179
|
+
- db/migrate/20141104150224_add_data_to_notification.rb
|
180
|
+
- db/migrate/20141117193436_add_seen_to_notifly_notification.rb
|
181
|
+
- lib/generators/notifly/install/install_generator.rb
|
182
|
+
- lib/generators/notifly/install/templates/config/initializers/notifly.rb
|
183
|
+
- lib/generators/notifly/install/utils.rb
|
184
|
+
- lib/generators/notifly/views/USAGE
|
185
|
+
- lib/generators/notifly/views/views_generator.rb
|
186
|
+
- lib/notifly.rb
|
187
|
+
- lib/notifly/engine.rb
|
188
|
+
- lib/notifly/models/base.rb
|
189
|
+
- lib/notifly/models/flyable.rb
|
190
|
+
- lib/notifly/models/notifiable.rb
|
191
|
+
- lib/notifly/models/options/fly.rb
|
192
|
+
- lib/notifly/railtie.rb
|
193
|
+
- lib/notifly/version.rb
|
194
|
+
- lib/tasks/notifly_tasks.rake
|
195
|
+
homepage: https://github.com/algorich/notifly
|
196
|
+
licenses:
|
197
|
+
- MIT
|
198
|
+
metadata: {}
|
199
|
+
post_install_message:
|
200
|
+
rdoc_options: []
|
201
|
+
require_paths:
|
202
|
+
- lib
|
203
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
204
|
+
requirements:
|
205
|
+
- - ">="
|
206
|
+
- !ruby/object:Gem::Version
|
207
|
+
version: '0'
|
208
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
209
|
+
requirements:
|
210
|
+
- - ">="
|
211
|
+
- !ruby/object:Gem::Version
|
212
|
+
version: '0'
|
213
|
+
requirements: []
|
214
|
+
rubyforge_project:
|
215
|
+
rubygems_version: 2.2.2
|
216
|
+
signing_key:
|
217
|
+
specification_version: 4
|
218
|
+
summary: A full notification system
|
219
|
+
test_files: []
|