]> nos-oignons.net Git - gestion-adh.git/commitdiff
Implement send-membership-reminders
authorLunar <lunar@anargeek.net>
Sun, 2 Jun 2013 17:00:35 +0000 (19:00 +0200)
committerLunar <lunar@anargeek.net>
Sun, 2 Jun 2013 17:00:35 +0000 (19:00 +0200)
15 files changed:
.gitignore
Gemfile.lock
README
bin/send-membership-reminders [new file with mode: 0755]
features/send-membership-reminders.feature [new file with mode: 0644]
features/step_definitions/commands.rb
features/step_definitions/emails.rb [new file with mode: 0644]
features/step_definitions/members.rb
features/support/env.rb
lib/nos_oignons.rb
lib/nos_oignons/member.rb
lib/nos_oignons/reminder.rb [new file with mode: 0644]
lib/nos_oignons/reminder_db.rb [new file with mode: 0644]
nos_oignons.gemspec
var/.placeholder [new file with mode: 0644]

index ab0f804f541038ac9d1a0bb71d10e84d9309a0f7..2ece9d8d66a82a71f738457f7931f3f1366fe810 100644 (file)
@@ -2,3 +2,5 @@
 
 /.bundle
 /vendor/bundle
 
 /.bundle
 /vendor/bundle
+
+/var/reminders.yaml
index f41add5c30570e782985355bee1d59280d8dc0e6..2728fde40bbb70459536a50c479b9e1d0ba593a2 100644 (file)
@@ -2,6 +2,7 @@ PATH
   remote: .
   specs:
     nos_oignons (0.0.1.dev)
   remote: .
   specs:
     nos_oignons (0.0.1.dev)
+      mail
       safe_yaml
 
 GEM
       safe_yaml
 
 GEM
@@ -24,11 +25,19 @@ GEM
     gherkin (2.12.0)
       multi_json (~> 1.3)
     json (1.7.7)
     gherkin (2.12.0)
       multi_json (~> 1.3)
     json (1.7.7)
+    mail (2.5.4)
+      mime-types (~> 1.16)
+      treetop (~> 1.4.8)
+    mime-types (1.23)
     multi_json (1.7.4)
     multi_json (1.7.4)
+    polyglot (0.3.3)
     rspec-expectations (2.13.0)
       diff-lcs (>= 1.1.3, < 2.0)
     safe_yaml (0.9.2)
     timecop (0.6.1)
     rspec-expectations (2.13.0)
       diff-lcs (>= 1.1.3, < 2.0)
     safe_yaml (0.9.2)
     timecop (0.6.1)
+    treetop (1.4.12)
+      polyglot
+      polyglot (>= 0.3.1)
 
 PLATFORMS
   ruby
 
 PLATFORMS
   ruby
diff --git a/README b/README
index 573ec803058fbe0d834cd30719604e82c2806564..0aafd0a4a48bda823599b97bdf8e1780dc44d3b1 100644 (file)
--- a/README
+++ b/README
@@ -100,6 +100,15 @@ Met à jour la liste des emails inscrites à la liste ag@ par rapport aux membre
 lancer les commandes `list_members`, `add_members` et `remove_members` via
 `sudo` sur le compte `list`.
 
 lancer les commandes `list_members`, `add_members` et `remove_members` via
 `sudo` sur le compte `list`.
 
+`send-membership-reminders`
+---------------------------
+
+Préviens les membres qu'il faut renouveller leur cotisation.
+
+Le fichier utiliser pour garder la liste des envois déjà effectués est soit
+celui indiqué par la variable d'environnement `NOS_OIGNONS_REMINDER_DB`, ou
+le fichier si elle est vide `var/reminders.yaml`.
+
 Développement
 =============
 
 Développement
 =============
 
diff --git a/bin/send-membership-reminders b/bin/send-membership-reminders
new file mode 100755 (executable)
index 0000000..b089585
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/ruby1.9.1
+#-*- coding: utf-8 -*-
+
+require 'rubygems'
+require 'bundler'
+Bundler.setup
+
+require 'mail'
+require 'nos_oignons'
+
+Mail.defaults do
+  delivery_method :sendmail
+end
+
+NosOignons.send_membership_reminders!
diff --git a/features/send-membership-reminders.feature b/features/send-membership-reminders.feature
new file mode 100644 (file)
index 0000000..cc8507b
--- /dev/null
@@ -0,0 +1,62 @@
+# language: fr
+
+Fonctionnalité: prévenir les membres qu'il faut renouveller leur cotisation
+  En tant que membre de Nos oignons, j'aimerais être prévenu lorsqu'il est
+  tant de renouvelle ma cotisation, afin de pouvoir rester membre de
+  l'association.
+
+  Scénario: Pas de messages
+    Soit une base avec Jane, à jour de cotisation
+    Lorsque j'exécute send-membership-reminders
+    Alors aucun email ne doit avoir été envoyé
+
+  Scénario: Pas de rappel après qu'il soit trop tard
+    Soit une base avec Pierre qui n'a pas payé sa cotisation cette année
+    Lorsque j'exécute send-membership-reminders
+    Alors aucun email ne doit avoir été envoyé
+
+  Scénario: Envoi de l'appel
+    Soit une base avec Jane qui doit renouveller sa cotisation d'ici 30 jours
+    Lorsque j'exécute send-membership-reminders
+    Alors un appel pour la cotisation doit avoir été envoyé
+
+  Scénario: Vérification de l'adresse postale
+    Soit une base avec Jane qui doit renouveller sa cotisation d'ici 30 jours
+    Lorsque j'exécute send-membership-reminders
+    Alors l'appel envoyé doit demander de vérifier l'adresse postale
+
+  Scénario: Pas de messages pour une toute nouvelle adhésion
+    Soit une nouvelle adhésion de Jane
+    Lorsque j'exécute send-membership-reminders
+    Alors aucun email ne doit avoir été envoyé
+
+  Scénario: Un seul envoi par membre
+    Soit une base avec Jane qui doit renouveller sa cotisation d'ici 30 jours
+    Lorsque j'exécute send-membership-reminders
+    Et que j'exécute send-membership-reminders
+    Alors 1 email doit avoir été envoyé
+
+  Scénario: Un an après
+    Soit une base avec Jane qui doit renouveller sa cotisation d'ici 30 jours
+    Et elle avait déjà reçu des appels l'année précédente
+    Lorsque j'exécute send-membership-reminders
+    Alors un appel pour la cotisation doit avoir été envoyé
+
+  Scénario: Premier rappel
+    Soit une base avec Jane qui doit renouveller sa cotisation d'ici 10 jours
+    Lorsque j'exécute send-membership-reminders
+    Alors un premier rappel pour la cotisation doit avoir été envoyé
+
+  Scénario: Dernier rappel
+    Soit une base avec Jane qui doit renouveller sa cotisation d'ici 2 jours
+    Lorsque j'exécute send-membership-reminders
+    Alors un dernier rappel pour la cotisation doit avoir été envoyé
+
+  Scénario: Plusieurs messages
+    Soit une base avec Pierre, à jour de cotisation
+    Et avec Jane qui doit renouveller sa cotisation d'ici 10 jours
+    Et avec Fatima qui doit renouveller sa cotisation d'ici 2 jours
+    Et avec Fred qui doit renouveller sa cotisation d'ici 10 jours
+    Et avec Moly qui doit renouveller sa cotisation d'ici 30 jours
+    Lorsque j'exécute send-membership-reminders
+    Alors 4 emails doivent avoir été envoyés
index 26886e6e2662b6f3c5e5ed5afdab968c0e128682..221523aa59181f50d61ce7717e96e5465baeba69 100644 (file)
@@ -21,6 +21,10 @@ When /^j'exécute update-ag-subscribers$/ do
   run_simple 'update-ag-subscribers'
 end
 
   run_simple 'update-ag-subscribers'
 end
 
+When /^(?:que )?j'exécute send-membership-reminders$/ do
+  NosOignons.send_membership_reminders!
+end
+
 Then /^je ne dois pas avoir eu d'erreur$/ do
   assert_exit_status(0)
 end
 Then /^je ne dois pas avoir eu d'erreur$/ do
   assert_exit_status(0)
 end
diff --git a/features/step_definitions/emails.rb b/features/step_definitions/emails.rb
new file mode 100644 (file)
index 0000000..9ed3f98
--- /dev/null
@@ -0,0 +1,29 @@
+#-*- coding: utf-8 -*-
+
+Then /^aucun email ne doit avoir été envoyé$/ do
+  Mail::TestMailer.deliveries.should be_empty
+end
+
+Then /^un appel pour la cotisation doit avoir été envoyé$/ do
+  Mail::TestMailer.deliveries.should have(1).email
+  mail = Mail::TestMailer.deliveries.first
+  expect(mail.from).to eql([NosOignons::BOARD_EMAIL])
+  expect(mail.to).to eql([@last_member['email']])
+  expect(mail.subject).to include('Renouvellement de votre cotisation')
+  expect(mail.body).to include(@last_member['name'])
+  expect(mail.body).to include('30 jours')
+end
+
+Then /^l'appel envoyé doit demander de vérifier l'adresse postale$/ do
+  mail = Mail::TestMailer.deliveries.first
+  expect(mail.body.to_s.gsub(/^ */, '')).to include(@last_member['address'])
+end
+
+Then /^un (premier|dernier) rappel pour la cotisation doit avoir été envoyé$/ do |kind|
+  mail = Mail::TestMailer.deliveries.first
+  expect(mail.subject).to include("(#{kind == 'premier' ? '' : 'dernier '}rappel)")
+end
+
+Then /^(\d+) emails? (?:doit|doivent) avoir été envoyés?$/ do |count|
+  Mail::TestMailer.deliveries.should have(count).email
+end
index 5bcc39c1bde36dc0c965cee9f8801df6a8eabb04..6e84484178f47f3dec2e53071adbe6917f157da9 100644 (file)
@@ -7,15 +7,15 @@ def init_db
 end
 
 def create_new_member(name, joined_on, paid_on)
 end
 
 def create_new_member(name, joined_on, paid_on)
-  data = { 'name' => name,
-           'address' => "At #{name}",
-           'email' => "#{name.downcase}@example.org",
-           'joined_on' => joined_on,
-           'membership_fee_paid_on' => paid_on
+  @last_member = { 'name' => name,
+                   'address' => "At #{name}",
+                   'email' => "#{name.downcase}@example.org",
+                   'joined_on' => joined_on,
+                   'membership_fee_paid_on' => paid_on
          }
   init_db unless @member_db_path
   file = member_filename_for_id(new_id)
          }
   init_db unless @member_db_path
   file = member_filename_for_id(new_id)
-  write_file file, render_member_file(data)
+  write_file file, render_member_file(@last_member)
 end
 
 Given /une base de membres vide$/ do
 end
 
 Given /une base de membres vide$/ do
@@ -39,11 +39,23 @@ Given /^une base avec (\w+) qui a adhéré le ([0-9-]+) et payé sa dernière co
   create_new_member(name, joined_on, paid_on)
 end
 
   create_new_member(name, joined_on, paid_on)
 end
 
+Given /^(?:une base )?avec (\w+) qui doit renouveller sa cotisation d'ici (\d+) jours$/ do |name, days_before_anniversary|
+  paid_on = Time.now.to_date - days_before_anniversary.to_i
+  joined_on = paid_on
+  create_new_member(name, joined_on, paid_on)
+end
+
 Given /^une nouvelle adhésion de (\w+)$/ do |name|
   joined_on = Time.now.strftime('%Y-%m-%d')
   create_new_member(name, joined_on, joined_on)
 end
 
 Given /^une nouvelle adhésion de (\w+)$/ do |name|
   joined_on = Time.now.strftime('%Y-%m-%d')
   create_new_member(name, joined_on, joined_on)
 end
 
+Given /^elle avait déjà reçu des appels l'année précédente$/ do
+  Timecop.travel(Time.now.to_date - 375) do
+    NosOignons::ReminderDb.instance.record(OpenStruct.new(@last_member))
+  end
+end
+
 When /^j'ajoute une fiche correcte pour une nouvelle adhésion$/ do
   @file = member_filename_for_id(new_id)
   write_file @file, render_member_file(EXTRA_MEMBER)
 When /^j'ajoute une fiche correcte pour une nouvelle adhésion$/ do
   @file = member_filename_for_id(new_id)
   write_file @file, render_member_file(EXTRA_MEMBER)
index 154fc30bd3892b7c7c65cc9b7185eb6259bdc515..210137afeb7da40e81f5b536493cabe4bf7f8ec3 100644 (file)
@@ -6,6 +6,7 @@ require 'timecop'
 require 'tmpdir'
 require 'aruba/cucumber'
 require 'safe_yaml'
 require 'tmpdir'
 require 'aruba/cucumber'
 require 'safe_yaml'
+require 'mail'
 
 SafeYAML::OPTIONS[:default_mode] = :safe
 
 
 SafeYAML::OPTIONS[:default_mode] = :safe
 
@@ -16,11 +17,19 @@ Before do
   ENV['GIT_COMMITTER_EMAIL'] = ENV['GIT_AUTHOR_EMAIL'] = 'test@example.org'
   @tmpdir = Dir.mktmpdir('gestion-adh')
   @dirs = [@tmpdir]
   ENV['GIT_COMMITTER_EMAIL'] = ENV['GIT_AUTHOR_EMAIL'] = 'test@example.org'
   @tmpdir = Dir.mktmpdir('gestion-adh')
   @dirs = [@tmpdir]
-  @aruba_io_wait_seconds = 1
+  @aruba_io_wait_seconds = 0.1
   @orig_wiki_path = ENV['NOS_OIGNONS_BOARD_WIKI_PATH']
   @orig_wiki_path = ENV['NOS_OIGNONS_BOARD_WIKI_PATH']
+  @orig_reminder_db_path = ENV['NOS_OIGNONS_REMINDER_DB']
+  ENV['NOS_OIGNONS_REMINDER_DB'] = File.join(current_dir, 'reminders.yaml')
+  NosOignons::ReminderDb.instance.reload!
+  Mail.defaults do
+    delivery_method :test
+  end
+  Mail::TestMailer.deliveries.clear
 end
 
 After do
 end
 
 After do
+  ENV['NOS_OIGNONS_REMINDER_DB'] = @orig_reminder_db_path
   ENV['NOS_OIGNONS_BOARD_WIKI_PATH'] = @orig_wiki_path
   FileUtils.remove_entry_secure @tmpdir
 end
   ENV['NOS_OIGNONS_BOARD_WIKI_PATH'] = @orig_wiki_path
   FileUtils.remove_entry_secure @tmpdir
 end
index 028cac97375f77f8d0227dcdef5ea8c3a5fbc941..26ffdc63e09b25e9eed5349246a6b4ef23c70c28 100644 (file)
@@ -3,8 +3,13 @@
 require 'nos_oignons/git'
 require 'nos_oignons/mailman'
 require 'nos_oignons/member'
 require 'nos_oignons/git'
 require 'nos_oignons/mailman'
 require 'nos_oignons/member'
+require 'nos_oignons/reminder'
+require 'nos_oignons/reminder_db'
 
 module NosOignons
 
 module NosOignons
+  BOARD_EMAIL = 'ca@nos-oignons.net'
+  MEMBER_MAILING_LIST = 'ag'
+
   # The following class methods are all meant to be called as command-line scripts
   class << self
     def list_emails!
   # The following class methods are all meant to be called as command-line scripts
   class << self
     def list_emails!
@@ -15,13 +20,32 @@ module NosOignons
       end
     end
 
       end
     end
 
-    MEMBER_MAILING_LIST = 'ag'
     def update_ag_subscribers!
     def update_ag_subscribers!
-      current_emails = NosOignons::Mailman.list_members(MEMBER_MAILING_LIST)
+      list = NosOignons::MEMBER_MAILING_LIST
+
+      current_emails = NosOignons::Mailman.list_members(list)
       uptodate_emails = NosOignons::Member.all.select(&:up_to_date?).collect(&:email)
 
       uptodate_emails = NosOignons::Member.all.select(&:up_to_date?).collect(&:email)
 
-      NosOignons::Mailman.add_members(MEMBER_MAILING_LIST, uptodate_emails - current_emails)
-      NosOignons::Mailman.remove_members(MEMBER_MAILING_LIST, current_emails - uptodate_emails)
+      NosOignons::Mailman.add_members(list, uptodate_emails - current_emails)
+      NosOignons::Mailman.remove_members(list, current_emails - uptodate_emails)
+    end
+
+    def send_membership_reminders!
+      today = Time.now.to_date
+      NosOignons::Member.all.select(&:up_to_date?).each do |member|
+        NosOignons::Reminder.all.sort_by(&:days).reverse.each do |reminder|
+          anniversary = Time.new(today.year, member.joined_on.month,
+                                 member.joined_on.day).to_date
+
+          next if member.membership_fee_paid_on > anniversary
+          next if member.membership_fee_paid_on > today - reminder.days
+          next if anniversary > today - reminder.days
+          next if member.reminded_on && member.reminded_on >= today
+
+          member.remind(reminder)
+          break
+        end
+      end
     end
 
     def pre_commit_hook!
     end
 
     def pre_commit_hook!
index 7e6254e81eeb8a6d4a82cee0dcd3c44ffc570e7a..46fc41b3f7f89066de16b824f5d593064e1130ca 100644 (file)
@@ -1,6 +1,8 @@
 require 'safe_yaml'
 SafeYAML::OPTIONS[:default_mode] = :safe
 
 require 'safe_yaml'
 SafeYAML::OPTIONS[:default_mode] = :safe
 
+require 'nos_oignons/reminder_db'
+
 module NosOignons
   MEMBER_FIELDS = [:name, :address, :email, :joined_on, :membership_fee_paid_on]
   MEMBER_MANDATORY_FIELDS = [:name, :email]
 module NosOignons
   MEMBER_FIELDS = [:name, :address, :email, :joined_on, :membership_fee_paid_on]
   MEMBER_MANDATORY_FIELDS = [:name, :email]
@@ -78,9 +80,18 @@ module NosOignons
     def up_to_date?
       return false if !joined_on || !membership_fee_paid_on
 
     def up_to_date?
       return false if !joined_on || !membership_fee_paid_on
 
-      now = Time.now.to_date
+      today = Time.now.to_date
       expire_on = Time.new(membership_fee_paid_on.year + 1, joined_on.month, joined_on.day).to_date
       expire_on = Time.new(membership_fee_paid_on.year + 1, joined_on.month, joined_on.day).to_date
-      now <= expire_on
+      today <= expire_on
+    end
+
+    def remind(reminder)
+      reminder.send(self)
+      ReminderDb.instance.record(self)
+    end
+
+    def reminded_on
+      ReminderDb.instance.last_reminder(self)
     end
   end
 end
     end
   end
 end
diff --git a/lib/nos_oignons/reminder.rb b/lib/nos_oignons/reminder.rb
new file mode 100644 (file)
index 0000000..068a667
--- /dev/null
@@ -0,0 +1,87 @@
+#-*- coding: utf-8 -*-
+
+module NosOignons
+  class Reminder < Struct.new(:days, :subject, :template)
+    class << self
+      def all
+        return @all if @all
+
+        @all = []
+        @all << Reminder.new(
+          30,
+          '[Nos oignons] Renouvellement de votre cotisation',
+          <<-END_OF_ERB.gsub(/^            /, ''))
+            Salut <%= member.name %>,
+
+            La date anniversaire de votre adhésion à Nos oignons est dans <%= days %> jours.
+            Il est donc temps de renouveller votre cotisation si vous souhaitez rester
+            membre de l'association. Si vous avez besoin de plus d'informations sur
+            comment faire, écrivez au conseil d'administration. Répondre à cet email
+            devrait faire l'affaire.
+
+            Au passage, est-ce que vous pourriez vérifier que l'adresse postale est
+            toujours bonne ? Voici celle que nous avons retenu :
+
+            <%= member.address.gsub(/^/, '    ') %>
+
+            Si ce n'est plus le cas, c'est chouette de le signaler.
+
+            Au plaisir de continuer l'aventure de Nos oignons avec vous,
+            -- 
+            Le robot du conseil d'administration
+          END_OF_ERB
+        @all << Reminder.new(
+          10,
+          '[Nos oignons] Renouvellement de votre cotisation (rappel)',
+          <<-END_OF_ERB.gsub(/^            /, ''))
+            Salut <%= member.name %>,
+
+            La date anniversaire de ton adhésion à Nos oignons est dans <%= days %> jours.
+            Si vous ne renouvellez pas votre cotisation d'ici là, vous perdrez la qualité
+            de membre de Nos oignons.
+
+            Pour plus d'informations sur comment faire pour renouveller sa cotisation,
+            c'est possible d'écrire au conseil d'administration. Répondre à ce mail
+            devrait fonctionner.
+
+            À bientôt ?
+            -- 
+            Le robot du conseil d'administration
+          END_OF_ERB
+        @all << Reminder.new(
+          2,
+          '[Nos oignons] Renouvellement de votre cotisation (dernier rappel)',
+          <<-END_OF_ERB.gsub(/^            /, ''))
+            Salut <%= member.name %>,
+
+            Dans <%= days %> jours, c'est la date anniversaire de ton adhésion à Nos oignons.
+            Vu que vous n'avez pas renouvellé votre cotisation, cela signifie que vous
+            allez bientôt quitter l'association…
+
+            Mais il n'est pas encore trop tard pour le faire !
+
+            Des questions ? Mieux vaut les poser au conseil d'administration. En répondant à
+            cet email par exemple.
+
+            Et sinon… bonne route ! :)
+
+            Bien à vous,
+            -- 
+            Le robot du conseil d'administration
+          END_OF_ERB
+        @all
+      end
+    end
+
+    def send(member)
+      locals = { :member => member, :days => days }
+      body = ERB.new(template).result(OpenStruct.new(locals).instance_eval { binding })
+      mail = Mail.new :charset => 'utf-8',
+                      :from => NosOignons::BOARD_EMAIL,
+                      :to => member.email,
+                      :subject => subject,
+                      :body => body
+      mail.deliver
+    end
+  end
+end
diff --git a/lib/nos_oignons/reminder_db.rb b/lib/nos_oignons/reminder_db.rb
new file mode 100644 (file)
index 0000000..6052875
--- /dev/null
@@ -0,0 +1,52 @@
+#-*- coding: utf-8 -*-
+
+require 'singleton'
+require 'tempfile'
+
+module NosOignons
+  class ReminderDb
+    DEFAULT_PATH = File.expand_path('../../../var/reminders.yaml', __FILE__)
+
+    include Singleton
+
+    def db_path
+      ENV['NOS_OIGNONS_REMINDER_DB'] || DEFAULT_PATH
+    end
+
+    def initialize
+      reload!
+    end
+
+    def reload!
+      # hash of email => last_reminder_date
+      begin
+        @reminders = YAML.load_file(db_path)
+      rescue Errno::ENOENT
+        @reminders = {}
+      end
+    end
+
+    def save
+      # save using atomic rename
+      file = Tempfile.new('reminder_db', File.dirname(db_path))
+      begin
+        file.write(YAML.dump(@reminders))
+        file.close
+        File.rename file.path, db_path
+      rescue
+        file.unlink
+        raise
+      end
+    end
+
+    def last_reminder(member)
+      @reminders[member.email]
+    end
+
+    def record(member)
+      @reminders[member.email] = Time.now.to_date
+      # Not efficient, but let's put people's mailbox as first priority
+      save
+    end
+  end
+end
index a8584229fa0680a28679642bcc1378f6a45de5fb..83b7ea22a49b4a4186e8168695fafdcdfc2c0bba 100644 (file)
@@ -8,4 +8,5 @@ Gem::Specification.new do |s|
   s.add_development_dependency 'json', '~> 1.7.7'
   s.add_development_dependency 'timecop'
   s.add_runtime_dependency 'safe_yaml'
   s.add_development_dependency 'json', '~> 1.7.7'
   s.add_development_dependency 'timecop'
   s.add_runtime_dependency 'safe_yaml'
+  s.add_runtime_dependency 'mail'
 end
 end
diff --git a/var/.placeholder b/var/.placeholder
new file mode 100644 (file)
index 0000000..e69de29