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
data/Rakefile CHANGED
@@ -12,6 +12,8 @@ rescue LoadError
12
12
  RDoc::Task = Rake::RDocTask
13
13
  end
14
14
 
15
+ require 'thor'
16
+
15
17
  RDoc::Task.new(:rdoc) do |rdoc|
16
18
  rdoc.rdoc_dir = 'rdoc'
17
19
  rdoc.title = 'MailyHerald'
@@ -20,4 +22,7 @@ RDoc::Task.new(:rdoc) do |rdoc|
20
22
  rdoc.rdoc_files.include('lib/**/*.rb')
21
23
  end
22
24
 
25
+ APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__)
26
+ load 'rails/tasks/engine.rake'
27
+
23
28
  Bundler::GemHelper.install_tasks
@@ -0,0 +1,11 @@
1
+ module MailyHerald
2
+ class TokensController < MailyHerald::ApplicationController
3
+ def get
4
+ @subscription = MailyHerald::Subscription.find_by_token(params[:token])
5
+ @subscription.try(:deactivate!)
6
+
7
+ redirect_to MailyHerald.token_redirect.try(:call, self, @subscription) || "/",
8
+ notice: @subscription ? t('maily_herald.subscription.deactivated') : t('maily_herald.subscription.undefined_token')
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module MailyHerald
2
+ # This helper helps overwriting Maily TokensController and/or views in main app
3
+ module TokensHelper
4
+ extend ActiveSupport::Concern
5
+
6
+ # Cover *_url and *_path generated methods
7
+ def method_missing(method, *args, &block)
8
+ method.to_s.end_with?("_url", "_path") ? main_app.send(method, *args, &block) : (raise NoMethodError)
9
+ rescue NoMethodError
10
+ super
11
+ end
12
+
13
+ included do
14
+ delegate :url_for, to: :main_app
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,91 @@
1
+ module MailyHerald
2
+ class Mailer < ActionMailer::Base
3
+ attr_reader :entity
4
+
5
+ def generic entity, mailing
6
+ destination = mailing.destination(entity)
7
+ subject = mailing.render_subject(entity)
8
+ content = mailing.render_template(entity)
9
+
10
+ opts = {
11
+ to: destination,
12
+ subject: subject
13
+ }
14
+ opts[:from] = mailing.from if mailing.from.present?
15
+
16
+ mail(opts) do |format|
17
+ format.text { render text: content }
18
+ end
19
+ end
20
+
21
+ class << self
22
+ #TODO make it instance method so we get access to instance attributes
23
+ def deliver_mail(mail) #:nodoc:
24
+ mailing = mail.maily_herald_data[:mailing]
25
+ entity = mail.maily_herald_data[:entity]
26
+
27
+
28
+ if mailing && entity
29
+ mailing.deliver_with_mailer_to(entity) do
30
+ ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
31
+ self.set_payload_for_mail(payload, mail)
32
+ yield # Let Mail do the delivery actions
33
+ end
34
+ mail
35
+ end
36
+ else
37
+ MailyHerald.logger.log_processing(mailing, entity, mail, prefix: "Delivery outside Maily")
38
+
39
+ ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
40
+ self.set_payload_for_mail(payload, mail)
41
+ yield # Let Mail do the delivery actions
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ def mail(headers = {}, &block)
48
+ return @_message if @_mail_was_called && headers.blank? && !block
49
+
50
+ # Assign instance variables availabe for template
51
+ @maily_subscription = @_message.maily_herald_data[:subscription]
52
+ @maily_entity = @_message.maily_herald_data[:entity]
53
+ @maily_mailing = @_message.maily_herald_data[:mailing]
54
+
55
+ super
56
+ end
57
+
58
+ protected
59
+
60
+ def process(*args) #:nodoc:
61
+ class << @_message
62
+ attr_accessor :maily_herald_data
63
+
64
+ def maily_herald_processable?
65
+ @maily_herald_processable ||= maily_herald_data[:mailing].processable?(maily_herald_data[:entity])
66
+ end
67
+
68
+ def maily_herald_conditions_met?
69
+ @maily_herald_conditions_met ||= maily_herald_data[:mailing].conditions_met?(maily_herald_data[:entity])
70
+ end
71
+ end
72
+
73
+ mailing = args[0].to_s == "generic" ? args[2] : MailyHerald.dispatch(args[0])
74
+ entity = args[1]
75
+
76
+ @_message.maily_herald_data = {
77
+ mailing: mailing,
78
+ entity: entity,
79
+ subscription: mailing.subscription_for(entity),
80
+ }
81
+
82
+ lookup_context.skip_default_locale!
83
+ super
84
+
85
+ @_message.to = mailing.destination(entity) unless @_message.to
86
+ @_message.from = mailing.from unless @_message.from
87
+
88
+ @_message
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,76 @@
1
+ module MailyHerald
2
+ class Dispatch < ActiveRecord::Base
3
+ belongs_to :list, class_name: "MailyHerald::List"
4
+
5
+ validates :list, presence: true
6
+ validates :state, presence: true, inclusion: {in: [:enabled, :disabled, :archived]}
7
+ validate do |dispatch|
8
+ dispatch.errors.add(:base, "Can't change this dispatch because it is locked.") if dispatch.locked?
9
+ end
10
+ before_destroy do |dispatch|
11
+ if dispatch.locked?
12
+ dispatch.errors.add(:base, "Can't destroy this dispatch because it is locked.")
13
+ false
14
+ end
15
+ end
16
+
17
+ delegate :subscription_for, to: :list
18
+
19
+ scope :enabled, lambda { where(state: :enabled) }
20
+ scope :disabled, lambda { where(state: :disabled) }
21
+ scope :archived, lambda { where(state: :archived) }
22
+ scope :not_archived, lambda { where("state != (?)", :archived) }
23
+
24
+ scope :sequence, lambda { where(type: Sequence) }
25
+ scope :one_time_mailing, lambda { where(type: OneTimeMailing) }
26
+ scope :periodical_mailing, lambda { where(type: PeriodicalMailing) }
27
+
28
+ def state
29
+ read_attribute(:state).to_sym
30
+ end
31
+
32
+ def enabled?
33
+ self.state == :enabled
34
+ end
35
+ def disabled?
36
+ self.state == :disabled
37
+ end
38
+ def archived?
39
+ self.state == :archived
40
+ end
41
+
42
+ def enable!
43
+ update_attribute(:state, "enabled")
44
+ end
45
+ def disable!
46
+ update_attribute(:state, "disabled")
47
+ end
48
+ def archive!
49
+ update_attribute(:state, "archived")
50
+ end
51
+
52
+ def enable
53
+ write_attribute(:state, "enabled")
54
+ end
55
+ def disable
56
+ write_attribute(:state, "disabled")
57
+ end
58
+ def archive
59
+ write_attribute(:state, "archived")
60
+ end
61
+
62
+ def list= l
63
+ l = MailyHerald::List.find_by_name(l.to_s) if l.is_a?(String) || l.is_a?(Symbol)
64
+ super(l)
65
+ end
66
+
67
+ def processable? entity
68
+ self.enabled? && (self.override_subscription? || self.list.subscribed?(entity)) && self.list.context.scope.exists?(entity)
69
+ end
70
+
71
+ def locked?
72
+ MailyHerald.dispatch_locked?(self.name)
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,99 @@
1
+ module MailyHerald
2
+ class List < ActiveRecord::Base
3
+ include MailyHerald::Autonaming
4
+
5
+ if Rails::VERSION::MAJOR == 3
6
+ attr_accessible :name, :title, :context_name
7
+ end
8
+
9
+ has_many :dispatches, class_name: "MailyHerald::Dispatch"
10
+ has_many :subscriptions, class_name: "MailyHerald::Subscription"
11
+
12
+ validate do |list|
13
+ list.errors.add(:base, "Can't change this list because it is locked.") if list.locked?
14
+ end
15
+ before_destroy do |list|
16
+ if list.locked?
17
+ list.errors.add(:base, "Can't destroy this list because it is locked.")
18
+ false
19
+ end
20
+ end
21
+
22
+ def context
23
+ @context ||= MailyHerald.context self.context_name
24
+ end
25
+
26
+ def subscribe! entity
27
+ s = subscription_for(entity)
28
+ s ? s.activate! : s = create_subscription_for(entity, true)
29
+ s
30
+ end
31
+
32
+ def unsubscribe! entity
33
+ s = subscription_for(entity)
34
+ s ? s.deactivate! : s = create_subscription_for(entity, false)
35
+ s
36
+ end
37
+
38
+ def subscribed? entity
39
+ subscription_for(entity).try(:active?)
40
+ end
41
+
42
+ # true if user has inactive subscription or never been subscribed
43
+ def unsubscribed? entity
44
+ s = subscription_for(entity)
45
+ s ? !s.active? : true
46
+ end
47
+
48
+ # true only if user was intentionally unsubscribed
49
+ def opted_out? entity
50
+ s = subscription_for(entity)
51
+ s ? !s.active? : false
52
+ end
53
+
54
+ def subscription_for entity
55
+ self.subscriptions.for_entity(entity).first
56
+ end
57
+
58
+ def active_subscription_count
59
+ subscribers.count(:id)
60
+ end
61
+
62
+ # Returns entities within the context's scope with active subscription
63
+ def subscribers
64
+ context.scope_with_subscription(self).where("#{Subscription.table_name}.active = (?)", true).where("#{Subscription.table_name}.list_id = (?)", self.id)
65
+ end
66
+
67
+ # Returns entities within the context's scope with inactive subscription
68
+ def opt_outs
69
+ context.scope_with_subscription(self).where("#{Subscription.table_name}.active = (?)", false).where("#{Subscription.table_name}.list_id = (?)", self.id)
70
+ end
71
+
72
+ # Returns entities within the context's scope without subscription
73
+ def potential_subscribers
74
+ sq = context.scope_with_subscription(self, :outer).where("#{Subscription.table_name}.list_id = (?)", self.id).pluck("#{context.model.table_name}.id")
75
+ context.scope.where(context.model.arel_table[:id].not_in(sq))
76
+ end
77
+
78
+ def logs
79
+ #Log.for_mailings(self.dispatches.select("id"))
80
+ Log.for_mailings(Dispatch.where("sequence_id IN (?) OR list_id = (?)", Sequence.where(list_id: self.id).select("id"), self.id).select("id"))
81
+ end
82
+
83
+ def locked?
84
+ MailyHerald.list_locked?(self.name)
85
+ end
86
+
87
+ private
88
+
89
+ def create_subscription_for entity, active = false
90
+ s = self.subscriptions.build.tap do |s|
91
+ s.entity = entity
92
+ s.active = active
93
+ end
94
+ s.save!
95
+ s
96
+ end
97
+
98
+ end
99
+ end
@@ -0,0 +1,67 @@
1
+ module MailyHerald
2
+ class Log < ActiveRecord::Base
3
+ AVAILABLE_STATUSES = [:scheduled, :delivered, :skipped, :error]
4
+
5
+ belongs_to :entity, polymorphic: true
6
+ belongs_to :mailing, class_name: "MailyHerald::Dispatch", foreign_key: :mailing_id
7
+
8
+ validates :entity, presence: true
9
+ validates :mailing, presence: true
10
+ validates :status, presence: true, inclusion: {in: AVAILABLE_STATUSES}
11
+
12
+ validates :processing_at, presence: true, if: :scheduled?
13
+
14
+ scope :ordered, lambda { order("processing_at asc") }
15
+ scope :for_entity, lambda {|entity| where(entity_id: entity.id, entity_type: entity.class.base_class) }
16
+ scope :for_mailing, lambda {|mailing| where(mailing_id: mailing.id) }
17
+ scope :for_mailings, lambda {|mailings| where("mailing_id in (?)", mailings) }
18
+ scope :delivered, lambda { where(status: :delivered) }
19
+ scope :skipped, lambda { where(status: :skipped) }
20
+ scope :error, lambda { where(status: :error) }
21
+ scope :scheduled, lambda { where(status: :scheduled) }
22
+ scope :processed, lambda { where(status: [:delivered, :skipped, :error]) }
23
+
24
+ serialize :data, Hash
25
+
26
+ if Rails::VERSION::MAJOR == 3
27
+ attr_accessible :status, :data
28
+ end
29
+
30
+ def self.create_for mailing, entity, attributes = {}
31
+ log = Log.new
32
+ log.set_attributes_for mailing, entity, attributes
33
+ log.save!
34
+ log
35
+ end
36
+
37
+ def set_attributes_for mailing, entity, attributes = {}
38
+ self.mailing = mailing
39
+ self.entity = entity
40
+ self.entity_email = mailing.destination(entity)
41
+
42
+ self.processing_at = attributes[:processing_at] || DateTime.now
43
+ self.status = attributes[:status]
44
+ self.data = attributes[:data]
45
+ end
46
+
47
+ def status
48
+ read_attribute(:status).to_sym
49
+ end
50
+
51
+ def delivered?
52
+ self.status == :delivered
53
+ end
54
+
55
+ def skipped?
56
+ self.status == :skipped
57
+ end
58
+
59
+ def error?
60
+ self.status == :error
61
+ end
62
+
63
+ def scheduled?
64
+ self.status == :scheduled
65
+ end
66
+ end
67
+ end
@@ -1,12 +1,144 @@
1
1
  module MailyHerald
2
- class Mailing < ActiveRecord::Base
3
- attr_accessible :name, :context, :sequence, :conditions, :title, :sender, :delay, :template
2
+ class Mailing < Dispatch
3
+ include MailyHerald::TemplateRenderer
4
+ include MailyHerald::Autonaming
4
5
 
5
- belongs_to :sequence, :class_name => "MailyHerald::Sequence"
6
- has_many :records, :as => :mailing, :class_name => "MailyHerald::MailingRecord"
6
+ if Rails::VERSION::MAJOR == 3
7
+ attr_accessible :name, :title, :subject, :context_name, :override_subscription,
8
+ :sequence, :conditions, :mailer_name, :title, :from, :relative_delay, :template, :start_at, :period
9
+ end
7
10
 
8
- def has_conditions?
9
- !conditions.empty?
10
- end
11
+ has_many :logs, class_name: "MailyHerald::Log"
12
+
13
+ validates :subject, presence: true, if: :generic_mailer?
14
+ validates :template, presence: true, if: :generic_mailer?
15
+ validate :template_syntax
16
+ validate :validate_conditions
17
+
18
+ before_validation do
19
+ write_attribute(:name, self.title.downcase.gsub(/\W/, "_")) if self.title && (!self.name || self.name.empty?)
20
+ end
21
+
22
+ after_initialize do
23
+ if self.new_record?
24
+ self.override_subscription = false
25
+ self.mailer_name = :generic
26
+ end
27
+ end
28
+
29
+ def periodical?
30
+ self.class == PeriodicalMailing
31
+ end
32
+
33
+ def one_time?
34
+ self.class == OneTimeMailing
35
+ end
36
+
37
+ def sequence?
38
+ self.class == SequenceMailing
39
+ end
40
+
41
+ def mailer_name
42
+ read_attribute(:mailer_name).to_sym
43
+ end
44
+
45
+ def mailer
46
+ if generic_mailer?
47
+ MailyHerald::Mailer
48
+ else
49
+ self.mailer_name.to_s.constantize
50
+ end
51
+ end
52
+
53
+ def has_conditions?
54
+ self.conditions && !self.conditions.empty?
55
+ end
56
+
57
+ def generic_mailer?
58
+ self.mailer_name == :generic
59
+ end
60
+
61
+ def conditions_met? entity
62
+ subscription = self.list.subscription_for(entity)
63
+
64
+ if self.list.context.attributes
65
+ evaluator = Utils::MarkupEvaluator.new(self.list.context.drop_for(entity, subscription))
66
+ evaluator.evaluate_conditions(self.conditions)
67
+ else
68
+ true
69
+ end
70
+ end
71
+
72
+ def destination entity
73
+ self.list.context.destination_for(entity)
74
+ end
75
+
76
+ def render_template entity
77
+ subscription = self.list.subscription_for(entity)
78
+ return unless subscription
79
+
80
+ drop = self.list.context.drop_for entity, subscription
81
+ perform_template_rendering drop, self.template
82
+ end
83
+
84
+ def render_subject entity
85
+ subscription = self.list.subscription_for(entity)
86
+ return unless subscription
87
+
88
+ drop = self.list.context.drop_for entity, subscription
89
+ perform_template_rendering drop, self.subject
90
+ end
91
+
92
+ def build_mail entity
93
+ if generic_mailer?
94
+ Mailer.generic(entity, self)
95
+ else
96
+ self.mailer.send(self.name, entity)
97
+ end
98
+ end
99
+
100
+ def deliver_to entity
101
+ build_mail(entity).deliver
102
+ end
103
+
104
+ protected
105
+
106
+ # Called from Mailer, block required
107
+ def deliver_with_mailer_to entity
108
+ unless processable?(entity)
109
+ MailyHerald.logger.log_processing(self, entity, prefix: "Not processable", level: :debug)
110
+ return
111
+ end
112
+
113
+ unless conditions_met?(entity)
114
+ MailyHerald.logger.log_processing(self, entity, prefix: "Conditions not met", level: :debug)
115
+ return {status: :skipped}
116
+ end
117
+
118
+ mail = yield # Let mailer do his job
119
+
120
+ MailyHerald.logger.log_processing(self, entity, mail, prefix: "Processed")
121
+
122
+ return {status: :delivered, data: {content: mail.to_s}}
123
+ rescue StandardError => e
124
+ MailyHerald.logger.log_processing(self, entity, prefix: "Error", level: :error)
125
+ return {status: :error, data: {msg: "#{e.to_s}\n\n#{e.backtrace.join("\n")}"}}
126
+ end
127
+
128
+ private
129
+
130
+ def template_syntax
131
+ begin
132
+ template = Liquid::Template.parse(self.template)
133
+ rescue StandardError => e
134
+ errors.add(:template, e.to_s)
135
+ end
136
+ end
137
+
138
+ def validate_conditions
139
+ evaluator = Utils::MarkupEvaluator.test_conditions(self.conditions)
140
+ rescue StandardError => e
141
+ errors.add(:conditions, e.to_s)
142
+ end
11
143
  end
12
144
  end