From 812661cadd5bce3966dc9cd279772fd016f27566 Mon Sep 17 00:00:00 2001 From: Hugo LEVY-FALK Date: Thu, 5 Jul 2018 15:48:07 +0200 Subject: [PATCH] Supprime tout ce qui ne sert plus pour les cotisations --- cotisations/forms.py | 70 +------- cotisations/models.py | 1 + .../cotisations/new_facture_solde.html | 158 ------------------ .../templates/cotisations/recharge.html | 45 ----- cotisations/urls.py | 6 - cotisations/views.py | 120 +------------ preferences/aes_field.py | 94 ----------- .../migrations/0036_auto_20180705_0840.py | 47 ++++++ .../migrations/0039_auto_20180115_0003.py | 1 - .../migrations/0040_auto_20180129_1745.py | 7 +- preferences/models.py | 44 ----- .../preferences/display_preferences.html | 62 +++---- 12 files changed, 77 insertions(+), 578 deletions(-) delete mode 100644 cotisations/templates/cotisations/new_facture_solde.html delete mode 100644 cotisations/templates/cotisations/recharge.html delete mode 100644 preferences/aes_field.py create mode 100644 preferences/migrations/0036_auto_20180705_0840.py diff --git a/cotisations/forms.py b/cotisations/forms.py index 5659d495..02ee3e3c 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -5,6 +5,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Hugo Levy-Falk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -78,24 +79,6 @@ class NewFactureForm(FormRevMixin, ModelForm): return cleaned_data -class CreditSoldeForm(NewFactureForm): - """ - Form used to make some operations on the user's balance if the option is - activated. - """ - class Meta(NewFactureForm.Meta): - model = Facture - fields = ['paiement', 'banque', 'cheque'] - - def __init__(self, *args, **kwargs): - super(CreditSoldeForm, self).__init__(*args, **kwargs) - # TODO : change solde to balance - self.fields['paiement'].queryset = Paiement.objects.exclude( - is_balance=True) - - montant = forms.DecimalField(max_digits=5, decimal_places=2, required=True) - - class SelectUserArticleForm(FormRevMixin, Form): """ Form used to select an article during the creation of an invoice for a @@ -300,57 +283,6 @@ class DelBanqueForm(FormRevMixin, Form): self.fields['banques'].queryset = Banque.objects.all() -# TODO : change facture to Invoice -class NewFactureSoldeForm(NewFactureForm): - """ - Form used to create an invoice - """ - - def __init__(self, *args, **kwargs): - prefix = kwargs.pop('prefix', self.Meta.model.__name__) - super(NewFactureSoldeForm, self).__init__( - *args, - prefix=prefix, - **kwargs - ) - self.fields['cheque'].required = False - self.fields['banque'].required = False - self.fields['cheque'].label = _('Cheque number') - self.fields['banque'].empty_label = _("Not specified") - self.fields['paiement'].empty_label = \ - _("Select a payment method") - # TODO : change paiement to payment - paiement_list = Paiement.objects.filter(type_paiement=1) - if paiement_list: - self.fields['paiement'].widget\ - .attrs['data-cheque'] = paiement_list.first().id - - class Meta: - # TODO : change facture to invoice - model = Facture - # TODO : change paiement to payment and baque to bank - fields = ['paiement', 'banque'] - - def clean(self): - cleaned_data = super(NewFactureSoldeForm, self).clean() - # TODO : change paiement to payment - paiement = cleaned_data.get("paiement") - cheque = cleaned_data.get("cheque") - # TODO : change banque to bank - banque = cleaned_data.get("banque") - # TODO : change paiement to payment - if not paiement: - raise forms.ValidationError( - _("A payment method must be specified.") - ) - # TODO : change paiement and banque to payment and bank - elif paiement.type_paiement == "check" and not (cheque and banque): - raise forms.ValidationError( - _("A cheque number and a bank must be specified.") - ) - return cleaned_data - - # TODO : Better name and docstring class RechargeForm(FormRevMixin, Form): """ diff --git a/cotisations/models.py b/cotisations/models.py index d6280045..37d030ea 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -6,6 +6,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Hugo Levy-Falk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by diff --git a/cotisations/templates/cotisations/new_facture_solde.html b/cotisations/templates/cotisations/new_facture_solde.html deleted file mode 100644 index dac68c54..00000000 --- a/cotisations/templates/cotisations/new_facture_solde.html +++ /dev/null @@ -1,158 +0,0 @@ -{% extends "cotisations/sidebar.html" %} -{% comment %} -Re2o est un logiciel d'administration développé initiallement au rezometz. Il -se veut agnostique au réseau considéré, de manière à être installable en -quelques clics. - -Copyright © 2017 Gabriel Détraz -Copyright © 2017 Goulven Kermarec -Copyright © 2017 Augustin Lemesle - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 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 General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -{% endcomment %} - -{% load bootstrap3 %} -{% load staticfiles%} -{% load i18n %} - -{% block title %}{% trans "Invoices creation and edition" %}{% endblock %} - -{% block content %} -{% bootstrap_form_errors venteform.management_form %} - -
- {% csrf_token %} -

{% trans "New invoice" %}

- {{ venteform.management_form }} - -

{% trans "Invoice's articles" %}

-
- {% for form in venteform.forms %} -
- {% trans "Article" %} :   - {% bootstrap_form form label_class='sr-only' %} -   - -
- {% endfor %} -
- -

- {% blocktrans %} - Total price : 0,00 € - {% endblocktrans %} -

- {% trans "Confirm" as tr_confirm %} - {% bootstrap_button tr_confirm button_type='submit' icon='star' %} -
- - - -{% endblock %} - diff --git a/cotisations/templates/cotisations/recharge.html b/cotisations/templates/cotisations/recharge.html deleted file mode 100644 index 6f4e9d9c..00000000 --- a/cotisations/templates/cotisations/recharge.html +++ /dev/null @@ -1,45 +0,0 @@ -{% extends "cotisations/sidebar.html" %} -{% comment %} -Re2o est un logiciel d'administration développé initiallement au rezometz. Il -se veut agnostique au réseau considéré, de manière à être installable en -quelques clics. - -Copyright © 2017 Gabriel Détraz -Copyright © 2017 Goulven Kermarec -Copyright © 2017 Augustin Lemesle - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 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 General Public License for more details. - -You should have received a copy of the GNU General Public License along -with this program; if not, write to the Free Software Foundation, Inc., -51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -{% endcomment %} - -{% load bootstrap3 %} -{% load staticfiles%} -{% load i18n %} - -{% block title %}{% trans "Balance refill" %}{% endblock %} - -{% block content %} -

{% trans "Balance refill" %}

-

- {% blocktrans %} - Balance : {{ solde }} € - {% endblocktrans %} -

-
- {% csrf_token %} - {% bootstrap_form rechargeform %} - {% trans "Confirm" as tr_confirm %} - {% bootstrap_button tr_confirm button_type='submit' icon='piggy-bank' %} -
-{% endblock %} diff --git a/cotisations/urls.py b/cotisations/urls.py index ce6aa0f4..c906c54a 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -133,11 +133,5 @@ urlpatterns = [ views.control, name='control' ), - url( - r'^new_facture_solde/(?P[0-9]+)$', - views.new_facture_solde, - name='new_facture_solde' - ), url(r'^$', views.index, name='index'), ] + payment_methods.urls.urlpatterns - diff --git a/cotisations/views.py b/cotisations/views.py index 80f89ea1..aa60aa8b 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -5,6 +5,7 @@ # Copyright © 2017 Gabriel Détraz # Copyright © 2017 Goulven Kermarec # Copyright © 2017 Augustin Lemesle +# Copyright © 2018 Hugo Levy-Falk # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -56,7 +57,7 @@ from re2o.acl import ( can_delete_set, can_change, ) -from preferences.models import OptionalUser, AssoOption, GeneralOption +from preferences.models import AssoOption, GeneralOption from .models import Facture, Article, Vente, Paiement, Banque from .forms import ( NewFactureForm, @@ -70,7 +71,6 @@ from .forms import ( NewFactureFormPdf, SelectUserArticleForm, SelectClubArticleForm, - CreditSoldeForm, RechargeForm ) from .tex import render_invoice @@ -88,10 +88,6 @@ def new_facture(request, user, userid): A bit of JS is used in the template to add articles in a fancier way. If everything is correct, save each one of the articles, save the purchase object associated and finally the newly created invoice. - - TODO : The whole verification process should be moved to the model. This - function should only act as a dumb interface between the model and the - user. """ invoice = Facture(user=user) # The template needs the list of articles (for the JS part) @@ -670,118 +666,6 @@ def index(request): }) -# TODO : merge this function with new_facture() which is nearly the same -# TODO : change facture to invoice -@login_required -def new_facture_solde(request, userid): - """ - View called to create a new invoice when using the balance to pay. - Currently, send the list of available articles for the user along with - a formset of a new invoice (based on the `:forms:NewFactureForm()` form. - A bit of JS is used in the template to add articles in a fancier way. - If everything is correct, save each one of the articles, save the - purchase object associated and finally the newly created invoice. - - TODO : The whole verification process should be moved to the model. This - function should only act as a dumb interface between the model and the - user. - """ - user = request.user - invoice = Facture(user=user) - payment, _created = Paiement.objects.get_or_create(is_balance=True) - invoice.paiement = payment - # The template needs the list of articles (for the JS part) - article_list = Article.objects.filter( - Q(type_user='All') | Q(type_user=request.user.class_name) - ) - if request.user.is_class_club: - article_formset = formset_factory(SelectClubArticleForm)( - request.POST or None - ) - else: - article_formset = formset_factory(SelectUserArticleForm)( - request.POST or None - ) - - if article_formset.is_valid(): - articles = article_formset - # Check if at leat one article has been selected - if any(art.cleaned_data for art in articles): - user_balance = OptionalUser.get_cached_value('user_solde') - negative_balance = OptionalUser.get_cached_value('solde_negatif') - # If the paiement using balance has been activated, - # checking that the total price won't get the user under - # the authorized minimum (negative_balance) - if user_balance: - total_price = 0 - for art_item in articles: - if art_item.cleaned_data: - total_price += art_item.cleaned_data['article']\ - .prix*art_item.cleaned_data['quantity'] - if float(user.solde) - float(total_price) < negative_balance: - messages.error( - request, - _("The balance is too low for this operation.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - # Saving the invoice - invoice.save() - - # Building a purchase for each article sold - for art_item in articles: - if art_item.cleaned_data: - article = art_item.cleaned_data['article'] - quantity = art_item.cleaned_data['quantity'] - new_purchase = Vente.objects.create( - facture=invoice, - name=article.name, - prix=article.prix, - type_cotisation=article.type_cotisation, - duration=article.duration, - number=quantity - ) - new_purchase.save() - - # In case a cotisation was bought, inform the user, the - # cotisation time has been extended too - if any(art_item.cleaned_data['article'].type_cotisation - for art_item in articles if art_item.cleaned_data): - messages.success( - request, - _("The cotisation of %(member_name)s has been successfully \ - extended to %(end_date)s.") % { - 'member_name': user.pseudo, - 'end_date': user.end_adhesion() - } - ) - # Else, only tell the invoice was created - else: - messages.success( - request, - _("The invoice has been successuflly created.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - messages.error( - request, - _("You need to choose at least one article.") - ) - return redirect(reverse( - 'users:profil', - kwargs={'userid': userid} - )) - - return form({ - 'venteform': article_formset, - 'articlelist': article_list - }, 'cotisations/new_facture_solde.html', request) - - # TODO : change solde to balance @login_required @can_create(Facture) diff --git a/preferences/aes_field.py b/preferences/aes_field.py deleted file mode 100644 index 1329b0a7..00000000 --- a/preferences/aes_field.py +++ /dev/null @@ -1,94 +0,0 @@ -# coding:utf-8 -# Re2o est un logiciel d'administration développé initiallement au rezometz. Il -# se veut agnostique au réseau considéré, de manière à être installable en -# quelques clics. -# -# Copyright © 2017 Gabriel Détraz -# Copyright © 2017 Goulven Kermarec -# Copyright © 2017 Augustin Lemesle -# Copyright © 2018 Maël Kervella -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License along -# with this program; if not, write to the Free Software Foundation, Inc., -# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. - -# App de gestion des machines pour re2o -# Gabriel Détraz, Augustin Lemesle -# Gplv2 -"""preferences.aes_field -Module defining a AESEncryptedField object that can be used in forms -to handle the use of properly encrypting and decrypting AES keys -""" - -import string -import binascii -from random import choice -from Crypto.Cipher import AES - -from django.db import models -from django.conf import settings - -EOD = '`%EofD%`' # This should be something that will not occur in strings - - -def genstring(length=16, chars=string.printable): - """ Generate a random string of length `length` and composed of - the characters in `chars` """ - return ''.join([choice(chars) for i in range(length)]) - - -def encrypt(key, s): - """ AES Encrypt a secret `s` with the key `key` """ - obj = AES.new(key) - datalength = len(s) + len(EOD) - if datalength < 16: - saltlength = 16 - datalength - else: - saltlength = 16 - datalength % 16 - ss = ''.join([s, EOD, genstring(saltlength)]) - return obj.encrypt(ss) - - -def decrypt(key, s): - """ AES Decrypt a secret `s` with the key `key` """ - obj = AES.new(key) - ss = obj.decrypt(s) - return ss.split(bytes(EOD, 'utf-8'))[0] - - -class AESEncryptedField(models.CharField): - """ A Field that can be used in forms for adding the support - of AES ecnrypted fields """ - def save_form_data(self, instance, data): - setattr(instance, self.name, - binascii.b2a_base64(encrypt(settings.AES_KEY, data))) - - def to_python(self, value): - if value is None: - return None - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') - - def from_db_value(self, value, *args, **kwargs): - if value is None: - return value - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') - - def get_prep_value(self, value): - if value is None: - return value - return binascii.b2a_base64(encrypt( - settings.AES_KEY, - value - )) diff --git a/preferences/migrations/0036_auto_20180705_0840.py b/preferences/migrations/0036_auto_20180705_0840.py new file mode 100644 index 00000000..9dc67dac --- /dev/null +++ b/preferences/migrations/0036_auto_20180705_0840.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-05 13:40 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0035_optionaluser_allow_self_subscription'), + ] + + operations = [ + migrations.RemoveField( + model_name='assooption', + name='payment', + ), + migrations.RemoveField( + model_name='assooption', + name='payment_id', + ), + migrations.RemoveField( + model_name='assooption', + name='payment_pass', + ), + migrations.RemoveField( + model_name='optionaluser', + name='allow_self_subscription', + ), + migrations.RemoveField( + model_name='optionaluser', + name='max_solde', + ), + migrations.RemoveField( + model_name='optionaluser', + name='min_online_payment', + ), + migrations.RemoveField( + model_name='optionaluser', + name='solde_negatif', + ), + migrations.RemoveField( + model_name='optionaluser', + name='user_solde', + ), + ] diff --git a/preferences/migrations/0039_auto_20180115_0003.py b/preferences/migrations/0039_auto_20180115_0003.py index 3dbe2b4c..f8da5c27 100644 --- a/preferences/migrations/0039_auto_20180115_0003.py +++ b/preferences/migrations/0039_auto_20180115_0003.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals from django.db import migrations, models -import preferences.aes_field class Migration(migrations.Migration): diff --git a/preferences/migrations/0040_auto_20180129_1745.py b/preferences/migrations/0040_auto_20180129_1745.py index dc7800f4..7b8248fd 100644 --- a/preferences/migrations/0040_auto_20180129_1745.py +++ b/preferences/migrations/0040_auto_20180129_1745.py @@ -3,7 +3,10 @@ from __future__ import unicode_literals from django.db import migrations, models -import preferences.aes_field +try: + import preferences.aes_field as aes_field +except ImportError: + import cotisations.payment_methods.comnpay.aes_field as aes_field class Migration(migrations.Migration): @@ -16,7 +19,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='assooption', name='payment_pass', - field=preferences.aes_field.AESEncryptedField(blank=True, max_length=255, null=True), + field=aes_field.AESEncryptedField(blank=True, max_length=255, null=True), ), migrations.AlterField( model_name='assooption', diff --git a/preferences/models.py b/preferences/models.py index 4a22edbe..2fd7d45a 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -35,8 +35,6 @@ import cotisations.models import machines.models from re2o.mixins import AclMixin -from .aes_field import AESEncryptedField - class PreferencesModel(models.Model): """ Base object for the Preferences objects @@ -67,22 +65,6 @@ class OptionalUser(AclMixin, PreferencesModel): PRETTY_NAME = "Options utilisateur" is_tel_mandatory = models.BooleanField(default=True) - user_solde = models.BooleanField(default=False) - solde_negatif = models.DecimalField( - max_digits=5, - decimal_places=2, - default=0 - ) - max_solde = models.DecimalField( - max_digits=5, - decimal_places=2, - default=50 - ) - min_online_payment = models.DecimalField( - max_digits=5, - decimal_places=2, - default=10 - ) gpg_fingerprint = models.BooleanField(default=True) all_can_create_club = models.BooleanField( default=False, @@ -96,13 +78,6 @@ class OptionalUser(AclMixin, PreferencesModel): default=False, help_text="Un nouvel utilisateur peut se créer son compte sur re2o" ) - allow_self_subscription = models.BooleanField( - default=False, - help_text=( - "Autoriser les utilisateurs à cotiser par eux mêmes via les" - " moyens de paiement permettant l'auto-cotisation." - ) - ) shell_default = models.OneToOneField( 'users.ListShell', on_delete=models.PROTECT, @@ -298,25 +273,6 @@ class AssoOption(AclMixin, PreferencesModel): blank=True, null=True ) - PAYMENT = ( - ('NONE', 'NONE'), - ('COMNPAY', 'COMNPAY'), - ) - payment = models.CharField( - max_length=255, - choices=PAYMENT, - default='NONE', - ) - payment_id = models.CharField( - max_length=255, - default='', - blank=True - ) - payment_pass = AESEncryptedField( - max_length=255, - null=True, - blank=True, - ) description = models.TextField( null=True, blank=True, diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 99e3e14f..09395b21 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -31,46 +31,30 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

Préférences utilisateur

- + Editer - +

- - - - - {% if useroptions.user_solde %} - - - {% endif %} - - + + - {% if useroptions.user_solde %} - - - - - - - {% endif %} - + - - - + + +
Téléphone obligatoirement requis {{ useroptions.is_tel_mandatory }}Activation du solde pour les utilisateurs{{ useroptions.user_solde }}
Champ gpg fingerprint {{ useroptions.gpg_fingerprint }}Solde négatif{{ useroptions.solde_negatif }}
Creations d'adhérents par tous {{ useroptions.all_can_create_adherent }}Creations de clubs par tous{{ useroptions.all_can_create_club }}Creations de clubs par tous{{ useroptions.all_can_create_club }}
Solde maximum{{ useroptions.max_solde }}Montant minimal de rechargement en ligne{{ useroptions.min_online_payment }}
Auto inscriptionAuto inscription {{ useroptions.self_adhesion }}Shell par défaut des utilisateurs{{ useroptions.shell_default }}
Shell par défaut des utilisateurs{{ useroptions.shell_default }}

Préférences machines

@@ -91,11 +75,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ machineoptions.max_lambdauser_aliases }} Support de l'ipv6 {{ machineoptions.ipv6_mode }} - - - Creation de machines + + + Creation de machines {{ machineoptions.create_machine }} - +

Préférences topologie

@@ -108,7 +92,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Politique générale de placement de vlan {{ topologieoptions.radius_general_policy }} - Ce réglage défini la politique vlan après acceptation radius : soit sur le vlan de la plage d'ip de la machine, soit sur un vlan prédéfini dans "Vlan où placer les machines après acceptation RADIUS" + Ce réglage défini la politique vlan après acceptation radius : soit sur le vlan de la plage d'ip de la machine, soit sur un vlan prédéfini dans "Vlan où placer les machines après acceptation RADIUS" @@ -144,12 +128,12 @@ with this program; if not, write to the Free Software Foundation, Inc., Temps avant expiration du lien de reinitialisation de mot de passe (en heures) {{ generaloptions.req_expire_hrs }} - + Message global affiché sur le site {{ generaloptions.general_message }} Résumé des CGU {{ generaloptions.GTU_sum_up }} - + CGU {{generaloptions.GTU}} @@ -171,8 +155,8 @@ with this program; if not, write to the Free Software Foundation, Inc., Adresse - {{ assooptions.adresse1 }}
- {{ assooptions.adresse2 }} + {{ assooptions.adresse1 }}
+ {{ assooptions.adresse2 }} Contact mail {{ assooptions.contact }} @@ -185,13 +169,9 @@ with this program; if not, write to the Free Software Foundation, Inc., Objet utilisateur de l'association {{ assooptions.utilisateur_asso }} - Moyen de paiement automatique - {{ assooptions.payment }} - - - Description de l'association - {{ assooptions.description | safe }} - + Description de l'association + {{ assooptions.description | safe }} +

Messages personalisé dans les mails

@@ -205,7 +185,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Mail de bienvenue (Français) {{ mailmessageoptions.welcome_mail_fr | safe }} - + Mail de bienvenue (Anglais) {{ mailmessageoptions.welcome_mail_en | safe }}