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,15 @@
1
+ module MailyHerald
2
+ module ModelExtensions
3
+ def self.included(base)
4
+ unloadable
5
+ base.class_eval do
6
+ has_many :maily_herald_subscriptions, as: :entity, class_name: "MailyHerald::Subscription", dependent: :destroy
7
+ has_many :maily_herald_logs, as: :entity, class_name: "MailyHerald::Log"
8
+
9
+ after_destroy do
10
+ self.maily_herald_logs.scheduled.destroy_all
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module MailyHerald
2
+ module TemplateRenderer
3
+ def self.included(base)
4
+ base.send :include, MailyHerald::TemplateRenderer::InstanceMethods
5
+ end
6
+
7
+ module InstanceMethods
8
+ protected
9
+
10
+ def perform_template_rendering drop, template
11
+ template = Liquid::Template.parse(template)
12
+ template.render! drop
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,7 +1,80 @@
1
1
  module MailyHerald
2
- module Utils
3
- def random_hex(n)
4
- SecureRandom.hex(n)
5
- end
6
- end
2
+ module Utils
3
+ def self.random_hex(n)
4
+ SecureRandom.hex(n)
5
+ end
6
+
7
+ class MarkupEvaluator
8
+ class DummyDrop < Liquid::Drop
9
+ def has_key?(name)
10
+ true
11
+ end
12
+
13
+ def invoke_drop name
14
+ true
15
+ end
16
+
17
+ alias :[] :invoke_drop
18
+ end
19
+
20
+ def self.test_conditions conditions
21
+ return true if !conditions || conditions.empty?
22
+
23
+ condition = self.create_liquid_condition conditions
24
+ template = Liquid::Template.parse(conditions)
25
+ raise StandardError unless template.errors.empty?
26
+
27
+ drop = DummyDrop.new
28
+ liquid_context = Liquid::Context.new([drop, template.assigns], template.instance_assigns, template.registers, true, {})
29
+ drop.context = liquid_context
30
+
31
+ condition.evaluate liquid_context
32
+ end
33
+
34
+
35
+ def initialize drop
36
+ @drop = drop
37
+ end
38
+
39
+ def evaluate_conditions conditions
40
+ return true if !conditions || conditions.empty?
41
+
42
+ condition = MarkupEvaluator.create_liquid_condition conditions
43
+ template = Liquid::Template.parse(conditions)
44
+
45
+ liquid_context = Liquid::Context.new([@drop, template.assigns], template.instance_assigns, template.registers, true, {})
46
+ @drop.context = liquid_context
47
+
48
+ condition.evaluate liquid_context
49
+ end
50
+
51
+ def evaluate_variable markup
52
+ template = Liquid::Template.parse(markup)
53
+
54
+ liquid_context = Liquid::Context.new([@drop, template.assigns], template.instance_assigns, template.registers, true, {})
55
+ @drop.context = liquid_context
56
+ liquid_context[markup]
57
+ end
58
+
59
+ private
60
+
61
+ def self.create_liquid_condition markup
62
+ expressions = markup.scan(Liquid::If::ExpressionsAndOperators).reverse
63
+ raise(Liquid::SyntaxError, Liquid::SyntaxHelp) unless expressions.shift =~ Liquid::If::Syntax
64
+
65
+ condition = Liquid::Condition.new($1, $2, $3)
66
+ while not expressions.empty?
67
+ operator = (expressions.shift).to_s.strip
68
+
69
+ raise(Liquid::SyntaxError, Liquid::SyntaxHelp) unless expressions.shift.to_s =~ Liquid::If::Syntax
70
+
71
+ new_condition = Liquid::Condition.new($1, $2, $3)
72
+ new_condition.send(operator.to_sym, condition)
73
+ condition = new_condition
74
+ end
75
+
76
+ condition
77
+ end
78
+ end
79
+ end
7
80
  end
@@ -1,3 +1,3 @@
1
1
  module MailyHerald
2
- VERSION = "0.0.1"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -1,3 +1,5 @@
1
+ # encoding: UTF-8
2
+
1
3
  $:.push File.expand_path("../lib", __FILE__)
2
4
 
3
5
  # Maintain your gem's version:
@@ -10,20 +12,26 @@ Gem::Specification.new do |s|
10
12
  s.authors = ["Łukasz Jachymczyk"]
11
13
  s.email = ["lukasz@sology.eu"]
12
14
  s.homepage = "https://github.com/Sology/maily_herald"
13
- s.summary = "Mailing framework for Ruby on Rails applications"
14
- #s.description = ""
15
+ s.license = "LGPL-3.0"
16
+ s.description = s.summary = "Email marketing solution for Ruby on Rails applications"
15
17
 
16
- s.files = `git ls-files`.split("\n")
17
- s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.files = `git ls-files`.split("\n")
19
+ s.executables = s.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ s.test_files = `git ls-files -- {spec,features}/**/`.split("\n")
18
21
 
19
- s.add_dependency "rails", "~> 3.2.8"
20
- s.add_dependency "liquid", "~> 2.6.0"
21
- s.add_dependency "sidekiq", "~> 2.13.0"
22
- # s.add_dependency "jquery-rails"
22
+ s.add_dependency "rails", "> 3.2"
23
+ s.add_dependency "liquid", "~> 2.6.1"
24
+ s.add_dependency "sidekiq", "~> 2.17.8"
23
25
 
24
26
  s.add_development_dependency "sqlite3"
25
27
  s.add_development_dependency "rspec-rails"
26
- s.add_development_dependency "shoulda-matchers", "~>1.0"#, "~> 3.0"
27
28
  s.add_development_dependency "factory_girl_rails"
28
29
  s.add_development_dependency "database_cleaner"
30
+ s.add_development_dependency "guard"
31
+ s.add_development_dependency "guard-rspec"
32
+ s.add_development_dependency "simplecov"
33
+ s.add_development_dependency "timecop"
34
+ s.add_development_dependency "spring-commands-rspec"
35
+ s.add_development_dependency "yard"
36
+ s.add_development_dependency "redcarpet" # for yard markdown formatting
29
37
  end
@@ -0,0 +1,81 @@
1
+ require 'spec_helper'
2
+
3
+ describe MailyHerald::TokensController do
4
+ before(:each) do
5
+ @user = FactoryGirl.create :user
6
+ @mailing = MailyHerald.one_time_mailing :test_mailing
7
+ @subscription = @mailing.subscription_for @user
8
+ end
9
+
10
+ describe "Unsubscribe action" do
11
+ before(:each) do
12
+ @mailing.token_action = :unsubscribe
13
+ @mailing.save
14
+ end
15
+
16
+ describe "when regular subscription" do
17
+ pending "should deactivate only one subscription" do
18
+ get :get, token: @subscription.token, use_route: :maily_herald
19
+ response.should redirect_to("/")
20
+ @subscription.reload
21
+
22
+ @subscription.active?.should_not be_true
23
+
24
+ @user.maily_herald_subscriptions.each do |s|
25
+ next unless s.target.subscription_group == @subscription.target.subscription_group
26
+ next if s == @subscription
27
+
28
+ s.active?.should be_true
29
+ end
30
+ end
31
+ end
32
+
33
+ describe "when aggregated subscription" do
34
+ before(:each) do
35
+ @mailing.subscription_group = :account
36
+ @mailing.save!
37
+ end
38
+
39
+ after(:each) do
40
+ @mailing.subscription_group = nil
41
+ @mailing.save!
42
+ end
43
+
44
+ pending "should deactivate subscription group" do
45
+ get :get, token: @subscription.token, use_route: :maily_herald
46
+ response.should redirect_to("/")
47
+ @subscription.reload
48
+
49
+ @subscription.active?.should_not be_true
50
+ @subscription.aggregate.should_not be_nil
51
+ @subscription.aggregate.active?.should_not be_true
52
+
53
+ @user.maily_herald_subscriptions.each do |s|
54
+ next unless s.target.subscription_group == @subscription.target.subscription_group
55
+
56
+ s.active?.should be_false
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ pending "Custom action" do
63
+ before(:each) do
64
+ @mailing.token_action = :custom
65
+ @mailing.should be_valid
66
+ @mailing.save
67
+ @mailing.token_custom_action.should_not be_nil
68
+ end
69
+
70
+ pending "should perform custom action" do
71
+ @subscription.reload
72
+ @subscription.target.token_action.should eq(:custom)
73
+ get :get, token: @subscription.token, use_route: :maily_herald
74
+ response.should redirect_to("/custom")
75
+ @subscription.reload
76
+ @user.reload
77
+
78
+ @user.name.should eq("changed")
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,35 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard :rspec do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+
17
+ # Capybara features specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
23
+ end
24
+
25
+
26
+ guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do
27
+ watch('config/application.rb')
28
+ watch('config/environment.rb')
29
+ watch('config/environments/test.rb')
30
+ watch(%r{^config/initializers/.+\.rb$})
31
+ watch('Gemfile.lock')
32
+ watch('spec/spec_helper.rb') { :rspec }
33
+ watch('test/test_helper.rb') { :test_unit }
34
+ watch(%r{features/support/}) { :cucumber }
35
+ end
@@ -0,0 +1,11 @@
1
+ class TestMailer < MailyHerald::Mailer
2
+ default from: "no-reply@flossmarket.com"
3
+
4
+ def sample_mail user
5
+ mail to: user.email, subject: "Test"
6
+ end
7
+
8
+ def sample_mail_error user
9
+ mail to: user.email, subject: "Test"
10
+ end
11
+ end
@@ -0,0 +1,2 @@
1
+ class Product < ActiveRecord::Base
2
+ end
@@ -0,0 +1,4 @@
1
+ class User < ActiveRecord::Base
2
+ scope :active, lambda { where(active: true) }
3
+ scope :inactive, lambda { where(active: false) }
4
+ end
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ load File.expand_path("../spring", __FILE__)
4
+ rescue LoadError
5
+ end
6
+ # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
7
+
8
+ APP_PATH = File.expand_path('../../config/application', __FILE__)
9
+ require File.expand_path('../../config/boot', __FILE__)
10
+ require 'rails/commands'
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ load File.expand_path("../spring", __FILE__)
4
+ rescue LoadError
5
+ end
6
+ require 'bundler/setup'
7
+ load Gem.bin_path('rake', 'rake')
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ begin
3
+ load File.expand_path("../spring", __FILE__)
4
+ rescue LoadError
5
+ end
6
+ require 'bundler/setup'
7
+ load Gem.bin_path('rspec-core', 'rspec')
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This file loads spring without using Bundler, in order to be fast
4
+ # It gets overwritten when you run the `spring binstub` command
5
+
6
+ unless defined?(Spring)
7
+ require "rubygems"
8
+ require "bundler"
9
+
10
+ if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m)
11
+ ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR)
12
+ ENV["GEM_HOME"] = ""
13
+ Gem.paths = ENV
14
+
15
+ gem "spring", match[1]
16
+ require "spring/binstub"
17
+ end
18
+ end
@@ -8,7 +8,7 @@ require "active_resource/railtie"
8
8
  require "sprockets/railtie"
9
9
  # require "rails/test_unit/railtie"
10
10
 
11
- Bundler.require
11
+ Bundler.require(*Rails.groups)
12
12
  require "maily_herald"
13
13
 
14
14
  module Dummy
@@ -34,4 +34,5 @@ Dummy::Application.configure do
34
34
 
35
35
  # Expands the lines which load the assets
36
36
  config.assets.debug = true
37
+ config.serve_static_assets = true
37
38
  end
@@ -28,6 +28,7 @@ Dummy::Application.configure do
28
28
  # The :test delivery method accumulates sent emails in the
29
29
  # ActionMailer::Base.deliveries array.
30
30
  config.action_mailer.delivery_method = :test
31
+ ActionMailer::Base.default :from => "no-reply@foo.com>"
31
32
 
32
33
  # Raise exception on mass assignment protection for Active Record models
33
34
  config.active_record.mass_assignment_sanitizer = :strict
@@ -0,0 +1,103 @@
1
+ MailyHerald.setup do |config|
2
+ config.token_redirect {|subscription| "/" }
3
+
4
+ config.context :all_users do |context|
5
+ context.scope {User.active}
6
+ context.destination {|user| user.email}
7
+ context.attributes do |user|
8
+ attribute_group(:user) do
9
+ attribute(:name) {user.name}
10
+ attribute(:email) {user.email}
11
+ attribute(:created_at) {user.created_at}
12
+ attribute(:weekly_notifications) {user.weekly_notifications}
13
+ attribute_group(:properties) do
14
+ attribute(:prop1) { user.name[0] }
15
+ attribute(:prop2) { 2 }
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ config.list :generic_list do |list|
22
+ list.context_name = :all_users
23
+ end
24
+
25
+ config.list :locked_list, locked: true do |list|
26
+ list.context_name = :all_users
27
+ end
28
+
29
+ config.one_time_mailing :locked_mailing, locked: true do |mailing|
30
+ mailing.enable
31
+ mailing.title = "Test mailing"
32
+ mailing.subject = "Test mailing"
33
+ mailing.list = :generic_list
34
+ mailing.template = "User name: {{user.name}}."
35
+ end
36
+
37
+ config.one_time_mailing :test_mailing do |mailing|
38
+ mailing.enable
39
+ mailing.title = "Test mailing"
40
+ mailing.subject = "Test mailing"
41
+ mailing.list = :generic_list
42
+ mailing.template = "User name: {{user.name}}."
43
+ end
44
+
45
+ config.one_time_mailing :sample_mail do |mailing|
46
+ mailing.enable
47
+ mailing.title = "Sample mailing"
48
+ mailing.list = :generic_list
49
+ mailing.mailer_name = "TestMailer"
50
+ end
51
+
52
+ config.sequence :newsletters do |seq|
53
+ seq.enable
54
+ seq.title = "Newsletters"
55
+ seq.list = :generic_list
56
+ seq.start_at = "user.created_at"
57
+ seq.mailing :initial_mail do |mailing|
58
+ mailing.title = "Test mailing #1"
59
+ mailing.subject = "Test mailing #1"
60
+ mailing.template = "User name: {{user.name}}."
61
+ mailing.absolute_delay = 1.hour
62
+ mailing.enable
63
+ end
64
+ seq.mailing :second_mail do |mailing|
65
+ mailing.title = "Test mailing #2"
66
+ mailing.subject = "Test mailing #2"
67
+ mailing.template = "User name: {{user.name}}."
68
+ mailing.absolute_delay = 3.hours
69
+ mailing.enable
70
+ end
71
+ seq.mailing :third_mail do |mailing|
72
+ mailing.title = "Test mailing #3"
73
+ mailing.subject = "Test mailing #3"
74
+ mailing.template = "User name: {{user.name}}."
75
+ mailing.absolute_delay = 6.hours
76
+ mailing.enable
77
+ end
78
+ end
79
+
80
+ config.periodical_mailing :weekly_summary do |mailing|
81
+ mailing.enable
82
+ mailing.title = "Weekly summary"
83
+ mailing.subject = "Weekly summary"
84
+ mailing.start_at = "user.created_at"
85
+ mailing.list = :generic_list
86
+ mailing.title = "Test periodical mailing"
87
+ mailing.template = "User name: {{user.name}}."
88
+ mailing.period = 7.days
89
+ mailing.conditions = "user.weekly_notifications"
90
+ end
91
+
92
+ config.periodical_mailing :weekly_summary_sg do |mailing|
93
+ mailing.enable
94
+ mailing.title = "Weekly summary"
95
+ mailing.subject = "Weekly summary"
96
+ mailing.start_at = "user.created_at"
97
+ mailing.list = :generic_list
98
+ mailing.title = "Test periodical mailing"
99
+ mailing.template = "User name: {{user.name}}."
100
+ mailing.period = 7.days
101
+ mailing.conditions = "user.weekly_notifications"
102
+ end
103
+ end