]> nos-oignons.net Git - gestion-adh.git/commitdiff
résolution conflit
authorned <ned@zen>
Thu, 14 Dec 2023 11:05:28 +0000 (12:05 +0100)
committerned <ned@zen>
Thu, 14 Dec 2023 11:05:28 +0000 (12:05 +0100)
22 files changed:
Gemfile
Gemfile.lock
README
features/list-emails.feature
features/send-membership-reminders.feature
features/step_definitions/commands.rb
features/step_definitions/git.rb
features/step_definitions/mailman.rb
features/step_definitions/members.rb
features/support/env.rb
features/support/fixtures.rb
features/support/mock_mailman/add_members [deleted file]
features/support/mock_mailman/list_members [deleted file]
features/support/mock_mailman/remove_members [deleted file]
features/support/mock_mailman/sudo [deleted file]
features/update-ag-subscribers.feature
lib/nos_oignons.rb
lib/nos_oignons/mailman.rb
lib/nos_oignons/member.rb
lib/nos_oignons/receipt.rb
lib/nos_oignons/reminder.rb
nos_oignons.gemspec

diff --git a/Gemfile b/Gemfile
index 42375775cdb6e62e91cfbec4a6e2704f658898c6..72db4c4b02835d58d55dc479014b3d3fa0be2936 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -19,3 +19,5 @@
 source 'https://rubygems.org'
 
 gemspec
+
+gem "matrix", "~> 0.4.2"
index b9bb2acbf685afc030e4a1b8858c5aafed607933..742e6fa779406ab2f6e9d77ee290ecdda48db164 100644 (file)
@@ -4,56 +4,110 @@ PATH
     nos_oignons (0.0.1.dev)
       mail
       prawn
+      rest-client
       safe_yaml
 
 GEM
   remote: https://rubygems.org/
   specs:
-    aruba (0.14.2)
-      childprocess (~> 0.5.6)
-      contracts (~> 0.9)
-      cucumber (>= 1.3.19)
-      ffi (~> 1.9.10)
-      rspec-expectations (>= 2.99)
-      thor (~> 0.19)
-    builder (3.2.3)
-    childprocess (0.5.9)
-      ffi (~> 1.0, >= 1.0.11)
-    contracts (0.16.0)
-    cucumber (2.4.0)
-      builder (>= 2.1.2)
-      cucumber-core (~> 1.5.0)
-      cucumber-wire (~> 0.0.1)
-      diff-lcs (>= 1.1.3)
-      gherkin (~> 4.0)
-      multi_json (>= 1.7.5, < 2.0)
-      multi_test (>= 0.1.2)
-    cucumber-core (1.5.0)
-      gherkin (~> 4.0)
-    cucumber-wire (0.0.1)
-    diff-lcs (1.3)
-    ffi (1.9.18)
-    gherkin (4.1.3)
-    json (2.1.0)
-    mail (2.6.6)
-      mime-types (>= 1.16, < 4)
-    mime-types (3.1)
+    addressable (2.8.4)
+      public_suffix (>= 2.0.2, < 6.0)
+    aruba (2.1.0)
+      bundler (>= 1.17, < 3.0)
+      childprocess (>= 2.0, < 5.0)
+      contracts (>= 0.16.0, < 0.18.0)
+      cucumber (>= 4.0, < 9.0)
+      rspec-expectations (~> 3.4)
+      thor (~> 1.0)
+    builder (3.2.4)
+    childprocess (4.1.0)
+    contracts (0.16.1)
+    crack (0.4.5)
+      rexml
+    cucumber (8.0.0)
+      builder (~> 3.2, >= 3.2.4)
+      cucumber-ci-environment (~> 9.0, >= 9.0.4)
+      cucumber-core (~> 11.0, >= 11.0.0)
+      cucumber-cucumber-expressions (~> 15.1, >= 15.1.1)
+      cucumber-gherkin (~> 23.0, >= 23.0.1)
+      cucumber-html-formatter (~> 19.1, >= 19.1.0)
+      cucumber-messages (~> 18.0, >= 18.0.0)
+      diff-lcs (~> 1.5, >= 1.5.0)
+      mime-types (~> 3.4, >= 3.4.1)
+      multi_test (~> 1.1, >= 1.1.0)
+      sys-uname (~> 1.2, >= 1.2.2)
+    cucumber-ci-environment (9.1.0)
+    cucumber-core (11.0.0)
+      cucumber-gherkin (~> 23.0, >= 23.0.1)
+      cucumber-messages (~> 18.0, >= 18.0.0)
+      cucumber-tag-expressions (~> 4.1, >= 4.1.0)
+    cucumber-cucumber-expressions (15.2.0)
+    cucumber-gherkin (23.0.1)
+      cucumber-messages (~> 18.0, >= 18.0.0)
+    cucumber-html-formatter (19.2.0)
+      cucumber-messages (~> 18.0, >= 18.0.0)
+    cucumber-messages (18.0.0)
+    cucumber-tag-expressions (4.1.0)
+    date (3.3.3)
+    diff-lcs (1.5.0)
+    domain_name (0.5.20190701)
+      unf (>= 0.0.5, < 1.0.0)
+    ffi (1.15.5)
+    hashdiff (1.0.1)
+    http-accept (1.7.0)
+    http-cookie (1.0.5)
+      domain_name (~> 0.5)
+    json (2.6.2)
+    mail (2.8.1)
+      mini_mime (>= 0.1.1)
+      net-imap
+      net-pop
+      net-smtp
+    matrix (0.4.2)
+    mime-types (3.4.1)
       mime-types-data (~> 3.2015)
-    mime-types-data (3.2016.0521)
-    multi_json (1.12.2)
-    multi_test (0.1.2)
-    pdf-core (0.7.0)
-    prawn (2.2.2)
-      pdf-core (~> 0.7.0)
-      ttfunk (~> 1.5)
-    rspec-expectations (3.6.0)
+    mime-types-data (3.2022.0105)
+    mini_mime (1.1.2)
+    multi_test (1.1.0)
+    net-imap (0.3.4)
+      date
+      net-protocol
+    net-pop (0.1.2)
+      net-protocol
+    net-protocol (0.2.1)
+      timeout
+    net-smtp (0.3.3)
+      net-protocol
+    netrc (0.11.0)
+    pdf-core (0.9.0)
+    prawn (2.4.0)
+      pdf-core (~> 0.9.0)
+      ttfunk (~> 1.7)
+    public_suffix (5.0.1)
+    rest-client (2.1.0)
+      http-accept (>= 1.7.0, < 2.0)
+      http-cookie (>= 1.0.2, < 2.0)
+      mime-types (>= 1.16, < 4.0)
+      netrc (~> 0.8)
+    rexml (3.2.5)
+    rspec-expectations (3.11.0)
       diff-lcs (>= 1.2.0, < 2.0)
-      rspec-support (~> 3.6.0)
-    rspec-support (3.6.0)
-    safe_yaml (1.0.4)
-    thor (0.20.0)
-    timecop (0.9.1)
-    ttfunk (1.5.1)
+      rspec-support (~> 3.11.0)
+    rspec-support (3.11.0)
+    safe_yaml (1.0.5)
+    sys-uname (1.2.2)
+      ffi (~> 1.1)
+    thor (1.2.1)
+    timecop (0.9.5)
+    timeout (0.3.2)
+    ttfunk (1.7.0)
+    unf (0.1.4)
+      unf_ext
+    unf_ext (0.0.8.2)
+    webmock (3.18.1)
+      addressable (>= 2.8.0)
+      crack (>= 0.3.2)
+      hashdiff (>= 0.4.0, < 2.0.0)
 
 PLATFORMS
   ruby
@@ -62,8 +116,10 @@ DEPENDENCIES
   aruba
   cucumber
   json
+  matrix (~> 0.4.2)
   nos_oignons!
   timecop
+  webmock
 
 BUNDLED WITH
-   1.13.6
+   2.2.5
diff --git a/README b/README
index 71c969fd061d39aa0e09b1451ed7ba1ccdc9d667..123a38ad591bc0d6749230ce815fb0efcd8fec1b 100644 (file)
--- a/README
+++ b/README
@@ -100,8 +100,16 @@ le dépôt central du wiki du C.A. (via un lien symbolique dans
 
 Met à jour la liste des adresses email inscrites à la liste *ag@* par rapport
 aux membres à jour de cotisation. À exécuter à travers un *cron*. A besoin de
-pouvoir lancer les commandes `list_members`, `add_members` et `remove_members`
-via `sudo` sur le compte `list`.
+pouvoir se connecter à l’API REST de Mailman 3.
+
+La variable d’environnement suivante doit être
+définie `NOS_OIGNONS_MAILMAN_REST_API_URL` avec par exemple :
+`https://user:password@mailman.example.org`
+
+À défaut, cette dernière sera lue depuis le fichier `mailman-rest-url`
+dans le dossier indiqué par la variable d’environnemnet `CREDENTIALS_DIRECTORY`
+(voir [System and Service Credentials](https://systemd.io/CREDENTIALS/) et
+la procédure de déploiement indiqué plus bas).
 
 `send-membership-reminders`
 ---------------------------
@@ -176,24 +184,101 @@ Pour rendre facilement accessible `list-members-emails`, on peut ajouter dans
 
 Ne pas oublier de le rendre exécutable.
 
-Pour permettre au script `update-ag-subscribers` de fonctionner, il est
-nécessaire de l'autoriser à exécuter certaines commandes de Mailman.
-Pour cela, on va créer un fichier dans `/etc/sudoers.d` :
+On va utiliser des
+[services](https://www.freedesktop.org/software/systemd/man/systemd.service.html)
+et des [timers](https://www.freedesktop.org/software/systemd/man/systemd.timer.html)
+systemd pour les scripts à utiliser régulièrement.
+
+Dans `/etc/systemd/system/gestion-adh-update-ag-subscribers.service` :
+
+    [Unit]
+    Description=Update subscribers of the `ag` mailing list according to memberships
+
+    [Service]
+    Type=oneshot
+    WorkingDirectory=/srv/ikiwiki/wiki-ca/gestion-adh
+    User=wiki-ca
+    ExecStart=/srv/ikiwiki/wiki-ca/gestion-adh/bin/update-ag-subscribers
+    Environment=BUNDLE_GEMFILE=/srv/ikiwiki/wiki-ca/gestion-adh/Gemfile NOS_OIGNONS_BOARD_WIKI_PATH=/srv/ikiwiki/wiki-ca/src
+    SyslogIdentifier=update-ag-subscribers
+    ProtectSystem=strict
+    ProtectHome=true
+    PrivateTmp=yes
+    PrivateDevices=yes
+
+Dans `/etc/systemd/system/gestion-adh-update-ag-subscribers.timer` :
+
+    [Unit]
+    Description=Run update-ag-subscribers every hour
+
+    [Timer]
+    RandomizedDelaySec=30min
+    OnCalendar=hourly
+
+    [Install]
+    WantedBy=timers.target
+
+Il est également nécessaire de configurer le *credential* avec les informations
+de connexion à Mailman :
+
+    echo -n 'https://USER:SECRET_PASSWORD@localhost:8001' | sudo systemd-creds encrypt --name=mailman-rest-url -p - - | sudo tee -a /etc/systemd/system/gestion-adh-update-subscribers.service.d/overrides.conf
+
+Dans `/etc/systemd/system/gestion-adh-send-membership-reminders.service` :
+
+    [Unit]
+    Description=Send reminders to renew membership
+
+    [Service]
+    Type=oneshot
+    WorkingDirectory=/srv/ikiwiki/wiki-ca/gestion-adh
+    User=wiki-ca
+    ExecStart=/srv/ikiwiki/wiki-ca/gestion-adh/bin/send-membership-reminders
+    Environment=BUNDLE_GEMFILE=/srv/ikiwiki/wiki-ca/gestion-adh/Gemfile NOS_OIGNONS_BOARD_WIKI_PATH=/srv/ikiwiki/wiki-ca/src
+    SyslogIdentifier=send-membership-reminders
+    ProtectSystem=strict
+    ProtectHome=true
+    PrivateTmp=yes
+    PrivateDevices=yes
+    ReadWritePaths=/srv/ikiwiki/wiki-ca/gestion-adh/var
+
+Dans `/etc/systemd/system/gestion-adh-send-membership-reminders.timer` :
+
+    [Unit]
+    Description=Run send-membership-reminders every day
+
+    [Timer]
+    OnCalendar=06:42
+
+    [Install]
+    WantedBy=timers.target
+
+Dans `/etc/systemd/system/gestion-adh-send-member-emails-to-advisors.service` :
+
+    [Unit]
+    Description=Send member emails to the advisory board
 
-    Defaults:wiki-ca !requiretty
+    [Service]
+    Type=oneshot
+    WorkingDirectory=/srv/ikiwiki/wiki-ca/gestion-adh
+    User=wiki-ca
+    ExecStart=/srv/ikiwiki/wiki-ca/gestion-adh/bin/send-member-emails-to-advisors
+    Environment=BUNDLE_GEMFILE=/srv/ikiwiki/wiki-ca/gestion-adh/Gemfile NOS_OIGNONS_BOARD_WIKI_PATH=/srv/ikiwiki/wiki-ca/src
+    SyslogIdentifier=send-member-emails-to-advisors
+    ProtectSystem=strict
+    ProtectHome=true
+    PrivateTmp=yes
+    PrivateDevices=yes
 
-    Cmnd_Alias AG_MANAGEMENT = /usr/sbin/list_members ag,\
-                               /usr/sbin/add_members -r - ag,\
-                               /usr/sbin/remove_members -f - ag
+Dans `/etc/systemd/system/gestion-adh-send-member-emails-to-advisors.timer` :
 
-    wiki-ca ALL = (list) NOPASSWD: AG_MANAGEMENT
+    [Unit]
+    Description=Run send-member-emails-to-advisors monthly
 
-Ensuite, pour exécuter régulièrement les scripts via le *crontab* du compte
-`wiki-ca`, il faut y ajouter :
+    [Timer]
+    OnCalendar=monthly
 
-    42 * * * * BUNDLE_GEMFILE=/srv/ikiwiki/wiki-ca/gestion-adh/Gemfile NOS_OIGNONS_BOARD_WIKI_PATH=/srv/ikiwiki/wiki-ca/src /srv/ikiwiki/wiki-ca/gestion-adh/bin/update-ag-subscribers
-    42 6 * * * BUNDLE_GEMFILE=/srv/ikiwiki/wiki-ca/gestion-adh/Gemfile NOS_OIGNONS_BOARD_WIKI_PATH=/srv/ikiwiki/wiki-ca/src /srv/ikiwiki/wiki-ca/gestion-adh/bin/send-membership-reminders
-    21 0 1 * * BUNDLE_GEMFILE=/srv/ikiwiki/wiki-ca/gestion-adh/Gemfile NOS_OIGNONS_BOARD_WIKI_PATH=/srv/ikiwiki/wiki-ca/src /srv/ikiwiki/wiki-ca/gestion-adh/bin/send-member-emails-to-advisors
+    [Install]
+    WantedBy=timers.target
 
 Pour installer le `pre-commit` *hook* sur le dépôt utilisé par
 Ikiwiki, on met dans `/srv/ikiwiki/wiki-ca/src/.git/hooks/pre-commit` :
index ebcf76f514dc4c72f87f1d5eb29a0d205c1291e5..c5615db00f7776ec54370d2812a712bb89494640 100644 (file)
@@ -76,3 +76,4 @@ Fonctionnalité: obtenir les emails des membres à jour de cotisations
       | 2012-12-15 | 2013-12-01 | 2014-01-01 | pierre@example.org |
       | 2012-12-15 | 2013-12-01 | 2014-12-16 |                    |
       | 2012-12-15 | 2013-12-01 | 2014-12-31 |                    |
+      | 2015-08-21 | 2021-08-11 | 2021-08-29 | pierre@example.org |
index 47cabf5226371a642be5034444f32c3796293ceb..9526dd1f7c87a2cad364dcc609f5fa7076cf1fb5 100644 (file)
@@ -46,6 +46,16 @@ Fonctionnalité: prévenir les membres qu'il faut renouveler leur cotisation
     Lorsque j'exécute send-membership-reminders le 2014-04-12
     Alors aucun email ne doit avoir été envoyé
 
+  Scénario: Pas de message si la cotisation a été renouvellée bien avant la date anniversaire
+    Soit une base avec jvoisin qui a adhéré le 2017-10-23 et payé sa dernière cotisation le 2019-06-19
+    Lorsque j'exécute send-membership-reminders le 2019-09-23
+    Alors aucun email ne doit avoir été envoyé
+
+  Scénario: Appel un mois avant si la cotisation a été payée bien avant la date anniversaire
+    Soit une base avec jvoisin qui a adhéré le 2017-10-23 et payé sa dernière cotisation le 2019-06-19
+    Lorsque j'exécute send-membership-reminders le 2020-09-23
+    Alors 1 email doit avoir été envoyé
+
   Scénario: Script pas exécuté tous les jours
     Soit une base avec Jane qui doit renouveler sa cotisation d'ici 9 jours
     Et qui a déjà reçu un appel 21 jours plus tôt
@@ -75,6 +85,29 @@ Fonctionnalité: prévenir les membres qu'il faut renouveler leur cotisation
     Lorsque j'exécute send-membership-reminders
     Alors un dernier rappel pour la cotisation doit avoir été envoyé
 
+  Scénario: Appel un mois avant si l’anniversaire est en décembre
+    Soit une base avec Fred qui a adhérée le 2017-12-05 et payé sa dernière cotisation le 2018-12-05
+    Lorsque j'exécute send-membership-reminders le 2019-11-05
+    Alors 1 email doit avoir été envoyé
+
+  Scénario: Appel 30 jours avant si l’anniversaire est en janvier
+    Soit une base avec Bruno qui a adhéré le 2018-01-05 et payé sa dernière cotisation le 2019-01-05
+    Lorsque j'exécute send-membership-reminders le 2019-12-06
+    Alors 1 email doit avoir été envoyé
+
+  Scénario: Premier rappel 10 jours avant si l’anniversaire est en janvier
+    Soit une base avec Bruno qui a adhéré le 2018-01-05 et payé sa dernière cotisation le 2019-01-05
+    Et qui a déjà reçu un appel le 2019-12-06
+    Lorsque j'exécute send-membership-reminders le 2019-12-26
+    Alors 1 email doit avoir été envoyé
+
+  Scénario: Deuxième rappel 2 jours avant si l’anniversaire est au 1er janvier
+    Soit une base avec Bruno qui a adhéré le 2018-01-01 et payé sa dernière cotisation le 2019-01-01
+    Et qui a déjà reçu un appel le 2019-12-02
+    Et qui a déjà reçu un appel le 2019-12-22
+    Lorsque j'exécute send-membership-reminders le 2019-12-30
+    Alors 1 email doit avoir été envoyé
+
   Scénario: Plusieurs messages
     Soit une base avec Pierre, à jour de cotisation
     Et avec Jane qui doit renouveler sa cotisation d'ici 10 jours
index 622a44b235ab7e56ab10812a080623f078c73f07..d4d7ab569a12ac430be80d0d49869e0b38d6c9a4 100644 (file)
@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 When /^j'exécute list\-emails$/ do
-  run_simple 'list-emails'
+  run_command_and_stop 'list-emails'
 end
 
 When /^j'exécute list-emails le (\d+)\-(\d+)\-(\d+)$/ do |year, month, day|$/
@@ -58,7 +58,7 @@ When /^j'exécute `create\-membership\-fee\-receipt ([0-9]+) ([0-9,]+)` le ([0-9
 end
 
 When /^j'exécute `(create\-membership\-fee\-receipt.*)`$/ do |cmd|
-  run_simple cmd, :fail_on_error => false
+  run_command_and_stop cmd, :fail_on_error => false
 end
 
 Then /^je ne dois pas avoir eu d'erreur$/ do
index 3c0e145c593177bfdac62ceba4c546e467e1e4ad..eef206e08dc9ec1459b801498968a4574f448686 100644 (file)
@@ -23,20 +23,20 @@ Given /^un clone du Git contenant les adhésions$/ do
   @main_repository_path = expand_path('main')
   create_directory 'main'
   cd 'main'
-  run_simple 'git init --quiet --bare'
+  run_command_and_stop 'git init --quiet --bare'
   cd '..'
 
   # Clone it now
-  run_simple "git clone --quiet --local file://#{expand_path('.')}/main clone"
+  run_command_and_stop "git clone --quiet --local file://#{expand_path('.')}/main clone"
   cd 'clone'
   create_directory 'Membres'
   BASE_MEMBERS.each_pair do |number, data|
     file = member_filename_for_id(number)
     File.write file, render_member_file(data)
-    run_simple "git add #{file}"
+    run_command_and_stop "git add #{file}"
   end
-  run_simple 'git commit -m "Initial data set from fixtures"'
-  run_simple 'git push --quiet origin master'
+  run_command_and_stop 'git commit -m "Initial data set from fixtures"'
+  run_command_and_stop 'git push --quiet origin master'
 end
 
 Given /^le « pre-commit hook » correctement configuré$/ do
@@ -50,12 +50,12 @@ Given /^le « pre-receive hook » configuré sur le dépôt principal$/ do
 end
 
 When /je fais un `commit` du nouveau fichier$/ do
-  run_simple "git add #{@file}"
-  run_simple "git commit #{@file} -m 'new file'", false # do not fail on error
+  run_command_and_stop "git add #{@file}"
+  run_command_and_stop "git commit #{@file} -m 'new file'", :fail_on_error => false # do not fail on error
 end
 
 When /je pousse la modification$/ do
-  run_simple "git add #{@file}"
-  run_simple "git commit #{@file} -m 'new file'"
-  run_simple 'git push origin master', false # do not fail on error
+  run_command_and_stop "git add #{@file}"
+  run_command_and_stop "git commit #{@file} -m 'new file'"
+  run_command_and_stop 'git push origin master', :fail_on_error => false # do not fail on error
 end
index e201bab96bc9591f8a6f306db9a242846387702a..c3c36cf3ba59be95bc0047f7b5e1a3fb8bf87e04 100644 (file)
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+require 'webmock'
 require 'json'
+require 'digest'
 
 Before('@mailman') do
-  @mock_mailman_db = expand_path('mock_mailman.json')
-  init_mailman_mock_db({})
-  ENV['MOCK_MAILMAN_DB'] = @mock_mailman_db
-  @orig_path = ENV['PATH']
-  ENV['PATH'] = "#{File.expand_path('../../support/mock_mailman', __FILE__)}:#{@orig_path}"
+  @ag_emails = []
+  stub_request(:any, /mailman.example.org/).
+    with(basic_auth: ['mailman_rest_user', 'mailman_rest_pass'])
+  stub_request(:get, 'https://mailman.example.org/3.0/lists/ag@nos-oignons.net/roster/member').
+    with(basic_auth: ['mailman_rest_user', 'mailman_rest_pass']).
+    to_return { |request| {body: roster_member_payload } }
 end
 
-After('@mailman') do
-  ENV['PATH'] = @orig_path
-  FileUtils.remove_entry_secure @mock_mailman_db
+def member_id_for_email(email)
+  Digest::SHA1.hexdigest(email)
 end
 
-def init_mailman_mock_db(dict)
-  File.open(@mock_mailman_db, 'w') { |f| f.write(JSON.dump(dict)) }
+def roster_member_payload
+  # This is an approximation of the actual payload from Mailman, but good enough for our needs
+  payload = {
+    "total_size": @ag_emails.count,
+    "start": 0,
+    "entries": @ag_emails.map { |email| { "email": email, "member_id": member_id_for_email(email) } },
+  }
+  payload.to_json
 end
 
-def mailman_mock_db
-  JSON.load(File.open(@mock_mailman_db))
+Given /^une liste ag@ avec comme emails inscrits:$/ do |subscriber_list|
+  @ag_emails = subscriber_list.strip.split
 end
 
-Given /^une liste ag@ avec comme emails inscrits:$/ do |subscriber_list|
-  emails = subscriber_list.strip.split
-  init_mailman_mock_db('ag' => emails)
+Then 'la liste ag@ doit avoir reçu l’inscription de {string}' do |email|
+  expect(
+    a_request(:post, "https://mailman.example.org/3.0/members").
+      with(headers: {'Content-Type': 'application/json'},
+           body: hash_including(
+             {"list_id": "ag.nos-oignons.net",
+              "subscriber": email,
+              "pre_verified": true,
+              "pre_confirmed": true,
+              "pre_approved": true,
+             }
+           ),
+          )).to have_been_made
+end
+
+Then 'la liste ag@ ne doit pas avoir reçu d’inscription' do
+  expect(
+    a_request(:post, "https://mailman.example.org/3.0/members")
+  ).not_to have_been_made
 end
 
-Then /^la liste ag@ doit avoir comme emails inscrits:$/ do |expected|
-  emails = expected.strip.split.sort
-  expect(mailman_mock_db['ag'].sort).to eql(emails)
+Then 'la liste ag@ doit avoir reçu la désinscription de {string}' do |email|
+  expect(
+    a_request(:delete, "https://mailman.example.org/3.0/members/#{member_id_for_email(email)}")
+  ).to have_been_made
 end
 
index d3d9b7c5864c1a7966280c8b655bc8047946e4dd..0cde0b509f52f633c9485fe62f037e88320f313a 100644 (file)
@@ -59,7 +59,7 @@ Given /^(?:une base )?avec (\w+)(, à jour de cotisation| qui n'a pas payé sa c
   create_new_member(name, joined_on, paid_on)
 end
 
-Given /^une base avec (\w+) qui a adhéré le ([0-9-]+) et payé sa dernière cotisation le ([0-9-]+)$/ do |name, joined_on, paid_on|
+Given /^une base avec (\w+) qui a adhérée? le ([0-9-]+) et payé sa dernière cotisation le ([0-9-]+)$/ do |name, joined_on, paid_on|
   create_new_member(name, joined_on, paid_on)
 end
 
@@ -118,6 +118,12 @@ Given /^qui a déjà reçu un appel (\d+) jours plus tôt$/ do |days_ago|
   end
 end
 
+Given /^qui a déjà reçu un appel le ([0-9-]+)$/ do |date|
+  Timecop.travel(Time.parse(date)) 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)
   File.write @file, render_member_file(EXTRA_MEMBER)
@@ -204,7 +210,7 @@ end
 
 When /^je supprime le nom sur une fiche existante$/ do
   @file = member_filename_for_id(1)
-  run_simple "sed -e '/^name:/d' -i #{@file}"
+  run_command_and_stop "sed -e '/^name:/d' -i #{@file}"
 end
 
 When /^j'ajoute un fichier hors de la base des adhérents$/ do
index ae8c5c3a1fe9a49ae58437715edf06db6631f5b2..4893603c31f28098e12196fa3a0648cae3977a88 100644 (file)
@@ -45,6 +45,7 @@ Before do
   NosOignons::ReminderDb.instance.reload!
   ENV['NOS_OIGNONS_RECEIPTS_DIR'] = expand_path('receipts')
   FileUtils.mkdir(ENV['NOS_OIGNONS_RECEIPTS_DIR'])
+  ENV['NOS_OIGNONS_MAILMAN_REST_API_URL'] = 'https://mailman_rest_user:mailman_rest_pass@mailman.example.org'
   Mail.defaults do
     delivery_method :test
   end
index 3c74f4b7e9f409e87aed063aac083c89d5128e5a..723f3516f06366d42a6a552801638521c9b72aba 100644 (file)
@@ -86,7 +86,7 @@ def member_filename_for_id(id)
 end
 
 def render_member_file(locals)
-  ERB.new(MEMBER_FILE_TEMPLATE, nil, '-').result(OpenStruct.new(locals).instance_eval { binding })
+  ERB.new(MEMBER_FILE_TEMPLATE, trim_mode: '-').result(OpenStruct.new(locals).instance_eval { binding })
 end
 
 def new_id
diff --git a/features/support/mock_mailman/add_members b/features/support/mock_mailman/add_members
deleted file mode 100755 (executable)
index d6dd3ba..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/ruby
-#-*- coding: utf-8 -*-
-#
-# Système de gestion des adhésions de Nos oignons
-# Copyright © 2013-2014 Nos oignons <contact@nos-oignons.net>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-require 'json'
-
-db = ENV['MOCK_MAILMAN_DB']
-
-lists = JSON.load(File.read(db))
-if ARGV[0] != '-r'
-  $stderr.puts "Bad call"
-  exit 1
-else
-  emails = (ARGV[1] == '-' ? $stdin : File.open(ARGV[1])).read.split
-  lists[ARGV[2]] = (lists[ARGV[2]] || []) + emails
-end
-File.open(db, 'w') { |f| f.write(JSON.dump(lists)) }
diff --git a/features/support/mock_mailman/list_members b/features/support/mock_mailman/list_members
deleted file mode 100755 (executable)
index 7550f78..0000000
+++ /dev/null
@@ -1,27 +0,0 @@
-#!/usr/bin/ruby
-#-*- coding: utf-8 -*-
-#
-# Système de gestion des adhésions de Nos oignons
-# Copyright © 2013-2014 Nos oignons <contact@nos-oignons.net>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-require 'json'
-
-db = ENV['MOCK_MAILMAN_DB']
-
-lists = JSON.load(File.read(db))
-(lists[ARGV[0]] || []).each do |email|
-  puts email
-end
diff --git a/features/support/mock_mailman/remove_members b/features/support/mock_mailman/remove_members
deleted file mode 100755 (executable)
index 063d7b3..0000000
+++ /dev/null
@@ -1,32 +0,0 @@
-#!/usr/bin/ruby
-#-*- coding: utf-8 -*-
-#
-# Système de gestion des adhésions de Nos oignons
-# Copyright © 2013-2014 Nos oignons <contact@nos-oignons.net>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-require 'json'
-
-db = ENV['MOCK_MAILMAN_DB']
-
-lists = JSON.load(File.read(db))
-if ARGV[0] != '-f'
-  $stderr.puts "Bad call"
-  exit 1
-else
-  emails = (ARGV[1] == '-' ? $stdin : File.open(ARGV[1])).read.split
-  lists[ARGV[2]] = (lists[ARGV[2]] || []) - emails
-end
-File.open(db, 'w') { |f| f.write(JSON.dump(lists)) }
diff --git a/features/support/mock_mailman/sudo b/features/support/mock_mailman/sudo
deleted file mode 100755 (executable)
index 8fcbc72..0000000
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/bin/sh
-#
-# Système de gestion des adhésions de Nos oignons
-# Copyright © 2013-2014 Nos oignons <contact@nos-oignons.net>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as
-# published by the Free Software Foundation, either version 3 of the
-# License, or (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-if ! [ "-u" = "$1" ] && ! [ "list" = "$2" ]; then
-       echo "Bad call" >&2
-       exit 1
-fi
-
-shift 2
-
-exec "$@"
index 4ee9725232d2145d032ef8e22db0b191cd0a7cd3..86f74c103bdab9bbb1cc167647ee2af3d8506ac9 100644 (file)
@@ -17,12 +17,7 @@ Fonctionnalité: mettre à jour les emails inscrites à la liste ag@
     Et avec Jane, à jour de cotisation
     Et avec Fatima, à jour de cotisation
     Lorsque j'exécute update-ag-subscribers
-    Alors la liste ag@ doit avoir comme emails inscrits:
-      """
-      pierre@example.org
-      jane@example.org
-      fatima@example.org
-      """
+    Alors la liste ag@ ne doit pas avoir reçu d’inscription
 
   Scénario: Nouvelle adhésion
     Soit une liste ag@ avec comme emails inscrits:
@@ -36,13 +31,7 @@ Fonctionnalité: mettre à jour les emails inscrites à la liste ag@
     Et avec Fatima, à jour de cotisation
     Et une nouvelle adhésion de Sean
     Lorsque j'exécute update-ag-subscribers
-    Alors la liste ag@ doit avoir comme emails inscrits:
-      """
-      pierre@example.org
-      jane@example.org
-      fatima@example.org
-      sean@example.org
-      """
+    Alors la liste ag@ doit avoir reçu l’inscription de "sean@example.org"
 
   Scénario: Non renouvellement de la cotisation
     Soit une liste ag@ avec comme emails inscrits:
@@ -55,11 +44,7 @@ Fonctionnalité: mettre à jour les emails inscrites à la liste ag@
     Et avec Jane qui n'a pas payé sa cotisation cette année
     Et avec Fatima, à jour de cotisation
     Lorsque j'exécute update-ag-subscribers
-    Alors la liste ag@ doit avoir comme emails inscrits:
-      """
-      pierre@example.org
-      fatima@example.org
-      """
+    Alors la liste ag@ doit avoir reçu la désinscription de "jane@example.org"
 
   Scénario: Un ajout et une suppression
     Soit une liste ag@ avec comme emails inscrits:
@@ -71,8 +56,5 @@ Fonctionnalité: mettre à jour les emails inscrites à la liste ag@
     Et avec Jane qui n'a pas payé sa cotisation cette année
     Et une nouvelle adhésion de Sean
     Lorsque j'exécute update-ag-subscribers
-    Alors la liste ag@ doit avoir comme emails inscrits:
-      """
-      pierre@example.org
-      sean@example.org
-      """
+    Alors la liste ag@ doit avoir reçu l’inscription de "sean@example.org"
+    Et la liste ag@ doit avoir reçu la désinscription de "jane@example.org"
index b85ebeaec4d255c2cc5ff3426726f2cca32d3bbe..ee6fe7ebd2fc0a9ae8af14d6021f40f7c345f0bf 100644 (file)
@@ -25,19 +25,18 @@ require 'nos_oignons/reminder_db'
 module NosOignons
   BOARD_EMAIL = 'ca@nos-oignons.net'
   ADVISORS_EMAIL = 'deontologie@nos-oignons.net'
-  MEMBER_MAILING_LIST = 'ag'
+  MEMBER_MAILING_LIST = 'ag@nos-oignons.net'
   CONTACT_INFO = <<-EOT.gsub(/^    /, '')
     Identifiant SIREN 842 479 313
     https://nos-oignons.net/
     contact@nos-oignons.net
     Téléphone : +33 9 72 42 96 04
-    Fax : +33 9 72 42 96 06
   EOT
   POSTAL_ADDRESS = <<-EOT.gsub(/^    /, '')
     Nos oignons
     Centre UBIDOCA, 7585
-    105 route des Pommiers
-    74370 Saint Martin Bellevue
+    78 allée Primavera
+    74370 Annecy
     France
   EOT
 
@@ -59,20 +58,29 @@ module NosOignons
     def update_ag_subscribers!
       list = NosOignons::MEMBER_MAILING_LIST
 
-      current_emails = NosOignons::Mailman.list_members(list)
+      current_members = NosOignons::Mailman.list_members(list)
       uptodate_emails = NosOignons::Member.all.select(&:up_to_date?).collect(&:email)
 
-      emails_to_add = uptodate_emails - current_emails
-      NosOignons::Mailman.add_members(list, emails_to_add) unless emails_to_add.empty?
-      emails_to_remove = current_emails - uptodate_emails
-      NosOignons::Mailman.remove_members(list, emails_to_remove) unless emails_to_remove.empty?
+      # Who is not subscribed yet?
+      emails_to_add = uptodate_emails - current_members.collect(&:email)
+      emails_to_add.each do |email|
+        NosOignons::Mailman.susbcribe_email(list, email)
+      end
+
+      # Who should not be subscribed anymore?
+      current_members.each do |list_member|
+        unless uptodate_emails.include?(list_member.email)
+          list_member.unsubscribe!
+        end
+      end
     end
 
     def send_membership_reminders!
       today = Time.now.to_date
+      reminders = NosOignons::Reminder.all.sort_by(&:days)
       NosOignons::Member.all.select(&:up_to_date?).each do |member|
-        reminders = NosOignons::Reminder.all.sort_by(&:days)
-        anniversary = Time.new(today.year, member.joined_on.month,
+        next if member.membership_fee_paid_on.year == (today + reminders.last.days).year
+        anniversary = Time.new(today.next_month.year, member.joined_on.month,
                                member.joined_on.day).to_date
         next if member.membership_fee_paid_on >= anniversary - reminders.last.days
         reminders.each do |reminder|
index 352b9689bbf258071b386866280d3afb945d05e9..007fd337183bb581c6508b467b4629d03581df39 100644 (file)
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-require 'shellwords'
+require 'json'
+require 'rest-client'
 
 module NosOignons
   module Mailman
+    class MailmanMember < Struct.new(:member_id, :email, keyword_init: true)
+      def unsubscribe!
+        Mailman::request(:delete, "/3.0/members/#{URI::encode_www_form_component(member_id)}")
+      end
+    end
+
     class << self
       def list_members(list)
-        `sudo -u list list_members #{Shellwords.escape(list)}`.strip.split
+        s = request(:get, "/3.0/lists/#{URI::encode_www_form_component(list)}/roster/member")
+        r = JSON.parse(s)
+        r["entries"].collect { |entry|
+            MailmanMember.new(member_id: entry["member_id"], email: entry["email"])
+          }
       end
 
-      def add_member(list, email)
-        add_members(list, [email])
+      def susbcribe_email(list, email)
+        list_id = list.gsub(/@/, '.')
+        request(:post, "/3.0/members", payload: {
+              list_id: list_id,
+              subscriber: email,
+              pre_verified: true,
+              pre_confirmed: true,
+              pre_approved: true,
+            }.to_json,
+            headers: { content_type: :json },
+          )
       end
 
-      def add_members(list, emails)
-        IO.popen(['sudo', '-u', 'list', 'add_members', '-r', '-', list], 'w') do |io|
-          emails.each do |email|
-            io.puts email
-          end
-        end
+      def request(method, path, **args)
+        RestClient::Request.execute(
+          **args,
+          method: method,
+          url: "#{rest_url}#{path}"
+        )
       end
 
-      def remove_member(list, email)
-        remove_members(list, [email])
-      end
+      private
+
+      def rest_url
+        @rest_url if defined?(@rest_url)
 
-      def remove_members(list, emails)
-        IO.popen(['sudo', '-u', 'list', 'remove_members', '-f', '-', list], 'w') do |io|
-          emails.each do |email|
-            io.puts email
+        @rest_url = ENV["NOS_OIGNONS_MAILMAN_REST_API_URL"]
+        if @rest_url.nil?
+          cred_path = File.join("#{ENV["CREDENTIALS_DIRECTORY"]}", "mailman-rest-url")
+          begin
+            @rest_url = File.open(cred_path).read
+          rescue Errno::ENOENT
+            raise ArgumentError.new("Environment variable `NOS_OIGNONS_MAILMAN_REST_API_URL` not defined, and unable to read the file `$CREDENTIALS_DIRECTORY/mailman-rest-url` either")
           end
         end
+        @rest_url
       end
     end
   end
index de079a34c12a6c842463fd524e825d949abe82e5..0feb8255759924d028cd7ee9cb4e7038bf00c623 100644 (file)
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU Affero General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
+require 'date'
 require 'safe_yaml'
 SafeYAML::OPTIONS[:default_mode] = :safe
 
index c42333fa6d32556a9f32b741225434e89578ae2f..794ebf12044ab7a0aa08e9c3d3431bcba60817ce 100644 (file)
@@ -57,7 +57,7 @@ module NosOignons
           pdf.image LOGO_PATH, :width => pdf.bounds.width, :align => :center, :vposition => :center
         end
         pdf.bounding_box([logo_width, pdf.bounds.top], :width => pdf.bounds.width / 3, :height => pdf.bounds.height) do
-          pdf.text 'Nœuds de sortie Tor financés par la communauté', :align => :center, :valign => :center
+          pdf.text "Nœuds de sortie Tor financés par la communauté\n\nIdentifiant SIREN 842 479 313", :align => :center, :valign => :center
         end
       end
       pdf.bounding_box([WINDOW_LEFT - pdf.bounds.absolute_left, pdf.bounds.absolute_top - WINDOW_TOP], :width => WINDOW_WIDTH, :height => WINDOW_BOTTOM - WINDOW_TOP) do
index dc85b77c0dfd53e22f9b1f8407b66be99efba681..bee6923464021e182dab48eb70476f0e0b793172 100644 (file)
@@ -44,14 +44,14 @@ module NosOignons
 
             <% unless member.address.nil? -%>
             Au passage, vous pouvez vérifier que l'adresse postale que vous nous
-            avez fournie est toujours bonne (vous n'êtes pas obligé de nous en fournir une), 
-            voici celle que nous avons :
+            avez fournie est toujours bonne. Vous n'êtes pas obligé·e de nous en
+            fournir une, mais voici celle que nous avons pour le moment :
 
             <%= member.address.gsub(/^/, '    ') %>
 
             <% end -%>
             
-           Au plaisir de continuer l'aventure de Nos oignons avec vous,
+            Au plaisir de continuer l'aventure de Nos oignons avec vous,
             -- 
             Le robot du conseil d'administration
 
@@ -86,13 +86,13 @@ module NosOignons
             Salut <%= member.name %>,
 
             Dans <%= days %> jours, c'est la date anniversaire de votre adhésion à
-            Nos oignons. Vu que vous n'avez pas renouvellé votre cotisation, cela signifie
-            que vous allez bientôt quitter l'association…
+            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.
+            Des questions ? Mieux vaut les poser au conseil d'administration. En
+            répondant à cet email par exemple.
 
             Et sinon… bonne route ! :)
 
@@ -109,7 +109,7 @@ module NosOignons
 
     def send(member)
       locals = { :member => member, :days => days }
-      body = ERB.new(template, nil, '-').result(OpenStruct.new(locals).instance_eval { binding })
+      body = ERB.new(template, trim_mode: '-').result(OpenStruct.new(locals).instance_eval { binding })
       mail = Mail.new :charset => 'utf-8',
                       :from => NosOignons::BOARD_EMAIL,
                       :to => member.email,
index c174de22a76b18f260cbbf577a793f76b33b212c..f8cc0184e393f21f20bc719db58d1c2db5dae615 100644 (file)
@@ -28,7 +28,9 @@ Gem::Specification.new do |s|
   s.add_development_dependency 'aruba'
   s.add_development_dependency 'json'
   s.add_development_dependency 'timecop'
+  s.add_development_dependency 'webmock'
   s.add_runtime_dependency 'safe_yaml'
   s.add_runtime_dependency 'mail'
   s.add_runtime_dependency 'prawn'
+  s.add_runtime_dependency 'rest-client'
 end