noticent 0.0.1.pre.pre → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/Gemfile.lock +58 -0
- data/LICENSE +201 -0
- data/README.md +77 -12
- data/config.ru +9 -0
- data/lib/noticent.rb +2 -1
- data/lib/noticent/active_record_opt_in_provider.rb +10 -1
- data/lib/noticent/channel.rb +24 -31
- data/lib/noticent/config.rb +76 -17
- data/lib/noticent/definitions/alert.rb +80 -6
- data/lib/noticent/definitions/channel.rb +7 -2
- data/lib/noticent/definitions/scope.rb +10 -6
- data/lib/noticent/dispatcher.rb +16 -10
- data/lib/noticent/version.rb +1 -1
- data/lib/noticent/view.rb +16 -14
- data/noticent.gemspec +25 -21
- data/testing/channels/email.rb +1 -0
- data/testing/channels/exclusive.rb +9 -0
- data/testing/channels/simple.rb +9 -0
- data/testing/payloads/comment_payload.rb +10 -0
- data/testing/payloads/post_payload.rb +44 -0
- data/testing/views/email/some_event.html.erb +3 -0
- data/testing/views/email/some_event.txt.erb +2 -0
- data/testing/views/layouts/layout.html.erb +1 -1
- data/testing/views/simple/default.html.erb +1 -0
- metadata +66 -6
data/config.ru
ADDED
data/lib/noticent.rb
CHANGED
@@ -8,7 +8,7 @@ module Noticent
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def opt_out(recipient_id:, scope:, entity_id:, alert_name:, channel_name:)
|
11
|
-
Noticent::OptIn.where(recipient_id: recipient_id, scope: scope, entity_id: entity_id, alert_name: alert_name, channel_name: channel_name).
|
11
|
+
Noticent::OptIn.where(recipient_id: recipient_id, scope: scope, entity_id: entity_id, alert_name: alert_name, channel_name: channel_name).destroy_all
|
12
12
|
end
|
13
13
|
|
14
14
|
def opted_in?(recipient_id:, scope:, entity_id:, alert_name:, channel_name:)
|
@@ -33,5 +33,14 @@ module Noticent
|
|
33
33
|
def remove_alert(scope:, alert_name:)
|
34
34
|
Noticent::OptIn.where('scope = ? AND alert_name = ?', scope, alert_name).destroy_all
|
35
35
|
end
|
36
|
+
|
37
|
+
def remove_entity(scope:, entity_id:)
|
38
|
+
Noticent::OptIn.where('scope = ? AND entity_id = ?', scope, entity_id).destroy_all
|
39
|
+
end
|
40
|
+
|
41
|
+
def remove_recipient(recipient_id:)
|
42
|
+
Noticent::OptIn.where(recipient_id: recipient_id).destroy_all
|
43
|
+
end
|
44
|
+
|
36
45
|
end
|
37
46
|
end
|
data/lib/noticent/channel.rb
CHANGED
@@ -2,41 +2,30 @@
|
|
2
2
|
|
3
3
|
module Noticent
|
4
4
|
class Channel
|
5
|
-
|
6
|
-
|
5
|
+
class_attribute :default_ext, default: :erb
|
6
|
+
class_attribute :default_format, default: :html
|
7
7
|
|
8
|
-
|
8
|
+
attr_accessor :data
|
9
|
+
|
10
|
+
def initialize(config, recipients, payload, configuration)
|
9
11
|
@config = config
|
10
12
|
@recipients = recipients
|
11
13
|
@payload = payload
|
12
|
-
@
|
14
|
+
@configuration = configuration
|
13
15
|
@current_user = payload.current_user if payload.respond_to? :current_user
|
16
|
+
@routes = Rails.application.routes.url_helpers
|
14
17
|
end
|
15
18
|
|
16
|
-
def render_within_context(template
|
17
|
-
|
18
|
-
template.nil? ?
|
19
|
+
def render_within_context(template:, content:, context:)
|
20
|
+
@content = ERB.new(content).result(context)
|
21
|
+
template.nil? ? @content : ERB.new(template).result(binding)
|
19
22
|
end
|
20
23
|
|
21
24
|
protected
|
22
25
|
|
23
26
|
attr_reader :payload
|
24
27
|
attr_reader :recipients
|
25
|
-
attr_reader :
|
26
|
-
|
27
|
-
class << self
|
28
|
-
def default_format(format)
|
29
|
-
@@default_format = format
|
30
|
-
end
|
31
|
-
|
32
|
-
def default_ext(ext)
|
33
|
-
@@default_ext = ext
|
34
|
-
end
|
35
|
-
end
|
36
|
-
|
37
|
-
def get_binding
|
38
|
-
binding
|
39
|
-
end
|
28
|
+
attr_reader :configuration
|
40
29
|
|
41
30
|
def current_user
|
42
31
|
raise Noticent::NoCurrentUser if @current_user.nil?
|
@@ -44,15 +33,13 @@ module Noticent
|
|
44
33
|
@current_user
|
45
34
|
end
|
46
35
|
|
47
|
-
def render(format:
|
36
|
+
def render(format: default_format, ext: default_ext, layout: "")
|
48
37
|
alert_name = caller[0][/`.*'/][1..-2]
|
49
|
-
channel_name = self.class.name.split(
|
38
|
+
channel_name = self.class.name.split("::").last.underscore
|
50
39
|
view_filename, layout_filename = filenames(channel: channel_name, alert: alert_name, format: format, ext: ext, layout: layout)
|
51
40
|
|
52
|
-
raise Noticent::ViewNotFound, "view #{view_filename} not found" unless File.exist?(view_filename)
|
53
|
-
|
54
41
|
view = View.new(view_filename, template_filename: layout_filename, channel: self)
|
55
|
-
view.process
|
42
|
+
view.process(binding)
|
56
43
|
|
57
44
|
[view.data, view.content]
|
58
45
|
end
|
@@ -60,16 +47,22 @@ module Noticent
|
|
60
47
|
private
|
61
48
|
|
62
49
|
def view_file(channel:, alert:, format:, ext:)
|
63
|
-
File.join(@config.view_dir, channel, "#{alert}.#{format}.#{ext}")
|
50
|
+
view_filename = File.join(@config.view_dir, channel, "#{alert}.#{format}.#{ext}")
|
51
|
+
if !File.exist?(view_filename)
|
52
|
+
# no specific file found, use a convention
|
53
|
+
view_filename = File.join(@config.view_dir, channel, "default.#{format}.#{ext}")
|
54
|
+
raise Noticent::ViewNotFound, "view #{view_filename} not found" unless File.exist?(view_filename)
|
55
|
+
end
|
56
|
+
|
57
|
+
return view_filename
|
64
58
|
end
|
65
59
|
|
66
60
|
def filenames(channel:, alert:, format:, ext:, layout:)
|
67
61
|
view_filename = view_file(channel: channel, alert: alert, format: format, ext: ext)
|
68
|
-
layout_filename =
|
69
|
-
layout_filename = File.join(@config.view_dir,
|
62
|
+
layout_filename = ""
|
63
|
+
layout_filename = File.join(@config.view_dir, "layouts", "#{layout}.#{format}.#{ext}") unless layout == ""
|
70
64
|
|
71
65
|
return view_filename, layout_filename
|
72
66
|
end
|
73
|
-
|
74
67
|
end
|
75
68
|
end
|
data/lib/noticent/config.rb
CHANGED
@@ -2,17 +2,20 @@
|
|
2
2
|
|
3
3
|
module Noticent
|
4
4
|
def self.configure(options = {}, &block)
|
5
|
-
if ENV[
|
5
|
+
if ENV["NOTICENT_RSPEC"] == "1"
|
6
6
|
options = options.merge(
|
7
|
-
base_module_name:
|
7
|
+
base_module_name: "Noticent::Testing",
|
8
8
|
base_dir: File.expand_path("#{File.dirname(__FILE__)}/../../testing"),
|
9
|
-
halt_on_error: true
|
9
|
+
halt_on_error: true,
|
10
10
|
)
|
11
11
|
end
|
12
12
|
|
13
13
|
@config = Noticent::Config::Builder.new(options, &block).build
|
14
14
|
@config.validate!
|
15
15
|
|
16
|
+
# construct dynamics
|
17
|
+
@config.create_dynamics
|
18
|
+
|
16
19
|
@config
|
17
20
|
end
|
18
21
|
|
@@ -28,12 +31,38 @@ module Noticent
|
|
28
31
|
engine.dispatch
|
29
32
|
end
|
30
33
|
|
34
|
+
# recipient is the recipient object id
|
35
|
+
# entities is an array of all entity ids this recipient needs to opt in based on the alert defaults
|
36
|
+
# scope is the name of the scope these entities belong to
|
37
|
+
def self.setup_recipient(recipient_id:, scope:, entity_ids:)
|
38
|
+
raise ArgumentError, "no scope named '#{scope}' found" if @config.scopes[scope].nil?
|
39
|
+
|
40
|
+
alerts = @config.alerts_by_scope(scope)
|
41
|
+
|
42
|
+
alerts.each do |alert|
|
43
|
+
channels = @config.alert_channels(alert.name)
|
44
|
+
|
45
|
+
channels.each do |channel|
|
46
|
+
next unless alert.default_for(channel.name)
|
47
|
+
|
48
|
+
entity_ids.each do |entity_id|
|
49
|
+
@config.opt_in_provider.opt_in(recipient_id: recipient_id,
|
50
|
+
scope: scope,
|
51
|
+
entity_id: entity_id,
|
52
|
+
alert_name: alert.name,
|
53
|
+
channel_name: channel.name)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
31
59
|
class Config
|
32
60
|
attr_reader :hooks
|
33
61
|
attr_reader :channels
|
34
62
|
attr_reader :scopes
|
35
63
|
attr_reader :alerts
|
36
64
|
attr_reader :products
|
65
|
+
attr_reader :channel_groups
|
37
66
|
|
38
67
|
def initialize(options = {})
|
39
68
|
@options = options
|
@@ -46,12 +75,6 @@ module Noticent
|
|
46
75
|
@channels.values.select { |x| x.group == group }
|
47
76
|
end
|
48
77
|
|
49
|
-
def channel_groups
|
50
|
-
return [] if @channels.nil?
|
51
|
-
|
52
|
-
@channels.values.collect(&:group).uniq
|
53
|
-
end
|
54
|
-
|
55
78
|
def alert_channels(alert_name)
|
56
79
|
alert = @alerts[alert_name]
|
57
80
|
raise ArgumentError, "no alert #{alert_name} found" if alert.nil?
|
@@ -90,23 +113,46 @@ module Noticent
|
|
90
113
|
end
|
91
114
|
|
92
115
|
def halt_on_error
|
93
|
-
@options[:halt_on_error].nil?
|
116
|
+
@options[:halt_on_error].nil? ? false : @options[:halt_on_error]
|
117
|
+
end
|
118
|
+
|
119
|
+
def skip_alert_with_no_subscribers
|
120
|
+
@options[:skip_alert_with_no_subscribers].nil? ? false : @options[:skip_alert_with_no_subscribers]
|
121
|
+
end
|
122
|
+
|
123
|
+
def default_value
|
124
|
+
@options[:default_value].nil? ? false : @options[:default_value]
|
125
|
+
end
|
126
|
+
|
127
|
+
def use_sub_modules
|
128
|
+
@options[:use_sub_modules].nil? ? false : @options[:use_sub_modules]
|
94
129
|
end
|
95
130
|
|
96
131
|
def payload_dir
|
97
|
-
File.join(base_dir,
|
132
|
+
File.join(base_dir, "payloads")
|
98
133
|
end
|
99
134
|
|
100
135
|
def scope_dir
|
101
|
-
File.join(base_dir,
|
136
|
+
File.join(base_dir, "scopes")
|
102
137
|
end
|
103
138
|
|
104
139
|
def channel_dir
|
105
|
-
File.join(base_dir,
|
140
|
+
File.join(base_dir, "channels")
|
106
141
|
end
|
107
142
|
|
108
143
|
def view_dir
|
109
|
-
File.join(base_dir,
|
144
|
+
File.join(base_dir, "views")
|
145
|
+
end
|
146
|
+
|
147
|
+
def create_dynamics
|
148
|
+
return if alerts.nil?
|
149
|
+
|
150
|
+
alerts.keys.each do |alert|
|
151
|
+
const_name = "ALERT_#{alert.to_s.upcase}"
|
152
|
+
next if Noticent.const_defined?(const_name)
|
153
|
+
|
154
|
+
Noticent.const_set(const_name, alert)
|
155
|
+
end
|
110
156
|
end
|
111
157
|
|
112
158
|
def validate!
|
@@ -119,7 +165,7 @@ module Noticent
|
|
119
165
|
def initialize(options = {}, &block)
|
120
166
|
@options = options
|
121
167
|
@config = Noticent::Config.new(options)
|
122
|
-
raise BadConfiguration,
|
168
|
+
raise BadConfiguration, "no OptInProvider configured" if @config.opt_in_provider.nil?
|
123
169
|
|
124
170
|
instance_eval(&block) if block_given?
|
125
171
|
|
@@ -150,6 +196,14 @@ module Noticent
|
|
150
196
|
@options[:halt_on_error] = value
|
151
197
|
end
|
152
198
|
|
199
|
+
def skip_alert_with_no_subscribers=(value)
|
200
|
+
@options[:skip_alert_with_no_subscribers] = value
|
201
|
+
end
|
202
|
+
|
203
|
+
def use_sub_modules=(value)
|
204
|
+
@options[:use_sub_modules] = value
|
205
|
+
end
|
206
|
+
|
153
207
|
def hooks
|
154
208
|
if @config.hooks.nil?
|
155
209
|
@config.instance_variable_set(:@hooks, Noticent::Definitions::Hooks.new)
|
@@ -177,8 +231,12 @@ module Noticent
|
|
177
231
|
|
178
232
|
def channel(name, group: :default, klass: nil, &block)
|
179
233
|
channels = @config.instance_variable_get(:@channels) || {}
|
234
|
+
channel_groups = @config.instance_variable_get(:@channel_groups) || []
|
180
235
|
|
181
236
|
raise BadConfiguration, "channel '#{name}' already defined" if channels.include? name
|
237
|
+
raise BadConfiguration, "a channel group named '#{group}' already exists. channels and channel groups cannot have duplicates" if channel_groups.include? name
|
238
|
+
|
239
|
+
channel_groups << group
|
182
240
|
|
183
241
|
channel = Noticent::Definitions::Channel.new(@config, name, group: group, klass: klass)
|
184
242
|
hooks.run(:pre_channel_registration, channel)
|
@@ -188,15 +246,16 @@ module Noticent
|
|
188
246
|
channels[name] = channel
|
189
247
|
|
190
248
|
@config.instance_variable_set(:@channels, channels)
|
249
|
+
@config.instance_variable_set(:@channel_groups, channel_groups.uniq)
|
191
250
|
channel
|
192
251
|
end
|
193
252
|
|
194
|
-
def scope(name, payload_class: nil, &block)
|
253
|
+
def scope(name, payload_class: nil, check_constructor: true, &block)
|
195
254
|
scopes = @config.instance_variable_get(:@scopes) || {}
|
196
255
|
|
197
256
|
raise BadConfiguration, "scope '#{name}' already defined" if scopes.include? name
|
198
257
|
|
199
|
-
scope = Noticent::Definitions::Scope.new(@config, name, payload_class: payload_class)
|
258
|
+
scope = Noticent::Definitions::Scope.new(@config, name, payload_class: payload_class, check_constructor: check_constructor)
|
200
259
|
scope.instance_eval(&block)
|
201
260
|
|
202
261
|
scopes[name] = scope
|
@@ -8,12 +8,15 @@ module Noticent
|
|
8
8
|
attr_reader :notifiers
|
9
9
|
attr_reader :config
|
10
10
|
attr_reader :products
|
11
|
+
attr_reader :constructor_name
|
11
12
|
|
12
|
-
def initialize(config, name:, scope:)
|
13
|
+
def initialize(config, name:, scope:, constructor_name:)
|
13
14
|
@config = config
|
14
15
|
@name = name
|
15
16
|
@scope = scope
|
17
|
+
@constructor_name = constructor_name
|
16
18
|
@products = Noticent::Definitions::ProductGroup.new(@config)
|
19
|
+
@defaults = { _any_: Noticent::Definitions::Alert::DefaultValue.new(self, :_any_, config.default_value) }
|
17
20
|
end
|
18
21
|
|
19
22
|
def notify(recipient, template: '')
|
@@ -27,21 +30,54 @@ module Noticent
|
|
27
30
|
alert_notifier
|
28
31
|
end
|
29
32
|
|
33
|
+
def default_for(channel)
|
34
|
+
raise ArgumentError, "no channel named '#{channel}' found" if @config.channels[channel].nil?
|
35
|
+
|
36
|
+
@defaults[channel].nil? ? @defaults[:_any_].value : @defaults[channel].value
|
37
|
+
end
|
38
|
+
|
39
|
+
def default_value
|
40
|
+
@defaults[:_any_].value
|
41
|
+
end
|
42
|
+
|
43
|
+
def default(value, &block)
|
44
|
+
defaults = @defaults
|
45
|
+
|
46
|
+
if block_given?
|
47
|
+
default = Noticent::Definitions::Alert::DefaultValue.new(self, :_any_, value)
|
48
|
+
default.instance_eval(&block)
|
49
|
+
|
50
|
+
defaults[default.channel] = default
|
51
|
+
else
|
52
|
+
defaults[:_any_].value = value
|
53
|
+
end
|
54
|
+
|
55
|
+
@defaults = defaults
|
56
|
+
|
57
|
+
default
|
58
|
+
end
|
59
|
+
|
30
60
|
def applies
|
31
61
|
@products
|
32
62
|
end
|
33
63
|
|
34
64
|
def validate!
|
35
65
|
channels = @config.alert_channels(@name)
|
66
|
+
raise BadConfiguration, "no notifiers are assigned to alert '#{@name}'" if @notifiers.nil? || @notifiers.empty?
|
67
|
+
|
36
68
|
channels.each do |channel|
|
37
69
|
raise BadConfiguration, "channel #{channel.name} (#{channel.klass}) has no method called #{@name}" unless channel.klass.method_defined? @name
|
38
70
|
end
|
71
|
+
|
72
|
+
# if a payload class is available, we can make sure it has a constructor with the name of the event
|
73
|
+
raise Noticent::BadConfiguration, "payload #{@scope.payload_class} doesn't have a class method called #{name}" if @scope.check_constructor && !@scope.payload_class.respond_to?(@constructor_name)
|
39
74
|
end
|
40
75
|
|
41
76
|
# holds a list of recipient + channel
|
42
77
|
class Notifier
|
43
78
|
attr_reader :recipient
|
44
|
-
attr_reader :channel_group
|
79
|
+
attr_reader :channel_group # group to be notified
|
80
|
+
attr_reader :channel # channel to be notified
|
45
81
|
attr_reader :template
|
46
82
|
|
47
83
|
def initialize(alert, recipient, template: '')
|
@@ -50,13 +86,51 @@ module Noticent
|
|
50
86
|
@config = alert.config
|
51
87
|
@template = template
|
52
88
|
@channel_group = :default
|
89
|
+
@channel = nil
|
90
|
+
end
|
91
|
+
|
92
|
+
def on(channel_group_or_name)
|
93
|
+
# is it a group or a channel name?
|
94
|
+
if @config.channel_groups.include? channel_group_or_name
|
95
|
+
# it's a group
|
96
|
+
@channel_group = channel_group_or_name
|
97
|
+
@channel = nil
|
98
|
+
elsif !@config.channels[channel_group_or_name].nil?
|
99
|
+
@channel_group = :_none_
|
100
|
+
@channel = @config.channels[channel_group_or_name]
|
101
|
+
else
|
102
|
+
# not a group and not a channel
|
103
|
+
raise ArgumentError, "no channel or channel group found named '#{channel_group_or_name}'"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
# returns an array of all channels this notifier should send to
|
108
|
+
def applicable_channels
|
109
|
+
if @channel_group == :_none_
|
110
|
+
# it's a single channel
|
111
|
+
[@channel]
|
112
|
+
else
|
113
|
+
@config.channels_by_group(@channel_group)
|
114
|
+
end
|
53
115
|
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class DefaultValue
|
119
|
+
attr_reader :channel
|
120
|
+
attr_accessor :value
|
121
|
+
|
122
|
+
def initialize(alert, channel, value)
|
123
|
+
@alert = alert
|
124
|
+
@channel = channel
|
125
|
+
@value = value
|
126
|
+
end
|
127
|
+
|
128
|
+
def on(channel)
|
129
|
+
raise BadConfiguration, "no channel named '#{channel}'" if @alert.config.channels[channel].nil?
|
54
130
|
|
55
|
-
|
56
|
-
# validate the group name
|
57
|
-
raise ArgumentError, "no channel group found named '#{channel_group}'" if @config.channels_by_group(channel_group).empty?
|
131
|
+
@channel = channel
|
58
132
|
|
59
|
-
|
133
|
+
self
|
60
134
|
end
|
61
135
|
end
|
62
136
|
end
|