Commit 48812ff5 authored by jocelyn's avatar jocelyn Committed by Team adminsys FFDN

Merge branch 'jd-improve-maillists' of FFDN/coin into master

parents 0308746f b56b732b
<style>
figure img {
max-width: 30rem;
}
</style>
Gestion des abonnements aux listes mail
=======================================
......@@ -18,13 +12,10 @@ Il existe une interface membre pour auto-gérer ses propres abonnements, et une
interface admin pour gérer l'ensemble des abonnements lorsqu'on possède les
droits admin dans coin.
<figure>
![vue adhérent](../_img/user-maillists.png)
![vue admin](../_img/admin-maillists.png)
<figcaption>Vues adhérent·e et admin</figcaption>
</figure>
Vues adhérent·e et admin
Fonctionnement
......@@ -63,14 +54,8 @@ des inscriptions sont faites par d'autres moyens (mail, interface du serveur de
liste de discussions… etc), ces modifications risquent d'être écrasées par la
gestion d'abonnements de Coin.
- La commande de synchro est lancée à chaque abonnement/désabonnement. Si vous
inscrivez 100 membres d'un coup, ça pourrait être un peu long si ça passe
par SSH. Astuce quand vous initialisez vos listes pour la première fois donc :
1. mettre une commande de synchro bidon,
2. faire toutes vos inscriptions
3. mettre la vraie commande de synchro
4. lancer une synchro manuelle de chaque liste
- La commande de synchro est lancée à chaque abonnement/désabonnement. Il y a
un outil d'import « en masse » : [import_mailling_list](#méthode-b-importer-des-abonnements-en-masse).
Mise en place
-------------
......@@ -118,8 +103,23 @@ envisageables en recourant à un petit script sur mesure.
### 3. Ajouter des listes
Deux méthodes, selon que vous voulez initialiser la liste avec une vide ou
pré-remplie avec une liste d'abonnés.
#### Méthode A : créer une liste vide
Se rendre dans l'admin de coin et dans la nouvelle catégorie « Listes mail »,
renseigner les listes mail que l'on souhaite voir gérées par Coin.
#### Méthode B : importer des abonnements « en masse »
Pour créer une liste et faire un import initial de tou·te·s ses abonné·e·s d'un
coup, vous pouvez utiliser la commande `./manage.py import_mailling_list` qui
permet de créer une liste à partir de son adresse, son nom et d'un fichier
texte contenant les adresses à abonner (qui doivent correspondre à des membres
renseignés dans coin).
Pour plus d'infos : `./manage.py import_mailling_list --help`
*NB : Il vous faudra ensuite aller renseigner, via l'interface d'admin de coin,
la description complète de la liste (celle que verront les membres).*
......@@ -44,9 +44,14 @@ class MaillingListSubscriptionInline(admin.TabularInline):
class SubscribersInline(MaillingListSubscriptionInline):
fields = ('member', 'email', 'maillinglist',)
readonly_fields = ('member', 'email', 'maillinglist',)
verbose_name_plural = "Abonné·e·s"
verbose_name = "abonné·e"
def email(self, instance):
return instance.member.email
class MaillingListAdmin(admin.ModelAdmin):
list_display = ('email', 'verbose_name')
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
import sys
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from coin.members.models import Member
from maillists.models import (
MaillingList,
MaillingListSubscription,
skip_maillist_sync,
)
"""Import a text file of email addresses into mailling list subscription"
Create a new mailling-list subscribing the provided addresses. The script will
try to map email addresses to members, and stop if some addresses do not belong
to any member.
This command takes care to avoid triggering a sync per single subscription.
"""
class Command(BaseCommand):
help = __doc__
def add_arguments(self, parser):
parser.add_argument(
'subscribers_file',
help="The text file with the subscribed email addresses, one per line",
)
parser.add_argument(
'--email',
help='Mail address of the list',
required=True,
)
parser.add_argument(
'--verbose-name',
help='The full human-targeted name of the list',
required=True,
)
parser.add_argument(
'--force',
help='Import email adresses skipping those who do not belong to any member',
action='store_true',
default=False
)
parser.add_argument(
'--dry-run',
help='Do not write anything to database, just parse the file and show unknown addresses',
action='store_true',
default=False
)
@staticmethod
def _iter_emails(filename):
with open(filename) as f:
for l in f.readlines():
email = l.strip()
if len(email) > 0:
yield l.strip()
@staticmethod
def _get_unknown_email(emails):
for email in emails:
try:
Member.objects.get(email=email)
except Member.DoesNotExist:
yield email
@transaction.atomic
def handle(self, subscribers_file, email, verbose_name, force, dry_run, *args, **kwargs):
ml = MaillingList.objects.create(
short_name=email.split('@')[0],
email=email,
description='À RENSEIGNER',
verbose_name=verbose_name,
)
unknown_emails = []
with skip_maillist_sync():
for email in self._iter_emails(subscribers_file):
try:
member = Member.objects.get(email=email)
except Member.DoesNotExist:
unknown_emails.append(email)
else:
mls_exists = MaillingListSubscription.objects.filter(
member=member,
maillinglist=ml,
).exists()
# Not using get_or_create because we want to set skip_sync
# before saving
if not mls_exists:
mls = MaillingListSubscription(
member=member,
maillinglist=ml,
)
mls.skip_sync = True
mls.save()
# Do it once… (db will be rollback if it fails)
sys.stdout.write('Pousse la liste sur le serveur… ',)
ml.sync_to_list_server()
print('OK')
if (len(unknown_emails) > 0) and not force:
print('ERREUR : Ces adresses ne correspondent à aucun membre')
for email in unknown_emails:
print(email)
raise CommandError(
"Rien n'a été créé en base, utiliser --force au besoin.")
elif force or len(unknown_emails) == 0:
if dry_run:
# exception triggers rollback
raise CommandError(
"--dry-run est utilisée, rien n'a été écrit en base")
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('maillists', '0001_initial'),
]
operations = [
migrations.AlterModelOptions(
name='maillinglistsubscription',
options={'verbose_name': 'abonnement \xe0 une liste mail', 'verbose_name_plural': 'abonnements aux listes mail'},
),
migrations.AlterField(
model_name='maillinglist',
name='email',
field=models.EmailField(unique=True, max_length=254, verbose_name="adresse mail d'envoi"),
),
migrations.AlterField(
model_name='maillinglist',
name='short_name',
field=models.CharField(help_text='c\'est l\'identifiant qui servira \xe0 communiquer avec le serveur de liste mail (typiquement, la partie avant le "@" dans l\'adress )', unique=True, max_length=50, verbose_name='identifiant technique'),
),
migrations.AlterField(
model_name='maillinglistsubscription',
name='maillinglist',
field=models.ForeignKey(verbose_name='liste mail', to='maillists.MaillingList'),
),
migrations.AlterField(
model_name='maillinglistsubscription',
name='member',
field=models.ForeignKey(verbose_name='membre', to=settings.AUTH_USER_MODEL),
),
migrations.AlterUniqueTogether(
name='maillinglistsubscription',
unique_together=set([('member', 'maillinglist')]),
),
]
......@@ -2,6 +2,7 @@
from __future__ import unicode_literals
from contextlib import contextmanager
import subprocess
from django.conf import settings
......@@ -29,14 +30,14 @@ class MaillingListSubscription(models.Model):
class MaillingList(models.Model):
short_name = models.CharField(
'identifiant technique', max_length=50,
'identifiant technique', max_length=50, unique=True,
help_text=(
"c'est l'identifiant qui servira à "
"communiquer avec le serveur de liste mail "
"(typiquement, la partie avant le \"@\" dans l'adress )"
)
)
email = models.EmailField("adresse mail d'envoi")
email = models.EmailField("adresse mail d'envoi", unique=True)
verbose_name = models.CharField(
'nom complet', max_length=130,
help_text="Nom affiché dans l'interface membre"
......@@ -84,20 +85,18 @@ class MaillingList(models.Model):
out_stderr.decode('utf-8')))
@receiver(post_save, sender=MaillingListSubscription)
def push_new_subscription(sender, instance, created, raw, *args, **kwargs):
if raw:
print("The synchronization of mailling list with Coin was not performed, please launch it by hand in the admin interface.")
else:
elif not getattr(sender, 'skip_sync', False):
instance.maillinglist.sync_to_list_server()
@receiver(post_delete, sender=MaillingListSubscription)
def push_remove_subscription(sender, instance, *args, **kwargs):
instance.maillinglist.sync_to_list_server()
def push_remove_subscription(sender, instance, *args, **kwargs):
if not getattr(sender, 'skip_sync', False):
instance.maillinglist.sync_to_list_server()
@receiver(pre_save, sender=Member)
def store_previous_email(sender, instance, *args, **kwargs):
"""Record the email address for post_save handler
......@@ -131,3 +130,36 @@ def update_an_email_address(sender, instance, *args, **kwargs):
# except SyncCommandError as e:
# print("error", e)
# we cannot send a message because we don't have the request
SIGNALS = [
(Member, pre_save, store_previous_email),
(Member, post_save, update_an_email_address),
(MaillingListSubscription, post_save, push_new_subscription),
(MaillingListSubscription, post_delete, push_remove_subscription),
]
def connect_signals():
for sender, signal, receiver in SIGNALS:
signal.connect(sender=sender, receiver=receiver)
def disconnect_signals():
for sender, signal, receiver in SIGNALS:
signal.disconnect(sender=sender, receiver=receiver)
# Do it once
connect_signals()
@contextmanager
def skip_maillist_sync():
""" Allows to skip temporary signals
"""
disconnect_signals()
try:
yield
finally:
connect_signals()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment