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
@@ -17,93 +17,90 @@ module MailyHerald
|
|
17
17
|
self.period = d.to_f.days
|
18
18
|
end
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
schedule = schedule_for entity
|
28
|
-
|
29
|
-
schedule.with_lock do
|
30
|
-
# make sure schedule hasn't been processed in the meantime
|
31
|
-
if schedule && schedule.processing_at <= current_time && schedule.scheduled?
|
32
|
-
attrs = super(entity)
|
33
|
-
if attrs
|
34
|
-
schedule.attributes = attrs
|
35
|
-
schedule.processing_at = current_time
|
36
|
-
schedule.save!
|
37
|
-
set_schedule_for entity, schedule
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end if schedule
|
41
|
-
end
|
42
|
-
|
20
|
+
# Sends mailing to all subscribed entities.
|
21
|
+
#
|
22
|
+
# Performs actual sending of emails; should be called in background.
|
23
|
+
#
|
24
|
+
# Returns array of {MailyHerald::Log} with actual `Mail::Message` objects stored
|
25
|
+
# in {MailyHerald::Log.mail} attributes.
|
43
26
|
def run
|
44
27
|
# TODO better scope here to exclude schedules for users outside context scope
|
45
|
-
schedules.where("processing_at <= (?)", Time.now).
|
28
|
+
schedules.where("processing_at <= (?)", Time.now).collect do |schedule|
|
46
29
|
if schedule.entity
|
47
|
-
|
30
|
+
mail = deliver schedule
|
31
|
+
schedule.reload
|
32
|
+
schedule.mail = mail
|
33
|
+
schedule
|
48
34
|
else
|
49
|
-
MailyHerald.logger.log_processing(schedule.mailing, {:
|
35
|
+
MailyHerald.logger.log_processing(schedule.mailing, {class: schedule.entity_type, id: schedule.entity_id}, prefix: "Removing schedule for non-existing entity")
|
50
36
|
schedule.destroy
|
51
37
|
end
|
52
38
|
end
|
53
39
|
end
|
54
40
|
|
41
|
+
# Returns collection of processed {Log}s for given entity.
|
55
42
|
def processed_logs entity
|
56
43
|
Log.ordered.for_entity(entity).for_mailing(self).processed
|
57
44
|
end
|
58
45
|
|
46
|
+
# Returns processing time for given entity.
|
47
|
+
#
|
48
|
+
# This is the time when next mailing should be sent.
|
49
|
+
# Calculation is done mased on last processed mailing for this entity or
|
50
|
+
# {#start_at} mailing attribute.
|
59
51
|
def start_processing_time entity
|
60
52
|
if processed_logs(entity).first
|
61
|
-
processed_logs(entity).first.
|
53
|
+
processed_logs(entity).first.processing_at
|
62
54
|
else
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
55
|
+
subscription = self.list.subscription_for(entity)
|
56
|
+
|
57
|
+
if has_start_at_proc?
|
58
|
+
start_at.call(entity, subscription)
|
59
|
+
else
|
67
60
|
evaluator = Utils::MarkupEvaluator.new(self.list.context.drop_for(entity, subscription))
|
68
61
|
|
69
|
-
evaluator.
|
62
|
+
evaluator.evaluate_start_at(self.start_at)
|
70
63
|
end
|
71
64
|
end
|
72
65
|
end
|
73
66
|
|
67
|
+
# Gets the timestamp of last processed email for given entity.
|
74
68
|
def last_processing_time entity
|
75
69
|
processed_logs(entity).last.try(:processing_at)
|
76
70
|
end
|
77
71
|
|
72
|
+
# Sets the delivery schedule for given entity
|
73
|
+
#
|
74
|
+
# New schedule will be created or existing one updated.
|
75
|
+
#
|
76
|
+
# Schedule is {Log} object of type "schedule".
|
78
77
|
def set_schedule_for entity, last_log = nil
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
subscribed = self.list.subscribed?(entity)
|
84
|
-
end
|
78
|
+
subscribed = self.list.subscribed?(entity)
|
79
|
+
log = schedule_for(entity)
|
80
|
+
last_log ||= processed_logs(entity).last
|
81
|
+
processing_at = calculate_processing_time(entity, last_log)
|
85
82
|
|
86
|
-
if !self.period || !self.start_at || !enabled? || !(self.override_subscription? || subscribed)
|
83
|
+
if !self.period || !self.start_at || !enabled? || !processing_at || !(self.override_subscription? || subscribed)
|
87
84
|
log = schedule_for(entity)
|
88
85
|
log.try(:destroy)
|
89
86
|
return
|
90
87
|
end
|
91
88
|
|
92
|
-
log = schedule_for(entity)
|
93
|
-
last_log ||= processed_logs(entity).last
|
94
|
-
|
95
89
|
log ||= Log.new
|
96
90
|
log.with_lock do
|
97
91
|
log.set_attributes_for(self, entity, {
|
98
92
|
status: :scheduled,
|
99
|
-
processing_at:
|
93
|
+
processing_at: processing_at,
|
100
94
|
})
|
101
95
|
log.save!
|
102
96
|
end
|
103
97
|
log
|
104
98
|
end
|
105
99
|
|
106
|
-
|
100
|
+
# Sets delivery schedules of all entities in mailing scope.
|
101
|
+
#
|
102
|
+
# New schedules will be created or existing ones updated.
|
103
|
+
def set_schedules
|
107
104
|
self.list.context.scope_with_subscription(self.list, :outer).each do |entity|
|
108
105
|
MailyHerald.logger.debug "Updating schedule of #{self} periodical for entity ##{entity.id} #{entity}"
|
109
106
|
set_schedule_for entity
|
@@ -111,29 +108,42 @@ module MailyHerald
|
|
111
108
|
end
|
112
109
|
|
113
110
|
def update_schedules_callback
|
114
|
-
Rails.env.test? ?
|
111
|
+
Rails.env.test? ? set_schedules : MailyHerald::ScheduleUpdater.perform_in(10.seconds, self.id)
|
115
112
|
end
|
116
113
|
|
114
|
+
# Returns {Log} object which is the delivery schedule for given entity.
|
117
115
|
def schedule_for entity
|
118
116
|
schedules.for_entity(entity).first
|
119
117
|
end
|
120
118
|
|
119
|
+
# Returns collection of all delivery schedules ({Log} collection).
|
121
120
|
def schedules
|
122
121
|
Log.ordered.scheduled.for_mailing(self)
|
123
122
|
end
|
124
123
|
|
124
|
+
# Calculates processing time for given entity.
|
125
125
|
def calculate_processing_time entity, last_log = nil
|
126
126
|
last_log ||= processed_logs(entity).last
|
127
127
|
|
128
|
+
spt = start_processing_time(entity)
|
129
|
+
|
128
130
|
if last_log && last_log.processing_at
|
129
131
|
last_log.processing_at + self.period
|
130
|
-
elsif
|
131
|
-
|
132
|
+
elsif individual_scheduling? && spt
|
133
|
+
spt
|
134
|
+
elsif general_scheduling?
|
135
|
+
if spt >= Time.now
|
136
|
+
spt
|
137
|
+
else
|
138
|
+
diff = (Time.now - spt).to_f
|
139
|
+
spt ? spt + ((diff/self.period).ceil * self.period) : nil
|
140
|
+
end
|
132
141
|
else
|
133
142
|
nil
|
134
143
|
end
|
135
144
|
end
|
136
145
|
|
146
|
+
# Get next email processing time for given entity.
|
137
147
|
def next_processing_time entity
|
138
148
|
schedule_for(entity).processing_at
|
139
149
|
end
|
@@ -141,5 +151,24 @@ module MailyHerald
|
|
141
151
|
def to_s
|
142
152
|
"<PeriodicalMailing: #{self.title || self.name}>"
|
143
153
|
end
|
154
|
+
|
155
|
+
private
|
156
|
+
|
157
|
+
def deliver_with_mailer schedule
|
158
|
+
current_time = Time.now
|
159
|
+
|
160
|
+
schedule.with_lock do
|
161
|
+
# make sure schedule hasn't been processed in the meantime
|
162
|
+
if schedule && schedule.processing_at <= current_time && schedule.scheduled?
|
163
|
+
schedule = super(schedule)
|
164
|
+
if schedule
|
165
|
+
schedule.processing_at = current_time if schedule.processed?
|
166
|
+
schedule.save!
|
167
|
+
|
168
|
+
set_schedule_for schedule.entity, schedule
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end if schedule
|
172
|
+
end
|
144
173
|
end
|
145
174
|
end
|
@@ -29,6 +29,15 @@ module MailyHerald
|
|
29
29
|
end
|
30
30
|
after_save :update_schedules_callback, if: Proc.new{|s| s.state_changed? || s.start_at_changed?}
|
31
31
|
|
32
|
+
# Fetches or defines an {SequenceMailing}.
|
33
|
+
#
|
34
|
+
# If no block provided, {SequenceMailing} with given +name+ is returned.
|
35
|
+
#
|
36
|
+
# If block provided, {SequenceMailing} with given +name+ is created or edited
|
37
|
+
# and block is evaluated within that mailing.
|
38
|
+
#
|
39
|
+
# @option options [true, false] :locked (false) Determines whether Mailing is locked.
|
40
|
+
# @see Dispatch#locked?
|
32
41
|
def mailing name, options = {}
|
33
42
|
if SequenceMailing.table_exists?
|
34
43
|
mailing = SequenceMailing.find_by_name(name)
|
@@ -48,45 +57,64 @@ module MailyHerald
|
|
48
57
|
end
|
49
58
|
end
|
50
59
|
|
60
|
+
# Sends sequence mailings to all subscribed entities.
|
61
|
+
#
|
62
|
+
# Performs actual sending of emails; should be called in background.
|
63
|
+
#
|
64
|
+
# Returns array of {MailyHerald::Log} with actual `Mail::Message` objects stored
|
65
|
+
# in {MailyHerald::Log.mail} attributes.
|
51
66
|
def run
|
52
67
|
# TODO better scope here to exclude schedules for users outside context scope
|
53
68
|
schedules.where("processing_at <= (?)", Time.now).each do |schedule|
|
54
69
|
if schedule.entity
|
55
|
-
schedule.mailing.
|
70
|
+
mail = schedule.mailing.send(:deliver, schedule)
|
71
|
+
schedule.reload
|
72
|
+
schedule.mail = mail
|
73
|
+
schedule
|
56
74
|
else
|
57
|
-
MailyHerald.logger.log_processing(schedule.mailing, {:
|
75
|
+
MailyHerald.logger.log_processing(schedule.mailing, {class: schedule.entity_type, id: schedule.entity_id}, prefix: "Removing schedule for non-existing entity")
|
58
76
|
schedule.destroy
|
59
77
|
end
|
60
78
|
end
|
61
79
|
end
|
62
80
|
|
81
|
+
# Returns collection of processed {Log}s for given entity.
|
63
82
|
def processed_logs entity
|
64
83
|
Log.ordered.processed.for_entity(entity).for_mailings(self.mailings.select(:id))
|
65
84
|
end
|
66
85
|
|
86
|
+
# Returns collection of processed {Log}s for given entity and mailing.
|
87
|
+
#
|
88
|
+
# @param entity [ActiveRecord::Base]
|
89
|
+
# @param mailing [SequenceMailing]
|
67
90
|
def processed_logs_for entity, mailing
|
68
91
|
Log.ordered.processed.for_entity(entity).for_mailing(self.mailings.find(mailing))
|
69
92
|
end
|
70
93
|
|
94
|
+
# Gets the timestamp of last processed email for given entity.
|
71
95
|
def last_processing_time entity
|
72
96
|
ls = processed_logs(entity)
|
73
97
|
ls.last.processing_at if ls.last
|
74
98
|
end
|
75
99
|
|
100
|
+
# Gets collection of {SequenceMailing} objects that are to be sent to entity.
|
76
101
|
def pending_mailings entity
|
77
102
|
ls = processed_logs(entity)
|
78
103
|
ls.empty? ? self.mailings.enabled : self.mailings.enabled.where("id not in (?)", ls.map(&:mailing_id))
|
79
104
|
end
|
80
105
|
|
106
|
+
# Gets collection of {SequenceMailing} objects that were sent to entity.
|
81
107
|
def processed_mailings entity
|
82
108
|
ls = processed_logs(entity)
|
83
109
|
ls.empty? ? self.mailings.where(id: nil) : self.mailings.where("id in (?)", ls.map(&:mailing_id))
|
84
110
|
end
|
85
111
|
|
112
|
+
# Gets last {SequenceMailing} object delivered to user.
|
86
113
|
def last_processed_mailing entity
|
87
114
|
processed_mailings(entity).last
|
88
115
|
end
|
89
116
|
|
117
|
+
# Gets next {SequenceMailing} object to be delivered to user.
|
90
118
|
def next_mailing entity
|
91
119
|
pending_mailings(entity).first
|
92
120
|
end
|
@@ -95,17 +123,19 @@ module MailyHerald
|
|
95
123
|
Log.ordered.processed.for_entity(entity).for_mailing(mailing).last
|
96
124
|
end
|
97
125
|
|
126
|
+
# Sets the delivery schedule for given entity
|
127
|
+
#
|
128
|
+
# New schedule will be created or existing one updated.
|
129
|
+
#
|
130
|
+
# Schedule is {Log} object of type "schedule".
|
98
131
|
def set_schedule_for entity
|
99
132
|
# TODO handle override subscription?
|
100
133
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
else
|
105
|
-
subscribed = self.list.subscribed?(entity)
|
106
|
-
end
|
134
|
+
subscribed = self.list.subscribed?(entity)
|
135
|
+
mailing = next_mailing(entity)
|
136
|
+
start_time = calculate_processing_time_for(entity, mailing) if mailing
|
107
137
|
|
108
|
-
if !subscribed || !self.start_at || !enabled? || !
|
138
|
+
if !subscribed || !self.start_at || !enabled? || !mailing || !start_time
|
109
139
|
log = schedule_for(entity)
|
110
140
|
log.try(:destroy)
|
111
141
|
return
|
@@ -116,14 +146,17 @@ module MailyHerald
|
|
116
146
|
log.with_lock do
|
117
147
|
log.set_attributes_for(mailing, entity, {
|
118
148
|
status: :scheduled,
|
119
|
-
processing_at:
|
149
|
+
processing_at: start_time,
|
120
150
|
})
|
121
151
|
log.save!
|
122
152
|
end
|
123
153
|
log
|
124
154
|
end
|
125
155
|
|
126
|
-
|
156
|
+
# Sets delivery schedules of all entities in mailing scope.
|
157
|
+
#
|
158
|
+
# New schedules will be created or existing ones updated.
|
159
|
+
def set_schedules
|
127
160
|
self.list.context.scope_with_subscription(self.list, :outer).each do |entity|
|
128
161
|
MailyHerald.logger.debug "Updating schedule of #{self} sequence for entity ##{entity.id} #{entity}"
|
129
162
|
set_schedule_for entity
|
@@ -131,17 +164,20 @@ module MailyHerald
|
|
131
164
|
end
|
132
165
|
|
133
166
|
def update_schedules_callback
|
134
|
-
Rails.env.test? ?
|
167
|
+
Rails.env.test? ? set_schedules : MailyHerald::ScheduleUpdater.perform_in(10.seconds, self.id)
|
135
168
|
end
|
136
169
|
|
170
|
+
# Returns {Log} object which is the delivery schedule for given entity.
|
137
171
|
def schedule_for entity
|
138
172
|
schedules.for_entity(entity).first
|
139
173
|
end
|
140
174
|
|
175
|
+
# Returns collection of all delivery schedules ({Log} collection).
|
141
176
|
def schedules
|
142
177
|
Log.ordered.scheduled.for_mailings(self.mailings.select(:id))
|
143
178
|
end
|
144
179
|
|
180
|
+
# Calculates processing time for given entity.
|
145
181
|
def calculate_processing_time_for entity, mailing = nil
|
146
182
|
mailing ||= next_mailing(entity)
|
147
183
|
ls = processed_logs(entity)
|
@@ -149,18 +185,21 @@ module MailyHerald
|
|
149
185
|
if ls.first
|
150
186
|
ls.last.processing_at + (mailing.absolute_delay - ls.last.mailing.absolute_delay)
|
151
187
|
else
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
188
|
+
subscription = self.list.subscription_for(entity)
|
189
|
+
|
190
|
+
if has_start_at_proc?
|
191
|
+
evaluated_start = start_at.call(entity, subscription)
|
192
|
+
else
|
156
193
|
evaluator = Utils::MarkupEvaluator.new(self.list.context.drop_for(entity, subscription))
|
157
|
-
evaluated_start = evaluator.evaluate_variable(self.start_at)
|
158
194
|
|
159
|
-
evaluated_start
|
195
|
+
evaluated_start = evaluator.evaluate_start_at(self.start_at)
|
160
196
|
end
|
197
|
+
|
198
|
+
evaluated_start ? evaluated_start + mailing.absolute_delay : nil
|
161
199
|
end
|
162
200
|
end
|
163
201
|
|
202
|
+
# Get next email processing time for given entity.
|
164
203
|
def next_processing_time entity
|
165
204
|
schedule_for(entity).try(:processing_at)
|
166
205
|
end
|
@@ -21,9 +21,7 @@ module MailyHerald
|
|
21
21
|
self.list_id = self.sequence.list_id
|
22
22
|
end
|
23
23
|
|
24
|
-
after_save if: Proc.new{|m| !m.skip_updating_schedules && (m.state_changed? || m.absolute_delay_changed?)}
|
25
|
-
self.sequence.update_schedules_callback
|
26
|
-
end
|
24
|
+
after_save :update_schedules_callback, if: Proc.new{|m| !m.skip_updating_schedules && (m.state_changed? || m.absolute_delay_changed?)}
|
27
25
|
|
28
26
|
def absolute_delay_in_days
|
29
27
|
"%.2f" % (self.absolute_delay.to_f / 1.day.seconds)
|
@@ -32,40 +30,51 @@ module MailyHerald
|
|
32
30
|
self.absolute_delay = d.to_f.days
|
33
31
|
end
|
34
32
|
|
33
|
+
# Checks if mailing has been sent to given entity.
|
35
34
|
def processed_to? entity
|
36
35
|
self.sequence.processed_mailings_for(entity).include?(self)
|
37
36
|
end
|
38
37
|
|
39
|
-
def
|
40
|
-
|
38
|
+
def update_schedules_callback
|
39
|
+
self.sequence.update_schedules_callback
|
41
40
|
end
|
42
41
|
|
43
|
-
|
44
|
-
|
42
|
+
# Returns collection of all delivery schedules ({Log} collection).
|
43
|
+
def schedules
|
44
|
+
self.sequence.schedules
|
45
|
+
end
|
45
46
|
|
46
|
-
|
47
|
+
def override_subscription?
|
48
|
+
self.sequence.override_subscription? || super
|
49
|
+
end
|
50
|
+
|
51
|
+
def processable? entity
|
52
|
+
self.sequence.enabled? && super
|
53
|
+
end
|
54
|
+
|
55
|
+
def locked?
|
56
|
+
MailyHerald.dispatch_locked?(self.sequence.name)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def deliver_with_mailer schedule
|
62
|
+
current_time = Time.now
|
47
63
|
|
48
64
|
schedule.with_lock do
|
49
65
|
# make sure schedule hasn't been processed in the meantime
|
50
66
|
if schedule && schedule.mailing == self && schedule.processing_at && schedule.processing_at <= current_time && schedule.scheduled?
|
51
67
|
|
52
|
-
|
53
|
-
if
|
54
|
-
schedule.
|
55
|
-
schedule.processing_at = current_time
|
68
|
+
schedule = super schedule
|
69
|
+
if schedule
|
70
|
+
schedule.processing_at = current_time if schedule.processed?
|
56
71
|
schedule.save!
|
57
|
-
|
72
|
+
|
73
|
+
self.sequence.set_schedule_for(schedule.entity) if schedule.processed?
|
58
74
|
end
|
59
75
|
end
|
60
76
|
end if schedule
|
61
77
|
end
|
62
78
|
|
63
|
-
def override_subscription?
|
64
|
-
self.sequence.override_subscription? || super
|
65
|
-
end
|
66
|
-
|
67
|
-
def processable? entity
|
68
|
-
self.sequence.enabled? && super
|
69
|
-
end
|
70
79
|
end
|
71
80
|
end
|