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.
- checksums.yaml +15 -0
- data/.gitignore +10 -4
- data/.rspec +5 -0
- data/Gemfile +1 -12
- data/Gemfile.lock +129 -82
- data/Guardfile +25 -0
- data/LICENSE +10 -0
- data/README.md +346 -0
- data/Rakefile +5 -0
- data/app/controllers/maily_herald/tokens_controller.rb +11 -0
- data/app/helpers/maily_herald/tokens_helper.rb +17 -0
- data/app/mailers/maily_herald/mailer.rb +91 -0
- data/app/models/maily_herald/dispatch.rb +76 -0
- data/app/models/maily_herald/list.rb +99 -0
- data/app/models/maily_herald/log.rb +67 -0
- data/app/models/maily_herald/mailing.rb +139 -7
- data/app/models/maily_herald/one_time_mailing.rb +26 -0
- data/app/models/maily_herald/periodical_mailing.rb +145 -0
- data/app/models/maily_herald/sequence.rb +169 -2
- data/app/models/maily_herald/sequence_mailing.rb +71 -0
- data/app/models/maily_herald/subscription.rb +67 -0
- data/bin/maily_herald +16 -0
- data/config/database.yml +5 -0
- data/config/locales/en.yml +6 -11
- data/config/routes.rb +10 -0
- data/config/spring.rb +1 -0
- data/db/migrate/20150205120443_create_maily_herald_tables.rb +53 -0
- data/db/migrate_legacy/20130711124555_create_maily_herald_tables.rb +67 -0
- data/db/migrate_legacy/20140612101023_create_lists.rb +33 -0
- data/lib/generators/maily_herald/install_generator.rb +3 -3
- data/lib/generators/templates/README +2 -0
- data/lib/generators/templates/maily_herald.rb +1 -0
- data/lib/maily_herald.rb +345 -23
- data/lib/maily_herald/autonaming.rb +34 -0
- data/lib/maily_herald/capistrano.rb +5 -0
- data/lib/maily_herald/capistrano/tasks.cap +67 -0
- data/lib/maily_herald/capistrano/tasks2.rb +20 -0
- data/lib/maily_herald/cli.rb +293 -0
- data/lib/maily_herald/condition_evaluator.rb +82 -0
- data/lib/maily_herald/config.rb +5 -0
- data/lib/maily_herald/context.rb +223 -77
- data/lib/maily_herald/engine.rb +17 -0
- data/lib/maily_herald/logging.rb +90 -0
- data/lib/maily_herald/manager.rb +53 -0
- data/lib/maily_herald/model_extensions.rb +15 -0
- data/lib/maily_herald/template_renderer.rb +16 -0
- data/lib/maily_herald/utils.rb +78 -5
- data/lib/maily_herald/version.rb +1 -1
- data/maily_herald.gemspec +17 -9
- data/spec/controllers/maily_herald/tokens_controller_spec.rb +81 -0
- data/spec/dummy/Guardfile +35 -0
- data/spec/dummy/app/mailers/test_mailer.rb +11 -0
- data/spec/dummy/app/models/product.rb +2 -0
- data/spec/dummy/app/models/user.rb +4 -0
- data/spec/dummy/app/views/test_mailer/sample_mail.text.erb +1 -0
- data/spec/dummy/bin/rails +10 -0
- data/spec/dummy/bin/rake +7 -0
- data/spec/dummy/bin/rspec +7 -0
- data/spec/dummy/bin/spring +18 -0
- data/spec/dummy/config/application.rb +1 -1
- data/spec/dummy/config/environments/development.rb +1 -0
- data/spec/dummy/config/environments/test.rb +1 -0
- data/spec/dummy/config/initializers/maily_herald.rb +103 -0
- data/spec/dummy/config/locales/maily_herald.en.yml +28 -0
- data/spec/dummy/db/migrate/20130723074347_create_users.rb +18 -0
- data/spec/dummy/db/schema.rb +82 -0
- data/spec/factories/products.rb +5 -0
- data/spec/factories/users.rb +11 -0
- data/spec/lib/context_spec.rb +41 -0
- data/spec/lib/maily_herald_spec.rb +32 -0
- data/spec/lib/utils_spec.rb +48 -0
- data/spec/mailers/maily_herald/mailer_spec.rb +38 -0
- data/spec/models/maily_herald/list_spec.rb +64 -0
- data/spec/models/maily_herald/log_spec.rb +36 -0
- data/spec/models/maily_herald/mailing_spec.rb +34 -0
- data/spec/models/maily_herald/one_time_mailing_spec.rb +112 -0
- data/spec/models/maily_herald/periodical_mailing_spec.rb +339 -0
- data/spec/models/maily_herald/sequence_mailing_spec.rb +18 -0
- data/spec/models/maily_herald/sequence_spec.rb +429 -0
- data/spec/models/maily_herald/subscription_spec.rb +32 -0
- data/spec/spec_helper.rb +31 -11
- metadata +199 -54
- data/MIT-LICENSE +0 -20
- data/README.rdoc +0 -3
- data/app/assets/images/maily_herald/.gitkeep +0 -0
- data/app/assets/javascripts/maily_herald/application.js +0 -15
- data/app/assets/stylesheets/maily_herald/application.css +0 -13
- data/app/helpers/maily_herald/application_helper.rb +0 -4
- data/app/helpers/maily_herald_helper.rb +0 -9
- data/app/models/maily_herald/mailing_record.rb +0 -6
- data/app/views/layouts/maily_herald/application.html.erb +0 -14
- data/db/migrate/20130711124555_create_maily_herald_tables.rb +0 -38
- data/lib/maily_herald/worker.rb +0 -15
data/lib/maily_herald/context.rb
CHANGED
@@ -1,79 +1,225 @@
|
|
1
1
|
module MailyHerald
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
2
|
+
|
3
|
+
# Abstraction layer for accessing collections of Entities and their attributes.
|
4
|
+
# Information provided by scope is used while sending {MailyHerald::Mailing mailings}.
|
5
|
+
#
|
6
|
+
# {Context} defines following:
|
7
|
+
#
|
8
|
+
# * Entity scope - +ActiveRecord::Relation+, list of Entities that will be returned
|
9
|
+
# by {Context}.
|
10
|
+
# * Entity model name - deducted automatically from scope.
|
11
|
+
# * Entity attributes - Defined as procs that can be evaluated for every item of
|
12
|
+
# the scope (single Entity).
|
13
|
+
#
|
14
|
+
# * Entity email - defined as proc or string/symbol (email method name).
|
15
|
+
#
|
16
|
+
class Context
|
17
|
+
|
18
|
+
# Context Attributes drop definition for Liquid
|
19
|
+
class Drop < Liquid::Drop
|
20
|
+
def initialize attrs
|
21
|
+
@attrs = attrs
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_key?(name)
|
25
|
+
name = name.to_s
|
26
|
+
|
27
|
+
@attrs.has_key? name
|
28
|
+
end
|
29
|
+
|
30
|
+
def invoke_drop name
|
31
|
+
name = name.to_s
|
32
|
+
|
33
|
+
if @attrs.has_key? name
|
34
|
+
if @attrs[name].is_a? Hash
|
35
|
+
Drop.new(@attrs[name])
|
36
|
+
else
|
37
|
+
@attrs[name].call
|
38
|
+
end
|
39
|
+
else
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
alias :[] :invoke_drop
|
45
|
+
end
|
46
|
+
|
47
|
+
class Attributes
|
48
|
+
def initialize block
|
49
|
+
@attrs = {}
|
50
|
+
@node = @parent_node = @attrs
|
51
|
+
@block = block
|
52
|
+
end
|
53
|
+
|
54
|
+
def setup entity = nil, subscription = nil
|
55
|
+
if entity
|
56
|
+
@attrs["subscription"] = Proc.new{ subscription } if subscription
|
57
|
+
instance_exec entity, &@block
|
58
|
+
else
|
59
|
+
instance_eval &@block
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def attribute_group name, &block
|
64
|
+
@parent_node = @node
|
65
|
+
@parent_node[name.to_s] ||= {}
|
66
|
+
@node = @parent_node[name.to_s]
|
67
|
+
yield
|
68
|
+
@node = @parent_node
|
69
|
+
end
|
70
|
+
|
71
|
+
def attribute name, &block
|
72
|
+
@node[name.to_s] = block
|
73
|
+
end
|
74
|
+
|
75
|
+
def for_drop
|
76
|
+
@attrs
|
77
|
+
end
|
78
|
+
|
79
|
+
def method_missing(m, *args, &block)
|
80
|
+
true
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Friendly name of the {Context}.
|
85
|
+
#
|
86
|
+
# Displayed ie. in the Web UI.
|
87
|
+
attr_accessor :title
|
88
|
+
|
89
|
+
# Identification name of the {Context}.
|
90
|
+
#
|
91
|
+
# This can be then used in {MailyHerald.context} method to fetch the {Context}.
|
92
|
+
#
|
93
|
+
# @see MailyHerald.context
|
94
|
+
attr_reader :name
|
95
|
+
|
96
|
+
attr_writer :destination
|
97
|
+
|
98
|
+
# Creates {Context} and sets its name.
|
99
|
+
def initialize name
|
100
|
+
@name = name
|
101
|
+
end
|
102
|
+
|
103
|
+
# Defines or returns Entity scope - collection of Entities.
|
104
|
+
#
|
105
|
+
# If block passed, it is saved as scope proc. Block has to return
|
106
|
+
# +ActiveRecord::Relation+ containing Entity objects that will belong to scope.
|
107
|
+
#
|
108
|
+
# If no block given, scope proc is called and Entity collection returned.
|
109
|
+
def scope &block
|
110
|
+
if block_given?
|
111
|
+
@scope = block
|
112
|
+
else
|
113
|
+
@scope.call
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# Fetches the Entity model class based on scope.
|
118
|
+
def model
|
119
|
+
@model ||= @scope.call.klass
|
120
|
+
end
|
121
|
+
|
122
|
+
# Entity email address.
|
123
|
+
#
|
124
|
+
# Can be eitner +Proc+ or attribute name (string, symbol).
|
125
|
+
#
|
126
|
+
# If block passed, it is saved as destination proc. Block has to:
|
127
|
+
#
|
128
|
+
# * accept single Entity object,
|
129
|
+
# * return Entity email.
|
130
|
+
#
|
131
|
+
# If no block given, +destination+ attribute is returned (a string, symbol or proc).
|
132
|
+
def destination &block
|
133
|
+
if block_given?
|
134
|
+
@destination = block
|
135
|
+
else
|
136
|
+
@destination
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns Entity email attribute name only if it is not defined as a proc.
|
141
|
+
def destination_attribute
|
142
|
+
@destination unless @destination.respond_to?(:call)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Fetches Entity's email address based on {Context} destination definition.
|
146
|
+
def destination_for entity
|
147
|
+
destination_attribute ? entity.send(@destination) : @destination.call(entity)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Simply filter Entity scope by email.
|
151
|
+
#
|
152
|
+
# If destination is provided in form of Entity attribute name (not the proc),
|
153
|
+
# this method creates the scope filtered by `query` email using SQL LIKE.
|
154
|
+
#
|
155
|
+
# @param query [String] email address which is being searched.
|
156
|
+
# @return [ActiveRecord::Relation] collection filtered by email address
|
157
|
+
def scope_like query
|
158
|
+
if destination_attribute
|
159
|
+
scope.where("#{model.table_name}.#{destination_attribute} LIKE (?)", "%#{query}%")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Returns Entity collection scope with joined {MailyHerald::Subscription}.
|
164
|
+
#
|
165
|
+
# @param list [List, Fixnum, String] {MailyHerald::List} reference
|
166
|
+
# @param mode [:inner, :outer] SQL JOIN mode
|
167
|
+
def scope_with_subscription list, mode = :inner
|
168
|
+
list_id = case list
|
169
|
+
when List
|
170
|
+
list.id
|
171
|
+
when Fixnum
|
172
|
+
list
|
173
|
+
when String
|
174
|
+
list.to_i
|
175
|
+
else
|
176
|
+
raise ArgumentError
|
177
|
+
end
|
178
|
+
|
179
|
+
join_mode = case mode
|
180
|
+
when :outer
|
181
|
+
"LEFT OUTER JOIN"
|
182
|
+
else
|
183
|
+
"INNER JOIN"
|
184
|
+
end
|
185
|
+
|
186
|
+
subscription_fields_select = Subscription.columns.collect{|c| "#{Subscription.table_name}.#{c.name} AS maily_subscription_#{c.name}"}.join(", ")
|
187
|
+
|
188
|
+
scope.select("#{model.table_name}.*, #{subscription_fields_select}").joins(
|
189
|
+
"#{join_mode} #{Subscription.table_name} ON #{Subscription.table_name}.entity_id = #{model.table_name}.id AND #{Subscription.table_name}.entity_type = '#{model.base_class.to_s}' AND #{Subscription.table_name}.list_id = '#{list_id}'"
|
190
|
+
)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Sepcify or return {Context} attributes.
|
194
|
+
#
|
195
|
+
# Defines Entity attributes that can be accessed using this Context.
|
196
|
+
# Attributes defined this way are then accesible in Liquid templates
|
197
|
+
# in Generic Mailer ({MailyHerald::Mailer#generic}).
|
198
|
+
#
|
199
|
+
# If block passed, it is used to create Context Attributes.
|
200
|
+
#
|
201
|
+
# If no block given, current attributes are returned.
|
202
|
+
def attributes &block
|
203
|
+
if block_given?
|
204
|
+
@attributes = Attributes.new block
|
205
|
+
else
|
206
|
+
@attributes
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
# Obtains {Context} attributes in a form of (nested) +Hash+ which
|
211
|
+
# values are procs each returning single Entity attribute value.
|
212
|
+
def attributes_list
|
213
|
+
attributes = @attributes.dup
|
214
|
+
attributes.setup
|
215
|
+
attributes.for_drop
|
216
|
+
end
|
217
|
+
|
218
|
+
# Returns Liquid drop created from Context attributes.
|
219
|
+
def drop_for entity, subscription
|
220
|
+
attributes = @attributes.dup
|
221
|
+
attributes.setup entity, subscription
|
222
|
+
Drop.new(attributes.for_drop)
|
223
|
+
end
|
224
|
+
end
|
79
225
|
end
|
data/lib/maily_herald/engine.rb
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
module MailyHerald
|
2
2
|
class Engine < ::Rails::Engine
|
3
3
|
isolate_namespace MailyHerald
|
4
|
+
|
5
|
+
config.generators do |g|
|
6
|
+
g.test_framework :rspec, fixture: false
|
7
|
+
g.fixture_replacement :factory_girl, dir: 'spec/factories'
|
8
|
+
end
|
9
|
+
|
10
|
+
config.to_prepare do
|
11
|
+
require_dependency 'maily_herald/model_extensions'
|
12
|
+
|
13
|
+
MailyHerald.contexts.each do|n, c|
|
14
|
+
if c.model
|
15
|
+
unless c.model.included_modules.include?(MailyHerald::ModelExtensions)
|
16
|
+
c.model.send(:include, MailyHerald::ModelExtensions)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
4
21
|
end
|
5
22
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module MailyHerald
|
5
|
+
module Logging
|
6
|
+
OPTIONS = {
|
7
|
+
target: STDOUT,
|
8
|
+
level: Logger::INFO,
|
9
|
+
progname: "app"
|
10
|
+
}
|
11
|
+
|
12
|
+
module LoggerExtensions
|
13
|
+
def log_processing *args
|
14
|
+
options = args.extract_options!
|
15
|
+
mailing, entity, mail = nil
|
16
|
+
args.each do |arg|
|
17
|
+
case arg
|
18
|
+
when Mailing
|
19
|
+
mailing = arg
|
20
|
+
when ::Mail::Message
|
21
|
+
mail = arg
|
22
|
+
else
|
23
|
+
entity = arg
|
24
|
+
end
|
25
|
+
end
|
26
|
+
prefix = options.delete(:prefix)
|
27
|
+
level = options.delete(:level) || :info
|
28
|
+
|
29
|
+
log_msg = []
|
30
|
+
if entity.is_a?(Hash)
|
31
|
+
log_msg << "<#{entity[:class]}##{entity[:id]}> #{mail.try(:to)}" if entity
|
32
|
+
else
|
33
|
+
log_msg << "<#{entity.try(:class).try(:name)}##{entity.try(:id)}> #{entity} #{mail.try(:to)}" if entity
|
34
|
+
end
|
35
|
+
log_msg << "<#{mailing.try(:class).try(:name)}##{mailing.try(:id)}> #{mailing}" if mailing
|
36
|
+
|
37
|
+
send(level, [prefix, log_msg.join(", ")].compact.join(": "))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class Formatter < Logger::Formatter
|
42
|
+
def call(severity, time, program_name, message)
|
43
|
+
"#{time.utc.iso8601} #{Process.pid} [Maily##{"%3s" % program_name}] #{severity}: #{message}\n"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.initialize(opts = {})
|
48
|
+
oldlogger = @logger
|
49
|
+
|
50
|
+
@options ||= OPTIONS.dup
|
51
|
+
@options.merge!(opts) if opts
|
52
|
+
|
53
|
+
@logger = Logger.new(@options[:target])
|
54
|
+
@logger.level = @options[:level]
|
55
|
+
@logger.formatter = Formatter.new
|
56
|
+
@logger.progname = @options[:progname]
|
57
|
+
@logger.extend(LoggerExtensions)
|
58
|
+
|
59
|
+
oldlogger.close if oldlogger
|
60
|
+
@logger
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.initialized?
|
64
|
+
!!@logger
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.logger opts = {}
|
68
|
+
@logger || initialize(opts)
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.logger=(log)
|
72
|
+
@logger = (log ? log : Logger.new('/dev/null'))
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.options
|
76
|
+
@options || OPTIONS.dup
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.safe_options
|
80
|
+
opts = self.options.dup
|
81
|
+
opts[:target] = nil if !opts[:target].is_a?(String)
|
82
|
+
opts
|
83
|
+
end
|
84
|
+
|
85
|
+
def logger
|
86
|
+
MailyHerald::Logging.logger
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module MailyHerald
|
2
|
+
class Manager
|
3
|
+
def self.handle_trigger type, entity
|
4
|
+
mailings = Mailing.where(trigger: type)
|
5
|
+
mailings.each do |mailing|
|
6
|
+
mailing.deliver_to entity
|
7
|
+
end
|
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)
|
13
|
+
|
14
|
+
mailing.deliver_to entity if mailing
|
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
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.run_mailing mailing
|
24
|
+
mailing = Mailing.find_by_name(mailing) if !mailing.is_a?(Mailing)
|
25
|
+
|
26
|
+
mailing.run if mailing
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.run_all
|
30
|
+
PeriodicalMailing.all.each {|m| m.run}
|
31
|
+
Sequence.all.each {|m| m.run}
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.simulate period
|
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
|
+
|
47
|
+
def self.job_enqueued?
|
48
|
+
Sidekiq::Queue.new.detect{|j| j.klass == "MailyHerald::Async" } ||
|
49
|
+
Sidekiq::Workers.new.detect{|w, msg| msg["payload"]["class"] == "MailyHerald::Async" } ||
|
50
|
+
Sidekiq::RetrySet.new.detect{|j| j.klass = "MailyHerald::Async" }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|