campagne 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. data/.gitignore +4 -0
  2. data/.rvmrc +1 -0
  3. data/Gemfile +3 -0
  4. data/README.md +21 -0
  5. data/Rakefile +1 -0
  6. data/app/assets/.DS_Store +0 -0
  7. data/app/assets/javascripts/campagne.js +15 -0
  8. data/app/assets/stylesheets/campagne.css +118 -0
  9. data/app/controllers/bulletin/.DS_Store +0 -0
  10. data/app/controllers/bulletin/campagne_campaigns_controller.rb +62 -0
  11. data/app/controllers/bulletin/campagne_deliveries_controller.rb +39 -0
  12. data/app/controllers/bulletin/campagne_lists_controller.rb +45 -0
  13. data/app/helpers/campagne/error_messages_helper.rb +26 -0
  14. data/app/helpers/campagne/layout_helper.rb +26 -0
  15. data/app/models/campagne/campagne_campaign.rb +25 -0
  16. data/app/models/campagne/campagne_contact.rb +10 -0
  17. data/app/models/campagne/campagne_delivery.rb +37 -0
  18. data/app/models/campagne/campagne_list.rb +11 -0
  19. data/app/models/campagne/sender.rb +19 -0
  20. data/app/models/campagne/sender_job.rb +29 -0
  21. data/app/validators/email_validator.rb +56 -0
  22. data/app/views/campagne/.DS_Store +0 -0
  23. data/app/views/campagne/campagne_campaigns/_form.html.erb +23 -0
  24. data/app/views/campagne/campagne_campaigns/deliveries.html.erb +30 -0
  25. data/app/views/campagne/campagne_campaigns/edit.html.erb +8 -0
  26. data/app/views/campagne/campagne_campaigns/index.html.erb +20 -0
  27. data/app/views/campagne/campagne_campaigns/new.html.erb +5 -0
  28. data/app/views/campagne/campagne_campaigns/preview.html.erb +1 -0
  29. data/app/views/campagne/campagne_campaigns/show.html.erb +35 -0
  30. data/app/views/campagne/campagne_lists/_form.html.erb +8 -0
  31. data/app/views/campagne/campagne_lists/import.html.erb +12 -0
  32. data/app/views/campagne/campagne_lists/index.html.erb +16 -0
  33. data/app/views/campagne/campagne_lists/new.html.erb +5 -0
  34. data/app/views/campagne/campagne_lists/show.html.erb +17 -0
  35. data/app/views/layouts/campagne/campagne.html.erb +30 -0
  36. data/campagne.gemspec +23 -0
  37. data/config/locales/en.yml +5 -0
  38. data/config/routes.rb +25 -0
  39. data/lib/bulletin.rb +18 -0
  40. data/lib/campagne/engine.rb +23 -0
  41. data/lib/campagne/version.rb +3 -0
  42. data/lib/generators/campagne/campagne_generator.rb +30 -0
  43. data/lib/generators/campagne/templates/1x1.gif +0 -0
  44. data/lib/generators/campagne/templates/initializer.rb +11 -0
  45. data/lib/generators/campagne/templates/migration.rb +48 -0
  46. data/lib/tasks/resque.rake +100 -0
  47. data/test/test_helper.rb +0 -0
  48. metadata +153 -0
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm ruby-1.9.3-p125
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/README.md ADDED
@@ -0,0 +1,21 @@
1
+ Campagne
2
+ ========
3
+
4
+ A simple Rails 3 engine gem to manage and send newsletters.
5
+
6
+
7
+ Use this on campaign body:
8
+
9
+ ||WB|| - WebBeacon
10
+ ||UNSUB|| - Unsubscribe
11
+ ||LINK|| - Link
12
+
13
+ # Add to Gemfile
14
+ gem 'campagne'
15
+ $ bundle
16
+
17
+ $ bundle exec rails g campagne
18
+ # Change config/initializers/campagne.rb
19
+
20
+ $ bundle exec rake db:migrate
21
+ http://localhost:3000/campagne
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
Binary file
@@ -0,0 +1,15 @@
1
+ // This is a manifest file that'll be compiled into application.js, which will include all the files
2
+ // listed below.
3
+ //
4
+ // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
5
+ // or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
6
+ //
7
+ // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
8
+ // the compiled file.
9
+ //
10
+ // WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
11
+ // GO AFTER THE REQUIRES BELOW.
12
+ //
13
+ //= require jquery
14
+ //= require jquery_ujs
15
+ //= require_tree .
@@ -0,0 +1,118 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the top of the
9
+ * compiled file, but it's generally better to create a new file per style scope.
10
+ *
11
+ *= require_self
12
+ *= require_tree .
13
+ */
14
+
15
+ body {
16
+ background-color: #CCC;
17
+ font-family: "Helvetica Nue", Helvetica, Arial;
18
+ font-size: 14px;
19
+ }
20
+
21
+ a img {
22
+ border: none;
23
+ }
24
+
25
+ a {
26
+ color: #0000FF;
27
+ }
28
+
29
+ .clear {
30
+ clear: both;
31
+ height: 0;
32
+ overflow: hidden;
33
+ }
34
+
35
+ #nav {
36
+ width: 75%;
37
+ margin: 0 auto;
38
+ padding: 0 40px;
39
+ margin-top: 20px;
40
+ }
41
+
42
+ #container {
43
+ width: 75%;
44
+ margin: 0 auto;
45
+ background-color: #FFF;
46
+ padding: 20px 40px;
47
+ border: solid 1px black;
48
+ margin-top: 20px;
49
+ }
50
+
51
+ #flash_notice, #flash_error {
52
+ padding: 5px 8px;
53
+ margin: 10px 0;
54
+ }
55
+
56
+ #flash_notice {
57
+ background-color: #CFC;
58
+ border: solid 1px #6C6;
59
+ }
60
+
61
+ #flash_error {
62
+ background-color: #FCC;
63
+ border: solid 1px #C66;
64
+ }
65
+
66
+ .fieldWithErrors {
67
+ display: inline;
68
+ }
69
+
70
+ #errorExplanation {
71
+ width: 400px;
72
+ border: 2px solid #CF0000;
73
+ padding: 0px;
74
+ padding-bottom: 12px;
75
+ margin-bottom: 20px;
76
+ background-color: #f0f0f0;
77
+ }
78
+
79
+ #errorExplanation h2 {
80
+ text-align: left;
81
+ font-weight: bold;
82
+ padding: 5px 5px 5px 15px;
83
+ font-size: 12px;
84
+ margin: 0;
85
+ background-color: #c00;
86
+ color: #fff;
87
+ }
88
+
89
+ #errorExplanation p {
90
+ color: #333;
91
+ margin-bottom: 0;
92
+ padding: 8px;
93
+ }
94
+
95
+ #errorExplanation ul {
96
+ margin: 2px 24px;
97
+ }
98
+
99
+ #errorExplanation ul li {
100
+ font-size: 12px;
101
+ list-style: disc;
102
+ }
103
+
104
+ table {
105
+ width: 50%;
106
+ border-top: 1px solid #CCC;
107
+ border-bottom: 1px solid #CCC;
108
+ }
109
+
110
+ table td {
111
+ border-top: 1px solid #CCC;
112
+ padding: 4px;
113
+ }
114
+
115
+ table th {
116
+ text-align: left;
117
+ padding: 4px;
118
+ }
Binary file
@@ -0,0 +1,62 @@
1
+ module Campagne
2
+ class CampagneCampaignsController < ApplicationController
3
+ respond_to :html, :xml
4
+ layout 'campagne/campagne'
5
+
6
+ def index
7
+ @campaigns = CampagneCampaign.all
8
+ end
9
+
10
+ def show
11
+ @campaign = CampagneCampaign.find(params[:id])
12
+ respond_with(@campaign)
13
+ end
14
+
15
+ def new
16
+ @campaign = CampagneCampaign.new
17
+ respond_with(@campaign)
18
+ end
19
+
20
+ def create
21
+ @campaign = CampagneCampaign.new(params[:campagne_campagne_campaign])
22
+ flash[:notice] = 'Campaign was successfully created.' if @campaign.save
23
+ respond_with(@campaign, :location => campagne_campagne_campaigns_path)
24
+ end
25
+
26
+ def edit
27
+ @campaign = CampagneCampaign.find(params[:id])
28
+ respond_with(@campaign)
29
+ end
30
+
31
+ def update
32
+ @campaign = CampagneCampaign.find(params[:id])
33
+ flash[:notice] = 'Campaign was successfully updated.' if @campaign.update_attributes(params[:campagne_campagne_campaign])
34
+ respond_with(@campaign)
35
+ end
36
+
37
+ def preview
38
+ @campaign = CampagneCampaign.find(params[:id])
39
+ render :layout => nil
40
+ end
41
+
42
+ def deliveries
43
+ @campaign = CampagneCampaign.find(params[:id])
44
+ @deliveries = @campaign.campagne_deliveries
45
+ end
46
+
47
+ def schedule
48
+ @campaign = CampagneCampaign.find(params[:id])
49
+ datetime = Time.zone.local(
50
+ params[:schedule][:"at(1i)"].to_i,
51
+ params[:schedule][:"at(2i)"].to_i,
52
+ params[:schedule][:"at(3i)"].to_i,
53
+ params[:schedule][:"at(4i)"].to_i,
54
+ params[:schedule][:"at(5i)"].to_i
55
+ )
56
+ Resque.enqueue_at(datetime, Campagne::SenderJob, @campaign.id)
57
+ redirect_to campagne_campagne_campaign_path(@campaign), :notice => 'Campaign was successfully scheduled.'
58
+ end
59
+
60
+
61
+ end
62
+ end
@@ -0,0 +1,39 @@
1
+ module Campagne
2
+ class CampagneDeliveriesController < ApplicationController
3
+
4
+ def see
5
+ if delivery = CampagneDelivery.find_by_token(params[:token])
6
+ delivery.see!(request)
7
+ end
8
+ image = File.read(File.join(Rails.root, "public/1x1.gif"))
9
+ send_data image, :type => "image/gif", :disposition => "inline"
10
+ end
11
+
12
+ def click
13
+ if delivery = CampagneDelivery.find_by_token(params[:token])
14
+ delivery.click!(request)
15
+ end
16
+ # TODO:
17
+ redirect_to "http://#{params[:link]}"
18
+ end
19
+
20
+ def unsubscribe
21
+ if delivery = CampagneDelivery.find_by_token(params[:token])
22
+ delivery.unsubscribe!(request)
23
+ render :text => 'Ok'
24
+ else
25
+ render :text => 'Error'
26
+ end
27
+ end
28
+
29
+ def bounce
30
+ if delivery = CampagneDelivery.find_by_token(params[:token])
31
+ delivery.bounce!
32
+ render :text => 'Ok'
33
+ else
34
+ render :text => 'Error'
35
+ end
36
+ end
37
+
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ module Campagne
2
+ class CampagneListsController < ApplicationController
3
+ respond_to :html, :xml
4
+ layout 'campagne/campagne'
5
+
6
+ def index
7
+ @lists = CampagneList.all
8
+ end
9
+
10
+ def show
11
+ @list = CampagneList.find(params[:id])
12
+ respond_with(@list)
13
+ end
14
+
15
+ def new
16
+ @list = CampagneList.new
17
+ respond_with(@list)
18
+ end
19
+
20
+ def create
21
+ @list = CampagneList.new(params[:campagne_campagne_list])
22
+ flash[:notice] = 'List was successfully created.' if @list.save
23
+ respond_with(@list, :location => campagne_campagne_lists_path)
24
+ end
25
+
26
+ def import
27
+ @list = CampagneList.find(params[:id])
28
+ if request.post?
29
+ emails = params[:emails].split("\r\n")
30
+ emails.each do |email|
31
+ if contact = CampagneContact.find_by_email(email)
32
+ if !@list.contacts.exists?(contact.id)
33
+ @list.contacts << contact
34
+ @list.save
35
+ end
36
+ else
37
+ @list.contacts.create(:email => email)
38
+ end
39
+ end
40
+ redirect_to campagne_campagne_lists_path, :notice => "Contacts were successfully imported."
41
+ end
42
+ end
43
+
44
+ end
45
+ end
@@ -0,0 +1,26 @@
1
+ module Campagne
2
+ module ErrorMessagesHelper
3
+ # Render error messages for the given objects. The :message and :header_message options are allowed.
4
+ def error_messages_for(*objects)
5
+ options = objects.extract_options!
6
+ options[:header_message] ||= I18n.t(:"activerecord.errors.header", :default => "Invalid Fields")
7
+ options[:message] ||= I18n.t(:"activerecord.errors.message", :default => "Correct the following errors and try again.")
8
+ messages = objects.compact.map { |o| o.errors.full_messages }.flatten
9
+ unless messages.empty?
10
+ content_tag(:div, :class => "error_messages") do
11
+ list_items = messages.map { |msg| content_tag(:li, msg) }
12
+ content_tag(:h2, options[:header_message]) + content_tag(:p, options[:message]) + content_tag(:ul, list_items.join.html_safe)
13
+ end
14
+ end
15
+ end
16
+
17
+ module FormBuilderAdditions
18
+ def error_messages(options = {})
19
+ @template.error_messages_for(@object, options)
20
+ end
21
+ end
22
+
23
+ end
24
+ end
25
+
26
+ ActionView::Helpers::FormBuilder.send(:include, Campagne::ErrorMessagesHelper::FormBuilderAdditions)
@@ -0,0 +1,26 @@
1
+ # These helper methods can be called in your template to set variables to be used in the layout
2
+ # This module should be included in all views globally,
3
+ # to do so you may need to add this line to your ApplicationController
4
+ # helper :layout
5
+ module Campagne
6
+ module LayoutHelper
7
+
8
+ def title(page_title, show_title = true)
9
+ content_for(:title) { h(page_title.to_s) }
10
+ @show_title = show_title
11
+ end
12
+
13
+ def show_title?
14
+ @show_title
15
+ end
16
+
17
+ def stylesheet(*args)
18
+ content_for(:head) { stylesheet_link_tag(*args) }
19
+ end
20
+
21
+ def javascript(*args)
22
+ content_for(:head) { javascript_include_tag(*args) }
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module Campagne
2
+ class CampagneCampaign < ActiveRecord::Base
3
+ has_and_belongs_to_many :campagne_lists
4
+ has_many :campagne_deliveries
5
+
6
+ attr_accessible :name, :subject, :body, :campagne_list_ids
7
+
8
+ validates :name, :presence => true, :uniqueness => true
9
+ validates :subject, :presence => true
10
+ validates :body, :presence => true
11
+
12
+ def formated_body(token)
13
+ formated_body = body.gsub('||WB||', "<img src=\"#{Rails.application.config.campagne_base_url}/campagne/see/#{token}\" width=\"1\" height=\"1\" />")
14
+ formated_body = formated_body.gsub('||UNSUB||', "#{Rails.application.config.campagne_base_url}/campagne/unsubscribe/#{token}")
15
+ formated_body = formated_body.gsub('||LINK||', "#{Rails.application.config.campagne_base_url}/campagne/click/#{token}?link=")
16
+ # formated_body = formated_body.gsub('||VIEW||', "#{Rails.application.config.campagne_base_url}/campagne/preview/#{token}")
17
+ formated_body
18
+ end
19
+
20
+ def preview
21
+ formated_body('0')
22
+ end
23
+
24
+ end
25
+ end
@@ -0,0 +1,10 @@
1
+ module Campagne
2
+ class CampagneContact < ActiveRecord::Base
3
+ has_and_belongs_to_many :campagne_lists
4
+
5
+ attr_accessible :email, :name
6
+
7
+ validates :email, :presence => true, :uniqueness => true, :email => true, :incorrect_email => true, :denied_email => true
8
+
9
+ end
10
+ end
@@ -0,0 +1,37 @@
1
+ module Campagne
2
+ class CampagneDelivery < ActiveRecord::Base
3
+ belongs_to :campagne_campaign
4
+
5
+ attr_accessible :contact_id, :campagne_list_id , :email, :token
6
+
7
+ def see!(request)
8
+ self.ip_address = request.remote_ip if ip_address.blank?
9
+ self.user_agent = request.user_agent if user_agent.blank?
10
+ self.seen_at = Time.now if seen_at.nil?
11
+ save
12
+ end
13
+
14
+ def click!(request)
15
+ self.ip_address = request.remote_ip if ip_address.blank?
16
+ self.user_agent = request.user_agent if user_agent.blank?
17
+ self.seen_at = Time.now if seen_at.nil?
18
+ self.clicked_at = Time.now if clicked_at.nil?
19
+ save
20
+ end
21
+
22
+ def unsubscribe!(request)
23
+ CampagneList.find(list_id).contacts.delete(CampagneContact.find(contact_id)) # Remove contact from list
24
+ self.ip_address = request.remote_ip if ip_address.blank?
25
+ self.user_agent = request.user_agent if user_agent.blank?
26
+ self.seen_at = Time.now if seen_at.nil?
27
+ self.unsubscribed_at = Time.now if unsubscribed_at.nil?
28
+ save
29
+ end
30
+
31
+ def bounce!
32
+ CampagneList.find(list_id).contacts.delete(CampagneContact.find(contact_id))
33
+ update_attribute(:bounced_at, Time.now) if bounced_at.nil?
34
+ end
35
+
36
+ end
37
+ end
@@ -0,0 +1,11 @@
1
+ module Campagne
2
+ class CampagneList < ActiveRecord::Base
3
+ has_and_belongs_to_many :campagne_contacts
4
+ has_and_belongs_to_many :campagne_campaigns
5
+
6
+ attr_accessible :name, :contact_ids
7
+
8
+ validates :name, :presence => true, :uniqueness => true
9
+
10
+ end
11
+ end
@@ -0,0 +1,19 @@
1
+ # encoding: UTF-8
2
+ module Campagne
3
+ class Sender
4
+
5
+ def self.deliver_email(email, subject, email_body, token)
6
+ sleep 0.5
7
+ ActionMailer::Base::mail({
8
+ :from => "#{Rails.application.config.campagne_from_name} <#{Rails.application.config.campagne_from_email}>",
9
+ :to => email.downcase,
10
+ :subject => subject,
11
+ :body => email_body,
12
+ :content_type => 'text/html; charset=UTF-8'#,
13
+ :'Return-Path' => "bounce+#{token}@#{Rails.application.config.campagne_domain}",
14
+ :'List-Unsubscribe' => "<mailto:unsubscribe-#{token}@#{Rails.application.config.campagne_domain}>, <#{Rails.application.config.campagne_base_url}/campagne/unsubscribe/#{token}>"
15
+ }).deliver
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,29 @@
1
+ # encoding: UTF-8
2
+ module Campagne
3
+ class SenderJob
4
+ @queue = :default
5
+
6
+ def self.generate_token
7
+ token = rand(36**18).to_s(36)
8
+ token = token.gsub('+', '')
9
+ while CampagneDelivery.where(:token => token).first do
10
+ token = rand(36**18).to_s(36)
11
+ end
12
+ token
13
+ end
14
+
15
+ def self.perform(campaign_id)
16
+ ActiveSupport::BufferedLogger.new(Rails.root.join('log/resque.log')).info([Time.now.iso8601, $$, "I", "---PERFORM---", campaign_id].join("\t"))
17
+ campaign = Campaign.find(campaign_id)
18
+ contacts = campaign.lists.map(&:contacts).flatten
19
+ contacts = contacts.sort_by {rand} # shuffle
20
+ contacts.each do |contact|
21
+ next if campaign.deliveries.where(:contact_id => contact.id).first
22
+ token = generate_token
23
+ Sender.deliver_email(contact.email, campaign.subject, campaign.formated_body(token), token)
24
+ campaign.deliveries.create(:contact_id => contact.id, :email => contact.email, :token => token)
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # encoding: UTF-8
2
+ require 'mail'
3
+
4
+ class EmailValidator < ActiveModel::EachValidator
5
+ def validate_each(record, attribute, value)
6
+ return if value.blank?
7
+ begin
8
+ # http://my.rails-royce.org/2010/07/21/email-validation-in-ruby-on-rails-without-regexp/
9
+ m = Mail::Address.new(value)
10
+ # We must check that value contains a domain and that value is an email address
11
+ r = m.domain && m.address == value
12
+ t = m.__send__(:tree)
13
+ # We need to dig into treetop
14
+ # A valid domain must have dot_atom_text elements size > 1
15
+ # user@localhost is excluded
16
+ # treetop must respond to domain
17
+ # We exclude valid email values like <user@localhost.com>
18
+ # Hence we use m.__send__(tree).domain
19
+ r &&= (t.domain.dot_atom_text.elements.size > 1)
20
+ rescue Exception => e
21
+ r = false
22
+ end
23
+ record.errors[attribute] << (options[:message] || 'inválido') unless r
24
+ end
25
+ end
26
+
27
+ class IncorrectEmailValidator < ActiveModel::EachValidator
28
+ def validate_each(record, attribute, value)
29
+ domains = %w(
30
+ hotmail.com.br gmail.com.br
31
+ hotamil.com hotimail.com hotmail.com.br hotmail.com.com hotmail.con hotmal.com hoymail.com hotmil.com
32
+ gamil.com gmail.com.br gmal.com gmeil.com.br gmial.com
33
+ teste.com teste.com.br
34
+ yaoo.com
35
+ .com.be
36
+ )
37
+ domains.each do |d|
38
+ if value && value.include?("@#{d}")
39
+ record.errors[attribute] << 'está incorreto'
40
+ break
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ class DeniedEmailValidator < ActiveModel::EachValidator
47
+ def validate_each(record, attribute, value)
48
+ domains = %w(mailinator.com dodgit.com uggsrock.com spambox.us spamhole.com spam.la trashymail.com guerrillamailblock.com spamspot.com spamfree tempomail.fr jetable.net maileater.com meltmail.com)
49
+ domains.each do |d|
50
+ if value && value.include?("@#{d}")
51
+ record.errors[attribute] << 'inválido'
52
+ break
53
+ end
54
+ end
55
+ end
56
+ end
Binary file
@@ -0,0 +1,23 @@
1
+ <%= form_for @campaign do |f| %>
2
+ <%= f.error_messages %>
3
+ <p>
4
+ <%= f.label :name %><br />
5
+ <%= f.text_field :name %>
6
+ </p>
7
+ <p>
8
+ <%= f.label :subject %><br />
9
+ <%= f.text_field :subject %>
10
+ </p>
11
+ <p>
12
+ <%= f.label :body %><br />
13
+ <%= f.text_area :body %>
14
+ </p>
15
+ <p>
16
+ <%= hidden_field_tag "campagne_campagne_campaign[campagne_list_ids][]", nil%>
17
+ <% Campagne::CampagneList.all.each do |list| %>
18
+ <%= check_box_tag "campagne_campagne_campaign[campagne_list_ids][]", list.id, @campaign.campagne_list_ids.include?(list.id), id: dom_id(list) %>
19
+ <%= label_tag dom_id(list), list.name %><br/>
20
+ <% end %>
21
+ </p>
22
+ <p><%= f.submit 'Create Campaign' %></p>
23
+ <% end %>
@@ -0,0 +1,30 @@
1
+ <% title "Deliveries" %>
2
+
3
+ <h2><%= @campaign.name %></h2>
4
+
5
+ <table style="width:100%">
6
+ <tr>
7
+ <th>Email</th>
8
+ <th>Token</th>
9
+ <th>Sent at</th>
10
+ <th>Seen at</th>
11
+ <th>Clicked at</th>
12
+ <th>Unsubscribed at</th>
13
+ <th>Bounced at</th>
14
+ <th>IP Address</th>
15
+ <th>User Agent</th>
16
+ </tr>
17
+ <% for delivery in @deliveries %>
18
+ <tr>
19
+ <td><%= delivery.email %></td>
20
+ <td><%= delivery.token %></td>
21
+ <td><%= delivery.created_at %></td>
22
+ <td><%= delivery.seen_at %></td>
23
+ <td><%= delivery.clicked_at %></td>
24
+ <td><%= delivery.unsubscribed_at %></td>
25
+ <td><%= delivery.bounced_at %></td>
26
+ <td><%= delivery.ip_address %></td>
27
+ <td><%= delivery.user_agent %></td>
28
+ </tr>
29
+ <% end %>
30
+ </table>
@@ -0,0 +1,8 @@
1
+ <% title "Edit Campaign" %>
2
+
3
+ <%= render 'form' %>
4
+
5
+ <p>
6
+ <%= link_to "Show", @campaign %> |
7
+ <%= link_to "View All", campagne_campagne_campaigns_path %>
8
+ </p>
@@ -0,0 +1,20 @@
1
+ <% title "Campaigns" %>
2
+
3
+ <p><%= link_to "New Campaign", new_campagne_campagne_campaign_path %></p>
4
+
5
+ <table>
6
+ <tr>
7
+ <th>Name</th>
8
+ <th>Subject</th>
9
+ </tr>
10
+ <% for campaign in @campaigns %>
11
+ <tr>
12
+ <td><%= campaign.name %></td>
13
+ <td><%= campaign.subject %></td>
14
+ <td><%= link_to "Preview", preview_campagne_campagne_campaign_path(campaign) %></td>
15
+ <td><%= link_to "Deliveries", deliveries_campagne_campagne_campaign_path(campaign) %></td>
16
+ <td><%= link_to "Show", campaign %></td>
17
+ <td><%= link_to "Edit", edit_campagne_campagne_campaign_path(campaign) %></td>
18
+ </tr>
19
+ <% end %>
20
+ </table>
@@ -0,0 +1,5 @@
1
+ <% title "New Campaign" %>
2
+
3
+ <%= render 'form' %>
4
+
5
+ <p><%= link_to "Back", campagne_campagne_campaigns_path %></p>
@@ -0,0 +1 @@
1
+ <%= raw(@campaign.preview) %>
@@ -0,0 +1,35 @@
1
+ <% title "Campaign" %>
2
+
3
+ <%= form_tag schedule_campagne_campagne_campaign_path(@campaign) do %>
4
+ <%= datetime_select("schedule", 'at', :default => Time.now) %>
5
+ <%= submit_tag 'Schedule' %>
6
+ <% end %>
7
+
8
+ <p>
9
+ <%= link_to "Preview", preview_campagne_campagne_campaign_path(@campaign) %> |
10
+ <%= link_to "Deliveries", deliveries_campagne_campagne_campaign_path(@campaign) %> |
11
+ <%= link_to "Edit", edit_campagne_campagne_campaign_path(@campaign) %> |
12
+ <%= link_to "View All", campagne_campagne_campaigns_path %>
13
+ </p>
14
+
15
+ <p>
16
+ <strong>Name:</strong>
17
+ <%= @campaign.name %>
18
+ </p>
19
+ <p>
20
+ <strong>Subject:</strong>
21
+ <%= @campaign.subject %>
22
+ </p>
23
+ <p>
24
+ <strong>Lists:</strong><br/>
25
+ <% @campaign.campagne_lists.each do |list| %>
26
+ <%= list.name %><br/>
27
+ <% end %>
28
+ </p>
29
+ <p>
30
+ <strong>Body:</strong><br />
31
+ <pre>
32
+ <%= raw(@campaign.body) %>
33
+ </pre>
34
+ </p>
35
+
@@ -0,0 +1,8 @@
1
+ <%= form_for @list do |f| %>
2
+ <%= f.error_messages %>
3
+ <p>
4
+ <%= f.label :name %><br />
5
+ <%= f.text_field :name %>
6
+ </p>
7
+ <p><%= f.submit 'Create List' %></p>
8
+ <% end %>
@@ -0,0 +1,12 @@
1
+ <% title "Import contacts" %>
2
+
3
+ <h2><%= @list.name %></h2>
4
+
5
+ <%= form_tag import_campagne_campagne_list_path(@list) do %>
6
+ <p>
7
+ <%= text_area_tag 'emails', nil, :style => 'width:600px;height:400px' %>
8
+ </p>
9
+ <p><%= submit_tag 'Import' %></p>
10
+ <% end %>
11
+
12
+ <p><%= link_to "Back", campagne_campagne_lists_path %></p>
@@ -0,0 +1,16 @@
1
+ <% title "Lists" %>
2
+
3
+ <p><%= link_to "New List", new_campagne_campagne_list_path %></p>
4
+
5
+ <table>
6
+ <tr>
7
+ <th>Name</th>
8
+ </tr>
9
+ <% for list in @lists %>
10
+ <tr>
11
+ <td><%= list.name %></td>
12
+ <td><%= link_to "Import", import_campagne_campagne_list_path(list) %></td>
13
+ <td><%= link_to "Show", list %></td>
14
+ </tr>
15
+ <% end %>
16
+ </table>
@@ -0,0 +1,5 @@
1
+ <% title "New List" %>
2
+
3
+ <%= render 'form' %>
4
+
5
+ <p><%= link_to "Back", campagne_campagne_lists_path %></p>
@@ -0,0 +1,17 @@
1
+ <% title "List" %>
2
+
3
+ <p>
4
+ <strong>Name:</strong>
5
+ <%= @list.name %>
6
+ </p>
7
+
8
+ <p>
9
+ <strong>Emails:</strong><br />
10
+ <% @list.campagne_contacts.each do |contact| %>
11
+ <%= contact.email %><br />
12
+ <% end %>
13
+ </p>
14
+
15
+ <p>
16
+ <%= link_to "View All", campagne_campagne_lists_path %>
17
+ </p>
@@ -0,0 +1,30 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= content_for?(:title) ? yield(:title) : "Untitled" %></title>
5
+ <%= stylesheet_link_tag :campagne %>
6
+ <%= javascript_include_tag :campagne %>
7
+ <%= csrf_meta_tag %>
8
+ <%= yield(:head) %>
9
+ </head>
10
+ <body>
11
+
12
+ <div id="nav">
13
+ <%= link_to 'Lists', campagne_campagne_lists_path %> |
14
+ <%= link_to 'Campaings', campagne_campagne_campaigns_path %>
15
+ </div>
16
+
17
+ <div id="container">
18
+ <% if notice %>
19
+ <%= content_tag :div, notice, :id => "flash_notice" %>
20
+ <% end %>
21
+
22
+ <% if alert %>
23
+ <%= content_tag :div, alert, :id => "flash_alert" %>
24
+ <% end %>
25
+
26
+ <%= content_tag :h1, yield(:title) if show_title? %>
27
+ <%= yield %>
28
+ </div>
29
+ </body>
30
+ </html>
data/campagne.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/campagne/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "campagne"
6
+ s.version = Campagne::VERSION
7
+ s.authors = ["Arley Lobato"]
8
+ s.email = ["arleylobato@gmail.com"]
9
+ s.homepage = "http://github.com/alobato/campagne"
10
+ s.summary = "campagne-#{s.version}"
11
+ s.description = "A simple Rails 3 engine gem to manage and send newsletters."
12
+
13
+ s.add_dependency "rails", "~> 3.2.3"
14
+ s.add_dependency "mysql2", "~> 0.3.11"
15
+ s.add_dependency "jquery-rails", "~> 2.0.2"
16
+ s.add_dependency "resque", "~> 1.20.0"
17
+ s.add_dependency "resque-scheduler", "~> 1.9.9"
18
+
19
+ s.files = `git ls-files`.split("\n")
20
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
21
+ s.require_path = 'lib'
22
+
23
+ end
@@ -0,0 +1,5 @@
1
+ # Sample localization file for English. Add more files in this directory for other locales.
2
+ # See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
3
+
4
+ en:
5
+ hello: "Hello world"
data/config/routes.rb ADDED
@@ -0,0 +1,25 @@
1
+ Rails.application.routes.draw do
2
+
3
+ namespace :campagne do
4
+ root to: 'campagne_lists#index'
5
+ mount Resque::Server.new, at: 'resque'
6
+ resources :campagne_lists do
7
+ member do
8
+ get 'import'
9
+ post 'import'
10
+ end
11
+ end
12
+ resources :campagne_campaigns do
13
+ member do
14
+ get 'preview'
15
+ get 'deliveries'
16
+ post 'schedule'
17
+ end
18
+ end
19
+ get 'see/:token' => 'campagne_deliveries#see'
20
+ get 'click/:token' => 'campagne_deliveries#click'
21
+ get 'unsubscribe/:token' => 'campagne_deliveries#unsubscribe'
22
+ get 'bounce/:token' => 'campagne_deliveries#bounce'
23
+ end
24
+
25
+ end
data/lib/bulletin.rb ADDED
@@ -0,0 +1,18 @@
1
+ # Requires
2
+ require "active_support/dependencies"
3
+
4
+ module Campagne
5
+
6
+ # Our host application root path
7
+ # We set this when the engine is initialized
8
+ mattr_accessor :app_root
9
+
10
+ # Yield self on setup for nice config blocks
11
+ def self.setup
12
+ yield self
13
+ end
14
+
15
+ end
16
+
17
+ # Require our engine
18
+ require "campagne/engine"
@@ -0,0 +1,23 @@
1
+ # http://edgeapi.rubyonrails.org/classes/Rails/Engine.html
2
+ module Campagne
3
+ class Engine < Rails::Engine
4
+
5
+ initializer "campagne.load_app_instance_data" do |app|
6
+ Campagne.setup do |config|
7
+ config.app_root = app.root
8
+ end
9
+ end
10
+
11
+ initializer "campagne.load_static_assets" do |app|
12
+ app.middleware.use ::ActionDispatch::Static, "#{root}/public"
13
+ end
14
+
15
+ initializer "campagne.load_resque_config" do |app|
16
+ require 'resque/server'
17
+ Resque::Server.use(Rack::Auth::Basic)
18
+ require 'resque_scheduler'
19
+ require 'resque_scheduler/server'
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module Campagne
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,30 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/migration'
3
+
4
+ class CampagneGenerator < Rails::Generators::Base
5
+ include Rails::Generators::Migration
6
+ def self.source_root
7
+ @source_root ||= File.join(File.dirname(__FILE__), 'templates')
8
+ end
9
+
10
+ def self.next_migration_number(dirname)
11
+ if ActiveRecord::Base.timestamped_migrations
12
+ Time.new.utc.strftime("%Y%m%d%H%M%S")
13
+ else
14
+ "%.3d" % (current_migration_number(dirname) + 1)
15
+ end
16
+ end
17
+
18
+ def create_migration_file
19
+ migration_template 'migration.rb', 'db/migrate/create_campagne_tables.rb'
20
+ end
21
+
22
+ def copy_initializer_file
23
+ copy_file 'initializer.rb', 'config/initializers/campagne.rb'
24
+ end
25
+
26
+ def copy_1_pixel_image
27
+ copy_file "1x1.gif", "public/1x1.gif"
28
+ end
29
+
30
+ end
@@ -0,0 +1,11 @@
1
+ require 'resque/server'
2
+ Resque::Server.use(Rack::Auth::Basic) do |username, password|
3
+ username == 'admin' && password == "secret"
4
+ end
5
+
6
+ Rails.application.class.configure do
7
+ config.campagne_from_name = 'Foo'
8
+ config.campagne_from_email = 'foo@bar.com'
9
+ config.campagne_domain = 'foobar.com'
10
+ config.campagne_base_url = 'http://www.foobar.com'
11
+ end
@@ -0,0 +1,48 @@
1
+ class CreateCampagneTables < ActiveRecord::Migration
2
+
3
+ def change
4
+ create_table :campagne_contacts do |t|
5
+ t.string :email
6
+ t.string :name
7
+ t.timestamps
8
+ end
9
+ create_table :campagne_lists do |t|
10
+ t.string :name
11
+ t.timestamps
12
+ end
13
+ create_table :campagne_contacts_campagne_lists, :id => false do |t|
14
+ t.integer :campagne_contact_id
15
+ t.integer :campagne_list_id
16
+ t.timestamps
17
+ end
18
+ create_table :campagne_campaigns do |t|
19
+ t.string :name
20
+ t.string :subject
21
+ t.text :body, :limit => 2147483647
22
+ t.timestamps
23
+ end
24
+ create_table :campagne_campaigns_campagne_lists, :id => false do |t|
25
+ t.integer :campagne_campaign_id
26
+ t.integer :campagne_list_id
27
+ t.timestamps
28
+ end
29
+ create_table :campagne_deliveries do |t|
30
+ t.integer :campagne_campaign_id
31
+ t.integer :campagne_contact_id
32
+ t.integer :campagne_list_id
33
+ t.string :email
34
+ t.string :token
35
+ t.datetime :seen_at
36
+ t.datetime :clicked_at
37
+ t.datetime :unsubscribed_at
38
+ t.datetime :bounced_at
39
+ t.string :ip_address
40
+ t.string :user_agent
41
+ t.timestamps
42
+ end
43
+ add_index "campagne_deliveries", ["token"], :name => "index_deliveries_on_token", :unique => true
44
+ add_index "campagne_campaigns_campagne_lists", ["campagne_campaign_id", "campagne_list_id"], :name => "index_campaigns_lists_on_c_and_l", :unique => true
45
+ add_index "campagne_contacts_campagne_lists", ["campagne_contact_id", "campagne_list_id"], :name => "index_contacts_lists_on_c_and_l", :unique => true
46
+ end
47
+
48
+ end
@@ -0,0 +1,100 @@
1
+ require 'resque/tasks'
2
+ require 'resque_scheduler/tasks'
3
+
4
+ def run_worker(queue, count = 1)
5
+ puts "Starting #{count} worker(s) with QUEUE: #{queue}"
6
+ ops = {:pgroup => true, :err => [(Rails.root + "log/workers_error.log").to_s, "a"],
7
+ :out => [(Rails.root + "log/workers.log").to_s, "a"]}
8
+ env_vars = {
9
+ "QUEUE" => queue.to_s,
10
+ "BACKGROUND" => "1",
11
+ "PIDFILE" => (Rails.root + "tmp/pids/resque.pid").to_s,
12
+ "VERBOSE" => "1"
13
+ }
14
+ count.times {
15
+ pid = spawn(env_vars, "rake resque:work", ops)
16
+ Process.detach(pid)
17
+ }
18
+ end
19
+
20
+ def run_scheduler
21
+ puts "Starting resque scheduler"
22
+ env_vars = {
23
+ "BACKGROUND" => "1",
24
+ "PIDFILE" => (Rails.root + "tmp/pids/resque_scheduler.pid").to_s,
25
+ "VERBOSE" => "1"
26
+ }
27
+ ops = {:pgroup => true, :err => [(Rails.root + "log/scheduler_error.log").to_s, "a"],
28
+ :out => [(Rails.root + "log/scheduler.log").to_s, "a"]}
29
+ pid = spawn(env_vars, "rake resque:scheduler", ops)
30
+ Process.detach(pid)
31
+ end
32
+
33
+ namespace :resque do
34
+ task :setup => :environment
35
+
36
+ desc "Restart running workers"
37
+ task :restart_workers => :environment do
38
+ Rake::Task['resque:stop_workers'].invoke
39
+ Rake::Task['resque:start_workers'].invoke
40
+ end
41
+
42
+ desc "Quit running workers"
43
+ task :stop_workers => :environment do
44
+ pids = Array.new
45
+ Resque.workers.each do |worker|
46
+ pids.concat(worker.worker_pids)
47
+ end
48
+ if pids.empty?
49
+ puts "No workers to kill"
50
+ else
51
+ syscmd = "kill -s QUIT #{pids.join(' ')}"
52
+ puts "Running syscmd: #{syscmd}"
53
+ system(syscmd)
54
+ end
55
+ end
56
+
57
+ desc "Start workers"
58
+ task :start_workers => :environment do
59
+ run_worker("default", 1)
60
+ end
61
+
62
+ desc "Restart scheduler"
63
+ task :restart_scheduler => :environment do
64
+ Rake::Task['resque:stop_scheduler'].invoke
65
+ Rake::Task['resque:start_scheduler'].invoke
66
+ end
67
+
68
+ desc "Quit scheduler"
69
+ task :stop_scheduler => :environment do
70
+ pidfile = Rails.root + "tmp/pids/resque_scheduler.pid"
71
+ if !File.exists?(pidfile)
72
+ puts "Scheduler not running"
73
+ else
74
+ pid = File.read(pidfile).to_i
75
+ syscmd = "kill -s QUIT #{pid}"
76
+ puts "Running syscmd: #{syscmd}"
77
+ system(syscmd)
78
+ FileUtils.rm_f(pidfile)
79
+ end
80
+ end
81
+
82
+ desc "Start scheduler"
83
+ task :start_scheduler => :environment do
84
+ run_scheduler
85
+ end
86
+
87
+ desc "Reload schedule"
88
+ task :reload_schedule => :environment do
89
+ pidfile = Rails.root + "tmp/pids/resque_scheduler.pid"
90
+
91
+ if !File.exists?(pidfile)
92
+ puts "Scheduler not running"
93
+ else
94
+ pid = File.read(pidfile).to_i
95
+ syscmd = "kill -s USR2 #{pid}"
96
+ puts "Running syscmd: #{syscmd}"
97
+ system(syscmd)
98
+ end
99
+ end
100
+ end
File without changes
metadata ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: campagne
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Arley Lobato
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-04-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: &70126380669840 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 3.2.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70126380669840
25
+ - !ruby/object:Gem::Dependency
26
+ name: mysql2
27
+ requirement: &70126380668860 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: 0.3.11
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70126380668860
36
+ - !ruby/object:Gem::Dependency
37
+ name: jquery-rails
38
+ requirement: &70126380668040 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.0.2
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70126380668040
47
+ - !ruby/object:Gem::Dependency
48
+ name: resque
49
+ requirement: &70126380667220 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: 1.20.0
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *70126380667220
58
+ - !ruby/object:Gem::Dependency
59
+ name: resque-scheduler
60
+ requirement: &70126380666460 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ~>
64
+ - !ruby/object:Gem::Version
65
+ version: 1.9.9
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *70126380666460
69
+ description: A simple Rails 3 engine gem to manage and send newsletters.
70
+ email:
71
+ - arleylobato@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - .gitignore
77
+ - .rvmrc
78
+ - Gemfile
79
+ - README.md
80
+ - Rakefile
81
+ - app/assets/.DS_Store
82
+ - app/assets/javascripts/campagne.js
83
+ - app/assets/stylesheets/campagne.css
84
+ - app/controllers/bulletin/.DS_Store
85
+ - app/controllers/bulletin/campagne_campaigns_controller.rb
86
+ - app/controllers/bulletin/campagne_deliveries_controller.rb
87
+ - app/controllers/bulletin/campagne_lists_controller.rb
88
+ - app/helpers/campagne/error_messages_helper.rb
89
+ - app/helpers/campagne/layout_helper.rb
90
+ - app/models/campagne/campagne_campaign.rb
91
+ - app/models/campagne/campagne_contact.rb
92
+ - app/models/campagne/campagne_delivery.rb
93
+ - app/models/campagne/campagne_list.rb
94
+ - app/models/campagne/sender.rb
95
+ - app/models/campagne/sender_job.rb
96
+ - app/validators/email_validator.rb
97
+ - app/views/campagne/.DS_Store
98
+ - app/views/campagne/campagne_campaigns/_form.html.erb
99
+ - app/views/campagne/campagne_campaigns/deliveries.html.erb
100
+ - app/views/campagne/campagne_campaigns/edit.html.erb
101
+ - app/views/campagne/campagne_campaigns/index.html.erb
102
+ - app/views/campagne/campagne_campaigns/new.html.erb
103
+ - app/views/campagne/campagne_campaigns/preview.html.erb
104
+ - app/views/campagne/campagne_campaigns/show.html.erb
105
+ - app/views/campagne/campagne_lists/_form.html.erb
106
+ - app/views/campagne/campagne_lists/import.html.erb
107
+ - app/views/campagne/campagne_lists/index.html.erb
108
+ - app/views/campagne/campagne_lists/new.html.erb
109
+ - app/views/campagne/campagne_lists/show.html.erb
110
+ - app/views/layouts/campagne/campagne.html.erb
111
+ - campagne.gemspec
112
+ - config/locales/en.yml
113
+ - config/routes.rb
114
+ - lib/bulletin.rb
115
+ - lib/campagne/engine.rb
116
+ - lib/campagne/version.rb
117
+ - lib/generators/campagne/campagne_generator.rb
118
+ - lib/generators/campagne/templates/1x1.gif
119
+ - lib/generators/campagne/templates/initializer.rb
120
+ - lib/generators/campagne/templates/migration.rb
121
+ - lib/tasks/resque.rake
122
+ - test/test_helper.rb
123
+ homepage: http://github.com/alobato/campagne
124
+ licenses: []
125
+ post_install_message:
126
+ rdoc_options: []
127
+ require_paths:
128
+ - lib
129
+ required_ruby_version: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ! '>='
133
+ - !ruby/object:Gem::Version
134
+ version: '0'
135
+ segments:
136
+ - 0
137
+ hash: 1196017273243914426
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ none: false
140
+ requirements:
141
+ - - ! '>='
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ segments:
145
+ - 0
146
+ hash: 1196017273243914426
147
+ requirements: []
148
+ rubyforge_project:
149
+ rubygems_version: 1.8.17
150
+ signing_key:
151
+ specification_version: 3
152
+ summary: campagne-0.0.2
153
+ test_files: []