maily_herald 0.0.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +10 -4
  3. data/.rspec +5 -0
  4. data/Gemfile +1 -12
  5. data/Gemfile.lock +129 -82
  6. data/Guardfile +25 -0
  7. data/LICENSE +10 -0
  8. data/README.md +346 -0
  9. data/Rakefile +5 -0
  10. data/app/controllers/maily_herald/tokens_controller.rb +11 -0
  11. data/app/helpers/maily_herald/tokens_helper.rb +17 -0
  12. data/app/mailers/maily_herald/mailer.rb +91 -0
  13. data/app/models/maily_herald/dispatch.rb +76 -0
  14. data/app/models/maily_herald/list.rb +99 -0
  15. data/app/models/maily_herald/log.rb +67 -0
  16. data/app/models/maily_herald/mailing.rb +139 -7
  17. data/app/models/maily_herald/one_time_mailing.rb +26 -0
  18. data/app/models/maily_herald/periodical_mailing.rb +145 -0
  19. data/app/models/maily_herald/sequence.rb +169 -2
  20. data/app/models/maily_herald/sequence_mailing.rb +71 -0
  21. data/app/models/maily_herald/subscription.rb +67 -0
  22. data/bin/maily_herald +16 -0
  23. data/config/database.yml +5 -0
  24. data/config/locales/en.yml +6 -11
  25. data/config/routes.rb +10 -0
  26. data/config/spring.rb +1 -0
  27. data/db/migrate/20150205120443_create_maily_herald_tables.rb +53 -0
  28. data/db/migrate_legacy/20130711124555_create_maily_herald_tables.rb +67 -0
  29. data/db/migrate_legacy/20140612101023_create_lists.rb +33 -0
  30. data/lib/generators/maily_herald/install_generator.rb +3 -3
  31. data/lib/generators/templates/README +2 -0
  32. data/lib/generators/templates/maily_herald.rb +1 -0
  33. data/lib/maily_herald.rb +345 -23
  34. data/lib/maily_herald/autonaming.rb +34 -0
  35. data/lib/maily_herald/capistrano.rb +5 -0
  36. data/lib/maily_herald/capistrano/tasks.cap +67 -0
  37. data/lib/maily_herald/capistrano/tasks2.rb +20 -0
  38. data/lib/maily_herald/cli.rb +293 -0
  39. data/lib/maily_herald/condition_evaluator.rb +82 -0
  40. data/lib/maily_herald/config.rb +5 -0
  41. data/lib/maily_herald/context.rb +223 -77
  42. data/lib/maily_herald/engine.rb +17 -0
  43. data/lib/maily_herald/logging.rb +90 -0
  44. data/lib/maily_herald/manager.rb +53 -0
  45. data/lib/maily_herald/model_extensions.rb +15 -0
  46. data/lib/maily_herald/template_renderer.rb +16 -0
  47. data/lib/maily_herald/utils.rb +78 -5
  48. data/lib/maily_herald/version.rb +1 -1
  49. data/maily_herald.gemspec +17 -9
  50. data/spec/controllers/maily_herald/tokens_controller_spec.rb +81 -0
  51. data/spec/dummy/Guardfile +35 -0
  52. data/spec/dummy/app/mailers/test_mailer.rb +11 -0
  53. data/spec/dummy/app/models/product.rb +2 -0
  54. data/spec/dummy/app/models/user.rb +4 -0
  55. data/spec/dummy/app/views/test_mailer/sample_mail.text.erb +1 -0
  56. data/spec/dummy/bin/rails +10 -0
  57. data/spec/dummy/bin/rake +7 -0
  58. data/spec/dummy/bin/rspec +7 -0
  59. data/spec/dummy/bin/spring +18 -0
  60. data/spec/dummy/config/application.rb +1 -1
  61. data/spec/dummy/config/environments/development.rb +1 -0
  62. data/spec/dummy/config/environments/test.rb +1 -0
  63. data/spec/dummy/config/initializers/maily_herald.rb +103 -0
  64. data/spec/dummy/config/locales/maily_herald.en.yml +28 -0
  65. data/spec/dummy/db/migrate/20130723074347_create_users.rb +18 -0
  66. data/spec/dummy/db/schema.rb +82 -0
  67. data/spec/factories/products.rb +5 -0
  68. data/spec/factories/users.rb +11 -0
  69. data/spec/lib/context_spec.rb +41 -0
  70. data/spec/lib/maily_herald_spec.rb +32 -0
  71. data/spec/lib/utils_spec.rb +48 -0
  72. data/spec/mailers/maily_herald/mailer_spec.rb +38 -0
  73. data/spec/models/maily_herald/list_spec.rb +64 -0
  74. data/spec/models/maily_herald/log_spec.rb +36 -0
  75. data/spec/models/maily_herald/mailing_spec.rb +34 -0
  76. data/spec/models/maily_herald/one_time_mailing_spec.rb +112 -0
  77. data/spec/models/maily_herald/periodical_mailing_spec.rb +339 -0
  78. data/spec/models/maily_herald/sequence_mailing_spec.rb +18 -0
  79. data/spec/models/maily_herald/sequence_spec.rb +429 -0
  80. data/spec/models/maily_herald/subscription_spec.rb +32 -0
  81. data/spec/spec_helper.rb +31 -11
  82. metadata +199 -54
  83. data/MIT-LICENSE +0 -20
  84. data/README.rdoc +0 -3
  85. data/app/assets/images/maily_herald/.gitkeep +0 -0
  86. data/app/assets/javascripts/maily_herald/application.js +0 -15
  87. data/app/assets/stylesheets/maily_herald/application.css +0 -13
  88. data/app/helpers/maily_herald/application_helper.rb +0 -4
  89. data/app/helpers/maily_herald_helper.rb +0 -9
  90. data/app/models/maily_herald/mailing_record.rb +0 -6
  91. data/app/views/layouts/maily_herald/application.html.erb +0 -14
  92. data/db/migrate/20130711124555_create_maily_herald_tables.rb +0 -38
  93. data/lib/maily_herald/worker.rb +0 -15
@@ -0,0 +1,26 @@
1
+ module MailyHerald
2
+ class OneTimeMailing < Mailing
3
+ validates :list, presence: true
4
+
5
+ # Returns array of Mail::Message
6
+ def run
7
+ self.list.subscriptions.collect do |subscription|
8
+ entity = subscription.entity
9
+
10
+ next unless processable?(entity)
11
+
12
+ deliver_to entity
13
+ end
14
+ end
15
+
16
+ # Returns single Mail::Message
17
+ def deliver_with_mailer_to entity
18
+ attrs = super entity
19
+ Log.create_for(self, entity, attrs) if attrs
20
+ end
21
+
22
+ def to_s
23
+ "<OneTimeMailing: #{self.title || self.name}>"
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,145 @@
1
+ module MailyHerald
2
+ class PeriodicalMailing < Mailing
3
+ if Rails::VERSION::MAJOR == 3
4
+ attr_accessible :period, :period_in_days
5
+ end
6
+
7
+ validates :list, presence: true
8
+ validates :start_at, presence: true
9
+ validates :period, presence: true, numericality: {greater_than: 0}
10
+
11
+ after_save :update_schedules_callback, if: Proc.new{|m| m.state_changed? || m.period_changed? || m.start_at_changed? || m.override_subscription?}
12
+
13
+ def period_in_days
14
+ "%.2f" % (self.period.to_f / 1.day.seconds)
15
+ end
16
+ def period_in_days= d
17
+ self.period = d.to_f.days
18
+ end
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
+
43
+ def run
44
+ # TODO better scope here to exclude schedules for users outside context scope
45
+ schedules.where("processing_at <= (?)", Time.now).each do |schedule|
46
+ if schedule.entity
47
+ deliver_to schedule.entity
48
+ else
49
+ MailyHerald.logger.log_processing(schedule.mailing, {:class => schedule.entity_type, :id => schedule.entity_id}, prefix: "Removing schedule for non-existing entity")
50
+ schedule.destroy
51
+ end
52
+ end
53
+ end
54
+
55
+ def processed_logs entity
56
+ Log.ordered.for_entity(entity).for_mailing(self).processed
57
+ end
58
+
59
+ def start_processing_time entity
60
+ if processed_logs(entity).first
61
+ processed_logs(entity).first.processed_at
62
+ else
63
+ begin
64
+ Time.parse(self.start_at)
65
+ rescue
66
+ subscription = self.list.subscription_for(entity)
67
+ evaluator = Utils::MarkupEvaluator.new(self.list.context.drop_for(entity, subscription))
68
+
69
+ evaluator.evaluate_variable(self.start_at)
70
+ end
71
+ end
72
+ end
73
+
74
+ def last_processing_time entity
75
+ processed_logs(entity).last.try(:processing_at)
76
+ end
77
+
78
+ 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
85
+
86
+ if !self.period || !self.start_at || !enabled? || !(self.override_subscription? || subscribed)
87
+ log = schedule_for(entity)
88
+ log.try(:destroy)
89
+ return
90
+ end
91
+
92
+ log = schedule_for(entity)
93
+ last_log ||= processed_logs(entity).last
94
+
95
+ log ||= Log.new
96
+ log.with_lock do
97
+ log.set_attributes_for(self, entity, {
98
+ status: :scheduled,
99
+ processing_at: calculate_processing_time(entity, last_log)
100
+ })
101
+ log.save!
102
+ end
103
+ log
104
+ end
105
+
106
+ def update_schedules
107
+ self.list.context.scope_with_subscription(self.list, :outer).each do |entity|
108
+ MailyHerald.logger.debug "Updating schedule of #{self} periodical for entity ##{entity.id} #{entity}"
109
+ set_schedule_for entity
110
+ end
111
+ end
112
+
113
+ def update_schedules_callback
114
+ Rails.env.test? ? update_schedules : MailyHerald::ScheduleUpdater.perform_in(10.seconds, self.id)
115
+ end
116
+
117
+ def schedule_for entity
118
+ schedules.for_entity(entity).first
119
+ end
120
+
121
+ def schedules
122
+ Log.ordered.scheduled.for_mailing(self)
123
+ end
124
+
125
+ def calculate_processing_time entity, last_log = nil
126
+ last_log ||= processed_logs(entity).last
127
+
128
+ if last_log && last_log.processing_at
129
+ last_log.processing_at + self.period
130
+ elsif start_processing_time(entity)
131
+ start_processing_time(entity)
132
+ else
133
+ nil
134
+ end
135
+ end
136
+
137
+ def next_processing_time entity
138
+ schedule_for(entity).processing_at
139
+ end
140
+
141
+ def to_s
142
+ "<PeriodicalMailing: #{self.title || self.name}>"
143
+ end
144
+ end
145
+ end
@@ -1,5 +1,172 @@
1
1
  module MailyHerald
2
- class Sequence < ActiveRecord::Base
3
- has_many :records, :as => :mailing, :class_name => "MailingRecord"
2
+ MailyHerald::Subscription #TODO fix this autoload for dev
3
+
4
+ class Sequence < Dispatch
5
+ if Rails::VERSION::MAJOR == 3
6
+ attr_accessible :name, :title, :override_subscription,
7
+ :conditions, :start_at, :period
8
+ end
9
+
10
+ include MailyHerald::Autonaming
11
+
12
+ has_many :logs, class_name: "MailyHerald::Log", through: :mailings
13
+ if Rails::VERSION::MAJOR == 3
14
+ has_many :mailings, class_name: "MailyHerald::SequenceMailing", order: "absolute_delay ASC", dependent: :destroy
15
+ else
16
+ has_many :mailings, -> { order("absolute_delay ASC") }, class_name: "MailyHerald::SequenceMailing", dependent: :destroy
17
+ end
18
+
19
+ validates :list, presence: true
20
+
21
+ before_validation do
22
+ write_attribute(:name, self.title.downcase.gsub(/\W/, "_")) if self.title && (!self.name || self.name.empty?)
23
+ end
24
+
25
+ after_initialize do
26
+ if self.new_record?
27
+ self.override_subscription = false
28
+ end
29
+ end
30
+ after_save :update_schedules_callback, if: Proc.new{|s| s.state_changed? || s.start_at_changed?}
31
+
32
+ def mailing name, options = {}
33
+ if SequenceMailing.table_exists?
34
+ mailing = SequenceMailing.find_by_name(name)
35
+ lock = options.delete(:locked)
36
+
37
+ if block_given? && !MailyHerald.dispatch_locked?(name) && (!mailing || lock)
38
+ mailing ||= self.mailings.build(name: name)
39
+ mailing.sequence = self
40
+ yield(mailing)
41
+ mailing.skip_updating_schedules = true if self.new_record?
42
+ mailing.save!
43
+
44
+ MailyHerald.lock_dispatch(name) if lock
45
+ end
46
+
47
+ mailing
48
+ end
49
+ end
50
+
51
+ def run
52
+ # TODO better scope here to exclude schedules for users outside context scope
53
+ schedules.where("processing_at <= (?)", Time.now).each do |schedule|
54
+ if schedule.entity
55
+ schedule.mailing.deliver_to schedule.entity
56
+ else
57
+ MailyHerald.logger.log_processing(schedule.mailing, {:class => schedule.entity_type, :id => schedule.entity_id}, prefix: "Removing schedule for non-existing entity")
58
+ schedule.destroy
59
+ end
60
+ end
61
+ end
62
+
63
+ def processed_logs entity
64
+ Log.ordered.processed.for_entity(entity).for_mailings(self.mailings.select(:id))
65
+ end
66
+
67
+ def processed_logs_for entity, mailing
68
+ Log.ordered.processed.for_entity(entity).for_mailing(self.mailings.find(mailing))
69
+ end
70
+
71
+ def last_processing_time entity
72
+ ls = processed_logs(entity)
73
+ ls.last.processing_at if ls.last
74
+ end
75
+
76
+ def pending_mailings entity
77
+ ls = processed_logs(entity)
78
+ ls.empty? ? self.mailings.enabled : self.mailings.enabled.where("id not in (?)", ls.map(&:mailing_id))
79
+ end
80
+
81
+ def processed_mailings entity
82
+ ls = processed_logs(entity)
83
+ ls.empty? ? self.mailings.where(id: nil) : self.mailings.where("id in (?)", ls.map(&:mailing_id))
84
+ end
85
+
86
+ def last_processed_mailing entity
87
+ processed_mailings(entity).last
88
+ end
89
+
90
+ def next_mailing entity
91
+ pending_mailings(entity).first
92
+ end
93
+
94
+ def mailing_processing_log_for entity, mailing
95
+ Log.ordered.processed.for_entity(entity).for_mailing(mailing).last
96
+ end
97
+
98
+ def set_schedule_for entity
99
+ # TODO handle override subscription?
100
+
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
107
+
108
+ if !subscribed || !self.start_at || !enabled? || !(mailing = next_mailing(entity))
109
+ log = schedule_for(entity)
110
+ log.try(:destroy)
111
+ return
112
+ end
113
+
114
+ log = schedule_for(entity)
115
+ log ||= Log.new
116
+ log.with_lock do
117
+ log.set_attributes_for(mailing, entity, {
118
+ status: :scheduled,
119
+ processing_at: calculate_processing_time_for(entity, mailing)
120
+ })
121
+ log.save!
122
+ end
123
+ log
124
+ end
125
+
126
+ def update_schedules
127
+ self.list.context.scope_with_subscription(self.list, :outer).each do |entity|
128
+ MailyHerald.logger.debug "Updating schedule of #{self} sequence for entity ##{entity.id} #{entity}"
129
+ set_schedule_for entity
130
+ end
131
+ end
132
+
133
+ def update_schedules_callback
134
+ Rails.env.test? ? update_schedules : MailyHerald::ScheduleUpdater.perform_in(10.seconds, self.id)
135
+ end
136
+
137
+ def schedule_for entity
138
+ schedules.for_entity(entity).first
139
+ end
140
+
141
+ def schedules
142
+ Log.ordered.scheduled.for_mailings(self.mailings.select(:id))
143
+ end
144
+
145
+ def calculate_processing_time_for entity, mailing = nil
146
+ mailing ||= next_mailing(entity)
147
+ ls = processed_logs(entity)
148
+
149
+ if ls.first
150
+ ls.last.processing_at + (mailing.absolute_delay - ls.last.mailing.absolute_delay)
151
+ else
152
+ begin
153
+ Time.parse(self.start_at) + mailing.absolute_delay
154
+ rescue
155
+ subscription = self.list.subscription_for(entity)
156
+ evaluator = Utils::MarkupEvaluator.new(self.list.context.drop_for(entity, subscription))
157
+ evaluated_start = evaluator.evaluate_variable(self.start_at)
158
+
159
+ evaluated_start + mailing.absolute_delay
160
+ end
161
+ end
162
+ end
163
+
164
+ def next_processing_time entity
165
+ schedule_for(entity).try(:processing_at)
166
+ end
167
+
168
+ def to_s
169
+ "<Sequence: #{self.title || self.name}>"
170
+ end
4
171
  end
5
172
  end
@@ -0,0 +1,71 @@
1
+ module MailyHerald
2
+ class SequenceMailing < Mailing
3
+ if Rails::VERSION::MAJOR == 3
4
+ attr_accessible :absolute_delay_in_days
5
+ end
6
+
7
+ attr_accessor :skip_updating_schedules
8
+
9
+ belongs_to :sequence, class_name: "MailyHerald::Sequence"
10
+
11
+ validates :absolute_delay,presence: true, numericality: true
12
+ validates :sequence, presence: true
13
+ validate do
14
+ self.errors.add(:list_id, :invalid) if self.list_id != self.sequence.try(:list_id)
15
+ end
16
+
17
+ delegate :subscription, to: :sequence
18
+ delegate :list, to: :sequence
19
+
20
+ before_validation do
21
+ self.list_id = self.sequence.list_id
22
+ end
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
27
+
28
+ def absolute_delay_in_days
29
+ "%.2f" % (self.absolute_delay.to_f / 1.day.seconds)
30
+ end
31
+ def absolute_delay_in_days= d
32
+ self.absolute_delay = d.to_f.days
33
+ end
34
+
35
+ def processed_to? entity
36
+ self.sequence.processed_mailings_for(entity).include?(self)
37
+ end
38
+
39
+ def deliver_to entity
40
+ super(entity)
41
+ end
42
+
43
+ def deliver_with_mailer_to entity
44
+ current_time = Time.now
45
+
46
+ schedule = self.sequence.schedule_for(entity)
47
+
48
+ schedule.with_lock do
49
+ # make sure schedule hasn't been processed in the meantime
50
+ if schedule && schedule.mailing == self && schedule.processing_at && schedule.processing_at <= current_time && schedule.scheduled?
51
+
52
+ attrs = super entity
53
+ if attrs
54
+ schedule.attributes = attrs
55
+ schedule.processing_at = current_time
56
+ schedule.save!
57
+ self.sequence.set_schedule_for(entity)
58
+ end
59
+ end
60
+ end if schedule
61
+ end
62
+
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
+ end
71
+ end
@@ -0,0 +1,67 @@
1
+ module MailyHerald
2
+ class Subscription < ActiveRecord::Base
3
+ belongs_to :entity, polymorphic: true
4
+ belongs_to :list, class_name: "MailyHerald::List"
5
+
6
+ validates :entity, presence: true
7
+ validates :list, presence: true
8
+ validates :token, presence: true, uniqueness: true
9
+ validate do
10
+ self.errors.add(:entity, :wrong_type) if self.entity_type != self.list.context.model.base_class.to_s
11
+ end
12
+
13
+ scope :for_entity, lambda {|entity| where(entity_id: entity.id, entity_type: entity.class.base_class.to_s) }
14
+ scope :active, lambda { where(active: true) }
15
+ scope :for_model, lambda {|model| joins("JOIN #{model.table_name} ON #{model.table_name}.id = #{Subscription.table_name}.entity_id AND #{Subscription.table_name}.entity_type = '#{model.base_class.to_s}'") }
16
+
17
+ serialize :data, Hash
18
+ serialize :settings, Hash
19
+
20
+ after_initialize do
21
+ if self.new_record?
22
+ self.token = MailyHerald::Utils.random_hex(20)
23
+ end
24
+ end
25
+
26
+ after_save :update_schedules, if: Proc.new{|s| s.active_changed?}
27
+
28
+ def active?
29
+ !new_record? && read_attribute(:active)
30
+ end
31
+
32
+ def deactivate!
33
+ update_attribute(:active, false)
34
+ end
35
+
36
+ def activate!
37
+ update_attribute(:active, true)
38
+ end
39
+
40
+ def toggle!
41
+ active? ? deactivate! : activate!
42
+ end
43
+
44
+ def token_url
45
+ MailyHerald::Engine.routes.url_helpers.ubsubscribe_url(self)
46
+ end
47
+
48
+ def to_liquid
49
+ {
50
+ "token_url" => token_url
51
+ }
52
+ end
53
+
54
+ def update_schedules
55
+ PeriodicalMailing.where(list_id: self.list).each do |m|
56
+ m.set_schedule_for self.entity
57
+ end
58
+ Sequence.where(list_id: self.list).each do |s|
59
+ s.set_schedule_for self.entity
60
+ end
61
+ end
62
+
63
+ def logs
64
+ self.list.logs.for_entity(self.entity)
65
+ end
66
+ end
67
+ end