Commit 90674d49 authored by chirac's avatar chirac

Merge branch 'mail_account' into 'master'

Mail account

See merge request federez/re2o!199
parents 3837084e bc0abb2c
......@@ -495,7 +495,8 @@ class UserSerializer(NamespacedHMSerializer):
class Meta:
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',
'end_access', 'uid', 'class_name', 'api_url')
extra_kwargs = {
......@@ -512,7 +513,8 @@ class ClubSerializer(NamespacedHMSerializer):
class Meta:
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',
'access', 'end_access', 'administrators', 'members',
'mailing', 'uid', 'api_url')
......@@ -529,9 +531,10 @@ class AdherentSerializer(NamespacedHMSerializer):
class Meta:
model = users.Adherent
fields = ('name', 'surname', 'pseudo', 'email', 'school', 'shell',
'comment', 'state', 'registered', 'telephone', 'room',
'solde', 'access', 'end_access', 'uid', 'api_url')
fields = ('name', 'surname', 'pseudo', 'email', 'local_email_redirect',
'local_email_enabled', 'school', 'shell', 'comment',
'state', 'registered', 'telephone', 'room', 'solde',
'access', 'end_access', 'uid', 'api_url')
extra_kwargs = {
'shell': {'view_name': 'shell-detail'}
}
......@@ -593,6 +596,15 @@ class WhitelistSerializer(NamespacedHMSerializer):
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
......@@ -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
......
......@@ -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/ban', views.BanViewSet)
router.register_viewset(r'users/whitelist', views.WhitelistViewSet)
router.register_viewset(r'users/emailaddress', views.EMailAddressViewSet)
# SERVICE REGEN
router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name='serviceregen')
# DHCP
router.register_view(r'dhcp/hostmacip', views.HostMacIpView),
# LOCAL EMAILS
router.register_view(r'localemail/users', views.LocalEmailUsersView),
# DNS
router.register_view(r'dns/zones', views.DNSZonesView),
# MAILING
......
......@@ -469,6 +469,21 @@ class WhitelistViewSet(viewsets.ReadOnlyModelViewSet):
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
......@@ -489,8 +504,26 @@ class ServiceRegenViewSet(viewsets.ModelViewSet):
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
class HostMacIpView(generics.ListAPIView):
"""Exposes the associations between hostname, mac address and IPv4 in
order to build the DHCP lease files.
......@@ -501,6 +534,7 @@ class HostMacIpView(generics.ListAPIView):
# DNS
class DNSZonesView(generics.ListAPIView):
"""Exposes the detailed information about each extension (hostnames,
IPs, DNS records, etc.) in order to build the DNS zone files.
......
# -*- 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'),
),
]
......@@ -30,6 +30,7 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.core.cache import cache
from django.forms import ValidationError
import machines.models
from re2o.mixins import AclMixin
......@@ -83,12 +84,32 @@ class OptionalUser(AclMixin, PreferencesModel):
blank=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:
permissions = (
("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)
def optionaluser_post_save(**kwargs):
......
......@@ -32,11 +32,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% block content %}
<h4>Préférences utilisateur</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalUser' %}">
<i class="fa fa-edit"></i>
Editer
<i class="fa fa-edit"></i> Editer
</a>
<p>
</p>
<h5>Généralités</h5>
<table class="table table-striped">
<tr>
<th>Téléphone obligatoirement requis</th>
......@@ -58,14 +57,25 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</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>
<h4>Préférences machines</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalMachine' %}">
<i class="fa fa-edit"></i>
Editer
<i class="fa fa-edit"></i> Editer
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>Mot de passe par machine</th>
......@@ -84,18 +94,22 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ machineoptions.create_machine|tick }}</td>
</tr>
</table>
<h4>Préférences topologie</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'OptionalTopologie' %}">
<i class="fa fa-edit"></i>
Editer
<i class="fa fa-edit"></i> Editer
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>Politique générale de placement de vlan</th>
<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>
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>
<td></td>
</tr>
<tr>
......@@ -105,13 +119,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ topologieoptions.vlan_decision_nok }}</td>
</tr>
</table>
<h4>Préférences generales</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'GeneralOption' %}">
<i class="fa fa-edit"></i>
Editer
<i class="fa fa-edit"></i> Editer
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>Nom du site web</th>
......@@ -142,13 +155,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{generaloptions.GTU}}</th>
</tr>
</table>
<h4>Données de l'association</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'AssoOption' %}">
<i class="fa fa-edit"></i>
Editer
<i class="fa fa-edit"></i> Editer
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>Nom</th>
......@@ -175,15 +187,13 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Description de l'association</th>
<td>{{ assooptions.description | safe }}</td>
</tr>
</table>
<h4>Messages personalisé dans les mails</h4>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'MailMessageOption' %}">
<i class="fa fa-edit"></i>
Editer
<i class="fa fa-edit"></i> Editer
</a>
<p>
</p>
<table class="table table-striped">
<tr>
<th>Mail de bienvenue (Français)</th>
......@@ -194,19 +204,22 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ mailmessageoptions.welcome_mail_en | safe }}</td>
</tr>
</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%}
<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 %}
<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 %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'preferences:edit-options' 'HomeOption' %}">
<i class="fa fa-edit"></i>
Editer
<i class="fa fa-edit"></i> Editer
</a>
<p>
<table class="table table-striped">
<tr>
<th>Url du compte twitter</th>
......@@ -219,7 +232,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ homeoptions.facebook_url }}</td>
</tr>
</table>
<br />
<br />
<br />
{% endblock %}
......@@ -34,6 +34,7 @@ from reversion.admin import VersionAdmin
from .models import (
User,
EMailAddress,
ServiceUser,
School,
ListRight,
......@@ -108,6 +109,11 @@ class BanAdmin(VersionAdmin):
pass
class EMailAddressAdmin(VersionAdmin):
"""Gestion des alias mail"""
pass
class WhitelistAdmin(VersionAdmin):
"""Gestion des whitelist"""
pass
......@@ -126,6 +132,8 @@ class UserAdmin(VersionAdmin, BaseUserAdmin):
'pseudo',
'surname',
'email',
'local_email_redirect',
'local_email_enabled',
'school',
'is_admin',
'shell'
......@@ -211,6 +219,7 @@ admin.site.register(School, SchoolAdmin)
admin.site.register(ListRight, ListRightAdmin)
admin.site.register(ListShell, ListShellAdmin)
admin.site.register(Ban, BanAdmin)
admin.site.register(EMailAddress, EMailAddressAdmin)
admin.site.register(Whitelist, WhitelistAdmin)
admin.site.register(Request, RequestAdmin)
# Now register the new UserAdmin...
......
......@@ -53,6 +53,7 @@ from .models import (
School,
ListRight,
Whitelist,
EMailAddress,
ListShell,
Ban,
Adherent,
......@@ -219,7 +220,7 @@ class UserChangeForm(FormRevMixin, forms.ModelForm):
class Meta:
model = Adherent
fields = ('pseudo', 'password', 'surname', 'email')
fields = ('pseudo', 'password', 'surname')
def __init__(self, *args, **kwargs):
prefix = kwargs.pop('prefix', self.Meta.model.__name__)
......@@ -312,7 +313,6 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
'name',
'surname',
'pseudo',
'email',
'school',
'comment',
'room',
......@@ -364,7 +364,6 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
fields = [
'surname',
'pseudo',
'email',
'school',
'comment',
'room',
......@@ -590,3 +589,39 @@ class WhitelistForm(FormRevMixin, ModelForm):
model = Whitelist
exclude = ['user']
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']
# -*- 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),
]
......@@ -148,7 +148,7 @@ class UserManager(BaseUserManager):
pseudo=pseudo,
surname=surname,
name=surname,
email=self.normalize_email(email),
email=self.normalize_email(mail),
)
user.set_password(password)
......@@ -195,6 +195,14 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
validators=[linux_user_validator]
)
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',
on_delete=models.PROTECT,
......@@ -674,6 +682,13 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
self.pwd_ntlm = hashNT(password)
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):
"""Look for an available name for a new interface for
this user by trying "pseudo0", "pseudo1", "pseudo2", ...
......@@ -792,6 +807,32 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
"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
def can_change_force(user_request, *_args, **_kwargs):
""" Check if a user can change a force
......@@ -886,9 +927,19 @@ class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
'shell': self.can_change_shell,
'force': self.can_change_force,
'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
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):
return self.pseudo
......@@ -1011,9 +1062,11 @@ class Club(User):
@receiver(post_save, sender=User)
def user_post_save(**kwargs):
""" Synchronisation post_save : envoie le mail de bienvenue si creation
Synchronise le pseudo, en créant un alias mail correspondant
Synchronise le ldap"""
is_created = kwargs['created']
user = kwargs['instance']
EMailAddress.objects.get_or_create(local_part=user.pseudo, user=user)
if is_created:
user.notif_inscription()
user.state_sync()
......@@ -1593,3 +1646,124 @@ class LdapServiceUserGroup(ldapdb.models.Model):
def __str__(self):
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")