maily_herald 0.8.0 → 0.9.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/.gitignore +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +0 -2
- data/Gemfile.lock +5 -13
- data/README.md +186 -69
- data/app/mailers/maily_herald/mailer.rb +44 -26
- data/app/models/maily_herald/ad_hoc_mailing.rb +109 -0
- data/app/models/maily_herald/dispatch.rb +97 -4
- data/app/models/maily_herald/list.rb +36 -7
- data/app/models/maily_herald/log.rb +79 -0
- data/app/models/maily_herald/mailing.rb +149 -24
- data/app/models/maily_herald/one_time_mailing.rb +114 -9
- data/app/models/maily_herald/periodical_mailing.rb +76 -47
- data/app/models/maily_herald/sequence.rb +57 -18
- data/app/models/maily_herald/sequence_mailing.rb +29 -20
- data/app/models/maily_herald/subscription.rb +23 -2
- data/config/routes.rb +2 -2
- data/lib/maily_herald.rb +57 -18
- data/lib/maily_herald/autonaming.rb +8 -0
- data/lib/maily_herald/context.rb +6 -2
- data/lib/maily_herald/logging.rb +2 -0
- data/lib/maily_herald/manager.rb +15 -31
- data/lib/maily_herald/utils.rb +65 -24
- data/lib/maily_herald/version.rb +1 -1
- data/maily_herald.gemspec +1 -1
- data/spec/controllers/maily_herald/tokens_controller_spec.rb +13 -13
- data/spec/dummy/app/mailers/ad_hoc_mailer.rb +11 -0
- data/spec/dummy/app/mailers/custom_one_time_mailer.rb +11 -0
- data/spec/dummy/config/application.rb +1 -1
- data/spec/dummy/config/environments/test.rb +1 -1
- data/spec/dummy/config/initializers/maily_herald.rb +3 -69
- data/spec/dummy/config/maily_herald.yml +3 -0
- data/spec/dummy/db/seeds.rb +73 -0
- data/spec/lib/context_spec.rb +7 -7
- data/spec/lib/maily_herald_spec.rb +7 -8
- data/spec/lib/utils_spec.rb +65 -25
- data/spec/mailers/maily_herald/mailer_spec.rb +20 -13
- data/spec/models/maily_herald/ad_hoc_mailing_spec.rb +169 -0
- data/spec/models/maily_herald/list_spec.rb +2 -1
- data/spec/models/maily_herald/log_spec.rb +10 -10
- data/spec/models/maily_herald/mailing_spec.rb +9 -8
- data/spec/models/maily_herald/one_time_mailing_spec.rb +212 -39
- data/spec/models/maily_herald/periodical_mailing_spec.rb +158 -92
- data/spec/models/maily_herald/sequence_mailing_spec.rb +2 -2
- data/spec/models/maily_herald/sequence_spec.rb +152 -139
- data/spec/models/maily_herald/subscription_spec.rb +21 -4
- metadata +17 -8
- data/lib/maily_herald/condition_evaluator.rb +0 -82
- data/lib/maily_herald/config.rb +0 -5
- data/spec/dummy/app/mailers/test_mailer.rb +0 -11
@@ -25,8 +25,23 @@ module MailyHerald
|
|
25
25
|
|
26
26
|
after_save :update_schedules, if: Proc.new{|s| s.active_changed?}
|
27
27
|
|
28
|
+
def self.get_from(entity)
|
29
|
+
if entity.has_attribute?(:maily_subscription_id) && entity.maily_subscription_id
|
30
|
+
subscription = MailyHerald::Subscription.new
|
31
|
+
|
32
|
+
entity.attributes.each do |k, v|
|
33
|
+
if match = k.match(/^maily_subscription_(\w+)$/)
|
34
|
+
subscription.send("#{match[1]}=", v)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
subscription.readonly!
|
39
|
+
subscription
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
28
43
|
def active?
|
29
|
-
|
44
|
+
read_attribute(:id) && read_attribute(:active)
|
30
45
|
end
|
31
46
|
|
32
47
|
def deactivate!
|
@@ -42,7 +57,7 @@ module MailyHerald
|
|
42
57
|
end
|
43
58
|
|
44
59
|
def token_url
|
45
|
-
MailyHerald::Engine.routes.url_helpers.
|
60
|
+
MailyHerald::Engine.routes.url_helpers.maily_unsubscribe_url(self)
|
46
61
|
end
|
47
62
|
|
48
63
|
def to_liquid
|
@@ -52,6 +67,12 @@ module MailyHerald
|
|
52
67
|
end
|
53
68
|
|
54
69
|
def update_schedules
|
70
|
+
AdHocMailing.where(list_id: self.list).each do |m|
|
71
|
+
m.set_schedule_for self.entity
|
72
|
+
end
|
73
|
+
OneTimeMailing.where(list_id: self.list).each do |m|
|
74
|
+
m.set_schedule_for self.entity
|
75
|
+
end
|
55
76
|
PeriodicalMailing.where(list_id: self.list).each do |m|
|
56
77
|
m.set_schedule_for self.entity
|
57
78
|
end
|
data/config/routes.rb
CHANGED
@@ -3,9 +3,9 @@ MailyHerald::Engine.routes.draw do
|
|
3
3
|
end
|
4
4
|
|
5
5
|
MailyHerald::Engine.routes.url_helpers.class.module_eval do
|
6
|
-
def
|
6
|
+
def maily_unsubscribe_url(subscription, *args)
|
7
7
|
options = args.extract_options! || {}
|
8
|
-
options = options.reverse_merge(controller: "/maily_herald/tokens", action: "get", token: subscription.token)
|
8
|
+
options = options.reverse_merge({controller: "/maily_herald/tokens", action: "get", token: subscription.token}.merge(Rails.application.routes.default_url_options))
|
9
9
|
|
10
10
|
MailyHerald::Engine.routes.url_helpers.url_for(options)
|
11
11
|
end
|
data/lib/maily_herald.rb
CHANGED
@@ -19,14 +19,10 @@ module MailyHerald
|
|
19
19
|
MailyHerald::Logging.initialize(logger_opts)
|
20
20
|
end
|
21
21
|
|
22
|
-
if args["mailing"]
|
23
|
-
MailyHerald::Manager.deliver args["mailing"], args["entity"]
|
24
|
-
elsif args["mailing"]
|
22
|
+
if args["mailing"]
|
25
23
|
MailyHerald::Manager.run_mailing args["mailing"]
|
26
24
|
elsif args["sequence"]
|
27
25
|
MailyHerald::Manager.run_sequence args["sequence"]
|
28
|
-
elsif args["simulate"]
|
29
|
-
MailyHerald::Manager.simulate args["simulate"]
|
30
26
|
else
|
31
27
|
MailyHerald::Manager.run_all
|
32
28
|
end
|
@@ -38,17 +34,30 @@ module MailyHerald
|
|
38
34
|
|
39
35
|
def perform id
|
40
36
|
dispatch = MailyHerald::Dispatch.find(id)
|
41
|
-
dispatch.
|
37
|
+
dispatch.set_schedules if dispatch.respond_to?(:set_schedules)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Initializer
|
42
|
+
def initialize klass
|
43
|
+
@klass = klass
|
44
|
+
end
|
45
|
+
|
46
|
+
def method_missing m, *args, &block
|
47
|
+
if %w{list ad_hoc_mailing one_time_mailing periodical_mailing sequence_mailing sequence}.include?(m.to_s)
|
48
|
+
options = args.extract_options!
|
49
|
+
@klass.send m, *args, options.merge(locked: true), &block
|
50
|
+
else
|
51
|
+
@klass.send m, *args, &block
|
52
|
+
end
|
42
53
|
end
|
43
54
|
end
|
44
55
|
|
45
56
|
autoload :Utils, 'maily_herald/utils'
|
46
|
-
autoload :ConditionEvaluator, 'maily_herald/condition_evaluator'
|
47
57
|
autoload :TemplateRenderer, 'maily_herald/template_renderer'
|
48
58
|
autoload :ModelExtensions, 'maily_herald/model_extensions'
|
49
59
|
autoload :Context, 'maily_herald/context'
|
50
60
|
autoload :Manager, 'maily_herald/manager'
|
51
|
-
autoload :Config, 'maily_herald/config'
|
52
61
|
autoload :Autonaming, 'maily_herald/autonaming'
|
53
62
|
autoload :Logging, 'maily_herald/logging'
|
54
63
|
|
@@ -105,6 +114,14 @@ module MailyHerald
|
|
105
114
|
self.locked_lists.include?(name.to_s)
|
106
115
|
end
|
107
116
|
|
117
|
+
def start_at_procs
|
118
|
+
@@start_at_procs ||= {}
|
119
|
+
end
|
120
|
+
|
121
|
+
def conditions_procs
|
122
|
+
@@conditions_procs ||= {}
|
123
|
+
end
|
124
|
+
|
108
125
|
# Obtains Redis connection.
|
109
126
|
def redis
|
110
127
|
@redis ||= begin
|
@@ -139,11 +156,14 @@ module MailyHerald
|
|
139
156
|
#
|
140
157
|
# To be used in initializer file.
|
141
158
|
def setup
|
142
|
-
|
159
|
+
logger.warn("Maily migrations seems to be pending. Skipping setup...") && return unless schema_loaded?
|
143
160
|
|
144
|
-
|
161
|
+
yield Initializer.new(self)
|
162
|
+
end
|
145
163
|
|
146
|
-
|
164
|
+
# Checks if Maily tables are present.
|
165
|
+
def schema_loaded?
|
166
|
+
!([MailyHerald::Dispatch, MailyHerald::List, MailyHerald::Log, MailyHerald::Subscription].collect(&:table_exists?).select{|v| !v}.length > 0)
|
147
167
|
end
|
148
168
|
|
149
169
|
# Fetches or defines a {Context}.
|
@@ -156,6 +176,7 @@ module MailyHerald
|
|
156
176
|
# @param name [Symbol] Identifier name of the Context.
|
157
177
|
def context name, &block
|
158
178
|
name = name.to_s
|
179
|
+
@@contexts ||= {}
|
159
180
|
|
160
181
|
if block_given?
|
161
182
|
@@contexts ||= {}
|
@@ -175,6 +196,30 @@ module MailyHerald
|
|
175
196
|
MailyHerald::Dispatch.find_by_name(name)
|
176
197
|
end
|
177
198
|
|
199
|
+
# Fetches or defines an {AdHocMailing}.
|
200
|
+
#
|
201
|
+
# If no block provided, {AdHocMailing} with given +name+ is returned.
|
202
|
+
#
|
203
|
+
# If block provided, {AdHocMailing} with given +name+ is created or edited
|
204
|
+
# and block is evaluated within that mailing.
|
205
|
+
#
|
206
|
+
# @option options [true, false] :locked (false) Determines whether Mailing is locked.
|
207
|
+
# @see Dispatch#locked?
|
208
|
+
def ad_hoc_mailing name, options = {}
|
209
|
+
mailing = MailyHerald::AdHocMailing.where(name: name).first
|
210
|
+
lock = options.delete(:locked)
|
211
|
+
|
212
|
+
if block_given? && !self.dispatch_locked?(name) && (!mailing || lock)
|
213
|
+
mailing ||= MailyHerald::AdHocMailing.new(name: name)
|
214
|
+
yield(mailing)
|
215
|
+
mailing.save!
|
216
|
+
|
217
|
+
MailyHerald.lock_dispatch(name) if lock
|
218
|
+
end
|
219
|
+
|
220
|
+
mailing
|
221
|
+
end
|
222
|
+
|
178
223
|
# Fetches or defines an {OneTimeMailing}.
|
179
224
|
#
|
180
225
|
# If no block provided, {OneTimeMailing} with given +name+ is returned.
|
@@ -311,13 +356,6 @@ module MailyHerald
|
|
311
356
|
end
|
312
357
|
end
|
313
358
|
|
314
|
-
def deliver mailing_name, entity_id
|
315
|
-
mailing_name = mailing_name.name if mailing_name.is_a?(Mailing)
|
316
|
-
entity_id = entity_id.id if !entity_id.is_a?(Fixnum)
|
317
|
-
|
318
|
-
Async.perform_async mailing: mailing_name, entity: entity_id, logger: MailyHerald::Logging.safe_options
|
319
|
-
end
|
320
|
-
|
321
359
|
def run_sequence seq_name
|
322
360
|
seq_name = seq_name.name if seq_name.is_a?(Sequence)
|
323
361
|
|
@@ -342,6 +380,7 @@ module MailyHerald
|
|
342
380
|
# Read options from config file
|
343
381
|
def read_options cfile = "config/maily_herald.yml"
|
344
382
|
opts = {}
|
383
|
+
cfile = Pathname.new(cfile).relative? && defined?(Rails) ? Rails.root + cfile : cfile
|
345
384
|
if File.exist?(cfile)
|
346
385
|
opts = YAML.load(ERB.new(IO.read(cfile)).result)
|
347
386
|
end
|
@@ -1,4 +1,12 @@
|
|
1
1
|
module MailyHerald
|
2
|
+
|
3
|
+
# Provides some common patters for models that have both :name and :title attributes.
|
4
|
+
# It adds some format constraints to :name and title attributes and makes sure
|
5
|
+
# that they are always both set properly.
|
6
|
+
#
|
7
|
+
# If only :name is provided, it will be used also as a:title.
|
8
|
+
# If only :title is provided, :name will be automatically generated out of it.
|
9
|
+
#
|
2
10
|
module Autonaming
|
3
11
|
def self.included(base)
|
4
12
|
base.extend ClassMethods
|
data/lib/maily_herald/context.rb
CHANGED
@@ -103,9 +103,9 @@ module MailyHerald
|
|
103
103
|
# Defines or returns Entity scope - collection of Entities.
|
104
104
|
#
|
105
105
|
# If block passed, it is saved as scope proc. Block has to return
|
106
|
-
# +ActiveRecord::Relation+ containing
|
106
|
+
# +ActiveRecord::Relation+ containing entity objects that will belong to scope.
|
107
107
|
#
|
108
|
-
# If no block given, scope proc is called and
|
108
|
+
# If no block given, scope proc is called and entity collection returned.
|
109
109
|
def scope &block
|
110
110
|
if block_given?
|
111
111
|
@scope = block
|
@@ -210,6 +210,8 @@ module MailyHerald
|
|
210
210
|
# Obtains {Context} attributes in a form of (nested) +Hash+ which
|
211
211
|
# values are procs each returning single Entity attribute value.
|
212
212
|
def attributes_list
|
213
|
+
return {} unless @attributes
|
214
|
+
|
213
215
|
attributes = @attributes.dup
|
214
216
|
attributes.setup
|
215
217
|
attributes.for_drop
|
@@ -217,6 +219,8 @@ module MailyHerald
|
|
217
219
|
|
218
220
|
# Returns Liquid drop created from Context attributes.
|
219
221
|
def drop_for entity, subscription
|
222
|
+
return {} unless @attributes
|
223
|
+
|
220
224
|
attributes = @attributes.dup
|
221
225
|
attributes.setup entity, subscription
|
222
226
|
Drop.new(attributes.for_drop)
|
data/lib/maily_herald/logging.rb
CHANGED
@@ -50,6 +50,8 @@ module MailyHerald
|
|
50
50
|
@options ||= OPTIONS.dup
|
51
51
|
@options.merge!(opts) if opts
|
52
52
|
|
53
|
+
@options[:target] = Rails.root + @options[:target] if @options[:target].is_a?(String) && Pathname.new(@options[:target]).relative? && defined?(Rails)
|
54
|
+
|
53
55
|
@logger = Logger.new(@options[:target])
|
54
56
|
@logger.level = @options[:level]
|
55
57
|
@logger.formatter = Formatter.new
|
data/lib/maily_herald/manager.rb
CHANGED
@@ -1,49 +1,33 @@
|
|
1
1
|
module MailyHerald
|
2
2
|
class Manager
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.deliver mailing, entity
|
11
|
-
mailing = Mailing.find_by_name(mailing) if !mailing.is_a?(Mailing)
|
12
|
-
entity = mailing.context.scope.find(entity) if entity.is_a?(Fixnum)
|
3
|
+
# Run scheduled sequence mailing deliveries.
|
4
|
+
#
|
5
|
+
# @param sequence [Sequence, String, Symbol] {Sequence} object or name
|
6
|
+
def self.run_sequence sequence
|
7
|
+
seqence = Sequence.find_by_name(seqence) unless seqence.is_a?(Sequence)
|
13
8
|
|
14
|
-
|
15
|
-
end
|
16
|
-
|
17
|
-
def self.run_sequence seq
|
18
|
-
seq = Sequence.find_by_name(seq) if !seq.is_a?(Sequence)
|
19
|
-
|
20
|
-
seq.run if seq
|
9
|
+
sequence.run if sequence
|
21
10
|
end
|
22
11
|
|
12
|
+
# Run scheduled periodical mailing deliveres.
|
13
|
+
#
|
14
|
+
# @param mailing [PeriodicalMailing, OneTimeMailing, String, Symbol]
|
15
|
+
# {AdHocMailing}, {OneTimeMailing} or {PeriodicalMailing} object or name
|
23
16
|
def self.run_mailing mailing
|
24
|
-
mailing = Mailing.find_by_name(mailing)
|
17
|
+
mailing = Mailing.find_by_name(mailing) unless mailing.is_a?(Mailing)
|
25
18
|
|
26
19
|
mailing.run if mailing
|
27
20
|
end
|
28
21
|
|
22
|
+
# Run all scheduled mailing deliveres.
|
29
23
|
def self.run_all
|
24
|
+
AdHocMailing.all.each {|m| m.run}
|
25
|
+
OneTimeMailing.all.each {|m| m.run}
|
30
26
|
PeriodicalMailing.all.each {|m| m.run}
|
31
27
|
Sequence.all.each {|m| m.run}
|
32
28
|
end
|
33
29
|
|
34
|
-
|
35
|
-
File.open("/tmp/maily_herlald_timetravel.lock", "w") {}
|
36
|
-
time = Time.now
|
37
|
-
end_time = time + period
|
38
|
-
while time < end_time
|
39
|
-
Timecop.freeze(time)
|
40
|
-
run_all
|
41
|
-
time = time + 1.day
|
42
|
-
end
|
43
|
-
Timecop.return
|
44
|
-
File.delete("/tmp/maily_herlald_timetravel.lock")
|
45
|
-
end
|
46
|
-
|
30
|
+
# Check if Maily sidekiq job is running.
|
47
31
|
def self.job_enqueued?
|
48
32
|
Sidekiq::Queue.new.detect{|j| j.klass == "MailyHerald::Async" } ||
|
49
33
|
Sidekiq::Workers.new.detect{|w, msg| msg["payload"]["class"] == "MailyHerald::Async" } ||
|
data/lib/maily_herald/utils.rb
CHANGED
@@ -5,32 +5,61 @@ module MailyHerald
|
|
5
5
|
end
|
6
6
|
|
7
7
|
class MarkupEvaluator
|
8
|
-
|
9
|
-
def has_key?(name)
|
10
|
-
true
|
11
|
-
end
|
8
|
+
VariableSignature = /\A[\w\.\[\]]+(\s*|\s*.+)?\Z/
|
12
9
|
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
module Filters
|
11
|
+
module Date
|
12
|
+
def minus input, no, unit
|
13
|
+
input - no.to_i.send(unit)
|
14
|
+
end
|
16
15
|
|
17
|
-
|
16
|
+
def plus input, no, unit
|
17
|
+
input + no.to_i.send(unit)
|
18
|
+
end
|
19
|
+
end
|
18
20
|
end
|
19
21
|
|
20
22
|
def self.test_conditions conditions
|
21
23
|
return true if !conditions || conditions.empty?
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
drop = Class.new(Liquid::Drop) do
|
26
|
+
def has_key?(name); true; end
|
27
|
+
def invoke_drop(name); true; end
|
28
|
+
alias :[] :invoke_drop
|
29
|
+
end.new
|
30
|
+
|
31
|
+
evaluator = Utils::MarkupEvaluator.new(drop)
|
32
|
+
evaluator.evaluate_conditions(conditions)
|
33
|
+
true
|
34
|
+
rescue
|
35
|
+
return false
|
32
36
|
end
|
33
37
|
|
38
|
+
def self.test_start_at markup
|
39
|
+
return true if !markup || markup.empty?
|
40
|
+
|
41
|
+
drop = Class.new(Liquid::Drop) do
|
42
|
+
def has_key?(name); true; end
|
43
|
+
def invoke_drop(name)
|
44
|
+
t = Time.now
|
45
|
+
t.define_singleton_method(:[]) do |v|
|
46
|
+
Time.now
|
47
|
+
end
|
48
|
+
t.define_singleton_method(:has_key?) do |v|
|
49
|
+
true
|
50
|
+
end
|
51
|
+
t
|
52
|
+
end
|
53
|
+
alias :[] :invoke_drop
|
54
|
+
end.new
|
55
|
+
|
56
|
+
evaluator = Utils::MarkupEvaluator.new(drop)
|
57
|
+
val = evaluator.evaluate_start_at(markup)
|
58
|
+
|
59
|
+
return val.is_a?(Time) || val.is_a?(DateTime)
|
60
|
+
rescue
|
61
|
+
return false
|
62
|
+
end
|
34
63
|
|
35
64
|
def initialize drop
|
36
65
|
@drop = drop
|
@@ -41,19 +70,31 @@ module MailyHerald
|
|
41
70
|
|
42
71
|
condition = MarkupEvaluator.create_liquid_condition conditions
|
43
72
|
template = Liquid::Template.parse(conditions)
|
73
|
+
raise StandardError unless template.errors.empty?
|
44
74
|
|
45
75
|
liquid_context = Liquid::Context.new([@drop, template.assigns], template.instance_assigns, template.registers, true, {})
|
46
|
-
@drop.context = liquid_context
|
76
|
+
@drop.context = liquid_context if @drop.is_a?(Liquid::Drop)
|
47
77
|
|
48
|
-
condition.evaluate liquid_context
|
78
|
+
val = condition.evaluate liquid_context
|
79
|
+
raise(ArgumentError, "Conditions do not evaluate to boolean (got `#{val}`)") unless [true, false].include?(val)
|
80
|
+
val
|
49
81
|
end
|
50
82
|
|
51
|
-
def
|
52
|
-
|
83
|
+
def evaluate_start_at markup
|
84
|
+
begin
|
85
|
+
Time.parse(markup)
|
86
|
+
rescue
|
87
|
+
raise(ArgumentError, "Start at is not a proper variable: `#{markup}`") unless VariableSignature =~ markup
|
53
88
|
|
54
|
-
|
55
|
-
|
56
|
-
|
89
|
+
liquid_context = Liquid::Context.new([@drop], {}, {}, true, {})
|
90
|
+
liquid_context.add_filters([Filters::Date])
|
91
|
+
|
92
|
+
@drop.context = liquid_context if @drop.is_a?(Liquid::Drop)
|
93
|
+
#liquid_context[markup]
|
94
|
+
|
95
|
+
variable = Liquid::Variable.new markup
|
96
|
+
variable.render(liquid_context)
|
97
|
+
end
|
57
98
|
end
|
58
99
|
|
59
100
|
private
|
data/lib/maily_herald/version.rb
CHANGED
data/maily_herald.gemspec
CHANGED
@@ -13,7 +13,7 @@ Gem::Specification.new do |s|
|
|
13
13
|
s.email = ["lukasz@sology.eu"]
|
14
14
|
s.homepage = "https://github.com/Sology/maily_herald"
|
15
15
|
s.license = "LGPL-3.0"
|
16
|
-
s.description = s.summary = "Email
|
16
|
+
s.description = s.summary = "Email processing solution for Ruby on Rails applications"
|
17
17
|
|
18
18
|
s.files = `git ls-files`.split("\n")
|
19
19
|
s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
|