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.
data/config.ru ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rubygems"
4
+ require "bundler"
5
+
6
+ Bundler.require :default, :development
7
+
8
+ Combustion.initialize! :all
9
+ run Combustion::Application
data/lib/noticent.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/all'
3
+ require "active_support/all"
4
+ require "rails/all"
4
5
 
5
6
  Dir["#{File.dirname(__FILE__)}/noticent/**/*.rb"].each { |f| load(f) }
6
7
  load "#{File.dirname(__FILE__)}/generators/noticent/noticent.rb"
@@ -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).delete
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
@@ -2,41 +2,30 @@
2
2
 
3
3
  module Noticent
4
4
  class Channel
5
- @@default_ext = :erb
6
- @@default_format = :html
5
+ class_attribute :default_ext, default: :erb
6
+ class_attribute :default_format, default: :html
7
7
 
8
- def initialize(config, recipients, payload, context)
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
- @context = context
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, content)
17
- rendered_content = ERB.new(content).result(get_binding)
18
- template.nil? ? rendered_content : ERB.new(template).result(get_binding { rendered_content })
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 :context
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: @@default_format, ext: @@default_ext, layout: '')
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('::').last.underscore
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, 'layouts', "#{layout}.#{format}.#{ext}") unless layout == ''
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
@@ -2,17 +2,20 @@
2
2
 
3
3
  module Noticent
4
4
  def self.configure(options = {}, &block)
5
- if ENV['NOTICENT_RSPEC'] == '1'
5
+ if ENV["NOTICENT_RSPEC"] == "1"
6
6
  options = options.merge(
7
- base_module_name: 'Noticent::Testing',
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? || false
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, 'payloads')
132
+ File.join(base_dir, "payloads")
98
133
  end
99
134
 
100
135
  def scope_dir
101
- File.join(base_dir, 'scopes')
136
+ File.join(base_dir, "scopes")
102
137
  end
103
138
 
104
139
  def channel_dir
105
- File.join(base_dir, 'channels')
140
+ File.join(base_dir, "channels")
106
141
  end
107
142
 
108
143
  def view_dir
109
- File.join(base_dir, 'views')
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, 'no OptInProvider configured' if @config.opt_in_provider.nil?
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
- def on(channel_group)
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
- @channel_group = channel_group
133
+ self
60
134
  end
61
135
  end
62
136
  end