noticent 0.0.1.pre.pre → 0.0.1
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 +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
|