diff --git a/coin/members/tests.py b/coin/members/tests.py
index 2bbeacf..2d1c707 100644
--- a/coin/members/tests.py
+++ b/coin/members/tests.py
@@ -16,7 +16,7 @@ from django.contrib.auth.models import User
from django.core import mail, management
from django.core.exceptions import ValidationError
-from coin.members.models import Member, MembershipFee, LdapUser
+from coin.members.models import Member, LdapUser
from coin.validation import chatroom_url_validator
@@ -298,67 +298,6 @@ class MemberTests(TestCase):
member.delete()
- def test_member_end_date_of_memberhip(self):
- """
- Test que end_date_of_membership d'un membre envoi bien la date de fin d'adhésion
- """
- # Créer un membre
- first_name = 'Tin'
- last_name = 'Tin'
- username = MemberTestsUtils.get_random_username()
- member = Member(first_name=first_name,
- last_name=last_name, username=username)
- member.save()
-
- start_date = date.today()
- end_date = start_date + relativedelta(years=+1)
-
- # Créé une cotisation
- membershipfee = MembershipFee(member=member, amount=20,
- start_date=start_date,
- end_date=end_date)
- membershipfee.save()
-
- self.assertEqual(member.end_date_of_membership(), end_date)
-
- def test_member_is_paid_up(self):
- """
- Test l'état "a jour de cotisation" d'un adhérent.
- """
- # Créé un membre
- first_name = 'Capitain'
- last_name = 'Haddock'
- username = MemberTestsUtils.get_random_username()
- member = Member(first_name=first_name,
- last_name=last_name, username=username)
- member.save()
-
- start_date = date.today()
- end_date = start_date + relativedelta(years=+1)
-
- # Test qu'un membre sans cotisation n'est pas à jour
- self.assertEqual(member.is_paid_up(), False)
-
- # Créé une cotisation passée
- membershipfee = MembershipFee(member=member, amount=20,
- start_date=date.today() +
- relativedelta(years=-1),
- end_date=date.today() + relativedelta(days=-10))
- membershipfee.save()
- # La cotisation s'étant terminée il y a 10 jours, il ne devrait pas
- # être à jour de cotistion
- self.assertEqual(member.is_paid_up(), False)
-
- # Créé une cotisation actuelle
- membershipfee = MembershipFee(member=member, amount=20,
- start_date=date.today() +
- relativedelta(days=-10),
- end_date=date.today() + relativedelta(days=+10))
- membershipfee.save()
- # La cotisation se terminant dans 10 jour, il devrait être à jour
- # de cotisation
- self.assertEqual(member.is_paid_up(), True)
-
def test_member_cant_be_created_without_names(self):
"""
Test qu'un membre ne peut pas être créé sans "noms"
@@ -374,7 +313,6 @@ class MemberTests(TestCase):
member.save()
-
class MemberAdminTests(TestCase):
def setUp(self):
@@ -417,83 +355,6 @@ class MemberAdminTests(TestCase):
member.delete()
-class MemberTestCallForMembershipCommand(TestCase):
-
- def setUp(self):
- # Créé un membre
- self.username = MemberTestsUtils.get_random_username()
- self.member = Member(first_name='Richard', last_name='Stallman',
- username=self.username)
- self.member.save()
-
-
- def tearDown(self):
- # Supprime le membre
- self.member.delete()
- MembershipFee.objects.all().delete()
-
- def create_membership_fee(self, end_date):
- # Créé une cotisation passée se terminant dans un mois
- membershipfee = MembershipFee(member=self.member, amount=20,
- start_date=end_date + relativedelta(years=-1),
- end_date=end_date)
- membershipfee.save()
-
- def create_membership_fee(self, end_date):
- # Créé une cotisation se terminant à la date indiquée
- membershipfee = MembershipFee(member=self.member, amount=20,
- start_date=end_date + relativedelta(years=-1),
- end_date=end_date)
- membershipfee.save()
- return membershipfee
-
- def do_test_email_sent(self, expected_emails = 1, reset_date_last_call = True):
- # Vide la outbox
- mail.outbox = []
- # Call command
- management.call_command('call_for_membership_fees', stdout=StringIO())
- # Test
- self.assertEqual(len(mail.outbox), expected_emails)
- # Comme on utilise le même membre, on reset la date de dernier envoi
- if reset_date_last_call:
- self.member.date_last_call_for_membership_fees_email = None
- self.member.save()
-
- def do_test_for_a_end_date(self, end_date, expected_emails=1, reset_date_last_call = True):
- # Supprimer toutes les cotisations (au cas ou)
- MembershipFee.objects.all().delete()
- # Créé la cotisation
- membershipfee = self.create_membership_fee(end_date)
- self.do_test_email_sent(expected_emails, reset_date_last_call)
- membershipfee.delete()
-
- def test_call_email_sent_at_expected_dates(self):
- # 1 mois avant la fin, à la fin et chaque mois après la fin pendant 3 mois
- self.do_test_for_a_end_date(date.today() + relativedelta(months=+1))
- self.do_test_for_a_end_date(date.today())
- self.do_test_for_a_end_date(date.today() + relativedelta(months=-1))
- self.do_test_for_a_end_date(date.today() + relativedelta(months=-2))
- self.do_test_for_a_end_date(date.today() + relativedelta(months=-3))
-
- def test_call_email_not_sent_if_active_membership_fee(self):
- # Créé une cotisation se terminant dans un mois
- membershipfee = self.create_membership_fee(date.today() + relativedelta(months=+1))
- # Un mail devrait être envoyé (ne pas vider date_last_call_for_membership_fees_email)
- self.do_test_email_sent(1, False)
- # Créé une cotisation enchainant et se terminant dans un an
- membershipfee = self.create_membership_fee(date.today() + relativedelta(months=+1, years=+1))
- # Pas de mail envoyé
- self.do_test_email_sent(0)
-
- def test_date_last_call_for_membership_fees_email(self):
- # Créé une cotisation se terminant dans un mois
- membershipfee = self.create_membership_fee(date.today() + relativedelta(months=+1))
- # Un mail envoyé (ne pas vider date_last_call_for_membership_fees_email)
- self.do_test_email_sent(1, False)
- # Tente un deuxième envoi, qui devrait être à 0
- self.do_test_email_sent(0)
-
-
class MemberTestsUtils(object):
@staticmethod
@@ -510,21 +371,3 @@ class TestValidators(TestCase):
with self.assertRaises(ValidationError):
chatroom_url_validator('http://#faimaison@irc.geeknode.org')
-
-class MembershipFeeTests(TestCase):
- def test_mandatory_start_date(self):
- member = Member(first_name='foo', last_name='foo', password='foo', email='foo')
- member.save()
-
- # If there is no start_date clean_fields() should raise an
- # error but not clean().
- membershipfee = MembershipFee(member=member)
- self.assertRaises(ValidationError, membershipfee.clean_fields)
- self.assertIsNone(membershipfee.clean())
-
- # If there is a start_date, everything is fine.
- membershipfee = MembershipFee(member=member, start_date=date.today())
- self.assertIsNone(membershipfee.clean_fields())
- self.assertIsNone(membershipfee.clean())
-
- member.delete()
diff --git a/coin/members/views.py b/coin/members/views.py
index 154964c..a02cbec 100644
--- a/coin/members/views.py
+++ b/coin/members/views.py
@@ -6,6 +6,7 @@ from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.conf import settings
from forms import PersonMemberChangeForm, OrganizationMemberChangeForm
+from coin.billing.models import Bill
@login_required
def index(request):
@@ -53,7 +54,7 @@ def subscriptions(request):
@login_required
def invoices(request):
balance = request.user.balance
- invoices = request.user.invoices.filter(validated=True).order_by('-date')
+ invoices = Bill.get_member_validated_bills(request.user)
payments = request.user.payments.filter().order_by('-date')
return render(request, 'members/invoices.html',
--
GitLab
From 0f12ca87c48699b07a481549333fda5309b51281 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Sat, 14 Apr 2018 22:33:55 +0200
Subject: [PATCH 011/195] Use more digits for payment amounts and member
balance (otherwise can't handle payment more than 999)
---
coin/billing/models.py | 2 +-
coin/members/models.py | 3 ++-
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/coin/billing/models.py b/coin/billing/models.py
index a392e97..75ccff2 100644
--- a/coin/billing/models.py
+++ b/coin/billing/models.py
@@ -509,7 +509,7 @@ class Payment(models.Model):
default='transfer',
choices=PAYMENT_MEAN_CHOICES,
verbose_name='moyen de paiement')
- amount = models.DecimalField(max_digits=5, decimal_places=2, null=True,
+ amount = models.DecimalField(max_digits=6, decimal_places=2, null=True,
verbose_name='montant')
date = models.DateField(default=datetime.date.today)
bill = models.ForeignKey(Bill, verbose_name='facture associée', null=True,
diff --git a/coin/members/models.py b/coin/members/models.py
index 5b9048a..379c319 100644
--- a/coin/members/models.py
+++ b/coin/members/models.py
@@ -95,10 +95,11 @@ class Member(CoinLdapSyncMixin, AbstractUser):
date_last_call_for_membership_fees_email = models.DateTimeField(null=True,
blank=True,
verbose_name="Date du dernier email de relance de cotisation envoyé")
+
send_membership_fees_email = models.BooleanField(
default=True, verbose_name='relance de cotisation',
help_text='Précise si l\'utilisateur doit recevoir des mails de relance pour la cotisation. Certains membres n\'ont pas à recevoir de relance (prélèvement automatique, membres d\'honneurs, etc.)')
- balance = models.DecimalField(max_digits=5, decimal_places=2, default=0,
+ balance = models.DecimalField(max_digits=6, decimal_places=2, default=0,
verbose_name='account balance')
objects = MemberManager()
--
GitLab
From 3981ea8913dbec355b2a647c236afbc46ce906c7 Mon Sep 17 00:00:00 2001
From: ljf
Date: Mon, 16 Apr 2018 00:43:48 +0200
Subject: [PATCH 012/195] [fix] Replace invoice by bill
---
coin/billing/models.py | 15 +++++++--------
.../admin/billing/invoice/change_form.html | 2 +-
coin/billing/templates/billing/invoice.html | 2 +-
3 files changed, 9 insertions(+), 10 deletions(-)
diff --git a/coin/billing/models.py b/coin/billing/models.py
index 75ccff2..e74a424 100644
--- a/coin/billing/models.py
+++ b/coin/billing/models.py
@@ -443,13 +443,12 @@ class Donation(Bill):
def save(self, *args, **kwargs):
- super(Donation, self).save(*args, **kwargs)
-
- def clean(self):
# Only if no amount already allocated...
- if not self.member or self.member.balance < self.amount:
+ if self.pk is None and (not self.member or self.member.balance < self.amount):
raise ValidationError("Le solde n'est pas suffisant pour payer ce don. \
Merci de commencer par enregistrer un paiement pour ce membre.")
+ super(Donation, self).save(*args, **kwargs)
+
class Meta:
verbose_name = 'don'
@@ -476,6 +475,10 @@ class MembershipFee(Bill):
return True
def save(self, *args, **kwargs):
+ # Only if no amount already allocated...
+ if self.pk is None and (not self.member or self.member.balance < self.amount):
+ raise ValidationError("Le solde n'est pas suffisant pour payer cette cotisation. \
+ Merci de commencer par enregistrer un paiement pour ce membre.")
super(MembershipFee, self).save(*args, **kwargs)
@@ -483,10 +486,6 @@ class MembershipFee(Bill):
def clean(self):
if self.start_date is not None and self.end_date is None:
self.end_date = self.start_date + datetime.timedelta(364)
- # Only if no amount already allocated...
- if not self.member or self.member.balance < self.amount:
- raise ValidationError("Le solde n'est pas suffisant pour payer cette cotisation. \
- Merci de commencer par enregistrer un paiement pour ce membre.")
class Meta:
verbose_name = 'cotisation'
diff --git a/coin/billing/templates/admin/billing/invoice/change_form.html b/coin/billing/templates/admin/billing/invoice/change_form.html
index 281786e..15f2c64 100644
--- a/coin/billing/templates/admin/billing/invoice/change_form.html
+++ b/coin/billing/templates/admin/billing/invoice/change_form.html
@@ -4,7 +4,7 @@
{% if not original.validated %}
+ Merci de payer par virement bancaire
+
+ Titulaire du compte : {% firstof branding.shortname branding.name %}
+ IBAN : {{ branding.bankinfo.iban|pretty_iban }}
+ {% if branding.bankinfo.bic %}
+ BIC : {{ branding.bankinfo.bic }}
+ {% endif %}
+
+
+
+ {% if invoice %}
+ Prière de faire figurer la reference suivante sur vos virement :
+ {% with member=invoice.member %}
+ ID {{ member.pk }} et ou {{ member.username }}
+ {% endwith %}
+ {% endif %}
+
+ {% if member %}
+ Prière de faire figurer la reference suivante sur vos virement :
+ ID {{ member.pk }} et ou {{ member.username }}
+ {% endif %}
+
À payer sans escompte avant le {{ invoice.date_due }}.
{% include "billing/payment_howto.html" %}
diff --git a/arn/templates/billing/payment_howto.html b/arn/templates/billing/payment_howto.html
index ded3e44..dbbaa51 100644
--- a/arn/templates/billing/payment_howto.html
+++ b/arn/templates/billing/payment_howto.html
@@ -12,14 +12,14 @@
{% if invoice %}
- Prière de faire figurer la reference suivante sur vos virement :
+ Prière de faire figurer l'une de vos reference adherent sur vos virement :
{% with member=invoice.member %}
ID {{ member.pk }} et ou {{ member.username }}
{% endwith %}
{% endif %}
{% if member %}
- Prière de faire figurer la reference suivante sur vos virement :
+ Prière de faire figurer l'une de vos reference adherent sur vos virement :
ID {{ member.pk }} et ou {{ member.username }}
{% endif %}
--
GitLab
From 002d01e688b85ed105fa2333f12cb336e957cb52 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Sun, 15 Apr 2018 17:36:43 +0200
Subject: [PATCH 021/195] Add a --antidate option to charge_subscriptions
action
---
coin/billing/create_subscriptions_invoices.py | 13 ++++---
.../commands/charge_subscriptions.py | 34 +++++++++++++++++--
coin/billing/models.py | 6 ++--
3 files changed, 44 insertions(+), 9 deletions(-)
diff --git a/coin/billing/create_subscriptions_invoices.py b/coin/billing/create_subscriptions_invoices.py
index 36f5a32..4f070dd 100644
--- a/coin/billing/create_subscriptions_invoices.py
+++ b/coin/billing/create_subscriptions_invoices.py
@@ -13,7 +13,7 @@ from coin.members.models import Member
from coin.billing.models import Invoice, InvoiceDetail
from django.conf import settings
-def create_all_members_invoices_for_a_period(date=None):
+def create_all_members_invoices_for_a_period(date=None, antidate=False):
"""
Pour chaque membre ayant au moins un abonnement actif, génère les factures
en prenant la date comme premier mois de la période de facturation
@@ -27,14 +27,14 @@ def create_all_members_invoices_for_a_period(date=None):
invoices = []
for member in members:
- invoice = create_member_invoice_for_a_period(member, date)
+ invoice = create_member_invoice_for_a_period(member, date, antidate)
if invoice is not None:
invoices.append(invoice)
return invoices
@transaction.atomic
-def create_member_invoice_for_a_period(member, date):
+def create_member_invoice_for_a_period(member, date, antidate):
"""
Créé si nécessaire une facture pour un membre en prenant la date passée
en paramètre comme premier mois de période. Renvoi la facture générée
@@ -157,7 +157,12 @@ def create_member_invoice_for_a_period(member, date):
if invoice.details.count() > 0:
invoice.save()
transaction.savepoint_commit(sid)
- invoice.validate() # Valide la facture et génère le PDF
+ # Valide la facture et génère le PDF
+ if antidate:
+ invoice.date_due = None # (reset the due date, will automatically be redefined when validating)
+ invoice.validate(period_to)
+ else:
+ invoice.validate()
return invoice
else:
transaction.savepoint_rollback(sid)
diff --git a/coin/billing/management/commands/charge_subscriptions.py b/coin/billing/management/commands/charge_subscriptions.py
index e248f65..a2327d3 100644
--- a/coin/billing/management/commands/charge_subscriptions.py
+++ b/coin/billing/management/commands/charge_subscriptions.py
@@ -1,5 +1,7 @@
# -*- coding: utf-8 -*-
import datetime
+
+from argparse import RawTextHelpFormatter
from django.core.management.base import BaseCommand, CommandError
from django.conf import settings
@@ -8,13 +10,39 @@ from coin.billing.create_subscriptions_invoices import create_all_members_invoic
class Command(BaseCommand):
- args = '[date=2011-07-04]'
+
help = 'Create invoices for members subscriptions for date specified (or today if no date passed)'
+ def create_parser(self, *args, **kwargs):
+ parser = super(Command, self).create_parser(*args, **kwargs)
+ parser.formatter_class = RawTextHelpFormatter
+ return parser
+
+ def add_arguments(self, parser):
+
+ parser.add_argument(
+ 'date',
+ type=str,
+ help="The date for the period for which to charge subscription (e.g. 2011-07-04)"
+ )
+
+ parser.add_argument(
+ '--antidate',
+ action='store_true',
+ dest='antidate',
+ default=False,
+ help="'Antidate' invoices, in the sense that invoices won't be validated with today's date but using the date of the end of the service. Meant to be use to charge subscription from a few months in the past..."
+ )
+
+
+
def handle(self, *args, **options):
verbosity = int(options['verbosity'])
+ antidate = options['antidate']
+ date = options["date"]
+
try:
- date = datetime.datetime.strptime(args[0], '%Y-%m-%d').date()
+ date = datetime.datetime.strptime(date, '%Y-%m-%d').date()
except IndexError:
date = datetime.date.today()
except ValueError:
@@ -25,7 +53,7 @@ class Command(BaseCommand):
self.stdout.write(
'Create invoices for all members for the date : %s' % date)
with respect_language(settings.LANGUAGE_CODE):
- invoices = create_all_members_invoices_for_a_period(date)
+ invoices = create_all_members_invoices_for_a_period(date, antidate)
if len(invoices) > 0 or verbosity >= 2:
self.stdout.write(
diff --git a/coin/billing/models.py b/coin/billing/models.py
index b27209c..24c62b5 100644
--- a/coin/billing/models.py
+++ b/coin/billing/models.py
@@ -209,12 +209,14 @@ class Invoice(models.Model):
self.pdf.save('%s.pdf' % self.number, pdf_file)
@transaction.atomic
- def validate(self):
+ def validate(self, custom_date=None):
"""
Switch invoice to validate mode. This set to False the draft field
and generate the pdf
"""
- self.date = datetime.date.today()
+
+ self.date = custom_date or datetime.date.today()
+
if not self.date_due:
self.date_due = self.date + datetime.timedelta(days=settings.PAYMENT_DELAY)
old_number = self.number
--
GitLab
From d0835194fce7ee3f36895ad4fe19c1a79bb07579 Mon Sep 17 00:00:00 2001
From: ljf
Date: Mon, 16 Apr 2018 00:47:06 +0200
Subject: [PATCH 022/195] [fix] Replace invoice by bill
---
arn/templates/billing/invoice.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/arn/templates/billing/invoice.html b/arn/templates/billing/invoice.html
index 0f77044..10f5469 100644
--- a/arn/templates/billing/invoice.html
+++ b/arn/templates/billing/invoice.html
@@ -7,7 +7,7 @@
+ Pour :
+ {% with member=bill.member %}
+ {{ member.last_name }} {{ member.first_name }}
+ {% if member.organization_name != "" %}{{ member.organization_name }} {% endif %}
+ {% if member.address %}{{member.address}} {% endif %}
+ {% if member.postal_code and member.city %}
+ {{ member.postal_code }} {{ member.city }}
+ {% endif %}
+ {% endwith %}
+
+
+
+
+
+
+
+
+
+
+ En date du {{ bill.date }}, l'association {{ branding.shortname|upper }} certifie avoir reçu un don d'un montant de {{ bill.amount }}€ de la part de {{ bill.member.first_name }} {{ bill.member.last_name }}.
+
+
+
+
+
+
+
+ N.B. : ce reçu n'a pas valeur de reçu fiscal et ne peut pas être utilisé pour prétendre à une réduction d'impôts.
+
+ Pour :
+ {% with member=bill.member %}
+ {{ member.last_name }} {{ member.first_name }}
+ {% if member.organization_name != "" %}{{ member.organization_name }} {% endif %}
+ {% if member.address %}{{member.address}} {% endif %}
+ {% if member.postal_code and member.city %}
+ {{ member.postal_code }} {{ member.city }}
+ {% endif %}
+ {% endwith %}
+
+
+
+
+
+
+
+
+
+
+ En date du {{ bill.date }}, l'association {{ branding.shortname|upper }} certifie avoir reçu et accepté une cotisation d'un montant de {{ bill.amount }}€ de la part de {{ bill.member.first_name }} {{ bill.member.last_name }}. Cette cotisation lui confère le statut de membre pour la période du {{ bill.start_date }} au {{ bill.end_date }}.
+
En date du {{ bill.date }}, l'association {{ branding.shortname|upper }} certifie avoir reçu et accepté une cotisation d'un montant de {{ bill.amount }}€ de la part de {{ bill.member.first_name }} {{ bill.member.last_name }}. Cette cotisation lui confère le statut de membre pour la période du {{ bill.start_date }} au {{ bill.end_date }}.
-
-
-
-
+{% endblock %}
--
GitLab
From 0cbca5119a8665d5e4eb7d1057c2907fded5e518 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Mon, 20 Aug 2018 14:36:18 +0200
Subject: [PATCH 034/195] Fix membership fees not being automatically applied
---
coin/billing/models.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/coin/billing/models.py b/coin/billing/models.py
index e0faf59..d77940c 100644
--- a/coin/billing/models.py
+++ b/coin/billing/models.py
@@ -782,13 +782,13 @@ def bill_changed(sender, instance, created, **kwargs):
@receiver(post_save, sender=MembershipFee)
@disable_for_loaddata
-def fee_changed(sender, instance, created, **kwargs):
+def membershipfee_changed(sender, instance, created, **kwargs):
if created and instance.member is not None:
update_accounting_for_member(instance.member)
@receiver(post_save, sender=Donation)
@disable_for_loaddata
-def fee_changed(sender, instance, created, **kwargs):
+def donation_changed(sender, instance, created, **kwargs):
if created and instance.member is not None:
update_accounting_for_member(instance.member)
--
GitLab
From cc00929646d62050580e6c71283ef0d262d1e00b Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Mon, 20 Aug 2018 19:41:46 +0200
Subject: [PATCH 035/195] Allow comment to be blank
---
coin/configuration/forms.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/coin/configuration/forms.py b/coin/configuration/forms.py
index 753e7aa..02f3742 100644
--- a/coin/configuration/forms.py
+++ b/coin/configuration/forms.py
@@ -10,7 +10,7 @@ from coin.configuration.models import Configuration
class ConfigurationForm(ModelForm):
- comment = forms.CharField(widget=forms.Textarea)
+ comment = forms.CharField(widget=forms.Textarea, blank=True)
class Meta:
model = Configuration
--
GitLab
From 1d5ae72f403ebecb3bf6f8abbe9512829d4b938e Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Tue, 21 Aug 2018 18:00:10 +0200
Subject: [PATCH 036/195] Allow to customize the ip allocation message
---
README.md | 1 +
coin/configuration/models.py | 11 +++++------
coin/settings_base.py | 4 ++++
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index f24d84e..77c1af1 100644
--- a/README.md
+++ b/README.md
@@ -349,6 +349,7 @@ List of available settings in your `settings_local.py` file.
- `HANDLE_BALANCE`: Allows to handle money balances for members (False default)
- `INVOICES_INCLUDE_CONFIG_COMMENTS`: Add comment related to a subscription configuration when generating invoices
- `MEMBER_CAN_EDIT_VPN_CONF`: Allow members to edit some part of their vpn configuration
+- `IP_ALLOCATION_MESSAGE`: Template string that will be used to log IP allocation in the corresponding coin.subnets logging system
- `DEBUG` : Enable debug for development **do not use in production** : display
stracktraces and enable [django-debug-toolbar](https://django-debug-toolbar.readthedocs.io).
diff --git a/coin/configuration/models.py b/coin/configuration/models.py
index 351d134..ae1ab59 100644
--- a/coin/configuration/models.py
+++ b/coin/configuration/models.py
@@ -9,6 +9,7 @@ from coin.offers.models import OfferSubscription
from django.db.models.signals import post_save, post_delete
from django.core.exceptions import ObjectDoesNotExist
from django.dispatch import receiver
+from django.conf import settings
from coin.resources.models import IPSubnet
@@ -139,21 +140,19 @@ def subnet_event(sender, **kwargs):
config.subnet_event()
offer = config.offersubscription.offer.name
- subref = config.offersubscription.get_subscription_reference()
+ ref = config.offersubscription.get_subscription_reference()
member = config.offersubscription.member
ip = subnet.inet
if kwargs['signal_type'] == "save":
- msg = "Allocating IP %s to member %s (%s - %s %s) (for offer %s, %s)"
+ msg = "[Allocating IP] " + settings.IP_ALLOCATION_MESSAGE
elif kwargs['signal_type'] == "delete":
- msg = "Deallocating IP %s from member %s (%s - %s %s) (was offer %s, %s)"
+ msg = "[Deallocating IP] " + settings.IP_ALLOCATION_MESSAGE
else:
# Does not happens
msg = ""
- subnet_log.info(msg % (ip, str(member.pk),
- member.username, member.first_name, member.last_name,
- offer, subref))
+ subnet_log.info(msg.format(ip=ip, member=member, offer=offer, ref=ref))
except ObjectDoesNotExist:
pass
diff --git a/coin/settings_base.py b/coin/settings_base.py
index b0c44ab..5b8ca89 100644
--- a/coin/settings_base.py
+++ b/coin/settings_base.py
@@ -302,3 +302,7 @@ HANDLE_BALANCE = False
# Add subscription comments in invoice items
INVOICES_INCLUDE_CONFIG_COMMENTS = True
+
+# String template used for the IP allocation log (c.f. coin.subnet loggers
+# This will get prefixed by [Allocating IP] or [Desallocating IP]
+IP_ALLOCATION_MESSAGE = "{ip} to {member.pk} ({member.username} - {member.first_name} {member.last_name}) (for offer {offer}, {ref})"
--
GitLab
From 1f01953a16ca8bef255b998abd1e703deb8a75d9 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Tue, 21 Aug 2018 18:04:36 +0200
Subject: [PATCH 037/195] Log as debug because for some weird reason, during
tests info messages are not logged x_x
---
coin/configuration/models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/coin/configuration/models.py b/coin/configuration/models.py
index ae1ab59..dcf9dde 100644
--- a/coin/configuration/models.py
+++ b/coin/configuration/models.py
@@ -152,7 +152,7 @@ def subnet_event(sender, **kwargs):
# Does not happens
msg = ""
- subnet_log.info(msg.format(ip=ip, member=member, offer=offer, ref=ref))
+ subnet_log.debug(msg.format(ip=ip, member=member, offer=offer, ref=ref))
except ObjectDoesNotExist:
pass
--
GitLab
From 19c21f4b32debf599ec86d8b906d25cf439b0567 Mon Sep 17 00:00:00 2001
From: ljf
Date: Wed, 22 Aug 2018 19:07:51 +0200
Subject: [PATCH 038/195] [enh] Use utils.send_templated_email
---
coin/members/models.py | 17 ++++++++---------
1 file changed, 8 insertions(+), 9 deletions(-)
diff --git a/coin/members/models.py b/coin/members/models.py
index 25a0b8b..982d454 100644
--- a/coin/members/models.py
+++ b/coin/members/models.py
@@ -44,15 +44,14 @@ def send_registration_notification(sender, user, request=None, **kwargs):
Send a notification to the admin if a user subscribe
"""
relative_link = reverse('admin:members_member_change', args=[user.id])
- abs_link = request.build_absolute_uri(relative_link)
-
- send_mail('[COIN] Nouvelle inscription',
- 'Bonjour,\n' +
- '%s s\'est enregistré(e) sur COIN.\n' % user.username +
- 'Lien d\'édition: %s' % abs_link,
- settings.DEFAULT_FROM_EMAIL,
- settings.NOTIFICATION_EMAILS,
- fail_silently=False)
+ edit_link = request.build_absolute_uri(relative_link)
+
+ utils.send_templated_email(
+ to=settings.NOTIFICATION_EMAILS,
+ subject_template='members/emails/new_member_subject.txt',
+ body_template='members/emails/new_member_email.html',
+ context={'member': self, 'edit_link': edit_link},
+ **kwargs)
class Member(CoinLdapSyncMixin, AbstractUser):
--
GitLab
From 7774ebb161a1ad3a5c3928ce81d8b0dd84047323 Mon Sep 17 00:00:00 2001
From: ljf
Date: Wed, 22 Aug 2018 23:44:58 +0200
Subject: [PATCH 039/195] [enh] Document settings
---
README.md | 1 +
coin/members/models.py | 14 +++++++-------
coin/settings_base.py | 3 +++
3 files changed, 11 insertions(+), 7 deletions(-)
diff --git a/README.md b/README.md
index f24d84e..766a1e2 100644
--- a/README.md
+++ b/README.md
@@ -351,6 +351,7 @@ List of available settings in your `settings_local.py` file.
- `MEMBER_CAN_EDIT_VPN_CONF`: Allow members to edit some part of their vpn configuration
- `DEBUG` : Enable debug for development **do not use in production** : display
stracktraces and enable [django-debug-toolbar](https://django-debug-toolbar.readthedocs.io).
+- `NOTIFICATION_EMAILS` : Emails on which to send notifications.
Accounting logs
---------------
diff --git a/coin/members/models.py b/coin/members/models.py
index 982d454..28c9af6 100644
--- a/coin/members/models.py
+++ b/coin/members/models.py
@@ -45,13 +45,13 @@ def send_registration_notification(sender, user, request=None, **kwargs):
"""
relative_link = reverse('admin:members_member_change', args=[user.id])
edit_link = request.build_absolute_uri(relative_link)
-
- utils.send_templated_email(
- to=settings.NOTIFICATION_EMAILS,
- subject_template='members/emails/new_member_subject.txt',
- body_template='members/emails/new_member_email.html',
- context={'member': self, 'edit_link': edit_link},
- **kwargs)
+ if settings.NOTIFICATION_EMAILS is not None:
+ utils.send_templated_email(
+ to=settings.NOTIFICATION_EMAILS,
+ subject_template='members/emails/new_member_subject.txt',
+ body_template='members/emails/new_member_email.html',
+ context={'member': self, 'edit_link': edit_link},
+ **kwargs)
class Member(CoinLdapSyncMixin, AbstractUser):
diff --git a/coin/settings_base.py b/coin/settings_base.py
index b0c44ab..3d2224c 100644
--- a/coin/settings_base.py
+++ b/coin/settings_base.py
@@ -16,6 +16,9 @@ ADMINS = (
# ('Your Name', 'your_email@example.com'),
)
+# Email on which to send emails
+NOTIFICATION_EMAILS = None
+
MANAGERS = ADMINS
DATABASES = {
--
GitLab
From 13c44a8579e79fb3b669808ea88b1e8eadeece55 Mon Sep 17 00:00:00 2001
From: ljf
Date: Sun, 18 Nov 2018 16:07:41 +0100
Subject: [PATCH 040/195] [fix] Missing templates
---
.../templates/members/emails/new_member_email.html | 9 +++++++++
.../members/emails/new_member_email_subject.txt | 1 +
2 files changed, 10 insertions(+)
create mode 100644 coin/members/templates/members/emails/new_member_email.html
create mode 100644 coin/members/templates/members/emails/new_member_email_subject.txt
diff --git a/coin/members/templates/members/emails/new_member_email.html b/coin/members/templates/members/emails/new_member_email.html
new file mode 100644
index 0000000..e5d2eec
--- /dev/null
+++ b/coin/members/templates/members/emails/new_member_email.html
@@ -0,0 +1,9 @@
+Bonjour,
+
+
{{ member }} s'est enregistré⋅e sur l'espace adhérent
+(COIN).
Note: nous importons les comptes tous les 1 à 2 mois, inutile donc de nous contacter si votre adhésion n'est pas validée malgré un paiement de moins de 2 mois.
+ Votre demande a bien été transmise à l'équipe de bénévoles !
+
+
+ En attendant sa validation, vous pouvez vous préparer à mettre en place un virement automatique grâce aux coordonnées bancaires décrites dans "Factures et paiements"
+
+ {% endif %}
+{% endif %}
+{{ block.super }}
+{% endblock %}
diff --git a/coin/configuration/templatetags/__init__.py b/coin/configuration/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/coin/configuration/templatetags/configuration.py b/coin/configuration/templatetags/configuration.py
new file mode 100644
index 0000000..135e5c9
--- /dev/null
+++ b/coin/configuration/templatetags/configuration.py
@@ -0,0 +1,11 @@
+from django.template import Library
+
+register = Library()
+
+@register.filter
+def provision_is_managed_via_hook(self):
+ return self.provision_is_managed_via_hook()
+
+@register.filter
+def state_is_managed_via_hook(self):
+ return self.state_is_managed_via_hook()
--
GitLab
From be825c55c5474a32b2d48241be85b6239902bf94 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Sat, 31 Aug 2019 22:08:09 +0200
Subject: [PATCH 060/195] Implement state update mechanism
---
.../commands/update_configuration_states.py | 68 +++++++++++++++++
coin/configuration/models.py | 75 ++++++++++++++++++-
2 files changed, 140 insertions(+), 3 deletions(-)
create mode 100644 coin/configuration/management/commands/update_configuration_states.py
diff --git a/coin/configuration/management/commands/update_configuration_states.py b/coin/configuration/management/commands/update_configuration_states.py
new file mode 100644
index 0000000..bf97716
--- /dev/null
+++ b/coin/configuration/management/commands/update_configuration_states.py
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+"""
+Fetch all states for configuration of a given type (e.g. VPS, ...) using the
+FETCH_STATES hook define for this conf type using settings.HOOKS.
+
+This command is meant to be called regularly from a cron job like :
+
+ */10 * * * * root manage.py update_configuration_states VPS
+
+
+The {conf_list} is a python list containing dicts like :
+ [ {"id": 4, "offer_name": "VPS-de-test", "offer_ref": "vps-test", "username": "sam" },
+ {"id": 6, "offer_name": "VPS-de-test", "offer_ref": "vps-test", "username": "saeed" },
+ {"id": 7, "offer_name": "VPS-2G-500", "offer_ref": "vps-2g", "username": "sasha" }
+ ]
+
+The {conf_list_csv} contains the same kind of information, but in the form of a CSV
+which is expected to be easier to interface with for bash hooks.
+
+ 4;VPS-de-test;vps-test;sam
+ 6;VPS-de-test;vps-test;seed
+ 7;VPS-2G-500;vps-2g;sasha
+
+The hook is expected to return the following kind of output (loaded from json or yaml or ...)
+
+ [{"id":4, "provisioned": "disabled", "status": "down", "status_color": "red"},
+ {"id":6, "provisioned": "yes", "status": "running", "status_color": "green"},
+ {"id":7, "provisioned": "ongoing", "status": "booting", "status_color": "orange"}
+ ]
+"""
+
+from __future__ import unicode_literals
+
+from argparse import RawTextHelpFormatter
+from django.core.management.base import BaseCommand, CommandError
+
+from coin.configuration.models import Configuration
+
+################################################################################
+
+
+class Command(BaseCommand):
+
+ help = __doc__
+
+ def create_parser(self, *args, **kwargs):
+ parser = super(Command, self).create_parser(*args, **kwargs)
+ parser.formatter_class = RawTextHelpFormatter
+ return parser
+
+ def add_arguments(self, parser):
+
+ parser.add_argument(
+ 'conf_type',
+ type=str,
+ help="Something like VPS, VPN, Housing, ..."
+ )
+
+ def handle(self, *args, **options):
+
+ if not options["conf_type"] != "":
+ raise CommandError("You must provide a configuration type")
+
+ conf_classes = {c()._meta.verbose_name: c().__class__ for c in Configuration.__subclasses__()}
+ if options["conf_type"] not in conf_classes:
+ raise CommandError("Unknown conf type %s ... known conf types are : %s" % (options["conf_type"], ', '.join(conf_classes.keys())))
+
+ conf_classes[options["conf_type"]].fetch_and_update_all_states()
diff --git a/coin/configuration/models.py b/coin/configuration/models.py
index 91adfdd..4968310 100644
--- a/coin/configuration/models.py
+++ b/coin/configuration/models.py
@@ -1,5 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+import datetime
+import csv
+# In Python3, StringIO will just become io
+import StringIO as io
from django.db import models
from polymorphic import PolymorphicModel
@@ -55,7 +59,7 @@ class Configuration(PolymorphicModel):
@classmethod
def state_is_managed_via_hook(self):
- return HookManager.is_defined(self._meta.verbose_name, "STATES")
+ return HookManager.is_defined(self._meta.verbose_name, "FETCH_ALL_STATES")
@staticmethod
def get_configurations_choices_list():
@@ -68,8 +72,11 @@ class Configuration(PolymorphicModel):
def convert_to_dict_for_hook(self):
# FIXME : check assertions here
- return {"offer": str(self.offersubscription.offer.name),
- "member": str(self.offersubscription.member.username)}
+ return {"id": self.pk,
+ "offer_name": str(self.offersubscription.offer.name),
+ "offer_ref": str(self.offersubscription.offer.reference),
+ "username": str(self.offersubscription.member.username),
+ }
def provision(self):
assert self.provisioned == "no", "Cette configuration est déjà provisionnée"
@@ -93,6 +100,68 @@ class Configuration(PolymorphicModel):
self.provisioned = "no"
self.save()
+ def update_state(self, provisioned=None, status=None, status_color=None, **kwargs):
+ if provisioned is not None:
+ provisioned = 'yes' if provisioned is True else provisioned
+ provisioned = 'no' if provisioned is False else provisioned
+ self.provisioned = provisioned
+ if status is not None:
+ self.status = status
+ if status_color is not None:
+ self.status_color = status_color
+ if any([info is not None for info in [provisioned, status, status_color]]):
+ self.last_status_update = datetime.datetime.now()
+ self.save()
+
+ @classmethod
+ def fetch_and_update_all_states(self):
+ """
+ Fetch all states for configuration of a given type (e.g. VPS, ...) using the
+ FETCH_ALL_STATES hook define for this conf type using settings.HOOKS.
+
+ The {conf_list} is a python list containing dicts like :
+ [ {"id": 4, "offer_name": "VPS-de-test", "offer_ref": "vps-test", "username": "sam" },
+ {"id": 6, "offer_name": "VPS-de-test", "offer_ref": "vps-test", "username": "saeed" },
+ {"id": 7, "offer_name": "VPS-2G-500", "offer_ref": "vps-2g", "username": "sasha" }
+ ]
+
+ The {conf_list_csv} contains the same kind of information, but in the form of a CSV
+ which is expected to be easier to interface with for bash hooks.
+
+ 4;VPS-de-test;vps-test;sam
+ 6;VPS-de-test;vps-test;seed
+ 7;VPS-2G-500;vps-2g;sasha
+
+ The hook is expected to return the following kind of output (loaded from json or yaml or ...)
+
+ [{"id":4, "provisioned": "disabled", "status": "down", "status_color": "red"},
+ {"id":6, "provisioned": "yes", "status": "running", "status_color": "green"},
+ {"id":7, "provisioned": "ongoing", "status": "booting", "status_color": "orange"}
+ ]
+ """
+ assert self.state_is_managed_via_hook(), "There is no hook defined for %s" % self._meta.verbose_name
+ confs_to_update = self.objects.all()
+
+ conf_list = [c.convert_to_dict_for_hook() for c in confs_to_update]
+ conf_list_csv = io.StringIO()
+ info_order = ["id", "offer_name", "offer_ref", "username"]
+ writer = csv.writer(conf_list_csv, delimiter=str(';'), quotechar=str('"'), quoting=csv.QUOTE_MINIMAL)
+ for c in conf_list:
+ writer.writerow([c[info] for info in info_order])
+ conf_list_csv = conf_list_csv.getvalue()
+
+ success, out, err = HookManager.run(self._meta.verbose_name, "FETCH_ALL_STATES", conf_list=conf_list, conf_list_csv=conf_list_csv)
+
+ if not success:
+ raise Exception("Some errors happened during the execution of FETCH_ALL_STATES for %s : %s" % (self._meta.verbose_name, err))
+
+ assert isinstance(out, list), "Was expecting to get a list as output of FETCH_ALL_STATES"
+ for state_infos in out:
+ assert isinstance(state_infos, dict) and "id" in state_infos, "Was expecting to get a list of dict as output of FETCH_ALL_STATES with at least 'id' in it"
+ conf_to_update = Configuration.objects.get(pk=state_infos.pop("id"))
+ print(state_infos)
+ conf_to_update.update_state(**state_infos)
+
def model_name(self):
return self.__class__.__name__
model_name.short_description = 'Nom du modèle'
--
GitLab
From 8601b38c795c5564d70f6c1b67bce798407579ed Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Sat, 31 Aug 2019 23:04:34 +0200
Subject: [PATCH 061/195] Display service state on user's subscription page
---
coin/configuration/models.py | 26 ++++++++++++++++---
.../templates/members/subscriptions.html | 3 +++
2 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/coin/configuration/models.py b/coin/configuration/models.py
index 4968310..c35b6ea 100644
--- a/coin/configuration/models.py
+++ b/coin/configuration/models.py
@@ -11,6 +11,7 @@ from coin.offers.models import OfferSubscription
from django.db.models.signals import post_save, post_delete
from django.core.exceptions import ObjectDoesNotExist
from django.dispatch import receiver
+from django.utils.safestring import mark_safe
from coin.hooks import HookManager
from coin.resources.models import IPSubnet
@@ -50,8 +51,8 @@ class Configuration(PolymorphicModel):
status = models.CharField(default="N/A", blank=False, null=False, max_length=32, verbose_name="status")
status_color_choices = ["green", "orange", "red", "grey"]
status_color = models.CharField(default="grey", max_length=16, choices=[(c, c) for c in status_color_choices], blank=True, null=False)
- last_status_update = models.DateField(null=True, blank=True,
- verbose_name="dernière mise à jour de l'état")
+ last_status_update = models.DateTimeField(null=True, blank=True,
+ verbose_name="dernière mise à jour de l'état")
@classmethod
def provision_is_managed_via_hook(self):
@@ -159,8 +160,27 @@ class Configuration(PolymorphicModel):
for state_infos in out:
assert isinstance(state_infos, dict) and "id" in state_infos, "Was expecting to get a list of dict as output of FETCH_ALL_STATES with at least 'id' in it"
conf_to_update = Configuration.objects.get(pk=state_infos.pop("id"))
- print(state_infos)
conf_to_update.update_state(**state_infos)
+ print conf_to_update.__dict__
+
+ def get_state_icon_display(self):
+
+ if self.provisioned == "yes":
+ text = self.status
+ color = self.status_color
+ elif self.provisioned == "ongoing":
+ text = "Pas encore provisionné"
+ color = "orange"
+ elif self.provisioned == "disabled":
+ text = "Désactivé"
+ color = "red"
+ else:
+ text = "Pas encore provisionné"
+ color = "grey"
+
+ text = "Status: %s\nDernière mise à jour: %s" % (text, str(self.last_status_update))
+
+ return mark_safe(''.format(color=color, text=text))
def model_name(self):
return self.__class__.__name__
diff --git a/coin/members/templates/members/subscriptions.html b/coin/members/templates/members/subscriptions.html
index aedfd1e..8ddc158 100644
--- a/coin/members/templates/members/subscriptions.html
+++ b/coin/members/templates/members/subscriptions.html
@@ -8,6 +8,7 @@
{% endif %}
{{ block.super }}
--
GitLab
From 1dedb394ab3babb0d7534c688315ea4d5bb9ae2e Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Sun, 5 Jul 2020 19:31:35 +0200
Subject: [PATCH 092/195] CSS tweak for admin index
---
coin/static/css/admin-local.css | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/coin/static/css/admin-local.css b/coin/static/css/admin-local.css
index 343d3a9..b65c674 100644
--- a/coin/static/css/admin-local.css
+++ b/coin/static/css/admin-local.css
@@ -20,6 +20,10 @@ form .inline-group .inline-related h3 .inline_label { /* TabularStacked
/* Cards in admin index */
+.dashboard #content {
+ width: auto;
+}
+
.app-card {
width: 31%;
border: 1px solid lightgray;
--
GitLab
From f85ac5e9f7cdbf019e2b9fa15eeefcb4047b4857 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Mon, 6 Jul 2020 01:31:21 +0200
Subject: [PATCH 093/195] Fix validation of SSH in case the comment contains
spaces
---
vps/models.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/vps/models.py b/vps/models.py
index f6b5e2c..a61b29c 100644
--- a/vps/models.py
+++ b/vps/models.py
@@ -29,7 +29,7 @@ def PublicSSHKeyValidator(ssh_pubkey):
raise ValidationError("Une clef SSH devrait être une chaine de caractères?")
try:
# From https://stackoverflow.com/questions/2494450/ssh-rsa-public-key-validation-using-a-regular-expression
- type_, key_string, comment = ssh_pubkey.split()
+ type_, key_string, comment = ssh_pubkey.strip().split(None, 2)
data = base64.decodestring(key_string)
int_len = 4
str_len = struct.unpack('>I', data[:int_len])[0] # this should return 7
--
GitLab
From 96a0e7d04c025dfc0f73332955348b0defa90a58 Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Mon, 6 Jul 2020 01:46:53 +0200
Subject: [PATCH 094/195] Fix mail sending when accepting/refusing offer
subscription request
---
coin/offers/models.py | 8 ++++----
.../offers/emails/subscription_request_accepted.html | 7 -------
.../offers/emails/subscription_request_accepted.txt | 8 +++++++-
.../emails/subscription_request_accepted_subject.txt | 1 +
.../offers/emails/subscription_request_refused.html | 5 -----
.../offers/emails/subscription_request_refused.txt | 4 ++++
.../emails/subscription_request_refused_subject.txt | 1 +
7 files changed, 17 insertions(+), 17 deletions(-)
delete mode 100644 coin/offers/templates/offers/emails/subscription_request_accepted.html
create mode 100644 coin/offers/templates/offers/emails/subscription_request_accepted_subject.txt
delete mode 100644 coin/offers/templates/offers/emails/subscription_request_refused.html
create mode 100644 coin/offers/templates/offers/emails/subscription_request_refused_subject.txt
diff --git a/coin/offers/models.py b/coin/offers/models.py
index db5f7e1..372f13e 100644
--- a/coin/offers/models.py
+++ b/coin/offers/models.py
@@ -313,8 +313,8 @@ class OfferSubscriptionRequest(Request):
send_templated_email(
to=self.member.email,
- subject_template='offers/emails/subscription_request_accepted.txt',
- body_template='offers/emails/subscription_request_accepted.html',
+ subject_template='offers/emails/subscription_request_accepted_subject.txt',
+ body_template='offers/emails/subscription_request_accepted.txt',
context={ 'request': self })
def refuse(self):
@@ -323,8 +323,8 @@ class OfferSubscriptionRequest(Request):
send_templated_email(
to=self.member.email,
- subject_template='offers/emails/subscription_request_refused.txt',
- body_template='offers/emails/subscription_request_refused.html',
+ subject_template='offers/emails/subscription_request_refused_subject.txt',
+ body_template='offers/emails/subscription_request_refused.txt',
context={ 'request': self })
@property
diff --git a/coin/offers/templates/offers/emails/subscription_request_accepted.html b/coin/offers/templates/offers/emails/subscription_request_accepted.html
deleted file mode 100644
index 49b0d9f..0000000
--- a/coin/offers/templates/offers/emails/subscription_request_accepted.html
+++ /dev/null
@@ -1,7 +0,0 @@
-Un bénévole viens de valider votre demande pour un {{ request.offer.name }}.
-
-Commentaire de l'équipe bénévole :
-
-{{ request.admin_comments }}
-
-Vous pouvez consulter votre espace adhérent pour plus d'informations sur l'état et la configuration de votre service.
diff --git a/coin/offers/templates/offers/emails/subscription_request_accepted.txt b/coin/offers/templates/offers/emails/subscription_request_accepted.txt
index 326812d..49b0d9f 100644
--- a/coin/offers/templates/offers/emails/subscription_request_accepted.txt
+++ b/coin/offers/templates/offers/emails/subscription_request_accepted.txt
@@ -1 +1,7 @@
-Votre demande pour un {{ request.offer.name }} a été accepté !
+Un bénévole viens de valider votre demande pour un {{ request.offer.name }}.
+
+Commentaire de l'équipe bénévole :
+
+{{ request.admin_comments }}
+
+Vous pouvez consulter votre espace adhérent pour plus d'informations sur l'état et la configuration de votre service.
diff --git a/coin/offers/templates/offers/emails/subscription_request_accepted_subject.txt b/coin/offers/templates/offers/emails/subscription_request_accepted_subject.txt
new file mode 100644
index 0000000..326812d
--- /dev/null
+++ b/coin/offers/templates/offers/emails/subscription_request_accepted_subject.txt
@@ -0,0 +1 @@
+Votre demande pour un {{ request.offer.name }} a été accepté !
diff --git a/coin/offers/templates/offers/emails/subscription_request_refused.html b/coin/offers/templates/offers/emails/subscription_request_refused.html
deleted file mode 100644
index 8bdb4ce..0000000
--- a/coin/offers/templates/offers/emails/subscription_request_refused.html
+++ /dev/null
@@ -1,5 +0,0 @@
-Votre demande pour un {{ request.offer.name }} a été refusée.
-
-Commentaire de l'équipe bénévole :
-
-{{ request.admin_comments }}
diff --git a/coin/offers/templates/offers/emails/subscription_request_refused.txt b/coin/offers/templates/offers/emails/subscription_request_refused.txt
index 1b22b18..8bdb4ce 100644
--- a/coin/offers/templates/offers/emails/subscription_request_refused.txt
+++ b/coin/offers/templates/offers/emails/subscription_request_refused.txt
@@ -1 +1,5 @@
Votre demande pour un {{ request.offer.name }} a été refusée.
+
+Commentaire de l'équipe bénévole :
+
+{{ request.admin_comments }}
diff --git a/coin/offers/templates/offers/emails/subscription_request_refused_subject.txt b/coin/offers/templates/offers/emails/subscription_request_refused_subject.txt
new file mode 100644
index 0000000..1b22b18
--- /dev/null
+++ b/coin/offers/templates/offers/emails/subscription_request_refused_subject.txt
@@ -0,0 +1 @@
+Votre demande pour un {{ request.offer.name }} a été refusée.
--
GitLab
From b3719259ea91361242f34b03d115698a5647a9ee Mon Sep 17 00:00:00 2001
From: Alexandre Aubin
Date: Mon, 6 Jul 2020 03:26:23 +0200
Subject: [PATCH 095/195] Rework VPS/VPN user page + misc CSS tweaks
---
coin/members/templates/members/detail.html | 2 +-
.../templates/members/subscriptions.html | 5 +-
coin/static/css/local.css | 27 ++-
vpn/templates/vpn/vpn.html | 160 +++++-------------
vps/templates/vps/vps.html | 141 ++++++---------
5 files changed, 111 insertions(+), 224 deletions(-)
diff --git a/coin/members/templates/members/detail.html b/coin/members/templates/members/detail.html
index 890209c..7d0013d 100644
--- a/coin/members/templates/members/detail.html
+++ b/coin/members/templates/members/detail.html
@@ -87,7 +87,7 @@
Je n'ai encore jamais cotisé.
{% endif %}
-
Note: nous importons les comptes tous les 1 à 2 mois, inutile donc de nous contacter si votre adhésion n'est pas validée malgré un paiement de moins de 2 mois.
+
Note: nous importons les comptes tous les 1 à 2 mois, inutile donc de nous contacter si votre adhésion n'est pas validée malgré un paiement de moins de 2 mois.