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.
Files changed (51) hide show
  1. checksums.yaml +8 -8
  2. data/.gitignore +1 -0
  3. data/.travis.yml +3 -0
  4. data/Gemfile +0 -2
  5. data/Gemfile.lock +5 -13
  6. data/README.md +186 -69
  7. data/app/mailers/maily_herald/mailer.rb +44 -26
  8. data/app/models/maily_herald/ad_hoc_mailing.rb +109 -0
  9. data/app/models/maily_herald/dispatch.rb +97 -4
  10. data/app/models/maily_herald/list.rb +36 -7
  11. data/app/models/maily_herald/log.rb +79 -0
  12. data/app/models/maily_herald/mailing.rb +149 -24
  13. data/app/models/maily_herald/one_time_mailing.rb +114 -9
  14. data/app/models/maily_herald/periodical_mailing.rb +76 -47
  15. data/app/models/maily_herald/sequence.rb +57 -18
  16. data/app/models/maily_herald/sequence_mailing.rb +29 -20
  17. data/app/models/maily_herald/subscription.rb +23 -2
  18. data/config/routes.rb +2 -2
  19. data/lib/maily_herald.rb +57 -18
  20. data/lib/maily_herald/autonaming.rb +8 -0
  21. data/lib/maily_herald/context.rb +6 -2
  22. data/lib/maily_herald/logging.rb +2 -0
  23. data/lib/maily_herald/manager.rb +15 -31
  24. data/lib/maily_herald/utils.rb +65 -24
  25. data/lib/maily_herald/version.rb +1 -1
  26. data/maily_herald.gemspec +1 -1
  27. data/spec/controllers/maily_herald/tokens_controller_spec.rb +13 -13
  28. data/spec/dummy/app/mailers/ad_hoc_mailer.rb +11 -0
  29. data/spec/dummy/app/mailers/custom_one_time_mailer.rb +11 -0
  30. data/spec/dummy/config/application.rb +1 -1
  31. data/spec/dummy/config/environments/test.rb +1 -1
  32. data/spec/dummy/config/initializers/maily_herald.rb +3 -69
  33. data/spec/dummy/config/maily_herald.yml +3 -0
  34. data/spec/dummy/db/seeds.rb +73 -0
  35. data/spec/lib/context_spec.rb +7 -7
  36. data/spec/lib/maily_herald_spec.rb +7 -8
  37. data/spec/lib/utils_spec.rb +65 -25
  38. data/spec/mailers/maily_herald/mailer_spec.rb +20 -13
  39. data/spec/models/maily_herald/ad_hoc_mailing_spec.rb +169 -0
  40. data/spec/models/maily_herald/list_spec.rb +2 -1
  41. data/spec/models/maily_herald/log_spec.rb +10 -10
  42. data/spec/models/maily_herald/mailing_spec.rb +9 -8
  43. data/spec/models/maily_herald/one_time_mailing_spec.rb +212 -39
  44. data/spec/models/maily_herald/periodical_mailing_spec.rb +158 -92
  45. data/spec/models/maily_herald/sequence_mailing_spec.rb +2 -2
  46. data/spec/models/maily_herald/sequence_spec.rb +152 -139
  47. data/spec/models/maily_herald/subscription_spec.rb +21 -4
  48. metadata +17 -8
  49. data/lib/maily_herald/condition_evaluator.rb +0 -82
  50. data/lib/maily_herald/config.rb +0 -5
  51. 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
- def deliver_to entity
21
- super(entity)
22
- end
23
-
24
- def deliver_with_mailer_to entity
25
- current_time = Time.now
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).each do |schedule|
28
+ schedules.where("processing_at <= (?)", Time.now).collect do |schedule|
46
29
  if schedule.entity
47
- deliver_to schedule.entity
30
+ mail = deliver schedule
31
+ schedule.reload
32
+ schedule.mail = mail
33
+ schedule
48
34
  else
49
- MailyHerald.logger.log_processing(schedule.mailing, {:class => schedule.entity_type, :id => schedule.entity_id}, prefix: "Removing schedule for non-existing entity")
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.processed_at
53
+ processed_logs(entity).first.processing_at
62
54
  else
63
- begin
64
- Time.parse(self.start_at)
65
- rescue
66
- subscription = self.list.subscription_for(entity)
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.evaluate_variable(self.start_at)
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
- # support entity with joined subscription table for better performance
80
- if entity.has_attribute?(:maily_subscription_id)
81
- subscribed = !!entity.maily_subscription_active
82
- else
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: calculate_processing_time(entity, last_log)
93
+ processing_at: processing_at,
100
94
  })
101
95
  log.save!
102
96
  end
103
97
  log
104
98
  end
105
99
 
106
- def update_schedules
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? ? update_schedules : MailyHerald::ScheduleUpdater.perform_in(10.seconds, self.id)
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 start_processing_time(entity)
131
- start_processing_time(entity)
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.deliver_to schedule.entity
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, {:class => schedule.entity_type, :id => schedule.entity_id}, prefix: "Removing schedule for non-existing entity")
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
- # support entity with joined subscription table for better performance
102
- if entity.has_attribute?(:maily_subscription_id)
103
- subscribed = !!entity.maily_subscription_active
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? || !(mailing = next_mailing(entity))
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: calculate_processing_time_for(entity, mailing)
149
+ processing_at: start_time,
120
150
  })
121
151
  log.save!
122
152
  end
123
153
  log
124
154
  end
125
155
 
126
- def update_schedules
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? ? update_schedules : MailyHerald::ScheduleUpdater.perform_in(10.seconds, self.id)
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
- begin
153
- Time.parse(self.start_at) + mailing.absolute_delay
154
- rescue
155
- subscription = self.list.subscription_for(entity)
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 + mailing.absolute_delay
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?)} do
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 deliver_to entity
40
- super(entity)
38
+ def update_schedules_callback
39
+ self.sequence.update_schedules_callback
41
40
  end
42
41
 
43
- def deliver_with_mailer_to entity
44
- current_time = Time.now
42
+ # Returns collection of all delivery schedules ({Log} collection).
43
+ def schedules
44
+ self.sequence.schedules
45
+ end
45
46
 
46
- schedule = self.sequence.schedule_for(entity)
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
- attrs = super entity
53
- if attrs
54
- schedule.attributes = attrs
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
- self.sequence.set_schedule_for(entity)
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