8
0
Fork 0
mirror of https://gitlab.federez.net/re2o/re2o synced 2024-07-04 13:14:06 +00:00

Merge branch 'mail_account' into 'master'

Mail account

See merge request federez/re2o!199
This commit is contained in:
chirac 2018-08-01 13:23:49 +02:00
commit 90674d4978
15 changed files with 820 additions and 165 deletions

View file

@ -495,7 +495,8 @@ class UserSerializer(NamespacedHMSerializer):
class Meta: class Meta:
model = users.User model = users.User
fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment', fields = ('surname', 'pseudo', 'email', 'local_email_redirect',
'local_email_enabled', 'school', 'shell', 'comment',
'state', 'registered', 'telephone', 'solde', 'access', 'state', 'registered', 'telephone', 'solde', 'access',
'end_access', 'uid', 'class_name', 'api_url') 'end_access', 'uid', 'class_name', 'api_url')
extra_kwargs = { extra_kwargs = {
@ -512,7 +513,8 @@ class ClubSerializer(NamespacedHMSerializer):
class Meta: class Meta:
model = users.Club model = users.Club
fields = ('name', 'pseudo', 'email', 'school', 'shell', 'comment', fields = ('name', 'pseudo', 'email', 'local_email_redirect',
'local_email_enabled', 'school', 'shell', 'comment',
'state', 'registered', 'telephone', 'solde', 'room', 'state', 'registered', 'telephone', 'solde', 'room',
'access', 'end_access', 'administrators', 'members', 'access', 'end_access', 'administrators', 'members',
'mailing', 'uid', 'api_url') 'mailing', 'uid', 'api_url')
@ -529,9 +531,10 @@ class AdherentSerializer(NamespacedHMSerializer):
class Meta: class Meta:
model = users.Adherent model = users.Adherent
fields = ('name', 'surname', 'pseudo', 'email', 'school', 'shell', fields = ('name', 'surname', 'pseudo', 'email', 'local_email_redirect',
'comment', 'state', 'registered', 'telephone', 'room', 'local_email_enabled', 'school', 'shell', 'comment',
'solde', 'access', 'end_access', 'uid', 'api_url') 'state', 'registered', 'telephone', 'room', 'solde',
'access', 'end_access', 'uid', 'api_url')
extra_kwargs = { extra_kwargs = {
'shell': {'view_name': 'shell-detail'} 'shell': {'view_name': 'shell-detail'}
} }
@ -593,6 +596,15 @@ class WhitelistSerializer(NamespacedHMSerializer):
fields = ('user', 'raison', 'date_start', 'date_end', 'active', 'api_url') fields = ('user', 'raison', 'date_start', 'date_end', 'active', 'api_url')
class EMailAddressSerializer(NamespacedHMSerializer):
"""Serialize `users.models.EMailAddress` objects.
"""
class Meta:
model = users.EMailAddress
fields = ('user', 'local_part', 'complete_email_address', 'api_url')
# SERVICE REGEN # SERVICE REGEN
@ -611,6 +623,21 @@ class ServiceRegenSerializer(NamespacedHMSerializer):
} }
# LOCAL EMAILS
class LocalEmailUsersSerializer(NamespacedHMSerializer):
email_address = EMailAddressSerializer(
read_only=True,
many=True
)
class Meta:
model = users.User
fields = ('local_email_enabled', 'local_email_redirect',
'email_address')
# DHCP # DHCP

View file

@ -93,10 +93,13 @@ router.register_viewset(r'users/listright', views.ListRightViewSet)
router.register_viewset(r'users/shell', views.ShellViewSet, base_name='shell') router.register_viewset(r'users/shell', views.ShellViewSet, base_name='shell')
router.register_viewset(r'users/ban', views.BanViewSet) router.register_viewset(r'users/ban', views.BanViewSet)
router.register_viewset(r'users/whitelist', views.WhitelistViewSet) router.register_viewset(r'users/whitelist', views.WhitelistViewSet)
router.register_viewset(r'users/emailaddress', views.EMailAddressViewSet)
# SERVICE REGEN # SERVICE REGEN
router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen') router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen')
# DHCP # DHCP
router.register_view(r'dhcp/hostmacip', views.HostMacIpView), router.register_view(r'dhcp/hostmacip', views.HostMacIpView),
# LOCAL EMAILS
router.register_view(r'localemail/users', views.LocalEmailUsersView),
# DNS # DNS
router.register_view(r'dns/zones', views.DNSZonesView), router.register_view(r'dns/zones', views.DNSZonesView),
# MAILING # MAILING

View file

@ -469,6 +469,21 @@ class WhitelistViewSet(viewsets.ReadOnlyModelViewSet):
serializer_class = serializers.WhitelistSerializer serializer_class = serializers.WhitelistSerializer
class EMailAddressViewSet(viewsets.ReadOnlyModelViewSet):
"""Exposes list and details of `users.models.EMailAddress` objects.
"""
serializer_class = serializers.EMailAddressSerializer
queryset = users.EMailAddress.objects.none()
def get_queryset(self):
if preferences.OptionalUser.get_cached_value(
'local_email_accounts_enabled'):
return (users.EMailAddress.objects
.filter(user__local_email_enabled=True))
else:
return users.EMailAddress.objects.none()
# SERVICE REGEN # SERVICE REGEN
@ -489,8 +504,26 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
return queryset return queryset
# LOCAL EMAILS
class LocalEmailUsersView(generics.ListAPIView):
"""Exposes all the aliases of the users that activated the internal address
"""
serializer_class = serializers.LocalEmailUsersSerializer
def get_queryset(self):
if preferences.OptionalUser.get_cached_value(
'local_email_accounts_enabled'):
return (users.User.objects
.filter(local_email_enabled=True))
else:
return users.User.objects.none()
# DHCP # DHCP
class HostMacIpView(generics.ListAPIView): class HostMacIpView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in """Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files. order to build the DHCP lease files.
@ -501,6 +534,7 @@ class HostMacIpView(generics.ListAPIView):
# DNS # DNS
class DNSZonesView(generics.ListAPIView): class DNSZonesView(generics.ListAPIView):
"""Exposes the detailed information about each extension (hostnames, """Exposes the detailed information about each extension (hostnames,
IPs, DNS records, etc.) in order to build the DNS zone files. IPs, DNS records, etc.) in order to build the DNS zone files.

View file

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-26 19:31
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0045_remove_unused_payment_fields'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='local_email_accounts_enabled',
field=models.BooleanField(default=False, help_text='Enable local email accounts for users'),
),
migrations.AddField(
model_name='optionaluser',
name='local_email_domain',
field=models.CharField(default='@example.org', help_text='Domain to use for local email accounts', max_length=32),
),
migrations.AddField(
model_name='optionaluser',
name='max_email_address',
field=models.IntegerField(default=15, help_text='Maximum number of local email address for a standard user'),
),
]

View file

@ -30,6 +30,7 @@ from django.db import models
from django.db.models.signals import post_save from django.db.models.signals import post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.core.cache import cache from django.core.cache import cache
from django.forms import ValidationError
import machines.models import machines.models
from re2o.mixins import AclMixin from re2o.mixins import AclMixin
@ -83,12 +84,32 @@ class OptionalUser(AclMixin, PreferencesModel):
blank=True, blank=True,
null=True null=True
) )
local_email_accounts_enabled = models.BooleanField(
default=False,
help_text="Enable local email accounts for users"
)
local_email_domain = models.CharField(
max_length = 32,
default = "@example.org",
help_text="Domain to use for local email accounts",
)
max_email_address = models.IntegerField(
default = 15,
help_text = "Maximum number of local email address for a standard user"
)
class Meta: class Meta:
permissions = ( permissions = (
("view_optionaluser", "Peut voir les options de l'user"), ("view_optionaluser", "Peut voir les options de l'user"),
) )
def clean(self):
"""Clean model:
Check the mail_extension
"""
if self.local_email_domain[0] != "@":
raise ValidationError("Mail domain must begin with @")
@receiver(post_save, sender=OptionalUser) @receiver(post_save, sender=OptionalUser)
def optionaluser_post_save(**kwargs): def optionaluser_post_save(**kwargs):

View file

@ -32,194 +32,204 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %} {% block content %}
<h4>Préférences utilisateur</h4> <h4>Préférences utilisateur</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i> Editer
Editer
</a> </a>
<p>
</p> <h5>Généralités</h5>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Téléphone obligatoirement requis</th> <th>Téléphone obligatoirement requis</th>
<td>{{ useroptions.is_tel_mandatory|tick }}</td> <td>{{ useroptions.is_tel_mandatory|tick }}</td>
<th>Auto inscription</th> <th>Auto inscription</th>
<td>{{ useroptions.self_adhesion|tick }}</td> <td>{{ useroptions.self_adhesion|tick }}</td>
</tr> </tr>
<tr> <tr>
<th>Champ gpg fingerprint</th> <th>Champ gpg fingerprint</th>
<td>{{ useroptions.gpg_fingerprint|tick }}</td> <td>{{ useroptions.gpg_fingerprint|tick }}</td>
<th>Shell par défaut des utilisateurs</th> <th>Shell par défaut des utilisateurs</th>
<td>{{ useroptions.shell_default }}</td> <td>{{ useroptions.shell_default }}</td>
</tr> </tr>
<tr> <tr>
<th>Creations d'adhérents par tous</th> <th>Creations d'adhérents par tous</th>
<td>{{ useroptions.all_can_create_adherent|tick }}</td> <td>{{ useroptions.all_can_create_adherent|tick }}</td>
<th>Creations de clubs par tous</th> <th>Creations de clubs par tous</th>
<td>{{ useroptions.all_can_create_club|tick }}</td> <td>{{ useroptions.all_can_create_club|tick }}</td>
</tr> </tr>
</table>
<h5>Comptes mails</h5>
<table class="table table-striped">
<tr>
<th>Gestion des comptes mails</th>
<td>{{ useroptions.local_email_accounts_enabled | tick }}</td>
<th>Extension mail interne</th>
<td>{{ useroptions.local_email_domain }}</td>
</tr>
<tr>
<th>Nombre d'alias mail max</th>
<td>{{ useroptions.max_email_address }}</td>
</tr>
</table> </table>
<h4>Préférences machines</h4> <h4>Préférences machines</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i> Editer
Editer
</a> </a>
<p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Mot de passe par machine</th> <th>Mot de passe par machine</th>
<td>{{ machineoptions.password_machine|tick }}</td> <td>{{ machineoptions.password_machine|tick }}</td>
<th>Machines/interfaces autorisées par utilisateurs</th> <th>Machines/interfaces autorisées par utilisateurs</th>
<td>{{ machineoptions.max_lambdauser_interfaces }}</td> <td>{{ machineoptions.max_lambdauser_interfaces }}</td>
</tr> </tr>
<tr> <tr>
<th>Alias dns autorisé par utilisateur</th> <th>Alias dns autorisé par utilisateur</th>
<td>{{ machineoptions.max_lambdauser_aliases }}</td> <td>{{ machineoptions.max_lambdauser_aliases }}</td>
<th>Support de l'ipv6</th> <th>Support de l'ipv6</th>
<td>{{ machineoptions.ipv6_mode }}</td> <td>{{ machineoptions.ipv6_mode }}</td>
</tr> </tr>
<tr> <tr>
<th>Creation de machines</th> <th>Creation de machines</th>
<td>{{ machineoptions.create_machine|tick }}</td> <td>{{ machineoptions.create_machine|tick }}</td>
</tr> </tr>
</table> </table>
<h4>Préférences topologie</h4> <h4>Préférences topologie</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i> Editer
Editer
</a> </a>
<p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Politique générale de placement de vlan</th> <th>Politique générale de placement de vlan</th>
<td>{{ topologieoptions.radius_general_policy }}</td> <td>{{ topologieoptions.radius_general_policy }}</td>
<th> 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"</th> <th>
<td></td> Ce réglage défini la politique vlan après acceptation radius :
</tr> soit sur le vlan de la plage d'ip de la machine, soit sur un
<tr> vlan prédéfini dans "Vlan où placer les machines après acceptation
<th>Vlan où placer les machines après acceptation RADIUS</th> RADIUS"
<td>{{ topologieoptions.vlan_decision_ok }}</td> </th>
<th>Vlan où placer les machines après rejet RADIUS</th> <td></td>
<td>{{ topologieoptions.vlan_decision_nok }}</td> </tr>
</tr> <tr>
<th>Vlan où placer les machines après acceptation RADIUS</th>
<td>{{ topologieoptions.vlan_decision_ok }}</td>
<th>Vlan où placer les machines après rejet RADIUS</th>
<td>{{ topologieoptions.vlan_decision_nok }}</td>
</tr>
</table> </table>
<h4>Préférences generales</h4> <h4>Préférences generales</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i> Editer
Editer
</a> </a>
<p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Nom du site web</th> <th>Nom du site web</th>
<td>{{ generaloptions.site_name }}</td> <td>{{ generaloptions.site_name }}</td>
<th>Adresse mail d'expedition automatique</th> <th>Adresse mail d'expedition automatique</th>
<td>{{ generaloptions.email_from }}</td> <td>{{ generaloptions.email_from }}</td>
</tr> </tr>
<tr> <tr>
<th>Affichage de résultats dans le champ de recherche</th> <th>Affichage de résultats dans le champ de recherche</th>
<td>{{ generaloptions.search_display_page }}</td> <td>{{ generaloptions.search_display_page }}</td>
<th>Nombre d'items affichés en liste (taille normale)</th> <th>Nombre d'items affichés en liste (taille normale)</th>
<td>{{ generaloptions.pagination_number }}</td> <td>{{ generaloptions.pagination_number }}</td>
</tr> </tr>
<tr> <tr>
<th>Nombre d'items affichés en liste (taille élevée)</th> <th>Nombre d'items affichés en liste (taille élevée)</th>
<td>{{ generaloptions.pagination_large_number }}</td> <td>{{ generaloptions.pagination_large_number }}</td>
<th>Temps avant expiration du lien de reinitialisation de mot de passe (en heures)</th> <th>Temps avant expiration du lien de reinitialisation de mot de passe (en heures)</th>
<td>{{ generaloptions.req_expire_hrs }}</td> <td>{{ generaloptions.req_expire_hrs }}</td>
</tr> </tr>
<tr> <tr>
<th>Message global affiché sur le site</th> <th>Message global affiché sur le site</th>
<td>{{ generaloptions.general_message }}</td> <td>{{ generaloptions.general_message }}</td>
<th>Résumé des CGU</th> <th>Résumé des CGU</th>
<td>{{ generaloptions.GTU_sum_up }}</td> <td>{{ generaloptions.GTU_sum_up }}</td>
<tr> <tr>
<tr> <tr>
<th>CGU</th> <th>CGU</th>
<td>{{generaloptions.GTU}}</th> <td>{{generaloptions.GTU}}</th>
</tr> </tr>
</table> </table>
<h4>Données de l'association</h4> <h4>Données de l'association</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i> Editer
Editer
</a> </a>
<p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Nom</th> <th>Nom</th>
<td>{{ assooptions.name }}</td> <td>{{ assooptions.name }}</td>
<th>SIRET</th> <th>SIRET</th>
<td>{{ assooptions.siret }}</td> <td>{{ assooptions.siret }}</td>
</tr> </tr>
<tr> <tr>
<th>Adresse</th> <th>Adresse</th>
<td>{{ assooptions.adresse1 }}<br> <td>{{ assooptions.adresse1 }}<br>
{{ assooptions.adresse2 }}</td> {{ assooptions.adresse2 }}</td>
<th>Contact mail</th> <th>Contact mail</th>
<td>{{ assooptions.contact }}</td> <td>{{ assooptions.contact }}</td>
</tr> </tr>
<tr> <tr>
<th>Telephone</th> <th>Telephone</th>
<td>{{ assooptions.telephone }}</td> <td>{{ assooptions.telephone }}</td>
<th>Pseudo d'usage</th> <th>Pseudo d'usage</th>
<td>{{ assooptions.pseudo }}</td> <td>{{ assooptions.pseudo }}</td>
</tr> </tr>
<tr> <tr>
<th>Objet utilisateur de l'association</th> <th>Objet utilisateur de l'association</th>
<td>{{ assooptions.utilisateur_asso }}</td> <td>{{ assooptions.utilisateur_asso }}</td>
<th>Description de l'association</th> <th>Description de l'association</th>
<td>{{ assooptions.description | safe }}</td> <td>{{ assooptions.description | safe }}</td>
</tr> </tr>
</table> </table>
<h4>Messages personalisé dans les mails</h4> <h4>Messages personalisé dans les mails</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i> Editer
Editer
</a> </a>
<p>
</p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Mail de bienvenue (Français)</th> <th>Mail de bienvenue (Français)</th>
<td>{{ mailmessageoptions.welcome_mail_fr | safe }}</td> <td>{{ mailmessageoptions.welcome_mail_fr | safe }}</td>
</tr> </tr>
<tr> <tr>
<th>Mail de bienvenue (Anglais)</th> <th>Mail de bienvenue (Anglais)</th>
<td>{{ mailmessageoptions.welcome_mail_en | safe }}</td> <td>{{ mailmessageoptions.welcome_mail_en | safe }}</td>
</tr> </tr>
</table> </table>
<h2>Liste des services et préférences page d'accueil</h2>
<h4>Liste des services et préférences page d'accueil</h4>
{% can_create preferences.Service%} {% can_create preferences.Service%}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus"></i> Ajouter un service</a> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:add-service' %}"><i class="fa fa-plus">
</i> Ajouter un service
</a>
{% acl_end %} {% acl_end %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-services' %}"><i class="fa fa-trash"></i> Supprimer un ou plusieurs service</a> <a class="btn btn-danger btn-sm" role="button" href="{% url 'preferences:del-services' %}"><i class="fa fa-trash">
</i> Supprimer un ou plusieurs service
</a>
{% include "preferences/aff_service.html" with service_list=service_list %} {% include "preferences/aff_service.html" with service_list=service_list %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}"> <a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i> Editer
Editer
</a> </a>
<p>
<table class="table table-striped"> <table class="table table-striped">
<tr> <tr>
<th>Url du compte twitter</th> <th>Url du compte twitter</th>
<td>{{ homeoptions.twitter_url }}</td> <td>{{ homeoptions.twitter_url }}</td>
<th>Nom utilisé pour afficher le compte</th> <th>Nom utilisé pour afficher le compte</th>
<td>{{ homeoptions.twitter_account_name }}</td> <td>{{ homeoptions.twitter_account_name }}</td>
</tr> </tr>
<tr> <tr>
<th>Url du compte facebook</th> <th>Url du compte facebook</th>
<td>{{ homeoptions.facebook_url }}</td> <td>{{ homeoptions.facebook_url }}</td>
</tr> </tr>
</table> </table>
<br />
<br />
<br />
{% endblock %} {% endblock %}

View file

@ -34,6 +34,7 @@ from reversion.admin import VersionAdmin
from .models import ( from .models import (
User, User,
EMailAddress,
ServiceUser, ServiceUser,
School, School,
ListRight, ListRight,
@ -108,6 +109,11 @@ class BanAdmin(VersionAdmin):
pass pass
class EMailAddressAdmin(VersionAdmin):
"""Gestion des alias mail"""
pass
class WhitelistAdmin(VersionAdmin): class WhitelistAdmin(VersionAdmin):
"""Gestion des whitelist""" """Gestion des whitelist"""
pass pass
@ -126,6 +132,8 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
'pseudo', 'pseudo',
'surname', 'surname',
'email', 'email',
'local_email_redirect',
'local_email_enabled',
'school', 'school',
'is_admin', 'is_admin',
'shell' 'shell'
@ -211,6 +219,7 @@ admin.site.register(School, SchoolAdmin)
admin.site.register(ListRight, ListRightAdmin) admin.site.register(ListRight, ListRightAdmin)
admin.site.register(ListShell, ListShellAdmin) admin.site.register(ListShell, ListShellAdmin)
admin.site.register(Ban, BanAdmin) admin.site.register(Ban, BanAdmin)
admin.site.register(EMailAddress, EMailAddressAdmin)
admin.site.register(Whitelist, WhitelistAdmin) admin.site.register(Whitelist, WhitelistAdmin)
admin.site.register(Request, RequestAdmin) admin.site.register(Request, RequestAdmin)
# Now register the new UserAdmin... # Now register the new UserAdmin...

View file

@ -53,6 +53,7 @@ from .models import (
School, School,
ListRight, ListRight,
Whitelist, Whitelist,
EMailAddress,
ListShell, ListShell,
Ban, Ban,
Adherent, Adherent,
@ -219,7 +220,7 @@ class UserChangeForm(FormRevMixin, forms.ModelForm):
class Meta: class Meta:
model = Adherent model = Adherent
fields = ('pseudo', 'password', 'surname', 'email') fields = ('pseudo', 'password', 'surname')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__) prefix = kwargs.pop('prefix', self.Meta.model.__name__)
@ -312,7 +313,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
'name', 'name',
'surname', 'surname',
'pseudo', 'pseudo',
'email',
'school', 'school',
'comment', 'comment',
'room', 'room',
@ -364,7 +364,6 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
fields = [ fields = [
'surname', 'surname',
'pseudo', 'pseudo',
'email',
'school', 'school',
'comment', 'comment',
'room', 'room',
@ -590,3 +589,39 @@ class WhitelistForm(FormRevMixin, ModelForm):
model = Whitelist model = Whitelist
exclude = ['user'] exclude = ['user']
widgets = {'date_end':DateTimePicker} widgets = {'date_end':DateTimePicker}
class EMailAddressForm(FormRevMixin, ModelForm):
"""Create and edit a local email address"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EMailAddressForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['local_part'].label = "Local part of the email"
self.fields['local_part'].help_text = "Can't contain @"
class Meta:
model = EMailAddress
exclude = ['user']
class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
"""Edit email-related settings"""
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
super(EmailSettingsForm, self).__init__(*args, prefix=prefix, **kwargs)
self.fields['email'].label = "Contact email address"
if 'local_email_redirect' in self.fields:
self.fields['local_email_redirect'].label = "Redirect local emails"
self.fields['local_email_redirect'].help_text = (
"Enable the automated redirection of the local email address "
"to the contact email address"
)
if 'local_email_enabled' in self.fields:
self.fields['local_email_enabled'].label = "Use local emails"
self.fields['local_email_enabled'].help_text = (
"Enable the use of the local email account"
)
class Meta:
model = User
fields = ['email', 'local_email_redirect', 'local_email_enabled']

View file

@ -0,0 +1,57 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2018-06-29 14:14
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import re2o.mixins
class Migration(migrations.Migration):
def create_initial_email_address(apps, schema_editor):
db_alias = schema_editor.connection.alias
User = apps.get_model("users", "User")
EMailAddress = apps.get_model("users", "EMailAddress")
users = User.objects.using(db_alias).all()
for user in users:
EMailAddress.objects.using(db_alias).create(
local_part=user.pseudo,
user=user
)
def delete_all_email_address(apps, schema_editor):
db_alias = schema_editor.connection.alias
EMailAddress = apps.get_model("users", "EMailAddress")
EMailAddress.objects.using(db_alias).delete()
dependencies = [
('users', '0072_auto_20180426_2021'),
]
operations = [
migrations.CreateModel(
name='EMailAddress',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('local_part', models.CharField(help_text="Local part of the email address", max_length=128, unique=True)),
('user', models.ForeignKey(help_text='User of the local email', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model),
options={'permissions': (('view_emailaddress', 'Can see a local email account object'),), 'verbose_name': 'Local email account', 'verbose_name_plural': 'Local email accounts'},
),
migrations.AddField(
model_name='user',
name='local_email_enabled',
field=models.BooleanField(default=False, help_text="Wether or not to enable the local email account."),
),
migrations.AddField(
model_name='user',
name='local_email_redirect',
field=models.BooleanField(default=False, help_text='Whether or not to redirect the local email messages to the main email.'),
),
migrations.RunPython(create_initial_email_address,
delete_all_email_address),
]

176
users/models.py Normal file → Executable file
View file

@ -148,7 +148,7 @@ class UserManager(BaseUserManager):
pseudo=pseudo, pseudo=pseudo,
surname=surname, surname=surname,
name=surname, name=surname,
email=self.normalize_email(email), email=self.normalize_email(mail),
) )
user.set_password(password) user.set_password(password)
@ -195,6 +195,14 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
validators=[linux_user_validator] validators=[linux_user_validator]
) )
email = models.EmailField() email = models.EmailField()
local_email_redirect = models.BooleanField(
default=False,
help_text="Whether or not to redirect the local email messages to the main email."
)
local_email_enabled = models.BooleanField(
default=False,
help_text="Wether or not to enable the local email account."
)
school = models.ForeignKey( school = models.ForeignKey(
'School', 'School',
on_delete=models.PROTECT, on_delete=models.PROTECT,
@ -674,6 +682,13 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
self.pwd_ntlm = hashNT(password) self.pwd_ntlm = hashNT(password)
return return
@cached_property
def email_address(self):
if (OptionalUser.get_cached_value('local_email_accounts_enabled')
and self.local_email_enabled):
return self.emailaddress_set.all()
return EMailAddress.objects.none()
def get_next_domain_name(self): def get_next_domain_name(self):
"""Look for an available name for a new interface for """Look for an available name for a new interface for
this user by trying "pseudo0", "pseudo1", "pseudo2", ... this user by trying "pseudo0", "pseudo1", "pseudo2", ...
@ -792,6 +807,32 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
"Droit requis pour changer le shell" "Droit requis pour changer le shell"
) )
@staticmethod
def can_change_local_email_redirect(user_request, *_args, **_kwargs):
""" Check if a user can change local_email_redirect.
:param user_request: The user who request
:returns: a message and a boolean which is True if the user has
the right to change a redirection
"""
return (
OptionalUser.get_cached_value('local_email_accounts_enabled'),
"La gestion des comptes mails doit être activée"
)
@staticmethod
def can_change_local_email_enabled(user_request, *_args, **_kwargs):
""" Check if a user can change internal address .
:param user_request: The user who request
:returns: a message and a boolean which is True if the user has
the right to change internal address
"""
return (
OptionalUser.get_cached_value('local_email_accounts_enabled'),
"La gestion des comptes mails doit être activée"
)
@staticmethod @staticmethod
def can_change_force(user_request, *_args, **_kwargs): def can_change_force(user_request, *_args, **_kwargs):
""" Check if a user can change a force """ Check if a user can change a force
@ -886,9 +927,19 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
'shell': self.can_change_shell, 'shell': self.can_change_shell,
'force': self.can_change_force, 'force': self.can_change_force,
'selfpasswd': self.check_selfpasswd, 'selfpasswd': self.check_selfpasswd,
'local_email_redirect': self.can_change_local_email_redirect,
'local_email_enabled' : self.can_change_local_email_enabled,
} }
self.__original_state = self.state self.__original_state = self.state
def clean(self, *args, **kwargs):
"""Check if this pseudo is already used by any mailalias.
Better than raising an error in post-save and catching it"""
if (EMailAddress.objects
.filter(local_part=self.pseudo)
.exclude(user=self)):
raise ValidationError("This pseudo is already in use.")
def __str__(self): def __str__(self):
return self.pseudo return self.pseudo
@ -1011,9 +1062,11 @@ class Club(User):
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def user_post_save(**kwargs): def user_post_save(**kwargs):
""" Synchronisation post_save : envoie le mail de bienvenue si creation """ Synchronisation post_save : envoie le mail de bienvenue si creation
Synchronise le pseudo, en créant un alias mail correspondant
Synchronise le ldap""" Synchronise le ldap"""
is_created = kwargs['created'] is_created = kwargs['created']
user = kwargs['instance'] user = kwargs['instance']
EMailAddress.objects.get_or_create(local_part=user.pseudo, user=user)
if is_created: if is_created:
user.notif_inscription() user.notif_inscription()
user.state_sync() user.state_sync()
@ -1593,3 +1646,124 @@ class LdapServiceUserGroup(ldapdb.models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
class EMailAddress(RevMixin, AclMixin, models.Model):
"""Defines a local email account for a user
"""
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
help_text="User of the local email",
)
local_part = models.CharField(
unique=True,
max_length=128,
help_text="Local part of the email address"
)
class Meta:
permissions = (
("view_emailaddress", "Can see a local email account object"),
)
verbose_name = "Local email account"
verbose_name_plural = "Local email accounts"
def __str__(self):
return self.local_part + OptionalUser.get_cached_value('local_email_domain')
@cached_property
def complete_email_address(self):
return self.local_part + OptionalUser.get_cached_value('local_email_domain')
@staticmethod
def can_create(user_request, userid, *_args, **_kwargs):
"""Check if a user can create a `EMailAddress` object.
Args:
user_request: The user who wants to create the object.
userid: The id of the user to whom the account is to be created
Returns:
a message and a boolean which is True if the user can create
a local email account.
"""
if user_request.has_perm('users.add_emailaddress'):
return True, None
if not OptionalUser.get_cached_value('local_email_accounts_enabled'):
return False, "The local email accounts are not enabled."
if int(user_request.id) != int(userid):
return False, "You don't have the right to add a local email account to another user."
elif user_request.email_address.count() >= OptionalUser.get_cached_value('max_email_address'):
return False, "You have reached the limit of {} local email account.".format(
OptionalUser.get_cached_value('max_email_address')
)
return True, None
def can_view(self, user_request, *_args, **_kwargs):
"""Check if a user can view the local email account
Args:
user_request: The user who wants to view the object.
Returns:
a message and a boolean which is True if the user can see
the local email account.
"""
if user_request.has_perm('users.view_emailaddress'):
return True, None
if not OptionalUser.get_cached_value('local_email_accounts_enabled'):
return False, "The local email accounts are not enabled."
if user_request == self.user:
return True, None
return False, "You don't have the right to edit someone else's local email account."
def can_delete(self, user_request, *_args, **_kwargs):
"""Check if a user can delete the alias
Args:
user_request: The user who wants to delete the object.
Returns:
a message and a boolean which is True if the user can delete
the local email account.
"""
if self.local_part == self.user.pseudo:
return False, ("You cannot delete a local email account whose "
"local part is the same as the username.")
if user_request.has_perm('users.delete_emailaddress'):
return True, None
if not OptionalUser.get_cached_value('local_email_accounts_enabled'):
return False, "The local email accounts are not enabled."
if user_request == self.user:
return True, None
return False, ("You don't have the right to delete someone else's "
"local email account")
def can_edit(self, user_request, *_args, **_kwargs):
"""Check if a user can edit the alias
Args:
user_request: The user who wants to edit the object.
Returns:
a message and a boolean which is True if the user can edit
the local email account.
"""
if self.local_part == self.user.pseudo:
return False, ("You cannot edit a local email account whose "
"local part is the same as the username.")
if user_request.has_perm('users.change_emailaddress'):
return True, None
if not OptionalUser.get_cached_value('local_email_accounts_enabled'):
return False, "The local email accounts are not enabled."
if user_request == self.user:
return True, None
return False, ("You don't have the right to edit someone else's "
"local email account")
def clean(self, *args, **kwargs):
if "@" in self.local_part:
raise ValidationError("The local part cannot contain a @")
super(EMailAddress, self).clean(*args, **kwargs)

View file

@ -0,0 +1,56 @@
{% 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 acl %}
{% load logs_extra %}
{% if emailaddress_list.paginator %}
{% include "pagination.html" with list=emailaddress_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>Email address</th>
<th></th>
</tr>
</thead>
{% for emailaddress in emailaddress_list %}
<td>{{ emailaddress.complete_email_address }}</td>
<td class="text-right">
{% can_delete emailaddress %}
{% include 'buttons/suppr.html' with href='users:del-emailaddress' id=emailaddress.id %}
{% acl_end %}
{% can_edit emailaddress %}
{% include 'buttons/edit.html' with href='users:edit-emailaddress' id=emailaddress.id %}
{% acl_end %}
{% history_button emailaddress %}
</td>
</tr>
{% endfor %}
</table>
{% if emailaddress_list.paginator %}
{% include "pagination.html" with list=emailaddress_list %}
{% endif %}

View file

@ -0,0 +1,34 @@
{% extends "users/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 %}
{% block title %}Local email accounts{% endblock %}
{% block content %}
<h2>Local email accounts</h2>
{% include "users/aff_emailaddress.html" with emailaddress_list=emailaddress_list %}
{% endblock %}

View file

@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% load bootstrap3 %} {% load bootstrap3 %}
{% load acl %} {% load acl %}
{% load logs_extra %} {% load logs_extra %}
{% load design %}
{% block title %}Profil{% endblock %} {% block title %}Profil{% endblock %}
@ -406,8 +407,58 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</div> </div>
</div> </div>
</div> </div>
<div class="panel panel-default">
<div class="panel-heading clearfix" data-parent="#accordion" data-toggle="collapse" data-target="#collapse7">
<h3 class="panel-title pull-left">
<i class="fa fa-envelope"></i> Email settings
</h3>
</div>
<div id="collapse7" class="panel-collapse collapse">
<div class="panel-body">
{% can_edit users %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-email-settings' users.id %}">
<i class="fa fa-pencil-alt"></i> Edit email settings
</a>
{% acl_end %}
</div>
<div class="panel-body">
{% if local_email_accounts_enabled %}
<div class="table-responsive">
<table class="table">
<tr>
<th colspan="2">Contact email address</th>
<td colspan="2">{{ users.email }}</td>
</tr>
<tr>
<th>Enable the local email account</th>
<td>{{ users.local_email_enabled | tick }}</td>
<th>Enable the local email redirection</th>
<td>{{ users.local_email_redirect | tick }}</td>
</tr>
</table>
</div>
{% if users.local_email_enabled %}
{% can_create EMailAddress users.id %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:add-emailaddress' users.id %}">
<i class="fa fa-plus-square"></i> Add an email address
</a>
{% acl_end %}
{% if emailaddress_list %}
{% include "users/aff_emailaddress.html" with emailaddress_list=emailaddress_list %}
{% endif %}
{% endif %}
{% else %}
<div class="table-responsive">
<table class="table">
<tr>
<th>Contact email address</th>
<td>{{ users.email }}</td>
</tr>
</table>
</div>
{% endif %}
</div>
</div>
</div>
</div> </div>
<br />
<br />
<br />
{% endblock %} {% endblock %}

View file

@ -64,6 +64,18 @@ urlpatterns = [
url(r'^del_whitelist/(?P<whitelistid>[0-9]+)$', url(r'^del_whitelist/(?P<whitelistid>[0-9]+)$',
views.del_whitelist, views.del_whitelist,
name='del-whitelist'), name='del-whitelist'),
url(r'^add_emailaddress/(?P<userid>[0-9]+)$',
views.add_emailaddress,
name='add-emailaddress'),
url(r'^edit_emailaddress/(?P<emailaddressid>[0-9]+)$',
views.edit_emailaddress,
name='edit-emailaddress'),
url(r'^del_emailaddress/(?P<emailaddressid>[0-9]+)$',
views.del_emailaddress,
name='del-emailaddress'),
url(r'^edit_email_settings/(?P<userid>[0-9]+)$',
views.edit_email_settings,
name='edit-email-settings'),
url(r'^add_school/$', views.add_school, name='add-school'), url(r'^add_school/$', views.add_school, name='add-school'),
url(r'^edit_school/(?P<schoolid>[0-9]+)$', url(r'^edit_school/(?P<schoolid>[0-9]+)$',
views.edit_school, views.edit_school,

View file

@ -81,10 +81,13 @@ from .models import (
Adherent, Adherent,
Club, Club,
ListShell, ListShell,
EMailAddress,
) )
from .forms import ( from .forms import (
BanForm, BanForm,
WhitelistForm, WhitelistForm,
EMailAddressForm,
EmailSettingsForm,
DelSchoolForm, DelSchoolForm,
DelListRightForm, DelListRightForm,
NewListRightForm, NewListRightForm,
@ -492,6 +495,101 @@ def del_whitelist(request, whitelist, **_kwargs):
) )
@login_required
@can_create(EMailAddress)
@can_edit(User)
def add_emailaddress(request, user, userid):
""" Create a new local email account"""
emailaddress_instance = EMailAddress(user=user)
emailaddress = EMailAddressForm(
request.POST or None,
instance=emailaddress_instance
)
if emailaddress.is_valid():
emailaddress.save()
messages.success(request, "Local email account created")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(userid)}
))
return form(
{'userform': emailaddress,
'showCGU': False,
'action_name': 'Add a local email account'},
'users/user.html',
request
)
@login_required
@can_edit(EMailAddress)
def edit_emailaddress(request, emailaddress_instance, **_kwargs):
""" Edit a local email account"""
emailaddress = EMailAddressForm(
request.POST or None,
instance=emailaddress_instance
)
if emailaddress.is_valid():
if emailaddress.changed_data:
emailaddress.save()
messages.success(request, "Local email account modified")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(emailaddress_instance.user.id)}
))
return form(
{'userform': emailaddress,
'showCGU': False,
'action_name': 'Edit a local email account'},
'users/user.html',
request
)
@login_required
@can_delete(EMailAddress)
def del_emailaddress(request, emailaddress, **_kwargs):
"""Delete a local email account"""
if request.method == "POST":
emailaddress.delete()
messages.success(request, "Local email account deleted")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(emailaddress.user.id)}
))
return form(
{'objet': emailaddress, 'objet_name': 'emailaddress'},
'users/delete.html',
request
)
@login_required
@can_edit(User)
def edit_email_settings(request, user_instance, **_kwargs):
"""Edit the email settings of a user"""
email_settings = EmailSettingsForm(
request.POST or None,
instance=user_instance,
user=request.user
)
if email_settings.is_valid():
if email_settings.changed_data:
email_settings.save()
messages.success(request, "Email settings updated")
return redirect(reverse(
'users:profil',
kwargs={'userid': str(user_instance.id)}
))
return form(
{'userform': email_settings,
'showCGU': False,
'action_name': 'Edit the email settings'},
'users/user.html',
request
)
@login_required @login_required
@can_create(School) @can_create(School)
def add_school(request): def add_school(request):
@ -914,7 +1012,11 @@ def profil(request, users, **_kwargs):
'white_list': whitelists, 'white_list': whitelists,
'user_solde': user_solde, 'user_solde': user_solde,
'solde_activated': Paiement.objects.filter(is_balance=True).exists(), 'solde_activated': Paiement.objects.filter(is_balance=True).exists(),
'asso_name': AssoOption.objects.first().name 'asso_name': AssoOption.objects.first().name,
'emailaddress_list': users.email_address,
'local_email_accounts_enabled': (
OptionalUser.objects.first().local_email_accounts_enabled
)
} }
) )