diff --git a/api/serializers.py b/api/serializers.py index c8cdffd9..398f2b19 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -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 diff --git a/api/urls.py b/api/urls.py index 67302789..7ee36073 100644 --- a/api/urls.py +++ b/api/urls.py @@ -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 diff --git a/api/views.py b/api/views.py index 7b01b0c3..ef083edf 100644 --- a/api/views.py +++ b/api/views.py @@ -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. diff --git a/preferences/migrations/0046_optionaluser_mail_extension.py b/preferences/migrations/0046_optionaluser_mail_extension.py new file mode 100644 index 00000000..9de1608a --- /dev/null +++ b/preferences/migrations/0046_optionaluser_mail_extension.py @@ -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'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index d39b9f8e..c2d6aa74 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -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): diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html old mode 100755 new mode 100644 index 34417695..a3f2dbc3 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -32,194 +32,204 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

Préférences utilisateur

- - Editer + Editer -

-

+ +
Généralités
- - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + +
Téléphone obligatoirement requis{{ useroptions.is_tel_mandatory|tick }}Auto inscription{{ useroptions.self_adhesion|tick }}
Champ gpg fingerprint{{ useroptions.gpg_fingerprint|tick }}Shell par défaut des utilisateurs{{ useroptions.shell_default }}
Creations d'adhérents par tous{{ useroptions.all_can_create_adherent|tick }}Creations de clubs par tous{{ useroptions.all_can_create_club|tick }}
Téléphone obligatoirement requis{{ useroptions.is_tel_mandatory|tick }}Auto inscription{{ useroptions.self_adhesion|tick }}
Champ gpg fingerprint{{ useroptions.gpg_fingerprint|tick }}Shell par défaut des utilisateurs{{ useroptions.shell_default }}
Creations d'adhérents par tous{{ useroptions.all_can_create_adherent|tick }}Creations de clubs par tous{{ useroptions.all_can_create_club|tick }}
+ +
Comptes mails
+ + + + + + + + + + +
Gestion des comptes mails{{ useroptions.local_email_accounts_enabled | tick }}Extension mail interne{{ useroptions.local_email_domain }}
Nombre d'alias mail max{{ useroptions.max_email_address }}

Préférences machines

- - Editer + Editer -

-

- - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + +
Mot de passe par machine{{ machineoptions.password_machine|tick }}Machines/interfaces autorisées par utilisateurs{{ machineoptions.max_lambdauser_interfaces }}
Alias dns autorisé par utilisateur{{ machineoptions.max_lambdauser_aliases }}Support de l'ipv6{{ machineoptions.ipv6_mode }}
Creation de machines{{ machineoptions.create_machine|tick }}
Mot de passe par machine{{ machineoptions.password_machine|tick }}Machines/interfaces autorisées par utilisateurs{{ machineoptions.max_lambdauser_interfaces }}
Alias dns autorisé par utilisateur{{ machineoptions.max_lambdauser_aliases }}Support de l'ipv6{{ machineoptions.ipv6_mode }}
Creation de machines{{ machineoptions.create_machine|tick }}
+ +

Préférences topologie

- - Editer + Editer -

-

- - - - - - - - - - - - + + + + + + + + + + + +
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"
Vlan où placer les machines après acceptation RADIUS{{ topologieoptions.vlan_decision_ok }}Vlan où placer les machines après rejet RADIUS{{ topologieoptions.vlan_decision_nok }}
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" +
Vlan où placer les machines après acceptation RADIUS{{ topologieoptions.vlan_decision_ok }}Vlan où placer les machines après rejet RADIUS{{ topologieoptions.vlan_decision_nok }}
+ +

Préférences generales

- - Editer + Editer -

-

- - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nom du site web{{ generaloptions.site_name }}Adresse mail d'expedition automatique{{ generaloptions.email_from }}
Affichage de résultats dans le champ de recherche{{ generaloptions.search_display_page }}Nombre d'items affichés en liste (taille normale){{ generaloptions.pagination_number }}
Nombre d'items affichés en liste (taille élevée){{ generaloptions.pagination_large_number }}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}} -
Nom du site web{{ generaloptions.site_name }}Adresse mail d'expedition automatique{{ generaloptions.email_from }}
Affichage de résultats dans le champ de recherche{{ generaloptions.search_display_page }}Nombre d'items affichés en liste (taille normale){{ generaloptions.pagination_number }}
Nombre d'items affichés en liste (taille élevée){{ generaloptions.pagination_large_number }}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}} +
+ +

Données de l'association

- - Editer + Editer -

-

- - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + +
Nom{{ assooptions.name }}SIRET{{ assooptions.siret }}
Adresse{{ assooptions.adresse1 }}
- {{ assooptions.adresse2 }}
Contact mail{{ assooptions.contact }}
Telephone{{ assooptions.telephone }}Pseudo d'usage{{ assooptions.pseudo }}
Objet utilisateur de l'association{{ assooptions.utilisateur_asso }}Description de l'association{{ assooptions.description | safe }}
Nom{{ assooptions.name }}SIRET{{ assooptions.siret }}
Adresse{{ assooptions.adresse1 }}
+ {{ assooptions.adresse2 }}
Contact mail{{ assooptions.contact }}
Telephone{{ assooptions.telephone }}Pseudo d'usage{{ assooptions.pseudo }}
Objet utilisateur de l'association{{ assooptions.utilisateur_asso }}Description de l'association{{ assooptions.description | safe }}
+ +

Messages personalisé dans les mails

- - Editer + Editer -

-

- - - - - - - - + + + + + + + +
Mail de bienvenue (Français){{ mailmessageoptions.welcome_mail_fr | safe }}
Mail de bienvenue (Anglais){{ mailmessageoptions.welcome_mail_en | safe }}
Mail de bienvenue (Français){{ mailmessageoptions.welcome_mail_fr | safe }}
Mail de bienvenue (Anglais){{ mailmessageoptions.welcome_mail_en | safe }}
-

Liste des services et préférences page d'accueil

+ + +

Liste des services et préférences page d'accueil

{% can_create preferences.Service%} - Ajouter un service + + Ajouter un service + {% acl_end %} - Supprimer un ou plusieurs service + + Supprimer un ou plusieurs service + {% include "preferences/aff_service.html" with service_list=service_list %} - - Editer + Editer -

- - - - - - - - - - - + + + + + + + + + +
Url du compte twitter{{ homeoptions.twitter_url }}Nom utilisé pour afficher le compte{{ homeoptions.twitter_account_name }}
Url du compte facebook{{ homeoptions.facebook_url }}
Url du compte twitter{{ homeoptions.twitter_url }}Nom utilisé pour afficher le compte{{ homeoptions.twitter_account_name }}
Url du compte facebook{{ homeoptions.facebook_url }}
-
-
-
{% endblock %} diff --git a/users/admin.py b/users/admin.py index a902d2e2..e7dd3240 100644 --- a/users/admin.py +++ b/users/admin.py @@ -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... diff --git a/users/forms.py b/users/forms.py index 670fbb98..cbffd961 100644 --- a/users/forms.py +++ b/users/forms.py @@ -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'] diff --git a/users/migrations/0073_auto_20180629_1614.py b/users/migrations/0073_auto_20180629_1614.py new file mode 100644 index 00000000..72ebffe6 --- /dev/null +++ b/users/migrations/0073_auto_20180629_1614.py @@ -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), + ] + diff --git a/users/models.py b/users/models.py old mode 100644 new mode 100755 index 067a0346..bdc37142 --- a/users/models.py +++ b/users/models.py @@ -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") + + 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) + diff --git a/users/templates/users/aff_emailaddress.html b/users/templates/users/aff_emailaddress.html new file mode 100644 index 00000000..14156ac9 --- /dev/null +++ b/users/templates/users/aff_emailaddress.html @@ -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 %} + + + + + + + + + {% for emailaddress in emailaddress_list %} + + + + {% endfor %} +
Email address
{{ emailaddress.complete_email_address }} + {% 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 %} +
+ +{% if emailaddress_list.paginator %} +{% include "pagination.html" with list=emailaddress_list %} +{% endif %} diff --git a/users/templates/users/index_emailaddress.html b/users/templates/users/index_emailaddress.html new file mode 100644 index 00000000..6976e168 --- /dev/null +++ b/users/templates/users/index_emailaddress.html @@ -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 %} +

Local email accounts

+ {% include "users/aff_emailaddress.html" with emailaddress_list=emailaddress_list %} +{% endblock %} + diff --git a/users/templates/users/profil.html b/users/templates/users/profil.html index 6e960fc2..1e1926cc 100644 --- a/users/templates/users/profil.html +++ b/users/templates/users/profil.html @@ -26,6 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} {% load logs_extra %} +{% load design %} {% block title %}Profil{% endblock %} @@ -406,8 +407,58 @@ with this program; if not, write to the Free Software Foundation, Inc., +
+
+

+ Email settings +

+
+
+
+ {% can_edit users %} + + Edit email settings + + {% acl_end %} +
+
+ {% if local_email_accounts_enabled %} +
+ + + + + + + + + + + +
Contact email address{{ users.email }}
Enable the local email account{{ users.local_email_enabled | tick }}Enable the local email redirection{{ users.local_email_redirect | tick }}
+
+ {% if users.local_email_enabled %} + {% can_create EMailAddress users.id %} + + Add an email address + + {% acl_end %} + {% if emailaddress_list %} + {% include "users/aff_emailaddress.html" with emailaddress_list=emailaddress_list %} + {% endif %} + {% endif %} + {% else %} +
+ + + + + +
Contact email address{{ users.email }}
+
+ {% endif %} +
+
+
-
-
-
{% endblock %} diff --git a/users/urls.py b/users/urls.py index 6bb374c3..f5114600 100644 --- a/users/urls.py +++ b/users/urls.py @@ -64,6 +64,18 @@ urlpatterns = [ url(r'^del_whitelist/(?P[0-9]+)$', views.del_whitelist, name='del-whitelist'), + url(r'^add_emailaddress/(?P[0-9]+)$', + views.add_emailaddress, + name='add-emailaddress'), + url(r'^edit_emailaddress/(?P[0-9]+)$', + views.edit_emailaddress, + name='edit-emailaddress'), + url(r'^del_emailaddress/(?P[0-9]+)$', + views.del_emailaddress, + name='del-emailaddress'), + url(r'^edit_email_settings/(?P[0-9]+)$', + views.edit_email_settings, + name='edit-email-settings'), url(r'^add_school/$', views.add_school, name='add-school'), url(r'^edit_school/(?P[0-9]+)$', views.edit_school, diff --git a/users/views.py b/users/views.py index a6444e33..a4f4ea83 100644 --- a/users/views.py +++ b/users/views.py @@ -81,10 +81,13 @@ from .models import ( Adherent, Club, ListShell, + EMailAddress, ) from .forms import ( BanForm, WhitelistForm, + EMailAddressForm, + EmailSettingsForm, DelSchoolForm, DelListRightForm, 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 @can_create(School) def add_school(request): @@ -914,7 +1012,11 @@ def profil(request, users, **_kwargs): 'white_list': whitelists, 'user_solde': user_solde, '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 + ) } )