From: ned Date: Thu, 14 Dec 2023 11:05:28 +0000 (+0100) Subject: résolution conflit X-Git-Url: https://nos-oignons.net/gitweb/gestion-adh.git/commitdiff_plain/ca6d8508a37e5540877066876a2714a6d0afe799?hp=b9f2243b2fe5fab0851cd58092630179556e7910 résolution conflit --- diff --git a/Gemfile b/Gemfile index 4237577..72db4c4 100644 --- a/Gemfile +++ b/Gemfile @@ -19,3 +19,5 @@ source 'https://rubygems.org' gemspec + +gem "matrix", "~> 0.4.2" diff --git a/Gemfile.lock b/Gemfile.lock index b9bb2ac..742e6fa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 71c969f..123a38a 100644 --- 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` : diff --git a/features/list-emails.feature b/features/list-emails.feature index ebcf76f..c5615db 100644 --- a/features/list-emails.feature +++ b/features/list-emails.feature @@ -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 | diff --git a/features/send-membership-reminders.feature b/features/send-membership-reminders.feature index 47cabf5..9526dd1 100644 --- a/features/send-membership-reminders.feature +++ b/features/send-membership-reminders.feature @@ -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 diff --git a/features/step_definitions/commands.rb b/features/step_definitions/commands.rb index 622a44b..d4d7ab5 100644 --- a/features/step_definitions/commands.rb +++ b/features/step_definitions/commands.rb @@ -17,7 +17,7 @@ # along with this program. If not, see . 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 diff --git a/features/step_definitions/git.rb b/features/step_definitions/git.rb index 3c0e145..eef206e 100644 --- a/features/step_definitions/git.rb +++ b/features/step_definitions/git.rb @@ -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 diff --git a/features/step_definitions/mailman.rb b/features/step_definitions/mailman.rb index e201bab..c3c36cf 100644 --- a/features/step_definitions/mailman.rb +++ b/features/step_definitions/mailman.rb @@ -16,36 +16,61 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +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 diff --git a/features/step_definitions/members.rb b/features/step_definitions/members.rb index d3d9b7c..0cde0b5 100644 --- a/features/step_definitions/members.rb +++ b/features/step_definitions/members.rb @@ -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 diff --git a/features/support/env.rb b/features/support/env.rb index ae8c5c3..4893603 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -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 diff --git a/features/support/fixtures.rb b/features/support/fixtures.rb index 3c74f4b..723f351 100644 --- a/features/support/fixtures.rb +++ b/features/support/fixtures.rb @@ -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 index d6dd3ba..0000000 --- a/features/support/mock_mailman/add_members +++ /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 -# -# 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 . - -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 index 7550f78..0000000 --- a/features/support/mock_mailman/list_members +++ /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 -# -# 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 . - -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 index 063d7b3..0000000 --- a/features/support/mock_mailman/remove_members +++ /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 -# -# 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 . - -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 index 8fcbc72..0000000 --- a/features/support/mock_mailman/sudo +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/sh -# -# Système de gestion des adhésions de Nos oignons -# Copyright © 2013-2014 Nos oignons -# -# 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 . - -if ! [ "-u" = "$1" ] && ! [ "list" = "$2" ]; then - echo "Bad call" >&2 - exit 1 -fi - -shift 2 - -exec "$@" diff --git a/features/update-ag-subscribers.feature b/features/update-ag-subscribers.feature index 4ee9725..86f74c1 100644 --- a/features/update-ag-subscribers.feature +++ b/features/update-ag-subscribers.feature @@ -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" diff --git a/lib/nos_oignons.rb b/lib/nos_oignons.rb index b85ebea..ee6fe7e 100644 --- a/lib/nos_oignons.rb +++ b/lib/nos_oignons.rb @@ -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| diff --git a/lib/nos_oignons/mailman.rb b/lib/nos_oignons/mailman.rb index 352b968..007fd33 100644 --- a/lib/nos_oignons/mailman.rb +++ b/lib/nos_oignons/mailman.rb @@ -16,37 +16,62 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -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 diff --git a/lib/nos_oignons/member.rb b/lib/nos_oignons/member.rb index de079a3..0feb825 100644 --- a/lib/nos_oignons/member.rb +++ b/lib/nos_oignons/member.rb @@ -16,6 +16,7 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +require 'date' require 'safe_yaml' SafeYAML::OPTIONS[:default_mode] = :safe diff --git a/lib/nos_oignons/receipt.rb b/lib/nos_oignons/receipt.rb index c42333f..794ebf1 100644 --- a/lib/nos_oignons/receipt.rb +++ b/lib/nos_oignons/receipt.rb @@ -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 diff --git a/lib/nos_oignons/reminder.rb b/lib/nos_oignons/reminder.rb index dc85b77..bee6923 100644 --- a/lib/nos_oignons/reminder.rb +++ b/lib/nos_oignons/reminder.rb @@ -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, diff --git a/nos_oignons.gemspec b/nos_oignons.gemspec index c174de2..f8cc018 100644 --- a/nos_oignons.gemspec +++ b/nos_oignons.gemspec @@ -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