source 'https://rubygems.org'
gemspec
+
+gem "matrix", "~> 0.4.2"
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
aruba
cucumber
json
+ matrix (~> 0.4.2)
nos_oignons!
timecop
+ webmock
BUNDLED WITH
- 1.13.6
+ 2.2.5
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`
---------------------------
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` :
| 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 |
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
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
# 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|$/
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
@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
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
# 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
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
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)
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
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
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
+++ /dev/null
-#!/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)) }
+++ /dev/null
-#!/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
+++ /dev/null
-#!/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)) }
+++ /dev/null
-#!/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 "$@"
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:
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:
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:
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"
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
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|
# 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
# 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
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
<% 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
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 ! :)
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,
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