/.bundle
/vendor/bundle
+
+/var/reminders.yaml
remote: .
specs:
nos_oignons (0.0.1.dev)
+ mail
safe_yaml
GEM
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)
+ 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)
+ treetop (1.4.12)
+ polyglot
+ polyglot (>= 0.3.1)
PLATFORMS
ruby
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
=============
--- /dev/null
+#!/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!
--- /dev/null
+# 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
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
--- /dev/null
+#-*- 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
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)
- write_file file, render_member_file(data)
+ write_file file, render_member_file(@last_member)
end
Given /une base de membres vide$/ do
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 /^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)
require 'tmpdir'
require 'aruba/cucumber'
require 'safe_yaml'
+require 'mail'
SafeYAML::OPTIONS[:default_mode] = :safe
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_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
+ ENV['NOS_OIGNONS_REMINDER_DB'] = @orig_reminder_db_path
ENV['NOS_OIGNONS_BOARD_WIKI_PATH'] = @orig_wiki_path
FileUtils.remove_entry_secure @tmpdir
end
require 'nos_oignons/git'
require 'nos_oignons/mailman'
require 'nos_oignons/member'
+require 'nos_oignons/reminder'
+require 'nos_oignons/reminder_db'
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!
end
end
- MEMBER_MAILING_LIST = 'ag'
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)
- 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!
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]
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
- 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
--- /dev/null
+#-*- 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
--- /dev/null
+#-*- 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
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