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,34 @@
1
+ module MailyHerald
2
+ module Autonaming
3
+ def self.included(base)
4
+ base.extend ClassMethods
5
+ base.send :include, MailyHerald::Autonaming::InstanceMethods
6
+
7
+ base.class_eval do
8
+ validates :name, presence: true, format: {with: /\A\w+\z/}, uniqueness: true
9
+ validates :title, presence: true
10
+
11
+ before_validation do
12
+ if self.title && !self.name
13
+ self.name = self.title.parameterize.underscore
14
+ elsif self.name && !self.title
15
+ self.title = self.name
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ module ClassMethods
22
+ end
23
+
24
+ module InstanceMethods
25
+ def self.included(base)
26
+ base.extend ClassMethods
27
+ end
28
+
29
+ def to_s
30
+ self.title || self.name
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,5 @@
1
+ if Gem::Specification.find_by_name('capistrano').version >= Gem::Version.new('3.0.0')
2
+ load File.expand_path('../capistrano/tasks.cap', __FILE__)
3
+ else
4
+ require_relative 'capistrano/tasks2'
5
+ end
@@ -0,0 +1,67 @@
1
+ namespace :load do
2
+ task :defaults do
3
+ set :maily_herald_default_hooks, -> { true }
4
+
5
+ set :maily_herald_env, -> { fetch(:rack_env, fetch(:rails_env, fetch(:stage))) }
6
+ set :maily_herald_role, -> { :app }
7
+
8
+ set :rbenv_map_bins, fetch(:rbenv_map_bins).to_a.concat(%w(maily_herald))
9
+ set :rvm_map_bins, fetch(:rvm_map_bins).to_a.concat(%w(maily_herald))
10
+ end
11
+ end
12
+
13
+ namespace :deploy do
14
+ before :starting, :check_maily_herald_hooks do
15
+ invoke 'maily_herald:add_default_hooks' if fetch(:maily_herald_default_hooks)
16
+ end
17
+ after :publishing, :restart_maily_herald do
18
+ invoke 'maily_herald:restart' if fetch(:maily_herald_default_hooks)
19
+ end
20
+ end
21
+
22
+ namespace :maily_herald do
23
+ def stop_maily_herald
24
+ execute :bundle, :exec, :maily_herald, "paperboy", "--stop"
25
+ end
26
+
27
+ def start_maily_herald
28
+ execute :bundle, :exec, :maily_herald, "paperboy", "--start"
29
+ end
30
+
31
+ def restart_maily_herald
32
+ execute :bundle, :exec, :maily_herald, "paperboy", "--restart"
33
+ end
34
+
35
+ task :add_default_hooks do
36
+ after 'deploy:updated', 'maily_herald:stop'
37
+ after 'deploy:reverted', 'maily_herald:stop'
38
+ after 'deploy:published', 'maily_herald:start'
39
+ end
40
+
41
+ desc 'Stop maily_herald'
42
+ task :stop do
43
+ on roles fetch(:maily_herald_role) do
44
+ within release_path do
45
+ stop_maily_herald
46
+ end
47
+ end
48
+ end
49
+
50
+ desc 'Start maily_herald'
51
+ task :start do
52
+ on roles fetch(:maily_herald_role) do
53
+ within release_path do
54
+ start_maily_herald
55
+ end
56
+ end
57
+ end
58
+
59
+ desc 'Restart maily_herald'
60
+ task :restart do
61
+ on roles fetch(:maily_herald_role) do
62
+ within release_path do
63
+ restart_maily_herald
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ Capistrano::Configuration.instance.load do
2
+ namespace :maily_herald do
3
+ desc "Stop maily_herald"
4
+ task :stop, roles: :app do
5
+ run "cd #{current_path}; bundle exec maily_herald --stop"
6
+ end
7
+
8
+ desc "Start maily_herald"
9
+ task :start, roles: :app do
10
+ run "cd #{current_path}; RAILS_ENV=#{rails_env} bundle exec maily_herald --start"
11
+ end
12
+
13
+ desc "Restart maily_herald"
14
+ task :restart, roles: :app do
15
+ run "cd #{current_path}; RAILS_ENV=#{rails_env} bundle exec maily_herald --restart"
16
+ end
17
+ end
18
+
19
+ after 'deploy', 'maily_herald:restart'
20
+ end
@@ -0,0 +1,293 @@
1
+ $stdout.sync = true
2
+
3
+ require 'yaml'
4
+ require 'singleton'
5
+ require 'optparse'
6
+ require 'erb'
7
+
8
+ require 'maily_herald'
9
+
10
+ module MailyHerald
11
+ class CLI
12
+ include Singleton
13
+
14
+ def parse(args=ARGV)
15
+ setup_options(args)
16
+ initialize_logger
17
+ end
18
+
19
+ def paperboy
20
+ if options[:action] == :stop
21
+ kill_daemon || exit(0)
22
+ elsif options[:action] == :ping
23
+ if daemon_running?
24
+ puts "PONG"
25
+ exit(0)
26
+ else
27
+ puts "No response..."
28
+ exit(1)
29
+ end
30
+ else
31
+ if options[:action] == :restart
32
+ kill_daemon
33
+ 5.times do
34
+ if daemon_running?
35
+ sleep 2
36
+ else
37
+ break
38
+ end
39
+
40
+ exit(0) if daemon_running?
41
+ end
42
+ end
43
+ exit(0) if options[:action] == :start && daemon_running?
44
+
45
+ daemonize
46
+ write_pid
47
+
48
+ self_read, self_write = IO.pipe
49
+
50
+ %w(INT TERM USR1 USR2).each do |sig|
51
+ trap sig do
52
+ self_write.puts(sig)
53
+ end
54
+ end
55
+
56
+ # We don't want to load whole app and its initializers just to set up Sidekiq client
57
+ # so let's just do that instead:
58
+ Sidekiq.redis = {url: options[:redis_url], namespace: options[:redis_namespace]}
59
+
60
+ redis = MailyHerald.redis
61
+ MailyHerald.logger.info "MailyHerald running in #{RUBY_DESCRIPTION}"
62
+
63
+ if !options[:daemon]
64
+ MailyHerald.logger.info 'Starting processing, hit Ctrl-C to stop'
65
+ end
66
+
67
+ begin
68
+ worker = Thread.new do
69
+ while true
70
+ unless MailyHerald::Manager.job_enqueued?
71
+ MailyHerald.run_all
72
+ else
73
+ # TODO: this is not logged
74
+ #MailyHerald.logger.error 'Unable to queue job'
75
+ end
76
+
77
+ sleep 20
78
+ end
79
+ end
80
+
81
+ while readable_io = IO.select([self_read])
82
+ signal = readable_io.first[0].gets.strip
83
+ handle_signal(signal)
84
+ end
85
+ rescue Interrupt
86
+ MailyHerald.logger.info 'Shutting down'
87
+ worker.exit
88
+ reset_pid
89
+ exit(0)
90
+ end
91
+ end
92
+ end
93
+
94
+ def setup_options(args)
95
+ cli = parse_options(args)
96
+
97
+ set_environment cli[:environment]
98
+
99
+ MailyHerald.options = MailyHerald.read_options(cli[:config_file] || "config/maily_herald.yml").merge(cli)
100
+ end
101
+
102
+ def initialize_logger
103
+ opts = {
104
+ level: options[:verbose] ? Logger::DEBUG : Logger::INFO,
105
+ progname: "cli",
106
+ }
107
+ opts[:target] = options[:logfile] if options[:logfile]
108
+
109
+ MailyHerald::Logging.initialize(opts)
110
+ MailyHerald.logger.info "Started with options: #{options}"
111
+ end
112
+
113
+ def parse_options(argv)
114
+ opts = {}
115
+ @parsers = {}
116
+
117
+ @parsers[:paperboy] = OptionParser.new do |o|
118
+ o.banner = "maily_herald paperboy [options]"
119
+
120
+ o.on "--start", "Start Paperboy daemon" do |arg|
121
+ opts[:action] = :start
122
+ opts[:daemon] = true
123
+ end
124
+
125
+ o.on "--stop", "Stop Paperboy daemon" do |arg|
126
+ opts[:action] = :stop
127
+ opts[:daemon] = true
128
+ end
129
+
130
+ o.on "--restart", "Restart Paperboy daemon" do |arg|
131
+ opts[:action] = :restart
132
+ opts[:daemon] = true
133
+ end
134
+
135
+ o.on "--ping", "Check if Paperboy daemon is running" do |arg|
136
+ opts[:action] = :ping
137
+ opts[:daemon] = true
138
+ end
139
+
140
+ o.on '-L', '--logfile PATH', "path to writable logfile" do |arg|
141
+ opts[:logfile] = arg
142
+ end
143
+
144
+ o.on '-P', '--pidfile PATH', "path to pidfile" do |arg|
145
+ opts[:pidfile] = arg
146
+ end
147
+ end
148
+
149
+ @parsers[:generic] = OptionParser.new do |o|
150
+ o.banner = "maily_herald [paperboy] [options]"
151
+
152
+ o.separator ""
153
+ o.separator "Common options:"
154
+
155
+ o.on '-c', '--config PATH', "path to YAML config file" do |arg|
156
+ opts[:config_file] = arg
157
+ end
158
+
159
+ o.on '-e', '--environment ENV', "Application environment" do |arg|
160
+ opts[:environment] = arg
161
+ end
162
+
163
+ o.on "-v", "--verbose", "Print more verbose output" do |arg|
164
+ opts[:verbose] = arg
165
+ end
166
+
167
+ o.on_tail "-h", "--help", "Show help" do
168
+ puts @parsers[:generic]
169
+ puts
170
+ puts @paperboy_parser
171
+ exit 1
172
+ end
173
+ end
174
+
175
+ if %w{paperboy}.include?(argv.first)
176
+ opts[:mode] = argv.first.to_sym
177
+ @parsers[argv.first.to_sym].parse!(argv)
178
+ end
179
+ @parsers[:generic].parse!(argv)
180
+
181
+ opts
182
+ end
183
+
184
+ def parse_config(cfile)
185
+ opts = {}
186
+ if File.exist?(cfile)
187
+ opts = YAML.load(ERB.new(IO.read(cfile)).result)
188
+ opts = opts.merge(opts.delete(@environment) || {})
189
+ end
190
+ opts
191
+ end
192
+
193
+ def options
194
+ MailyHerald.options
195
+ end
196
+
197
+ def daemonize
198
+ return unless options[:daemon]
199
+
200
+ raise ArgumentError, "You really should set a logfile if you're going to daemonize" unless options[:logfile]
201
+ files_to_reopen = []
202
+ ObjectSpace.each_object(File) do |file|
203
+ files_to_reopen << file unless file.closed?
204
+ end
205
+
206
+ Process.daemon(true, true)
207
+
208
+ files_to_reopen.each do |file|
209
+ begin
210
+ file.reopen file.path, "a+"
211
+ file.sync = true
212
+ rescue ::Exception
213
+ end
214
+ end
215
+
216
+ [$stdout, $stderr].each do |io|
217
+ File.open(options[:logfile], 'ab') do |f|
218
+ io.reopen(f)
219
+ end
220
+ io.sync = true
221
+ end
222
+ $stdin.reopen('/dev/null')
223
+
224
+ initialize_logger
225
+ end
226
+
227
+ def write_pid
228
+ return unless options[:daemon]
229
+
230
+ if path = options[:pidfile]
231
+ File.open(path, 'w') do |f|
232
+ f.puts Process.pid
233
+ end
234
+ end
235
+ end
236
+
237
+ def read_pid
238
+ if path = options[:pidfile]
239
+ File.read(path).to_i
240
+ end
241
+ end
242
+
243
+ def reset_pid
244
+ return unless options[:daemon]
245
+
246
+ if path = options[:pidfile]
247
+ File.open(path, 'w') do |f|
248
+ f.puts nil
249
+ end
250
+ end
251
+ end
252
+
253
+ def daemon_running?
254
+ read_pid > 0 && Process.kill(0, read_pid)
255
+ rescue
256
+ return false
257
+ end
258
+
259
+ def kill_daemon
260
+ Process.kill("INT", read_pid) if read_pid > 0
261
+ return true
262
+ rescue
263
+ return false
264
+ end
265
+
266
+ private
267
+
268
+ def set_environment(cli_env)
269
+ @environment = cli_env || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
270
+ end
271
+
272
+ def handle_signal(sig)
273
+ MailyHerald.logger.debug "Got #{sig} signal"
274
+ case sig
275
+ when 'INT'
276
+ # Handle Ctrl-C in JRuby like MRI
277
+ # http://jira.codehaus.org/browse/JRUBY-4637
278
+ raise Interrupt
279
+ when 'TERM'
280
+ # Heroku sends TERM and then waits 10 seconds for process to exit.
281
+ raise Interrupt
282
+ when 'USR1'
283
+ MailyHerald.logger.info "Received USR1, doing nothing..."
284
+ when 'USR2'
285
+ if MailyHerald.options[:logfile]
286
+ MailyHerald.logger.info "Received USR2, reopening log file"
287
+ MailyHerald::Logging.initialize_logger(target: MailyHerald.options[:logfile])
288
+ end
289
+ end
290
+ end
291
+
292
+ end
293
+ end
@@ -0,0 +1,82 @@
1
+ module MailyHerald
2
+ #module ConditionEvaluator
3
+ #class DummyDrop < Liquid::Drop
4
+ #def has_key?(name)
5
+ #true
6
+ #end
7
+
8
+ #def invoke_drop name
9
+ #true
10
+ #end
11
+
12
+ #alias :[] :invoke_drop
13
+ #end
14
+
15
+ #def self.included(base)
16
+ #base.validate :validate_conditions
17
+
18
+ #base.send :include, MailyHerald::ConditionEvaluator::InstanceMethods
19
+ #end
20
+
21
+ #module InstanceMethods
22
+ #def has_conditions?
23
+ #self.conditions && !self.conditions.empty?
24
+ #end
25
+
26
+ #def evaluate_conditions_for entity
27
+ #if has_conditions?
28
+ #condition = create_liquid_condition self.conditions
29
+ #template = Liquid::Template.parse(self.conditions)
30
+ #drop = context.drop_for entity, subscription_for(entity)
31
+
32
+ #liquid_context = Liquid::Context.new([drop, template.assigns], template.instance_assigns, template.registers, true, {})
33
+ #drop.context = liquid_context
34
+
35
+ #condition.evaluate liquid_context
36
+ #else
37
+ #true
38
+ #end
39
+ #end
40
+
41
+ #def test_conditions
42
+ #return true unless self.conditions
43
+
44
+ #condition = create_liquid_condition self.conditions
45
+ #template = Liquid::Template.parse(self.conditions)
46
+ #return false unless template.errors.empty?
47
+
48
+ #drop = DummyDrop.new
49
+ #liquid_context = Liquid::Context.new([drop, template.assigns], template.instance_assigns, template.registers, true, {})
50
+ #drop.context = liquid_context
51
+
52
+ #condition.evaluate liquid_context
53
+ #end
54
+
55
+ #private
56
+
57
+ #def create_liquid_condition markup
58
+ #expressions = markup.scan(Liquid::If::ExpressionsAndOperators).reverse
59
+ #raise(Liquid::SyntaxError, Liquid::SyntaxHelp) unless expressions.shift =~ Liquid::If::Syntax
60
+
61
+ #condition = Liquid::Condition.new($1, $2, $3)
62
+ #while not expressions.empty?
63
+ #operator = (expressions.shift).to_s.strip
64
+
65
+ #raise(Liquid::SyntaxError, Liquid::SyntaxHelp) unless expressions.shift.to_s =~ Liquid::If::Syntax
66
+
67
+ #new_condition = Liquid::Condition.new($1, $2, $3)
68
+ #new_condition.send(operator.to_sym, condition)
69
+ #condition = new_condition
70
+ #end
71
+
72
+ #condition
73
+ #end
74
+
75
+ #def validate_conditions
76
+ #test_conditions
77
+ #rescue StandardError => e
78
+ #errors.add(:conditions, e.to_s)
79
+ #end
80
+ #end
81
+ #end
82
+ end